diff --git a/src/cli.ts b/src/cli.ts index 534b221..9b239cf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,7 +6,19 @@ import { animateBanner } from "./banner.js"; const args = parse(process.argv.slice(2), { // note: args parses `--no-repos` as minimist-style negation → `repos: false` - boolean: ["json", "all", "fresh", "help", "version", "mine", "repos"], + boolean: [ + "json", + "all", + "fresh", + "help", + "version", + "mine", + "repos", + "down", + "stale", + "managed", + "self-hosted", + ], string: ["as"], alias: { h: "help", v: "version" }, }); @@ -17,6 +29,10 @@ const usage = `usage: --all include localhost/dev junk knots --fresh skip the 5-minute cache --no-repos skip the slow repo sweep (no counts, knots-only) + --down only unreachable knots + --stale only pre-v1.15 knots (the silent-data-loss cohort) + --managed only tangled-hosted knots + --self-hosted only self-hosted knots 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) @@ -69,7 +85,11 @@ switch (command) { case "list": await ( await import("./commands/list.js") - ).run({ ...args, noRepos: args.repos === false }); + ).run({ + ...args, + noRepos: args.repos === false, + selfHosted: args["self-hosted"], + }); break; case "check": await (await import("./commands/check.js")).run( diff --git a/src/commands/list.ts b/src/commands/list.ts index 643eb0a..b102176 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -9,6 +9,10 @@ export async function run(flags: { all?: boolean; fresh?: boolean; noRepos?: boolean; + down?: boolean; + stale?: boolean; + managed?: boolean; + selfHosted?: boolean; }) { if (!flags.json) p.intro(styleText("green", "fiddling with knots")); const spin = flags.json ? null : p.spinner(); @@ -25,15 +29,38 @@ export async function run(flags: { } spin?.stop(`network assembled ${dim(`(${network.generatedAt})`)}`); + // status flags or together, hosting flags or together, groups and together + type K = Network["knots"][number]; + const groups = [ + [ + flags.down && ((k: K) => !k.local && !k.probe?.reachable), + flags.stale && + ((k: K) => !k.local && k.probe?.reachable && k.version !== "v1.15+"), + ], + [ + flags.managed && ((k: K) => k.managed), + flags.selfHosted && ((k: K) => !k.managed && !k.local), + ], + ] + .map((g) => g.filter((f) => typeof f === "function")) + .filter((g) => g.length > 0); + const filtered = groups.length + ? network.knots.filter((k) => groups.every((g) => g.some((f) => f(k)))) + : network.knots; + if (flags.json) { - console.log(JSON.stringify(network, null, 2)); + console.log( + JSON.stringify( + groups.length ? { ...network, knots: filtered } : network, + null, + 2, + ), + ); return; } - const shown = flags.all - ? network.knots - : network.knots.filter((k) => !k.local); - const hidden = network.knots.length - shown.length; + const shown = flags.all ? filtered : filtered.filter((k) => !k.local); + const hidden = filtered.length - shown.length; const versionLabel = (k: (typeof shown)[number]) => { if (k.local) return dim("local"); @@ -83,6 +110,7 @@ export async function run(flags: { (flags.noRepos ? "repo sweep skipped — counts/inference unavailable (--no-repos)" : `${network.totalRepos} repos across ${network.repoOwners} owners`) + + (groups.length ? ` · filtered from ${network.knots.length}` : "") + (hidden ? ` · ${hidden} local/dev knots hidden (--all)` : ""), ), );