mason-context 0.14.1 → 0.14.2

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/mason.js CHANGED
@@ -262,15 +262,15 @@ function parseExecution(raw) {
262
262
  function automationFailure(error) {
263
263
  const recorded = failureSchema.safeParse(error?.failure);
264
264
  if (recorded.success) return recorded.data;
265
- const message3 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
265
+ const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
266
266
  const codes = /* @__PURE__ */ new Set();
267
267
  let cause = error;
268
268
  for (let i = 0; cause && i < 8; i++) {
269
269
  codes.add(String(cause.code));
270
270
  cause = cause.cause;
271
271
  }
272
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message3) ? "inputs-changed" : /Automation is busy/.test(message3) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message3) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message3) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message3) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
- return { code, message: message3, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
272
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
+ return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
274
274
  }
275
275
  function failureMessage(error) {
276
276
  const failure = automationFailure(error);
@@ -1105,9 +1105,133 @@ var init_deleted_reference = __esm({
1105
1105
  }
1106
1106
  });
1107
1107
 
1108
- // src/audit/checks/new-module.ts
1109
- import fg2 from "fast-glob";
1108
+ // src/audit/inputs.ts
1109
+ import fs6 from "fs/promises";
1110
+ import * as nativeFs from "fs";
1110
1111
  import path9 from "path";
