archtracker-mcp 0.3.1 → 0.4.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 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** — AST-based static analysis via [dependency-cruiser](https://github.com/sverweij/dependency-cruiser)
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
- - **TypeScript / JavaScript** project (for dependency analysis)
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
- - **依存関係グラフ分析** — [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) によるAST静的解析
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
- - **TypeScript / JavaScript** プロジェクト(依存関係分析用)
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
@@ -4,128 +4,15 @@
4
4
  import { Command } from "commander";
5
5
  import { watch } from "fs";
6
6
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
7
- import { join as join5 } from "path";
7
+ import { join as join6 } from "path";
8
8
 
9
9
  // src/analyzer/analyze.ts
10
- import { resolve as resolve4 } from "path";
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 as resolve2 } from "path";
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 = resolve2(rootDir);
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 resolve3 } from "path";
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 = resolve3(dirname(sourceFile), importPath);
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 = resolve3(dirname(sourceFile), withExt);
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 = resolve3(dirname(sourceFile), withExt);
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 = resolve3(dirname(sourceFile), importPath);
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: null,
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 = resolve4(rootDir);
1451
- const language = options.language ?? await detectLanguage(absRootDir);
1394
+ const absRootDir = resolve3(rootDir);
1452
1395
  try {
1453
- if (language === "javascript") {
1454
- return await new DependencyCruiserEngine().analyze(absRootDir, options);
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}`);
@@ -2981,11 +2931,30 @@ function startViewer(graph, options = {}) {
2981
2931
  };
2982
2932
  }
2983
2933
 
2934
+ // src/utils/version.ts
2935
+ import { readFileSync as readFileSync2 } from "fs";
2936
+ import { join as join5, dirname as dirname2 } from "path";
2937
+ import { fileURLToPath } from "url";
2938
+ function loadVersion() {
2939
+ let dir = dirname2(fileURLToPath(import.meta.url));
2940
+ for (let i = 0; i < 5; i++) {
2941
+ try {
2942
+ const pkg = JSON.parse(readFileSync2(join5(dir, "package.json"), "utf-8"));
2943
+ return pkg.version;
2944
+ } catch {
2945
+ dir = dirname2(dir);
2946
+ }
2947
+ }
2948
+ return "0.0.0";
2949
+ }
2950
+ var VERSION = loadVersion();
2951
+
2984
2952
  // src/cli/index.ts
2953
+ var VALID_LANGUAGES = LANGUAGE_IDS;
2985
2954
  var program = new Command();
2986
2955
  program.name("archtracker").description(
2987
2956
  "Architecture & Dependency Tracker \u2014 Prevent missed architecture changes in AI-driven development"
2988
- ).version("0.3.1").option("--lang <locale>", "Language (en/ja, auto-detected from LANG env)").hook("preAction", (thisCommand) => {
2957
+ ).version(VERSION).option("--lang <locale>", "Language (en/ja, auto-detected from LANG env)").hook("preAction", (thisCommand) => {
2989
2958
  const lang = thisCommand.opts().lang;
2990
2959
  if (lang === "en" || lang === "ja") {
2991
2960
  setLocale(lang);
@@ -2994,11 +2963,13 @@ program.name("archtracker").description(
2994
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(
2995
2964
  "-e, --exclude <patterns...>",
2996
2965
  "Exclude patterns (regex)"
2997
- ).action(async (opts) => {
2966
+ ).option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
2998
2967
  try {
2968
+ const language = validateLanguage(opts.language);
2999
2969
  console.log(t("cli.analyzing"));
3000
2970
  const graph = await analyzeProject(opts.target, {
3001
- exclude: opts.exclude
2971
+ exclude: opts.exclude,
2972
+ language
3002
2973
  });
3003
2974
  const snapshot = await saveSnapshot(opts.root, graph);
3004
2975
  console.log(t("cli.snapshotSaved"));
@@ -3027,11 +2998,13 @@ program.command("analyze").description(
3027
2998
  ).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option(
3028
2999
  "-e, --exclude <patterns...>",
3029
3000
  "Exclude patterns (regex)"
3030
- ).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) => {
3031
3002
  try {
3003
+ const language = validateLanguage(opts.language);
3032
3004
  console.log(t("cli.analyzing"));
3033
3005
  const graph = await analyzeProject(opts.target, {
3034
- exclude: opts.exclude
3006
+ exclude: opts.exclude,
3007
+ language
3035
3008
  });
3036
3009
  const report = formatAnalysisReport(graph, { topN: parseInt(opts.top, 10) });
3037
3010
  console.log(report);
@@ -3045,15 +3018,16 @@ program.command("analyze").description(
3045
3018
  });
3046
3019
  program.command("check").description(
3047
3020
  "Compare snapshot with current code and report change impacts"
3048
- ).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) => {
3049
3022
  try {
3023
+ const language = validateLanguage(opts.language);
3050
3024
  const existingSnapshot = await loadSnapshot(opts.root);
3051
3025
  if (!existingSnapshot) {
3052
3026
  console.log(t("cli.noSnapshot"));
3053
3027
  process.exit(1);
3054
3028
  }
3055
3029
  console.log(t("cli.analyzing"));
3056
- const currentGraph = await analyzeProject(opts.target);
3030
+ const currentGraph = await analyzeProject(opts.target, { language });
3057
3031
  const diff = computeDiff(existingSnapshot.graph, currentGraph);
3058
3032
  const report = formatDiffReport(diff);
3059
3033
  console.log(report);
@@ -3067,12 +3041,13 @@ program.command("check").description(
3067
3041
  });
3068
3042
  program.command("context").description(
3069
3043
  "Display current architecture context (for AI session initialization)"
3070
- ).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) => {
3071
3045
  try {
3046
+ const language = validateLanguage(opts.language);
3072
3047
  let snapshot = await loadSnapshot(opts.root);
3073
3048
  if (!snapshot) {
3074
3049
  console.log(t("cli.autoGenerating"));
3075
- const graph2 = await analyzeProject(opts.target);
3050
+ const graph2 = await analyzeProject(opts.target, { language });
3076
3051
  snapshot = await saveSnapshot(opts.root, graph2);
3077
3052
  }
3078
3053
  const graph = snapshot.graph;
@@ -3110,19 +3085,20 @@ program.command("serve").description(
3110
3085
  ).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("-p, --port <number>", "Port number", "3000").option(
3111
3086
  "-e, --exclude <patterns...>",
3112
3087
  "Exclude patterns (regex)"
3113
- ).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) => {
3114
3089
  try {
3090
+ const language = validateLanguage(opts.language);
3115
3091
  console.log(t("web.starting"));
3116
3092
  console.log(t("cli.analyzing"));
3117
3093
  let graph;
3118
3094
  let diff = null;
3119
3095
  const snapshot = await loadSnapshot(opts.root);
3120
3096
  if (snapshot) {
3121
- const currentGraph = await analyzeProject(opts.target, { exclude: opts.exclude });
3097
+ const currentGraph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
3122
3098
  diff = computeDiff(snapshot.graph, currentGraph);
3123
3099
  graph = currentGraph;
3124
3100
  } else {
3125
- graph = await analyzeProject(opts.target, { exclude: opts.exclude });
3101
+ graph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
3126
3102
  }
3127
3103
  const port = parseInt(opts.port, 10);
3128
3104
  const viewer = startViewer(graph, { port, diff });
@@ -3136,7 +3112,7 @@ program.command("serve").description(
3136
3112
  debounce = setTimeout(async () => {
3137
3113
  try {
3138
3114
  console.log(t("web.reloading"));
3139
- const newGraph = await analyzeProject(opts.target, { exclude: opts.exclude });
3115
+ const newGraph = await analyzeProject(opts.target, { exclude: opts.exclude, language });
3140
3116
  viewer.close();
3141
3117
  startViewer(newGraph, { port });
3142
3118
  console.log(t("web.reloaded"));
@@ -3170,15 +3146,22 @@ jobs:
3170
3146
  - run: npx archtracker check --target ${opts.target} --ci
3171
3147
  `;
3172
3148
  try {
3173
- const dir = join5(".github", "workflows");
3149
+ const dir = join6(".github", "workflows");
3174
3150
  await mkdir2(dir, { recursive: true });
3175
- const path = join5(dir, "arch-check.yml");
3151
+ const path = join6(dir, "arch-check.yml");
3176
3152
  await writeFile2(path, workflow, "utf-8");
3177
3153
  console.log(t("ci.generated", { path }));
3178
3154
  } catch (error) {
3179
3155
  handleError(error);
3180
3156
  }
3181
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
+ }
3182
3165
  function handleError(error) {
3183
3166
  if (error instanceof AnalyzerError) {
3184
3167
  console.error(t("error.cli.analyzer", { message: error.message }));