A command-line interface for tangled, the decentralized Git collaboration platform built on the AT Protocol. eric-wien.tngl.io/tng-cli
cli git atproto tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>tng -- Tangled CLI</title> <style> :root { --bg: #0d1117; --bg-secondary: #161b22; --bg-tertiary: #21262d; --text: #c9d1d9; --text-muted: #8b949e; --text-bright: #f0f6fc; --accent: #58a6ff; --accent-secondary: #bc8cff; --border: #30363d; --green: #3fb950; --code-bg: #161b22; --sidebar-width: 260px; --content-max: 860px; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; display: flex; min-height: 100vh; }
/* Sidebar */ .sidebar { width: var(--sidebar-width); position: fixed; top: 0; left: 0; bottom: 0; background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 24px 0; z-index: 10; }
.sidebar-header { padding: 0 20px 20px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
.sidebar-header h1 { font-size: 20px; color: var(--text-bright); font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header p { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
.sidebar nav { padding: 0 12px; }
.sidebar nav a { display: block; padding: 6px 12px; color: var(--text-muted); text-decoration: none; font-size: 14px; border-radius: 6px; transition: background 0.15s, color 0.15s; }
.sidebar nav a:hover { background: var(--bg-tertiary); color: var(--text); }
.sidebar nav a.active { background: rgba(88, 166, 255, 0.1); color: var(--accent); }
.sidebar .nav-section { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-muted); padding: 16px 12px 6px; }
/* Main content */ .main { margin-left: var(--sidebar-width); flex: 1; padding: 40px 48px; max-width: calc(var(--sidebar-width) + var(--content-max) + 96px); }
h2 { font-size: 28px; font-weight: 700; color: var(--text-bright); margin: 48px 0 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
h2:first-child { margin-top: 0; }
h3 { font-size: 20px; font-weight: 600; color: var(--text-bright); margin: 32px 0 12px; }
h4 { font-size: 16px; font-weight: 600; color: var(--accent-secondary); margin: 24px 0 8px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; }
p { margin: 0 0 16px; }
a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }
code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 13px; background: var(--code-bg); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border); }
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px 20px; overflow-x: auto; margin: 0 0 16px; font-size: 13px; line-height: 1.5; }
pre code { background: none; border: none; padding: 0; }
table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 14px; }
th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--border); color: var(--text-bright); font-weight: 600; }
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
td code { font-size: 12px; }
.hero { margin-bottom: 40px; }
.hero h2 { font-size: 36px; border: none; margin: 0 0 12px; }
.hero p { font-size: 18px; color: var(--text-muted); }
.release { display: inline-block; margin-bottom: 12px; padding: 3px 9px; border: 1px solid var(--green); border-radius: 999px; color: var(--green); font: 600 12px "SFMono-Regular", Consolas, monospace; }
.install-box { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 20px 24px; margin: 24px 0; }
.install-box pre { background: var(--bg); margin: 8px 0 0; }
.flag-table td:first-child { white-space: nowrap; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 13px; color: var(--accent); }
.cmd-group { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 16px 20px; margin: 16px 0; }
.cmd-group pre { background: var(--bg); margin: 8px 0 0; }
@media (max-width: 768px) { .sidebar { display: none; } .main { margin-left: 0; padding: 24px; } } </style></head><body> <aside class="sidebar"> <div class="sidebar-header"> <h1>tng</h1> <p>Tangled CLI</p> </div> <nav> <a href="#getting-started" class="active">Getting Started</a> <a href="#installation">Installation</a> <a href="#authentication">Authentication</a>
<div class="nav-section">Commands</div> <a href="#cmd-auth">auth</a> <a href="#cmd-repo">repo</a> <a href="#cmd-pr">pr</a> <a href="#cmd-issue">issue</a> <a href="#cmd-browse">browse</a> <a href="#cmd-utils">version / completion</a>
<div class="nav-section">Configuration</div> <a href="#config">Config File</a> <a href="#env-vars">Environment Variables</a> <a href="#repo-context">Repository Context</a> <a href="#shell-completions">Shell Completions</a>
<div class="nav-section">Reference</div> <a href="#architecture">Architecture</a> <a href="#project-structure">Project Structure</a> </nav> </aside>
<main class="main"> <!-- Hero --> <section class="hero" id="getting-started"> <span class="release">v1.0.1</span> <h2>tng -- Tangled CLI</h2> <p>Work with <a href="https://tangled.org">Tangled</a>, the decentralized Git collaboration platform, from your terminal.</p> </section>
<div class="install-box"> <strong>Quick start</strong> <pre><code>tng auth login --handle example.bsky.socialtng repo clone example.bsky.social/my-projectcd my-projecttng pr listtng issue create --title "Bug report"</code></pre> </div>
<!-- Installation --> <h2 id="installation">Installation</h2>
<h3>From source (Go)</h3> <p>Requires Go 1.26.5 or newer.</p> <pre><code>go install tangled.org/eric.wien/tng-cli/cmd/tng@latest</code></pre>
<h3>Build manually</h3> <pre><code>git clone git@knot.eric.wien:eric.wien/tng-clicd tng-climake build./bin/tng version</code></pre>
<h3>Makefile targets</h3> <table> <tr><th>Target</th><th>Description</th></tr> <tr><td><code>make build</code></td><td>Build the binary to <code>./bin/tng</code></td></tr> <tr><td><code>make test</code></td><td>Run all tests</td></tr> <tr><td><code>make lint</code></td><td>Run golangci-lint</td></tr> <tr><td><code>make install</code></td><td>Install to <code>/usr/local/bin</code></td></tr> <tr><td><code>make clean</code></td><td>Remove build artifacts</td></tr> </table>
<!-- Authentication --> <h2 id="authentication">Authentication</h2> <p><code>tng</code> authenticates using AT Protocol OAuth 2.0 with PKCE and DPoP. This is the same protocol used by Bluesky. Your handle can be from any AT Protocol provider.</p>
<pre><code># Opens browser for authorizationtng auth login --handle example.bsky.social
# Paste the localhost callback URL in a headless environmenttng auth login --manual --handle example.bsky.social
# Check current sessiontng auth status
# Sign out (revokes tokens)tng auth logout</code></pre>
<p>The login flow:</p> <ol style="margin: 0 0 16px 24px;"> <li>Resolves your handle to a DID and PDS endpoint</li> <li>Sends a Pushed Authorization Request (PAR) with PKCE challenge</li> <li>Opens your browser, or displays an authorization URL in a remote session</li> <li>Handles the OAuth callback on a local HTTP server or from a pasted callback URL</li> <li>Exchanges the code for DPoP-bound access tokens</li> <li>Stores the session locally at <code>~/.config/tng/</code></li> </ol> <p>Over SSH, manual browser authentication is selected automatically. Open the displayed URL on your local machine, authorize the CLI, then copy the full localhost callback URL from the browser's address bar back into the terminal. Use <code>--manual</code> to select this flow in other headless environments.</p> <p>Tokens are automatically refreshed. The public client flow gives sessions a lifetime of approximately two weeks.</p>
<!-- Commands: auth --> <h2 id="cmd-auth">tng auth</h2> <p>Manage authentication with your Tangled / AT Protocol account.</p>
<h4>tng auth login</h4> <p>Authenticate via browser-based OAuth flow.</p> <div class="cmd-group"><pre><code>tng auth logintng auth login --handle example.bsky.socialtng auth login --manual --handle example.bsky.socialtng auth login --handle did:plc:abc123</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>--handle</td><td>AT Protocol handle or DID. If omitted, prompted interactively.</td></tr> <tr><td>--manual</td><td>Complete login by pasting the localhost callback URL. Automatically enabled over SSH.</td></tr> </table>
<h4>tng auth status</h4> <p>Show all authenticated accounts. The active account is marked with <code>*</code>.</p>
<h4>tng auth logout</h4> <p>Revoke tokens and remove the session.</p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>--did</td><td>DID of account to log out. Defaults to the active account.</td></tr> </table>
<!-- Commands: repo --> <h2 id="cmd-repo">tng repo</h2> <p>Create, clone, view, and list Tangled repositories. Alias: <code>tng r</code></p>
<h4>tng repo create <name></h4> <p>Create a repository on a Tangled knot. New repositories publish their knot-issued repository DID, so they are indexed by the Tangled appview. Pass <code>--knot</code> or configure <code>default_knot</code>.</p> <div class="cmd-group"><pre><code>tng repo create my-project --knot knot1.tangled.shtng repo create my-project -d "Project description" --knot knot1.tangled.shtng repo create my-project --knot https://knot.eric.wientng repo create my-project --default-branch main --clone --knot knot1.tangled.sh</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-d, --description</td><td>Repository description</td></tr> <tr><td>--default-branch</td><td>Default branch name (default: <code>main</code>)</td></tr> <tr><td>-k, --knot</td><td>Knot URL to create the repo on (e.g., <code>https://knot.eric.wien</code>). Falls back to <code>default_knot</code> in config.</td></tr> <tr><td>-c, --clone</td><td>Clone the repository after creation</td></tr> </table>
<h4>tng repo clone <owner/repo> [directory]</h4> <p>Clone a Tangled repository via SSH.</p> <div class="cmd-group"><pre><code>tng repo clone example.bsky.social/my-projecttng repo clone example.bsky.social/my-project ./my-dir</code></pre></div>
<h4>tng repo view [owner/repo]</h4> <p>Display repository details: file tree, last commit, and README. Uses the current repo if no argument is given.</p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-w, --web</td><td>Open in browser instead of printing</td></tr> </table>
<h4>tng repo list [owner]</h4> <p>List repositories for a user. Defaults to the authenticated user. Alias: <code>tng repo ls</code></p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-L, --limit</td><td>Maximum number of results (default: 30)</td></tr> </table>
<!-- Commands: pr --> <h2 id="cmd-pr">tng pr</h2> <p>Create, view, merge, and manage pull requests.</p> <p>Tangled uses a round-based review model. Each push to a PR creates a new immutable round. Reviewers can view interdiffs between rounds.</p>
<h4>tng pr create</h4> <div class="cmd-group"><pre><code>tng pr create --title "Add feature" --body "Description"tng pr create -t "Fix bug" --base main --head fix-branch</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-t, --title</td><td>PR title (required)</td></tr> <tr><td>-B, --body</td><td>PR description</td></tr> <tr><td>-b, --base</td><td>Target branch (default: <code>main</code>)</td></tr> <tr><td>-H, --head</td><td>Source branch (default: current branch)</td></tr> </table>
<h4>tng pr list</h4> <div class="cmd-group"><pre><code>tng pr listtng pr list --repo example.bsky.social/my-projecttng pr list --web</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-R, --repo</td><td>Filter by repository (owner/name)</td></tr> <tr><td>-w, --web</td><td>Print the tangled.sh pull requests URL</td></tr> </table>
<h4>tng pr view <rkey></h4> <p>Display PR details including title, branches, and body.</p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>--author</td><td>DID of the PR author (defaults to you)</td></tr> <tr><td>-w, --web</td><td>Open in browser</td></tr> </table>
<h4>tng pr diff <rkey></h4> <p>Print the raw diff of a pull request to stdout.</p>
<h4>tng pr merge <rkey></h4> <p>Merge a pull request. The PR must be in <code>open</code> status.</p>
<h4>tng pr close <rkey></h4> <p>Close a pull request without merging.</p>
<h4>tng pr comment <rkey></h4> <p>Add a comment to a pull request.</p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-b, --body</td><td>Comment text (required)</td></tr> </table>
<!-- Commands: issue --> <h2 id="cmd-issue">tng issue</h2> <p>Create, view, and manage issues. Alias: <code>tng i</code></p>
<h4>tng issue create</h4> <div class="cmd-group"><pre><code>tng issue create --title "Bug: login fails" --body "Steps to reproduce..."tng issue create -t "Feature request"</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-t, --title</td><td>Issue title (required)</td></tr> <tr><td>-b, --body</td><td>Issue body</td></tr> <tr><td>-R, --repo</td><td>Repository override</td></tr> </table>
<h4>tng issue list</h4> <div class="cmd-group"><pre><code>tng issue listtng issue list --repo example.bsky.social/my-projecttng issue list --web</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-R, --repo</td><td>Filter by repository (owner/name)</td></tr> <tr><td>-w, --web</td><td>Print the tangled.sh issues URL</td></tr> </table>
<h4>tng issue view <rkey></h4> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>--author</td><td>DID of the issue author (defaults to you)</td></tr> <tr><td>-w, --web</td><td>Open in browser</td></tr> </table>
<h4>tng issue close <rkey></h4> <p>Close an issue.</p>
<h4>tng issue comment <rkey></h4> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-b, --body</td><td>Comment text (required)</td></tr> </table>
<!-- Commands: browse --> <h2 id="cmd-browse">tng browse</h2> <p>Open the current repository (or a specific file) in your web browser.</p> <div class="cmd-group"><pre><code>tng browse # Open repo roottng browse src/main.go # Open specific filetng browse -n # Print URL, don't opentng browse -R example.bsky.social/proj # Specify repo</code></pre></div> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>-R, --repo</td><td>Repository override</td></tr> <tr><td>-n, --no-browser</td><td>Print the URL instead of opening it</td></tr> </table>
<!-- Commands: utils --> <h2 id="cmd-utils">Utilities</h2>
<h4>tng upgrade</h4> <p>Upgrade to the latest released version via the Go module proxy and <code>go install</code> (requires a Go toolchain). Alias: <code>tng update</code></p> <table class="flag-table"> <tr><th>Flag</th><th>Description</th></tr> <tr><td>--check</td><td>Only check for a newer version, don't install</td></tr> </table>
<h4>tng version</h4> <p>Print the version, build date, and commit hash.</p>
<h4>tng completion <shell></h4> <p>Generate shell completion scripts. Supported: <code>bash</code>, <code>zsh</code>, <code>fish</code>, <code>powershell</code>.</p>
<!-- Config --> <h2 id="config">Configuration</h2> <p>Config is stored at <code>~/.config/tng/config.yml</code> (respects <code>XDG_CONFIG_HOME</code> and <code>TNG_CONFIG_DIR</code>).</p> <pre><code>default_knot: "" # Default knot for repo creationapi_host: "" # Override API host (default: https://tangled.sh)</code></pre>
<h2 id="env-vars">Environment Variables</h2> <table> <tr><th>Variable</th><th>Description</th></tr> <tr><td><code>TNG_HOST</code></td><td>Override API host (default: <code>https://tangled.sh</code>)</td></tr> <tr><td><code>TNG_CONFIG_DIR</code></td><td>Override config directory</td></tr> <tr><td><code>TNG_REPO</code></td><td>Override repo context (<code>owner/name</code>)</td></tr> <tr><td><code>TNG_FORCE_TTY</code></td><td>Force color output in non-TTY contexts</td></tr> <tr><td><code>NO_COLOR</code></td><td>Disable all color output</td></tr> </table>
<h2 id="repo-context">Repository Context</h2> <p>Inside a cloned Tangled repository, <code>tng</code> automatically detects the repo from git remotes. Resolution priority:</p> <ol style="margin: 0 0 16px 24px;"> <li><code>TNG_REPO</code> environment variable</li> <li><code>git config tng.default-repo</code></li> <li>Git remotes matching <code>tangled.sh</code> (priority: <code>upstream</code> > <code>tangled</code> > <code>origin</code>)</li> </ol> <p>Supported remote formats:</p> <pre><code>git@tangled.sh:owner/repohttps://tangled.sh/owner/repo</code></pre>
<h2 id="shell-completions">Shell Completions</h2> <pre><code># Bashtng completion bash > /etc/bash_completion.d/tng
# Zsh (add to fpath)tng completion zsh > "${fpath[1]}/_tng"
# Fishtng completion fish > ~/.config/fish/completions/tng.fish</code></pre>
<!-- Architecture --> <h2 id="architecture">Architecture</h2> <p><code>tng</code> follows the same patterns as the <a href="https://github.com/cli/cli">GitHub CLI</a>:</p> <ul style="margin: 0 0 16px 24px;"> <li>Go + <a href="https://github.com/spf13/cobra">cobra</a> for command parsing</li> <li>Factory pattern with lazy dependency initialization</li> <li>AT Protocol OAuth via <a href="https://github.com/bluesky-social/indigo">indigo</a> (PKCE + DPoP)</li> <li>XRPC API client for <code>sh.tangled.*</code> namespace endpoints</li> <li>Automatic git remote detection for repository context</li> </ul>
<h3>Key differences from GitHub CLI</h3> <table> <tr><th>Aspect</th><th>gh (GitHub)</th><th>tng (Tangled)</th></tr> <tr><td>Auth</td><td>GitHub OAuth device flow</td><td>AT Protocol OAuth 2.0 with PKCE + DPoP</td></tr> <tr><td>Identity</td><td>GitHub username</td><td>AT Protocol DID + handle</td></tr> <tr><td>API</td><td>REST + GraphQL</td><td>XRPC (AT Protocol RPC)</td></tr> <tr><td>PRs</td><td>Mutable, linear</td><td>Round-based, immutable per round, interdiffs</td></tr> <tr><td>Data</td><td>Centralized on GitHub</td><td>AT Protocol records on user's PDS</td></tr> <tr><td>Hosting</td><td>Monolithic service</td><td>Knots (federated git servers)</td></tr> </table>
<h2 id="project-structure">Project Structure</h2> <pre><code>cmd/tng/ Entry pointinternal/ tngcmd/ Root command assembly, exit codes authflow/ AT Protocol OAuth flow config/ Config files and session storage build/ Version info (set via ldflags)pkg/ cmd/ Command implementations auth/ login, logout, status repo/ create, clone, view, list pr/ create, list, view, diff, merge, close, comment issue/ create, list, view, close, comment browse/ Open in browser version/ Print version completion/ Shell completions cmdutil/ Factory, errors, arg helpers iostreams/ TTY detection, color, terminal widthapi/ XRPC API client and domain methodscontext/ Git remote resolutiongit/ Git CLI wrapper, remote parsing</code></pre>
<p style="margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border); color: var(--text-muted); font-size: 13px;"> tng-cli is open source. Source code at <a href="https://tangled.org/eric.wien/tng-cli">tangled.org/eric.wien/tng-cli</a>. </p> </main>
<script> // Highlight active nav link on scroll const sections = document.querySelectorAll('h2[id], section[id]'); const navLinks = document.querySelectorAll('.sidebar nav a');
function updateActive() { let current = ''; sections.forEach(section => { if (window.scrollY >= section.offsetTop - 80) { current = section.getAttribute('id'); } }); navLinks.forEach(link => { link.classList.toggle('active', link.getAttribute('href') === '#' + current); }); }
window.addEventListener('scroll', updateActive); updateActive(); </script></body></html>