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 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
@@ -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 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}`);
@@ -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 }));