#!/usr/bin/env node import { readFileSync } from "node:fs"; import { styleText } from "node:util"; import { parse } from "@bomb.sh/args"; import { animateBanner } from "./banner.js"; // completion calls answer before anything else — no banner, no arg parsing if (process.argv[2] === "complete") { const { runComplete } = await import("./completions.js"); runComplete(process.argv.slice(3)); process.exit(0); } 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", "down", "stale", "managed", "self-hosted", ], string: ["as"], alias: { h: "help", v: "version" }, }); const usage = `usage: fid list every knot on the network --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) --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) fid stats network aggregates: versions, hosting, top knots fid repos what repos live on a knot fid spindles the ci runner fleet fid find search repos across the network fid watch live network board (q to quit) fid complete print completion script (zsh/bash/fish/powershell) fid --version print the version `; // colors go in after the columns are padded, so alignment math never sees ansi const colorUsage = (s: string) => s .split("\n") .map((line) => { if (line === "usage:") return styleText("dim", line); const m = /^(\s+)(\S.*?)(\s{2,})(.*)$/.exec(line); if (!m) return line; const [, indent, left, gap, desc] = m; const colored = left .replace( /^fid (?!--)(\S+)/, (_, cmd: string) => `fid ${styleText(["green", "bold"], cmd)}`, ) .replace(/--[\w-]+/g, (f) => styleText("cyan", f)) .replace(/<[^>]+>/g, (a) => styleText("magenta", a)); return indent + colored + gap + styleText("dim", desc); }) .join("\n"); if (args.version) { const pkg = JSON.parse( readFileSync(new URL("../package.json", import.meta.url), "utf8"), ); console.log(`fid ${pkg.version}`); process.exit(0); } const [command] = args._; // keep --json pipe-clean if (!args.json) await animateBanner(); if (args.help) { console.log(colorUsage(usage)); process.exit(0); } if (!command) { console.log( styleText("dim", "run ") + styleText("cyan", "fid -h") + styleText("dim", " to see what fid can do"), ); process.exit(1); } switch (command) { case "list": await ( await import("./commands/list.js") ).run({ ...args, noRepos: args.repos === false, selfHosted: args["self-hosted"], }); break; case "check": await (await import("./commands/check.js")).run( args, args._[1] === undefined ? undefined : String(args._[1]), ); break; case "stats": await (await import("./commands/stats.js")).run(args); break; case "repos": await (await import("./commands/repos.js")).run( args, args._[1] === undefined ? undefined : String(args._[1]), ); break; case "spindles": await (await import("./commands/spindles.js")).run(args); break; case "find": await (await import("./commands/find.js")).run( args, args._.slice(1).map(String).join(" "), ); break; case "watch": await (await import("./commands/watch.js")).run(args); break; default: console.error(`unknown command: ${command}\n`); console.log(colorUsage(usage)); process.exit(1); }