the tangled network observatory ◈ every knot, its health, its version, in your terminal
tangled cli tui knot monitoring observability atproto federation
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: shell completions via bomb.sh/tab

+115
+6
README.md
··· 17 17 pnpm dev list 18 18 ``` 19 19 20 + shell completions (zsh/bash/fish/powershell) — `fid check <tab>` completes real knot hostnames: 21 + 22 + ```sh 23 + echo 'source <(fid complete zsh)' >> ~/.zshrc 24 + ``` 25 + 20 26 ## commands 21 27 22 28 ```
+1
package.json
··· 23 23 }, 24 24 "dependencies": { 25 25 "@bomb.sh/args": "^0.3.1", 26 + "@bomb.sh/tab": "^0.0.21", 26 27 "@bomb.sh/tty": "^0.8.0", 27 28 "@clack/prompts": "^0.11.0", 28 29 "zod": "^4.0.0"
+22
pnpm-lock.yaml
··· 11 11 '@bomb.sh/args': 12 12 specifier: ^0.3.1 13 13 version: 0.3.1 14 + '@bomb.sh/tab': 15 + specifier: ^0.0.21 16 + version: 0.0.21(cac@6.7.14) 14 17 '@bomb.sh/tty': 15 18 specifier: ^0.8.0 16 19 version: 0.8.0 ··· 98 101 99 102 '@bomb.sh/args@0.3.1': 100 103 resolution: {integrity: sha512-CwxKrfgcorUPP6KfYD59aRdBYWBTsfsxT+GmoLVnKo5Tmyoqbpo0UNcjngRMyU+6tiPbd18RuIYxhgAn44wU/Q==} 104 + 105 + '@bomb.sh/tab@0.0.21': 106 + resolution: {integrity: sha512-L9tqUOFS9GFJu+EWaoN6EvNAFKHtROJ3VmKP0AKGN4AnR/JXZbNPC9TXJhrNdTJISiUN7ZU0uJCq64fvyUVSvg==} 107 + hasBin: true 108 + peerDependencies: 109 + cac: ^6.7.14 110 + citty: ^0.1.6 || ^0.2.0 111 + commander: ^13.1.0 || ^14.0.0 || ^15.0.0 112 + peerDependenciesMeta: 113 + cac: 114 + optional: true 115 + citty: 116 + optional: true 117 + commander: 118 + optional: true 101 119 102 120 '@bomb.sh/tty@0.8.0': 103 121 resolution: {integrity: sha512-tEE0mtqnEp2uJt4kQS5AjdghCD5KN4BobqHOlfhaiNyrigxFBKk61XkDy//4D7V/AejrTvubh44B4NV35lsnxw==} ··· 718 736 optional: true 719 737 720 738 '@bomb.sh/args@0.3.1': {} 739 + 740 + '@bomb.sh/tab@0.0.21(cac@6.7.14)': 741 + optionalDependencies: 742 + cac: 6.7.14 721 743 722 744 '@bomb.sh/tty@0.8.0': {} 723 745
+8
src/cli.ts
··· 4 4 import { parse } from "@bomb.sh/args"; 5 5 import { animateBanner } from "./banner.js"; 6 6 7 + // completion calls answer before anything else — no banner, no arg parsing 8 + if (process.argv[2] === "complete") { 9 + const { runComplete } = await import("./completions.js"); 10 + runComplete(process.argv.slice(3)); 11 + process.exit(0); 12 + } 13 + 7 14 const args = parse(process.argv.slice(2), { 8 15 // note: args parses `--no-repos` as minimist-style negation → `repos: false` 9 16 boolean: [ ··· 41 48 fid spindles the ci runner fleet 42 49 fid find <query> search repos across the network 43 50 fid watch live network board (q to quit) 51 + fid complete <shell> print completion script (zsh/bash/fish/powershell) 44 52 fid --version print the version 45 53 `; 46 54
+78
src/completions.ts
··· 1 + import { readFileSync, statSync } from "node:fs"; 2 + import { join } from "node:path"; 3 + import t, { type ArgumentHandler } from "@bomb.sh/tab"; 4 + import { cacheDir } from "./util.js"; 5 + 6 + // completions must answer instantly, so hosts come from the newest cached 7 + // snapshot (full or lite) — never the network. cold cache → no suggestions. 8 + const hostHandler: ArgumentHandler = (complete) => { 9 + const snapshots = ["network3", "network3-lite"] 10 + .map((key) => join(cacheDir, `${key}.json`)) 11 + .map((file) => { 12 + try { 13 + return { file, mtime: statSync(file).mtimeMs }; 14 + } catch { 15 + return null; 16 + } 17 + }) 18 + .filter((s) => s !== null) 19 + .sort((a, b) => b.mtime - a.mtime); 20 + if (!snapshots[0]) return; 21 + try { 22 + const network = JSON.parse(readFileSync(snapshots[0].file, "utf8")) as { 23 + knots: { 24 + host: string; 25 + local: boolean; 26 + repoCount: number; 27 + version: string; 28 + }[]; 29 + }; 30 + for (const k of network.knots) { 31 + if (!k.local) complete(k.host, `${k.repoCount} repos · ${k.version}`); 32 + } 33 + } catch { 34 + // unreadable snapshot — no suggestions 35 + } 36 + }; 37 + 38 + const list = t.command("list", "every knot on the network"); 39 + list.option("json", "machine-readable output"); 40 + list.option("all", "include localhost/dev junk knots"); 41 + list.option("fresh", "skip the 5-minute cache"); 42 + list.option("no-repos", "skip the slow repo sweep (no counts, knots-only)"); 43 + list.option("down", "only unreachable knots"); 44 + list.option("stale", "only pre-v1.15 knots (the silent-data-loss cohort)"); 45 + list.option("managed", "only tangled-hosted knots"); 46 + list.option("self-hosted", "only self-hosted knots"); 47 + 48 + const check = t.command("check", "health-check one knot"); 49 + check.option("mine", "check every knot you own instead"); 50 + check.option("as", "who you are (handle or did)"); 51 + check.option("json", "machine-readable output"); 52 + check.argument("host", hostHandler); 53 + 54 + t.command("stats", "network aggregates").option( 55 + "json", 56 + "machine-readable output", 57 + ); 58 + 59 + const repos = t.command("repos", "what repos live on a knot"); 60 + repos.option("json", "machine-readable output"); 61 + repos.argument("host", hostHandler); 62 + 63 + t.command("spindles", "the ci runner fleet").option( 64 + "json", 65 + "machine-readable output", 66 + ); 67 + 68 + t.command("find", "search repos across the network").option( 69 + "json", 70 + "machine-readable output", 71 + ); 72 + 73 + t.command("watch", "live network board"); 74 + 75 + export function runComplete(argv: string[]) { 76 + if (argv[0] === "--") t.parse(argv.slice(1)); 77 + else t.setup("fid", "fid", argv[0] ?? "zsh"); 78 + }