A basic CLI to set up Tangled repos
JavaScript 100%

README.md

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, records 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 announces it on your PDS,
  • runs git init and sets origin to 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-project creates some-project) and announces 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 origin at the Tangled repo (replacing any previous origin),
  • if the repo has a package.json, points its repository field 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 issues and pulls, keeping their original timestamps. PR patches are fetched from GitHub and attached, targeting the PR's base branch.
  • Comments come along (conversation comments; inline review comments don't).
  • Labels are recreated as Tangled label definitions (with their colors), the repo is subscribed to them, and they're applied to the copied items.
  • 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: Tangled models assignment as the platform's shared DID-valued "assignee" label, so 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).

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 alice finds alice.tngl.sh). Without -u, the default user is used.
  • tangy user list shows the logged-in users (* marks the default); tangy user default <user> changes the default, and tangy user default shows 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.

How it works #

  • Auth uses ATProto OAuth via a loopback redirect (@atproto/oauth-client-node). Sessions (refresh tokens + DPoP 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 scoped to the knot, calls the knot's sh.tangled.repo.create XRPC endpoint, then writes the sh.tangled.repo record to your PDS.
  • Issues, pulls, comments, labels and reactions are plain records (sh.tangled.repo.issue, sh.tangled.repo.pull, sh.tangled.feed.comment, sh.tangled.label.*, sh.tangled.feed.reaction) written to the acting user's PDS; appviews pick them up from the firehose. Issues and pulls point at the repo via its own DID, comments reference their issue/pull by strongRef (uri + cid), and PR patches are uploaded as gzipped git format-patch blobs.
  • 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 by tangy 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 https://tangled.org

View them with tangy configuration get [-u <user>].