1112
+ import { execFile as execFile6 } from "child_process";
1113
+ import { promisify as promisify6 } from "util";
1114
+ import fg2 from "fast-glob";
1115
+ async function auditInputPath(root, file) {
1116
+ if (file === ".") return fs6.realpath(root);
1117
+ try {
1118
+ return await storePath(root, file);
1119
+ } catch (error) {
1120
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
1121
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
1122
+ }
1123
+ throw error;
1124
+ }
1125
+ }
1126
+ async function readAuditInput(root, file) {
1127
+ try {
1128
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
1129
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
1130
+ return value;
1131
+ } catch (error) {
1132
+ if (error.code === "ENOENT") return null;
1133
+ throw error;
1134
+ }
1135
+ }
1136
+ async function auditGlob(root, patterns, options) {
1137
+ if (Array.isArray(patterns) && !patterns.length) return [];
1138
+ const directories = /* @__PURE__ */ new Set();
1139
+ const readdir2 = ((directory, opts, callback) => {
1140
+ const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
1141
+ directories.add(relative);
1142
+ if (directories.size > MAX_INPUTS) {
1143
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
1144
+ return;
1145
+ }
1146
+ auditInputPath(root, relative).then(() => {
1147
+ nativeFs.readdir(directory, opts, callback);
1148
+ }, callback);
1149
+ });
1150
+ const lstat2 = ((file, callback) => {
1151
+ const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
1152
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1153
+ });
1154
+ const stream = fg2.stream(patterns, {
1155
+ cwd: root,
1156
+ ignore: options.ignore,
1157
+ onlyFiles: false,
1158
+ onlyDirectories: options.onlyDirectories,
1159
+ objectMode: true,
1160
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
1161
+ fs: { readdir: readdir2, lstat: lstat2 },
1162
+ concurrency: 16
1163
+ });
1164
+ const matches = [];
1165
+ try {
1166
+ for await (const value of stream) {
1167
+ const entry = value;
1168
+ const file = entry.path;
1169
+ if (options.select && !options.select(file)) continue;
1170
+ await auditInputPath(root, file);
1171
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1172
+ matches.push(file);
1173
+ if (matches.length > MAX_INPUTS) {
1174
+ const largest = [...matches.reduce((counts, item) => {
1175
+ const dir = item.split("/")[0];
1176
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
1177
+ return counts;
1178
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1179
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
1180
+ }
1181
+ }
1182
+ } finally {
1183
+ stream.destroy();
1184
+ }
1185
+ return matches.sort();
1186
+ }
1187
+ async function gitSourcePaths(root) {
1188
+ const { stdout } = await execute(
1189
+ "git",
1190
+ [
1191
+ "ls-files",
1192
+ "-z",
1193
+ "--cached",
1194
+ "--others",
1195
+ "--exclude-standard",
1196
+ "--",
1197
+ ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
1198
+ ],
1199
+ { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
1200
+ );
1201
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
1202
+ }
1203
+ async function gitIgnoredPaths(root, files) {
1204
+ if (!files.length) return /* @__PURE__ */ new Set();
1205
+ const operation = execute(
1206
+ "git",
1207
+ ["check-ignore", "-z", "--stdin"],
1208
+ { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
1209
+ );
1210
+ operation.child.stdin.on("error", () => {
1211
+ });
1212
+ operation.child.stdin.end(files.join("\0") + "\0");
1213
+ try {
1214
+ return new Set((await operation).stdout.split("\0").filter(Boolean));
1215
+ } catch (error) {
1216
+ if (error.code === 1) return /* @__PURE__ */ new Set();
1217
+ throw error;
1218
+ }
1219
+ }
1220
+ var execute, MAX_INPUTS;
1221
+ var init_inputs = __esm({
1222
+ "src/audit/inputs.ts"() {
1223
+ "use strict";
1224
+ init_files();
1225
+ init_storage();
1226
+ execute = promisify6(execFile6);
1227
+ MAX_INPUTS = 1e5;
1228
+ }
1229
+ });
1230
+
1231
+ // src/audit/checks/new-module.ts
1232
+ import fg3 from "fast-glob";
1233
+ import fs7 from "fs/promises";
1234
+ import path10 from "path";
1111
1235
  function escapeRegExp(text2) {
1112
1236
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1113
1237
  }
@@ -1118,22 +1242,6 @@ function isMentioned(combinedDocs, name) {
1118
1242
  );
1119
1243
  return re.test(combinedDocs);
1120
1244
  }
1121
- async function listSubdirs(absDir) {
1122
- const dirs = await fg2("*", {
1123
- cwd: absDir,
1124
- onlyDirectories: true,
1125
- suppressErrors: true
1126
- });
1127
- return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
1128
- }
1129
- async function countSourceFiles(absDir) {
1130
- const files = await fg2(SOURCE_GLOB, {
1131
- cwd: absDir,
1132
- ignore: SOURCE_IGNORE,
1133
- suppressErrors: true
1134
- });
1135
- return files.length;
1136
- }
1137
1245
  async function checkNewModules(ctx) {
1138
1246
  const result = emptyResult();
1139
1247
  if (ctx.docs.length === 0) return result;
@@ -1161,21 +1269,48 @@ async function checkNewModules(ctx) {
1161
1269
  return result;
1162
1270
  }
1163
1271
  async function moduleCandidates(root, combinedDocs) {
1272
+ const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1273
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1274
+ const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1275
+ const listSubdirs = async (dir) => {
1276
+ const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1277
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
1278
+ const ignored = await gitIgnoredPaths(root, dirs);
1279
+ const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1280
+ return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
1281
+ ignore,
1282
+ onlyDirectories: true,
1283
+ label: "Module directory discovery",
1284
+ select: (file) => visible.has(file)
1285
+ })).map((file) => path10.posix.basename(file));
1286
+ };
1287
+ const countSourceFiles = async (dir) => {
1288
+ const files = sources.filter((file) => file.startsWith(dir + "/"));
1289
+ let count3 = 0;
1290
+ for (let offset = 0; offset < files.length; offset += 256) {
1291
+ count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1292
+ ignore,
1293
+ followSymbolicLinks: false,
1294
+ label: `Source discovery in ${dir}`
1295
+ })).length;
1296
+ if (count3 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1297
+ }
1298
+ return count3;
1299
+ };
1164
1300
  const candidates = [];
1165
- for (const topDir of await listSubdirs(root)) {
1166
- const absTop = path9.join(root, topDir);
1301
+ for (const topDir of await listSubdirs(".")) {
1167
1302
  const topMentioned = isMentioned(combinedDocs, topDir);
1168
1303
  if (!topMentioned) {
1169
- const count3 = await countSourceFiles(absTop);
1304
+ const count3 = await countSourceFiles(topDir);
1170
1305
  if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
1171
1306
  continue;
1172
1307
  }
1173
- const subdirs = await listSubdirs(absTop);
1308
+ const subdirs = await listSubdirs(topDir);
1174
1309
  const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
1175
1310
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1176
1311
  for (const sub of subdirs) {
1177
1312
  if (isMentioned(combinedDocs, sub)) continue;
1178
- const count3 = await countSourceFiles(path9.join(absTop, sub));
1313
+ const count3 = await countSourceFiles(`${topDir}/${sub}`);
1179
1314
  if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1180
1315
  candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
1181
1316
  }
@@ -1187,7 +1322,8 @@ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1187
1322
  var init_new_module = __esm({
1188
1323
  "src/audit/checks/new-module.ts"() {
1189
1324
  "use strict";
1190
- init_snapshot();
1325
+ init_files();
1326
+ init_inputs();
1191
1327
  init_git();
1192
1328
  init_checks();
1193
1329
  DIR_DENYLIST = /* @__PURE__ */ new Set([
@@ -1215,19 +1351,10 @@ var init_new_module = __esm({
1215
1351
  });
1216
1352
 
1217
1353
  // src/audit/checks/stale-count.ts
1218
- import fs6 from "fs/promises";
1219
- import path10 from "path";
1220
- import fg3 from "fast-glob";
1221
- async function readIfExists(absPath) {
1222
- try {
1223
- return await fs6.readFile(absPath, "utf-8");
1224
- } catch {
1225
- return null;
1226
- }
1227
- }
1354
+ import path11 from "path";
1228
1355
  async function countGradleModules(root) {
1229
1356
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1230
- const content2 = await readIfExists(path10.join(root, name));
1357
+ const content2 = await readAuditInput(root, name);
1231
1358
  if (content2 === null) continue;
1232
1359
  const members = [];
1233
1360
  for (const call of content2.matchAll(/include\s*\(([^)]*)\)/g)) {
@@ -1241,26 +1368,28 @@ async function countGradleModules(root) {
1241
1368
  return null;
1242
1369
  }
1243
1370
  async function countNpmWorkspaces(root) {
1244
- const pkgRaw = await readIfExists(path10.join(root, "package.json"));
1371
+ const pkgRaw = await readAuditInput(root, "package.json");
1245
1372
  if (pkgRaw !== null) {
1373
+ let globs = [];
1246
1374
  try {
1247
1375
  const pkg = JSON.parse(pkgRaw);
1248
- const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
1249
- if (globs.length > 0) {
1250
- const matched = await fg3(
1251
- globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1252
- { cwd: root, ignore: ["**/node_modules/**"] }
1253
- );
1254
- return {
1255
- actual: matched.length,
1256
- countedFrom: "package.json workspaces",
1257
- members: matched.map((m) => path10.dirname(m)).sort()
1258
- };
1259
- }
1376
+ globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
1260
1377
  } catch {
1261
1378
  }
1379
+ if (globs.length > 0) {
1380
+ const matched = await auditGlob(
1381
+ root,
1382
+ globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1383
+ { ignore: ["**/node_modules/**"], label: "npm workspace discovery" }
1384
+ );
1385
+ return {
1386
+ actual: matched.length,
1387
+ countedFrom: "package.json workspaces",
1388
+ members: matched.map((m) => path11.dirname(m)).sort()
1389
+ };
1390
+ }
1262
1391
  }
1263
- const pnpmRaw = await readIfExists(path10.join(root, "pnpm-workspace.yaml"));
1392
+ const pnpmRaw = await readAuditInput(root, "pnpm-workspace.yaml");
1264
1393
  if (pnpmRaw !== null) {
1265
1394
  const globs = [];
1266
1395
  let inPackages = false;
@@ -1279,21 +1408,22 @@ async function countNpmWorkspaces(root) {
1279
1408
  }
1280
1409
  }
1281
1410
  if (globs.length > 0) {
1282
- const matched = await fg3(
1411
+ const matched = await auditGlob(
1412
+ root,
1283
1413
  globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1284
- { cwd: root, ignore: ["**/node_modules/**"] }
1414
+ { ignore: ["**/node_modules/**"], label: "pnpm workspace discovery" }
1285
1415
  );
1286
1416
  return {
1287
1417
  actual: matched.length,
1288
1418
  countedFrom: "pnpm-workspace.yaml",
1289
- members: matched.map((m) => path10.dirname(m)).sort()
1419
+ members: matched.map((m) => path11.dirname(m)).sort()
1290
1420
  };
1291
1421
  }
1292
1422
  }
1293
1423
  return null;
1294
1424
  }
1295
1425
  async function countCargoCrates(root) {
1296
- const content2 = await readIfExists(path10.join(root, "Cargo.toml"));
1426
+ const content2 = await readAuditInput(root, "Cargo.toml");
1297
1427
  if (content2 === null) return null;
1298
1428
  const membersBlock = content2.match(/members\s*=\s*\[([\s\S]*?)\]/);
1299
1429
  if (!membersBlock) return null;
@@ -1304,12 +1434,12 @@ async function countCargoCrates(root) {
1304
1434
  const members = /* @__PURE__ */ new Set();
1305
1435
  for (const entry of entries) {
1306
1436
  if (/[*?[\]{}]/.test(entry)) {
1307
- const matched = await fg3(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1308
- cwd: root,
1309
- ignore: ["**/target/**"]
1437
+ const matched = await auditGlob(root, `${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1438
+ ignore: ["**/target/**"],
1439
+ label: "Cargo workspace discovery"
1310
1440
  });
1311
- for (const m of matched) members.add(path10.dirname(m));
1312
- } else if (await readIfExists(path10.join(root, entry, "Cargo.toml")) !== null) {
1441
+ for (const m of matched) members.add(path11.dirname(m));
1442
+ } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1313
1443
  members.add(entry);
1314
1444
  }
1315
1445
  }
@@ -1363,18 +1493,18 @@ var MEMBERS_CAP;
1363
1493
  var init_stale_count = __esm({
1364
1494
  "src/audit/checks/stale-count.ts"() {
1365
1495
  "use strict";
1496
+ init_inputs();
1366
1497
  init_checks();
1367
1498
  MEMBERS_CAP = 50;
1368
1499
  }
1369
1500
  });
1370
1501
 
1371
1502
  // src/audit/checks/dead-command.ts
1372
- import fs7 from "fs/promises";
1373
- import path11 from "path";
1374
- import fg4 from "fast-glob";
1375
- async function scriptsOf(absManifest) {
1503
+ async function scriptsOf(root, manifest) {
1504
+ const raw = await readAuditInput(root, manifest);
1505
+ if (raw === null) return null;
1376
1506
  try {
1377
- const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
1507
+ const pkg = JSON.parse(raw);
1378
1508
  return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1379
1509
  } catch {
1380
1510
  return null;
@@ -1386,7 +1516,7 @@ async function checkDeadCommands(ctx) {
1386
1516
  (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1387
1517
  );
1388
1518
  if (commandClaims.length === 0) return result;
1389
- const rootScripts = await scriptsOf(path11.join(ctx.root, "package.json"));
1519
+ const rootScripts = await scriptsOf(ctx.root, "package.json");
1390
1520
  if (rootScripts === null) {
1391
1521
  result.skipped.push({
1392
1522
  check: "dead-command",
@@ -1403,7 +1533,7 @@ async function checkDeadCommands(ctx) {
1403
1533
  const manifests = await commandManifests(ctx.root);
1404
1534
  manifestsChecked = ["package.json", ...manifests.sort()];
1405
1535
  for (const manifest of manifests) {
1406
- const scripts = await scriptsOf(path11.join(ctx.root, manifest));
1536
+ const scripts = await scriptsOf(ctx.root, manifest);
1407
1537
  for (const name of scripts ?? []) workspaceScripts.add(name);
1408
1538
  }
1409
1539
  return workspaceScripts;
@@ -1429,23 +1559,24 @@ async function checkDeadCommands(ctx) {
1429
1559
  return result;
1430
1560
  }
1431
1561
  function commandManifests(root) {
1432
- return fg4("**/package.json", {
1433
- cwd: root,
1434
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"]
1562
+ return auditGlob(root, "**/package.json", {
1563
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"],
1564
+ label: "Command manifest discovery"
1435
1565
  });
1436
1566
  }
1437
1567
  var AVAILABLE_SCRIPTS_CAP;
1438
1568
  var init_dead_command = __esm({
1439
1569
  "src/audit/checks/dead-command.ts"() {
1440
1570
  "use strict";
1571
+ init_inputs();
1441
1572
  init_checks();
1442
1573
  AVAILABLE_SCRIPTS_CAP = 30;
1443
1574
  }
1444
1575
  });
1445
1576
 
1446
1577
  // src/audit/release-metadata.ts
1447
- import { execFile as execFile6 } from "child_process";
1448
- import { promisify as promisify6 } from "util";
1578
+ import { execFile as execFile7 } from "child_process";
1579
+ import { promisify as promisify7 } from "util";
1449
1580
  async function git(root, args2) {
1450
1581
  return (await exec6("git", args2, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1451
1582
  }
@@ -1497,7 +1628,7 @@ var exec6;
1497
1628
  var init_release_metadata = __esm({
1498
1629
  "src/audit/release-metadata.ts"() {
1499
1630
  "use strict";
1500
- exec6 = promisify6(execFile6);
1631
+ exec6 = promisify7(execFile7);
1501
1632
  }
1502
1633
  });
1503
1634
 
@@ -1736,7 +1867,7 @@ var init_provenance = __esm({
1736
1867
  sources: z3.array(decisionSourceSchema).max(20),
1737
1868
  history: z3.array(eventSchema).min(1)
1738
1869
  }).superRefine((record, ctx) => {
1739
- const invalid = (message3) => ctx.addIssue({ code: "custom", message: message3 });
1870
+ const invalid = (message2) => ctx.addIssue({ code: "custom", message: message2 });
1740
1871
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1741
1872
  let previous;
1742
1873
  for (const event of record.history) {
@@ -2262,9 +2393,9 @@ __export(evidence_exports, {
2262
2393
  import fs11 from "fs/promises";
2263
2394
  import path17 from "path";
2264
2395
  import { createHash as createHash3 } from "crypto";
2265
- import { execFile as execFile7 } from "child_process";
2266
- import { promisify as promisify7 } from "util";
2267
- import fg5 from "fast-glob";
2396
+ import { execFile as execFile8 } from "child_process";
2397
+ import { promisify as promisify8 } from "util";
2398
+ import fg4 from "fast-glob";
2268
2399
  import { z as z5 } from "zod";
2269
2400
  async function git2(root, ...args2) {
2270
2401
  return (await exec7("git", args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
@@ -2280,34 +2411,13 @@ async function workspace(dir) {
2280
2411
  }
2281
2412
  return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2282
2413
  }
2283
- async function content(root, file) {
2284
- try {
2285
- const value = await readBoundedFile(await storePath(root, file), 10 * 1024 * 1024);
2286
- if (value === null) throw new Error("Unreadable or oversized audit input: " + file);
2287
- return value;
2288
- } catch (error) {
2289
- if (error.code === "ENOENT") return null;
2290
- throw error;
2291
- }
2292
- }
2293
2414
  async function readInputs(root) {
2294
- const [headText, docStatus, inventory, shallowPath, replacements] = await Promise.all([
2415
+ const [headText, docStatus, shallowPath, replacements] = await Promise.all([
2295
2416
  git2(root, "rev-parse", "HEAD"),
2296
2417
  git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
2297
- fg5("**/*", {
2298
- cwd: root,
2299
- dot: true,
2300
- onlyFiles: false,
2301
- followSymbolicLinks: false,
2302
- objectMode: true,
2303
- ignore: SOURCE_IGNORE
2304
- }),
2305
2418
  git2(root, "rev-parse", "--git-path", "shallow"),
2306
2419
  git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2307
2420
  ]);
2308
- const entries = inventory.filter((f) => !internal(f.path) && f.path !== ".git").sort((a, b) => a.path.localeCompare(b.path));
2309
- const files = entries.map((f) => f.path);
2310
- if (files.length > 1e5) throw new Error("Automation input inventory exceeds 100,000 paths; use an explicit scoped audit.");
2311
2421
  const head = headText.trim();
2312
2422
  let shallow = null;
2313
2423
  try {
@@ -2328,9 +2438,19 @@ async function readInputs(root) {
2328
2438
  claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2329
2439
  }
2330
2440
  }
2331
- if (entries.some((f) => f.dirent.isSymbolicLink())) throw new Error("Automation inventory contains a symbolic link; use an explicit audit to inspect its scope. No cached verification was recorded.");
2332
2441
  const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
2333
2442
  const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
2443
+ const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
2444
+ const rootPackage = await content(root, "package.json");
2445
+ let rootScripts = null;
2446
+ if (rootPackage !== null) {
2447
+ try {
2448
+ const pkg = JSON.parse(rootPackage);
2449
+ rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
2450
+ } catch {
2451
+ }
2452
+ }
2453
+ const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
2334
2454
  const decisionDirectory = await storePath(root, ".mason/decisions");
2335
2455
  const decisionPresence = await fs11.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2336
2456
  if (error.code === "ENOENT") return "absent";
@@ -2339,16 +2459,19 @@ async function readInputs(root) {
2339
2459
  const [modules, counts, manifests, decisionFiles] = await Promise.all([
2340
2460
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
2341
2461
  Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
2342
- commandManifests(root),
2343
- fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2462
+ needsWorkspaceScripts ? commandManifests(root) : [],
2463
+ fg4(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2344
2464
  ]);
2345
- const packages = await Promise.all(["package.json", ...manifests.sort()].map(async (file) => [file, await content(root, file)]));
2346
- const decisions = await Promise.all(decisionFiles.sort().map(async (file) => [file, await content(root, file)]));
2465
+ const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
2466
+ const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
2467
+ await storePath(root, file);
2468
+ return [file, await content(root, file)];
2469
+ }));
2347
2470
  const [status, index2] = decisions.length ? await Promise.all([
2348
2471
  git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2349
2472
  git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2350
2473
  ]) : ["", ""];
2351
- const common = [2, engineVersion, head, shallow, replacements, docContents];
2474
+ const common = [3, engineVersion, head, shallow, replacements, docContents];
2352
2475
  const keys = {
2353
2476
  "deleted-reference": hash([common, claims, docStatus]),
2354
2477
  "new-module": hash([common, modules]),
@@ -2384,7 +2507,7 @@ function checkCache(raw, inputs) {
2384
2507
  return { version: 1, entries: canonical, digest: hash(canonical) };
2385
2508
  } };
2386
2509
  }
2387
- var exec7, engineVersion, hash, internal, cacheSchema;
2510
+ var exec7, engineVersion, hash, content, internal, cacheSchema;
2388
2511
  var init_evidence = __esm({
2389
2512
  "src/automation/evidence.ts"() {
2390
2513
  "use strict";
@@ -2392,14 +2515,15 @@ var init_evidence = __esm({
2392
2515
  init_claims();
2393
2516
  init_repair();
2394
2517
  init_checks();
2395
- init_files();
2518
+ init_inputs();
2396
2519
  init_new_module();
2397
2520
  init_stale_count();
2398
2521
  init_dead_command();
2399
2522
  init_storage();
2400
- exec7 = promisify7(execFile7);
2401
- engineVersion = true ? "0.14.1" : "development";
2523
+ exec7 = promisify8(execFile8);
2524
+ engineVersion = true ? "0.14.2" : "development";
2402
2525
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2526
+ content = readAuditInput;
2403
2527
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2404
2528
  cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
2405
2529
  }
@@ -2897,8 +3021,8 @@ async function runAutomationHook(host, stdin) {
2897
3021
  }
2898
3022
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
2899
3023
  } catch (error) {
2900
- const message3 = failureMessage(error);
2901
- return { systemMessage: message3, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message3 } } : {} };
3024
+ const message2 = failureMessage(error);
3025
+ return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
2902
3026
  }
2903
3027
  }
2904
3028
  function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
@@ -3108,8 +3232,8 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
3108
3232
  });
3109
3233
 
3110
3234
  // src/review/cochange.ts
3111
- import { execFile as execFile8 } from "child_process";
3112
- import { promisify as promisify8 } from "util";
3235
+ import { execFile as execFile9 } from "child_process";
3236
+ import { promisify as promisify9 } from "util";
3113
3237
  async function buildMatrix(resolvedRoot) {
3114
3238
  try {
3115
3239
  const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
@@ -3174,7 +3298,7 @@ var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
3174
3298
  var init_cochange = __esm({
3175
3299
  "src/review/cochange.ts"() {
3176
3300
  "use strict";
3177
- exec8 = promisify8(execFile8);
3301
+ exec8 = promisify9(execFile9);
3178
3302
  HISTORY_COMMITS = 1500;
3179
3303
  MIN_FILE_COMMITS = 5;
3180
3304
  MIN_SHARED_COMMITS = 4;
@@ -3217,8 +3341,8 @@ var init_paths2 = __esm({
3217
3341
  });
3218
3342
 
3219
3343
  // src/review/evidence/types.ts
3220
- function messagePreview(message3) {
3221
- return { message: message3.slice(0, 4e3), ...message3.length > 4e3 ? { truncated: true } : {} };
3344
+ function messagePreview(message2) {
3345
+ return { message: message2.slice(0, 4e3), ...message2.length > 4e3 ? { truncated: true } : {} };
3222
3346
  }
3223
3347
  var MAX_FINDINGS;
3224
3348
  var init_types2 = __esm({
@@ -3639,8 +3763,8 @@ var init_evidence2 = __esm({
3639
3763
  // src/review/review.ts
3640
3764
  import fs14 from "fs/promises";
3641
3765
  import path19 from "path";
3642
- import { execFile as execFile9 } from "child_process";
3643
- import { promisify as promisify9 } from "util";
3766
+ import { execFile as execFile10 } from "child_process";
3767
+ import { promisify as promisify10 } from "util";
3644
3768
  async function resolveMergeBase(resolvedRoot, base, head) {
3645
3769
  try {
3646
3770
  const { stdout } = await exec9("git", ["merge-base", base, head], {
@@ -3762,7 +3886,7 @@ var init_review = __esm({
3762
3886
  init_cochange();
3763
3887
  init_evidence2();
3764
3888
  init_snapshot();
3765
- exec9 = promisify9(execFile9);
3889
+ exec9 = promisify10(execFile10);
3766
3890
  MAX_ANALYZED_FILES = 50;
3767
3891
  }
3768
3892
  });
@@ -4167,8 +4291,8 @@ import fs17 from "fs/promises";
4167
4291
  import path22 from "path";
4168
4292
  import { fileURLToPath } from "url";
4169
4293
  import { randomUUID as randomUUID6, createHash as createHash5 } from "crypto";
4170
- import { execFile as execFile10 } from "child_process";
4171
- import { promisify as promisify10 } from "util";
4294
+ import { execFile as execFile11 } from "child_process";
4295
+ import { promisify as promisify11 } from "util";
4172
4296
  async function packageRoot() {
4173
4297
  let directory = path22.dirname(fileURLToPath(import.meta.url));
4174
4298
  for (let i = 0; i < 8; i++) {
@@ -4276,7 +4400,7 @@ var init_runtime2 = __esm({
4276
4400
  init_storage();
4277
4401
  init_evidence();
4278
4402
  init_bundle();
4279
- exec10 = promisify10(execFile10);
4403
+ exec10 = promisify11(execFile11);
4280
4404
  checksum = (bytes) => createHash5("sha256").update(bytes).digest("hex");
4281
4405
  BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
4282
4406
  }
@@ -4363,7 +4487,7 @@ function summarizeActivation(status) {
4363
4487
  ` Runtime: ${state.runtime}; MCP: ${state.mcp}; instructions: ${state.instructions}.`,
4364
4488
  ` Hooks: ${state.hookConfiguration}; observed: ${state.observedEvents.join(", ") || "none"}.`,
4365
4489
  ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
4366
- ...state.pending.map((message3) => " Next: " + message3)
4490
+ ...state.pending.map((message2) => " Next: " + message2)
4367
4491
  );
4368
4492
  }
4369
4493
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
@@ -4524,15 +4648,18 @@ var init_setup = __esm({
4524
4648
  import fs18 from "fs/promises";
4525
4649
  import os3 from "os";
4526
4650
  import path23 from "path";
4527
- import { execFile as execFile11 } from "child_process";
4528
- import { promisify as promisify11 } from "util";
4651
+ import { execFile as execFile12 } from "child_process";
4652
+ import { promisify as promisify12 } from "util";
4529
4653
  import { createHash as createHash6, randomUUID as randomUUID8 } from "crypto";
4530
4654
  import { z as z17 } from "zod";
4531
4655
  async function configurePath(bin, previous, save, options = {}) {
4532
4656
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
4533
4657
  const changes = [...previous];
4658
+ const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
4659
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path23.posix.isAbsolute(entry) && path23.posix.resolve(entry) === path23.posix.resolve(bin));
4660
+ const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
4534
4661
  const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
4535
- Add ${bin} to your PATH, then run: mason setup --host codex` });
4662
+ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
4536
4663
  if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
4537
4664
  if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
4538
4665
  try {
@@ -4676,20 +4803,19 @@ try {
4676
4803
  @{ value = $next; kind = $kind } | ConvertTo-Json -Compress
4677
4804
  } finally { $key.Dispose() }
4678
4805
  `;
4679
- const { stdout } = await execute("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
4806
+ const { stdout } = await execute2("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
4680
4807
  return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
4681
4808
  }
4682
- var execute, pathChangeSchema, quote, message2, normalizeWindowsEntry, hasWindowsEntry;
4809
+ var execute2, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
4683
4810
  var init_path = __esm({
4684
4811
  "src/distribution/path.ts"() {
4685
4812
  "use strict";
4686
- execute = promisify11(execFile11);
4813
+ execute2 = promisify12(execFile12);
4687
4814
  pathChangeSchema = z17.discriminatedUnion("kind", [
4688
4815
  z17.object({ kind: z17.literal("profile"), file: z17.string(), block: z17.string(), created: z17.boolean() }),
4689
4816
  z17.object({ kind: z17.literal("windows"), entry: z17.string(), created: z17.boolean() })
4690
4817
  ]);
4691
4818
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
4692
- message2 = "Open a new terminal, then run: mason setup --host codex";
4693
4819
  normalizeWindowsEntry = (value) => path23.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
4694
4820
  hasWindowsEntry = (value, entry, env = process.env) => {
4695
4821
  const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
@@ -4977,20 +5103,20 @@ async function runAutomationCli(argv, stdin = "", io = {
4977
5103
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
4978
5104
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
4979
5105
  } catch (error) {
4980
- const message3 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
5106
+ const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
4981
5107
  if (action === "hook" || isHookCommand(argv)) {
4982
- io.out(JSON.stringify({ systemMessage: message3 }));
5108
+ io.out(JSON.stringify({ systemMessage: message2 }));
4983
5109
  return 0;
4984
5110
  }
4985
5111
  if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...action === "setup" ? { action, status: "incomplete", next: "Rerun the same setup command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
4986
- else io.err(message3);
5112
+ else io.err(message2);
4987
5113
  return 2;
4988
5114
  }
4989
5115
  }
4990
5116
 
4991
5117
  // bin/mason.ts
4992
5118
  var [command, ...args] = process.argv.slice(2);
4993
- var usage = `Mason ${"0.14.1"}
5119
+ var usage = `Mason ${"0.14.2"}
4994
5120
 
4995
5121
  Usage: mason <command> [options]
4996
5122
 
@@ -5008,7 +5134,7 @@ Use mason <command> --help for options. npm users can keep using mason-auto,
5008
5134
  mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
5009
5135
  try {
5010
5136
  if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
5011
- else if (["--version", "-v"].includes(command)) console.log("0.14.1");
5137
+ else if (["--version", "-v"].includes(command)) console.log("0.14.2");
5012
5138
  else if (command === "internal-install") {
5013
5139
  if (args.length) throw new Error("internal-install takes no arguments.");
5014
5140
  const { sourceRuntime: sourceRuntime2 } = await Promise.resolve().then(() => (init_runtime2(), runtime_exports));