# tang `tang` is an independent, automation-first CLI for [Tangled](https://tangled.org). It gives humans and coding agents one command surface for repository context, issues, pull requests, and Spindle CI without scraping the web UI. Source of truth: ## Install Published releases support three paths: ```bash # npm (Node 22+) npm install --global tangled-cli # or download a checksummed tang-{platform}-{arch} binary from the release page # https://github.com/just-cameron/tangled-cli/releases # source checkout git clone git@tangled.org:cameron.stream/tangled-cli cd tangled-cli bun install --frozen-lockfile bun run build bun link ``` Tagged releases build Linux, macOS, and Windows binaries, attach SHA-256 files, and publish the npm package with provenance. Those artifacts are suitable inputs for a Homebrew tap; the tap formula should pin the release URL and checksum rather than building the repository at install time. The Tangled repository is authoritative. GitHub is a release mirror used to run the cross-platform artifact workflow; tag the same revision in both places. Maintainers must configure the mirror's `NPM_TOKEN` Actions secret before cutting a release. The npm `repository` metadata points at that public mirror because npm provenance requires it to exactly match the GitHub repository that performs the publish. Verify the installation: ```bash tang --version tang doctor ``` ## Authenticate OAuth is the default. The browser callback binds to loopback only, and session secrets are stored in the OS keychain. OAuth login asks only for what `tang` does with your account, as AT Protocol [permission scopes](https://atproto.com/specs/permission), not for the whole account (`transition:generic`): - `repo:` the records it writes to your PDS: `org.tangled.graph.attestation` (create), legacy `sh.tangled.repo.issue` (create, update, delete), `sh.tangled.repo.issue.state` and `sh.tangled.label.op` (create), and `sh.tangled.repo.pull` (create, delete); - `rpc:…?aud=*` the methods it asks your PDS for service-auth tokens to call: a repository knot's `com.atproto.repo.createRecord`, `putRecord`, `deleteRecord` and `org.tangled.review.submitPatch` (the knot is addressed by each repository's DID, so any audience), and a spindle's `sh.tangled.ci.triggerPipeline` and `cancelPipeline`; - `blob:application/gzip` for a pull request's patch. The list lives in `src/lib/permissions.ts`, which is also the only code that writes to your PDS or asks for service auth. A session signed in before this keeps its old, whole-account grant until you sign in again; `tang auth status --diagnostic` shows the scope a session holds. App-password login has no scopes. ```bash tang auth login tang auth status tang auth status --diagnostic # expiry/scope/audience; token value stays hidden tang auth logout # confirms before deleting the session ``` On macOS, `tang` stores OAuth credentials in the login Keychain and accesses them through Apple's stable `/usr/bin/security` helper. The normal login flow does not ask you to enter any Keychain item data: provide your handle, approve the browser login, and `tang` stores the resulting session automatically. If a legacy item needs authorization, macOS may show its standard system dialog; enter a Mac login password only in that recognizable dialog. A terminal prompt that says `password data for new item` is never an instruction to enter an account or Mac password. Legacy PDS app passwords remain available: ```bash tang auth login --app-password ``` `auth status --show-token` exists for exceptional debugging. It refuses JSON and non-interactive output, then presents a warning and requires confirmation. Tokens grant account access and should not be pasted into issues, logs, or agent prompts. ## Select a repository Repo-scoped commands use the first available selector in this order: 1. global `--repo ` 2. `TANG_REPO` 3. configured remote (`TANGLED_REMOTE`, `.tangledrc`, or user config) 4. a Tangled Git remote in the current checkout Accepted selectors include: ```text owner.handle/repository did:plc:owner/repository did:plc:repository at://did:plc:owner/sh.tangled.repo/ https://tangled.org/owner.handle/repository ``` Examples: ```bash tang --repo cameron.stream/tangled-cli context TANG_REPO=did:plc:... tang issue list tang context --json ``` Resolution follows the repository record to its stable repository DID and discovers its knot and Spindle. Public reads go to the record owner's authoritative PDS when a signed-in PDS cannot serve a foreign record. ## Global automation flags ```text --repo operate outside a Git checkout --json machine-readable output --fields filter comma-separated fields when using global --json --no-input never prompt; fail with a remediation instead -y, --yes grant confirmation to commands that support it ``` Environment equivalents: ```text TANG_REPO TANG_JSON=1 TANG_NO_INPUT=1 TANG_YES=1 ``` Precedence is explicit command flag → environment → discovered/configured context. `--no-input` never implies consent: a destructive action also needs `--yes`. ## Issues and pull requests ```bash tang issue list --state open --label needs-triage tang issue create "Context fails across PDSes" --body-file issue.md tang issue view 12 # with its labels, and its comments by id tang issue edit 12 --title "Context fails across PDSes and knots" tang issue comment 12 --body "Reproduced on knot1." tang issue comment 12 --edit 3myysu4pkg5hk --body "Reproduced on knot1 and knot2." tang issue close 12 tang issue reopen 12 tang issue delete 12 --yes tang label defs # the repository's label catalogue tang label create needs-triage --color "#F97316" tang label add issue 12 needs-triage tang label remove issue 12 needs-triage --yes tang label list issue 12 tang pr create --base main --head feature --title "Add feature" --body-file pr.md tang -R owner/upstream pr create --head feature --title "Add feature" # from a fork tang pr list --all tang pr view tang pr delete --yes ``` List commands support `--cursor`, `--limit`, and `--all`. Bodies accept flags, files, or stdin (`--body-file -`). Pull creation checks whether the head is behind its base, builds and compresses the Git patch, uploads it as an AT Protocol blob, then creates the pull record. ### Where issues live A repository with its own DID keeps its issues in that DID's account, hosted by its knot: the DID document (`https://plc.directory/`) names the knot as `#atproto_pds`, beside a `#tangled_knot` service. Issues are `org.tangled.track.ticket` records there, comments are `org.tangled.feed.comment` records, and the label catalogue is `org.tangled.label.definition` records; a ticket's labels are its `tangledLabels`, keyed by those definitions' record keys. `tang` reads that store anonymously (`com.atproto.repo.listRecords` and `getRecord` on the knot). To write, it asks the signed-in PDS for a service-auth token whose audience is the repo DID and whose method is the write (`com.atproto.server.getServiceAuth`), and POSTs `com.atproto.repo.createRecord`, `putRecord` or `deleteRecord` to the knot with it. The knot checks permission, stamps the record with `x-tngl-editor` and answers with `x-tngl-change`, the change it made. Like tangled.org's own client, `tang` then attests every create and edit with an `org.tangled.graph.attestation` record in the author's PDS naming that change. Edits read the stored version first and swap on its CID, so a newer write by someone else makes the edit fail rather than vanish. The repository is found through the owner's `org.tangled.repo.declaration/` record, falling back to a `sh.tangled.repo` record. ### Issue numbers A ticket's record key **is** its issue number, minted by the knot, so `12`, `#12`, `https://tangled.org///issues/12` and `at:///org.tangled.track.ticket/12` all name the same issue, durably. JSON marks such numbers `"numberKind": "record-key"`. ### Legacy records Repositories without a knot store keep the old path: `sh.tangled.repo.issue` records in each author's PDS, `sh.tangled.repo.issue.state` records for state, and `sh.tangled.label.op` records for labels. Those records store no number; `tang` computes a deterministic 1-based order after collecting them all, so there: - use an AT-URI or exact rkey in scripts; - treat `#12` as a human-facing fallback, not a durable automation ID; - JSON includes `id`, `uri`, and `numberKind: "computed"` so callers can make that distinction; - numeric resolution emits a warning. In a repository with a knot store, a legacy record is still read by its AT-URI or rkey (`tang issue view at://…/sh.tangled.repo.issue/`), and `tang issue list --legacy` lists them. `tang issue delete at:///sh.tangled.repo.issue/ --yes` deletes one of your own legacy records from your PDS; it works outside a checkout and refuses anyone else's. In a repository with a knot store, `tang pr create` opens a pull request as tangled.org keeps them now: an `org.tangled.track.ticket` with a `targetBranch`, numbered by the knot, then its `org.tangled.review.patch`, which only the knot writes, through `org.tangled.review.submitPatch`, naming the ticket, the base and head commits, and the source branch. The knot fetches the head from what was pushed, so the head branch must be on the remote at the commit it is locally, or nothing is written; if the knot refuses the patch, the ticket is taken back. From a fork's clone, `tang -R pr create` opens it in the upstream repository instead: the head branch must be on the fork's own remote, the upstream's base branch is fetched (into `FETCH_HEAD` only) and the head compared with that, and the patch names the fork as the repository the knot fetches the head from; an upstream without a knot store is refused. `pr list` and `view` show such a pull request's branch as `:`. A session signed in before `tang` asked for `submitPatch` must sign in again. Elsewhere it writes a legacy `sh.tangled.repo.pull` record with the patch as a blob. There, `pr list`, `view` and `delete` work on those tickets: a pull request is addressed by its number, `completed` shows as merged, `view` gives its head commit, its rounds (one for each patch) and its comments, and `delete` removes the ticket alone, since its patches are the knot's. ## Pipelines and CI Pipeline commands use the current `sh.tangled.ci.*` Spindle XRPC namespace directly: ```bash tang pipeline list tang ci list --all # `ci` aliases `pipeline` tang pipeline view tang pipeline logs tang pipeline logs -w check tang run --sha "$(git rev-parse HEAD)" # top-level dispatch shortcut tang pipeline run -w check -i mode=full tang pipeline retry --yes tang pipeline cancel --yes tang pipeline cancel -w site --yes ``` `list` and `view` are public Spindle queries. `run`, `retry`, and `cancel` mint method-bound service-auth tokens through the signed-in PDS, then call the repository's configured Spindle. `logs` opens the Spindle subscription and decodes AT Protocol CBOR event frames; it does not scrape or proxy through the Tangled website. ## JSON contracts Examples: ```bash tang issue list --json tang issue list --json id,title,state tang pipeline list --json id,status,commit tang pipeline logs --json ``` Paginated list output is an envelope: ```json { "items": [], "count": 0, "nextCursor": "optional opaque cursor", "addressing": "issues and pulls also carry the identifier warning" } ``` Field selection filters objects inside `items`, not envelope metadata. Streaming logs are newline-delimited JSON: one complete event object per stdout line. In JSON mode, normal status prose stays off stdout; failures use stderr and a nonzero exit status. ## Diagnostics ```bash tang doctor tang doctor --json ``` The doctor checks the runtime, config directory, DID resolution, live authentication, repository selection/resolution, Spindle discovery/query access, and Git. It prints concrete remediation for warnings and failures and exits nonzero for failed checks. ## Command map ```text tang auth login|status|logout tang ssh-key add|verify tang config list|get|set|unset tang context tang doctor tang issue create|list|view|edit|close|reopen|comment|delete tang pr create|list|view|delete tang label defs|create|list|add|remove tang pipeline|ci list|view|logs|run|retry|cancel tang run ``` Use `tang --help` for exact flags. ## Status - **Issues, comments and issue labels** work against tangled.org's current `org.tangled.*` records in the repository's knot store, and show up on the web: list (state, labels, `--state`, `--label`), view (labels, comments), create, edit, close, reopen, comment, edit one's own comment, delete, and label defs/create/add/remove/list. - **Legacy issues** (`sh.tangled.repo.issue`) are still read, listed with `--legacy`, and your own can be deleted. - **Not yet:** deleting comments, replies, reactions, assignees, milestones; editing, closing, reopening, commenting on or merging pull requests kept as tickets, and new rounds for them. ## Development Prerequisites are Node 22+ and Bun. ```bash bun install --frozen-lockfile bun run dev -- --help bun run check # typecheck + Biome + Vitest bun run build bun run build:binary ``` Architecture: - `src/commands/` — Commander adapters and terminal contracts - `src/lib/` — repository resolution, addressing, PDS access, API, and CI logic - `src/lexicon/` — generated record-oriented Tangled types - `lexicons/` — vendored Tangled schemas, including current CI XRPC schemas - `tests/` — unit and command tests Update vendored schemas with `bun run update-lexicons`. CI lexicons are intentionally not fed to the record code generator because they describe queries, procedures, and subscriptions; `src/lib/ci-api.ts` implements that transport boundary directly.