@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.
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +1615 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +34 -0
|
@@ -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
|