mason-context 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -108
- package/dist/bin/mason-mcp.js +281 -136
- package/dist/bin/mason-mcp.js.map +1 -1
- package/dist/bin/mason.js +389 -169
- package/dist/bin/mason.js.map +1 -1
- package/dist/src/cli.js +389 -169
- package/dist/src/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/mason.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
4
10
|
var __esm = (fn, res) => function __init() {
|
|
5
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
12
|
};
|
|
@@ -372,48 +378,44 @@ function formatPromptForCopy(system, userMessage) {
|
|
|
372
378
|
|
|
373
379
|
${userMessage}`;
|
|
374
380
|
}
|
|
381
|
+
function spawnWithStdin(command, args, input) {
|
|
382
|
+
const { spawn } = __require("child_process");
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const proc = spawn(command, args, {
|
|
385
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
386
|
+
timeout: 3e5
|
|
387
|
+
});
|
|
388
|
+
let stdout = "";
|
|
389
|
+
let stderr = "";
|
|
390
|
+
proc.stdout.on("data", (data) => {
|
|
391
|
+
stdout += data.toString();
|
|
392
|
+
});
|
|
393
|
+
proc.stderr.on("data", (data) => {
|
|
394
|
+
stderr += data.toString();
|
|
395
|
+
});
|
|
396
|
+
proc.on("close", (code) => {
|
|
397
|
+
if (code === 0) {
|
|
398
|
+
resolve(stdout.trim());
|
|
399
|
+
} else {
|
|
400
|
+
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
proc.on("error", reject);
|
|
404
|
+
proc.stdin.write(input);
|
|
405
|
+
proc.stdin.end();
|
|
406
|
+
});
|
|
407
|
+
}
|
|
375
408
|
async function callClaudeCLI(system, userMessage) {
|
|
376
|
-
const fs7 = await import("fs/promises");
|
|
377
|
-
const os2 = await import("os");
|
|
378
|
-
const path6 = await import("path");
|
|
379
409
|
const prompt = `${system}
|
|
380
410
|
|
|
381
411
|
${userMessage}`;
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
await fs7.writeFile(tmpFile, prompt, "utf-8");
|
|
385
|
-
const promptContent = await fs7.readFile(tmpFile, "utf-8");
|
|
386
|
-
const { stdout } = await exec4(
|
|
387
|
-
"sh",
|
|
388
|
-
["-c", `cat "${tmpFile}" | claude -p`],
|
|
389
|
-
{ maxBuffer: 1e7, timeout: 3e5 }
|
|
390
|
-
);
|
|
391
|
-
return stdout.trim();
|
|
392
|
-
} finally {
|
|
393
|
-
await fs7.unlink(tmpFile).catch(() => {
|
|
394
|
-
});
|
|
395
|
-
}
|
|
412
|
+
return spawnWithStdin("claude", ["-p"], prompt);
|
|
396
413
|
}
|
|
397
414
|
async function callGeminiCLI(system, userMessage) {
|
|
398
|
-
const fs7 = await import("fs/promises");
|
|
399
|
-
const os2 = await import("os");
|
|
400
|
-
const path6 = await import("path");
|
|
401
415
|
const prompt = `${system}
|
|
402
416
|
|
|
403
417
|
${userMessage}`;
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
await fs7.writeFile(tmpFile, prompt, "utf-8");
|
|
407
|
-
const { stdout } = await exec4(
|
|
408
|
-
"sh",
|
|
409
|
-
["-c", `cat "${tmpFile}" | gemini -p ""`],
|
|
410
|
-
{ maxBuffer: 1e7, timeout: 3e5 }
|
|
411
|
-
);
|
|
412
|
-
return stdout.trim();
|
|
413
|
-
} finally {
|
|
414
|
-
await fs7.unlink(tmpFile).catch(() => {
|
|
415
|
-
});
|
|
416
|
-
}
|
|
418
|
+
return spawnWithStdin("gemini", ["-p", ""], prompt);
|
|
417
419
|
}
|
|
418
420
|
async function callOllamaCLI(host, model, system, userMessage) {
|
|
419
421
|
const response = await fetch(`${host}/api/chat`, {
|
|
@@ -524,6 +526,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
524
526
|
const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
|
|
525
527
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
526
528
|
if (selected.size >= maxFiles) break;
|
|
529
|
+
const resolvedPath = path2.resolve(rootDir, filePath);
|
|
530
|
+
if (!resolvedPath.startsWith(path2.resolve(rootDir))) continue;
|
|
527
531
|
selected.set(filePath, "always-include (project config)");
|
|
528
532
|
}
|
|
529
533
|
let configCount = 0;
|
|
@@ -695,7 +699,9 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
695
699
|
const results = [];
|
|
696
700
|
for (const [filePath, reason] of selected) {
|
|
697
701
|
try {
|
|
698
|
-
const fullPath = path2.
|
|
702
|
+
const fullPath = path2.resolve(rootDir, filePath);
|
|
703
|
+
if (!fullPath.startsWith(path2.resolve(rootDir))) continue;
|
|
704
|
+
if (isSensitiveFile(filePath)) continue;
|
|
699
705
|
const stat = await fs3.stat(fullPath);
|
|
700
706
|
if (stat.size > 1e5) continue;
|
|
701
707
|
const content = await fs3.readFile(fullPath, "utf-8");
|
|
@@ -713,10 +719,15 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
713
719
|
}
|
|
714
720
|
return results;
|
|
715
721
|
}
|
|
722
|
+
function isSensitiveFile(filePath) {
|
|
723
|
+
const basename = path2.basename(filePath);
|
|
724
|
+
return SENSITIVE_PATTERNS.some((p) => p.test(basename));
|
|
725
|
+
}
|
|
716
726
|
async function readFullFile(rootDir, filePath) {
|
|
717
727
|
try {
|
|
718
728
|
const fullPath = path2.join(path2.resolve(rootDir), filePath);
|
|
719
729
|
if (!fullPath.startsWith(path2.resolve(rootDir))) return null;
|
|
730
|
+
if (isSensitiveFile(filePath)) return null;
|
|
720
731
|
const content = await fs3.readFile(fullPath, "utf-8");
|
|
721
732
|
return {
|
|
722
733
|
path: filePath,
|
|
@@ -727,7 +738,7 @@ async function readFullFile(rootDir, filePath) {
|
|
|
727
738
|
return null;
|
|
728
739
|
}
|
|
729
740
|
}
|
|
730
|
-
var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES;
|
|
741
|
+
var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES, SENSITIVE_PATTERNS;
|
|
731
742
|
var init_sampler = __esm({
|
|
732
743
|
"src/mcp/sampler.ts"() {
|
|
733
744
|
"use strict";
|
|
@@ -873,6 +884,21 @@ var init_sampler = __esm({
|
|
|
873
884
|
"**/BuildConfig.java"
|
|
874
885
|
];
|
|
875
886
|
PREVIEW_LINES = 60;
|
|
887
|
+
SENSITIVE_PATTERNS = [
|
|
888
|
+
/^\.env$/,
|
|
889
|
+
/^\.env\./,
|
|
890
|
+
/\.pem$/,
|
|
891
|
+
/\.key$/,
|
|
892
|
+
/\.p12$/,
|
|
893
|
+
/\.pfx$/,
|
|
894
|
+
/\.jks$/,
|
|
895
|
+
/id_rsa/,
|
|
896
|
+
/id_ed25519/,
|
|
897
|
+
/credentials\./,
|
|
898
|
+
/secret/i,
|
|
899
|
+
/\.keystore$/,
|
|
900
|
+
/local\.properties$/
|
|
901
|
+
];
|
|
876
902
|
}
|
|
877
903
|
});
|
|
878
904
|
|
|
@@ -1182,12 +1208,200 @@ var init_snapshot = __esm({
|
|
|
1182
1208
|
}
|
|
1183
1209
|
});
|
|
1184
1210
|
|
|
1185
|
-
// src/
|
|
1211
|
+
// src/impact/impact.ts
|
|
1212
|
+
var impact_exports = {};
|
|
1213
|
+
__export(impact_exports, {
|
|
1214
|
+
analyzeImpact: () => analyzeImpact
|
|
1215
|
+
});
|
|
1186
1216
|
import fs5 from "fs/promises";
|
|
1187
1217
|
import path4 from "path";
|
|
1188
1218
|
import { execFile as execFile7 } from "child_process";
|
|
1189
1219
|
import { promisify as promisify7 } from "util";
|
|
1190
1220
|
import fg3 from "fast-glob";
|
|
1221
|
+
async function analyzeImpact(rootDir, targetFiles) {
|
|
1222
|
+
const resolvedRoot = path4.resolve(rootDir);
|
|
1223
|
+
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
1224
|
+
const [cochange, references, tests] = await Promise.all([
|
|
1225
|
+
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
1226
|
+
getReferences(resolvedRoot, resolvedTargets),
|
|
1227
|
+
getRelatedTests(resolvedRoot, resolvedTargets)
|
|
1228
|
+
]);
|
|
1229
|
+
return {
|
|
1230
|
+
targetFiles: resolvedTargets,
|
|
1231
|
+
cochange,
|
|
1232
|
+
references,
|
|
1233
|
+
tests
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async function resolveTargetFiles(rootDir, targets) {
|
|
1237
|
+
const resolved = [];
|
|
1238
|
+
for (const target of targets) {
|
|
1239
|
+
if (target.includes("/")) {
|
|
1240
|
+
resolved.push(target);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
const matches = await fg3(`**/${target}`, {
|
|
1244
|
+
cwd: rootDir,
|
|
1245
|
+
ignore: IGNORE
|
|
1246
|
+
});
|
|
1247
|
+
if (matches.length > 0) {
|
|
1248
|
+
resolved.push(matches[0]);
|
|
1249
|
+
} else {
|
|
1250
|
+
const noExt = target.replace(/\.[^.]+$/, "");
|
|
1251
|
+
const extMatches = await fg3(`**/${noExt}.*`, {
|
|
1252
|
+
cwd: rootDir,
|
|
1253
|
+
ignore: IGNORE
|
|
1254
|
+
});
|
|
1255
|
+
if (extMatches.length > 0) {
|
|
1256
|
+
resolved.push(extMatches[0]);
|
|
1257
|
+
} else {
|
|
1258
|
+
resolved.push(target);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return resolved;
|
|
1263
|
+
}
|
|
1264
|
+
async function getCochangeFiles(rootDir, targetFiles) {
|
|
1265
|
+
const cochangeCounts = /* @__PURE__ */ new Map();
|
|
1266
|
+
let totalTargetCommits = 0;
|
|
1267
|
+
for (const targetFile of targetFiles) {
|
|
1268
|
+
try {
|
|
1269
|
+
const { stdout: commitLog } = await exec7(
|
|
1270
|
+
"git",
|
|
1271
|
+
["log", "--format=%H", "-n", "500", "--", targetFile],
|
|
1272
|
+
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
1273
|
+
);
|
|
1274
|
+
const commits = commitLog.trim().split("\n").filter(Boolean);
|
|
1275
|
+
totalTargetCommits += commits.length;
|
|
1276
|
+
if (commits.length === 0) continue;
|
|
1277
|
+
for (const commit of commits) {
|
|
1278
|
+
try {
|
|
1279
|
+
const { stdout: filesInCommit } = await exec7(
|
|
1280
|
+
"git",
|
|
1281
|
+
["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
|
|
1282
|
+
{ cwd: rootDir }
|
|
1283
|
+
);
|
|
1284
|
+
const files = filesInCommit.trim().split("\n").filter(Boolean);
|
|
1285
|
+
for (const file of files) {
|
|
1286
|
+
if (targetFiles.includes(file)) continue;
|
|
1287
|
+
cochangeCounts.set(file, (cochangeCounts.get(file) ?? 0) + 1);
|
|
1288
|
+
}
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (totalTargetCommits === 0) return [];
|
|
1296
|
+
return [...cochangeCounts.entries()].map(([file, count]) => ({
|
|
1297
|
+
file,
|
|
1298
|
+
cochangeRate: Math.round(count / totalTargetCommits * 100) / 100,
|
|
1299
|
+
sharedCommits: count
|
|
1300
|
+
})).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
|
|
1301
|
+
}
|
|
1302
|
+
async function getReferences(rootDir, targetFiles) {
|
|
1303
|
+
const searchNames = /* @__PURE__ */ new Set();
|
|
1304
|
+
for (const target of targetFiles) {
|
|
1305
|
+
const basename = path4.basename(target).replace(/\.[^.]+$/, "");
|
|
1306
|
+
searchNames.add(basename);
|
|
1307
|
+
}
|
|
1308
|
+
const allSourceFiles = await fg3(`**/${SOURCE_EXTENSIONS2}`, {
|
|
1309
|
+
cwd: rootDir,
|
|
1310
|
+
ignore: IGNORE
|
|
1311
|
+
});
|
|
1312
|
+
const targetSet = new Set(targetFiles);
|
|
1313
|
+
const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
|
|
1314
|
+
const results = /* @__PURE__ */ new Map();
|
|
1315
|
+
const batchSize = 50;
|
|
1316
|
+
for (let i = 0; i < filesToSearch.length; i += batchSize) {
|
|
1317
|
+
const batch = filesToSearch.slice(i, i + batchSize);
|
|
1318
|
+
await Promise.all(
|
|
1319
|
+
batch.map(async (file) => {
|
|
1320
|
+
try {
|
|
1321
|
+
const content = await fs5.readFile(
|
|
1322
|
+
path4.join(rootDir, file),
|
|
1323
|
+
"utf-8"
|
|
1324
|
+
);
|
|
1325
|
+
for (const name of searchNames) {
|
|
1326
|
+
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
|
|
1327
|
+
if (regex.test(content)) {
|
|
1328
|
+
if (!results.has(file)) results.set(file, /* @__PURE__ */ new Set());
|
|
1329
|
+
results.get(file).add(name);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
})
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return [...results.entries()].map(([file, matches]) => ({
|
|
1338
|
+
file,
|
|
1339
|
+
matches: [...matches]
|
|
1340
|
+
})).sort((a, b) => b.matches.length - a.matches.length);
|
|
1341
|
+
}
|
|
1342
|
+
async function getRelatedTests(rootDir, targetFiles) {
|
|
1343
|
+
const testPatterns = [
|
|
1344
|
+
"**/*.test.*",
|
|
1345
|
+
"**/*.spec.*",
|
|
1346
|
+
"**/*Test.kt",
|
|
1347
|
+
"**/*Test.java",
|
|
1348
|
+
"**/*Tests.kt",
|
|
1349
|
+
"**/*Tests.java",
|
|
1350
|
+
"**/test_*.py",
|
|
1351
|
+
"**/*_test.py",
|
|
1352
|
+
"**/*_test.go",
|
|
1353
|
+
"**/*Tests.swift",
|
|
1354
|
+
"**/*Test.swift",
|
|
1355
|
+
"**/*_test.rs"
|
|
1356
|
+
];
|
|
1357
|
+
const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
|
|
1358
|
+
const results = [];
|
|
1359
|
+
for (const target of targetFiles) {
|
|
1360
|
+
const targetBaseName = path4.basename(target).replace(/\.[^.]+$/, "");
|
|
1361
|
+
for (const testFile of testFiles) {
|
|
1362
|
+
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1363
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1364
|
+
if (sourceName === targetBaseName) {
|
|
1365
|
+
results.push({
|
|
1366
|
+
file: testFile,
|
|
1367
|
+
confidence: "exact"
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return results;
|
|
1373
|
+
}
|
|
1374
|
+
function escapeRegex(str) {
|
|
1375
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1376
|
+
}
|
|
1377
|
+
var exec7, IGNORE, SOURCE_EXTENSIONS2;
|
|
1378
|
+
var init_impact = __esm({
|
|
1379
|
+
"src/impact/impact.ts"() {
|
|
1380
|
+
"use strict";
|
|
1381
|
+
exec7 = promisify7(execFile7);
|
|
1382
|
+
IGNORE = [
|
|
1383
|
+
"**/node_modules/**",
|
|
1384
|
+
"**/dist/**",
|
|
1385
|
+
"**/build/**",
|
|
1386
|
+
"**/.gradle/**",
|
|
1387
|
+
"**/target/**",
|
|
1388
|
+
"**/.git/**",
|
|
1389
|
+
"**/vendor/**",
|
|
1390
|
+
"**/__pycache__/**",
|
|
1391
|
+
"**/venv/**",
|
|
1392
|
+
"**/.venv/**",
|
|
1393
|
+
"**/generated/**"
|
|
1394
|
+
];
|
|
1395
|
+
SOURCE_EXTENSIONS2 = "*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,h,dart,gradle.kts,gradle}";
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
// src/mcp/tools.ts
|
|
1400
|
+
import fs6 from "fs/promises";
|
|
1401
|
+
import path5 from "path";
|
|
1402
|
+
import { execFile as execFile8 } from "child_process";
|
|
1403
|
+
import { promisify as promisify8 } from "util";
|
|
1404
|
+
import fg4 from "fast-glob";
|
|
1191
1405
|
async function buildContext(dir) {
|
|
1192
1406
|
return {
|
|
1193
1407
|
rootDir: dir,
|
|
@@ -1195,7 +1409,7 @@ async function buildContext(dir) {
|
|
|
1195
1409
|
};
|
|
1196
1410
|
}
|
|
1197
1411
|
async function analyzeProject(dir) {
|
|
1198
|
-
const rootDir =
|
|
1412
|
+
const rootDir = path5.resolve(dir);
|
|
1199
1413
|
const context = await buildContext(rootDir);
|
|
1200
1414
|
const results = await runAll(context);
|
|
1201
1415
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -1249,7 +1463,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1249
1463
|
const present = [];
|
|
1250
1464
|
for (const file of buildFiles) {
|
|
1251
1465
|
try {
|
|
1252
|
-
await
|
|
1466
|
+
await fs6.access(path5.join(rootDir, file));
|
|
1253
1467
|
present.push(file);
|
|
1254
1468
|
} catch {
|
|
1255
1469
|
}
|
|
@@ -1267,9 +1481,9 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1267
1481
|
];
|
|
1268
1482
|
const testInfo = {};
|
|
1269
1483
|
for (const pattern of testDirs) {
|
|
1270
|
-
const files = await
|
|
1484
|
+
const files = await fg4(`${pattern}/**/*`, {
|
|
1271
1485
|
cwd: rootDir,
|
|
1272
|
-
ignore:
|
|
1486
|
+
ignore: IGNORE2,
|
|
1273
1487
|
onlyFiles: true
|
|
1274
1488
|
});
|
|
1275
1489
|
if (files.length > 0) {
|
|
@@ -1287,18 +1501,18 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1287
1501
|
{ pattern: "**/*_test.rs", label: "*_test.rs" }
|
|
1288
1502
|
];
|
|
1289
1503
|
for (const { pattern, label } of testFilePatterns) {
|
|
1290
|
-
const files = await
|
|
1504
|
+
const files = await fg4(pattern, { cwd: rootDir, ignore: IGNORE2 });
|
|
1291
1505
|
if (files.length > 0) {
|
|
1292
1506
|
testInfo[label] = files.length;
|
|
1293
1507
|
}
|
|
1294
1508
|
}
|
|
1295
|
-
const sourceFiles = await
|
|
1509
|
+
const sourceFiles = await fg4("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
|
|
1296
1510
|
cwd: rootDir,
|
|
1297
|
-
ignore:
|
|
1511
|
+
ignore: IGNORE2
|
|
1298
1512
|
});
|
|
1299
1513
|
const fileCounts = {};
|
|
1300
1514
|
for (const file of sourceFiles) {
|
|
1301
|
-
const ext =
|
|
1515
|
+
const ext = path5.extname(file).slice(1);
|
|
1302
1516
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
1303
1517
|
}
|
|
1304
1518
|
return {
|
|
@@ -1309,7 +1523,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1309
1523
|
};
|
|
1310
1524
|
}
|
|
1311
1525
|
async function getCodeSamples(dir, count = 15) {
|
|
1312
|
-
const rootDir =
|
|
1526
|
+
const rootDir = path5.resolve(dir);
|
|
1313
1527
|
const samples = await sampleFiles(rootDir, count);
|
|
1314
1528
|
const output = {
|
|
1315
1529
|
note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
|
|
@@ -1323,19 +1537,11 @@ async function getCodeSamples(dir, count = 15) {
|
|
|
1323
1537
|
};
|
|
1324
1538
|
return JSON.stringify(output, null, 2);
|
|
1325
1539
|
}
|
|
1326
|
-
async function getFileContent(dir, filePath) {
|
|
1327
|
-
const rootDir = path4.resolve(dir);
|
|
1328
|
-
const result = await readFullFile(rootDir, filePath);
|
|
1329
|
-
if (!result) {
|
|
1330
|
-
return JSON.stringify({ error: `Could not read file: ${filePath}` });
|
|
1331
|
-
}
|
|
1332
|
-
return JSON.stringify(result, null, 2);
|
|
1333
|
-
}
|
|
1334
1540
|
async function getProjectStructure(dir) {
|
|
1335
|
-
const rootDir =
|
|
1336
|
-
const allFiles = await
|
|
1541
|
+
const rootDir = path5.resolve(dir);
|
|
1542
|
+
const allFiles = await fg4("**/*", {
|
|
1337
1543
|
cwd: rootDir,
|
|
1338
|
-
ignore:
|
|
1544
|
+
ignore: IGNORE2,
|
|
1339
1545
|
onlyFiles: true
|
|
1340
1546
|
});
|
|
1341
1547
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
@@ -1348,7 +1554,7 @@ async function getProjectStructure(dir) {
|
|
|
1348
1554
|
}
|
|
1349
1555
|
const info2 = dirInfo.get(dirPath);
|
|
1350
1556
|
info2.fileCount++;
|
|
1351
|
-
const ext =
|
|
1557
|
+
const ext = path5.extname(file).slice(1);
|
|
1352
1558
|
if (ext) {
|
|
1353
1559
|
info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
|
|
1354
1560
|
}
|
|
@@ -1370,7 +1576,7 @@ async function getProjectStructure(dir) {
|
|
|
1370
1576
|
return JSON.stringify(output, null, 2);
|
|
1371
1577
|
}
|
|
1372
1578
|
async function getTestMap(dir) {
|
|
1373
|
-
const rootDir =
|
|
1579
|
+
const rootDir = path5.resolve(dir);
|
|
1374
1580
|
const testPatterns = [
|
|
1375
1581
|
"**/*.test.*",
|
|
1376
1582
|
"**/*.spec.*",
|
|
@@ -1385,15 +1591,15 @@ async function getTestMap(dir) {
|
|
|
1385
1591
|
"**/*Test.swift",
|
|
1386
1592
|
"**/*_test.rs"
|
|
1387
1593
|
];
|
|
1388
|
-
const testFiles = await
|
|
1389
|
-
const sourceFiles = await
|
|
1594
|
+
const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
1595
|
+
const sourceFiles = await fg4(
|
|
1390
1596
|
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
1391
|
-
{ cwd: rootDir, ignore:
|
|
1597
|
+
{ cwd: rootDir, ignore: IGNORE2 }
|
|
1392
1598
|
);
|
|
1393
1599
|
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1394
1600
|
for (const file of sourceFiles) {
|
|
1395
1601
|
if (testFiles.includes(file)) continue;
|
|
1396
|
-
const baseName =
|
|
1602
|
+
const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
|
|
1397
1603
|
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1398
1604
|
existing.push(file);
|
|
1399
1605
|
sourceByBaseName.set(baseName, existing);
|
|
@@ -1401,7 +1607,7 @@ async function getTestMap(dir) {
|
|
|
1401
1607
|
const pairs = [];
|
|
1402
1608
|
const unmatched = [];
|
|
1403
1609
|
for (const testFile of testFiles) {
|
|
1404
|
-
const testBaseName =
|
|
1610
|
+
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1405
1611
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1406
1612
|
if (!sourceName) {
|
|
1407
1613
|
unmatched.push(testFile);
|
|
@@ -1409,10 +1615,10 @@ async function getTestMap(dir) {
|
|
|
1409
1615
|
}
|
|
1410
1616
|
const candidates = sourceByBaseName.get(sourceName);
|
|
1411
1617
|
if (candidates && candidates.length > 0) {
|
|
1412
|
-
const testDir =
|
|
1618
|
+
const testDir = path5.dirname(testFile);
|
|
1413
1619
|
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1414
|
-
const candidateDir =
|
|
1415
|
-
const bestDir =
|
|
1620
|
+
const candidateDir = path5.dirname(candidate);
|
|
1621
|
+
const bestDir = path5.dirname(best);
|
|
1416
1622
|
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1417
1623
|
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1418
1624
|
return candidateOverlap > bestOverlap ? candidate : best;
|
|
@@ -1444,7 +1650,7 @@ function commonSegments(pathA, pathB) {
|
|
|
1444
1650
|
return count;
|
|
1445
1651
|
}
|
|
1446
1652
|
async function getSnapshot(dir) {
|
|
1447
|
-
const rootDir =
|
|
1653
|
+
const rootDir = path5.resolve(dir);
|
|
1448
1654
|
const snapshot = await loadSnapshot(rootDir);
|
|
1449
1655
|
if (!snapshot) {
|
|
1450
1656
|
return JSON.stringify({
|
|
@@ -1454,23 +1660,31 @@ async function getSnapshot(dir) {
|
|
|
1454
1660
|
}
|
|
1455
1661
|
const currentHash = await getCurrentGitHash(rootDir);
|
|
1456
1662
|
const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
|
|
1663
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
1664
|
+
const compactFeatures = {};
|
|
1665
|
+
for (const [name, feat] of Object.entries(snapshot.features)) {
|
|
1666
|
+
const unique = feat.files.filter((f) => !seenFiles.has(f));
|
|
1667
|
+
if (unique.length === 0) continue;
|
|
1668
|
+
for (const f of unique) seenFiles.add(f);
|
|
1669
|
+
compactFeatures[name] = unique;
|
|
1670
|
+
}
|
|
1671
|
+
const compactFlows = {};
|
|
1672
|
+
for (const [name, flow] of Object.entries(snapshot.flows)) {
|
|
1673
|
+
compactFlows[name] = flow.chain;
|
|
1674
|
+
}
|
|
1457
1675
|
const output = {
|
|
1458
1676
|
exists: true,
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
featureCount: Object.keys(snapshot.features).length,
|
|
1462
|
-
flowCount: Object.keys(snapshot.flows).length,
|
|
1463
|
-
features: snapshot.features,
|
|
1464
|
-
flows: snapshot.flows,
|
|
1677
|
+
features: compactFeatures,
|
|
1678
|
+
flows: compactFlows,
|
|
1465
1679
|
stale: isStale
|
|
1466
1680
|
};
|
|
1467
1681
|
if (isStale) {
|
|
1468
|
-
output.message = "Snapshot is behind HEAD.
|
|
1682
|
+
output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
|
|
1469
1683
|
}
|
|
1470
|
-
return JSON.stringify(output
|
|
1684
|
+
return JSON.stringify(output);
|
|
1471
1685
|
}
|
|
1472
1686
|
async function fullAnalysis(dir) {
|
|
1473
|
-
const rootDir =
|
|
1687
|
+
const rootDir = path5.resolve(dir);
|
|
1474
1688
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
1475
1689
|
analyzeProject(dir),
|
|
1476
1690
|
getProjectStructure(dir),
|
|
@@ -1496,7 +1710,7 @@ async function fullAnalysis(dir) {
|
|
|
1496
1710
|
return JSON.stringify(output, null, 2);
|
|
1497
1711
|
}
|
|
1498
1712
|
async function saveSnapshotData(dir, features, flows) {
|
|
1499
|
-
const rootDir =
|
|
1713
|
+
const rootDir = path5.resolve(dir);
|
|
1500
1714
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
1501
1715
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1502
1716
|
const existing = await loadSnapshot(rootDir);
|
|
@@ -1527,28 +1741,13 @@ async function saveSnapshotData(dir, features, flows) {
|
|
|
1527
1741
|
flows: Object.keys(flows).length
|
|
1528
1742
|
});
|
|
1529
1743
|
}
|
|
1530
|
-
async function
|
|
1531
|
-
const
|
|
1532
|
-
const
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
try {
|
|
1536
|
-
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1537
|
-
existing = JSON.parse(raw);
|
|
1538
|
-
} catch {
|
|
1539
|
-
}
|
|
1540
|
-
if (config.patterns) existing.patterns = config.patterns;
|
|
1541
|
-
if (config.alwaysInclude) existing.alwaysInclude = config.alwaysInclude;
|
|
1542
|
-
if (config.ignore) existing.ignore = config.ignore;
|
|
1543
|
-
await fs5.mkdir(configDir, { recursive: true });
|
|
1544
|
-
await fs5.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
1545
|
-
return JSON.stringify({
|
|
1546
|
-
status: "saved",
|
|
1547
|
-
path: configPath,
|
|
1548
|
-
config: existing
|
|
1549
|
-
});
|
|
1744
|
+
async function getImpact(dir, files) {
|
|
1745
|
+
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
1746
|
+
const rootDir = path5.resolve(dir);
|
|
1747
|
+
const result = await analyzeImpact2(rootDir, files);
|
|
1748
|
+
return JSON.stringify(result, null, 2);
|
|
1550
1749
|
}
|
|
1551
|
-
var
|
|
1750
|
+
var exec8, IGNORE2;
|
|
1552
1751
|
var init_tools = __esm({
|
|
1553
1752
|
"src/mcp/tools.ts"() {
|
|
1554
1753
|
"use strict";
|
|
@@ -1556,8 +1755,8 @@ var init_tools = __esm({
|
|
|
1556
1755
|
init_git();
|
|
1557
1756
|
init_sampler();
|
|
1558
1757
|
init_snapshot();
|
|
1559
|
-
|
|
1560
|
-
|
|
1758
|
+
exec8 = promisify8(execFile8);
|
|
1759
|
+
IGNORE2 = [
|
|
1561
1760
|
"**/node_modules/**",
|
|
1562
1761
|
"**/dist/**",
|
|
1563
1762
|
"**/build/**",
|
|
@@ -1590,12 +1789,12 @@ function createMcpServer() {
|
|
|
1590
1789
|
version: "0.1.0"
|
|
1591
1790
|
},
|
|
1592
1791
|
{
|
|
1593
|
-
instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis
|
|
1792
|
+
instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis and then save_snapshot to create one. 3) If the snapshot is stale, tell the user and offer to update it. 4) Use your native file reading tool to read files the snapshot points to. 5) Before modifying a file, call get_impact to check what else might be affected. 6) After making significant changes (new features, refactors, architecture changes), call save_snapshot to update the concept map."
|
|
1594
1793
|
}
|
|
1595
1794
|
);
|
|
1596
1795
|
server.tool(
|
|
1597
1796
|
"full_analysis",
|
|
1598
|
-
"Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then
|
|
1797
|
+
"Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then read specific files natively for full content.",
|
|
1599
1798
|
{
|
|
1600
1799
|
dir: z.string().describe("Absolute path to the project root directory")
|
|
1601
1800
|
},
|
|
@@ -1621,7 +1820,7 @@ function createMcpServer() {
|
|
|
1621
1820
|
);
|
|
1622
1821
|
server.tool(
|
|
1623
1822
|
"get_code_samples",
|
|
1624
|
-
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth.
|
|
1823
|
+
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
|
|
1625
1824
|
{
|
|
1626
1825
|
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1627
1826
|
count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
@@ -1633,46 +1832,6 @@ function createMcpServer() {
|
|
|
1633
1832
|
};
|
|
1634
1833
|
}
|
|
1635
1834
|
);
|
|
1636
|
-
server.tool(
|
|
1637
|
-
"get_file_content",
|
|
1638
|
-
"Read the full content of a specific file. Use this after get_code_samples to drill into files you want to understand fully.",
|
|
1639
|
-
{
|
|
1640
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1641
|
-
file_path: z.string().describe("Relative path to the file within the project (e.g., 'src/main.ts')")
|
|
1642
|
-
},
|
|
1643
|
-
async ({ dir, file_path }) => {
|
|
1644
|
-
const result = await getFileContent(dir, file_path);
|
|
1645
|
-
return {
|
|
1646
|
-
content: [{ type: "text", text: result }]
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
);
|
|
1650
|
-
server.tool(
|
|
1651
|
-
"get_project_structure",
|
|
1652
|
-
"Get the directory structure of a project with file counts and extension breakdown per directory. Shows top-level files and annotated directory listing up to 2 levels deep. Useful for understanding project layout before diving into code.",
|
|
1653
|
-
{
|
|
1654
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1655
|
-
},
|
|
1656
|
-
async ({ dir }) => {
|
|
1657
|
-
const result = await getProjectStructure(dir);
|
|
1658
|
-
return {
|
|
1659
|
-
content: [{ type: "text", text: result }]
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
);
|
|
1663
|
-
server.tool(
|
|
1664
|
-
"get_test_map",
|
|
1665
|
-
"Map test files to their corresponding source files by name matching. Shows which source files have tests and which don't. Useful for understanding test coverage patterns and test organization conventions.",
|
|
1666
|
-
{
|
|
1667
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1668
|
-
},
|
|
1669
|
-
async ({ dir }) => {
|
|
1670
|
-
const result = await getTestMap(dir);
|
|
1671
|
-
return {
|
|
1672
|
-
content: [{ type: "text", text: result }]
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
);
|
|
1676
1835
|
server.tool(
|
|
1677
1836
|
"get_snapshot",
|
|
1678
1837
|
"Get the project's concept map \u2014 a lookup table from features and flows to the files that implement them. Use this to jump straight to relevant files instead of exploring. Example: 'home screen' \u2192 [HomeScreen.kt, HomeViewModel.kt, HomeModule.kt]. If stale, run 'mason snapshot-update' to refresh.",
|
|
@@ -1713,20 +1872,14 @@ function createMcpServer() {
|
|
|
1713
1872
|
}
|
|
1714
1873
|
);
|
|
1715
1874
|
server.tool(
|
|
1716
|
-
"
|
|
1717
|
-
"
|
|
1875
|
+
"get_impact",
|
|
1876
|
+
"Analyze the impact of changing specific files. Returns three signals: git co-change (files that historically change together), references (files that mention the target by name), and related tests. Use this before editing a file to understand what else might need updating.",
|
|
1718
1877
|
{
|
|
1719
1878
|
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1720
|
-
|
|
1721
|
-
alwaysInclude: z.array(z.string()).optional().describe("Specific file paths to always include in samples (e.g., 'src/core/config.ts')"),
|
|
1722
|
-
ignore: z.array(z.string()).optional().describe("Additional glob patterns to ignore (e.g., '**/fixtures/**')")
|
|
1879
|
+
files: z.array(z.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
1723
1880
|
},
|
|
1724
|
-
async ({ dir,
|
|
1725
|
-
const result = await
|
|
1726
|
-
patterns,
|
|
1727
|
-
alwaysInclude,
|
|
1728
|
-
ignore
|
|
1729
|
-
});
|
|
1881
|
+
async ({ dir, files }) => {
|
|
1882
|
+
const result = await getImpact(dir, files);
|
|
1730
1883
|
return {
|
|
1731
1884
|
content: [{ type: "text", text: result }]
|
|
1732
1885
|
};
|
|
@@ -1753,8 +1906,8 @@ init_config();
|
|
|
1753
1906
|
init_providers();
|
|
1754
1907
|
init_tools();
|
|
1755
1908
|
import { Command } from "commander";
|
|
1756
|
-
import
|
|
1757
|
-
import
|
|
1909
|
+
import fs7 from "fs/promises";
|
|
1910
|
+
import path6 from "path";
|
|
1758
1911
|
import ora from "ora";
|
|
1759
1912
|
import chalk2 from "chalk";
|
|
1760
1913
|
|
|
@@ -1819,11 +1972,11 @@ function createCLI() {
|
|
|
1819
1972
|
"Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
|
|
1820
1973
|
).version("0.1.0");
|
|
1821
1974
|
program2.command("setup").description("Register Mason as an MCP server with Claude Code").option("--scope <scope>", "Config scope: user or project", "user").action(async (opts) => {
|
|
1822
|
-
const { execFile:
|
|
1823
|
-
const { promisify:
|
|
1824
|
-
const
|
|
1975
|
+
const { execFile: execFile9 } = await import("child_process");
|
|
1976
|
+
const { promisify: promisify9 } = await import("util");
|
|
1977
|
+
const exec9 = promisify9(execFile9);
|
|
1825
1978
|
try {
|
|
1826
|
-
await
|
|
1979
|
+
await exec9("claude", ["--version"]);
|
|
1827
1980
|
} catch {
|
|
1828
1981
|
error(
|
|
1829
1982
|
"Claude Code CLI not found. Install it from https://claude.ai/code"
|
|
@@ -1842,7 +1995,7 @@ function createCLI() {
|
|
|
1842
1995
|
"mason-ai",
|
|
1843
1996
|
"mcp"
|
|
1844
1997
|
];
|
|
1845
|
-
await
|
|
1998
|
+
await exec9("claude", args);
|
|
1846
1999
|
success("Mason registered with Claude Code.");
|
|
1847
2000
|
info("Restart Claude Code to start using Mason's tools.");
|
|
1848
2001
|
} catch (err) {
|
|
@@ -1897,7 +2050,7 @@ function createCLI() {
|
|
|
1897
2050
|
);
|
|
1898
2051
|
process.exit(1);
|
|
1899
2052
|
}
|
|
1900
|
-
const rootDir =
|
|
2053
|
+
const rootDir = path6.resolve(dir);
|
|
1901
2054
|
const runConfig = opts.model ? { ...config, model: opts.model } : config;
|
|
1902
2055
|
const spinner = ora("Analyzing codebase...").start();
|
|
1903
2056
|
const analysisData = await fullAnalysis(rootDir);
|
|
@@ -1927,11 +2080,40 @@ ${analysisData}`
|
|
|
1927
2080
|
error("LLM returned empty response.");
|
|
1928
2081
|
process.exit(1);
|
|
1929
2082
|
}
|
|
1930
|
-
const claudeDir =
|
|
1931
|
-
await
|
|
1932
|
-
const outPath =
|
|
1933
|
-
await
|
|
2083
|
+
const claudeDir = path6.join(rootDir, ".claude");
|
|
2084
|
+
await fs7.mkdir(claudeDir, { recursive: true });
|
|
2085
|
+
const outPath = path6.join(claudeDir, "CLAUDE.md");
|
|
2086
|
+
await fs7.writeFile(outPath, markdown, "utf-8");
|
|
1934
2087
|
success(`Generated ${outPath}`);
|
|
2088
|
+
try {
|
|
2089
|
+
const { createSnapshot: createSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2090
|
+
const spinner2 = ora("Building concept map...").start();
|
|
2091
|
+
const snapshot = await createSnapshot2(rootDir, runConfig);
|
|
2092
|
+
spinner2.stop();
|
|
2093
|
+
const featureCount = Object.keys(snapshot.features).length;
|
|
2094
|
+
const flowCount = Object.keys(snapshot.flows).length;
|
|
2095
|
+
success(
|
|
2096
|
+
`Concept map created: ${featureCount} features, ${flowCount} flows`
|
|
2097
|
+
);
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
const hookPath = path6.join(rootDir, ".git", "hooks", "post-commit");
|
|
2101
|
+
try {
|
|
2102
|
+
const hookContent = await fs7.readFile(hookPath, "utf-8");
|
|
2103
|
+
if (!hookContent.includes("mason snapshot-update")) {
|
|
2104
|
+
console.log(
|
|
2105
|
+
chalk2.gray(
|
|
2106
|
+
'\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
|
|
2107
|
+
)
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
} catch {
|
|
2111
|
+
console.log(
|
|
2112
|
+
chalk2.gray(
|
|
2113
|
+
'\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
|
|
2114
|
+
)
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
1935
2117
|
} catch (err) {
|
|
1936
2118
|
spinner.stop();
|
|
1937
2119
|
error(
|
|
@@ -1945,7 +2127,7 @@ ${analysisData}`
|
|
|
1945
2127
|
createSnapshot: createSnapshot2,
|
|
1946
2128
|
installHook: installHook2
|
|
1947
2129
|
} = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
1948
|
-
const rootDir =
|
|
2130
|
+
const rootDir = path6.resolve(dir);
|
|
1949
2131
|
if (opts.installHook) {
|
|
1950
2132
|
try {
|
|
1951
2133
|
await installHook2(rootDir);
|
|
@@ -1983,7 +2165,7 @@ ${analysisData}`
|
|
|
1983
2165
|
});
|
|
1984
2166
|
program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
|
|
1985
2167
|
const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
1986
|
-
const rootDir =
|
|
2168
|
+
const rootDir = path6.resolve(dir);
|
|
1987
2169
|
const config = await loadConfig();
|
|
1988
2170
|
if (!config) return;
|
|
1989
2171
|
try {
|
|
@@ -1995,8 +2177,46 @@ ${analysisData}`
|
|
|
1995
2177
|
} catch {
|
|
1996
2178
|
}
|
|
1997
2179
|
});
|
|
2180
|
+
program2.command("impact").description("Show files affected by changes to a given file").argument("<files...>", "File paths or names to analyze").option("-d, --dir <dir>", "Project directory", ".").action(async (files, opts) => {
|
|
2181
|
+
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
2182
|
+
const rootDir = path6.resolve(opts.dir);
|
|
2183
|
+
const spinner = ora("Analyzing impact...").start();
|
|
2184
|
+
const result = await analyzeImpact2(rootDir, files);
|
|
2185
|
+
spinner.stop();
|
|
2186
|
+
console.log(chalk2.bold(`
|
|
2187
|
+
Impact analysis for: ${result.targetFiles.join(", ")}
|
|
2188
|
+
`));
|
|
2189
|
+
if (result.cochange.length > 0) {
|
|
2190
|
+
console.log(chalk2.bold(" Co-change (files that historically change together):"));
|
|
2191
|
+
for (const entry of result.cochange) {
|
|
2192
|
+
const rate = chalk2.gray(`${Math.round(entry.cochangeRate * 100)}%`);
|
|
2193
|
+
console.log(` ${rate} ${entry.file} ${chalk2.gray(`(${entry.sharedCommits} shared commits)`)}`);
|
|
2194
|
+
}
|
|
2195
|
+
console.log();
|
|
2196
|
+
}
|
|
2197
|
+
if (result.references.length > 0) {
|
|
2198
|
+
console.log(chalk2.bold(" References (files that mention the target):"));
|
|
2199
|
+
for (const entry of result.references) {
|
|
2200
|
+
console.log(` ${entry.file} ${chalk2.gray(`[${entry.matches.join(", ")}]`)}`);
|
|
2201
|
+
}
|
|
2202
|
+
console.log();
|
|
2203
|
+
}
|
|
2204
|
+
if (result.tests.length > 0) {
|
|
2205
|
+
console.log(chalk2.bold(" Related tests:"));
|
|
2206
|
+
for (const entry of result.tests) {
|
|
2207
|
+
console.log(` ${entry.file} ${chalk2.gray(`(${entry.confidence})`)}`);
|
|
2208
|
+
}
|
|
2209
|
+
console.log();
|
|
2210
|
+
}
|
|
2211
|
+
const total = result.cochange.length + result.references.length + result.tests.length;
|
|
2212
|
+
if (total === 0) {
|
|
2213
|
+
info("No impact detected \u2014 this file may be independent.");
|
|
2214
|
+
} else {
|
|
2215
|
+
console.log(chalk2.bold(` ${total} related file(s) found`));
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
1998
2218
|
program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
|
|
1999
|
-
const rootDir =
|
|
2219
|
+
const rootDir = path6.resolve(dir);
|
|
2000
2220
|
const spinner = ora("Analyzing codebase...").start();
|
|
2001
2221
|
const context = await buildContext2(rootDir);
|
|
2002
2222
|
const results = await runAll(context);
|