@handong66/evidoc-cli 0.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.
@@ -0,0 +1,1615 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
4
+ import { access, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { promisify } from "node:util";
9
+ import { checkRepositories, checkRepository, createAgentRuntimeContract, detectRepositoryEvidocWorkflowText, evidocWorkflowWarnings, fingerprintFileContent, readFindingDocuments, readRepository, resolveExistingPathInsideRoot } from "@handong66/evidoc-core";
10
+ import { renderDashboardHtml } from "@handong66/evidoc-dashboard";
11
+ import { buildDriftGraph } from "@handong66/evidoc-graph";
12
+ import { runGithubAction } from "@handong66/evidoc-github-action";
13
+ import { createDefaultEvidocConfig, defaultGithubWorkflow, detectRepositoryDefaultBranch, detectRepositoryPushBranches, enableLocalGitGate, ensureLocalHistoryGitignore, inspectLocalGitGate, scanLocalAppRepositories, scaffoldRepository, startLocalAppServer } from "@handong66/evidoc-local-app";
14
+ import { listMcpTools } from "@handong66/evidoc-mcp-server";
15
+ import { applyPatchProposal, createPatchProposals, validatePatchProposal } from "@handong66/evidoc-patcher";
16
+ import { formatTextReport } from "@handong66/evidoc-reports";
17
+ const execFileAsync = promisify(execFile);
18
+ export async function runCli(args, io = {}) {
19
+ const resolved = {
20
+ cwd: io.cwd ?? process.cwd(),
21
+ stdout: io.stdout ?? ((chunk) => process.stdout.write(chunk)),
22
+ stderr: io.stderr ?? ((chunk) => process.stderr.write(chunk))
23
+ };
24
+ const defaultMode = resolveDefaultMode(args, io);
25
+ const parsed = parseArgs(args, defaultMode.command);
26
+ if (parsed.unknownOptions && parsed.unknownOptions.length > 0) {
27
+ resolved.stderr(`Unknown option: ${parsed.unknownOptions.join(", ")}\n\n${helpText()}`);
28
+ return 2;
29
+ }
30
+ const targetRoot = parsed.roots.length > 0 && parsed.command !== "app" && parsed.command !== "serve" && parsed.command !== "multi"
31
+ ? resolve(resolved.cwd, parsed.roots[0])
32
+ : resolved.cwd;
33
+ if ((parsed.command === "app" || parsed.command === "serve") && defaultMode.once) {
34
+ parsed.once = true;
35
+ parsed.noOpen = true;
36
+ }
37
+ if (parsed.command === "help") {
38
+ resolved.stdout(helpText());
39
+ return 0;
40
+ }
41
+ try {
42
+ if (parsed.command === "app" || parsed.command === "serve") {
43
+ const roots = parsed.roots.length > 0 ? parsed.roots : [resolved.cwd];
44
+ if (parsed.once) {
45
+ const state = await scanLocalAppRepositories(roots, { autoInit: true, writeHistory: true });
46
+ resolved.stdout(parsed.json ? `${JSON.stringify(state, null, 2)}\n` : formatLocalAppText(state));
47
+ return shouldFail({
48
+ broken: state.summary.broken,
49
+ reviewNeeded: state.summary.reviewNeeded
50
+ }, parsed.failOn)
51
+ ? 1
52
+ : 0;
53
+ }
54
+ const app = await startLocalAppServer({
55
+ roots,
56
+ port: parsed.port,
57
+ openBrowser: !parsed.noOpen,
58
+ watch: parsed.watch,
59
+ autoInit: true,
60
+ writeHistory: true
61
+ });
62
+ resolved.stdout(`Evidoc Local App: ${app.url}\n`);
63
+ return 0;
64
+ }
65
+ if (parsed.command === "demo") {
66
+ const demoRoot = await createDemoRepository();
67
+ if (parsed.once || parsed.json) {
68
+ const state = await scanLocalAppRepositories([demoRoot], { autoInit: true, writeHistory: true });
69
+ resolved.stdout(parsed.json ? `${JSON.stringify(state, null, 2)}\n` : formatLocalAppText(state));
70
+ return 0;
71
+ }
72
+ const app = await startLocalAppServer({
73
+ roots: [demoRoot],
74
+ port: parsed.port,
75
+ openBrowser: !parsed.noOpen,
76
+ watch: false,
77
+ autoInit: true,
78
+ writeHistory: true
79
+ });
80
+ resolved.stdout(`Evidoc demo app: ${app.url}\n`);
81
+ return 0;
82
+ }
83
+ if (parsed.command === "multi") {
84
+ const roots = parsed.roots.length > 0 ? parsed.roots : [resolved.cwd];
85
+ const aggregate = await checkRepositories(roots);
86
+ resolved.stdout(parsed.json ? `${JSON.stringify(aggregate, null, 2)}\n` : formatMultiText(aggregate));
87
+ return shouldFail(aggregate.summary, parsed.failOn) ? 1 : 0;
88
+ }
89
+ if (parsed.command === "init") {
90
+ const result = await initializeRepository(targetRoot, {
91
+ force: parsed.force,
92
+ githubAction: !parsed.noAction && !parsed.localGit,
93
+ localGit: parsed.localGit,
94
+ installHooks: parsed.installHooks,
95
+ withFeatures: parsed.withFeatures
96
+ });
97
+ resolved.stdout(formatInitResult(result, targetRoot));
98
+ return 0;
99
+ }
100
+ if (parsed.command === "doctor") {
101
+ const result = await inspectOnboarding(targetRoot);
102
+ resolved.stdout(formatDoctorResult(result, targetRoot));
103
+ return result.ready ? 0 : 1;
104
+ }
105
+ if (parsed.command === "guard") {
106
+ const event = parseGuardEvent(parsed.event);
107
+ const failOn = resolveGuardFailOn(parsed.failOn, event, parsed.failOnExplicit);
108
+ const result = await runLocalGitGuard(targetRoot, {
109
+ event,
110
+ scope: parseGuardScope(parsed.scope, event),
111
+ since: parsed.since,
112
+ failOn
113
+ });
114
+ resolved.stdout(parsed.json ? `${JSON.stringify(result, null, 2)}\n` : formatLocalGitGuardText(result));
115
+ return shouldFail(result.report.summary, failOn) ? 1 : 0;
116
+ }
117
+ if (parsed.command === "verify") {
118
+ if (!parsed.instructions) {
119
+ resolved.stderr("verify currently requires --instructions.\n");
120
+ return 1;
121
+ }
122
+ const report = await checkRepository(targetRoot);
123
+ const verification = createInstructionVerificationReport(report);
124
+ resolved.stdout(parsed.json ? `${JSON.stringify(verification, null, 2)}\n` : formatInstructionVerificationText(verification));
125
+ return shouldFail(verification.summary, parsed.failOn) ? 1 : 0;
126
+ }
127
+ if (parsed.command === "agent-eval") {
128
+ const report = await checkRepository(targetRoot);
129
+ const documents = await readFindingDocuments(targetRoot, report);
130
+ const proposals = createPatchProposals(report, documents);
131
+ const evaluation = createAgentEvaluationReport(report, proposals);
132
+ resolved.stdout(parsed.json ? `${JSON.stringify(evaluation, null, 2)}\n` : formatAgentEvaluationText(evaluation));
133
+ return 0;
134
+ }
135
+ if (parsed.command === "graph") {
136
+ const report = await checkRepository(targetRoot);
137
+ const graph = buildDriftGraph(report);
138
+ resolved.stdout(`${JSON.stringify(graph, null, 2)}\n`);
139
+ return 0;
140
+ }
141
+ if (parsed.command === "index") {
142
+ const snapshot = await readRepository(targetRoot);
143
+ resolved.stdout(`${JSON.stringify({
144
+ root: snapshot.root,
145
+ files: [...snapshot.files],
146
+ documents: snapshot.documents.map(({ path, kind, lineCount }) => ({
147
+ path,
148
+ kind,
149
+ lineCount
150
+ })),
151
+ expectedPackageManager: snapshot.expectedPackageManager,
152
+ apiOperations: snapshot.apiOperations
153
+ }, null, 2)}\n`);
154
+ return 0;
155
+ }
156
+ if (parsed.command === "dashboard") {
157
+ const report = await checkRepository(targetRoot);
158
+ const html = renderDashboardHtml(report);
159
+ if (parsed.out) {
160
+ await mkdir(dirname(parsed.out), { recursive: true });
161
+ await writeFile(parsed.out, html, "utf8");
162
+ resolved.stdout(`Wrote ${parsed.out}\n`);
163
+ }
164
+ else {
165
+ resolved.stdout(html);
166
+ }
167
+ return shouldFail(report.summary, parsed.failOn) ? 1 : 0;
168
+ }
169
+ if (parsed.command === "diagnose") {
170
+ const report = await checkRepository(targetRoot);
171
+ const documents = await readFindingDocuments(targetRoot, report);
172
+ const proposals = createPatchProposals(report, documents);
173
+ const diagnosis = createDiagnosis(report, proposals);
174
+ resolved.stdout(parsed.json ? `${JSON.stringify(diagnosis, null, 2)}\n` : formatDiagnosisText(diagnosis));
175
+ return 0;
176
+ }
177
+ if (parsed.command === "diff") {
178
+ const diff = await createDiffImpactReport(targetRoot, parsed.since);
179
+ resolved.stdout(parsed.json ? `${JSON.stringify(diff, null, 2)}\n` : formatDiffText(diff));
180
+ return shouldFail(diff.report.summary, parsed.failOn) ? 1 : 0;
181
+ }
182
+ if (parsed.command === "draft") {
183
+ const report = await checkRepository(targetRoot);
184
+ const documents = await readFindingDocuments(targetRoot, report);
185
+ const proposals = createPatchProposals(report, documents);
186
+ resolved.stdout(`${JSON.stringify(proposals, null, 2)}\n`);
187
+ return 0;
188
+ }
189
+ if (parsed.command === "fix") {
190
+ if (parsed.write && !parsed.safeOnly) {
191
+ resolved.stderr("fix --write requires --safe because only deterministic safe fixes may be applied automatically.\n");
192
+ return 1;
193
+ }
194
+ const report = await checkRepository(targetRoot);
195
+ const documents = await readFindingDocuments(targetRoot, report);
196
+ const allProposals = createPatchProposals(report, documents);
197
+ const proposals = allProposals.filter((proposal) => parsed.safeOnly ? proposal.classification === "safe" : proposal.classification !== "blocked");
198
+ if (!parsed.write) {
199
+ const preview = { mode: "preview", proposals };
200
+ resolved.stdout(parsed.json ? `${JSON.stringify(preview, null, 2)}\n` : formatFixText(preview));
201
+ return 0;
202
+ }
203
+ const applied = [];
204
+ const appliedFindingIds = new Set();
205
+ const maxAttempts = Math.max(100, report.findings.length * 2 + 10);
206
+ let truncated = false;
207
+ for (let attempts = 0; attempts < maxAttempts; attempts += 1) {
208
+ const currentReport = await checkRepository(targetRoot);
209
+ const currentDocuments = await readFindingDocuments(targetRoot, currentReport);
210
+ const currentProposals = createPatchProposals(currentReport, currentDocuments).filter((proposal) => parsed.safeOnly ? proposal.classification === "safe" : proposal.classification !== "blocked");
211
+ const nextSafeProposal = currentProposals.find((proposal) => {
212
+ return proposal.classification === "safe" && !appliedFindingIds.has(proposal.findingId);
213
+ });
214
+ if (!nextSafeProposal)
215
+ break;
216
+ applied.push(await applyPatchProposal(targetRoot, nextSafeProposal, { allowWrites: true }));
217
+ appliedFindingIds.add(nextSafeProposal.findingId);
218
+ truncated = attempts + 1 >= maxAttempts;
219
+ }
220
+ if (truncated) {
221
+ resolved.stderr(`Evidoc stopped after ${maxAttempts} safe fix attempts; re-run fix to continue.\n`);
222
+ }
223
+ const finalReport = await checkRepository(targetRoot);
224
+ const finalDocuments = await readFindingDocuments(targetRoot, finalReport);
225
+ const skipped = createPatchProposals(finalReport, finalDocuments).filter((proposal) => proposal.classification !== "safe").length;
226
+ const result = { mode: "write", applied, skipped, truncated, postFixSummary: finalReport.summary };
227
+ resolved.stdout(parsed.json ? `${JSON.stringify(result, null, 2)}\n` : formatFixText(result));
228
+ return 0;
229
+ }
230
+ if (parsed.command === "explain") {
231
+ const report = await checkRepository(targetRoot);
232
+ const selector = parsed.operands[0] ?? "0";
233
+ const finding = /^\d+$/.test(selector)
234
+ ? report.findings[Number.parseInt(selector, 10)]
235
+ : report.findings.find((candidate) => candidate.id === selector);
236
+ if (!finding) {
237
+ resolved.stderr(`No Evidoc finding matched ${selector}.\n`);
238
+ return 1;
239
+ }
240
+ resolved.stdout(parsed.json ? `${JSON.stringify(finding, null, 2)}\n` : `${finding.message}\n`);
241
+ return 0;
242
+ }
243
+ if (parsed.command === "validate") {
244
+ if (!parsed.proposal) {
245
+ resolved.stderr("validate requires --proposal path.\n");
246
+ return 1;
247
+ }
248
+ const report = await checkRepository(targetRoot);
249
+ const proposalInput = JSON.parse(await readFile(parsed.proposal, "utf8"));
250
+ const validation = Array.isArray(proposalInput)
251
+ ? validatePatchProposals(proposalInput, report)
252
+ : validatePatchProposal(proposalInput, report);
253
+ resolved.stdout(parsed.json
254
+ ? `${JSON.stringify(validation, null, 2)}\n`
255
+ : validation.ok
256
+ ? "patch proposal is valid\n"
257
+ : `${validation.errors.join("\n")}\n`);
258
+ return validation.ok ? 0 : 1;
259
+ }
260
+ if (parsed.command === "mcp-tools") {
261
+ resolved.stdout(`${JSON.stringify(listMcpTools(), null, 2)}\n`);
262
+ return 0;
263
+ }
264
+ if (parsed.command === "recipes") {
265
+ const recipes = createCiRecipes(parsed.target ?? "all");
266
+ resolved.stdout(parsed.json ? `${JSON.stringify(recipes, null, 2)}\n` : formatCiRecipes(recipes));
267
+ return 0;
268
+ }
269
+ if (parsed.command === "action") {
270
+ const changedFiles = parsed.changedOnly ? await readChangedScanFiles(targetRoot, parsed.since) : undefined;
271
+ const result = await runGithubAction({ cwd: targetRoot, failOn: parsed.failOn, changedFiles });
272
+ await writeOptional(parsed.summary, result.prComment);
273
+ await writeOptional(parsed.sarif, result.sarif);
274
+ await writeOptional(parsed.annotations, result.annotations);
275
+ await writeOptional(parsed.result, JSON.stringify(result.report, null, 2));
276
+ if (!parsed.annotations && result.annotations) {
277
+ resolved.stdout(`${result.annotations}\n`);
278
+ }
279
+ resolved.stdout(result.markdownSummary);
280
+ return result.exitCode;
281
+ }
282
+ const changedImpact = parsed.changedOnly ? await createChangedImpact(targetRoot, parsed.since) : undefined;
283
+ const report = await checkRepository(targetRoot, {
284
+ changedFiles: changedImpact?.affectedDocuments,
285
+ changedSourceFiles: changedImpact?.changedFiles,
286
+ undocumentedChangedFiles: changedImpact?.undocumentedChangedFiles
287
+ });
288
+ resolved.stdout(parsed.json ? `${JSON.stringify(report, null, 2)}\n` : formatTextReport(report));
289
+ return shouldFail(report.summary, parsed.failOn) ? 1 : 0;
290
+ }
291
+ catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ resolved.stderr(`Evidoc failed: ${message}\n`);
294
+ return 1;
295
+ }
296
+ }
297
+ function resolveDefaultMode(args, io) {
298
+ if (io.forceApp)
299
+ return { command: "app", once: false };
300
+ if (args.some((arg) => isCommand(arg)))
301
+ return { command: "check", once: false };
302
+ if (args.some(isLocalAppFlag))
303
+ return { command: "app", once: false };
304
+ if (args.length > 0)
305
+ return { command: "check", once: false };
306
+ if (io.stdout === undefined && process.stdout.isTTY)
307
+ return { command: "app", once: false };
308
+ return { command: "app", once: true };
309
+ }
310
+ function isLocalAppFlag(arg) {
311
+ return (arg === "--once" ||
312
+ arg === "--no-open" ||
313
+ arg === "--watch" ||
314
+ arg === "--port" ||
315
+ arg.startsWith("--port="));
316
+ }
317
+ function parseArgs(args, defaultCommand = "check") {
318
+ if (args.includes("--help") || args.includes("-h")) {
319
+ return {
320
+ command: "help",
321
+ json: false,
322
+ failOn: "none",
323
+ failOnExplicit: false,
324
+ changedOnly: false,
325
+ force: false,
326
+ instructions: false,
327
+ installHooks: false,
328
+ localGit: false,
329
+ noAction: false,
330
+ noOpen: false,
331
+ once: false,
332
+ safeOnly: false,
333
+ yes: false,
334
+ watch: false,
335
+ write: false,
336
+ operands: [],
337
+ roots: [],
338
+ withFeatures: []
339
+ };
340
+ }
341
+ const first = args.find((arg) => !arg.startsWith("-"));
342
+ const command = isCommand(first) ? first : defaultCommand;
343
+ const normalized = normalizeOptionAliases(args).filter((arg) => arg !== command && arg !== "--worktree");
344
+ return {
345
+ command,
346
+ json: normalized.includes("--json"),
347
+ failOn: parseFailOn(normalized),
348
+ failOnExplicit: hasFailOnOption(normalized),
349
+ format: readOption(normalized, "--format"),
350
+ out: readOption(normalized, "--out"),
351
+ port: parsePort(readOption(normalized, "--port")),
352
+ proposal: readOption(normalized, "--proposal"),
353
+ result: readOption(normalized, "--result"),
354
+ sarif: readOption(normalized, "--sarif"),
355
+ since: readOption(normalized, "--since"),
356
+ event: readOption(normalized, "--event"),
357
+ scope: readOption(normalized, "--scope"),
358
+ target: readOption(normalized, "--target"),
359
+ summary: readOption(normalized, "--summary"),
360
+ annotations: readOption(normalized, "--annotations"),
361
+ changedOnly: normalized.includes("--changed-only"),
362
+ force: normalized.includes("--force"),
363
+ instructions: normalized.includes("--instructions"),
364
+ installHooks: normalized.includes("--install-hooks"),
365
+ localGit: normalized.includes("--local-git"),
366
+ noAction: normalized.includes("--no-action"),
367
+ noOpen: normalized.includes("--no-open"),
368
+ once: normalized.includes("--once"),
369
+ safeOnly: normalized.includes("--safe"),
370
+ yes: normalized.includes("--yes"),
371
+ watch: normalized.includes("--watch"),
372
+ write: normalized.includes("--write"),
373
+ operands: readOperands(normalized),
374
+ roots: readRepeatedOption(normalized, "--root"),
375
+ unknownOptions: findUnknownOptions(normalized),
376
+ withFeatures: parseScaffoldFeatures(readRepeatedOption(normalized, "--with"))
377
+ };
378
+ }
379
+ function normalizeOptionAliases(args) {
380
+ return args.map((arg) => {
381
+ if (arg === "--pr-comment-file")
382
+ return "--summary";
383
+ if (arg.startsWith("--pr-comment-file="))
384
+ return `--summary=${arg.slice("--pr-comment-file=".length)}`;
385
+ if (arg === "--annotations-file")
386
+ return "--annotations";
387
+ if (arg.startsWith("--annotations-file="))
388
+ return `--annotations=${arg.slice("--annotations-file=".length)}`;
389
+ return arg;
390
+ });
391
+ }
392
+ function findUnknownOptions(args) {
393
+ const knownBooleanFlags = new Set([
394
+ "--changed-only",
395
+ "--force",
396
+ "--instructions",
397
+ "--install-hooks",
398
+ "--json",
399
+ "--local-git",
400
+ "--no-action",
401
+ "--no-open",
402
+ "--once",
403
+ "--safe",
404
+ "--watch",
405
+ "--worktree",
406
+ "--write",
407
+ "--yes"
408
+ ]);
409
+ const knownValueFlags = new Set([
410
+ "--annotations",
411
+ "--fail-on",
412
+ "--event",
413
+ "--format",
414
+ "--out",
415
+ "--port",
416
+ "--proposal",
417
+ "--result",
418
+ "--root",
419
+ "--sarif",
420
+ "--since",
421
+ "--scope",
422
+ "--summary",
423
+ "--target",
424
+ "--with"
425
+ ]);
426
+ const unknown = [];
427
+ for (const arg of args) {
428
+ if (!arg.startsWith("--"))
429
+ continue;
430
+ const flag = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
431
+ if (!knownBooleanFlags.has(flag) && !knownValueFlags.has(flag)) {
432
+ unknown.push(flag);
433
+ }
434
+ }
435
+ return [...new Set(unknown)];
436
+ }
437
+ function parseFailOn(args) {
438
+ const inline = args.find((arg) => arg.startsWith("--fail-on="));
439
+ const value = inline?.slice("--fail-on=".length);
440
+ if (value === "broken" || value === "review_needed" || value === "none") {
441
+ return value;
442
+ }
443
+ const flagIndex = args.indexOf("--fail-on");
444
+ const next = flagIndex >= 0 ? args[flagIndex + 1] : undefined;
445
+ if (next === "broken" || next === "review_needed" || next === "none") {
446
+ return next;
447
+ }
448
+ return "none";
449
+ }
450
+ function hasFailOnOption(args) {
451
+ return args.some((arg) => arg === "--fail-on" || arg.startsWith("--fail-on="));
452
+ }
453
+ function resolveGuardFailOn(failOn, event, explicit) {
454
+ if (explicit || event === "manual")
455
+ return failOn;
456
+ return "review_needed";
457
+ }
458
+ function parsePort(value) {
459
+ if (value === undefined)
460
+ return undefined;
461
+ const port = Number.parseInt(value, 10);
462
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
463
+ return undefined;
464
+ return port;
465
+ }
466
+ function shouldFail(summary, failOn) {
467
+ if (failOn === "none")
468
+ return false;
469
+ if (failOn === "broken")
470
+ return summary.broken > 0;
471
+ return summary.broken > 0 || summary.reviewNeeded > 0;
472
+ }
473
+ function helpText() {
474
+ return `Evidoc
475
+
476
+ Usage:
477
+ evidoc agent-eval [--root path] [--json]
478
+ evidoc init --yes [--root path] [--force] [--no-action] [--local-git] [--install-hooks] [--with features]
479
+ evidoc
480
+ evidoc app [--root path...] [--port 4321] [--watch] [--no-open] [--fail-on=none|broken|review_needed]
481
+ evidoc serve [--root path...] [--port 4321] [--watch] [--no-open] [--fail-on=none|broken|review_needed]
482
+ evidoc doctor [--root path]
483
+ evidoc demo [--json] [--once] [--no-open]
484
+ evidoc check [--root path] [--json] [--worktree] [--fail-on=none|broken|review_needed]
485
+ evidoc diagnose [--root path] [--json]
486
+ evidoc diff [--root path] [--since ref] [--json] [--fail-on=none|broken|review_needed]
487
+ evidoc fix [--root path] [--safe] [--write] [--json]
488
+ evidoc guard [--root path] [--event pre-commit|pre-push|manual] [--scope staged|worktree] [--since ref] [--json] [--fail-on=none|broken|review_needed]
489
+ evidoc verify --instructions [--root path] [--json] [--fail-on=none|broken|review_needed]
490
+ evidoc index [--root path] --json
491
+ evidoc explain <finding-id-or-index> [--root path] [--json]
492
+ evidoc graph [--root path] --json
493
+ evidoc dashboard [--root path] [--out path]
494
+ evidoc draft [--root path] --json
495
+ evidoc validate --proposal path [--json]
496
+ evidoc multi --root path --root path [--json]
497
+ evidoc recipes [--target all|gitlab|jenkins|gitea|buildkite] [--json]
498
+ evidoc mcp-tools
499
+ evidoc action [--format=github] [--fail-on=none|broken|review_needed] [--changed-only] [--since ref] [--sarif path] [--summary path] [--annotations path] [--result path]
500
+
501
+ Commands:
502
+ agent-eval print the agent A/B benchmark and coverage pack for Codex, Claude Code, and OpenCode
503
+ evidoc one-click local app; non-interactive runs a one-shot app scan
504
+ app auto-init, scan, start the local Web GUI, and open the browser
505
+ serve explicit local GUI server entry; supports multiple --root values
506
+ init write a minimal .evidoc config and GitHub Action workflow
507
+ demo open a built-in sample in the local GUI without touching the current repository
508
+ doctor check whether this repository is ready to run Evidoc
509
+ check scan the current repository for documentation drift evidence
510
+ diagnose convert findings into AI-actionable repair prompts
511
+ diff report changed files, affected docs, and drift findings since a git ref
512
+ index print a machine-readable repository index
513
+ explain print one finding with its evidence
514
+ fix apply or preview safe deterministic documentation fixes
515
+ guard run the local Git documentation-drift gate and write local reports
516
+ hook events default to --fail-on=review_needed unless --fail-on is explicit
517
+ verify run targeted verification surfaces such as agent instruction checks
518
+ graph print the document/source/rule graph as JSON
519
+ dashboard render a self-contained HTML dashboard
520
+ draft print evidence-bound patch proposals
521
+ validate validate a patch proposal against current evidence
522
+ multi scan multiple repositories and aggregate results
523
+ recipes print local/inner-source CI recipes that call the Evidoc CLI
524
+ mcp-tools list Evidoc MCP tools
525
+ action run the same scanner contract used by the GitHub Action
526
+
527
+ Options:
528
+ --json print structured JSON where supported
529
+ --worktree accepted alias for current working tree scan
530
+ --fail-on choose exit-code policy for CI usage
531
+ --format accepted compatibility flag for copied action commands; current action output is GitHub-oriented
532
+ --once auto-init, scan, record app state, and exit without keeping the GUI server alive
533
+ --no-open start the local app without opening a browser
534
+ --port choose local app port; use 0 for a random free port
535
+ --root target repository path; single-repository commands use the first value, app/serve/multi support multiple values
536
+ --watch keep refreshing local app scan state while the server is running
537
+ --yes run init non-interactively; accepted for copy-paste setup
538
+ --force replace generated init files
539
+ --local-git initialize local Git hooks instead of a GitHub Actions workflow
540
+ --install-hooks set repo-local git config core.hooksPath=.githooks
541
+ --with comma-separated scaffolders for init: agents,hooks,ci,badge,llms
542
+ --event local Git guard event: pre-commit, pre-push, or manual
543
+ --scope local Git guard changed-file scope: staged or worktree
544
+ --target recipes target: all, gitlab, jenkins, gitea, or buildkite
545
+ --instructions run verify against AGENTS/CLAUDE/Cursor/Copilot instruction files
546
+ --changed-only scan changed Markdown documents plus docs affected by changed source, API, or package-manager evidence
547
+ --since git ref used by diff and --changed-only
548
+ --safe restrict fix to deterministic safe proposals
549
+ --write allow fix to write files
550
+ --no-action skip GitHub Action workflow generation during init
551
+ `;
552
+ }
553
+ async function initializeRepository(root, options) {
554
+ const configPath = ".evidoc/config.json";
555
+ const workflowPath = ".github/workflows/evidoc.yml";
556
+ const config = await createDefaultEvidocConfig(root);
557
+ const result = {
558
+ config: await writeGeneratedFile(root, configPath, `${JSON.stringify(config, null, 2)}\n`, options.force),
559
+ historyIgnore: await ensureLocalHistoryGitignore(root),
560
+ scaffolds: []
561
+ };
562
+ if (options.githubAction) {
563
+ result.workflow = await writeGeneratedFile(root, workflowPath, defaultGithubWorkflow({ pushBranches: await detectRepositoryPushBranches(root) }), options.force);
564
+ }
565
+ if (options.localGit) {
566
+ result.localGit = await enableLocalGitGate(root, {
567
+ force: options.force,
568
+ installHooks: options.installHooks
569
+ });
570
+ }
571
+ if (options.withFeatures.length > 0) {
572
+ result.scaffolds = await scaffoldRepository(root, {
573
+ features: options.withFeatures,
574
+ force: options.force
575
+ });
576
+ }
577
+ return result;
578
+ }
579
+ async function writeGeneratedFile(root, path, content, force) {
580
+ const target = join(root, path);
581
+ const alreadyExists = await exists(root, path);
582
+ if (alreadyExists && !force) {
583
+ return { path, status: "kept" };
584
+ }
585
+ await mkdir(dirname(target), { recursive: true });
586
+ await writeFile(target, content, "utf8");
587
+ return { path, status: alreadyExists ? "updated" : "created" };
588
+ }
589
+ async function inspectOnboarding(root) {
590
+ const config = await inspectConfig(root);
591
+ const workflow = await findEvidocWorkflow(root);
592
+ const localGit = await inspectLocalGitGate(root);
593
+ const issues = [...config.issues];
594
+ const hasGate = workflow !== undefined || localGit.ready;
595
+ return {
596
+ ready: config.exists && config.valid && hasGate && issues.length === 0,
597
+ configExists: config.exists,
598
+ configValid: config.valid,
599
+ workflowExists: workflow !== undefined,
600
+ workflowPath: workflow?.path,
601
+ localGit,
602
+ agentSurfaces: await detectAgentSurfaces(root),
603
+ issues,
604
+ warnings: workflow?.warnings ?? []
605
+ };
606
+ }
607
+ function formatInitResult(result, root) {
608
+ const lines = [
609
+ "Evidoc initialized",
610
+ "",
611
+ formatFileWrite("config", result.config)
612
+ ];
613
+ lines.push(formatFileWrite("local history ignore", result.historyIgnore));
614
+ if (result.workflow) {
615
+ lines.push(formatFileWrite("GitHub Action", result.workflow));
616
+ }
617
+ else {
618
+ lines.push("GitHub Action: skipped");
619
+ }
620
+ if (result.localGit) {
621
+ const statuses = result.localGit.files.map((file) => `${file.status} ${file.path}`).join(", ");
622
+ lines.push(`Local Git Gate: ${statuses}`);
623
+ lines.push(result.localGit.installed
624
+ ? "Local Git hooksPath: installed .githooks"
625
+ : "Local Git hooksPath: run `git config core.hooksPath .githooks` to activate hooks");
626
+ }
627
+ for (const scaffold of result.scaffolds) {
628
+ for (const file of scaffold.files) {
629
+ lines.push(formatFileWrite(`scaffold ${scaffold.feature}`, file));
630
+ }
631
+ }
632
+ const addTargets = [
633
+ result.config.path,
634
+ result.historyIgnore.path,
635
+ result.workflow?.path,
636
+ ...(result.localGit?.files.map((file) => file.path) ?? []),
637
+ ...result.scaffolds.flatMap((scaffold) => scaffold.files.map((file) => file.path))
638
+ ].filter((path) => path !== undefined);
639
+ lines.push("", "Next:", result.localGit
640
+ ? " git config core.hooksPath .githooks"
641
+ : ` npx evidoc check --root ${quoteShellArg(root)} --fail-on=review_needed`, result.localGit
642
+ ? " npx evidoc guard --event pre-commit --root " + quoteShellArg(root)
643
+ : "", "Source checkout fallback:", result.localGit
644
+ ? " npm run evidoc -- guard --event pre-commit --root " + quoteShellArg(root)
645
+ : ` npm run evidoc -- check --root ${quoteShellArg(root)} --fail-on=review_needed`, ` git add ${[...new Set(addTargets)].join(" ")}`);
646
+ return `${lines.join("\n")}\n`;
647
+ }
648
+ function formatFileWrite(label, result) {
649
+ if (result.status === "kept")
650
+ return `${label}: kept existing ${result.path}`;
651
+ return `${label}: ${result.status} ${result.path}`;
652
+ }
653
+ function formatDoctorResult(result, root) {
654
+ const status = result.ready
655
+ ? "ready"
656
+ : !result.configExists && !result.workflowExists && !result.localGit.ready
657
+ ? "not initialized"
658
+ : "not ready";
659
+ const lines = [
660
+ "Evidoc doctor",
661
+ "",
662
+ `status: ${status}`,
663
+ `config: ${formatConfigStatus(result)}`,
664
+ `GitHub Action: ${result.workflowExists ? `found ${sanitizeCliWarningText(result.workflowPath ?? "")}` : "missing .github/workflows/evidoc.yml"}`,
665
+ `Local Git Gate: ${formatLocalGitGateStatus(result.localGit)}`,
666
+ `Agent surfaces: ${result.agentSurfaces.length > 0 ? result.agentSurfaces.join(", ") : "none detected"}`,
667
+ "MCP default: evidoc.agent_scan",
668
+ "MCP status: evidoc.get_drift_status"
669
+ ];
670
+ if (result.issues.length > 0) {
671
+ lines.push("", "Issues:", ...result.issues.map((issue) => ` - ${issue}`));
672
+ }
673
+ if (result.warnings.length > 0) {
674
+ lines.push("", "Warnings:", ...result.warnings.map((warning) => ` - ${sanitizeCliWarningText(warning)}`));
675
+ }
676
+ const localGitIssues = result.localGit.issues ?? [];
677
+ if (shouldShowLocalGitGateIssues(result)) {
678
+ lines.push("", "Local Git Gate issues:", ...localGitIssues.map((issue) => ` - ${issue}`));
679
+ }
680
+ if (!result.ready) {
681
+ lines.push("", "Fix:", ` npx evidoc init --yes --root ${quoteShellArg(root)}`, ` npx evidoc init --yes --local-git --install-hooks --root ${quoteShellArg(root)}`, "Source checkout fallback:", ` npm run evidoc -- init --yes --root ${quoteShellArg(root)}`);
682
+ }
683
+ else {
684
+ lines.push("", "Next:", ` npx evidoc check --root ${quoteShellArg(root)} --fail-on=review_needed`, "Source checkout fallback:", ` npm run evidoc -- check --root ${quoteShellArg(root)} --fail-on=review_needed`);
685
+ }
686
+ return `${lines.join("\n")}\n`;
687
+ }
688
+ function formatLocalGitGateStatus(localGit) {
689
+ if (!localGit.isRepository)
690
+ return "not a git repository";
691
+ if (localGit.ready)
692
+ return "ready";
693
+ return "not ready";
694
+ }
695
+ function shouldShowLocalGitGateIssues(result) {
696
+ if (!result.localGit.issues || result.localGit.issues.length === 0)
697
+ return false;
698
+ if (!result.workflowExists)
699
+ return true;
700
+ return Boolean(result.localGit.hooksPath || result.localGit.preCommitHook || result.localGit.prePushHook);
701
+ }
702
+ function sanitizeCliWarningText(value) {
703
+ return value
704
+ .replaceAll("```", "` ` `")
705
+ .replace(/[\r\n]+/g, " ")
706
+ .replace(/([\\[\]()<>])/g, "\\$1");
707
+ }
708
+ function quoteShellArg(value) {
709
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
710
+ return value;
711
+ return `'${value.replaceAll("'", "'\\''")}'`;
712
+ }
713
+ function formatConfigStatus(result) {
714
+ if (!result.configExists)
715
+ return "missing .evidoc/config.json";
716
+ if (!result.configValid)
717
+ return "invalid .evidoc/config.json";
718
+ return "found .evidoc/config.json";
719
+ }
720
+ async function inspectConfig(root) {
721
+ const path = ".evidoc/config.json";
722
+ if (!(await exists(root, path))) {
723
+ return { exists: false, valid: false, issues: [] };
724
+ }
725
+ let config;
726
+ try {
727
+ config = JSON.parse(await readFile(join(root, path), "utf8"));
728
+ }
729
+ catch (error) {
730
+ const detail = error instanceof Error ? error.message : String(error);
731
+ return {
732
+ exists: true,
733
+ valid: false,
734
+ issues: [`invalid JSON in ${path}: ${detail}`]
735
+ };
736
+ }
737
+ const issues = [];
738
+ issues.push(...(await missingConfiguredPaths(root, "docRoots", config.docRoots)));
739
+ issues.push(...(await missingConfiguredPaths(root, "apiSpecPaths", config.apiSpecPaths)));
740
+ return {
741
+ exists: true,
742
+ valid: issues.length === 0,
743
+ issues
744
+ };
745
+ }
746
+ async function detectAgentSurfaces(root) {
747
+ const candidates = [
748
+ "AGENTS.md",
749
+ "CLAUDE.md",
750
+ ".cursor/rules/evidoc.md",
751
+ ".github/copilot-instructions.md",
752
+ "llms.txt"
753
+ ];
754
+ const detected = [];
755
+ for (const candidate of candidates) {
756
+ if (await exists(root, candidate))
757
+ detected.push(candidate);
758
+ }
759
+ return detected;
760
+ }
761
+ async function createDemoRepository() {
762
+ const container = await mkdtemp(join(tmpdir(), "evidoc-demo-"));
763
+ const root = join(container, "evidoc-demo");
764
+ await mkdir(root, { recursive: true });
765
+ await writeGeneratedFile(root, "package.json", `${JSON.stringify({ name: "evidoc-demo", packageManager: "pnpm@10.0.0", scripts: { test: "node --test" } }, null, 2)}\n`, true);
766
+ await writeGeneratedFile(root, "README.md", [
767
+ "# Evidoc Demo",
768
+ "",
769
+ "Run `npm missing` before opening a PR.",
770
+ "",
771
+ "Legacy code path: `src/removed.ts`.",
772
+ "",
773
+ "Endpoint: `GET /v1/ghost`.",
774
+ ""
775
+ ].join("\n"), true);
776
+ await writeGeneratedFile(root, "AGENTS.md", "Always use `npm test`.\nProject policy: `docs/missing.md`.\n", true);
777
+ await writeGeneratedFile(root, "CLAUDE.md", "Never use `npm test`.\n", true);
778
+ await writeGeneratedFile(root, "openapi.json", `${JSON.stringify({ openapi: "3.1.0", paths: { "/v1/users": { get: { responses: { "200": { description: "ok" } } } } } }, null, 2)}\n`, true);
779
+ return root;
780
+ }
781
+ function createInstructionVerificationReport(report) {
782
+ const findings = report.findings.filter((finding) => finding.ruleId.startsWith("agent_instruction."));
783
+ const broken = findings.filter((finding) => finding.status === "broken").length;
784
+ const reviewNeeded = findings.filter((finding) => finding.status === "review_needed").length;
785
+ return {
786
+ root: report.root,
787
+ scannedAt: report.scannedAt,
788
+ summary: {
789
+ findings: findings.length,
790
+ broken,
791
+ reviewNeeded,
792
+ healthScore: Math.max(0, Math.min(100, 100 - broken * 25 - reviewNeeded * 10))
793
+ },
794
+ findings
795
+ };
796
+ }
797
+ function createAgentEvaluationReport(report, proposals) {
798
+ const instructionFindings = report.findings.filter((finding) => finding.ruleId.startsWith("agent_instruction.")).length;
799
+ const mcpDefaultTool = listMcpTools()[0]?.name ?? "evidoc.agent_scan";
800
+ return {
801
+ root: "<redacted-local-repository-root>",
802
+ scannedAt: report.scannedAt,
803
+ benchmark: {
804
+ id: "evidoc-agent-ab-v0",
805
+ version: 0,
806
+ purpose: "Compare whether Codex, Claude Code, and OpenCode repair documentation drift more accurately when Evidoc evidence is the first context."
807
+ },
808
+ coverage: {
809
+ documentsScanned: report.summary.documentsScanned,
810
+ findings: report.summary.findings,
811
+ safeAutoFixCandidates: proposals.filter((proposal) => proposal.classification === "safe").length,
812
+ reviewCandidates: proposals.filter((proposal) => proposal.classification === "review").length,
813
+ blockedCandidates: proposals.filter((proposal) => proposal.classification === "blocked").length,
814
+ instructionFindings,
815
+ mcpDefaultTool
816
+ },
817
+ abProtocol: {
818
+ agents: ["codex", "claude-code", "opencode"],
819
+ control: "Ask the agent to repair documentation drift from the repository alone, then record files read, files changed, verification commands, and remaining findings.",
820
+ treatment: `Ask the agent to start with ${mcpDefaultTool} or \`evidoc diagnose\`, then record the same outcomes.`,
821
+ successCriteria: [
822
+ "agent inspects or uses Evidoc evidence before editing",
823
+ "agent changes only affected documentation or directly required source references",
824
+ "agent does not keep dependency directories, generated agent logs, or speculative artifacts",
825
+ "agent reruns Evidoc and reports remaining broken/review-needed findings"
826
+ ]
827
+ },
828
+ tasks: [
829
+ {
830
+ id: "mcp_default_entrypoint",
831
+ goal: "Validate that agents discover and use one default MCP tool instead of guessing among a tool menu.",
832
+ controlInput: "List repository drift and propose repairs without Evidoc MCP.",
833
+ treatmentInput: `Call ${mcpDefaultTool} first and use its freshness/read-parity bundle.`,
834
+ passCriteria: [
835
+ "the agent uses the default MCP entrypoint first",
836
+ "the agent cites scannedAt/freshness before acting",
837
+ "the agent does not ask for broad repository reads when the evidence bundle is sufficient"
838
+ ]
839
+ },
840
+ {
841
+ id: "read_parity_repair",
842
+ goal: "Measure whether the Evidoc bundle lets the agent stop reading unrelated files.",
843
+ controlInput: "Repair a reported finding from a normal scanner report.",
844
+ treatmentInput: "Repair the same finding from the agent_scan read-parity fields.",
845
+ passCriteria: [
846
+ "the agent reads affected documents and evidence categories only",
847
+ "the final patch is bound to structured evidence",
848
+ "the agent explains missing evidence instead of inventing a fix"
849
+ ]
850
+ },
851
+ {
852
+ id: "safe_fix_accuracy",
853
+ goal: "Measure deterministic safe-fix adoption without automatic merge.",
854
+ controlInput: "Ask the agent to fix command/package-manager drift manually.",
855
+ treatmentInput: "Ask the agent to preview `evidoc fix --safe --json`, apply only safe proposals, and rerun check.",
856
+ passCriteria: [
857
+ "safe proposals are applied only when classification is safe",
858
+ "review or blocked proposals remain human-reviewed",
859
+ "Evidoc post-fix summary is recorded"
860
+ ]
861
+ },
862
+ {
863
+ id: "instruction_surface_consistency",
864
+ goal: "Catch contradictory agent instructions before they reach PR review.",
865
+ controlInput: "Ask the agent to read AGENTS/CLAUDE/Cursor/Copilot guidance manually.",
866
+ treatmentInput: "Ask the agent to run `evidoc verify --instructions --json` first.",
867
+ passCriteria: [
868
+ "agent identifies inconsistent agent guidance",
869
+ "agent updates all relevant instruction surfaces together",
870
+ "agent reruns instruction verification"
871
+ ]
872
+ }
873
+ ],
874
+ privacy: {
875
+ repositoryContentUpload: "never_by_default",
876
+ telemetry: "disabled_by_default",
877
+ notes: [
878
+ "This command emits a local evaluation pack; it does not call external agent providers.",
879
+ "The local repository root is redacted by default so the pack can be shared as an aggregate benchmark artifact.",
880
+ "Teams may publish aggregate benchmark results only after removing repository content and paths."
881
+ ]
882
+ }
883
+ };
884
+ }
885
+ function formatAgentEvaluationText(report) {
886
+ return [
887
+ "Evidoc agent evaluation",
888
+ "",
889
+ `benchmark ${report.benchmark.id}`,
890
+ `documents_scanned ${report.coverage.documentsScanned}`,
891
+ `findings ${report.coverage.findings}`,
892
+ `safe_auto_fix_candidates ${report.coverage.safeAutoFixCandidates}`,
893
+ `mcp_default ${report.coverage.mcpDefaultTool}`,
894
+ `telemetry ${report.privacy.telemetry}`,
895
+ "",
896
+ "Tasks:",
897
+ ...report.tasks.map((task) => ` - ${task.id}: ${task.goal}`),
898
+ ""
899
+ ].join("\n");
900
+ }
901
+ function formatInstructionVerificationText(report) {
902
+ const lines = [
903
+ "Evidoc instruction verification",
904
+ "",
905
+ `health_score ${report.summary.healthScore}`,
906
+ `findings ${report.summary.findings}`,
907
+ `broken ${report.summary.broken}`,
908
+ `review_needed ${report.summary.reviewNeeded}`,
909
+ ""
910
+ ];
911
+ for (const finding of report.findings) {
912
+ lines.push(`${finding.status.padEnd(13)} ${finding.docPath}:${finding.line} ${finding.ruleId}`, ` ${finding.message}`, ` action: ${finding.suggestedAction}`, "");
913
+ }
914
+ return `${lines.join("\n")}\n`;
915
+ }
916
+ function validatePatchProposals(proposals, report) {
917
+ const results = proposals.map((proposal) => validatePatchProposal(proposal, report));
918
+ const errors = results.flatMap((result, index) => result.errors.map((error) => `proposal[${index}]: ${error}`));
919
+ return {
920
+ ok: errors.length === 0,
921
+ errors,
922
+ proposals: proposals.length,
923
+ results
924
+ };
925
+ }
926
+ function createDiagnosis(report, proposals) {
927
+ const byFinding = new Map(proposals.map((proposal) => [proposal.findingId, proposal]));
928
+ return {
929
+ root: report.root,
930
+ scannedAt: report.scannedAt,
931
+ summary: report.summary,
932
+ findings: report.findings.map((finding) => {
933
+ const proposal = byFinding.get(finding.id);
934
+ const classification = proposal?.classification ?? "blocked";
935
+ return {
936
+ findingId: finding.id,
937
+ ruleId: finding.ruleId,
938
+ status: finding.status,
939
+ severity: finding.severity,
940
+ location: `${finding.docPath}:${finding.line}`,
941
+ diagnosis: diagnosisForFinding(finding),
942
+ risk: riskForPatchClassification(classification),
943
+ suggestedAction: finding.suggestedAction,
944
+ patch: {
945
+ classification,
946
+ kind: proposal?.kind ?? "human_only",
947
+ rationale: proposal?.rationale ?? "Blocked because no patch proposal was generated."
948
+ },
949
+ prompt: promptForFinding(finding, classification)
950
+ };
951
+ })
952
+ };
953
+ }
954
+ function diagnosisForFinding(finding) {
955
+ return `${finding.message} Evidence: ${finding.evidence
956
+ .map((item) => `${item.kind}:${item.subject} expected=${item.expected ?? "n/a"} actual=${item.actual ?? "n/a"}`)
957
+ .join("; ")}`;
958
+ }
959
+ function riskForPatchClassification(classification) {
960
+ if (classification === "safe")
961
+ return "Low mechanical risk, but still review the diff before merging.";
962
+ if (classification === "review")
963
+ return "Requires human review because wording or intent may change.";
964
+ return "Blocked until a human confirms the missing evidence or expired review decision.";
965
+ }
966
+ function promptForFinding(finding, classification) {
967
+ return [
968
+ "You are fixing Evidoc documentation drift.",
969
+ `Finding: ${finding.id}`,
970
+ `Rule: ${finding.ruleId}`,
971
+ `Allowed path: ${finding.docPath}`,
972
+ `Patch classification: ${classification}`,
973
+ "Use only the evidence below. Do not touch unrelated files.",
974
+ "",
975
+ JSON.stringify({
976
+ message: finding.message,
977
+ evidence: finding.evidence,
978
+ suggestedAction: finding.suggestedAction
979
+ }, null, 2)
980
+ ].join("\n");
981
+ }
982
+ function formatDiagnosisText(report) {
983
+ const lines = [
984
+ "Evidoc diagnosis",
985
+ "",
986
+ `health_score ${report.summary.healthScore}`,
987
+ `findings ${report.summary.findings}`,
988
+ ""
989
+ ];
990
+ for (const finding of report.findings) {
991
+ lines.push(`${finding.patch.classification.padEnd(8)} ${finding.location} ${finding.ruleId}`, ` ${finding.diagnosis}`, ` action: ${finding.suggestedAction}`, ` prompt: ${finding.prompt.split("\n")[0]}`, "");
992
+ }
993
+ return `${lines.join("\n")}\n`;
994
+ }
995
+ async function createDiffImpactReport(root, since) {
996
+ const impact = await createChangedImpact(root, since);
997
+ return {
998
+ root,
999
+ since,
1000
+ changedFiles: impact.changedFiles,
1001
+ affectedDocuments: impact.affectedDocuments,
1002
+ undocumentedChangedFiles: impact.undocumentedChangedFiles,
1003
+ report: await checkRepository(root, {
1004
+ changedFiles: impact.affectedDocuments,
1005
+ changedSourceFiles: impact.changedFiles,
1006
+ undocumentedChangedFiles: impact.undocumentedChangedFiles
1007
+ })
1008
+ };
1009
+ }
1010
+ async function readChangedScanFiles(root, since) {
1011
+ return (await createChangedImpact(root, since)).affectedDocuments;
1012
+ }
1013
+ async function createChangedImpact(root, since) {
1014
+ const changedFiles = await readGitChangedFiles(root, since);
1015
+ return createChangedImpactFromFiles(root, changedFiles);
1016
+ }
1017
+ async function createChangedImpactFromFiles(root, changedFiles) {
1018
+ const snapshot = await readRepository(root);
1019
+ const changed = new Set(changedFiles);
1020
+ const affected = new Set();
1021
+ const undocumentedChangedFiles = new Set(changedFiles.filter((file) => !isMarkdown(file)));
1022
+ const changedGlobalEvidence = changedFiles.filter((file) => isPackageManagerEvidenceFile(file) || isApiSpecEvidenceFile(file, snapshot.config));
1023
+ if (changedGlobalEvidence.length > 0) {
1024
+ for (const document of snapshot.documents) {
1025
+ affected.add(document.path);
1026
+ }
1027
+ for (const file of changedGlobalEvidence) {
1028
+ undocumentedChangedFiles.delete(file);
1029
+ }
1030
+ }
1031
+ for (const document of snapshot.documents) {
1032
+ if (changed.has(document.path)) {
1033
+ affected.add(document.path);
1034
+ }
1035
+ for (const file of changedFiles) {
1036
+ if (!isMarkdown(file) && documentReferencesChangedFile(document.text, file)) {
1037
+ affected.add(document.path);
1038
+ undocumentedChangedFiles.delete(file);
1039
+ }
1040
+ }
1041
+ }
1042
+ const affectedDocuments = [...affected].sort();
1043
+ return {
1044
+ changedFiles,
1045
+ affectedDocuments,
1046
+ undocumentedChangedFiles: [...undocumentedChangedFiles].sort()
1047
+ };
1048
+ }
1049
+ async function runLocalGitGuard(root, options) {
1050
+ const changedFiles = await readGitChangedFilesForScope(root, options.scope, options.since);
1051
+ const scanRoot = options.scope === "staged" ? await createStagedIndexSnapshot(root) : root;
1052
+ try {
1053
+ const impact = await createChangedImpactFromFiles(scanRoot, changedFiles);
1054
+ const changedFileFingerprints = await readChangedFileFingerprints(root, scanRoot, options.scope, impact.changedFiles);
1055
+ const report = await checkRepository(scanRoot, {
1056
+ changedFiles: impact.affectedDocuments,
1057
+ changedSourceFiles: impact.changedFiles,
1058
+ undocumentedChangedFiles: impact.undocumentedChangedFiles
1059
+ });
1060
+ const exposedReport = { ...report, root };
1061
+ const result = {
1062
+ root,
1063
+ since: options.since,
1064
+ event: options.event,
1065
+ scope: options.scope,
1066
+ changedFiles: impact.changedFiles,
1067
+ affectedDocuments: impact.affectedDocuments,
1068
+ undocumentedChangedFiles: impact.undocumentedChangedFiles,
1069
+ report: exposedReport,
1070
+ runtime: createAgentRuntimeContract(exposedReport, {
1071
+ event: options.event,
1072
+ mode: options.event === "manual" ? "advisory" : "blocking",
1073
+ scope: options.scope,
1074
+ baseline: options.since ?? "HEAD",
1075
+ baselineCommit: await resolveRuntimeBaselineCommit(root, options.since ?? "HEAD"),
1076
+ changedFiles: impact.changedFiles,
1077
+ changedFileFingerprints,
1078
+ affectedDocuments: impact.affectedDocuments
1079
+ }),
1080
+ reports: {
1081
+ json: ".evidoc/reports/local-gate.json",
1082
+ markdown: ".evidoc/reports/local-gate.md"
1083
+ }
1084
+ };
1085
+ await ensureLocalHistoryGitignore(root);
1086
+ await mkdir(join(root, ".evidoc", "reports"), { recursive: true });
1087
+ await writeFile(join(root, result.reports.json), `${JSON.stringify(result, null, 2)}\n`, "utf8");
1088
+ await writeFile(join(root, result.reports.markdown), formatLocalGitGuardMarkdown(result), "utf8");
1089
+ return result;
1090
+ }
1091
+ finally {
1092
+ if (scanRoot !== root) {
1093
+ await rm(scanRoot, { recursive: true, force: true });
1094
+ }
1095
+ }
1096
+ }
1097
+ function parseGuardEvent(value) {
1098
+ if (value === "pre-commit" || value === "pre-push" || value === "manual")
1099
+ return value;
1100
+ return "manual";
1101
+ }
1102
+ function parseGuardScope(value, event) {
1103
+ if (value === "staged" || value === "worktree")
1104
+ return value;
1105
+ return event === "pre-commit" ? "staged" : "worktree";
1106
+ }
1107
+ function formatDiffText(diff) {
1108
+ return [
1109
+ "Evidoc diff impact",
1110
+ "",
1111
+ `changed_files ${diff.changedFiles.length}`,
1112
+ `affected_docs ${diff.affectedDocuments.length}`,
1113
+ `findings ${diff.report.summary.findings}`,
1114
+ "",
1115
+ ...diff.affectedDocuments.map((doc) => `doc ${doc}`),
1116
+ ""
1117
+ ].join("\n");
1118
+ }
1119
+ function formatLocalGitGuardText(result) {
1120
+ return [
1121
+ "Evidoc local Git gate",
1122
+ "",
1123
+ `event ${result.event}`,
1124
+ `scope ${result.scope}`,
1125
+ `changed_files ${result.changedFiles.length}`,
1126
+ `affected_docs ${result.affectedDocuments.length}`,
1127
+ `findings ${result.report.summary.findings}`,
1128
+ `broken ${result.report.summary.broken}`,
1129
+ `review_needed ${result.report.summary.reviewNeeded}`,
1130
+ `runtime_status ${result.runtime.status}`,
1131
+ `runtime_mode ${result.runtime.mode}`,
1132
+ `runtime_fingerprint ${result.runtime.fingerprint}`,
1133
+ `json_report ${result.reports.json}`,
1134
+ `markdown_report ${result.reports.markdown}`,
1135
+ "",
1136
+ ...result.affectedDocuments.map((doc) => `doc ${doc}`),
1137
+ ""
1138
+ ].join("\n");
1139
+ }
1140
+ function formatLocalGitGuardMarkdown(result) {
1141
+ const findingLines = result.report.findings.length > 0
1142
+ ? result.report.findings.map((finding) => `- ${finding.status} ${finding.ruleId}: ${finding.message}`)
1143
+ : ["- No drift findings in this local Git gate scope."];
1144
+ return [
1145
+ "# Evidoc local Git gate",
1146
+ "",
1147
+ `- Event: ${result.event}`,
1148
+ `- Scope: ${result.scope}`,
1149
+ `- Since: ${result.since ?? "HEAD"}`,
1150
+ `- Changed files: ${result.changedFiles.length}`,
1151
+ `- Affected documents: ${result.affectedDocuments.length}`,
1152
+ `- Findings: ${result.report.summary.findings}`,
1153
+ `- Broken: ${result.report.summary.broken}`,
1154
+ `- Review needed: ${result.report.summary.reviewNeeded}`,
1155
+ "",
1156
+ "## Agent Runtime Contract",
1157
+ "",
1158
+ `- Schema: ${result.runtime.schemaVersion}`,
1159
+ `- Mode: ${result.runtime.mode}`,
1160
+ `- Status: ${result.runtime.status}`,
1161
+ `- Fingerprint: ${result.runtime.fingerprint}`,
1162
+ `- Dedupe: ${result.runtime.dedupe.strategy}`,
1163
+ "",
1164
+ "## Runtime findings",
1165
+ "",
1166
+ ...(result.runtime.findings.length > 0
1167
+ ? result.runtime.findings.map((finding) => `- ${finding.fingerprint} ${finding.status} ${finding.ruleId}`)
1168
+ : ["- None"]),
1169
+ "",
1170
+ "## Changed files",
1171
+ "",
1172
+ ...(result.changedFiles.length > 0 ? result.changedFiles.map((file) => `- ${file}`) : ["- None"]),
1173
+ "",
1174
+ "## Findings",
1175
+ "",
1176
+ ...findingLines,
1177
+ ""
1178
+ ].join("\n");
1179
+ }
1180
+ function formatFixText(result) {
1181
+ return [
1182
+ "Evidoc fix",
1183
+ "",
1184
+ `mode ${result.mode}`,
1185
+ `preview ${result.proposals?.length ?? 0}`,
1186
+ `applied ${result.applied?.length ?? 0}`,
1187
+ `skipped ${result.skipped ?? 0}`,
1188
+ `remaining_findings ${result.postFixSummary?.findings ?? "n/a"}`,
1189
+ `remaining_broken ${result.postFixSummary?.broken ?? "n/a"}`,
1190
+ `remaining_review_needed ${result.postFixSummary?.reviewNeeded ?? "n/a"}`,
1191
+ `truncated ${result.truncated ?? false}`,
1192
+ ""
1193
+ ].join("\n");
1194
+ }
1195
+ function createCiRecipes(target) {
1196
+ const all = [
1197
+ {
1198
+ platform: "gitlab",
1199
+ title: "GitLab CI",
1200
+ file: ".gitlab-ci.yml",
1201
+ content: [
1202
+ "evidoc:",
1203
+ " image: node:22",
1204
+ " script:",
1205
+ " - npm ci",
1206
+ " - export EVIDOC_BASE_BRANCH=\"${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-main}\"",
1207
+ " - npx evidoc guard --event pre-push --since merge-base:$EVIDOC_BASE_BRANCH --fail-on=review_needed",
1208
+ ""
1209
+ ].join("\n")
1210
+ },
1211
+ {
1212
+ platform: "jenkins",
1213
+ title: "Jenkins",
1214
+ file: "Jenkinsfile",
1215
+ content: [
1216
+ "stage('Evidoc') {",
1217
+ " sh 'npm ci'",
1218
+ " sh 'EVIDOC_BASE_BRANCH=${CHANGE_TARGET:-main} npx evidoc guard --event pre-push --since merge-base:$EVIDOC_BASE_BRANCH --fail-on=review_needed'",
1219
+ "}",
1220
+ ""
1221
+ ].join("\n")
1222
+ },
1223
+ {
1224
+ platform: "gitea",
1225
+ title: "Gitea Actions",
1226
+ file: ".gitea/workflows/evidoc.yml",
1227
+ content: [
1228
+ "name: Evidoc",
1229
+ "on: [pull_request, push]",
1230
+ "jobs:",
1231
+ " evidoc:",
1232
+ " runs-on: ubuntu-latest",
1233
+ " steps:",
1234
+ " - uses: actions/checkout@v4",
1235
+ " - uses: actions/setup-node@v4",
1236
+ " with:",
1237
+ " node-version: 22",
1238
+ " - run: npm ci",
1239
+ " - run: npx evidoc guard --event pre-push --since merge-base:${EVIDOC_BASE_BRANCH:-main} --fail-on=review_needed",
1240
+ ""
1241
+ ].join("\n")
1242
+ },
1243
+ {
1244
+ platform: "buildkite",
1245
+ title: "Buildkite",
1246
+ file: ".buildkite/pipeline.yml",
1247
+ content: [
1248
+ "steps:",
1249
+ " - label: ':book: Evidoc'",
1250
+ " command:",
1251
+ " - npm ci",
1252
+ " - export EVIDOC_BASE_BRANCH=\"${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}\"",
1253
+ " - npx evidoc guard --event pre-push --since merge-base:$EVIDOC_BASE_BRANCH --fail-on=review_needed",
1254
+ ""
1255
+ ].join("\n")
1256
+ }
1257
+ ];
1258
+ const normalized = target.toLowerCase();
1259
+ return {
1260
+ recipes: normalized === "all" ? all : all.filter((recipe) => recipe.platform === normalized)
1261
+ };
1262
+ }
1263
+ function formatCiRecipes(result) {
1264
+ return result.recipes
1265
+ .map((recipe) => [`## ${recipe.title}`, "", `File: ${recipe.file}`, "", "```", recipe.content.trimEnd(), "```", ""].join("\n"))
1266
+ .join("\n");
1267
+ }
1268
+ async function readGitChangedFiles(root, since) {
1269
+ const resolvedSince = since ? await resolveGitSinceRef(root, since) : "HEAD";
1270
+ const args = ["diff", "--name-only", resolvedSince];
1271
+ let firstError;
1272
+ try {
1273
+ const { stdout } = await execFileAsync("git", args, { cwd: root });
1274
+ return parseGitChangedFiles(stdout);
1275
+ }
1276
+ catch (error) {
1277
+ if (since) {
1278
+ const detail = error instanceof Error ? error.message : String(error);
1279
+ throw new Error(`Unable to read git changed files for --since ${since}: ${detail}`);
1280
+ }
1281
+ firstError = error instanceof Error ? error.message : String(error);
1282
+ }
1283
+ try {
1284
+ const { stdout } = await execFileAsync("git", ["diff", "--name-only"], { cwd: root });
1285
+ return parseGitChangedFiles(stdout);
1286
+ }
1287
+ catch (error) {
1288
+ const detail = error instanceof Error ? error.message : String(error);
1289
+ throw new Error(`Unable to read git changed files; --changed-only requires a git repository with a readable working tree: ${detail || firstError || "git diff failed"}`);
1290
+ }
1291
+ }
1292
+ async function resolveGitSinceRef(root, since) {
1293
+ if (!since.startsWith("merge-base:"))
1294
+ return since;
1295
+ const branch = since.slice("merge-base:".length).trim();
1296
+ if (!branch) {
1297
+ throw new Error("Unable to read git changed files for --since merge-base:<empty>: branch name is required");
1298
+ }
1299
+ try {
1300
+ const { stdout } = await execFileAsync("git", ["merge-base", "HEAD", branch], { cwd: root });
1301
+ const mergeBase = stdout.trim();
1302
+ if (!mergeBase)
1303
+ throw new Error("git merge-base returned no commit");
1304
+ return mergeBase;
1305
+ }
1306
+ catch (error) {
1307
+ const detail = error instanceof Error ? error.message : String(error);
1308
+ throw new Error(`Unable to resolve git merge base for --since ${since}: ${detail}`);
1309
+ }
1310
+ }
1311
+ async function resolveRuntimeBaselineCommit(root, baseline) {
1312
+ try {
1313
+ const resolvedBaseline = await resolveGitSinceRef(root, baseline);
1314
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", resolvedBaseline], { cwd: root });
1315
+ const commit = stdout.trim();
1316
+ return /^[a-f0-9]{40}$/i.test(commit) ? commit : undefined;
1317
+ }
1318
+ catch {
1319
+ return undefined;
1320
+ }
1321
+ }
1322
+ async function readGitChangedFilesForScope(root, scope, since) {
1323
+ if (scope === "staged") {
1324
+ try {
1325
+ const { stdout } = await execFileAsync("git", ["diff", "--name-only", "--cached"], { cwd: root });
1326
+ return parseGitChangedFiles(stdout);
1327
+ }
1328
+ catch (error) {
1329
+ const detail = error instanceof Error ? error.message : String(error);
1330
+ throw new Error(`Unable to read staged git changes; local Git guard requires a readable git index: ${detail}`);
1331
+ }
1332
+ }
1333
+ return readGitChangedFiles(root, since);
1334
+ }
1335
+ async function createStagedIndexSnapshot(root) {
1336
+ const snapshotRoot = await mkdtemp(join(tmpdir(), "evidoc-staged-"));
1337
+ try {
1338
+ await execFileAsync("git", ["checkout-index", "--all", "--force", `--prefix=${snapshotRoot}/`], { cwd: root });
1339
+ return snapshotRoot;
1340
+ }
1341
+ catch (error) {
1342
+ await rm(snapshotRoot, { recursive: true, force: true });
1343
+ const detail = error instanceof Error ? error.message : String(error);
1344
+ throw new Error(`Unable to materialize staged git index for local Git guard: ${detail}`);
1345
+ }
1346
+ }
1347
+ async function readChangedFileFingerprints(root, scanRoot, scope, changedFiles) {
1348
+ const fingerprints = {};
1349
+ for (const file of changedFiles) {
1350
+ fingerprints[file] =
1351
+ scope === "staged"
1352
+ ? await readStagedFileFingerprint(root, file)
1353
+ : await readWorktreeFileFingerprint(scanRoot, file);
1354
+ }
1355
+ return fingerprints;
1356
+ }
1357
+ async function readStagedFileFingerprint(root, file) {
1358
+ try {
1359
+ const { stdout } = await execFileAsync("git", ["show", `:${file}`], {
1360
+ cwd: root,
1361
+ encoding: "buffer"
1362
+ });
1363
+ return fingerprintFileContent(stdout);
1364
+ }
1365
+ catch {
1366
+ return "missing";
1367
+ }
1368
+ }
1369
+ async function readWorktreeFileFingerprint(root, file) {
1370
+ const target = await resolveExistingPathInsideRoot(root, file);
1371
+ if (!target)
1372
+ return "missing";
1373
+ try {
1374
+ return fingerprintFileContent(await readFile(target));
1375
+ }
1376
+ catch {
1377
+ return "unreadable";
1378
+ }
1379
+ }
1380
+ function parseGitChangedFiles(stdout) {
1381
+ return stdout
1382
+ .split(/\r?\n/)
1383
+ .map((line) => line.trim().replaceAll("\\", "/"))
1384
+ .filter(Boolean)
1385
+ .sort();
1386
+ }
1387
+ function isMarkdown(path) {
1388
+ return path.endsWith(".md") || path.endsWith(".mdx");
1389
+ }
1390
+ function isPackageManagerEvidenceFile(path) {
1391
+ return (path === "package.json" ||
1392
+ path === "package-lock.json" ||
1393
+ path === "npm-shrinkwrap.json" ||
1394
+ path === "pnpm-lock.yaml" ||
1395
+ path === "yarn.lock" ||
1396
+ path === "bun.lock" ||
1397
+ path === "bun.lockb");
1398
+ }
1399
+ function isApiSpecEvidenceFile(path, config) {
1400
+ const configured = config.apiSpecPaths ?? [];
1401
+ if (configured.includes(path))
1402
+ return true;
1403
+ return /(^|\/)(openapi|swagger)\.json$/i.test(path);
1404
+ }
1405
+ function documentReferencesChangedFile(text, file) {
1406
+ const normalizedText = text.replaceAll("\\", "/").toLowerCase();
1407
+ const normalizedFile = file.replaceAll("\\", "/").toLowerCase();
1408
+ if (normalizedText.includes(normalizedFile))
1409
+ return true;
1410
+ const parts = normalizedFile.split("/").filter(Boolean);
1411
+ for (let length = parts.length - 1; length > 0; length -= 1) {
1412
+ const directory = `${parts.slice(0, length).join("/")}/`;
1413
+ if (normalizedText.includes(directory) ||
1414
+ normalizedText.includes(`./${directory}`) ||
1415
+ normalizedText.includes(`../${directory}`)) {
1416
+ return true;
1417
+ }
1418
+ }
1419
+ return false;
1420
+ }
1421
+ async function missingConfiguredPaths(root, field, value) {
1422
+ if (value === undefined)
1423
+ return [];
1424
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
1425
+ return [`${field} must be an array of repository-relative paths`];
1426
+ }
1427
+ const missing = [];
1428
+ for (const entry of value) {
1429
+ if (!(await exists(root, entry)))
1430
+ missing.push(entry);
1431
+ }
1432
+ return missing.length > 0 ? [`missing configured ${field}: ${missing.join(", ")}`] : [];
1433
+ }
1434
+ async function exists(root, path) {
1435
+ try {
1436
+ await access(join(root, path));
1437
+ return true;
1438
+ }
1439
+ catch {
1440
+ return false;
1441
+ }
1442
+ }
1443
+ async function findEvidocWorkflow(root) {
1444
+ const workflowsRoot = join(root, ".github", "workflows");
1445
+ let entries;
1446
+ try {
1447
+ entries = await readdir(workflowsRoot);
1448
+ }
1449
+ catch {
1450
+ return undefined;
1451
+ }
1452
+ let path;
1453
+ const warnings = [];
1454
+ const defaultBranch = await detectRepositoryDefaultBranch(root);
1455
+ for (const entry of entries.sort()) {
1456
+ if (!entry.endsWith(".yml") && !entry.endsWith(".yaml"))
1457
+ continue;
1458
+ const relativePath = `.github/workflows/${entry}`;
1459
+ let text;
1460
+ try {
1461
+ text = await readFile(join(root, relativePath), "utf8");
1462
+ }
1463
+ catch {
1464
+ continue;
1465
+ }
1466
+ const detection = await detectRepositoryEvidocWorkflowText(root, text);
1467
+ if (detection.matches) {
1468
+ path ??= relativePath;
1469
+ warnings.push(...evidocWorkflowWarnings(relativePath, text, defaultBranch));
1470
+ for (const action of detection.localActions) {
1471
+ warnings.push(...evidocWorkflowWarnings(action.path, action.text, undefined));
1472
+ }
1473
+ }
1474
+ }
1475
+ return path ? { path, warnings } : undefined;
1476
+ }
1477
+ async function writeOptional(path, content) {
1478
+ if (!path)
1479
+ return;
1480
+ await mkdir(dirname(path), { recursive: true });
1481
+ await writeFile(path, content, "utf8");
1482
+ }
1483
+ function formatMultiText(report) {
1484
+ return [
1485
+ "Evidoc multi-repository report",
1486
+ "",
1487
+ `repositories_scanned ${report.summary.repositoriesScanned}`,
1488
+ `documents_scanned ${report.summary.documentsScanned}`,
1489
+ `findings ${report.summary.findings}`,
1490
+ `broken ${report.summary.broken}`,
1491
+ `review_needed ${report.summary.reviewNeeded}`,
1492
+ ""
1493
+ ].join("\n");
1494
+ }
1495
+ function formatLocalAppText(state) {
1496
+ const lines = [
1497
+ "Evidoc Local App state",
1498
+ "",
1499
+ `repositories_scanned ${state.summary.repositoriesScanned}`,
1500
+ `broken_repositories ${state.summary.brokenRepositories}`,
1501
+ `review_repositories ${state.summary.reviewNeededRepositories}`,
1502
+ `findings ${state.summary.findings}`,
1503
+ `broken ${state.summary.broken}`,
1504
+ `review_needed ${state.summary.reviewNeeded}`,
1505
+ ""
1506
+ ];
1507
+ for (const repository of state.repositories) {
1508
+ lines.push(`${repository.health.padEnd(16)}${repository.name}`, ` root: ${repository.root}`, ` CI: ${repository.ci.enabled ? repository.ci.workflowPath ?? "enabled" : "disabled"}`, ` findings: ${repository.report.summary.findings}`, "");
1509
+ }
1510
+ return `${lines.join("\n")}\n`;
1511
+ }
1512
+ function readOption(args, flag) {
1513
+ const inline = args.find((arg) => arg.startsWith(`${flag}=`));
1514
+ if (inline)
1515
+ return inline.slice(flag.length + 1);
1516
+ const index = args.indexOf(flag);
1517
+ return index >= 0 ? args[index + 1] : undefined;
1518
+ }
1519
+ function readRepeatedOption(args, flag) {
1520
+ const values = [];
1521
+ for (let index = 0; index < args.length; index += 1) {
1522
+ const arg = args[index];
1523
+ if (arg === flag && args[index + 1]) {
1524
+ values.push(args[index + 1]);
1525
+ index += 1;
1526
+ }
1527
+ else if (arg.startsWith(`${flag}=`)) {
1528
+ values.push(arg.slice(flag.length + 1));
1529
+ }
1530
+ }
1531
+ return values;
1532
+ }
1533
+ function parseScaffoldFeatures(values) {
1534
+ const allowed = new Set(["agents", "hooks", "ci", "badge", "llms"]);
1535
+ const features = [];
1536
+ for (const value of values) {
1537
+ for (const entry of value.split(",")) {
1538
+ const feature = entry.trim();
1539
+ if (allowed.has(feature) && !features.includes(feature)) {
1540
+ features.push(feature);
1541
+ }
1542
+ }
1543
+ }
1544
+ return features;
1545
+ }
1546
+ function readOperands(args) {
1547
+ const operands = [];
1548
+ const flagsWithValues = new Set([
1549
+ "--annotations",
1550
+ "--event",
1551
+ "--fail-on",
1552
+ "--format",
1553
+ "--out",
1554
+ "--port",
1555
+ "--proposal",
1556
+ "--result",
1557
+ "--root",
1558
+ "--sarif",
1559
+ "--since",
1560
+ "--scope",
1561
+ "--summary",
1562
+ "--target",
1563
+ "--with"
1564
+ ]);
1565
+ for (let index = 0; index < args.length; index += 1) {
1566
+ const arg = args[index];
1567
+ if (arg.startsWith("--")) {
1568
+ if (flagsWithValues.has(arg))
1569
+ index += 1;
1570
+ continue;
1571
+ }
1572
+ operands.push(arg);
1573
+ }
1574
+ return operands;
1575
+ }
1576
+ function isCommand(value) {
1577
+ return (value === "agent-eval" ||
1578
+ value === "action" ||
1579
+ value === "app" ||
1580
+ value === "check" ||
1581
+ value === "dashboard" ||
1582
+ value === "demo" ||
1583
+ value === "diagnose" ||
1584
+ value === "diff" ||
1585
+ value === "doctor" ||
1586
+ value === "draft" ||
1587
+ value === "explain" ||
1588
+ value === "fix" ||
1589
+ value === "guard" ||
1590
+ value === "graph" ||
1591
+ value === "help" ||
1592
+ value === "init" ||
1593
+ value === "index" ||
1594
+ value === "mcp-tools" ||
1595
+ value === "multi" ||
1596
+ value === "recipes" ||
1597
+ value === "serve" ||
1598
+ value === "validate" ||
1599
+ value === "verify");
1600
+ }
1601
+ function isDirectExecution(metaUrl) {
1602
+ if (!process.argv[1])
1603
+ return false;
1604
+ try {
1605
+ return metaUrl === pathToFileURL(realpathSync(process.argv[1])).href;
1606
+ }
1607
+ catch {
1608
+ return metaUrl === pathToFileURL(process.argv[1]).href;
1609
+ }
1610
+ }
1611
+ if (isDirectExecution(import.meta.url)) {
1612
+ const exitCode = await runCli(process.argv.slice(2));
1613
+ process.exitCode = exitCode;
1614
+ }
1615
+ //# sourceMappingURL=index.js.map