agent-doc-system 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_FOR_AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/adapters/ansible.mjs +306 -0
- package/adapters/cache.mjs +27 -0
- package/adapters/contract-consumers.mjs +56 -0
- package/adapters/database.mjs +116 -0
- package/adapters/deploy-config.mjs +34 -0
- package/adapters/dockerfile.mjs +26 -0
- package/adapters/events.mjs +145 -0
- package/adapters/generic.mjs +86 -0
- package/adapters/github-actions.mjs +106 -0
- package/adapters/go.mjs +91 -0
- package/adapters/graphql.mjs +28 -0
- package/adapters/index.mjs +56 -0
- package/adapters/node.mjs +118 -0
- package/adapters/object-store.mjs +64 -0
- package/adapters/oidc.mjs +56 -0
- package/adapters/openapi.mjs +50 -0
- package/adapters/pnpm.mjs +42 -0
- package/adapters/protobuf.mjs +35 -0
- package/adapters/registry.mjs +27 -0
- package/adapters/resource-client.mjs +61 -0
- package/adapters/turborepo.mjs +135 -0
- package/adapters/typescript.mjs +85 -0
- package/adapters/wrangler.mjs +206 -0
- package/bin/agentdoc.mjs +494 -0
- package/bin/commands.mjs +169 -0
- package/bin/usage.txt +28 -0
- package/core/acceptances.mjs +69 -0
- package/core/audit.mjs +456 -0
- package/core/authority/classes.mjs +39 -0
- package/core/authority/engine.mjs +266 -0
- package/core/authority/facts.mjs +41 -0
- package/core/codes.mjs +126 -0
- package/core/compile.mjs +1018 -0
- package/core/contracts.mjs +121 -0
- package/core/derive.mjs +102 -0
- package/core/descriptors.mjs +489 -0
- package/core/determinism.mjs +95 -0
- package/core/discover.mjs +147 -0
- package/core/facts.mjs +53 -0
- package/core/fsx.mjs +348 -0
- package/core/graph.mjs +260 -0
- package/core/impact.mjs +175 -0
- package/core/indexes.mjs +165 -0
- package/core/jsonschema.mjs +298 -0
- package/core/normative.mjs +171 -0
- package/core/observations.mjs +129 -0
- package/core/provenance.mjs +83 -0
- package/core/query.mjs +384 -0
- package/core/relations.mjs +286 -0
- package/core/scaffold.mjs +322 -0
- package/core/secrets.mjs +92 -0
- package/core/sourcescan.mjs +88 -0
- package/core/toml.mjs +163 -0
- package/core/yaml.mjs +477 -0
- package/docs/ADAPTERS.md +207 -0
- package/docs/AUTHORITY.md +269 -0
- package/docs/CI.md +138 -0
- package/docs/COMPARISON-KODA.md +164 -0
- package/docs/EVALS.md +166 -0
- package/docs/MIGRATION.md +261 -0
- package/docs/PROVENANCE.md +133 -0
- package/docs/QUERY.md +180 -0
- package/docs/SPEC.md +339 -0
- package/docs/VERIFICATION.md +28 -0
- package/docs/assets/readme/agentdoc-hero.webp +0 -0
- package/docs/assets/readme/agentdoc-in-action.webp +0 -0
- package/docs/assets/readme/how-agentdoc-works.webp +0 -0
- package/docs/assets/readme/why-agentdoc.webp +0 -0
- package/docs/ci/generic-ci.sh +28 -0
- package/docs/ci/github-actions.yml +35 -0
- package/evals/harness.mjs +426 -0
- package/evals/scenarios/fixture-a-01-change-api.json +9 -0
- package/evals/scenarios/fixture-a-02-change-db.json +9 -0
- package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
- package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
- package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-b-04-journey.json +9 -0
- package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
- package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
- package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
- package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
- package/evals/scenarios/koda-02-change-db-schema.json +20 -0
- package/evals/scenarios/koda-03-change-auth.json +21 -0
- package/evals/scenarios/koda-04-add-event-producer.json +19 -0
- package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
- package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
- package/evals/scenarios/koda-07-change-shared-package.json +16 -0
- package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
- package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
- package/evals/thresholds.json +11 -0
- package/package.json +46 -4
- package/schemas/api.schema.json +84 -0
- package/schemas/common.schema.json +147 -0
- package/schemas/component.schema.json +44 -0
- package/schemas/config.schema.json +801 -0
- package/schemas/domain.schema.json +14 -0
- package/schemas/graph.schema.json +1084 -0
- package/schemas/journeys.schema.json +47 -0
- package/schemas/observation.schema.json +143 -0
- package/schemas/resource.schema.json +28 -0
- package/schemas/system.schema.json +26 -0
- package/skills/agent-doc-system/SKILL.md +117 -0
- package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
- package/skills/agent-doc-system/references/cli.md +97 -0
- package/skills/agent-doc-system/references/create-migrate.md +113 -0
- package/skills/agent-doc-system/references/maintenance.md +52 -0
- package/templates/ADR.md +26 -0
- package/templates/ARCHITECTURE.md +19 -0
- package/templates/CONSTRAINTS.md +17 -0
- package/templates/PRODUCT.md +17 -0
- package/templates/api.yaml +10 -0
- package/templates/ci.yml +31 -0
- package/templates/component.yaml +14 -0
- package/templates/domain.yaml +6 -0
- package/templates/journey.yaml +7 -0
- package/templates/observation.yaml +19 -0
- package/templates/resource.yaml +8 -0
- package/templates/system.yaml +10 -0
- package/templates/warning-acceptance.yaml +14 -0
package/bin/agentdoc.mjs
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agentdoc — portable documentation & software-catalog system.
|
|
3
|
+
//
|
|
4
|
+
// The command list lives in bin/usage.txt and is printed from there, so this
|
|
5
|
+
// header, `--help` and the shipped file cannot drift into three different
|
|
6
|
+
// claims about what the tool does. The flags each command accepts live in
|
|
7
|
+
// bin/commands.mjs, which drives both parsing and `agentdoc <command> --help`.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { AgentDocError, CODES } from "../core/codes.mjs";
|
|
12
|
+
import { compile, sha256Hex } from "../core/compile.mjs";
|
|
13
|
+
import { Repo } from "../core/fsx.mjs";
|
|
14
|
+
import { assertFresh, readGraph, COMPILER_VERSION, GRAPH_SCHEMA_VERSION, COMPILER_NAME } from "../core/graph.mjs";
|
|
15
|
+
import { queryContext, renderContext } from "../core/query.mjs";
|
|
16
|
+
import { impact } from "../core/impact.mjs";
|
|
17
|
+
import { audit, scaffoldProposal } from "../core/audit.mjs";
|
|
18
|
+
import { observationFreshness, stalenessDiagnostics } from "../core/observations.mjs";
|
|
19
|
+
import { assertNoSecretsInText } from "../core/secrets.mjs";
|
|
20
|
+
import { installTemplates } from "../core/scaffold.mjs";
|
|
21
|
+
import { COMMANDS, flagsOf, parseArgs, commandHelp, UsageError } from "./commands.mjs";
|
|
22
|
+
import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
|
|
25
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const PACKAGE_ROOT = path.join(HERE, "..");
|
|
27
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
28
|
+
const MIN_NODE_MAJOR = 22;
|
|
29
|
+
// Commands are run from wherever the agent happens to be. Walk up until the
|
|
30
|
+
// configuration is found, so `agentdoc query src/foo.ts` works from a
|
|
31
|
+
// subdirectory instead of failing with a misleading missing-file error.
|
|
32
|
+
function findRoot(start) {
|
|
33
|
+
let dir = start;
|
|
34
|
+
for (let i = 0; i < 24; i++) {
|
|
35
|
+
if (fs.existsSync(path.join(dir, "agentdoc", "agentdoc.config.yaml"))) return dir;
|
|
36
|
+
const up = path.dirname(dir);
|
|
37
|
+
if (up === dir) break;
|
|
38
|
+
dir = up;
|
|
39
|
+
}
|
|
40
|
+
return start;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ROOT = findRoot(process.cwd());
|
|
44
|
+
const argv = process.argv.slice(2);
|
|
45
|
+
const cmd = argv[0];
|
|
46
|
+
// Flags are declared per command in bin/commands.mjs. An undeclared flag is a
|
|
47
|
+
// usage error (exit 2), so a typo in a gate flag can never pass for the gate.
|
|
48
|
+
let parsed = { flags: new Map(), positional: [] };
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, cmd)) {
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArgs(cmd, argv.slice(1));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (!(e instanceof UsageError)) throw e;
|
|
54
|
+
console.error(e.message);
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const positional = parsed.positional;
|
|
59
|
+
// The only way to read a flag. Reading one the command does not declare is a
|
|
60
|
+
// defect in this file, not user input: it would be a flag `--help` never shows.
|
|
61
|
+
function opt(name) {
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(flagsOf(cmd), name)) {
|
|
63
|
+
throw new Error("flag --" + name + " is read by '" + cmd + "' but not declared for it in bin/commands.mjs");
|
|
64
|
+
}
|
|
65
|
+
return parsed.flags.has(name) ? parsed.flags.get(name) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fail(errors, stats) {
|
|
69
|
+
for (const e of errors) console.error("ERROR " + (e.format ? e.format() : e.code + ": " + e.message));
|
|
70
|
+
if (stats) console.error("agentdoc: " + JSON.stringify(stats));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function usage(exitCode) {
|
|
75
|
+
const text = fs.readFileSync(path.join(HERE, "usage.txt"), "utf8");
|
|
76
|
+
(exitCode === 0 ? process.stdout : process.stderr).write(text);
|
|
77
|
+
process.exit(exitCode);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printDiagnostics(res) {
|
|
81
|
+
for (const d of res.diagnostics || []) {
|
|
82
|
+
const prefix = d.severity === "error" ? "ERROR " : d.acceptance ? "WARN ACCEPTED " : "WARN ";
|
|
83
|
+
console.log(prefix + d.code + ": " + d.message);
|
|
84
|
+
if (d.acceptance) console.log(" " + d.acceptance.classification + ": " + d.acceptance.reason);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function gateObservationFreshness(res) {
|
|
89
|
+
// Time-dependent gates run before artifact gates. A committed graph can be
|
|
90
|
+
// stale for many reasons; when the cause is an observation that has aged out
|
|
91
|
+
// of its promise, saying "rebuild the graph" sends the reader down the wrong
|
|
92
|
+
// path entirely.
|
|
93
|
+
const abs = path.join(ROOT, res.graphPath);
|
|
94
|
+
if (!fs.existsSync(abs)) return null;
|
|
95
|
+
const { graph } = readGraph(res.repo, res.graphPath);
|
|
96
|
+
const fresh = observationFreshness(graph.observations || [], new Date());
|
|
97
|
+
const stale = stalenessDiagnostics(fresh);
|
|
98
|
+
if (!stale.length) return fresh;
|
|
99
|
+
if (!opt("allow-stale-observations")) {
|
|
100
|
+
for (const d of stale) console.error("ERROR " + d.code + ": " + d.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
// Staleness was explicitly allowed through. This is where a rule's
|
|
104
|
+
// degradeOnStale is applied — at gate time, never at compile time, so a
|
|
105
|
+
// compile stays a pure function of the checkout. `conflict` keeps the
|
|
106
|
+
// election and warns; `unresolved` refuses to stand on a stale observation.
|
|
107
|
+
// Match the elected assertion to the stale observation *set* by capturedAt
|
|
108
|
+
// + environment — capturedAt alone can collide across sets.
|
|
109
|
+
const staleSets = new Set(fresh.filter((f) => f.stale).map((f) => f.capturedAt + "\u0000" + f.environment));
|
|
110
|
+
const assertionById = new Map((graph.assertions || []).map((a) => [a.id, a]));
|
|
111
|
+
const rules = new Map((((res.cfg || {}).authority || {}).rules || []).map((r) => [r.id, r]));
|
|
112
|
+
const degraded = [];
|
|
113
|
+
for (const c of graph.conflicts || []) {
|
|
114
|
+
if (c.status !== "resolved" || !c.election || !c.election.electedAssertionId) continue;
|
|
115
|
+
const rule = rules.get(c.election.ruleId);
|
|
116
|
+
if (!rule || !rule.degradeOnStale) continue;
|
|
117
|
+
const elected = assertionById.get(c.election.electedAssertionId);
|
|
118
|
+
const obs = elected && elected.observed;
|
|
119
|
+
if (!obs || !obs.at || !staleSets.has(obs.at + "\u0000" + obs.environment)) continue;
|
|
120
|
+
if (rule.degradeOnStale === "unresolved") {
|
|
121
|
+
degraded.push(
|
|
122
|
+
c.subject + " " + c.key + " — the elected observation (" + obs.at + ") is past its maxAgeDays " +
|
|
123
|
+
"and authority rule " + rule.id + " requires the election to degrade to unresolved"
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
console.error(
|
|
127
|
+
"WARN " + CODES.ELECTION_STALE + ": " + c.subject + " " + c.key +
|
|
128
|
+
" — the elected observation (" + obs.at + ") is past its maxAgeDays; authority rule " +
|
|
129
|
+
rule.id + " keeps the election"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (degraded.length) {
|
|
134
|
+
for (const m of degraded) console.error("ERROR " + CODES.CONFLICT_UNRESOLVED + ": " + m);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
return fresh;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireFresh(res, fresh) {
|
|
141
|
+
const abs = path.join(ROOT, res.graphPath);
|
|
142
|
+
if (!fs.existsSync(abs)) {
|
|
143
|
+
fail([new AgentDocError(CODES.GRAPH_MISSING, "no compiled graph at " + res.graphPath + " — run `agentdoc compile`")]);
|
|
144
|
+
}
|
|
145
|
+
const { graph, text } = readGraph(res.repo, res.graphPath);
|
|
146
|
+
try {
|
|
147
|
+
assertFresh(graph, {
|
|
148
|
+
inputHash: res.stats.inputHash,
|
|
149
|
+
dirty: res.stats.dirty,
|
|
150
|
+
compilerVersion: COMPILER_VERSION,
|
|
151
|
+
schemaVersion: GRAPH_SCHEMA_VERSION,
|
|
152
|
+
});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
fail([e]);
|
|
155
|
+
}
|
|
156
|
+
if (graph.source.dirty && opt("require-clean")) {
|
|
157
|
+
fail([new AgentDocError(
|
|
158
|
+
CODES.DIRTY_SOURCE,
|
|
159
|
+
"catalog inputs differ from HEAD (" + res.stats.commit + ") — CI/publish evidence requires a clean source; commit the change and rebuild"
|
|
160
|
+
)], res.stats);
|
|
161
|
+
}
|
|
162
|
+
// `fresh` is normally precomputed by the caller — gateObservationFreshness
|
|
163
|
+
// also evaluates election degradation, so calling it twice would print the
|
|
164
|
+
// same staleness warning twice. The fallback keeps a forgotten call-site safe.
|
|
165
|
+
return { graph, text, fresh: fresh === undefined ? (gateObservationFreshness(res) || []) : fresh };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function out(obj, asJson) {
|
|
169
|
+
const text = asJson ? JSON.stringify(obj, null, 2) + "\n" : null;
|
|
170
|
+
if (text) assertNoSecretsInText(text, "output");
|
|
171
|
+
process.stdout.write(text || obj + "\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
176
|
+
console.log("agentdoc " + VERSION);
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
180
|
+
// `agentdoc help <command>` is the same as `agentdoc <command> --help`.
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, argv[1])) {
|
|
182
|
+
process.stdout.write(commandHelp(argv[1]));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
usage(0);
|
|
186
|
+
}
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(COMMANDS, cmd) && opt("help")) {
|
|
188
|
+
process.stdout.write(commandHelp(cmd));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
if (cmd === "validate") {
|
|
192
|
+
const res = compile(ROOT);
|
|
193
|
+
printDiagnostics(res);
|
|
194
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
195
|
+
console.log("agentdoc validate: PASS " + JSON.stringify(res.stats));
|
|
196
|
+
} else if (cmd === "compile") {
|
|
197
|
+
const res = compile(ROOT);
|
|
198
|
+
printDiagnostics(res);
|
|
199
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
200
|
+
if (opt("require-clean") && res.stats.dirty) {
|
|
201
|
+
fail([new AgentDocError(CODES.DIRTY_SOURCE, "catalog inputs differ from HEAD — commit the change and rebuild")], res.stats);
|
|
202
|
+
}
|
|
203
|
+
fs.mkdirSync(path.dirname(path.join(ROOT, res.graphPath)), { recursive: true });
|
|
204
|
+
const tmp = path.join(ROOT, res.graphPath) + ".tmp-" + process.pid;
|
|
205
|
+
fs.writeFileSync(tmp, res.serialized);
|
|
206
|
+
fs.renameSync(tmp, path.join(ROOT, res.graphPath));
|
|
207
|
+
console.log("agentdoc compile: PASS " + JSON.stringify(res.stats));
|
|
208
|
+
console.log("graph: " + res.graphPath);
|
|
209
|
+
} else if (cmd === "check") {
|
|
210
|
+
const res = compile(ROOT);
|
|
211
|
+
printDiagnostics(res);
|
|
212
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
213
|
+
const preFresh = gateObservationFreshness(res);
|
|
214
|
+
const { graph, text, fresh } = requireFresh(res, preFresh);
|
|
215
|
+
// `source.commit` records which checkout produced the graph, and a checkout
|
|
216
|
+
// without VCS — an extracted archive, a vendored dependency — compiles to
|
|
217
|
+
// `0000000`. That is a difference in provenance, not in content, and failing
|
|
218
|
+
// on it would make a committed graph unusable in exactly the situation it
|
|
219
|
+
// is most needed: shipped as a file, with no history. Everything else must
|
|
220
|
+
// still match byte for byte.
|
|
221
|
+
const withoutCommit = (t) => t.replace(/^(\s*)"commit": "[0-9a-f]*",$/m, '$1"commit": "<provenance>",');
|
|
222
|
+
if (withoutCommit(text) !== withoutCommit(res.serialized)) {
|
|
223
|
+
fail([new AgentDocError(
|
|
224
|
+
CODES.STALE_GRAPH,
|
|
225
|
+
"compiled graph differs from a fresh deterministic compile — commit the descriptor or authority change and rebuild"
|
|
226
|
+
)]);
|
|
227
|
+
}
|
|
228
|
+
const gj = JSON.stringify(graph, null, 2) + "\n";
|
|
229
|
+
if (gj !== text) {
|
|
230
|
+
fail([new AgentDocError(CODES.STALE_GRAPH, "compiled graph is not canonically serialized — rebuild the graph")]);
|
|
231
|
+
}
|
|
232
|
+
// `--strict` is the migration gate: the graph is current *and* nothing is
|
|
233
|
+
// left for a human. Every warning is either accepted with pinned evidence
|
|
234
|
+
// or the gate fails, so a normative section nobody reviewed or a
|
|
235
|
+
// requirement nothing proves cannot ride along in a green check. Opt-in, so
|
|
236
|
+
// plain `check` keeps meaning "the committed graph is fresh".
|
|
237
|
+
if (opt("strict")) {
|
|
238
|
+
const open = (res.diagnostics || []).filter((d) => d.severity === "warning" && !d.acceptance);
|
|
239
|
+
if (open.length) {
|
|
240
|
+
fail(
|
|
241
|
+
open.map((d) => new AgentDocError(d.code, (d.subject && !d.message.includes(d.subject) ? d.subject + ": " : "") + d.message)),
|
|
242
|
+
{ ...res.stats, strict: true }
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log("agentdoc check: PASS " + JSON.stringify({ ...res.stats, observationFreshness: fresh.length, ...(opt("strict") ? { strict: true } : {}) }));
|
|
247
|
+
} else if (cmd === "query") {
|
|
248
|
+
const needle = positional[0];
|
|
249
|
+
if (!needle) { console.error("usage: agentdoc query <repo-path|entity-ref> [--json] [--md] [--budget relations=10,verification=4]"); process.exit(2); }
|
|
250
|
+
const res = compile(ROOT);
|
|
251
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
252
|
+
// Time-dependent gates first, for the same reason as `check`: a stale
|
|
253
|
+
// observation is a different problem from a stale graph and sends the reader
|
|
254
|
+
// somewhere else entirely.
|
|
255
|
+
const preFresh = gateObservationFreshness(res);
|
|
256
|
+
const { graph } = requireFresh(res, preFresh);
|
|
257
|
+
// `--budget` exists because the default budgets are generous enough that a
|
|
258
|
+
// focused query never truncates, which makes the truncation contract
|
|
259
|
+
// untestable and unreachable from the CLI. An agent working in a tight
|
|
260
|
+
// context needs to be able to say so.
|
|
261
|
+
let budget = {};
|
|
262
|
+
const raw = opt("budget");
|
|
263
|
+
if (raw === true || raw === "") {
|
|
264
|
+
console.error("usage: --budget section=limit,... (limits are positive integers)");
|
|
265
|
+
process.exit(2);
|
|
266
|
+
}
|
|
267
|
+
if (raw) {
|
|
268
|
+
for (const pair of raw.split(",")) {
|
|
269
|
+
const [k, v] = pair.split("=");
|
|
270
|
+
if (!k || !/^\d+$/.test(String(v))) {
|
|
271
|
+
console.error("usage: --budget section=limit,... (limits are positive integers)");
|
|
272
|
+
process.exit(2);
|
|
273
|
+
}
|
|
274
|
+
budget[k.trim()] = Number(v);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const ctx = queryContext(graph, needle, { budget });
|
|
278
|
+
if (opt("md")) out(renderContext(ctx));
|
|
279
|
+
else if (opt("json")) out(ctx, true);
|
|
280
|
+
else out(renderContext(ctx));
|
|
281
|
+
} else if (cmd === "impact") {
|
|
282
|
+
// Impact answers a question about the working tree, so it compiles in
|
|
283
|
+
// memory rather than reading the committed graph. Refusing a stale graph
|
|
284
|
+
// would be right for `query` (which feeds a prompt) and wrong here: the
|
|
285
|
+
// whole point of impact is to run *before* you rebuild.
|
|
286
|
+
const res = compile(ROOT);
|
|
287
|
+
if (res.errors.length) fail(res.errors, res.stats);
|
|
288
|
+
const graph = res.graph;
|
|
289
|
+
const fromRef = opt("diff") || opt("from");
|
|
290
|
+
let paths = positional;
|
|
291
|
+
if (fromRef && fromRef !== true) {
|
|
292
|
+
if (!res.repo.hasGit()) {
|
|
293
|
+
console.error("agentdoc impact --diff requires a git repository; this checkout has no VCS");
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
paths = res.repo.diffPaths(fromRef);
|
|
297
|
+
if (paths === null) {
|
|
298
|
+
console.error("cannot resolve diff ref '" + fromRef + "' — is it a valid commit, branch or range?");
|
|
299
|
+
process.exit(2);
|
|
300
|
+
}
|
|
301
|
+
if (!paths.length) {
|
|
302
|
+
console.error("no changed files between " + fromRef + " and HEAD");
|
|
303
|
+
process.exit(2);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!paths.length) {
|
|
307
|
+
console.error("usage: agentdoc impact <path...> | agentdoc impact --diff <git-ref>");
|
|
308
|
+
process.exit(2);
|
|
309
|
+
}
|
|
310
|
+
out(impact(graph, paths), true);
|
|
311
|
+
} else if (cmd === "audit") {
|
|
312
|
+
const res = compile(ROOT);
|
|
313
|
+
if (!res.graph) {
|
|
314
|
+
// An audit must work on a repository that cannot compile yet; report the
|
|
315
|
+
// compilation failures as part of the audit rather than aborting.
|
|
316
|
+
const report = {
|
|
317
|
+
compileErrors: res.errors.map((e) => ({ code: e.code, message: e.message, path: e.path || null, ref: e.ref || null })),
|
|
318
|
+
stats: res.stats,
|
|
319
|
+
};
|
|
320
|
+
out(report, true);
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
const report = audit(res.repo, res.cfg, res);
|
|
324
|
+
report.scaffold = scaffoldProposal(res.repo, res.cfg, report);
|
|
325
|
+
out(report, true);
|
|
326
|
+
} else if (cmd === "scaffold") {
|
|
327
|
+
const res = compile(ROOT);
|
|
328
|
+
const write = opt("write");
|
|
329
|
+
// Scaffolding is how a repository reaches a compiling state: requiring the
|
|
330
|
+
// compile to succeed first deadlocks CREATE on every repository whose units
|
|
331
|
+
// are discovered by adapters (go.work, pnpm-workspace, supplementalRoots)
|
|
332
|
+
// rather than already carrying descriptors. When a usable configuration
|
|
333
|
+
// exists, write what can be proven and report what still blocks the compile.
|
|
334
|
+
if (!res.cfg) {
|
|
335
|
+
out({
|
|
336
|
+
scaffolded: [],
|
|
337
|
+
blocked: res.errors.map((e) => ({ code: e.code, message: e.message })),
|
|
338
|
+
note: "the repository cannot compile yet; install the configuration first with `agentdoc init`",
|
|
339
|
+
}, true);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const report = res.graph ? audit(res.repo, res.cfg, res) : null;
|
|
343
|
+
const proposal = report ? scaffoldProposal(res.repo, res.cfg, report) : null;
|
|
344
|
+
const result = installTemplates(res.repo, res.cfg, {
|
|
345
|
+
write: Boolean(write),
|
|
346
|
+
eligible: res.discovery && res.discovery.eligible,
|
|
347
|
+
});
|
|
348
|
+
out({
|
|
349
|
+
...result,
|
|
350
|
+
...(proposal ? { proposal } : {}),
|
|
351
|
+
...(res.errors.length
|
|
352
|
+
? { blocked: res.errors.map((e) => ({ code: e.code, message: e.message })) }
|
|
353
|
+
: {}),
|
|
354
|
+
}, true);
|
|
355
|
+
} else if (cmd === "init") {
|
|
356
|
+
const result = installTemplates(null, null, { write: true, root: ROOT, force: opt("force") });
|
|
357
|
+
out(result, true);
|
|
358
|
+
} else if (cmd === "eval") {
|
|
359
|
+
const which = positional[0] || "routing";
|
|
360
|
+
if (which !== "routing") { console.error("usage: agentdoc eval routing [--json]"); process.exit(2); }
|
|
361
|
+
// The harness ships, but it is loaded lazily so a trimmed or damaged
|
|
362
|
+
// installation cannot make every other command fail on a module the user
|
|
363
|
+
// never asked for.
|
|
364
|
+
let runRoutingEval;
|
|
365
|
+
try {
|
|
366
|
+
({ runRoutingEval } = await import("../evals/harness.mjs"));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
if (e && e.code === "ERR_MODULE_NOT_FOUND") {
|
|
369
|
+
console.error("agentdoc eval: the evaluation harness is missing from this installation — reinstall the agentdoc CLI, or run it from a source checkout");
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
const report = runRoutingEval(ROOT, { json: opt("json") });
|
|
375
|
+
if (opt("json")) { out(report, true); process.exit(report.verdict === "FAIL" ? 1 : 0); }
|
|
376
|
+
else {
|
|
377
|
+
console.log(JSON.stringify(report.metrics, null, 2));
|
|
378
|
+
console.log("thresholds: " + JSON.stringify(report.thresholds));
|
|
379
|
+
console.log("verdict: " + report.verdict + " (" + report.failures.join("; ") + ")");
|
|
380
|
+
}
|
|
381
|
+
if (report.verdict === "FAIL") process.exit(1);
|
|
382
|
+
} else if (cmd === "pin-evidence") {
|
|
383
|
+
// Reviewed overrides pin their evidence by content hash, and those digests
|
|
384
|
+
// are re-verified on every compile. That is only workable if an author can
|
|
385
|
+
// obtain the current digest without computing SHA-256 by hand — otherwise
|
|
386
|
+
// the discipline quietly becomes "delete the override", which is worse
|
|
387
|
+
// than not requiring it.
|
|
388
|
+
// Read the configuration directly rather than through `compile`: a stale
|
|
389
|
+
// digest is precisely the condition this command exists to report, and it
|
|
390
|
+
// must be reportable when `compile` refuses to run.
|
|
391
|
+
const repo = new Repo(ROOT);
|
|
392
|
+
const cfg = loadConfig(repo, loadSchemaBundle().bundle);
|
|
393
|
+
const results = [];
|
|
394
|
+
for (const o of cfg.reviewedOverrides || []) {
|
|
395
|
+
for (const ev of o.evidence || []) {
|
|
396
|
+
const p = path.join(ROOT, ev.path);
|
|
397
|
+
if (!fs.existsSync(p)) {
|
|
398
|
+
results.push({ subject: o.subject, path: ev.path, current: null, pinned: ev.sha256, status: "MISSING" });
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const current = sha256Hex(fs.readFileSync(p));
|
|
402
|
+
results.push({
|
|
403
|
+
subject: o.subject, path: ev.path, current,
|
|
404
|
+
pinned: ev.sha256,
|
|
405
|
+
status: current === ev.sha256 ? "CURRENT" : "STALE",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (opt("json")) { out({ evidence: results }, true); }
|
|
410
|
+
else {
|
|
411
|
+
for (const r of results) {
|
|
412
|
+
console.log(r.status.padEnd(8) + " " + r.path);
|
|
413
|
+
if (r.status === "STALE") {
|
|
414
|
+
console.log(" pinned: " + r.pinned);
|
|
415
|
+
console.log(" current: " + r.current + " <- update the digest after reviewing the change");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
console.log(results.length + " evidence entries across " + (cfg.reviewedOverrides || []).length + " reviewed overrides");
|
|
419
|
+
}
|
|
420
|
+
if (results.some((r) => r.status !== "CURRENT")) process.exit(1);
|
|
421
|
+
} else if (cmd === "doctor") {
|
|
422
|
+
// Two scopes, kept explicit: `doctor` is a machine check — is this
|
|
423
|
+
// installation of the CLI itself healthy — and never fails because a
|
|
424
|
+
// project happens not to be configured yet. `doctor --project` adds the
|
|
425
|
+
// project checks and fails on those too.
|
|
426
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
427
|
+
const machine = {
|
|
428
|
+
agentdoc: VERSION,
|
|
429
|
+
node: process.version,
|
|
430
|
+
nodeSupported: major >= MIN_NODE_MAJOR,
|
|
431
|
+
nodeRequired: ">=" + MIN_NODE_MAJOR,
|
|
432
|
+
compiler: COMPILER_NAME + "@" + COMPILER_VERSION,
|
|
433
|
+
graphSchema: GRAPH_SCHEMA_VERSION,
|
|
434
|
+
installRoot: PACKAGE_ROOT,
|
|
435
|
+
schemaBundle: null,
|
|
436
|
+
gitAvailable: false,
|
|
437
|
+
externalRuntimeDependencies: [],
|
|
438
|
+
};
|
|
439
|
+
try {
|
|
440
|
+
const { versionInputs } = loadSchemaBundle();
|
|
441
|
+
machine.schemaBundle = { ok: true, schemas: versionInputs.length };
|
|
442
|
+
} catch (e) {
|
|
443
|
+
machine.schemaBundle = { ok: false, error: (e.code ? e.code + ": " : "") + e.message };
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
execFileSync("git", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
447
|
+
machine.gitAvailable = true;
|
|
448
|
+
} catch {
|
|
449
|
+
machine.gitAvailable = false;
|
|
450
|
+
}
|
|
451
|
+
const machineOk = machine.nodeSupported && machine.schemaBundle.ok;
|
|
452
|
+
|
|
453
|
+
const report = { scope: "machine", ok: machineOk, machine };
|
|
454
|
+
if (opt("project")) {
|
|
455
|
+
const configFound = fs.existsSync(path.join(ROOT, "agentdoc", "agentdoc.config.yaml"));
|
|
456
|
+
const project = {
|
|
457
|
+
root: ROOT,
|
|
458
|
+
configurationFound: configFound,
|
|
459
|
+
configPath: "agentdoc/agentdoc.config.yaml",
|
|
460
|
+
writable: null,
|
|
461
|
+
graphPresent: false,
|
|
462
|
+
};
|
|
463
|
+
try {
|
|
464
|
+
fs.accessSync(ROOT, fs.constants.W_OK);
|
|
465
|
+
project.writable = true;
|
|
466
|
+
} catch {
|
|
467
|
+
project.writable = false;
|
|
468
|
+
}
|
|
469
|
+
if (configFound) {
|
|
470
|
+
try {
|
|
471
|
+
const cfg = loadConfig(new Repo(ROOT), loadSchemaBundle().bundle);
|
|
472
|
+
project.graphPath = cfg.output?.graph || ".agentdoc/graph.json";
|
|
473
|
+
project.graphPresent = fs.existsSync(path.join(ROOT, project.graphPath));
|
|
474
|
+
} catch (e) {
|
|
475
|
+
project.configError = (e.code ? e.code + ": " : "") + e.message;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
report.scope = "machine+project";
|
|
479
|
+
report.project = project;
|
|
480
|
+
report.ok = machineOk && configFound && project.writable === true && !project.configError;
|
|
481
|
+
}
|
|
482
|
+
out(report, true);
|
|
483
|
+
if (!report.ok) process.exit(1);
|
|
484
|
+
} else {
|
|
485
|
+
usage(2);
|
|
486
|
+
}
|
|
487
|
+
} catch (e) {
|
|
488
|
+
if (e instanceof AgentDocError) {
|
|
489
|
+
console.error("ERROR " + e.format());
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
console.error("agentdoc internal error: " + (e && e.stack ? e.stack : String(e)));
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
package/bin/commands.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Command and flag declarations.
|
|
2
|
+
//
|
|
3
|
+
// One table drives three things: which flags a command accepts, how they are
|
|
4
|
+
// parsed, and what `agentdoc <command> --help` prints. Parsing and help read
|
|
5
|
+
// the same rows, so they cannot drift. A flag that is not declared for the
|
|
6
|
+
// command it is given to is a usage error, never silently ignored: a
|
|
7
|
+
// misspelled gate flag (`check --strcit`) must not turn into a green,
|
|
8
|
+
// non-strict gate.
|
|
9
|
+
|
|
10
|
+
const JSON_FLAG = { json: { desc: "print the machine-readable report" } };
|
|
11
|
+
// Commands whose output is always JSON accept --json too, as a no-op, so a
|
|
12
|
+
// script that says what it expects is not rejected for being explicit.
|
|
13
|
+
const JSON_ALWAYS = { json: { desc: "accepted for explicitness; the output is always JSON" } };
|
|
14
|
+
|
|
15
|
+
export const COMMANDS = {
|
|
16
|
+
init: {
|
|
17
|
+
synopsis: "agentdoc init [--force]",
|
|
18
|
+
summary: "write a configuration and documentation skeleton",
|
|
19
|
+
flags: { force: { desc: "overwrite files that already exist" }, ...JSON_ALWAYS },
|
|
20
|
+
},
|
|
21
|
+
audit: {
|
|
22
|
+
synopsis: "agentdoc audit",
|
|
23
|
+
summary: "inventory the repository and classify every fact (JSON report)",
|
|
24
|
+
flags: { ...JSON_ALWAYS },
|
|
25
|
+
},
|
|
26
|
+
scaffold: {
|
|
27
|
+
synopsis: "agentdoc scaffold [--write]",
|
|
28
|
+
summary: "generate a CREATE-mode skeleton from the audit (dry run by default)",
|
|
29
|
+
flags: { write: { desc: "write the proposed files instead of listing them" }, ...JSON_ALWAYS },
|
|
30
|
+
},
|
|
31
|
+
validate: {
|
|
32
|
+
synopsis: "agentdoc validate",
|
|
33
|
+
summary: "sources, schemas and semantic checks",
|
|
34
|
+
flags: {},
|
|
35
|
+
},
|
|
36
|
+
compile: {
|
|
37
|
+
synopsis: "agentdoc compile [--require-clean]",
|
|
38
|
+
summary: "deterministic graph build (atomic write)",
|
|
39
|
+
flags: { "require-clean": { desc: "refuse when catalog inputs differ from HEAD" } },
|
|
40
|
+
},
|
|
41
|
+
check: {
|
|
42
|
+
synopsis: "agentdoc check [--strict] [--require-clean] [--allow-stale-observations]",
|
|
43
|
+
summary: "freshness, determinism and gate check",
|
|
44
|
+
flags: {
|
|
45
|
+
strict: { desc: "also fail on every warning not accepted in warningAcceptances (the end-of-MIGRATE gate)" },
|
|
46
|
+
"require-clean": { desc: "refuse a graph built from inputs that differ from HEAD" },
|
|
47
|
+
"allow-stale-observations": { desc: "let observations past maxAgeDays through, applying each rule's degradeOnStale" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
query: {
|
|
51
|
+
synopsis: "agentdoc query <path|ref> [--md] [--json] [--budget section=n,...]",
|
|
52
|
+
summary: "route minimal context for a task",
|
|
53
|
+
flags: {
|
|
54
|
+
md: { desc: "compact markdown context (the default)" },
|
|
55
|
+
...JSON_FLAG,
|
|
56
|
+
budget: { value: "section=n,...", desc: "cap sections, e.g. relations=10,verification=4; truncation is reported" },
|
|
57
|
+
"require-clean": { desc: "refuse a graph built from inputs that differ from HEAD" },
|
|
58
|
+
"allow-stale-observations": { desc: "let observations past maxAgeDays through, applying each rule's degradeOnStale" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
impact: {
|
|
62
|
+
synopsis: "agentdoc impact <paths...> | agentdoc impact --diff <git-ref>",
|
|
63
|
+
summary: "what a change forces you to keep consistent",
|
|
64
|
+
flags: {
|
|
65
|
+
diff: { value: "git-ref", desc: "use the files changed since <git-ref> instead of paths" },
|
|
66
|
+
from: { value: "git-ref", desc: "alias of --diff" },
|
|
67
|
+
...JSON_ALWAYS,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
eval: {
|
|
71
|
+
synopsis: "agentdoc eval routing [--json]",
|
|
72
|
+
summary: "agent-routing evaluation against registered scenarios (SKIP when the repo has none)",
|
|
73
|
+
flags: { ...JSON_FLAG },
|
|
74
|
+
},
|
|
75
|
+
"pin-evidence": {
|
|
76
|
+
synopsis: "agentdoc pin-evidence [--json]",
|
|
77
|
+
summary: "verify digests pinned by reviewed overrides",
|
|
78
|
+
flags: { ...JSON_FLAG },
|
|
79
|
+
},
|
|
80
|
+
doctor: {
|
|
81
|
+
synopsis: "agentdoc doctor [--project]",
|
|
82
|
+
summary: "machine self-check: is this CLI install healthy",
|
|
83
|
+
flags: { project: { desc: "also check the current project's catalog health" }, ...JSON_ALWAYS },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Accepted by every command.
|
|
88
|
+
const COMMON_FLAGS = { help: { desc: "show this help" } };
|
|
89
|
+
|
|
90
|
+
export function flagsOf(cmd) {
|
|
91
|
+
return { ...(COMMANDS[cmd] ? COMMANDS[cmd].flags : {}), ...COMMON_FLAGS };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class UsageError extends Error {}
|
|
95
|
+
|
|
96
|
+
// Parse argv (without the command) against the command's declaration.
|
|
97
|
+
// Returns { flags: Map(name -> true|string), positional }.
|
|
98
|
+
export function parseArgs(cmd, args) {
|
|
99
|
+
const declared = flagsOf(cmd);
|
|
100
|
+
const flags = new Map();
|
|
101
|
+
const positional = [];
|
|
102
|
+
for (let i = 0; i < args.length; i++) {
|
|
103
|
+
const a = args[i];
|
|
104
|
+
if (a === "-h") { flags.set("help", true); continue; }
|
|
105
|
+
if (!a.startsWith("--")) { positional.push(a); continue; }
|
|
106
|
+
const eq = a.indexOf("=");
|
|
107
|
+
const name = a.slice(2, eq < 0 ? undefined : eq);
|
|
108
|
+
const spec = declared[name];
|
|
109
|
+
if (!spec) throw new UsageError(unknownFlagMessage(cmd, name));
|
|
110
|
+
if (!spec.value) {
|
|
111
|
+
if (eq >= 0) throw new UsageError("agentdoc " + cmd + ": --" + name + " does not take a value");
|
|
112
|
+
flags.set(name, true);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let v = eq >= 0 ? a.slice(eq + 1) : args[i + 1];
|
|
116
|
+
if (eq < 0) {
|
|
117
|
+
if (v === undefined || v.startsWith("--")) v = "";
|
|
118
|
+
else i++;
|
|
119
|
+
}
|
|
120
|
+
if (!v) throw new UsageError("agentdoc " + cmd + ": --" + name + " requires a value (" + spec.value + ")");
|
|
121
|
+
flags.set(name, v);
|
|
122
|
+
}
|
|
123
|
+
return { flags, positional };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unknownFlagMessage(cmd, name) {
|
|
127
|
+
const own = Object.keys(flagsOf(cmd));
|
|
128
|
+
const near = own
|
|
129
|
+
.map((f) => [f, distance(f, name)])
|
|
130
|
+
.filter(([f, d]) => d <= Math.max(1, Math.floor(f.length / 3)))
|
|
131
|
+
.sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1))[0];
|
|
132
|
+
const elsewhere = Object.keys(COMMANDS).filter((c) => c !== cmd && COMMANDS[c].flags[name]);
|
|
133
|
+
let hint = "";
|
|
134
|
+
if (near) hint = " (did you mean --" + near[0] + "?)";
|
|
135
|
+
else if (elsewhere.length) hint = " (--" + name + " is accepted by: " + elsewhere.join(", ") + ")";
|
|
136
|
+
return "agentdoc " + cmd + ": unknown flag --" + name + hint +
|
|
137
|
+
"\nrun `agentdoc " + cmd + " --help` for the flags this command accepts";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function distance(a, b) {
|
|
141
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
142
|
+
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
143
|
+
for (let i = 1; i <= a.length; i++) {
|
|
144
|
+
for (let j = 1; j <= b.length; j++) {
|
|
145
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
146
|
+
// Adjacent transposition: `strcit` is one slip away from `strict`.
|
|
147
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return dp[a.length][b.length];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function commandHelp(cmd) {
|
|
154
|
+
const c = COMMANDS[cmd];
|
|
155
|
+
const flags = flagsOf(cmd);
|
|
156
|
+
const rows = Object.entries(flags).map(([name, spec]) => ["--" + name + (spec.value ? " <" + spec.value + ">" : ""), spec.desc]);
|
|
157
|
+
const width = Math.max(...rows.map((r) => r[0].length));
|
|
158
|
+
return [
|
|
159
|
+
"usage: " + c.synopsis,
|
|
160
|
+
"",
|
|
161
|
+
c.summary,
|
|
162
|
+
"",
|
|
163
|
+
"flags:",
|
|
164
|
+
...rows.map(([f, d]) => " " + f.padEnd(width) + " " + d),
|
|
165
|
+
"",
|
|
166
|
+
"Exit codes: 0 ok, 1 failure, 2 usage.",
|
|
167
|
+
"",
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|