mason-context 0.3.7 → 0.7.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/README.md +263 -61
- package/dist/mason-audit.js +1366 -0
- package/dist/mason-audit.js.map +1 -0
- package/dist/mason-drift.js +541 -0
- package/dist/mason-drift.js.map +1 -0
- package/dist/mason-mcp.js +4283 -0
- package/dist/mason-mcp.js.map +1 -0
- package/dist/mason.js +19 -0
- package/dist/mason.js.map +1 -0
- package/package.json +12 -11
- package/dist/bin/mason-mcp.js +0 -1412
- package/dist/bin/mason-mcp.js.map +0 -1
- package/dist/bin/mason.js +0 -2338
- package/dist/bin/mason.js.map +0 -1
- package/dist/src/cli.js +0 -2337
- package/dist/src/cli.js.map +0 -1
|
@@ -0,0 +1,1366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/audit/cli.ts
|
|
4
|
+
import path14 from "path";
|
|
5
|
+
|
|
6
|
+
// src/audit/audit.ts
|
|
7
|
+
import fs9 from "fs/promises";
|
|
8
|
+
import path13 from "path";
|
|
9
|
+
|
|
10
|
+
// src/drift/drift.ts
|
|
11
|
+
import fs3 from "fs/promises";
|
|
12
|
+
import path4 from "path";
|
|
13
|
+
import { execFile as execFile3 } from "child_process";
|
|
14
|
+
import { promisify as promisify3 } from "util";
|
|
15
|
+
|
|
16
|
+
// src/snapshot/snapshot.ts
|
|
17
|
+
import fs2 from "fs/promises";
|
|
18
|
+
import path3 from "path";
|
|
19
|
+
import { execFile as execFile2 } from "child_process";
|
|
20
|
+
import { promisify as promisify2 } from "util";
|
|
21
|
+
import fg3 from "fast-glob";
|
|
22
|
+
|
|
23
|
+
// src/mcp/sampler.ts
|
|
24
|
+
import fs from "fs/promises";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { execFile } from "child_process";
|
|
27
|
+
import { promisify } from "util";
|
|
28
|
+
import fg from "fast-glob";
|
|
29
|
+
var exec = promisify(execFile);
|
|
30
|
+
|
|
31
|
+
// src/test-map.ts
|
|
32
|
+
import path2 from "path";
|
|
33
|
+
import fg2 from "fast-glob";
|
|
34
|
+
|
|
35
|
+
// src/snapshot/snapshot.ts
|
|
36
|
+
var exec2 = promisify2(execFile2);
|
|
37
|
+
async function getCurrentGitHash(rootDir) {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
|
|
40
|
+
cwd: rootDir
|
|
41
|
+
});
|
|
42
|
+
return stdout.trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return "unknown";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}";
|
|
48
|
+
var SOURCE_IGNORE = [
|
|
49
|
+
"**/node_modules/**",
|
|
50
|
+
"**/dist/**",
|
|
51
|
+
"**/build/**",
|
|
52
|
+
"**/.gradle/**",
|
|
53
|
+
"**/target/**",
|
|
54
|
+
"**/.git/**",
|
|
55
|
+
"**/vendor/**",
|
|
56
|
+
"**/__pycache__/**",
|
|
57
|
+
"**/venv/**",
|
|
58
|
+
"**/.venv/**",
|
|
59
|
+
"**/*.min.*",
|
|
60
|
+
"**/*.map",
|
|
61
|
+
"**/generated/**",
|
|
62
|
+
"**/R.java",
|
|
63
|
+
"**/BuildConfig.java"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// src/drift/drift.ts
|
|
67
|
+
var exec3 = promisify3(execFile3);
|
|
68
|
+
async function getChangesWithStatus(resolvedRoot, fromHash) {
|
|
69
|
+
if (!fromHash || fromHash === "unknown") return null;
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await exec3(
|
|
72
|
+
"git",
|
|
73
|
+
["diff", "--name-status", "-M", fromHash, "HEAD"],
|
|
74
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
75
|
+
);
|
|
76
|
+
const changes = [];
|
|
77
|
+
for (const line of stdout.split("\n")) {
|
|
78
|
+
if (!line.trim()) continue;
|
|
79
|
+
const parts = line.split(" ");
|
|
80
|
+
if (parts.some((p) => p.startsWith(".mason/"))) continue;
|
|
81
|
+
const code = parts[0];
|
|
82
|
+
if (code.startsWith("R") && parts.length >= 3) {
|
|
83
|
+
changes.push({
|
|
84
|
+
status: "renamed",
|
|
85
|
+
path: parts[2],
|
|
86
|
+
previousPath: parts[1]
|
|
87
|
+
});
|
|
88
|
+
} else if (code.startsWith("C") && parts.length >= 3) {
|
|
89
|
+
changes.push({ status: "added", path: parts[2] });
|
|
90
|
+
} else if (code === "A" && parts.length >= 2) {
|
|
91
|
+
changes.push({ status: "added", path: parts[1] });
|
|
92
|
+
} else if (code === "D" && parts.length >= 2) {
|
|
93
|
+
changes.push({ status: "deleted", path: parts[1] });
|
|
94
|
+
} else if (parts.length >= 2) {
|
|
95
|
+
changes.push({ status: "modified", path: parts[1] });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return changes;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/audit/docs.ts
|
|
105
|
+
import fs4 from "fs/promises";
|
|
106
|
+
import path5 from "path";
|
|
107
|
+
import { execFile as execFile5 } from "child_process";
|
|
108
|
+
import { promisify as promisify5 } from "util";
|
|
109
|
+
|
|
110
|
+
// src/audit/tree.ts
|
|
111
|
+
var MIN_GLYPH_LINES = 3;
|
|
112
|
+
var GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
|
|
113
|
+
function glyphIndex(line) {
|
|
114
|
+
for (const glyph of GLYPHS) {
|
|
115
|
+
const idx = line.indexOf(glyph);
|
|
116
|
+
if (idx !== -1) return idx;
|
|
117
|
+
}
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
function isSpacerLine(line) {
|
|
121
|
+
return /^[\s│|]*$/.test(line);
|
|
122
|
+
}
|
|
123
|
+
function entryName(afterGlyph) {
|
|
124
|
+
let name = afterGlyph.replace(/^\s+/, "");
|
|
125
|
+
const hash = name.search(/\s+#/);
|
|
126
|
+
if (hash !== -1) name = name.slice(0, hash);
|
|
127
|
+
const columns = name.search(/\s{2,}/);
|
|
128
|
+
if (columns !== -1) name = name.slice(0, columns);
|
|
129
|
+
name = name.trim();
|
|
130
|
+
if (!name || /\s/.test(name)) return null;
|
|
131
|
+
return name;
|
|
132
|
+
}
|
|
133
|
+
function extractTreeClaims(blockLines, blockStartLine) {
|
|
134
|
+
const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
|
|
135
|
+
if (glyphLines < MIN_GLYPH_LINES) return [];
|
|
136
|
+
const claims = [];
|
|
137
|
+
const stack = [];
|
|
138
|
+
let rootPrefix = "";
|
|
139
|
+
let started = false;
|
|
140
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
141
|
+
const line = blockLines[i];
|
|
142
|
+
const col = glyphIndex(line);
|
|
143
|
+
if (col === -1) {
|
|
144
|
+
if (isSpacerLine(line)) continue;
|
|
145
|
+
if (!started) {
|
|
146
|
+
const candidate = line.trim();
|
|
147
|
+
if (candidate.endsWith("/") && !/\s/.test(candidate)) {
|
|
148
|
+
rootPrefix = candidate.replace(/\/+$/, "");
|
|
149
|
+
claims.push({
|
|
150
|
+
path: rootPrefix,
|
|
151
|
+
line: blockStartLine + i,
|
|
152
|
+
excerpt: candidate
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
return claims;
|
|
158
|
+
}
|
|
159
|
+
started = true;
|
|
160
|
+
const name = entryName(line.slice(col + GLYPHS[0].length));
|
|
161
|
+
if (name === null) return claims;
|
|
162
|
+
while (stack.length > 0 && stack[stack.length - 1].col >= col) {
|
|
163
|
+
stack.pop();
|
|
164
|
+
}
|
|
165
|
+
const isDir = name.endsWith("/");
|
|
166
|
+
const cleanName = name.replace(/\/+$/, "");
|
|
167
|
+
const segments = [
|
|
168
|
+
...rootPrefix ? [rootPrefix] : [],
|
|
169
|
+
...stack.map((s) => s.name),
|
|
170
|
+
cleanName
|
|
171
|
+
];
|
|
172
|
+
claims.push({
|
|
173
|
+
path: segments.join("/"),
|
|
174
|
+
line: blockStartLine + i,
|
|
175
|
+
excerpt: name
|
|
176
|
+
});
|
|
177
|
+
if (isDir) stack.push({ col, name: cleanName });
|
|
178
|
+
}
|
|
179
|
+
return claims;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/audit/claims.ts
|
|
183
|
+
var ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
184
|
+
"package.json",
|
|
185
|
+
"package-lock.json",
|
|
186
|
+
"pnpm-workspace.yaml",
|
|
187
|
+
"tsconfig.json",
|
|
188
|
+
"tsup.config.ts",
|
|
189
|
+
"vitest.config.ts",
|
|
190
|
+
"Makefile",
|
|
191
|
+
"Dockerfile",
|
|
192
|
+
"docker-compose.yml",
|
|
193
|
+
"Cargo.toml",
|
|
194
|
+
"go.mod",
|
|
195
|
+
"go.sum",
|
|
196
|
+
"pyproject.toml",
|
|
197
|
+
"requirements.txt",
|
|
198
|
+
"Gemfile",
|
|
199
|
+
"composer.json",
|
|
200
|
+
"settings.gradle.kts",
|
|
201
|
+
"settings.gradle",
|
|
202
|
+
"build.gradle.kts",
|
|
203
|
+
"build.gradle",
|
|
204
|
+
"manifest.json",
|
|
205
|
+
"server.json",
|
|
206
|
+
"README.md",
|
|
207
|
+
"CHANGELOG.md",
|
|
208
|
+
"LICENSE",
|
|
209
|
+
"CLAUDE.md",
|
|
210
|
+
"AGENTS.md",
|
|
211
|
+
".gitignore",
|
|
212
|
+
".env.example"
|
|
213
|
+
]);
|
|
214
|
+
var SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
215
|
+
var COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
|
|
216
|
+
var COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
217
|
+
var COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
218
|
+
var IGNORE_LINE = "<!-- mason:ignore -->";
|
|
219
|
+
var IGNORE_START = "<!-- mason:ignore-start -->";
|
|
220
|
+
var IGNORE_END = "<!-- mason:ignore-end -->";
|
|
221
|
+
function normalizePathToken(token) {
|
|
222
|
+
let t = token.trim();
|
|
223
|
+
if (!t) return null;
|
|
224
|
+
if (/\s/.test(t)) return null;
|
|
225
|
+
if (t.includes("://") || t.includes("\\")) return null;
|
|
226
|
+
if (/[*?[\]{}<>$`]/.test(t)) return null;
|
|
227
|
+
if (t.startsWith("/") || t.startsWith("~") || t.startsWith("./") || t.startsWith("../")) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
t = t.replace(/:\d+(?:-\d+)?$/, "");
|
|
231
|
+
if (t.includes(":")) return null;
|
|
232
|
+
const normalized = t.replace(/\/+$/, "");
|
|
233
|
+
if (!normalized) return null;
|
|
234
|
+
if (normalized.includes("/")) return normalized;
|
|
235
|
+
return ROOT_FILE_NAMES.has(normalized) ? normalized : null;
|
|
236
|
+
}
|
|
237
|
+
function exactTokenPath(line) {
|
|
238
|
+
const trimmed = line.trim();
|
|
239
|
+
if (!trimmed || /\s/.test(trimmed) || !trimmed.includes("/")) return null;
|
|
240
|
+
return normalizePathToken(trimmed);
|
|
241
|
+
}
|
|
242
|
+
function computeIgnoredLines(lines) {
|
|
243
|
+
const ignored = new Array(lines.length).fill(false);
|
|
244
|
+
let inRegion = false;
|
|
245
|
+
let ignoreNext = false;
|
|
246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
247
|
+
const line = lines[i];
|
|
248
|
+
if (line.includes(IGNORE_START)) {
|
|
249
|
+
inRegion = true;
|
|
250
|
+
ignored[i] = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (line.includes(IGNORE_END)) {
|
|
254
|
+
inRegion = false;
|
|
255
|
+
ignored[i] = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (inRegion) {
|
|
259
|
+
ignored[i] = true;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (ignoreNext) {
|
|
263
|
+
if (line.trim().length === 0) continue;
|
|
264
|
+
ignored[i] = true;
|
|
265
|
+
ignoreNext = false;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (line.includes(IGNORE_LINE)) {
|
|
269
|
+
ignored[i] = true;
|
|
270
|
+
const rest = line.replace(IGNORE_LINE, "").trim();
|
|
271
|
+
if (rest.length === 0) ignoreNext = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return ignored;
|
|
275
|
+
}
|
|
276
|
+
function extractClaims(content) {
|
|
277
|
+
const lines = content.split("\n");
|
|
278
|
+
const ignored = computeIgnoredLines(lines);
|
|
279
|
+
const paths = /* @__PURE__ */ new Map();
|
|
280
|
+
const counts = [];
|
|
281
|
+
const commands = /* @__PURE__ */ new Map();
|
|
282
|
+
const addPath = (claim) => {
|
|
283
|
+
if (!paths.has(claim.path)) paths.set(claim.path, claim);
|
|
284
|
+
};
|
|
285
|
+
const addCommand = (claim) => {
|
|
286
|
+
if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);
|
|
287
|
+
};
|
|
288
|
+
let inFence = false;
|
|
289
|
+
let fenceInfo = "";
|
|
290
|
+
let fenceMarker = "";
|
|
291
|
+
let blockLines = [];
|
|
292
|
+
let blockStartLine = 0;
|
|
293
|
+
const processBlock = () => {
|
|
294
|
+
for (const claim of extractTreeClaims(blockLines, blockStartLine)) {
|
|
295
|
+
addPath(claim);
|
|
296
|
+
}
|
|
297
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
298
|
+
const exact = exactTokenPath(blockLines[i]);
|
|
299
|
+
if (exact) {
|
|
300
|
+
addPath({
|
|
301
|
+
path: exact,
|
|
302
|
+
line: blockStartLine + i,
|
|
303
|
+
excerpt: blockLines[i].trim()
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
for (let i = 0; i < lines.length; i++) {
|
|
309
|
+
const line = lines[i];
|
|
310
|
+
const lineNo = i + 1;
|
|
311
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)(.*)$/);
|
|
312
|
+
if (fenceMatch) {
|
|
313
|
+
if (!inFence) {
|
|
314
|
+
inFence = true;
|
|
315
|
+
fenceMarker = fenceMatch[1][0];
|
|
316
|
+
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
317
|
+
blockLines = [];
|
|
318
|
+
blockStartLine = lineNo + 1;
|
|
319
|
+
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
320
|
+
inFence = false;
|
|
321
|
+
processBlock();
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (inFence) {
|
|
326
|
+
blockLines.push(ignored[i] ? "" : line);
|
|
327
|
+
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
328
|
+
for (const m of line.matchAll(COMMAND_RE)) {
|
|
329
|
+
addCommand({
|
|
330
|
+
scriptName: m[2],
|
|
331
|
+
invocation: m[0],
|
|
332
|
+
line: lineNo,
|
|
333
|
+
excerpt: m[0]
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ignored[i]) continue;
|
|
340
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
341
|
+
const normalized = normalizePathToken(m[1]);
|
|
342
|
+
if (normalized) {
|
|
343
|
+
addPath({ path: normalized, line: lineNo, excerpt: m[1] });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const m of line.matchAll(/"([A-Za-z][\w.@-]*(?:\/[\w.@-]+)+\/?)"/g)) {
|
|
347
|
+
const normalized = normalizePathToken(m[1]);
|
|
348
|
+
if (normalized) {
|
|
349
|
+
addPath({ path: normalized, line: lineNo, excerpt: m[1] });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const m of line.matchAll(COUNT_RE)) {
|
|
353
|
+
const rest = line.slice((m.index ?? 0) + m[0].length);
|
|
354
|
+
if (COUNT_DENYLIST_RE.test(rest)) continue;
|
|
355
|
+
counts.push({
|
|
356
|
+
count: Number.parseInt(m[1], 10),
|
|
357
|
+
unit: m[2].toLowerCase(),
|
|
358
|
+
line: lineNo,
|
|
359
|
+
excerpt: m[0]
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
for (const m of line.matchAll(COMMAND_RE)) {
|
|
363
|
+
addCommand({
|
|
364
|
+
scriptName: m[2],
|
|
365
|
+
invocation: m[0],
|
|
366
|
+
line: lineNo,
|
|
367
|
+
excerpt: m[0]
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (inFence) processBlock();
|
|
372
|
+
return {
|
|
373
|
+
paths: [...paths.values()],
|
|
374
|
+
counts,
|
|
375
|
+
commands: [...commands.values()]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/audit/git.ts
|
|
380
|
+
import { execFile as execFile4 } from "child_process";
|
|
381
|
+
import { promisify as promisify4 } from "util";
|
|
382
|
+
var exec4 = promisify4(execFile4);
|
|
383
|
+
var COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
384
|
+
function parseCommitLine(line) {
|
|
385
|
+
const parts = line.split(" ");
|
|
386
|
+
if (parts.length < 3 || !parts[0]) return null;
|
|
387
|
+
return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(" ") };
|
|
388
|
+
}
|
|
389
|
+
async function lastCommitOf(resolvedRoot, relPath) {
|
|
390
|
+
try {
|
|
391
|
+
const { stdout } = await exec4(
|
|
392
|
+
"git",
|
|
393
|
+
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
394
|
+
{ cwd: resolvedRoot }
|
|
395
|
+
);
|
|
396
|
+
const line = stdout.trim().split("\n")[0];
|
|
397
|
+
return line ? parseCommitLine(line) : null;
|
|
398
|
+
} catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
403
|
+
try {
|
|
404
|
+
const { stdout } = await exec4(
|
|
405
|
+
"git",
|
|
406
|
+
[
|
|
407
|
+
"log",
|
|
408
|
+
"-1",
|
|
409
|
+
"--diff-filter=D",
|
|
410
|
+
`--format=${COMMIT_FORMAT}`,
|
|
411
|
+
"--",
|
|
412
|
+
relPath
|
|
413
|
+
],
|
|
414
|
+
{ cwd: resolvedRoot }
|
|
415
|
+
);
|
|
416
|
+
const line = stdout.trim().split("\n")[0];
|
|
417
|
+
return line ? parseCommitLine(line) : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function firstCommitOf(resolvedRoot, relPath) {
|
|
423
|
+
try {
|
|
424
|
+
const { stdout } = await exec4(
|
|
425
|
+
"git",
|
|
426
|
+
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
427
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
428
|
+
);
|
|
429
|
+
const line = stdout.trim().split("\n")[0];
|
|
430
|
+
return line ? parseCommitLine(line) : null;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
436
|
+
if (!fromHash || fromHash === "unknown") return null;
|
|
437
|
+
try {
|
|
438
|
+
const { stdout } = await exec4(
|
|
439
|
+
"git",
|
|
440
|
+
[
|
|
441
|
+
"log",
|
|
442
|
+
`${fromHash}..HEAD`,
|
|
443
|
+
`--format=%x01${COMMIT_FORMAT}`,
|
|
444
|
+
"--name-only",
|
|
445
|
+
"--",
|
|
446
|
+
...pathspecs
|
|
447
|
+
],
|
|
448
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
449
|
+
);
|
|
450
|
+
const commits = [];
|
|
451
|
+
for (const block of stdout.split("")) {
|
|
452
|
+
if (!block.trim()) continue;
|
|
453
|
+
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
454
|
+
const ref = parseCommitLine(lines[0]);
|
|
455
|
+
if (!ref) continue;
|
|
456
|
+
commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
|
|
457
|
+
}
|
|
458
|
+
return { commits, total: commits.length };
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/audit/docs.ts
|
|
465
|
+
var exec5 = promisify5(execFile5);
|
|
466
|
+
var DOC_CANDIDATES = [
|
|
467
|
+
"AGENTS.md",
|
|
468
|
+
"CLAUDE.md",
|
|
469
|
+
".claude/CLAUDE.md"
|
|
470
|
+
];
|
|
471
|
+
async function isDirty(resolvedRoot, relPath) {
|
|
472
|
+
try {
|
|
473
|
+
const { stdout } = await exec5(
|
|
474
|
+
"git",
|
|
475
|
+
["status", "--porcelain", "--", relPath],
|
|
476
|
+
{ cwd: resolvedRoot }
|
|
477
|
+
);
|
|
478
|
+
return stdout.trim().length > 0;
|
|
479
|
+
} catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function discoverDocs(resolvedRoot) {
|
|
484
|
+
const docs = [];
|
|
485
|
+
for (const candidate of DOC_CANDIDATES) {
|
|
486
|
+
let content;
|
|
487
|
+
try {
|
|
488
|
+
content = await fs4.readFile(path5.join(resolvedRoot, candidate), "utf-8");
|
|
489
|
+
} catch {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
docs.push({
|
|
493
|
+
path: candidate,
|
|
494
|
+
content,
|
|
495
|
+
lineCount: content.split("\n").length,
|
|
496
|
+
lastCommit: await lastCommitOf(resolvedRoot, candidate),
|
|
497
|
+
dirty: await isDirty(resolvedRoot, candidate),
|
|
498
|
+
claims: extractClaims(content)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return docs;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/audit/types.ts
|
|
505
|
+
var ALL_CHECKS = [
|
|
506
|
+
"deleted-reference",
|
|
507
|
+
"new-module",
|
|
508
|
+
"stale-count",
|
|
509
|
+
"dead-command",
|
|
510
|
+
"deps-changed",
|
|
511
|
+
"decision-anchor-drift"
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
// src/audit/checks/deleted-reference.ts
|
|
515
|
+
import fs5 from "fs/promises";
|
|
516
|
+
import path6 from "path";
|
|
517
|
+
async function exists(absPath) {
|
|
518
|
+
try {
|
|
519
|
+
await fs5.access(absPath);
|
|
520
|
+
return true;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function checkDeletedReferences(ctx) {
|
|
526
|
+
const result = emptyResult();
|
|
527
|
+
for (const doc of ctx.docs) {
|
|
528
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
529
|
+
const renames = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const change of changes ?? []) {
|
|
531
|
+
if (change.status === "renamed" && change.previousPath) {
|
|
532
|
+
renames.set(change.previousPath, change.path);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
for (const claim of doc.claims.paths) {
|
|
536
|
+
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (await exists(path6.join(ctx.root, claim.path))) continue;
|
|
540
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
541
|
+
const renamedTo = renames.get(claim.path) ?? null;
|
|
542
|
+
if (renamedTo) {
|
|
543
|
+
result.issues.push({
|
|
544
|
+
type: "deleted-reference",
|
|
545
|
+
message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
|
|
546
|
+
anchor,
|
|
547
|
+
confidence: "certain",
|
|
548
|
+
evidence: {
|
|
549
|
+
kind: "missing-path",
|
|
550
|
+
claimed: claim.path,
|
|
551
|
+
renamedTo,
|
|
552
|
+
deletedInCommit: null,
|
|
553
|
+
everTracked: true,
|
|
554
|
+
parentDirExists: true
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const tracked = await lastCommitOf(ctx.root, claim.path);
|
|
560
|
+
if (tracked) {
|
|
561
|
+
const deleted = await deletingCommitOf(ctx.root, claim.path);
|
|
562
|
+
const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
|
|
563
|
+
result.issues.push({
|
|
564
|
+
type: "deleted-reference",
|
|
565
|
+
message: `\`${claim.path}\` no longer exists${detail}`,
|
|
566
|
+
anchor,
|
|
567
|
+
confidence: "certain",
|
|
568
|
+
evidence: {
|
|
569
|
+
kind: "missing-path",
|
|
570
|
+
claimed: claim.path,
|
|
571
|
+
renamedTo: null,
|
|
572
|
+
deletedInCommit: deleted,
|
|
573
|
+
everTracked: true,
|
|
574
|
+
parentDirExists: await exists(
|
|
575
|
+
path6.join(ctx.root, path6.dirname(claim.path))
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const parentDirExists = await exists(
|
|
582
|
+
path6.join(ctx.root, path6.dirname(claim.path))
|
|
583
|
+
);
|
|
584
|
+
if (!parentDirExists) continue;
|
|
585
|
+
const issue = {
|
|
586
|
+
type: "deleted-reference",
|
|
587
|
+
message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
|
|
588
|
+
anchor,
|
|
589
|
+
confidence: "likely",
|
|
590
|
+
evidence: {
|
|
591
|
+
kind: "missing-path",
|
|
592
|
+
claimed: claim.path,
|
|
593
|
+
renamedTo: null,
|
|
594
|
+
deletedInCommit: null,
|
|
595
|
+
everTracked: false,
|
|
596
|
+
parentDirExists: true
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
result.issues.push(issue);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/audit/checks/new-module.ts
|
|
606
|
+
import fg4 from "fast-glob";
|
|
607
|
+
import path7 from "path";
|
|
608
|
+
var DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
609
|
+
"node_modules",
|
|
610
|
+
"dist",
|
|
611
|
+
"build",
|
|
612
|
+
"out",
|
|
613
|
+
"coverage",
|
|
614
|
+
"target",
|
|
615
|
+
"vendor",
|
|
616
|
+
"__pycache__",
|
|
617
|
+
"venv",
|
|
618
|
+
".venv",
|
|
619
|
+
".git",
|
|
620
|
+
".gradle",
|
|
621
|
+
".mason",
|
|
622
|
+
".claude",
|
|
623
|
+
".github",
|
|
624
|
+
".vscode",
|
|
625
|
+
".idea"
|
|
626
|
+
]);
|
|
627
|
+
var SECOND_LEVEL_MIN_SOURCE_FILES = 2;
|
|
628
|
+
var ENUMERATION_THRESHOLD = 2;
|
|
629
|
+
function escapeRegExp(text) {
|
|
630
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
631
|
+
}
|
|
632
|
+
function isMentioned(combinedDocs, name) {
|
|
633
|
+
const re = new RegExp(
|
|
634
|
+
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
635
|
+
"im"
|
|
636
|
+
);
|
|
637
|
+
return re.test(combinedDocs);
|
|
638
|
+
}
|
|
639
|
+
async function listSubdirs(absDir) {
|
|
640
|
+
const dirs = await fg4("*", {
|
|
641
|
+
cwd: absDir,
|
|
642
|
+
onlyDirectories: true,
|
|
643
|
+
suppressErrors: true
|
|
644
|
+
});
|
|
645
|
+
return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
|
|
646
|
+
}
|
|
647
|
+
async function countSourceFiles(absDir) {
|
|
648
|
+
const files = await fg4(SOURCE_GLOB, {
|
|
649
|
+
cwd: absDir,
|
|
650
|
+
ignore: SOURCE_IGNORE,
|
|
651
|
+
suppressErrors: true
|
|
652
|
+
});
|
|
653
|
+
return files.length;
|
|
654
|
+
}
|
|
655
|
+
async function checkNewModules(ctx) {
|
|
656
|
+
const result = emptyResult();
|
|
657
|
+
if (ctx.docs.length === 0) return result;
|
|
658
|
+
const combinedDocs = ctx.docs.map((d) => d.content).join("\n");
|
|
659
|
+
const primaryDoc = ctx.docs[0].path;
|
|
660
|
+
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
661
|
+
const flag = async (dir, sourceFileCount) => {
|
|
662
|
+
result.issues.push({
|
|
663
|
+
type: "new-module",
|
|
664
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in any context file`,
|
|
665
|
+
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
666
|
+
confidence: "likely",
|
|
667
|
+
evidence: {
|
|
668
|
+
kind: "unmentioned-dir",
|
|
669
|
+
dir,
|
|
670
|
+
sourceFileCount,
|
|
671
|
+
firstCommit: await firstCommitOf(ctx.root, dir),
|
|
672
|
+
checkedDocs
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
};
|
|
676
|
+
for (const topDir of await listSubdirs(ctx.root)) {
|
|
677
|
+
const absTop = path7.join(ctx.root, topDir);
|
|
678
|
+
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
679
|
+
if (!topMentioned) {
|
|
680
|
+
const count = await countSourceFiles(absTop);
|
|
681
|
+
if (count >= 1) await flag(topDir, count);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const subdirs = await listSubdirs(absTop);
|
|
685
|
+
const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
|
|
686
|
+
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
687
|
+
for (const sub of subdirs) {
|
|
688
|
+
if (isMentioned(combinedDocs, sub)) continue;
|
|
689
|
+
const count = await countSourceFiles(path7.join(absTop, sub));
|
|
690
|
+
if (count >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
691
|
+
await flag(`${topDir}/${sub}`, count);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/audit/checks/stale-count.ts
|
|
699
|
+
import fs6 from "fs/promises";
|
|
700
|
+
import path8 from "path";
|
|
701
|
+
import fg5 from "fast-glob";
|
|
702
|
+
var MEMBERS_CAP = 50;
|
|
703
|
+
async function readIfExists(absPath) {
|
|
704
|
+
try {
|
|
705
|
+
return await fs6.readFile(absPath, "utf-8");
|
|
706
|
+
} catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function countGradleModules(root) {
|
|
711
|
+
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
712
|
+
const content = await readIfExists(path8.join(root, name));
|
|
713
|
+
if (content === null) continue;
|
|
714
|
+
const members = [];
|
|
715
|
+
for (const call of content.matchAll(/include\s*\(([^)]*)\)/g)) {
|
|
716
|
+
for (const proj of call[1].matchAll(/["']([^"']+)["']/g)) {
|
|
717
|
+
members.push(proj[1]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (members.length === 0) return null;
|
|
721
|
+
return { actual: members.length, countedFrom: name, members };
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
async function countNpmWorkspaces(root) {
|
|
726
|
+
const pkgRaw = await readIfExists(path8.join(root, "package.json"));
|
|
727
|
+
if (pkgRaw !== null) {
|
|
728
|
+
try {
|
|
729
|
+
const pkg = JSON.parse(pkgRaw);
|
|
730
|
+
const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
|
|
731
|
+
if (globs.length > 0) {
|
|
732
|
+
const matched = await fg5(
|
|
733
|
+
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
734
|
+
{ cwd: root, ignore: ["**/node_modules/**"] }
|
|
735
|
+
);
|
|
736
|
+
return {
|
|
737
|
+
actual: matched.length,
|
|
738
|
+
countedFrom: "package.json workspaces",
|
|
739
|
+
members: matched.map((m) => path8.dirname(m)).sort()
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const pnpmRaw = await readIfExists(path8.join(root, "pnpm-workspace.yaml"));
|
|
746
|
+
if (pnpmRaw !== null) {
|
|
747
|
+
const globs = [];
|
|
748
|
+
let inPackages = false;
|
|
749
|
+
for (const line of pnpmRaw.split("\n")) {
|
|
750
|
+
if (/^packages\s*:/.test(line)) {
|
|
751
|
+
inPackages = true;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (inPackages) {
|
|
755
|
+
const entry = line.match(/^\s*-\s*["']?([^"'#\s]+)/);
|
|
756
|
+
if (entry) {
|
|
757
|
+
if (!entry[1].startsWith("!")) globs.push(entry[1]);
|
|
758
|
+
} else if (line.trim().length > 0 && !line.startsWith(" ")) {
|
|
759
|
+
inPackages = false;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (globs.length > 0) {
|
|
764
|
+
const matched = await fg5(
|
|
765
|
+
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
766
|
+
{ cwd: root, ignore: ["**/node_modules/**"] }
|
|
767
|
+
);
|
|
768
|
+
return {
|
|
769
|
+
actual: matched.length,
|
|
770
|
+
countedFrom: "pnpm-workspace.yaml",
|
|
771
|
+
members: matched.map((m) => path8.dirname(m)).sort()
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
async function countCargoCrates(root) {
|
|
778
|
+
const content = await readIfExists(path8.join(root, "Cargo.toml"));
|
|
779
|
+
if (content === null) return null;
|
|
780
|
+
const membersBlock = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
781
|
+
if (!membersBlock) return null;
|
|
782
|
+
const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map(
|
|
783
|
+
(m) => m[1]
|
|
784
|
+
);
|
|
785
|
+
if (entries.length === 0) return null;
|
|
786
|
+
const members = /* @__PURE__ */ new Set();
|
|
787
|
+
for (const entry of entries) {
|
|
788
|
+
if (/[*?[\]{}]/.test(entry)) {
|
|
789
|
+
const matched = await fg5(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
|
|
790
|
+
cwd: root,
|
|
791
|
+
ignore: ["**/target/**"]
|
|
792
|
+
});
|
|
793
|
+
for (const m of matched) members.add(path8.dirname(m));
|
|
794
|
+
} else if (await readIfExists(path8.join(root, entry, "Cargo.toml")) !== null) {
|
|
795
|
+
members.add(entry);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (members.size === 0) return null;
|
|
799
|
+
return {
|
|
800
|
+
actual: members.size,
|
|
801
|
+
countedFrom: "Cargo.toml workspace members",
|
|
802
|
+
members: [...members].sort()
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async function resolveCountSource(root, claim) {
|
|
806
|
+
const unit = claim.unit.replace(/s$/, "");
|
|
807
|
+
if (unit === "module") return countGradleModules(root);
|
|
808
|
+
if (unit === "workspace") return countNpmWorkspaces(root);
|
|
809
|
+
if (unit === "crate") return countCargoCrates(root);
|
|
810
|
+
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
811
|
+
}
|
|
812
|
+
async function checkStaleCounts(ctx) {
|
|
813
|
+
const result = emptyResult();
|
|
814
|
+
for (const doc of ctx.docs) {
|
|
815
|
+
for (const claim of doc.claims.counts) {
|
|
816
|
+
const source = await resolveCountSource(ctx.root, claim);
|
|
817
|
+
if (source === null || source.actual === claim.count) continue;
|
|
818
|
+
result.issues.push({
|
|
819
|
+
type: "stale-count",
|
|
820
|
+
message: `says "${claim.excerpt}" but ${source.countedFrom} resolves to ${source.actual}`,
|
|
821
|
+
anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
|
|
822
|
+
confidence: "certain",
|
|
823
|
+
evidence: {
|
|
824
|
+
kind: "count-mismatch",
|
|
825
|
+
claimed: claim.count,
|
|
826
|
+
actual: source.actual,
|
|
827
|
+
unit: claim.unit,
|
|
828
|
+
countedFrom: source.countedFrom,
|
|
829
|
+
members: source.members.slice(0, MEMBERS_CAP)
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/audit/checks/dead-command.ts
|
|
838
|
+
import fs7 from "fs/promises";
|
|
839
|
+
import path9 from "path";
|
|
840
|
+
import fg6 from "fast-glob";
|
|
841
|
+
var AVAILABLE_SCRIPTS_CAP = 30;
|
|
842
|
+
async function scriptsOf(absManifest) {
|
|
843
|
+
try {
|
|
844
|
+
const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
|
|
845
|
+
return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function checkDeadCommands(ctx) {
|
|
851
|
+
const result = emptyResult();
|
|
852
|
+
const commandClaims = ctx.docs.flatMap(
|
|
853
|
+
(doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
|
|
854
|
+
);
|
|
855
|
+
if (commandClaims.length === 0) return result;
|
|
856
|
+
const rootScripts = await scriptsOf(path9.join(ctx.root, "package.json"));
|
|
857
|
+
if (rootScripts === null) {
|
|
858
|
+
result.skipped.push({
|
|
859
|
+
check: "dead-command",
|
|
860
|
+
reason: "no package.json at the repo root"
|
|
861
|
+
});
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
const rootSet = new Set(rootScripts);
|
|
865
|
+
let workspaceScripts = null;
|
|
866
|
+
let manifestsChecked = ["package.json"];
|
|
867
|
+
const loadWorkspaceScripts = async () => {
|
|
868
|
+
if (workspaceScripts !== null) return workspaceScripts;
|
|
869
|
+
workspaceScripts = /* @__PURE__ */ new Set();
|
|
870
|
+
const manifests = await fg6("**/package.json", {
|
|
871
|
+
cwd: ctx.root,
|
|
872
|
+
ignore: [
|
|
873
|
+
"**/node_modules/**",
|
|
874
|
+
"**/dist/**",
|
|
875
|
+
"**/build/**",
|
|
876
|
+
"package.json"
|
|
877
|
+
]
|
|
878
|
+
});
|
|
879
|
+
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
880
|
+
for (const manifest of manifests) {
|
|
881
|
+
const scripts = await scriptsOf(path9.join(ctx.root, manifest));
|
|
882
|
+
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
883
|
+
}
|
|
884
|
+
return workspaceScripts;
|
|
885
|
+
};
|
|
886
|
+
for (const { doc, claim } of commandClaims) {
|
|
887
|
+
if (rootSet.has(claim.scriptName)) continue;
|
|
888
|
+
const elsewhere = await loadWorkspaceScripts();
|
|
889
|
+
if (elsewhere.has(claim.scriptName)) continue;
|
|
890
|
+
result.issues.push({
|
|
891
|
+
type: "dead-command",
|
|
892
|
+
message: `\`${claim.invocation}\` refers to script "${claim.scriptName}", which exists in no package.json`,
|
|
893
|
+
anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
|
|
894
|
+
confidence: "certain",
|
|
895
|
+
evidence: {
|
|
896
|
+
kind: "missing-script",
|
|
897
|
+
scriptName: claim.scriptName,
|
|
898
|
+
invocation: claim.invocation,
|
|
899
|
+
manifestsChecked,
|
|
900
|
+
availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/audit/checks/deps-changed.ts
|
|
908
|
+
var MANIFEST_COMMITS_CAP = 10;
|
|
909
|
+
var MANIFEST_PATHSPECS = [
|
|
910
|
+
":(glob)**/package.json",
|
|
911
|
+
":(glob)**/build.gradle.kts",
|
|
912
|
+
":(glob)**/build.gradle",
|
|
913
|
+
"settings.gradle.kts",
|
|
914
|
+
"settings.gradle",
|
|
915
|
+
"gradle/libs.versions.toml",
|
|
916
|
+
":(glob)**/Cargo.toml",
|
|
917
|
+
"go.mod",
|
|
918
|
+
"pyproject.toml",
|
|
919
|
+
"requirements.txt",
|
|
920
|
+
"Gemfile",
|
|
921
|
+
"composer.json"
|
|
922
|
+
];
|
|
923
|
+
async function checkDepsChanged(ctx) {
|
|
924
|
+
const result = emptyResult();
|
|
925
|
+
for (const doc of ctx.docs) {
|
|
926
|
+
if (!doc.lastCommit) {
|
|
927
|
+
result.skipped.push({
|
|
928
|
+
check: "deps-changed",
|
|
929
|
+
reason: `${doc.path} has no commit history`
|
|
930
|
+
});
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (doc.dirty) {
|
|
934
|
+
result.skipped.push({
|
|
935
|
+
check: "deps-changed",
|
|
936
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
937
|
+
});
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const range = await commitsTouchingSince(
|
|
941
|
+
ctx.root,
|
|
942
|
+
doc.lastCommit.hash,
|
|
943
|
+
MANIFEST_PATHSPECS
|
|
944
|
+
);
|
|
945
|
+
if (range === null) {
|
|
946
|
+
result.skipped.push({
|
|
947
|
+
check: "deps-changed",
|
|
948
|
+
reason: `${doc.path}: commit range unreachable (shallow clone?)`
|
|
949
|
+
});
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (range.total === 0) continue;
|
|
953
|
+
const latest = range.commits[0];
|
|
954
|
+
result.advisories.push({
|
|
955
|
+
type: "deps-changed",
|
|
956
|
+
message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
|
|
957
|
+
anchor: { doc: doc.path, line: null, excerpt: null },
|
|
958
|
+
evidence: {
|
|
959
|
+
kind: "doc-behind-manifests",
|
|
960
|
+
docLastCommit: doc.lastCommit,
|
|
961
|
+
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
962
|
+
totalCommits: range.total
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/decisions/drift.ts
|
|
970
|
+
import path12 from "path";
|
|
971
|
+
|
|
972
|
+
// src/decisions/decisions.ts
|
|
973
|
+
import fs8 from "fs/promises";
|
|
974
|
+
import path11 from "path";
|
|
975
|
+
import { createHash } from "crypto";
|
|
976
|
+
|
|
977
|
+
// src/context/lexical.ts
|
|
978
|
+
import path10 from "path";
|
|
979
|
+
|
|
980
|
+
// src/decisions/decisions.ts
|
|
981
|
+
function decisionsDir(rootDir) {
|
|
982
|
+
return path11.join(rootDir, ".mason", "decisions");
|
|
983
|
+
}
|
|
984
|
+
async function loadDecisions(rootDir) {
|
|
985
|
+
let entries;
|
|
986
|
+
try {
|
|
987
|
+
entries = await fs8.readdir(decisionsDir(rootDir));
|
|
988
|
+
} catch {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
const records = [];
|
|
992
|
+
for (const entry of entries) {
|
|
993
|
+
if (!entry.endsWith(".json")) continue;
|
|
994
|
+
try {
|
|
995
|
+
const raw = await fs8.readFile(
|
|
996
|
+
path11.join(decisionsDir(rootDir), entry),
|
|
997
|
+
"utf-8"
|
|
998
|
+
);
|
|
999
|
+
const parsed = JSON.parse(raw);
|
|
1000
|
+
if (parsed.version !== 1 || !parsed.id || !parsed.title || !parsed.body) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
records.push(parsed);
|
|
1004
|
+
} catch {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return records.sort((a, b) => a.id.localeCompare(b.id));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/decisions/drift.ts
|
|
1012
|
+
async function computeDecisionDrift(rootDir, decisions) {
|
|
1013
|
+
const resolvedRoot = path12.resolve(rootDir);
|
|
1014
|
+
const records = decisions ?? await loadDecisions(resolvedRoot);
|
|
1015
|
+
const report = {
|
|
1016
|
+
historyAvailable: true,
|
|
1017
|
+
totalDecisions: records.length,
|
|
1018
|
+
staleDecisions: {}
|
|
1019
|
+
};
|
|
1020
|
+
const head = await getCurrentGitHash(resolvedRoot);
|
|
1021
|
+
const changesByHash = /* @__PURE__ */ new Map();
|
|
1022
|
+
for (const record of records) {
|
|
1023
|
+
if (record.status !== "active" || record.files.length === 0) continue;
|
|
1024
|
+
if (record.refreshedHash === head) continue;
|
|
1025
|
+
let touched = changesByHash.get(record.refreshedHash);
|
|
1026
|
+
if (touched === void 0) {
|
|
1027
|
+
const changes = await getChangesWithStatus(
|
|
1028
|
+
resolvedRoot,
|
|
1029
|
+
record.refreshedHash
|
|
1030
|
+
);
|
|
1031
|
+
if (changes === null) {
|
|
1032
|
+
touched = null;
|
|
1033
|
+
} else {
|
|
1034
|
+
touched = /* @__PURE__ */ new Set();
|
|
1035
|
+
for (const change of changes) {
|
|
1036
|
+
touched.add(change.path);
|
|
1037
|
+
if (change.previousPath) touched.add(change.previousPath);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
changesByHash.set(record.refreshedHash, touched);
|
|
1041
|
+
}
|
|
1042
|
+
if (touched === null) {
|
|
1043
|
+
report.historyAvailable = false;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const hits = record.files.filter((f) => touched.has(f));
|
|
1047
|
+
if (hits.length > 0) {
|
|
1048
|
+
report.staleDecisions[record.id] = hits;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return report;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/audit/checks/decision-anchor.ts
|
|
1055
|
+
async function checkDecisionAnchors(ctx) {
|
|
1056
|
+
const result = emptyResult();
|
|
1057
|
+
if (!ctx.decisionsPresent) return result;
|
|
1058
|
+
const records = await loadDecisions(ctx.root);
|
|
1059
|
+
const drift = await computeDecisionDrift(ctx.root, records);
|
|
1060
|
+
if (!drift.historyAvailable) {
|
|
1061
|
+
result.skipped.push({
|
|
1062
|
+
check: "decision-anchor-drift",
|
|
1063
|
+
reason: "some decision base commits are unreachable (shallow clone?)"
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
1067
|
+
for (const [id, changedFiles] of Object.entries(drift.staleDecisions)) {
|
|
1068
|
+
const record = byId.get(id);
|
|
1069
|
+
if (!record) continue;
|
|
1070
|
+
result.advisories.push({
|
|
1071
|
+
type: "decision-anchor-drift",
|
|
1072
|
+
message: `decision "${record.title}" has anchor files that changed since it was verified \u2013 needs human re-verification`,
|
|
1073
|
+
anchor: {
|
|
1074
|
+
doc: `.mason/decisions/${id}.json`,
|
|
1075
|
+
line: null,
|
|
1076
|
+
excerpt: record.title
|
|
1077
|
+
},
|
|
1078
|
+
evidence: {
|
|
1079
|
+
kind: "decision-anchor",
|
|
1080
|
+
decisionId: id,
|
|
1081
|
+
title: record.title,
|
|
1082
|
+
changedFiles,
|
|
1083
|
+
refreshedHash: record.refreshedHash
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/audit/checks/index.ts
|
|
1091
|
+
var CHECKS = {
|
|
1092
|
+
"deleted-reference": checkDeletedReferences,
|
|
1093
|
+
"new-module": checkNewModules,
|
|
1094
|
+
"stale-count": checkStaleCounts,
|
|
1095
|
+
"dead-command": checkDeadCommands,
|
|
1096
|
+
"deps-changed": checkDepsChanged,
|
|
1097
|
+
"decision-anchor-drift": checkDecisionAnchors
|
|
1098
|
+
};
|
|
1099
|
+
function emptyResult() {
|
|
1100
|
+
return { issues: [], advisories: [], skipped: [] };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/audit/audit.ts
|
|
1104
|
+
async function computeAudit(rootDir, options = {}) {
|
|
1105
|
+
const resolvedRoot = path13.resolve(rootDir);
|
|
1106
|
+
const docs = await discoverDocs(resolvedRoot);
|
|
1107
|
+
if (docs.length === 0) return null;
|
|
1108
|
+
const headHash = await getCurrentGitHash(resolvedRoot);
|
|
1109
|
+
const report = {
|
|
1110
|
+
version: 1,
|
|
1111
|
+
root: resolvedRoot,
|
|
1112
|
+
gitAvailable: headHash !== "unknown",
|
|
1113
|
+
docs: docs.map((d) => ({
|
|
1114
|
+
path: d.path,
|
|
1115
|
+
lastCommit: d.lastCommit,
|
|
1116
|
+
dirty: d.dirty,
|
|
1117
|
+
lineCount: d.lineCount
|
|
1118
|
+
})),
|
|
1119
|
+
decisionsChecked: false,
|
|
1120
|
+
issues: [],
|
|
1121
|
+
advisories: [],
|
|
1122
|
+
skippedChecks: [],
|
|
1123
|
+
clean: true
|
|
1124
|
+
};
|
|
1125
|
+
if (!report.gitAvailable) return report;
|
|
1126
|
+
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
1127
|
+
for (const doc of docs) {
|
|
1128
|
+
changesSinceDoc.set(
|
|
1129
|
+
doc.path,
|
|
1130
|
+
doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
let decisionsPresent = false;
|
|
1134
|
+
try {
|
|
1135
|
+
await fs9.access(path13.join(resolvedRoot, ".mason", "decisions"));
|
|
1136
|
+
decisionsPresent = true;
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
report.decisionsChecked = decisionsPresent;
|
|
1140
|
+
const ctx = {
|
|
1141
|
+
root: resolvedRoot,
|
|
1142
|
+
docs,
|
|
1143
|
+
headHash,
|
|
1144
|
+
changesSinceDoc,
|
|
1145
|
+
decisionsPresent
|
|
1146
|
+
};
|
|
1147
|
+
const selected = options.checks ?? ALL_CHECKS;
|
|
1148
|
+
for (const name of ALL_CHECKS) {
|
|
1149
|
+
if (!selected.includes(name)) continue;
|
|
1150
|
+
const { issues, advisories, skipped } = await CHECKS[name](ctx);
|
|
1151
|
+
report.issues.push(...issues);
|
|
1152
|
+
report.advisories.push(...advisories);
|
|
1153
|
+
report.skippedChecks.push(...skipped);
|
|
1154
|
+
}
|
|
1155
|
+
report.clean = report.issues.length === 0;
|
|
1156
|
+
return report;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/audit/cli.ts
|
|
1160
|
+
var USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
|
|
1161
|
+
|
|
1162
|
+
Audits the repo's AI context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)
|
|
1163
|
+
against repo reality: referenced paths that no longer exist, undocumented
|
|
1164
|
+
modules, stale counts, dead npm scripts, and manifests newer than the doc.
|
|
1165
|
+
Deterministic: no LLM call, no network \u2013 safe for CI. Works on any repo with
|
|
1166
|
+
a context file; no Mason setup required.
|
|
1167
|
+
|
|
1168
|
+
Options:
|
|
1169
|
+
--dir <path> Project root to audit (default: current directory)
|
|
1170
|
+
--json Print the full audit report as JSON (additive-only schema)
|
|
1171
|
+
--fix-prompt When issues exist, print a work order for ANY coding agent
|
|
1172
|
+
(Claude, Codex, Gemini, ...) \u2013 pipe it to your agent CLI to
|
|
1173
|
+
close the loop. Prints the clean summary when there are none.
|
|
1174
|
+
--checks <list> Comma-separated subset of checks to run (default: all):
|
|
1175
|
+
${ALL_CHECKS.join(", ")}
|
|
1176
|
+
--help Show this help
|
|
1177
|
+
|
|
1178
|
+
Exit codes:
|
|
1179
|
+
0 no issues (advisories may still be present)
|
|
1180
|
+
1 provable issues found
|
|
1181
|
+
2 error (no context file, not a git repository, bad arguments)`;
|
|
1182
|
+
function parseArgs(argv) {
|
|
1183
|
+
const parsed = {
|
|
1184
|
+
dir: process.cwd(),
|
|
1185
|
+
json: false,
|
|
1186
|
+
fixPrompt: false,
|
|
1187
|
+
help: false,
|
|
1188
|
+
checks: void 0
|
|
1189
|
+
};
|
|
1190
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1191
|
+
const arg = argv[i];
|
|
1192
|
+
if (arg === "--json") {
|
|
1193
|
+
parsed.json = true;
|
|
1194
|
+
} else if (arg === "--fix-prompt") {
|
|
1195
|
+
parsed.fixPrompt = true;
|
|
1196
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1197
|
+
parsed.help = true;
|
|
1198
|
+
} else if (arg === "--dir") {
|
|
1199
|
+
const value = argv[++i];
|
|
1200
|
+
if (!value) throw new Error("--dir requires a path argument");
|
|
1201
|
+
parsed.dir = value;
|
|
1202
|
+
} else if (arg === "--checks") {
|
|
1203
|
+
const value = argv[++i];
|
|
1204
|
+
if (!value) throw new Error("--checks requires a comma-separated list");
|
|
1205
|
+
const names = value.split(",").map((n) => n.trim()).filter(Boolean);
|
|
1206
|
+
for (const name of names) {
|
|
1207
|
+
if (!ALL_CHECKS.includes(name)) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`Unknown check: ${name} (valid: ${ALL_CHECKS.join(", ")})`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
parsed.checks = names;
|
|
1214
|
+
} else if (!arg.startsWith("-") && parsed.dir === process.cwd()) {
|
|
1215
|
+
parsed.dir = arg;
|
|
1216
|
+
} else {
|
|
1217
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return parsed;
|
|
1221
|
+
}
|
|
1222
|
+
function issueLine(issue) {
|
|
1223
|
+
const where = issue.anchor.line !== null ? `line ${issue.anchor.line}` : "doc-level";
|
|
1224
|
+
const likely = issue.confidence === "likely" ? " (likely)" : "";
|
|
1225
|
+
return ` [${issue.type}]${likely} ${where}: ${issue.message}`;
|
|
1226
|
+
}
|
|
1227
|
+
function formatAuditSummary(report) {
|
|
1228
|
+
const lines = [];
|
|
1229
|
+
for (const doc of report.docs) {
|
|
1230
|
+
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
1231
|
+
const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
|
|
1232
|
+
if (docIssues.length === 0) {
|
|
1233
|
+
lines.push(`${doc.path} \u2013 clean (${committed})`);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
lines.push(
|
|
1237
|
+
`${doc.path} \u2013 ${docIssues.length} issue${docIssues.length === 1 ? "" : "s"} (${committed})`
|
|
1238
|
+
);
|
|
1239
|
+
for (const issue of docIssues) lines.push(issueLine(issue));
|
|
1240
|
+
}
|
|
1241
|
+
const docPaths = new Set(report.docs.map((d) => d.path));
|
|
1242
|
+
for (const issue of report.issues) {
|
|
1243
|
+
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
1244
|
+
}
|
|
1245
|
+
if (report.advisories.length > 0) {
|
|
1246
|
+
lines.push("Advisories (do not affect the exit code):");
|
|
1247
|
+
for (const advisory of report.advisories) {
|
|
1248
|
+
lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (report.skippedChecks.length > 0) {
|
|
1252
|
+
for (const skip of report.skippedChecks) {
|
|
1253
|
+
lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
lines.push(
|
|
1257
|
+
report.clean ? `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? "" : "s"} audited).` : `${report.issues.length} issue${report.issues.length === 1 ? "" : "s"} across ${report.docs.length} doc${report.docs.length === 1 ? "" : "s"}.`
|
|
1258
|
+
);
|
|
1259
|
+
return lines.join("\n");
|
|
1260
|
+
}
|
|
1261
|
+
function formatFixPrompt(report) {
|
|
1262
|
+
const flaggedDocs = [...new Set(report.issues.map((i) => i.anchor.doc))];
|
|
1263
|
+
const lines = [];
|
|
1264
|
+
lines.push(
|
|
1265
|
+
"The AI context files in this repository contain claims that are provably out of date. Fix ONLY the flagged claims. Work autonomously; do not ask questions."
|
|
1266
|
+
);
|
|
1267
|
+
lines.push("");
|
|
1268
|
+
lines.push("RULES:");
|
|
1269
|
+
lines.push(
|
|
1270
|
+
`- Edit ONLY these files: ${flaggedDocs.join(", ")}. Never modify source code, configs, or anything else \u2013 the docs must be brought to match the code, not the other way around.`
|
|
1271
|
+
);
|
|
1272
|
+
lines.push(
|
|
1273
|
+
"- Keep diffs minimal: change the smallest span that makes each claim true."
|
|
1274
|
+
);
|
|
1275
|
+
lines.push(
|
|
1276
|
+
"- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository."
|
|
1277
|
+
);
|
|
1278
|
+
lines.push(
|
|
1279
|
+
"- deleted-reference: if evidence shows renamedTo, update the path; otherwise remove the reference, or rephrase to past tense if the sentence is about history. Deleted paths inside directory trees: delete the tree line."
|
|
1280
|
+
);
|
|
1281
|
+
lines.push(
|
|
1282
|
+
"- stale-count: replace the number with the actual count from the evidence."
|
|
1283
|
+
);
|
|
1284
|
+
lines.push(
|
|
1285
|
+
"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
|
|
1286
|
+
);
|
|
1287
|
+
lines.push(
|
|
1288
|
+
"- new-module: add a one-line factual mention of the directory where sibling modules are described; read the directory's files first and describe only what you verified."
|
|
1289
|
+
);
|
|
1290
|
+
lines.push(
|
|
1291
|
+
"- Do NOT touch anything listed under ADVISORIES \u2013 list them in your summary for human review instead."
|
|
1292
|
+
);
|
|
1293
|
+
lines.push("");
|
|
1294
|
+
lines.push("AUDIT REPORT (deterministic, computed against git HEAD):");
|
|
1295
|
+
lines.push(
|
|
1296
|
+
JSON.stringify(
|
|
1297
|
+
{ issues: report.issues, advisories: report.advisories },
|
|
1298
|
+
null,
|
|
1299
|
+
2
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
lines.push("");
|
|
1303
|
+
lines.push(
|
|
1304
|
+
"Finish by summarizing each edit and citing the evidence item it resolves."
|
|
1305
|
+
);
|
|
1306
|
+
return lines.join("\n");
|
|
1307
|
+
}
|
|
1308
|
+
async function runAuditCli(argv, io = {
|
|
1309
|
+
out: (line) => process.stdout.write(`${line}
|
|
1310
|
+
`),
|
|
1311
|
+
err: (line) => process.stderr.write(`${line}
|
|
1312
|
+
`)
|
|
1313
|
+
}) {
|
|
1314
|
+
let args;
|
|
1315
|
+
try {
|
|
1316
|
+
args = parseArgs(argv);
|
|
1317
|
+
if (args.json && args.fixPrompt) {
|
|
1318
|
+
throw new Error("--json and --fix-prompt are mutually exclusive");
|
|
1319
|
+
}
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
1322
|
+
io.err(USAGE);
|
|
1323
|
+
return 2;
|
|
1324
|
+
}
|
|
1325
|
+
if (args.help) {
|
|
1326
|
+
io.out(USAGE);
|
|
1327
|
+
return 0;
|
|
1328
|
+
}
|
|
1329
|
+
const rootDir = path14.resolve(args.dir);
|
|
1330
|
+
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
1331
|
+
if (!report) {
|
|
1332
|
+
io.err(
|
|
1333
|
+
`No CLAUDE.md, .claude/CLAUDE.md, or AGENTS.md found in ${rootDir}.`
|
|
1334
|
+
);
|
|
1335
|
+
return 2;
|
|
1336
|
+
}
|
|
1337
|
+
if (!report.gitAvailable) {
|
|
1338
|
+
io.err(
|
|
1339
|
+
`Could not determine git HEAD in ${rootDir} \u2013 not a git repository, or git is unavailable.`
|
|
1340
|
+
);
|
|
1341
|
+
return 2;
|
|
1342
|
+
}
|
|
1343
|
+
if (args.fixPrompt) {
|
|
1344
|
+
io.out(
|
|
1345
|
+
report.clean ? formatAuditSummary(report) : formatFixPrompt(report)
|
|
1346
|
+
);
|
|
1347
|
+
return report.clean ? 0 : 1;
|
|
1348
|
+
}
|
|
1349
|
+
if (args.json) {
|
|
1350
|
+
io.out(JSON.stringify(report, null, 2));
|
|
1351
|
+
} else {
|
|
1352
|
+
io.out(formatAuditSummary(report));
|
|
1353
|
+
}
|
|
1354
|
+
return report.clean ? 0 : 1;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// bin/mason-audit.ts
|
|
1358
|
+
runAuditCli(process.argv.slice(2)).then(
|
|
1359
|
+
(code) => process.exit(code),
|
|
1360
|
+
(err) => {
|
|
1361
|
+
process.stderr.write(`mason-audit error: ${err}
|
|
1362
|
+
`);
|
|
1363
|
+
process.exit(2);
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
//# sourceMappingURL=mason-audit.js.map
|