diff --git a/src/cli.ts b/src/cli.ts index 2cc8928..534b221 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,7 +5,8 @@ import { parse } from "@bomb.sh/args"; import { animateBanner } from "./banner.js"; const args = parse(process.argv.slice(2), { - boolean: ["json", "all", "fresh", "help", "version", "mine"], + // note: args parses `--no-repos` as minimist-style negation → `repos: false` + boolean: ["json", "all", "fresh", "help", "version", "mine", "repos"], string: ["as"], alias: { h: "help", v: "version" }, }); @@ -15,6 +16,7 @@ const usage = `usage: --json machine-readable output --all include localhost/dev junk knots --fresh skip the 5-minute cache + --no-repos skip the slow repo sweep (no counts, knots-only) fid check health-check one knot (exit 0 ok / 1 stale / 2 down) --mine check every knot you own instead --as who you are (or {"handle":"..."} in ~/.config/fid/config.json) @@ -65,7 +67,9 @@ if (args.help || !command) { switch (command) { case "list": - await (await import("./commands/list.js")).run(args); + await ( + await import("./commands/list.js") + ).run({ ...args, noRepos: args.repos === false }); break; case "check": await (await import("./commands/check.js")).run( diff --git a/src/commands/list.ts b/src/commands/list.ts index 0cae97e..643eb0a 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -8,13 +8,16 @@ export async function run(flags: { json?: boolean; all?: boolean; fresh?: boolean; + noRepos?: boolean; }) { if (!flags.json) p.intro(styleText("green", "fiddling with knots")); const spin = flags.json ? null : p.spinner(); spin?.start("assembling the network"); let network: Network; try { - network = await loadNetwork(!!flags.fresh, (msg) => spin?.message(msg)); + network = await loadNetwork(!!flags.fresh, (msg) => spin?.message(msg), { + repos: !flags.noRepos, + }); } catch (err) { spin?.stop("upstream failure", 1); console.error(err instanceof Error ? err.message : err); @@ -48,7 +51,7 @@ export async function run(flags: { const rows = shown.map((k) => [ k.host, - String(k.repoCount), + flags.noRepos ? dim("—") : String(k.repoCount), versionLabel(k), latencyLabel(k), k.local ? dim("—") : k.managed ? "managed" : "self-hosted", @@ -77,7 +80,9 @@ export async function run(flags: { p.outro( dim( `${shown.length} knots · ${reachable} reachable · ${stale} pre-v1.15 · ` + - `${network.totalRepos} repos across ${network.repoOwners} owners` + + (flags.noRepos + ? "repo sweep skipped — counts/inference unavailable (--no-repos)" + : `${network.totalRepos} repos across ${network.repoOwners} owners`) + (hidden ? ` · ${hidden} local/dev knots hidden (--all)` : ""), ), ); diff --git a/src/core/network.ts b/src/core/network.ts index a1b9210..8cae92a 100644 --- a/src/core/network.ts +++ b/src/core/network.ts @@ -148,18 +148,24 @@ export function groupNetwork(input: { }; } +export type AssembleOpts = { repos?: boolean }; // repos: false skips the slow sh.tangled.repo sweep + export async function assembleNetwork( onProgress?: (msg: string) => void, + opts: AssembleOpts = {}, ): Promise { const progress = onProgress ?? (() => {}); + const sweepRepos = opts.repos !== false; progress("enumerating the network via relay…"); const [knotDids, spindleDids, repoDids] = await Promise.all([ listDidsByCollection("sh.tangled.knot"), listDidsByCollection("sh.tangled.spindle"), - listDidsByCollection("sh.tangled.repo", (n) => - progress(`enumerating repo owners… ${n}`), - ), + sweepRepos + ? listDidsByCollection("sh.tangled.repo", (n) => + progress(`enumerating repo owners… ${n}`), + ) + : [], ]); // resolution + pds reads fan out across many hosts — go wide, cache makes reruns cheap @@ -248,9 +254,12 @@ export async function assembleNetwork( export async function loadNetwork( fresh: boolean, onProgress?: (msg: string) => void, + opts: AssembleOpts = {}, ): Promise { - // key bumped when the snapshot shape changes — v2 lacked `spindles` - return cached("network3", fresh ? 0 : 5 * 60 * 1000, () => - assembleNetwork(onProgress), + // key bumped when the snapshot shape changes — v2 lacked `spindles`. + // repo-less runs cache separately so a partial snapshot never serves a full one + const key = opts.repos === false ? "network3-lite" : "network3"; + return cached(key, fresh ? 0 : 5 * 60 * 1000, () => + assembleNetwork(onProgress, opts), ); } diff --git a/tests/network.test.ts b/tests/network.test.ts index 0c9b7fd..8df3313 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -88,6 +88,27 @@ test("groupNetwork groups repos by knot host and classifies versions", () => { }); }); +test("groupNetwork with an empty repo sweep: record knots only, zero counts, no inference", () => { + const network = groupNetwork({ + knotOwners: [ + { + identity: { did: "did:plc:a", handle: "oppi.li" }, + hosts: ["knot1.tangled.sh"], + }, + ], + spindleOwners: [], + repos: [], + repoOwners: 0, + probes: new Map(), + }); + expect(network.knots.map((k) => k.host)).toEqual(["knot1.tangled.sh"]); + expect(network.knots[0]).toMatchObject({ + repoCount: 0, + ownersInferred: false, + }); + expect(network.totalRepos).toBe(0); +}); + afterEach(() => vi.unstubAllGlobals()); test("fetchServiceHosts handles both lexicon shapes: host field and rkey-as-host", async () => {