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: add --no-repos fast path to list

+50 -11
+6 -2
src/cli.ts
··· 5 5 import { animateBanner } from "./banner.js"; 6 6 7 7 const args = parse(process.argv.slice(2), { 8 - boolean: ["json", "all", "fresh", "help", "version", "mine"], 8 + // note: args parses `--no-repos` as minimist-style negation → `repos: false` 9 + boolean: ["json", "all", "fresh", "help", "version", "mine", "repos"], 9 10 string: ["as"], 10 11 alias: { h: "help", v: "version" }, 11 12 }); ··· 15 16 --json machine-readable output 16 17 --all include localhost/dev junk knots 17 18 --fresh skip the 5-minute cache 19 + --no-repos skip the slow repo sweep (no counts, knots-only) 18 20 fid check <host> health-check one knot (exit 0 ok / 1 stale / 2 down) 19 21 --mine check every knot you own instead 20 22 --as <handle> who you are (or {"handle":"..."} in ~/.config/fid/config.json) ··· 65 67 66 68 switch (command) { 67 69 case "list": 68 - await (await import("./commands/list.js")).run(args); 70 + await ( 71 + await import("./commands/list.js") 72 + ).run({ ...args, noRepos: args.repos === false }); 69 73 break; 70 74 case "check": 71 75 await (await import("./commands/check.js")).run(
+8 -3
src/commands/list.ts
··· 8 8 json?: boolean; 9 9 all?: boolean; 10 10 fresh?: boolean; 11 + noRepos?: boolean; 11 12 }) { 12 13 if (!flags.json) p.intro(styleText("green", "fiddling with knots")); 13 14 const spin = flags.json ? null : p.spinner(); 14 15 spin?.start("assembling the network"); 15 16 let network: Network; 16 17 try { 17 - network = await loadNetwork(!!flags.fresh, (msg) => spin?.message(msg)); 18 + network = await loadNetwork(!!flags.fresh, (msg) => spin?.message(msg), { 19 + repos: !flags.noRepos, 20 + }); 18 21 } catch (err) { 19 22 spin?.stop("upstream failure", 1); 20 23 console.error(err instanceof Error ? err.message : err); ··· 48 51 49 52 const rows = shown.map((k) => [ 50 53 k.host, 51 - String(k.repoCount), 54 + flags.noRepos ? dim("—") : String(k.repoCount), 52 55 versionLabel(k), 53 56 latencyLabel(k), 54 57 k.local ? dim("—") : k.managed ? "managed" : "self-hosted", ··· 77 80 p.outro( 78 81 dim( 79 82 `${shown.length} knots · ${reachable} reachable · ${stale} pre-v1.15 · ` + 80 - `${network.totalRepos} repos across ${network.repoOwners} owners` + 83 + (flags.noRepos 84 + ? "repo sweep skipped — counts/inference unavailable (--no-repos)" 85 + : `${network.totalRepos} repos across ${network.repoOwners} owners`) + 81 86 (hidden ? ` · ${hidden} local/dev knots hidden (--all)` : ""), 82 87 ), 83 88 );
+15 -6
src/core/network.ts
··· 148 148 }; 149 149 } 150 150 151 + export type AssembleOpts = { repos?: boolean }; // repos: false skips the slow sh.tangled.repo sweep 152 + 151 153 export async function assembleNetwork( 152 154 onProgress?: (msg: string) => void, 155 + opts: AssembleOpts = {}, 153 156 ): Promise<Network> { 154 157 const progress = onProgress ?? (() => {}); 158 + const sweepRepos = opts.repos !== false; 155 159 156 160 progress("enumerating the network via relay…"); 157 161 const [knotDids, spindleDids, repoDids] = await Promise.all([ 158 162 listDidsByCollection("sh.tangled.knot"), 159 163 listDidsByCollection("sh.tangled.spindle"), 160 - listDidsByCollection("sh.tangled.repo", (n) => 161 - progress(`enumerating repo owners… ${n}`), 162 - ), 164 + sweepRepos 165 + ? listDidsByCollection("sh.tangled.repo", (n) => 166 + progress(`enumerating repo owners… ${n}`), 167 + ) 168 + : [], 163 169 ]); 164 170 165 171 // resolution + pds reads fan out across many hosts — go wide, cache makes reruns cheap ··· 248 254 export async function loadNetwork( 249 255 fresh: boolean, 250 256 onProgress?: (msg: string) => void, 257 + opts: AssembleOpts = {}, 251 258 ): Promise<Network> { 252 - // key bumped when the snapshot shape changes — v2 lacked `spindles` 253 - return cached("network3", fresh ? 0 : 5 * 60 * 1000, () => 254 - assembleNetwork(onProgress), 259 + // key bumped when the snapshot shape changes — v2 lacked `spindles`. 260 + // repo-less runs cache separately so a partial snapshot never serves a full one 261 + const key = opts.repos === false ? "network3-lite" : "network3"; 262 + return cached(key, fresh ? 0 : 5 * 60 * 1000, () => 263 + assembleNetwork(onProgress, opts), 255 264 ); 256 265 }
+21
tests/network.test.ts
··· 88 88 }); 89 89 }); 90 90 91 + test("groupNetwork with an empty repo sweep: record knots only, zero counts, no inference", () => { 92 + const network = groupNetwork({ 93 + knotOwners: [ 94 + { 95 + identity: { did: "did:plc:a", handle: "oppi.li" }, 96 + hosts: ["knot1.tangled.sh"], 97 + }, 98 + ], 99 + spindleOwners: [], 100 + repos: [], 101 + repoOwners: 0, 102 + probes: new Map(), 103 + }); 104 + expect(network.knots.map((k) => k.host)).toEqual(["knot1.tangled.sh"]); 105 + expect(network.knots[0]).toMatchObject({ 106 + repoCount: 0, 107 + ownersInferred: false, 108 + }); 109 + expect(network.totalRepos).toBe(0); 110 + }); 111 + 91 112 afterEach(() => vi.unstubAllGlobals()); 92 113 93 114 test("fetchServiceHosts handles both lexicon shapes: host field and rkey-as-host", async () => {