|
| 1 | +/** |
| 2 | + * generate-facts.ts — single source of truth for the factual copy shared by |
| 3 | + * the CLI docs and the website (site/src/facts.json). |
| 4 | + * |
| 5 | + * Derivations: |
| 6 | + * - engines + default engine: the live adapter registry (src/adapters) and |
| 7 | + * DEFAULT_ENGINE (src/command.ts) |
| 8 | + * - subcommand names: scanned from src/cli-runner.ts (`subcommand === "x"`), |
| 9 | + * so a new subcommand without a description entry here FAILS the build |
| 10 | + * - version: package.json |
| 11 | + * |
| 12 | + * Usage: |
| 13 | + * bun run scripts/generate-facts.ts # write site/src/facts.json |
| 14 | + * bun run scripts/generate-facts.ts --check # exit 1 if the file is stale |
| 15 | + */ |
| 16 | + |
| 17 | +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs"; |
| 18 | +import { join, dirname } from "path"; |
| 19 | +import { getRegisteredAdapters } from "../src/adapters"; |
| 20 | +import { DEFAULT_ENGINE } from "../src/command"; |
| 21 | + |
| 22 | +const ROOT = join(import.meta.dir, ".."); |
| 23 | +const FACTS_PATH = join(ROOT, "site", "src", "facts.json"); |
| 24 | + |
| 25 | +// --- version ----------------------------------------------------------- |
| 26 | +// Only the base version is embedded: semantic-release bumps the prerelease |
| 27 | +// number on every release commit, and the site badge shouldn't churn (or |
| 28 | +// fail facts:check) each time. |
| 29 | +const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")); |
| 30 | +const versionBase = (pkg.version as string).split("-")[0]; // 3.0.0-next.4 → 3.0.0 |
| 31 | + |
| 32 | +// --- engines ----------------------------------------------------------- |
| 33 | +const ENGINE_LABELS: Record<string, string> = { |
| 34 | + agy: "agy (Antigravity)", |
| 35 | +}; |
| 36 | +const engines = getRegisteredAdapters(); |
| 37 | +const enginesLabel = engines.map((e) => ENGINE_LABELS[e] ?? e).join(", "); |
| 38 | + |
| 39 | +// --- subcommands (names derived from cli-runner.ts) --------------------- |
| 40 | +const cliRunnerSource = readFileSync(join(ROOT, "src", "cli-runner.ts"), "utf-8"); |
| 41 | +const discovered = new Set<string>(); |
| 42 | +for (const match of cliRunnerSource.matchAll(/subcommand === "([a-z-]+)"/g)) { |
| 43 | + discovered.add(match[1]!); |
| 44 | +} |
| 45 | +discovered.add("help"); // handled as a flag in src/cli.ts |
| 46 | + |
| 47 | +/** Display order + copy. Every discovered subcommand MUST appear here. */ |
| 48 | +const COMMAND_DOCS: Record<string, { usage: string; description: string }> = { |
| 49 | + init: { usage: "init [--engine <e>] [-y]", description: "bootstrap a flow roster (agent-guided; -y scaffolds deterministically)" }, |
| 50 | + create: { usage: "create [name] [flags]", description: "create a new flow file" }, |
| 51 | + explain: { usage: "explain <flow.md>", description: "show resolved config without executing (free)" }, |
| 52 | + eval: { usage: "eval <flow.md>", description: "run the flow's eval suite — costs engine turns" }, |
| 53 | + complain: { usage: 'complain <flow.md> "msg"', description: "record evolution evidence (free)" }, |
| 54 | + evolve: { usage: "evolve <flow.md> [--check]", description: "evidence-gated prompt evolution; --check is free" }, |
| 55 | + install: { usage: "install <url|gh:org/repo/path@ref>", description: "install a flow from a registry" }, |
| 56 | + remove: { usage: "remove <name>", description: "remove an installed flow" }, |
| 57 | + list: { usage: "list", description: "list installed registry flows" }, |
| 58 | + setup: { usage: "setup", description: "configure shell (PATH, aliases)" }, |
| 59 | + logs: { usage: "logs", description: "show the flow log directory" }, |
| 60 | + help: { usage: "help", description: "full built-in help" }, |
| 61 | +}; |
| 62 | + |
| 63 | +const undocumented = [...discovered].filter((name) => !COMMAND_DOCS[name]); |
| 64 | +if (undocumented.length > 0) { |
| 65 | + console.error( |
| 66 | + `generate-facts: subcommand(s) handled in cli-runner.ts but missing from COMMAND_DOCS: ${undocumented.join(", ")}` |
| 67 | + ); |
| 68 | + process.exit(1); |
| 69 | +} |
| 70 | +const stale = Object.keys(COMMAND_DOCS).filter((name) => !discovered.has(name)); |
| 71 | +if (stale.length > 0) { |
| 72 | + console.error( |
| 73 | + `generate-facts: COMMAND_DOCS documents subcommand(s) no longer handled in cli-runner.ts: ${stale.join(", ")}` |
| 74 | + ); |
| 75 | + process.exit(1); |
| 76 | +} |
| 77 | +const commands = Object.entries(COMMAND_DOCS).map(([name, doc]) => ({ name, ...doc })); |
| 78 | + |
| 79 | +// --- static-but-centralized facts --------------------------------------- |
| 80 | +const ladder = [ |
| 81 | + { rung: "--engine flag", note: "deprecated aliases: --_command/-_c, --tool" }, |
| 82 | + { rung: "MDFLOW_ENGINE env var", note: "" }, |
| 83 | + { rung: "filename (task.claude.md)", note: "must name a real engine" }, |
| 84 | + { rung: "frontmatter engine:", note: "deprecated aliases: tool:/_tool: (they warn)" }, |
| 85 | + { rung: "config engine:", note: "project config beats ~/.mdflow/config.yaml" }, |
| 86 | + { rung: `default: ${DEFAULT_ENGINE}`, note: "implicit picks are announced on stderr" }, |
| 87 | +]; |
| 88 | + |
| 89 | +const mdFlags = [ |
| 90 | + { flag: "--engine", description: "specify the engine to run" }, |
| 91 | + { flag: "--_dry-run", description: "preview without executing" }, |
| 92 | + { flag: "--_edit", description: "edit prompt in $EDITOR" }, |
| 93 | + { flag: "--_context", description: "show context tree" }, |
| 94 | + { flag: "--raw", description: "raw output (for piping)" }, |
| 95 | + { flag: "--json", description: "single JSON result object" }, |
| 96 | +]; |
| 97 | + |
| 98 | +const facts = { |
| 99 | + $generated: "by scripts/generate-facts.ts — DO NOT EDIT; run `bun run facts`", |
| 100 | + versionBase, |
| 101 | + defaultEngine: DEFAULT_ENGINE, |
| 102 | + engines, |
| 103 | + enginesLabel, |
| 104 | + install: "npx mdflow init", |
| 105 | + repo: "https://github.com/johnlindquist/mdflow", |
| 106 | + ladder, |
| 107 | + commands, |
| 108 | + mdFlags, |
| 109 | +}; |
| 110 | + |
| 111 | +// --- write / check ------------------------------------------------------- |
| 112 | +const output = JSON.stringify(facts, null, 2) + "\n"; |
| 113 | +const checkMode = process.argv.includes("--check"); |
| 114 | + |
| 115 | +if (checkMode) { |
| 116 | + const current = existsSync(FACTS_PATH) ? readFileSync(FACTS_PATH, "utf-8") : ""; |
| 117 | + if (current !== output) { |
| 118 | + console.error( |
| 119 | + "generate-facts: site/src/facts.json is stale. Run `bun run facts` and commit the result." |
| 120 | + ); |
| 121 | + process.exit(1); |
| 122 | + } |
| 123 | + console.log("generate-facts: site/src/facts.json is up to date."); |
| 124 | +} else { |
| 125 | + mkdirSync(dirname(FACTS_PATH), { recursive: true }); |
| 126 | + writeFileSync(FACTS_PATH, output); |
| 127 | + console.log(`generate-facts: wrote ${FACTS_PATH}`); |
| 128 | +} |
0 commit comments