tangy #
A small CLI to set up and manage Tangled repos. (I thought this must exist but I haven't found it.)
It talks to Tangled the same way the website does, but from your terminal: it logs you in with your own ATProto account, creates the repo on a knot, declares it on your PDS, and wires up a local git repo pointing at it.
Install #
npm install
npm link # optional: puts `tangy` on your PATH
Requires Node ≥ 20.
Usage #
# 1. Log in (opens your browser to authorize; credentials go in the OS keychain)
tangy login alice.tngl.sh
# log in more accounts whenever you like — earlier logins are kept
tangy login work.example.com
# 2. Choose where each user's repos get created locally
tangy configuration set root ~/Code/tangled # default user
tangy configuration set root ~/Work/tangled -u work.example.com
# (optional) pick a different knot to host on; defaults to knot1.tangled.sh
tangy configuration set knot knot1.tangled.sh
# 3. Create a repo — makes it on Tangled, wires it up locally, and pushes
tangy repo create my-project
tangy repo create my-project -d "A short description"
tangy repo create my-project --no-push # skip the automatic push
tangy repo create my-project -u work.example.com # act as another account
# 4. Or move an existing local git repo into Tangled
tangy move ~/Code/some-project
# 5. Bring over a GitHub repo's open issues and PRs (comments, labels,
# assignees, reactions included)
tangy copy owner/repo my-project
tangy copy https://github.com/owner/repo --dry-run
# See who is logged in, and pick who commands act as by default
tangy user list
tangy user default work.example.com
# …later — logs out the default user, or the one named with -u
tangy logout
tangy logout -u alice.tngl.sh
tangy repo create <name>:
- creates
<root>/<name>/locally, - creates the repo on your knot and declares it on your PDS,
- runs
git initand setsoriginto the Tangled repo, - writes
README.md(# <name>) and a Node-friendly.gitignore, - makes an initial commit (if your git identity is configured),
- pushes that commit to
origin— this assumes your SSH key is already added to Tangled.
Pushing is on by default. Pass --no-push to skip it and push yourself later:
tangy repo create my-project --no-push
cd <root>/my-project
git push -u origin main
If the push fails (e.g. your SSH key isn't on Tangled yet), the repo has still
been created both locally and remotely — add your key and run
git push -u origin main from the repo directory.
tangy move <dir> adopts an existing local git repo into Tangled:
- creates a repo on your knot named after the directory (so
tangy move ~/Code/some-projectcreatessome-project) and declares it on your PDS, - moves the directory into
<root>/— it refuses to overwrite anything already there, and if the repo already sits at<root>/<name>it just skips the move, - points
originat the Tangled repo (replacing any previousorigin), - if the repo has a
package.json, points itsrepositoryfield at the Tangled repo and commits just that change (left uncommitted if git has no identity configured or the repo has no commits yet), - pushes the current branch (the knot's default branch is set to match it).
The directory must be the root of a git work tree and on a checked-out branch.
Other branches and tags are not pushed automatically. --no-push and
-d/--description work the same as for repo create. If creating the repo on
Tangled fails, the directory is moved back where it was.
Copying issues and PRs from GitHub #
tangy copy <github> [repo] copies a GitHub repo's open issues and pull
requests (closed ones stay behind) into a Tangled repo you own — by default
one with the same name, which must already exist (tangy repo create /
tangy move first). It takes -u <user> like everything else, and re-running
it is safe: tangy remembers what it already copied (in copies.json next to
its config) and only picks up new items and new comments.
What gets copied, and how:
- Issues and PRs become Tangled tickets, keeping their original
timestamps. A PR's commits are fetched from GitHub and pushed to a
github/pr-<number>branch on the Tangled repo (over SSH, like any push — your key needs to be on Tangled), then handed to the knot as a patch against the PR's base branch (or the repo's default branch, when Tangled doesn't have the base branch). The knot builds the diff itself, so a PR whose commits share no history with the target branch is skipped. - Comments come along (conversation comments; inline review comments don't).
- Labels are added to the repo's own label catalogue (with their colors) and applied to the copied items. Labels the repo already has are reused by name.
- Reactions are summarised in the item ("Reactions: 👍 3 · ❤️ 1"), and any reaction you made on GitHub is also recreated natively as you.
- Assignees are recreated natively too: every assignee whose GitHub account maps to an AT account is assigned for real. Assignees without a mapping fall back to an "Assignees: …" line in the item.
Since tangy can't impersonate other people, everything is created by the acting user. Anything authored by someone else gets a provenance line first:
Original by @foo = @foo.eurosky.social.
The = @handle part comes from a GitHub→AT mapping that tangy builds up as it
goes: the first time it sees a GitHub account it asks you for the matching AT
handle (press Enter to ignore that account), and remembers the answer either
way. Items you authored yourself (tangy asks for your GitHub username once)
are created as you, with no provenance line. Bots are never asked about.
--dry-run shows what would be copied — and which accounts would need
mapping — without writing anything. --no-issues / --no-pulls restrict what
gets copied. GitHub access is anonymous unless a GITHUB_TOKEN/GH_TOKEN
env var or a logged-in gh CLI is around (recommended for big repos: the
anonymous API allows only 60 requests/hour).
Copies made by older tangy versions, which wrote issues and comments to your PDS, still count: Tangled has moved those records into the repo's account, and tangy matches them up by title and timestamp before adding new comments.
Multiple users #
Several accounts can be logged in at once, and each keeps its own settings —
most importantly its own root, so different identities can keep their repos
in different places.
tangy login <handle>adds an account without touching existing logins. The first account to log in becomes the default user.- Every account-bound command (
logout,configuration set/get,repo create,move) takes-u <user>to pick the account: a handle, a DID, or any unique handle prefix (-u alicefindsalice.tngl.sh). Without-u, the default user is used. tangy user listshows the logged-in users (*marks the default);tangy user default <user>changes the default, andtangy user defaultshows it.tangy logout [-u <user>]logs out just that user. If the default user logs out and exactly one account remains, it becomes the default.
Console output always names the account being acted through, e.g.
Creating "my-project" on knot1.tangled.sh as alice.tngl.sh….
Config files written by older single-user versions of tangy are upgraded automatically.
Staying logged in #
Out of the box, tangy is an OAuth loopback client, which ATProto PDSes treat as a public client: the whole session is hard-capped at two weeks from login, no matter how often tokens are refreshed. If tangy keeps logging you out, that's why — and no cron job can fix it in this mode.
The fix is to run tangy as a confidential client, which authenticates to the PDS with a private key. Confidential sessions last up to two years, with refresh tokens that survive three months of inactivity — so they can be kept alive indefinitely by refreshing now and then. This needs a site you control, because the PDS reads the client's metadata from a URL:
# 1. Generate a signing key (stored in the keychain) and the two files to host
tangy client setup https://example.com/tangy/
# 2. Host the generated client-metadata.json and callback.html at that URL
# (they're written to ./tangy-client/, or wherever -o/--out points)
# 3. Log in again (existing sessions belong to the old client identity)
tangy login alice.tngl.sh
# 4. Keep sessions fresh forever: a launchd job that runs `tangy refresh`
# twice a day (and at login)
tangy keepalive on
tangy refresh [-u <user>] force-rotates the stored tokens by hand,
tangy keepalive status|off manages the background job (macOS launchd —
unlike cron it catches up on runs the machine slept through; on other
platforms keepalive on prints a crontab line to add yourself), and
tangy client show|clear inspects or abandons the confidential identity.
The callback page just bounces the browser back to the same local server
the loopback flow uses, so login feels identical.
How it works #
Tangled's org.tangled.* lexicons split a repository's data in two: your PDS
holds what you own (your profile, your claim to a repo), and the repo itself
is an ATProto account of its own — the repo DID — hosted by its knot, which
holds everything about the repo (issues, pull requests, comments, labels).
tangy follows that split, the same way the tangled.org web app does.
- Auth uses ATProto OAuth (
@atproto/oauth-client-node) — by default as a loopback client, optionally as a confidential one (see "Staying logged in"). Sessions (refresh tokens + DPoP key) and the confidential client's signing key are stored in the OS keychain via@napi-rs/keyring, never on disk. - Repo creation asks your PDS for a short-lived service-auth JWT addressed
to the knot and calls the knot's
org.tangled.repo.createRepo; the knot mints the repo DID and writes itsorg.tangled.repo.manifest. tangy then writes the other half of the ownership claim, anorg.tangled.repo.declarationkeyed by the repo's slug, to your PDS, and asks the appview's API to confirm it indexed it (unwinding both halves if it refuses). - Issues, pulls, comments, labels and reactions are records in the repo's
account on the knot (
org.tangled.track.ticket— a pull request is a ticket with a target branch —org.tangled.review.patch,org.tangled.feed.comment,org.tangled.feed.reaction,org.tangled.label.definition). tangy writes them through the knot'scom.atproto.repo.createRecord, with a service-auth JWT addressed to the repo DID; the knot checks you may contribute and stamps each record with who wrote it. Patches go throughorg.tangled.review.submitPatch, naming base and head commits that are already on the repo. After every knot write, tangy adds anorg.tangled.graph.attestationto your PDS so the record is claimed by you and not just by the knot. - Settings (root, knot, …) live per user in
~/.config/tangy/config.json, along with which user is the default and the GitHub→AT account mapping used bytangy copy.
Configuration keys #
Each key is stored per user (add -u <user> to set or read another account's
value):
| key | meaning | default |
|---|---|---|
root |
local directory this user's repos are created in | (unset) |
knot |
knot hostname to host this user's new repos on | knot1.tangled.sh |
appview |
Tangled appview base URL (used in links) | https://tangled.org |
api |
the appview's API, asked to confirm new repos got indexed | https://api.tangled.org |
View them with tangy configuration get [-u <user>].
- JavaScript 100%