mason-context 0.14.0 → 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/CHANGELOG.md +17 -0
- package/README.md +2 -2
- package/dist/mason-audit.js +214 -69
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +239 -115
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +266 -142
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +497 -171
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason.js
CHANGED
|
@@ -1105,9 +1105,133 @@ var init_deleted_reference = __esm({
|
|
|
1105
1105
|
}
|
|
1106
1106
|
});
|
|
1107
1107
|
|
|
1108
|
-
// src/audit/
|
|
1109
|
-
import
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1411
|
+
const matched = await auditGlob(
|
|
1412
|
+
root,
|
|
1283
1413
|
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
1284
|
-
{
|
|
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) =>
|
|
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
|
|
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
|
|
1308
|
-
|
|
1309
|
-
|
|
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(
|
|
1312
|
-
} else if (await
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
|
1448
|
-
import { promisify as
|
|
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 =
|
|
1631
|
+
exec6 = promisify7(execFile7);
|
|
1501
1632
|
}
|
|
1502
1633
|
});
|
|
1503
1634
|
|
|
@@ -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
|
|
2266
|
-
import { promisify as
|
|
2267
|
-
import
|
|
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,
|
|
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
|
-
|
|
2462
|
+
needsWorkspaceScripts ? commandManifests(root) : [],
|
|
2463
|
+
fg4(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
2344
2464
|
]);
|
|
2345
|
-
const packages =
|
|
2346
|
-
const decisions = await Promise.all(decisionFiles.sort().map(async (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 = [
|
|
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
|
-
|
|
2518
|
+
init_inputs();
|
|
2396
2519
|
init_new_module();
|
|
2397
2520
|
init_stale_count();
|
|
2398
2521
|
init_dead_command();
|
|
2399
2522
|
init_storage();
|
|
2400
|
-
exec7 =
|
|
2401
|
-
engineVersion = true ? "0.14.
|
|
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
|
}
|
|
@@ -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
|
|
3112
|
-
import { promisify as
|
|
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 =
|
|
3301
|
+
exec8 = promisify9(execFile9);
|
|
3178
3302
|
HISTORY_COMMITS = 1500;
|
|
3179
3303
|
MIN_FILE_COMMITS = 5;
|
|
3180
3304
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -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
|
|
3643
|
-
import { promisify as
|
|
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 =
|
|
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
|
|
4171
|
-
import { promisify as
|
|
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 =
|
|
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
|
}
|
|
@@ -4520,6 +4644,187 @@ var init_setup = __esm({
|
|
|
4520
4644
|
}
|
|
4521
4645
|
});
|
|
4522
4646
|
|
|
4647
|
+
// src/distribution/path.ts
|
|
4648
|
+
import fs18 from "fs/promises";
|
|
4649
|
+
import os3 from "os";
|
|
4650
|
+
import path23 from "path";
|
|
4651
|
+
import { execFile as execFile12 } from "child_process";
|
|
4652
|
+
import { promisify as promisify12 } from "util";
|
|
4653
|
+
import { createHash as createHash6, randomUUID as randomUUID8 } from "crypto";
|
|
4654
|
+
import { z as z17 } from "zod";
|
|
4655
|
+
async function configurePath(bin, previous, save, options = {}) {
|
|
4656
|
+
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
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";
|
|
4661
|
+
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
4662
|
+
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
4663
|
+
if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
|
|
4664
|
+
if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
|
|
4665
|
+
try {
|
|
4666
|
+
if (platform === "win32") {
|
|
4667
|
+
const current = await windowsPath("read");
|
|
4668
|
+
if (!hasWindowsEntry(current.value, bin)) {
|
|
4669
|
+
if (!changes.some((c) => c.kind === "windows" && c.entry === bin)) {
|
|
4670
|
+
changes.push({ kind: "windows", entry: bin, created: current.value === null });
|
|
4671
|
+
await save(changes);
|
|
4672
|
+
}
|
|
4673
|
+
await windowsPath("add", bin, current);
|
|
4674
|
+
}
|
|
4675
|
+
return { status: "configured", changes, message: message2 };
|
|
4676
|
+
}
|
|
4677
|
+
const shell = path23.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
4678
|
+
const userHome = options.userHome ?? os3.homedir();
|
|
4679
|
+
let profiles;
|
|
4680
|
+
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
4681
|
+
if (env.MASON_PROFILE) profiles = [path23.resolve(env.MASON_PROFILE)];
|
|
4682
|
+
else if (shell === "zsh") profiles = [path23.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
4683
|
+
else if (shell === "sh") profiles = [path23.join(userHome, ".profile")];
|
|
4684
|
+
else {
|
|
4685
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path23.join(userHome, name));
|
|
4686
|
+
let login = candidates[2];
|
|
4687
|
+
for (const file of candidates) if (await fs18.lstat(file).then(() => true, (error) => {
|
|
4688
|
+
if (error.code === "ENOENT") return false;
|
|
4689
|
+
throw error;
|
|
4690
|
+
})) {
|
|
4691
|
+
login = file;
|
|
4692
|
+
break;
|
|
4693
|
+
}
|
|
4694
|
+
profiles = [path23.join(userHome, ".bashrc"), login];
|
|
4695
|
+
}
|
|
4696
|
+
const marker = `# >>> mason PATH ${createHash6("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
4697
|
+
for (const profile of profiles) {
|
|
4698
|
+
const stat = await fs18.lstat(profile).catch((error) => {
|
|
4699
|
+
if (error.code === "ENOENT") return null;
|
|
4700
|
+
throw error;
|
|
4701
|
+
});
|
|
4702
|
+
const file = stat?.isSymbolicLink() ? await fs18.realpath(profile) : path23.resolve(profile);
|
|
4703
|
+
const original = await fs18.readFile(file).catch((error) => {
|
|
4704
|
+
if (error.code === "ENOENT" && !stat) return null;
|
|
4705
|
+
throw error;
|
|
4706
|
+
});
|
|
4707
|
+
if (original && !Buffer.from(original.toString("utf8")).equals(original)) return manual(`Shell profile is not UTF-8: ${profile}.`);
|
|
4708
|
+
const text2 = original?.toString("utf8") ?? "";
|
|
4709
|
+
const owned = changes.find((c) => c.kind === "profile" && c.file === file);
|
|
4710
|
+
if (owned && text2.includes(owned.block)) continue;
|
|
4711
|
+
if (text2.includes(marker) || owned && text2.includes("# <<< mason PATH")) return manual(`Mason's PATH block was edited; retained ${profile}.`);
|
|
4712
|
+
const newline = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
4713
|
+
const block = owned?.block ?? ("\n" + marker + '\ncase ":${PATH-}:" in\n *:' + quote(bin) + ":*) ;;\n *) export PATH=" + quote(bin) + ':"${PATH-}" ;;\nesac\n# <<< mason PATH <<<\n').replaceAll("\n", newline);
|
|
4714
|
+
if (!owned) {
|
|
4715
|
+
changes.push({ kind: "profile", file, block, created: original === null });
|
|
4716
|
+
await save(changes);
|
|
4717
|
+
}
|
|
4718
|
+
await fs18.mkdir(path23.dirname(file), { recursive: true });
|
|
4719
|
+
const handle = await fs18.open(file, original === null ? "ax+" : "a+", 384);
|
|
4720
|
+
try {
|
|
4721
|
+
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
4722
|
+
await handle.writeFile(block);
|
|
4723
|
+
await handle.sync();
|
|
4724
|
+
} finally {
|
|
4725
|
+
await handle.close();
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return { status: "configured", changes, message: message2 };
|
|
4729
|
+
} catch (error) {
|
|
4730
|
+
return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
async function removePath(changes) {
|
|
4734
|
+
const warnings = [];
|
|
4735
|
+
for (const change of changes) {
|
|
4736
|
+
try {
|
|
4737
|
+
if (change.kind === "windows") {
|
|
4738
|
+
const current = await windowsPath("read");
|
|
4739
|
+
if (!(current.value ?? "").split(";").includes(change.entry) && hasWindowsEntry(current.value, change.entry)) throw new Error("PATH entry was edited");
|
|
4740
|
+
await windowsPath("remove", change.entry, current, change.created);
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
const stat = await fs18.lstat(change.file).catch((error) => {
|
|
4744
|
+
if (error.code === "ENOENT") return null;
|
|
4745
|
+
throw error;
|
|
4746
|
+
});
|
|
4747
|
+
if (!stat) continue;
|
|
4748
|
+
if (!stat.isFile()) throw new Error("profile type changed");
|
|
4749
|
+
const bytes = await fs18.readFile(change.file), text2 = bytes.toString("utf8");
|
|
4750
|
+
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
4751
|
+
const index2 = text2.indexOf(change.block);
|
|
4752
|
+
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
4753
|
+
const prefix = text2.slice(0, index2), suffix = text2.slice(index2 + change.block.length);
|
|
4754
|
+
const separator = prefix && suffix && !prefix.endsWith("\n") ? change.block.startsWith("\r\n") ? "\r\n" : "\n" : "";
|
|
4755
|
+
const next = Buffer.from(prefix + separator + suffix);
|
|
4756
|
+
const temporary = change.file + ".mason-" + randomUUID8();
|
|
4757
|
+
try {
|
|
4758
|
+
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
4759
|
+
const handle = await fs18.open(temporary, "wx", stat.mode & 511);
|
|
4760
|
+
try {
|
|
4761
|
+
await handle.writeFile(next);
|
|
4762
|
+
await handle.chmod(stat.mode & 511);
|
|
4763
|
+
if (process.platform !== "win32") await handle.chown(stat.uid, stat.gid);
|
|
4764
|
+
await handle.sync();
|
|
4765
|
+
} finally {
|
|
4766
|
+
await handle.close();
|
|
4767
|
+
}
|
|
4768
|
+
const current = await fs18.lstat(change.file);
|
|
4769
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs18.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
4770
|
+
await fs18.rename(temporary, change.file);
|
|
4771
|
+
} finally {
|
|
4772
|
+
await fs18.rm(temporary, { force: true });
|
|
4773
|
+
}
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
return warnings;
|
|
4779
|
+
}
|
|
4780
|
+
async function windowsPath(action, entry = "", expected, removeEmpty = false) {
|
|
4781
|
+
const payload = Buffer.from(JSON.stringify({ action, entry, expected, removeEmpty })).toString("base64");
|
|
4782
|
+
const script = `$ErrorActionPreference = 'Stop'
|
|
4783
|
+
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
4784
|
+
$p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json
|
|
4785
|
+
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
|
|
4786
|
+
try {
|
|
4787
|
+
$value = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
4788
|
+
$kind = if ($null -eq $value) { $null } else { $key.GetValueKind('Path').ToString() }
|
|
4789
|
+
if ($null -ne $kind -and $kind -notin @('String','ExpandString')) { throw 'User PATH is not a string.' }
|
|
4790
|
+
if ($p.action -eq 'read') { @{ value = $value; kind = $kind } | ConvertTo-Json -Compress; exit 0 }
|
|
4791
|
+
if ($p.expected -and ($value -cne $p.expected.value -or $kind -cne $p.expected.kind)) { throw 'User PATH changed concurrently; retry the operation.' }
|
|
4792
|
+
$parts = if ($null -eq $value -or $value -eq '') { @() } else { @($value.Split(';')) }
|
|
4793
|
+
if ($p.action -eq 'add') { $next = (@($p.entry) + $parts) -join ';' }
|
|
4794
|
+
else { $next = (@($parts | Where-Object { $_ -cne $p.entry })) -join ';' }
|
|
4795
|
+
if ($next -cne $value) {
|
|
4796
|
+
$writeKind = if ($kind) { [Microsoft.Win32.RegistryValueKind]$kind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
|
4797
|
+
if ($p.action -eq 'remove' -and $p.removeEmpty -and $next -eq '') { $key.DeleteValue('Path', $false) }
|
|
4798
|
+
else { $key.SetValue('Path', $next, $writeKind) }
|
|
4799
|
+
Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class MasonEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r); }'
|
|
4800
|
+
$result = [UIntPtr]::Zero
|
|
4801
|
+
[void][MasonEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
|
|
4802
|
+
}
|
|
4803
|
+
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
4804
|
+
} finally { $key.Dispose() }
|
|
4805
|
+
`;
|
|
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 });
|
|
4807
|
+
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
4808
|
+
}
|
|
4809
|
+
var execute2, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
4810
|
+
var init_path = __esm({
|
|
4811
|
+
"src/distribution/path.ts"() {
|
|
4812
|
+
"use strict";
|
|
4813
|
+
execute2 = promisify12(execFile12);
|
|
4814
|
+
pathChangeSchema = z17.discriminatedUnion("kind", [
|
|
4815
|
+
z17.object({ kind: z17.literal("profile"), file: z17.string(), block: z17.string(), created: z17.boolean() }),
|
|
4816
|
+
z17.object({ kind: z17.literal("windows"), entry: z17.string(), created: z17.boolean() })
|
|
4817
|
+
]);
|
|
4818
|
+
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4819
|
+
normalizeWindowsEntry = (value) => path23.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
4820
|
+
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
4821
|
+
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
4822
|
+
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
4823
|
+
return (value ?? "").split(";").some((part) => normalizeWindowsEntry(expand(part)) === normalizeWindowsEntry(entry));
|
|
4824
|
+
};
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
|
|
4523
4828
|
// src/distribution/install.ts
|
|
4524
4829
|
var install_exports2 = {};
|
|
4525
4830
|
__export(install_exports2, {
|
|
@@ -4529,55 +4834,55 @@ __export(install_exports2, {
|
|
|
4529
4834
|
uninstallStandalone: () => uninstallStandalone,
|
|
4530
4835
|
upgradeStandalone: () => upgradeStandalone
|
|
4531
4836
|
});
|
|
4532
|
-
import
|
|
4533
|
-
import
|
|
4534
|
-
import
|
|
4837
|
+
import fs19 from "fs/promises";
|
|
4838
|
+
import os4 from "os";
|
|
4839
|
+
import path24 from "path";
|
|
4535
4840
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4536
4841
|
import { spawn } from "child_process";
|
|
4537
|
-
import { randomUUID as
|
|
4538
|
-
import { z as
|
|
4842
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
4843
|
+
import { z as z18 } from "zod";
|
|
4539
4844
|
async function replace(file, content2, mode = 384) {
|
|
4540
|
-
const temp = file + ".tmp-" +
|
|
4845
|
+
const temp = file + ".tmp-" + randomUUID9();
|
|
4541
4846
|
try {
|
|
4542
|
-
await
|
|
4543
|
-
await
|
|
4847
|
+
await fs19.writeFile(temp, content2, { mode, flag: "wx" });
|
|
4848
|
+
await fs19.rename(temp, file);
|
|
4544
4849
|
} finally {
|
|
4545
|
-
await
|
|
4850
|
+
await fs19.rm(temp, { force: true });
|
|
4546
4851
|
}
|
|
4547
4852
|
}
|
|
4548
4853
|
async function installStandalone(source) {
|
|
4549
4854
|
const bundle = await verifyBundle(source);
|
|
4550
|
-
const selectedHome =
|
|
4551
|
-
await
|
|
4552
|
-
const home = await
|
|
4553
|
-
const selectedBin =
|
|
4855
|
+
const selectedHome = path24.resolve(defaultHome());
|
|
4856
|
+
await fs19.mkdir(selectedHome, { recursive: true });
|
|
4857
|
+
const home = await fs19.realpath(selectedHome);
|
|
4858
|
+
const selectedBin = path24.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path24.join(home, "bin") : path24.join(os4.homedir(), ".local/bin")));
|
|
4554
4859
|
return withLock(home, ".install-lock", async () => {
|
|
4555
4860
|
const raw = await readStoreJson(home, "install.json");
|
|
4556
4861
|
const previous = raw === null ? null : recordSchema2.parse(raw);
|
|
4557
|
-
if (!previous) for (const file of await
|
|
4862
|
+
if (!previous) for (const file of await fs19.readdir(home)) {
|
|
4558
4863
|
if (file === ".install-lock") continue;
|
|
4559
|
-
const directory =
|
|
4560
|
-
const stat = await
|
|
4561
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
4864
|
+
const directory = path24.join(home, file);
|
|
4865
|
+
const stat = await fs19.lstat(directory);
|
|
4866
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs19.readdir(directory)).length) {
|
|
4562
4867
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
4563
4868
|
}
|
|
4564
4869
|
}
|
|
4565
|
-
await
|
|
4566
|
-
const bin = await
|
|
4870
|
+
await fs19.mkdir(selectedBin, { recursive: true });
|
|
4871
|
+
const bin = await fs19.realpath(selectedBin);
|
|
4567
4872
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
4568
4873
|
const id = bundle.manifestHash.slice(0, 24);
|
|
4569
4874
|
const destination = await storePath(home, "versions/" + id);
|
|
4570
4875
|
const launchers = process.platform === "win32" ? {
|
|
4571
4876
|
"mason.cmd": WINDOWS_BATCH_LAUNCHER,
|
|
4572
|
-
"mason.ps1": `$ErrorActionPreference = 'Stop'
|
|
4877
|
+
"mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
|
|
4573
4878
|
$previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
4574
4879
|
$token = [Guid]::NewGuid().ToString()
|
|
4575
4880
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
4576
4881
|
try {
|
|
4577
|
-
& ${psQuote(
|
|
4882
|
+
& ${psQuote(path24.join(destination, "node.exe"))} ${psQuote(path24.join(destination, "app/dist/mason.js"))} @args
|
|
4578
4883
|
$code = $LASTEXITCODE
|
|
4579
4884
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
4580
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
4885
|
+
if ([IO.File]::ReadAllText(${psQuote(path24.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
4581
4886
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
4582
4887
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
4583
4888
|
}
|
|
@@ -4586,48 +4891,66 @@ try {
|
|
|
4586
4891
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
4587
4892
|
`
|
|
4588
4893
|
} : { mason: `#!/bin/sh
|
|
4589
|
-
exec ${shQuote(
|
|
4894
|
+
exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destination, "app/dist/mason.js"))} "$@"
|
|
4590
4895
|
` };
|
|
4591
4896
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
4592
4897
|
const file = await storePath(bin, name);
|
|
4593
|
-
const old = await
|
|
4898
|
+
const old = await fs19.readFile(file, "utf8").catch((error) => {
|
|
4594
4899
|
if (error.code === "ENOENT") return null;
|
|
4595
4900
|
throw error;
|
|
4596
4901
|
});
|
|
4597
4902
|
if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
|
|
4598
4903
|
}
|
|
4599
|
-
const
|
|
4904
|
+
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
4905
|
+
const checkRetired = async (name, expected) => {
|
|
4906
|
+
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4907
|
+
const actual = await fs19.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
4908
|
+
if (error.code === "ENOENT") return null;
|
|
4909
|
+
throw error;
|
|
4910
|
+
});
|
|
4911
|
+
if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
|
|
4912
|
+
};
|
|
4913
|
+
for (const [name, expected] of retired) await checkRetired(name, expected);
|
|
4914
|
+
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
4600
4915
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
4601
|
-
const stage = await storePath(home, "versions/.install-" +
|
|
4602
|
-
await
|
|
4916
|
+
const stage = await storePath(home, "versions/.install-" + randomUUID9(), true);
|
|
4917
|
+
await fs19.mkdir(stage);
|
|
4603
4918
|
try {
|
|
4604
4919
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
4605
4920
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
4606
|
-
await
|
|
4607
|
-
await
|
|
4921
|
+
await fs19.copyFile(from, to);
|
|
4922
|
+
await fs19.chmod(to, (await fs19.stat(from)).mode & 511);
|
|
4608
4923
|
}
|
|
4609
4924
|
await verifyBundle(stage, bundle.manifestHash);
|
|
4610
|
-
const exists2 = await
|
|
4925
|
+
const exists2 = await fs19.lstat(destination).then(() => true, (error) => {
|
|
4611
4926
|
if (error.code === "ENOENT") return false;
|
|
4612
4927
|
throw error;
|
|
4613
4928
|
});
|
|
4614
4929
|
if (exists2) await verifyBundle(destination, bundle.manifestHash);
|
|
4615
|
-
else await
|
|
4930
|
+
else await fs19.rename(stage, destination);
|
|
4616
4931
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
4932
|
+
for (const [name, expected] of retired) {
|
|
4933
|
+
await checkRetired(name, expected);
|
|
4934
|
+
await fs19.rm(await storePath(bin, name), { force: true });
|
|
4935
|
+
}
|
|
4617
4936
|
await writeStoreJson(home, "install.json", receipt);
|
|
4618
4937
|
} finally {
|
|
4619
|
-
await
|
|
4938
|
+
await fs19.rm(stage, { recursive: true, force: true });
|
|
4620
4939
|
}
|
|
4621
|
-
|
|
4940
|
+
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
4941
|
+
receipt.pathChanges = changes;
|
|
4942
|
+
await writeStoreJson(home, "install.json", receipt);
|
|
4943
|
+
});
|
|
4944
|
+
return { home, bin, version: bundle.manifest.version, path: pathResult };
|
|
4622
4945
|
});
|
|
4623
4946
|
}
|
|
4624
4947
|
async function currentInstallation() {
|
|
4625
|
-
const app =
|
|
4626
|
-
const bundle =
|
|
4948
|
+
const app = path24.dirname(path24.dirname(fileURLToPath2(import.meta.url)));
|
|
4949
|
+
const bundle = path24.dirname(app), home = path24.dirname(path24.dirname(bundle));
|
|
4627
4950
|
const raw = await readStoreJson(home, "install.json");
|
|
4628
4951
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
4629
4952
|
const record = recordSchema2.parse(raw);
|
|
4630
|
-
if (record.home !== await
|
|
4953
|
+
if (record.home !== await fs19.realpath(home) || path24.dirname(bundle) !== path24.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
4631
4954
|
return { home, bundle, record };
|
|
4632
4955
|
}
|
|
4633
4956
|
async function upgradeStandalone(version) {
|
|
@@ -4635,7 +4958,7 @@ async function upgradeStandalone(version) {
|
|
|
4635
4958
|
const { home, bundle, record } = await currentInstallation();
|
|
4636
4959
|
await verifyBundle(bundle);
|
|
4637
4960
|
const windows = process.platform === "win32";
|
|
4638
|
-
const script =
|
|
4961
|
+
const script = path24.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
4639
4962
|
return new Promise((resolve, reject) => {
|
|
4640
4963
|
const child = spawn(
|
|
4641
4964
|
windows ? "powershell.exe" : "sh",
|
|
@@ -4654,40 +4977,44 @@ async function uninstallStandalone() {
|
|
|
4654
4977
|
}
|
|
4655
4978
|
await withLock(home, ".install-lock", async () => {
|
|
4656
4979
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
4657
|
-
if (!
|
|
4980
|
+
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4658
4981
|
const file = await storePath(record.bin, name);
|
|
4659
|
-
const actual = await
|
|
4982
|
+
const actual = await fs19.readFile(file, "utf8").catch((error) => {
|
|
4660
4983
|
if (error.code === "ENOENT") return null;
|
|
4661
4984
|
throw error;
|
|
4662
4985
|
});
|
|
4663
4986
|
if (actual !== null && actual !== expected) throw new Error("Launcher was edited; retained: " + file);
|
|
4664
4987
|
}
|
|
4988
|
+
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
4665
4989
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
4666
|
-
for (const name of Object.keys(record.launchers)) await
|
|
4990
|
+
for (const name of Object.keys(record.launchers)) await fs19.rm(await storePath(record.bin, name), { force: true });
|
|
4667
4991
|
});
|
|
4668
|
-
if (process.platform !== "win32") await
|
|
4992
|
+
if (process.platform !== "win32") await fs19.rm(home, { recursive: true, force: true });
|
|
4669
4993
|
return home;
|
|
4670
4994
|
}
|
|
4671
|
-
var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, defaultHome;
|
|
4995
|
+
var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
4672
4996
|
var init_install2 = __esm({
|
|
4673
4997
|
"src/distribution/install.ts"() {
|
|
4674
4998
|
"use strict";
|
|
4675
4999
|
init_bundle();
|
|
4676
5000
|
init_storage();
|
|
4677
5001
|
init_store();
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5002
|
+
init_path();
|
|
5003
|
+
recordSchema2 = z18.object({
|
|
5004
|
+
format: z18.literal(1),
|
|
5005
|
+
home: z18.string(),
|
|
5006
|
+
bin: z18.string(),
|
|
5007
|
+
current: z18.string().regex(/^[a-f0-9]{24}$/),
|
|
5008
|
+
version: z18.string(),
|
|
5009
|
+
launchers: z18.record(z18.string()),
|
|
5010
|
+
previousLaunchers: z18.record(z18.string()).optional(),
|
|
5011
|
+
pathChanges: z18.array(pathChangeSchema).optional()
|
|
4686
5012
|
});
|
|
4687
5013
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4688
5014
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
4689
|
-
WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason.ps1" %*\r\n';
|
|
4690
|
-
|
|
5015
|
+
WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
5016
|
+
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5017
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path24.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path24.join(os4.homedir(), ".local/share/mason"));
|
|
4691
5018
|
}
|
|
4692
5019
|
});
|
|
4693
5020
|
|
|
@@ -4789,7 +5116,7 @@ async function runAutomationCli(argv, stdin = "", io = {
|
|
|
4789
5116
|
|
|
4790
5117
|
// bin/mason.ts
|
|
4791
5118
|
var [command, ...args] = process.argv.slice(2);
|
|
4792
|
-
var usage = `Mason ${"0.14.
|
|
5119
|
+
var usage = `Mason ${"0.14.2"}
|
|
4793
5120
|
|
|
4794
5121
|
Usage: mason <command> [options]
|
|
4795
5122
|
|
|
@@ -4807,7 +5134,7 @@ Use mason <command> --help for options. npm users can keep using mason-auto,
|
|
|
4807
5134
|
mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
|
|
4808
5135
|
try {
|
|
4809
5136
|
if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
4810
|
-
else if (["--version", "-v"].includes(command)) console.log("0.14.
|
|
5137
|
+
else if (["--version", "-v"].includes(command)) console.log("0.14.2");
|
|
4811
5138
|
else if (command === "internal-install") {
|
|
4812
5139
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
4813
5140
|
const { sourceRuntime: sourceRuntime2 } = await Promise.resolve().then(() => (init_runtime2(), runtime_exports));
|
|
@@ -4816,8 +5143,7 @@ try {
|
|
|
4816
5143
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
4817
5144
|
const installed = await installStandalone2(selected.bundleRoot);
|
|
4818
5145
|
console.log(`Installed Mason ${installed.version} in ${installed.home}.
|
|
4819
|
-
|
|
4820
|
-
Add that directory to PATH if needed, then run: mason setup --host codex`);
|
|
5146
|
+
${installed.path.message}`);
|
|
4821
5147
|
} else if (command === "upgrade" || command === "uninstall") {
|
|
4822
5148
|
if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Project versions change only when you rerun setup." : "Usage: mason uninstall. Removes the standalone user installation; project runtimes, configuration and knowledge are retained.");
|
|
4823
5149
|
else {
|