archtracker-mcp 0.3.2 → 0.4.1
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 +8 -4
- package/dist/cli/index.js +105 -140
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -9
- package/dist/index.js +59 -128
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +64 -131
- package/dist/mcp/index.js.map +1 -1
- package/package.json +4 -6
- package/scripts/postinstall.cjs +0 -45
- package/scripts/screenshot.mjs +0 -121
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ When AI agents modify code, they **miss cascading impacts**:
|
|
|
39
39
|
|
|
40
40
|
## Features
|
|
41
41
|
|
|
42
|
-
- **Dependency Graph Analysis** —
|
|
42
|
+
- **Dependency Graph Analysis** — Regex-based static analysis for **13 languages** (JS/TS, Python, Rust, Go, Java, C/C++, C#, Ruby, PHP, Swift, Kotlin, Dart, Scala)
|
|
43
43
|
- **Interactive Web Viewer** — Force-directed graph, hierarchy diagram, diff view with D3.js
|
|
44
44
|
- **Impact Simulation** — Click any file to visualize transitive dependents (BFS traversal)
|
|
45
45
|
- **Snapshot Diffing** — Save architecture snapshots and detect drift over time
|
|
@@ -156,6 +156,7 @@ archtracker ci-setup [options] Generate GitHub Actions workflow
|
|
|
156
156
|
Options:
|
|
157
157
|
-t, --target <dir> Target directory (default: "src")
|
|
158
158
|
-r, --root <dir> Project root (default: ".")
|
|
159
|
+
-l, --language <lang> Target language (auto-detected if omitted)
|
|
159
160
|
-p, --port <number> Port for web viewer (default: 3000)
|
|
160
161
|
-w, --watch Watch for file changes and auto-reload
|
|
161
162
|
-e, --exclude <pattern> Exclude patterns (regex)
|
|
@@ -251,7 +252,8 @@ The web viewer also supports language switching via the settings panel.
|
|
|
251
252
|
## Requirements
|
|
252
253
|
|
|
253
254
|
- **Node.js** >= 18.0.0
|
|
254
|
-
|
|
255
|
+
|
|
256
|
+
Supported languages: JavaScript/TypeScript, Python, Rust, Go, Java, C/C++, C#, Ruby, PHP, Swift, Kotlin, Dart, Scala
|
|
255
257
|
|
|
256
258
|
## Contributing
|
|
257
259
|
|
|
@@ -287,7 +289,7 @@ AI エージェントがコードを修正する際、**波及的な影響を見
|
|
|
287
289
|
|
|
288
290
|
## 機能
|
|
289
291
|
|
|
290
|
-
- **依存関係グラフ分析** —
|
|
292
|
+
- **依存関係グラフ分析** — 正規表現ベースの静的解析、**13言語**対応(JS/TS, Python, Rust, Go, Java, C/C++, C#, Ruby, PHP, Swift, Kotlin, Dart, Scala)
|
|
291
293
|
- **インタラクティブ Web ビューア** — D3.js による力学モデルグラフ、階層図、差分ビュー
|
|
292
294
|
- **影響シミュレーション** — ファイルをクリックして推移的な被依存ファイルを可視化(BFS探索)
|
|
293
295
|
- **スナップショット差分** — アーキテクチャスナップショットを保存し、ドリフトを検出
|
|
@@ -404,6 +406,7 @@ archtracker ci-setup [options] GitHub Actions ワークフローを生成
|
|
|
404
406
|
オプション:
|
|
405
407
|
-t, --target <dir> 対象ディレクトリ(デフォルト: "src")
|
|
406
408
|
-r, --root <dir> プロジェクトルート(デフォルト: ".")
|
|
409
|
+
-l, --language <lang> 対象言語(省略時は自動検出)
|
|
407
410
|
-p, --port <number> Web ビューアのポート(デフォルト: 3000)
|
|
408
411
|
-w, --watch ファイル変更の監視と自動リロード
|
|
409
412
|
-e, --exclude <pattern> 除外パターン(正規表現)
|
|
@@ -479,7 +482,8 @@ Web ビューアでも設定パネルから言語を切り替え可能です。
|
|
|
479
482
|
## 動作要件
|
|
480
483
|
|
|
481
484
|
- **Node.js** >= 18.0.0
|
|
482
|
-
|
|
485
|
+
|
|
486
|
+
対応言語: JavaScript/TypeScript, Python, Rust, Go, Java, C/C++, C#, Ruby, PHP, Swift, Kotlin, Dart, Scala
|
|
483
487
|
|
|
484
488
|
## コントリビュート
|
|
485
489
|
|
package/dist/cli/index.js
CHANGED
|
@@ -7,125 +7,12 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
7
7
|
import { join as join6 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/analyzer/analyze.ts
|
|
10
|
-
import { resolve as
|
|
11
|
-
|
|
12
|
-
// src/analyzer/engines/dependency-cruiser.ts
|
|
13
|
-
import { resolve } from "path";
|
|
14
|
-
import { cruise } from "dependency-cruiser";
|
|
15
|
-
var DEFAULT_EXCLUDE = [
|
|
16
|
-
"node_modules",
|
|
17
|
-
"\\.d\\.ts$",
|
|
18
|
-
"dist",
|
|
19
|
-
"build",
|
|
20
|
-
"coverage",
|
|
21
|
-
"\\.archtracker"
|
|
22
|
-
];
|
|
23
|
-
var DependencyCruiserEngine = class {
|
|
24
|
-
async analyze(rootDir, options = {}) {
|
|
25
|
-
const {
|
|
26
|
-
exclude = [],
|
|
27
|
-
maxDepth = 0,
|
|
28
|
-
tsConfigPath,
|
|
29
|
-
includeTypeOnly = true
|
|
30
|
-
} = options;
|
|
31
|
-
const absRootDir = resolve(rootDir);
|
|
32
|
-
const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
|
|
33
|
-
const excludePattern = allExclude.join("|");
|
|
34
|
-
const cruiseOptions = {
|
|
35
|
-
baseDir: absRootDir,
|
|
36
|
-
exclude: { path: excludePattern },
|
|
37
|
-
doNotFollow: { path: "node_modules" },
|
|
38
|
-
maxDepth,
|
|
39
|
-
tsPreCompilationDeps: includeTypeOnly ? true : false,
|
|
40
|
-
combinedDependencies: false
|
|
41
|
-
};
|
|
42
|
-
if (tsConfigPath) {
|
|
43
|
-
cruiseOptions.tsConfig = { fileName: tsConfigPath };
|
|
44
|
-
}
|
|
45
|
-
let result;
|
|
46
|
-
try {
|
|
47
|
-
result = await cruise(["."], cruiseOptions);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
-
throw new Error(`dependency-cruiser failed: ${message}`, {
|
|
51
|
-
cause: error
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
if (result.exitCode !== 0 && !result.output) {
|
|
55
|
-
throw new Error(`Analysis exited with code ${result.exitCode}`);
|
|
56
|
-
}
|
|
57
|
-
const cruiseResult = result.output;
|
|
58
|
-
return this.buildGraph(absRootDir, cruiseResult);
|
|
59
|
-
}
|
|
60
|
-
buildGraph(rootDir, cruiseResult) {
|
|
61
|
-
const files = {};
|
|
62
|
-
const edges = [];
|
|
63
|
-
const circularSet = /* @__PURE__ */ new Set();
|
|
64
|
-
const circularDependencies = [];
|
|
65
|
-
for (const mod of cruiseResult.modules) {
|
|
66
|
-
if (this.isExternalModule(mod)) continue;
|
|
67
|
-
files[mod.source] = {
|
|
68
|
-
path: mod.source,
|
|
69
|
-
exists: !mod.couldNotResolve,
|
|
70
|
-
dependencies: [],
|
|
71
|
-
dependents: []
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
for (const mod of cruiseResult.modules) {
|
|
75
|
-
for (const dep of mod.dependencies) {
|
|
76
|
-
if (dep.couldNotResolve || dep.coreModule) continue;
|
|
77
|
-
if (!files[mod.source] || this.isExternalDep(dep)) continue;
|
|
78
|
-
const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
|
|
79
|
-
edges.push({ source: mod.source, target: dep.resolved, type: edgeType });
|
|
80
|
-
if (files[mod.source]) {
|
|
81
|
-
files[mod.source].dependencies.push(dep.resolved);
|
|
82
|
-
}
|
|
83
|
-
if (files[dep.resolved]) {
|
|
84
|
-
files[dep.resolved].dependents.push(mod.source);
|
|
85
|
-
}
|
|
86
|
-
if (dep.circular && dep.cycle) {
|
|
87
|
-
const cyclePath = dep.cycle.map((c) => c.name);
|
|
88
|
-
const cycleKey = [...cyclePath].sort().join("\u2192");
|
|
89
|
-
if (!circularSet.has(cycleKey)) {
|
|
90
|
-
circularSet.add(cycleKey);
|
|
91
|
-
circularDependencies.push({ cycle: cyclePath });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
rootDir,
|
|
98
|
-
files,
|
|
99
|
-
edges,
|
|
100
|
-
circularDependencies,
|
|
101
|
-
totalFiles: Object.keys(files).length,
|
|
102
|
-
totalEdges: edges.length
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
isExternalModule(mod) {
|
|
106
|
-
if (mod.coreModule) return true;
|
|
107
|
-
const depTypes = mod.dependencyTypes ?? [];
|
|
108
|
-
if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
|
|
109
|
-
return isExternalPath(mod.source);
|
|
110
|
-
}
|
|
111
|
-
isExternalDep(dep) {
|
|
112
|
-
if (dep.coreModule) return true;
|
|
113
|
-
if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core"))
|
|
114
|
-
return true;
|
|
115
|
-
return isExternalPath(dep.resolved);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
function isExternalPath(source) {
|
|
119
|
-
if (source.startsWith("@")) return true;
|
|
120
|
-
if (!source.includes("/") && !source.includes("\\") && !source.includes("."))
|
|
121
|
-
return true;
|
|
122
|
-
if (source.startsWith("node:")) return true;
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
10
|
+
import { resolve as resolve3 } from "path";
|
|
11
|
+
import { stat as stat3 } from "fs/promises";
|
|
125
12
|
|
|
126
13
|
// src/analyzer/engines/regex-engine.ts
|
|
127
14
|
import { readdir, readFile } from "fs/promises";
|
|
128
|
-
import { join, relative, resolve
|
|
15
|
+
import { join, relative, resolve } from "path";
|
|
129
16
|
|
|
130
17
|
// src/analyzer/engines/cycle.ts
|
|
131
18
|
function detectCycles(edges) {
|
|
@@ -683,7 +570,7 @@ var RegexEngine = class {
|
|
|
683
570
|
this.config = config;
|
|
684
571
|
}
|
|
685
572
|
async analyze(rootDir, options = {}) {
|
|
686
|
-
const absRootDir =
|
|
573
|
+
const absRootDir = resolve(rootDir);
|
|
687
574
|
const excludePatterns = [
|
|
688
575
|
...this.config.defaultExclude ?? [],
|
|
689
576
|
...options.exclude ?? [],
|
|
@@ -930,7 +817,26 @@ async function scanExtensions(dir, counts, maxDepth, currentDepth) {
|
|
|
930
817
|
|
|
931
818
|
// src/analyzer/engines/languages.ts
|
|
932
819
|
import { readFileSync } from "fs";
|
|
933
|
-
import { join as join3, dirname, resolve as
|
|
820
|
+
import { join as join3, dirname, resolve as resolve2 } from "path";
|
|
821
|
+
|
|
822
|
+
// src/analyzer/engines/types.ts
|
|
823
|
+
var LANGUAGE_IDS = [
|
|
824
|
+
"javascript",
|
|
825
|
+
"python",
|
|
826
|
+
"rust",
|
|
827
|
+
"go",
|
|
828
|
+
"java",
|
|
829
|
+
"c-cpp",
|
|
830
|
+
"c-sharp",
|
|
831
|
+
"ruby",
|
|
832
|
+
"php",
|
|
833
|
+
"swift",
|
|
834
|
+
"kotlin",
|
|
835
|
+
"dart",
|
|
836
|
+
"scala"
|
|
837
|
+
];
|
|
838
|
+
|
|
839
|
+
// src/analyzer/engines/languages.ts
|
|
934
840
|
var python = {
|
|
935
841
|
id: "python",
|
|
936
842
|
extensions: [".py"],
|
|
@@ -1194,7 +1100,7 @@ var cCpp = {
|
|
|
1194
1100
|
{ regex: /^#include\s+"([^"]+)"/gm }
|
|
1195
1101
|
],
|
|
1196
1102
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1197
|
-
const fromSource =
|
|
1103
|
+
const fromSource = resolve2(dirname(sourceFile), importPath);
|
|
1198
1104
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1199
1105
|
const fromRoot = join3(rootDir, importPath);
|
|
1200
1106
|
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
@@ -1218,7 +1124,7 @@ var ruby = {
|
|
|
1218
1124
|
],
|
|
1219
1125
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1220
1126
|
const withExt = importPath.endsWith(".rb") ? importPath : importPath + ".rb";
|
|
1221
|
-
const fromSource =
|
|
1127
|
+
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1222
1128
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1223
1129
|
const fromRoot = join3(rootDir, withExt);
|
|
1224
1130
|
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
@@ -1241,7 +1147,7 @@ var php = {
|
|
|
1241
1147
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1242
1148
|
if (importPath.includes("/") || importPath.endsWith(".php")) {
|
|
1243
1149
|
const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
|
|
1244
|
-
const fromSource =
|
|
1150
|
+
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1245
1151
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1246
1152
|
const fromRoot2 = join3(rootDir, withExt);
|
|
1247
1153
|
if (projectFiles.has(fromRoot2)) return fromRoot2;
|
|
@@ -1361,7 +1267,7 @@ var dart = {
|
|
|
1361
1267
|
if (projectFiles.has(full)) return full;
|
|
1362
1268
|
return null;
|
|
1363
1269
|
}
|
|
1364
|
-
const resolved =
|
|
1270
|
+
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
1365
1271
|
if (projectFiles.has(resolved)) return resolved;
|
|
1366
1272
|
return null;
|
|
1367
1273
|
},
|
|
@@ -1425,9 +1331,47 @@ var scala = {
|
|
|
1425
1331
|
},
|
|
1426
1332
|
defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
|
|
1427
1333
|
};
|
|
1334
|
+
var javascript = {
|
|
1335
|
+
id: "javascript",
|
|
1336
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
1337
|
+
commentStyle: "c-style",
|
|
1338
|
+
importPatterns: [
|
|
1339
|
+
// ES6: import [type] [stuff from] "path"
|
|
1340
|
+
{ regex: /import\s+(?:type\s+)?(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+)["']/g },
|
|
1341
|
+
// Dynamic: import("path")
|
|
1342
|
+
{ regex: /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g },
|
|
1343
|
+
// Re-export: export [type] { stuff } from "path" / export * from "path"
|
|
1344
|
+
{ regex: /export\s+(?:type\s+)?(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?)\s+from\s+["']([^"']+)["']/g },
|
|
1345
|
+
// CommonJS: require("path")
|
|
1346
|
+
{ regex: /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g }
|
|
1347
|
+
],
|
|
1348
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1349
|
+
if (importPath.startsWith("node:")) return null;
|
|
1350
|
+
if (!importPath.startsWith(".")) return null;
|
|
1351
|
+
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
1352
|
+
if (projectFiles.has(resolved)) return resolved;
|
|
1353
|
+
if (resolved.endsWith(".js")) {
|
|
1354
|
+
const tsPath = resolved.slice(0, -3) + ".ts";
|
|
1355
|
+
if (projectFiles.has(tsPath)) return tsPath;
|
|
1356
|
+
const tsxPath = resolved.slice(0, -3) + ".tsx";
|
|
1357
|
+
if (projectFiles.has(tsxPath)) return tsxPath;
|
|
1358
|
+
}
|
|
1359
|
+
if (resolved.endsWith(".jsx")) {
|
|
1360
|
+
const tsxPath = resolved.slice(0, -4) + ".tsx";
|
|
1361
|
+
if (projectFiles.has(tsxPath)) return tsxPath;
|
|
1362
|
+
}
|
|
1363
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]) {
|
|
1364
|
+
if (projectFiles.has(resolved + ext)) return resolved + ext;
|
|
1365
|
+
}
|
|
1366
|
+
for (const idx of ["/index.ts", "/index.tsx", "/index.js", "/index.jsx"]) {
|
|
1367
|
+
if (projectFiles.has(resolved + idx)) return resolved + idx;
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
},
|
|
1371
|
+
defaultExclude: ["node_modules", "\\.d\\.ts$", "dist", "build", "coverage"]
|
|
1372
|
+
};
|
|
1428
1373
|
var LANGUAGE_CONFIGS = {
|
|
1429
|
-
javascript
|
|
1430
|
-
// handled by DependencyCruiserEngine
|
|
1374
|
+
javascript,
|
|
1431
1375
|
python,
|
|
1432
1376
|
rust,
|
|
1433
1377
|
go,
|
|
@@ -1447,12 +1391,18 @@ function getLanguageConfig(id) {
|
|
|
1447
1391
|
|
|
1448
1392
|
// src/analyzer/analyze.ts
|
|
1449
1393
|
async function analyzeProject(rootDir, options = {}) {
|
|
1450
|
-
const absRootDir =
|
|
1451
|
-
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1394
|
+
const absRootDir = resolve3(rootDir);
|
|
1452
1395
|
try {
|
|
1453
|
-
|
|
1454
|
-
|
|
1396
|
+
const s = await stat3(absRootDir);
|
|
1397
|
+
if (!s.isDirectory()) {
|
|
1398
|
+
throw new AnalyzerError(`Not a directory: ${absRootDir}`);
|
|
1455
1399
|
}
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
if (error instanceof AnalyzerError) throw error;
|
|
1402
|
+
throw new AnalyzerError(`Directory not found: ${absRootDir}`, { cause: error });
|
|
1403
|
+
}
|
|
1404
|
+
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1405
|
+
try {
|
|
1456
1406
|
const config = getLanguageConfig(language);
|
|
1457
1407
|
if (!config) {
|
|
1458
1408
|
throw new AnalyzerError(`No analyzer config for language: ${language}`);
|
|
@@ -3000,6 +2950,7 @@ function loadVersion() {
|
|
|
3000
2950
|
var VERSION = loadVersion();
|
|
3001
2951
|
|
|
3002
2952
|
// src/cli/index.ts
|
|
2953
|
+
var VALID_LANGUAGES = LANGUAGE_IDS;
|
|
3003
2954
|
var program = new Command();
|
|
3004
2955
|
program.name("archtracker").description(
|
|
3005
2956
|
"Architecture & Dependency Tracker \u2014 Prevent missed architecture changes in AI-driven development"
|
|
@@ -3012,11 +2963,13 @@ program.name("archtracker").description(
|
|
|
3012
2963
|
program.command("init").description("Generate initial snapshot and save to .archtracker/").option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option(
|
|
3013
2964
|
"-e, --exclude <patterns...>",
|
|
3014
2965
|
"Exclude patterns (regex)"
|
|
3015
|
-
).action(async (opts) => {
|
|
2966
|
+
).option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3016
2967
|
try {
|
|
2968
|
+
const language = validateLanguage(opts.language);
|
|
3017
2969
|
console.log(t("cli.analyzing"));
|
|
3018
2970
|
const graph = await analyzeProject(opts.target, {
|
|
3019
|
-
exclude: opts.exclude
|
|
2971
|
+
exclude: opts.exclude,
|
|
2972
|
+
language
|
|
3020
2973
|
});
|
|
3021
2974
|
const snapshot = await saveSnapshot(opts.root, graph);
|
|
3022
2975
|
console.log(t("cli.snapshotSaved"));
|
|
@@ -3045,11 +2998,13 @@ program.command("analyze").description(
|
|
|
3045
2998
|
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option(
|
|
3046
2999
|
"-e, --exclude <patterns...>",
|
|
3047
3000
|
"Exclude patterns (regex)"
|
|
3048
|
-
).option("-n, --top <number>", "Number of top components to show", "10").option("--save", "Also save a snapshot after analysis").action(async (opts) => {
|
|
3001
|
+
).option("-n, --top <number>", "Number of top components to show", "10").option("--save", "Also save a snapshot after analysis").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3049
3002
|
try {
|
|
3003
|
+
const language = validateLanguage(opts.language);
|
|
3050
3004
|
console.log(t("cli.analyzing"));
|
|
3051
3005
|
const graph = await analyzeProject(opts.target, {
|
|
3052
|
-
exclude: opts.exclude
|
|
3006
|
+
exclude: opts.exclude,
|
|
3007
|
+
language
|
|
3053
3008
|
});
|
|
3054
3009
|
const report = formatAnalysisReport(graph, { topN: parseInt(opts.top, 10) });
|
|
3055
3010
|
console.log(report);
|
|
@@ -3063,15 +3018,16 @@ program.command("analyze").description(
|
|
|
3063
3018
|
});
|
|
3064
3019
|
program.command("check").description(
|
|
3065
3020
|
"Compare snapshot with current code and report change impacts"
|
|
3066
|
-
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--ci", "CI mode: exit code 1 if affected files exist").action(async (opts) => {
|
|
3021
|
+
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--ci", "CI mode: exit code 1 if affected files exist").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3067
3022
|
try {
|
|
3023
|
+
const language = validateLanguage(opts.language);
|
|
3068
3024
|
const existingSnapshot = await loadSnapshot(opts.root);
|
|
3069
3025
|
if (!existingSnapshot) {
|
|
3070
3026
|
console.log(t("cli.noSnapshot"));
|
|
3071
3027
|
process.exit(1);
|
|
3072
3028
|
}
|
|
3073
3029
|
console.log(t("cli.analyzing"));
|
|
3074
|
-
const currentGraph = await analyzeProject(opts.target);
|
|
3030
|
+
const currentGraph = await analyzeProject(opts.target, { language });
|
|
3075
3031
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
3076
3032
|
const report = formatDiffReport(diff);
|
|
3077
3033
|
console.log(report);
|
|
@@ -3085,12 +3041,13 @@ program.command("check").description(
|
|
|
3085
3041
|
});
|
|
3086
3042
|
program.command("context").description(
|
|
3087
3043
|
"Display current architecture context (for AI session initialization)"
|
|
3088
|
-
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--json", "Output in JSON format").action(async (opts) => {
|
|
3044
|
+
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--json", "Output in JSON format").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3089
3045
|
try {
|
|
3046
|
+
const language = validateLanguage(opts.language);
|
|
3090
3047
|
let snapshot = await loadSnapshot(opts.root);
|
|
3091
3048
|
if (!snapshot) {
|
|
3092
3049
|
console.log(t("cli.autoGenerating"));
|
|
3093
|
-
const graph2 = await analyzeProject(opts.target);
|
|
3050
|
+
const graph2 = await analyzeProject(opts.target, { language });
|
|
3094
3051
|
snapshot = await saveSnapshot(opts.root, graph2);
|
|
3095
3052
|
}
|
|
3096
3053
|
const graph = snapshot.graph;
|
|
@@ -3128,19 +3085,20 @@ program.command("serve").description(
|
|
|
3128
3085
|
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("-p, --port <number>", "Port number", "3000").option(
|
|
3129
3086
|
"-e, --exclude <patterns...>",
|
|
3130
3087
|
"Exclude patterns (regex)"
|
|
3131
|
-
).option("-w, --watch", "Watch for file changes and auto-reload").action(async (opts) => {
|
|
3088
|
+
).option("-w, --watch", "Watch for file changes and auto-reload").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3132
3089
|
try {
|
|
3090
|
+
const language = validateLanguage(opts.language);
|
|
3133
3091
|
console.log(t("web.starting"));
|
|
3134
3092
|
console.log(t("cli.analyzing"));
|
|
3135
3093
|
let graph;
|
|
3136
3094
|
let diff = null;
|
|
3137
3095
|
const snapshot = await loadSnapshot(opts.root);
|
|
3138
3096
|
if (snapshot) {
|
|
3139
|
-
const currentGraph = await analyzeProject(opts.target, { exclude: opts.exclude });
|
|
3097
|
+
const currentGraph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
|
|
3140
3098
|
diff = computeDiff(snapshot.graph, currentGraph);
|
|
3141
3099
|
graph = currentGraph;
|
|
3142
3100
|
} else {
|
|
3143
|
-
graph = await analyzeProject(opts.target, { exclude: opts.exclude });
|
|
3101
|
+
graph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
|
|
3144
3102
|
}
|
|
3145
3103
|
const port = parseInt(opts.port, 10);
|
|
3146
3104
|
const viewer = startViewer(graph, { port, diff });
|
|
@@ -3154,7 +3112,7 @@ program.command("serve").description(
|
|
|
3154
3112
|
debounce = setTimeout(async () => {
|
|
3155
3113
|
try {
|
|
3156
3114
|
console.log(t("web.reloading"));
|
|
3157
|
-
const newGraph = await analyzeProject(opts.target, { exclude: opts.exclude });
|
|
3115
|
+
const newGraph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
|
|
3158
3116
|
viewer.close();
|
|
3159
3117
|
startViewer(newGraph, { port });
|
|
3160
3118
|
console.log(t("web.reloaded"));
|
|
@@ -3197,6 +3155,13 @@ jobs:
|
|
|
3197
3155
|
handleError(error);
|
|
3198
3156
|
}
|
|
3199
3157
|
});
|
|
3158
|
+
function validateLanguage(lang) {
|
|
3159
|
+
if (!lang) return void 0;
|
|
3160
|
+
if (VALID_LANGUAGES.includes(lang)) return lang;
|
|
3161
|
+
console.error(`Invalid language: ${lang}`);
|
|
3162
|
+
console.error(`Valid languages: ${LANGUAGE_IDS.join(", ")}`);
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
}
|
|
3200
3165
|
function handleError(error) {
|
|
3201
3166
|
if (error instanceof AnalyzerError) {
|
|
3202
3167
|
console.error(t("error.cli.analyzer", { message: error.message }));
|