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.

fid / src / cli.ts
3.3 kB 105 lines
1#!/usr/bin/env node 2import { readFileSync } from "node:fs"; 3import { styleText } from "node:util"; 4import { parse } from "@bomb.sh/args"; 5import { animateBanner } from "./banner.js"; 6 7const args = parse(process.argv.slice(2), { 8 // note: args parses `--no-repos` as minimist-style negation → `repos: false` 9 boolean: ["json", "all", "fresh", "help", "version", "mine", "repos"], 10 string: ["as"], 11 alias: { h: "help", v: "version" }, 12}); 13 14const usage = `usage: 15 fid list every knot on the network 16 --json machine-readable output 17 --all include localhost/dev junk knots 18 --fresh skip the 5-minute cache 19 --no-repos skip the slow repo sweep (no counts, knots-only) 20 fid check <host> health-check one knot (exit 0 ok / 1 stale / 2 down) 21 --mine check every knot you own instead 22 --as <handle> who you are (or {"handle":"..."} in ~/.config/fid/config.json) 23 fid stats network aggregates: versions, hosting, top knots 24 fid repos <host> what repos live on a knot 25 fid spindles the ci runner fleet 26 fid find <query> search repos across the network 27 fid watch live network board (q to quit) 28 fid --version print the version 29`; 30 31// colors go in after the columns are padded, so alignment math never sees ansi 32const colorUsage = (s: string) => 33 s 34 .split("\n") 35 .map((line) => { 36 if (line === "usage:") return styleText("dim", line); 37 const m = /^(\s+)(\S.*?)(\s{2,})(.*)$/.exec(line); 38 if (!m) return line; 39 const [, indent, left, gap, desc] = m; 40 const colored = left 41 .replace( 42 /^fid (?!--)(\S+)/, 43 (_, cmd: string) => `fid ${styleText(["green", "bold"], cmd)}`, 44 ) 45 .replace(/--[\w-]+/g, (f) => styleText("cyan", f)) 46 .replace(/<[^>]+>/g, (a) => styleText("magenta", a)); 47 return indent + colored + gap + styleText("dim", desc); 48 }) 49 .join("\n"); 50 51if (args.version) { 52 const pkg = JSON.parse( 53 readFileSync(new URL("../package.json", import.meta.url), "utf8"), 54 ); 55 console.log(`fid ${pkg.version}`); 56 process.exit(0); 57} 58 59const [command] = args._; 60// keep --json pipe-clean; watch takes the whole screen immediately 61if (!args.json && command !== "watch") await animateBanner(); 62 63if (args.help || !command) { 64 console.log(colorUsage(usage)); 65 process.exit(args.help ? 0 : 1); 66} 67 68switch (command) { 69 case "list": 70 await ( 71 await import("./commands/list.js") 72 ).run({ ...args, noRepos: args.repos === false }); 73 break; 74 case "check": 75 await (await import("./commands/check.js")).run( 76 args, 77 args._[1] === undefined ? undefined : String(args._[1]), 78 ); 79 break; 80 case "stats": 81 await (await import("./commands/stats.js")).run(args); 82 break; 83 case "repos": 84 await (await import("./commands/repos.js")).run( 85 args, 86 args._[1] === undefined ? undefined : String(args._[1]), 87 ); 88 break; 89 case "spindles": 90 await (await import("./commands/spindles.js")).run(args); 91 break; 92 case "find": 93 await (await import("./commands/find.js")).run( 94 args, 95 args._.slice(1).map(String).join(" "), 96 ); 97 break; 98 case "watch": 99 await (await import("./commands/watch.js")).run(args); 100 break; 101 default: 102 console.error(`unknown command: ${command}\n`); 103 console.log(colorUsage(usage)); 104 process.exit(1); 105}