archtracker-mcp 0.1.1 → 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/dist/cli/index.js CHANGED
@@ -1,12 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli/index.ts
4
10
  import { Command } from "commander";
5
11
  import { watch } from "fs";
6
12
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
7
- import { join as join2 } from "path";
13
+ import { join as join5 } from "path";
8
14
 
9
15
  // src/analyzer/analyze.ts
16
+ import { resolve as resolve4 } from "path";
17
+
18
+ // src/analyzer/engines/dependency-cruiser.ts
10
19
  import { resolve } from "path";
11
20
  import { cruise } from "dependency-cruiser";
12
21
  var DEFAULT_EXCLUDE = [
@@ -17,110 +26,630 @@ var DEFAULT_EXCLUDE = [
17
26
  "coverage",
18
27
  "\\.archtracker"
19
28
  ];
20
- async function analyzeProject(rootDir, options = {}) {
21
- const {
22
- exclude = [],
23
- maxDepth = 0,
24
- tsConfigPath,
25
- includeTypeOnly = true
26
- } = options;
27
- const absRootDir = resolve(rootDir);
28
- const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
29
- const excludePattern = allExclude.join("|");
30
- const cruiseOptions = {
31
- baseDir: absRootDir,
32
- exclude: { path: excludePattern },
33
- doNotFollow: { path: "node_modules" },
34
- maxDepth,
35
- tsPreCompilationDeps: includeTypeOnly ? true : false,
36
- combinedDependencies: false
37
- };
38
- if (tsConfigPath) {
39
- cruiseOptions.tsConfig = { fileName: tsConfigPath };
29
+ var DependencyCruiserEngine = class {
30
+ async analyze(rootDir, options = {}) {
31
+ const {
32
+ exclude = [],
33
+ maxDepth = 0,
34
+ tsConfigPath,
35
+ includeTypeOnly = true
36
+ } = options;
37
+ const absRootDir = resolve(rootDir);
38
+ const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
39
+ const excludePattern = allExclude.join("|");
40
+ const cruiseOptions = {
41
+ baseDir: absRootDir,
42
+ exclude: { path: excludePattern },
43
+ doNotFollow: { path: "node_modules" },
44
+ maxDepth,
45
+ tsPreCompilationDeps: includeTypeOnly ? true : false,
46
+ combinedDependencies: false
47
+ };
48
+ if (tsConfigPath) {
49
+ cruiseOptions.tsConfig = { fileName: tsConfigPath };
50
+ }
51
+ let result;
52
+ try {
53
+ result = await cruise(["."], cruiseOptions);
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`dependency-cruiser failed: ${message}`, {
57
+ cause: error
58
+ });
59
+ }
60
+ if (result.exitCode !== 0 && !result.output) {
61
+ throw new Error(`Analysis exited with code ${result.exitCode}`);
62
+ }
63
+ const cruiseResult = result.output;
64
+ return this.buildGraph(absRootDir, cruiseResult);
40
65
  }
41
- let result;
42
- try {
43
- result = await cruise(["."], cruiseOptions);
44
- } catch (error) {
45
- const message = error instanceof Error ? error.message : String(error);
46
- throw new AnalyzerError(
47
- `dependency-cruiser \u306E\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${message}`,
48
- { cause: error }
49
- );
66
+ buildGraph(rootDir, cruiseResult) {
67
+ const files = {};
68
+ const edges = [];
69
+ const circularSet = /* @__PURE__ */ new Set();
70
+ const circularDependencies = [];
71
+ for (const mod of cruiseResult.modules) {
72
+ if (this.isExternalModule(mod)) continue;
73
+ files[mod.source] = {
74
+ path: mod.source,
75
+ exists: !mod.couldNotResolve,
76
+ dependencies: [],
77
+ dependents: []
78
+ };
79
+ }
80
+ for (const mod of cruiseResult.modules) {
81
+ for (const dep of mod.dependencies) {
82
+ if (dep.couldNotResolve || dep.coreModule) continue;
83
+ if (!files[mod.source] || this.isExternalDep(dep)) continue;
84
+ const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
85
+ edges.push({ source: mod.source, target: dep.resolved, type: edgeType });
86
+ if (files[mod.source]) {
87
+ files[mod.source].dependencies.push(dep.resolved);
88
+ }
89
+ if (files[dep.resolved]) {
90
+ files[dep.resolved].dependents.push(mod.source);
91
+ }
92
+ if (dep.circular && dep.cycle) {
93
+ const cyclePath = dep.cycle.map((c) => c.name);
94
+ const cycleKey = [...cyclePath].sort().join("\u2192");
95
+ if (!circularSet.has(cycleKey)) {
96
+ circularSet.add(cycleKey);
97
+ circularDependencies.push({ cycle: cyclePath });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return {
103
+ rootDir,
104
+ files,
105
+ edges,
106
+ circularDependencies,
107
+ totalFiles: Object.keys(files).length,
108
+ totalEdges: edges.length
109
+ };
50
110
  }
51
- if (result.exitCode !== 0 && !result.output) {
52
- throw new AnalyzerError(
53
- `\u89E3\u6790\u304C\u30A8\u30E9\u30FC\u30B3\u30FC\u30C9 ${result.exitCode} \u3067\u7D42\u4E86\u3057\u307E\u3057\u305F`
54
- );
111
+ isExternalModule(mod) {
112
+ if (mod.coreModule) return true;
113
+ const depTypes = mod.dependencyTypes ?? [];
114
+ if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
115
+ return isExternalPath(mod.source);
55
116
  }
56
- const cruiseResult = result.output;
57
- return buildGraph(absRootDir, cruiseResult);
117
+ isExternalDep(dep) {
118
+ if (dep.coreModule) return true;
119
+ if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core"))
120
+ return true;
121
+ return isExternalPath(dep.resolved);
122
+ }
123
+ };
124
+ function isExternalPath(source) {
125
+ if (source.startsWith("@")) return true;
126
+ if (!source.includes("/") && !source.includes("\\") && !source.includes("."))
127
+ return true;
128
+ if (source.startsWith("node:")) return true;
129
+ return false;
58
130
  }
59
- function buildGraph(rootDir, cruiseResult) {
60
- const files = {};
61
- const edges = [];
62
- const circularSet = /* @__PURE__ */ new Set();
63
- const circularDependencies = [];
64
- for (const mod of cruiseResult.modules) {
65
- if (isExternalModule(mod)) continue;
66
- files[mod.source] = {
67
- path: mod.source,
68
- exists: !mod.couldNotResolve,
69
- dependencies: [],
70
- dependents: []
131
+
132
+ // src/analyzer/engines/regex-engine.ts
133
+ import { readdir, readFile } from "fs/promises";
134
+ import { join, relative, resolve as resolve2 } from "path";
135
+
136
+ // src/analyzer/engines/cycle.ts
137
+ function detectCycles(edges) {
138
+ const adj = /* @__PURE__ */ new Map();
139
+ for (const edge of edges) {
140
+ if (!adj.has(edge.source)) adj.set(edge.source, []);
141
+ adj.get(edge.source).push(edge.target);
142
+ }
143
+ const visited = /* @__PURE__ */ new Set();
144
+ const inStack = /* @__PURE__ */ new Set();
145
+ const cycles = [];
146
+ const cycleKeys = /* @__PURE__ */ new Set();
147
+ function dfs(node, path) {
148
+ if (inStack.has(node)) {
149
+ const cycleStart = path.indexOf(node);
150
+ if (cycleStart !== -1) {
151
+ const cycle = path.slice(cycleStart);
152
+ const key = [...cycle].sort().join("\u2192");
153
+ if (!cycleKeys.has(key)) {
154
+ cycleKeys.add(key);
155
+ cycles.push({ cycle });
156
+ }
157
+ }
158
+ return;
159
+ }
160
+ if (visited.has(node)) return;
161
+ visited.add(node);
162
+ inStack.add(node);
163
+ path.push(node);
164
+ for (const neighbor of adj.get(node) ?? []) {
165
+ dfs(neighbor, path);
166
+ }
167
+ path.pop();
168
+ inStack.delete(node);
169
+ }
170
+ for (const node of adj.keys()) {
171
+ if (!visited.has(node)) {
172
+ dfs(node, []);
173
+ }
174
+ }
175
+ return cycles;
176
+ }
177
+
178
+ // src/analyzer/engines/regex-engine.ts
179
+ var RegexEngine = class {
180
+ constructor(config) {
181
+ this.config = config;
182
+ }
183
+ async analyze(rootDir, options = {}) {
184
+ const absRootDir = resolve2(rootDir);
185
+ const excludePatterns = [
186
+ ...this.config.defaultExclude ?? [],
187
+ ...options.exclude ?? [],
188
+ "\\.archtracker"
189
+ ].map((p) => new RegExp(p));
190
+ const projectFiles = await this.collectFiles(
191
+ absRootDir,
192
+ excludePatterns,
193
+ options.maxDepth ?? 0
194
+ );
195
+ const projectFileSet = new Set(projectFiles);
196
+ const files = {};
197
+ const edges = [];
198
+ for (const filePath of projectFiles) {
199
+ const relPath = relative(absRootDir, filePath);
200
+ files[relPath] = {
201
+ path: relPath,
202
+ exists: true,
203
+ dependencies: [],
204
+ dependents: []
205
+ };
206
+ }
207
+ for (const filePath of projectFiles) {
208
+ const relSource = relative(absRootDir, filePath);
209
+ let content;
210
+ try {
211
+ content = await readFile(filePath, "utf-8");
212
+ } catch {
213
+ continue;
214
+ }
215
+ const imports = this.extractImports(content);
216
+ for (const importPath of imports) {
217
+ const resolved = this.config.resolveImport(
218
+ importPath,
219
+ filePath,
220
+ absRootDir,
221
+ projectFileSet
222
+ );
223
+ if (!resolved) continue;
224
+ const relTarget = relative(absRootDir, resolved);
225
+ if (!files[relTarget]) continue;
226
+ edges.push({
227
+ source: relSource,
228
+ target: relTarget,
229
+ type: "static"
230
+ });
231
+ files[relSource].dependencies.push(relTarget);
232
+ files[relTarget].dependents.push(relSource);
233
+ }
234
+ }
235
+ const circularDependencies = detectCycles(edges);
236
+ return {
237
+ rootDir: absRootDir,
238
+ files,
239
+ edges,
240
+ circularDependencies,
241
+ totalFiles: Object.keys(files).length,
242
+ totalEdges: edges.length
71
243
  };
72
244
  }
73
- for (const mod of cruiseResult.modules) {
74
- for (const dep of mod.dependencies) {
75
- if (dep.couldNotResolve || dep.coreModule) continue;
76
- if (!files[mod.source] || isExternalDep(dep)) continue;
77
- const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
78
- edges.push({
79
- source: mod.source,
80
- target: dep.resolved,
81
- type: edgeType
82
- });
83
- if (files[mod.source]) {
84
- files[mod.source].dependencies.push(dep.resolved);
245
+ extractImports(content) {
246
+ const imports = [];
247
+ for (const pattern of this.config.importPatterns) {
248
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
249
+ let match;
250
+ while ((match = regex.exec(content)) !== null) {
251
+ if (match[1]) {
252
+ imports.push(match[1]);
253
+ }
85
254
  }
86
- if (files[dep.resolved]) {
87
- files[dep.resolved].dependents.push(mod.source);
255
+ }
256
+ return imports;
257
+ }
258
+ async collectFiles(dir, excludePatterns, maxDepth, currentDepth = 0) {
259
+ if (maxDepth > 0 && currentDepth >= maxDepth) return [];
260
+ const results = [];
261
+ let entries;
262
+ try {
263
+ entries = await readdir(dir, { withFileTypes: true });
264
+ } catch {
265
+ return results;
266
+ }
267
+ for (const entry of entries) {
268
+ const fullPath = join(dir, entry.name);
269
+ const relPath = relative(dir, fullPath);
270
+ if (excludePatterns.some(
271
+ (p) => p.test(entry.name) || p.test(relPath) || p.test(fullPath)
272
+ )) {
273
+ continue;
88
274
  }
89
- if (dep.circular && dep.cycle) {
90
- const cyclePath = dep.cycle.map((c) => c.name);
91
- const cycleKey = [...cyclePath].sort().join("\u2192");
92
- if (!circularSet.has(cycleKey)) {
93
- circularSet.add(cycleKey);
94
- circularDependencies.push({ cycle: cyclePath });
275
+ if (entry.isDirectory()) {
276
+ if (entry.name.startsWith(".")) continue;
277
+ const sub = await this.collectFiles(
278
+ fullPath,
279
+ excludePatterns,
280
+ maxDepth,
281
+ currentDepth + 1
282
+ );
283
+ results.push(...sub);
284
+ } else if (entry.isFile()) {
285
+ const dotIdx = entry.name.lastIndexOf(".");
286
+ if (dotIdx > 0) {
287
+ const ext = entry.name.slice(dotIdx);
288
+ if (this.config.extensions.includes(ext)) {
289
+ results.push(fullPath);
290
+ }
95
291
  }
96
292
  }
97
293
  }
294
+ return results;
98
295
  }
99
- return {
100
- rootDir,
101
- files,
102
- edges,
103
- circularDependencies,
104
- totalFiles: Object.keys(files).length,
105
- totalEdges: edges.length
106
- };
296
+ };
297
+
298
+ // src/analyzer/engines/detect.ts
299
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
300
+ import { join as join2 } from "path";
301
+ var MARKERS = [
302
+ { file: "Cargo.toml", language: "rust" },
303
+ { file: "go.mod", language: "go" },
304
+ { file: "pyproject.toml", language: "python" },
305
+ { file: "setup.py", language: "python" },
306
+ { file: "requirements.txt", language: "python" },
307
+ { file: "Pipfile", language: "python" },
308
+ { file: "pom.xml", language: "java" },
309
+ { file: "build.gradle", language: "java" },
310
+ { file: "build.gradle.kts", language: "kotlin" },
311
+ { file: "Package.swift", language: "swift" },
312
+ { file: "Gemfile", language: "ruby" },
313
+ { file: "composer.json", language: "php" },
314
+ { file: "CMakeLists.txt", language: "c-cpp" },
315
+ { file: "Makefile", language: "c-cpp" },
316
+ { file: "package.json", language: "javascript" },
317
+ { file: "tsconfig.json", language: "javascript" }
318
+ ];
319
+ var EXT_MAP = {
320
+ ".ts": "javascript",
321
+ ".tsx": "javascript",
322
+ ".js": "javascript",
323
+ ".jsx": "javascript",
324
+ ".mjs": "javascript",
325
+ ".cjs": "javascript",
326
+ ".py": "python",
327
+ ".rs": "rust",
328
+ ".go": "go",
329
+ ".java": "java",
330
+ ".c": "c-cpp",
331
+ ".cpp": "c-cpp",
332
+ ".cc": "c-cpp",
333
+ ".cxx": "c-cpp",
334
+ ".h": "c-cpp",
335
+ ".hpp": "c-cpp",
336
+ ".rb": "ruby",
337
+ ".php": "php",
338
+ ".swift": "swift",
339
+ ".kt": "kotlin",
340
+ ".kts": "kotlin"
341
+ };
342
+ async function detectLanguage(rootDir) {
343
+ for (const marker of MARKERS) {
344
+ try {
345
+ const s = await stat2(join2(rootDir, marker.file));
346
+ if (s.isFile() || s.isDirectory()) {
347
+ return marker.language;
348
+ }
349
+ } catch {
350
+ }
351
+ }
352
+ const counts = /* @__PURE__ */ new Map();
353
+ try {
354
+ await scanExtensions(rootDir, counts, 2, 0);
355
+ } catch {
356
+ }
357
+ if (counts.size > 0) {
358
+ let maxLang = "javascript";
359
+ let maxCount = 0;
360
+ for (const [lang, count] of counts) {
361
+ if (count > maxCount) {
362
+ maxCount = count;
363
+ maxLang = lang;
364
+ }
365
+ }
366
+ return maxLang;
367
+ }
368
+ return "javascript";
107
369
  }
108
- function isExternalModule(mod) {
109
- if (mod.coreModule) return true;
110
- const depTypes = mod.dependencyTypes ?? [];
111
- if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
112
- return isExternalPath(mod.source);
370
+ async function scanExtensions(dir, counts, maxDepth, currentDepth) {
371
+ if (currentDepth >= maxDepth) return;
372
+ const entries = await readdir2(dir, { withFileTypes: true });
373
+ for (const entry of entries) {
374
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
375
+ if (entry.isDirectory() && currentDepth < maxDepth - 1) {
376
+ await scanExtensions(
377
+ join2(dir, entry.name),
378
+ counts,
379
+ maxDepth,
380
+ currentDepth + 1
381
+ );
382
+ } else if (entry.isFile()) {
383
+ const dotIdx = entry.name.lastIndexOf(".");
384
+ if (dotIdx > 0) {
385
+ const ext = entry.name.slice(dotIdx);
386
+ const lang = EXT_MAP[ext];
387
+ if (lang) {
388
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
389
+ }
390
+ }
391
+ }
392
+ }
113
393
  }
114
- function isExternalDep(dep) {
115
- if (dep.coreModule) return true;
116
- if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
117
- return isExternalPath(dep.resolved);
394
+
395
+ // src/analyzer/engines/languages.ts
396
+ import { join as join3, dirname, resolve as resolve3 } from "path";
397
+ var python = {
398
+ id: "python",
399
+ extensions: [".py"],
400
+ importPatterns: [
401
+ // from package.module import something
402
+ { regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm },
403
+ // import package.module
404
+ { regex: /^import\s+([\w.]+)/gm }
405
+ ],
406
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
407
+ if (importPath.startsWith(".")) {
408
+ const dots = importPath.match(/^\.+/)?.[0].length ?? 1;
409
+ let base = dirname(sourceFile);
410
+ for (let i = 1; i < dots; i++) base = dirname(base);
411
+ const rest = importPath.slice(dots).replace(/\./g, "/");
412
+ return tryPythonResolve(join3(base, rest), projectFiles);
413
+ }
414
+ const parts = importPath.replace(/\./g, "/");
415
+ return tryPythonResolve(join3(rootDir, parts), projectFiles);
416
+ },
417
+ defaultExclude: ["__pycache__", "\\.venv", "venv", "\\.egg-info", "dist", "build"]
418
+ };
419
+ function tryPythonResolve(base, projectFiles) {
420
+ if (projectFiles.has(base + ".py")) return base + ".py";
421
+ if (projectFiles.has(join3(base, "__init__.py"))) return join3(base, "__init__.py");
422
+ return null;
118
423
  }
119
- function isExternalPath(source) {
120
- if (source.startsWith("@")) return true;
121
- if (!source.includes("/") && !source.includes("\\") && !source.includes(".")) return true;
122
- if (source.startsWith("node:")) return true;
123
- return false;
424
+ var rust = {
425
+ id: "rust",
426
+ extensions: [".rs"],
427
+ importPatterns: [
428
+ // use crate::module::item;
429
+ { regex: /\buse\s+crate::(\w[\w:]*)/gm },
430
+ // mod child;
431
+ { regex: /\bmod\s+(\w+)\s*;/gm }
432
+ ],
433
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
434
+ const srcDir = join3(rootDir, "src");
435
+ if (!importPath.includes("::")) {
436
+ const parentDir = dirname(sourceFile);
437
+ const asFile = join3(parentDir, importPath + ".rs");
438
+ if (projectFiles.has(asFile)) return asFile;
439
+ const asDir = join3(parentDir, importPath, "mod.rs");
440
+ if (projectFiles.has(asDir)) return asDir;
441
+ return null;
442
+ }
443
+ const segments = importPath.split("::");
444
+ for (let i = segments.length; i > 0; i--) {
445
+ const path = segments.slice(0, i).join("/");
446
+ const asFile = join3(srcDir, path + ".rs");
447
+ if (projectFiles.has(asFile)) return asFile;
448
+ const asDir = join3(srcDir, path, "mod.rs");
449
+ if (projectFiles.has(asDir)) return asDir;
450
+ }
451
+ return null;
452
+ },
453
+ defaultExclude: ["target"]
454
+ };
455
+ var go = {
456
+ id: "go",
457
+ extensions: [".go"],
458
+ importPatterns: [
459
+ // import "path" or import ( "path" )
460
+ { regex: /\bimport\s+(?:\w+\s+)?"([^"]+)"/gm },
461
+ // import block entries
462
+ { regex: /^\s+(?:\w+\s+)?"([^"]+)"/gm }
463
+ ],
464
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
465
+ const modPrefix = goModulePrefix(rootDir);
466
+ if (!modPrefix || !importPath.startsWith(modPrefix)) return null;
467
+ const relPath = importPath.slice(modPrefix.length + 1);
468
+ const pkgDir = join3(rootDir, relPath);
469
+ for (const f of projectFiles) {
470
+ if (f.startsWith(pkgDir + "/") && f.endsWith(".go")) return f;
471
+ }
472
+ return null;
473
+ },
474
+ defaultExclude: ["vendor"]
475
+ };
476
+ var goModCache = /* @__PURE__ */ new Map();
477
+ function goModulePrefix(rootDir) {
478
+ if (goModCache.has(rootDir)) return goModCache.get(rootDir);
479
+ try {
480
+ const fs = __require("fs");
481
+ const content = fs.readFileSync(join3(rootDir, "go.mod"), "utf-8");
482
+ const match = content.match(/^module\s+(.+)$/m);
483
+ const prefix = match ? match[1].trim() : null;
484
+ goModCache.set(rootDir, prefix);
485
+ return prefix;
486
+ } catch {
487
+ goModCache.set(rootDir, null);
488
+ return null;
489
+ }
490
+ }
491
+ var java = {
492
+ id: "java",
493
+ extensions: [".java"],
494
+ importPatterns: [
495
+ // import com.example.ClassName;
496
+ { regex: /^import\s+(?:static\s+)?([\w.]+);/gm }
497
+ ],
498
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
499
+ const filePath = importPath.replace(/\./g, "/") + ".java";
500
+ for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
501
+ const full = join3(rootDir, srcRoot, filePath);
502
+ if (projectFiles.has(full)) return full;
503
+ }
504
+ return null;
505
+ },
506
+ defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
507
+ };
508
+ var cCpp = {
509
+ id: "c-cpp",
510
+ extensions: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"],
511
+ importPatterns: [
512
+ // #include "file.h" (skip <system> includes)
513
+ { regex: /^#include\s+"([^"]+)"/gm }
514
+ ],
515
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
516
+ const fromSource = resolve3(dirname(sourceFile), importPath);
517
+ if (projectFiles.has(fromSource)) return fromSource;
518
+ const fromRoot = join3(rootDir, importPath);
519
+ if (projectFiles.has(fromRoot)) return fromRoot;
520
+ for (const incDir of ["include", "src"]) {
521
+ const full = join3(rootDir, incDir, importPath);
522
+ if (projectFiles.has(full)) return full;
523
+ }
524
+ return null;
525
+ },
526
+ defaultExclude: ["build", "cmake-build", "\\.o$", "\\.obj$"]
527
+ };
528
+ var ruby = {
529
+ id: "ruby",
530
+ extensions: [".rb"],
531
+ importPatterns: [
532
+ // require_relative 'path'
533
+ { regex: /\brequire_relative\s+['"]([^'"]+)['"]/gm },
534
+ // require 'path' (for project-internal requires)
535
+ { regex: /\brequire\s+['"]([^'"]+)['"]/gm }
536
+ ],
537
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
538
+ const withExt = importPath.endsWith(".rb") ? importPath : importPath + ".rb";
539
+ const fromSource = resolve3(dirname(sourceFile), withExt);
540
+ if (projectFiles.has(fromSource)) return fromSource;
541
+ const fromRoot = join3(rootDir, withExt);
542
+ if (projectFiles.has(fromRoot)) return fromRoot;
543
+ const fromLib = join3(rootDir, "lib", withExt);
544
+ if (projectFiles.has(fromLib)) return fromLib;
545
+ return null;
546
+ },
547
+ defaultExclude: ["vendor", "\\.bundle"]
548
+ };
549
+ var php = {
550
+ id: "php",
551
+ extensions: [".php"],
552
+ importPatterns: [
553
+ // require/include/require_once/include_once 'path'
554
+ { regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
555
+ // use Namespace\Class (PSR-4 style)
556
+ { regex: /^use\s+([\w\\]+)/gm }
557
+ ],
558
+ resolveImport(importPath, sourceFile, rootDir, projectFiles) {
559
+ if (importPath.includes("/") || importPath.endsWith(".php")) {
560
+ const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
561
+ const fromSource = resolve3(dirname(sourceFile), withExt);
562
+ if (projectFiles.has(fromSource)) return fromSource;
563
+ const fromRoot2 = join3(rootDir, withExt);
564
+ if (projectFiles.has(fromRoot2)) return fromRoot2;
565
+ return null;
566
+ }
567
+ const filePath = importPath.replace(/\\/g, "/") + ".php";
568
+ const fromRoot = join3(rootDir, filePath);
569
+ if (projectFiles.has(fromRoot)) return fromRoot;
570
+ const fromSrc = join3(rootDir, "src", filePath);
571
+ if (projectFiles.has(fromSrc)) return fromSrc;
572
+ return null;
573
+ },
574
+ defaultExclude: ["vendor"]
575
+ };
576
+ var swift = {
577
+ id: "swift",
578
+ extensions: [".swift"],
579
+ importPatterns: [
580
+ // import ModuleName
581
+ { regex: /^import\s+(?:class|struct|enum|protocol|func|var|let|typealias)?\s*(\w+)/gm }
582
+ ],
583
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
584
+ const spmDir = join3(rootDir, "Sources", importPath);
585
+ for (const f of projectFiles) {
586
+ if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
587
+ }
588
+ return null;
589
+ },
590
+ defaultExclude: ["\\.build", "DerivedData"]
591
+ };
592
+ var kotlin = {
593
+ id: "kotlin",
594
+ extensions: [".kt", ".kts"],
595
+ importPatterns: [
596
+ // import com.example.ClassName
597
+ { regex: /^import\s+([\w.]+)/gm }
598
+ ],
599
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
600
+ const filePath = importPath.replace(/\./g, "/");
601
+ for (const ext of [".kt", ".kts"]) {
602
+ for (const srcRoot of [
603
+ "",
604
+ "src/main/kotlin/",
605
+ "src/main/java/",
606
+ "src/",
607
+ "app/src/main/kotlin/",
608
+ "app/src/main/java/"
609
+ ]) {
610
+ const full = join3(rootDir, srcRoot, filePath + ext);
611
+ if (projectFiles.has(full)) return full;
612
+ }
613
+ }
614
+ return null;
615
+ },
616
+ defaultExclude: ["build", "\\.gradle", "\\.idea"]
617
+ };
618
+ var LANGUAGE_CONFIGS = {
619
+ javascript: null,
620
+ // handled by DependencyCruiserEngine
621
+ python,
622
+ rust,
623
+ go,
624
+ java,
625
+ "c-cpp": cCpp,
626
+ ruby,
627
+ php,
628
+ swift,
629
+ kotlin
630
+ };
631
+ function getLanguageConfig(id) {
632
+ return LANGUAGE_CONFIGS[id] ?? null;
633
+ }
634
+
635
+ // src/analyzer/analyze.ts
636
+ async function analyzeProject(rootDir, options = {}) {
637
+ const absRootDir = resolve4(rootDir);
638
+ const language = options.language ?? await detectLanguage(absRootDir);
639
+ try {
640
+ if (language === "javascript") {
641
+ return await new DependencyCruiserEngine().analyze(absRootDir, options);
642
+ }
643
+ const config = getLanguageConfig(language);
644
+ if (!config) {
645
+ throw new AnalyzerError(`No analyzer config for language: ${language}`);
646
+ }
647
+ return await new RegexEngine(config).analyze(absRootDir, options);
648
+ } catch (error) {
649
+ if (error instanceof AnalyzerError) throw error;
650
+ const message = error instanceof Error ? error.message : String(error);
651
+ throw new AnalyzerError(message, { cause: error });
652
+ }
124
653
  }
125
654
  var AnalyzerError = class extends Error {
126
655
  constructor(message, options) {
@@ -387,8 +916,8 @@ function formatAnalysisReport(graph, options = {}) {
387
916
  }
388
917
 
389
918
  // src/storage/snapshot.ts
390
- import { mkdir, writeFile, readFile, access } from "fs/promises";
391
- import { join } from "path";
919
+ import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
920
+ import { join as join4 } from "path";
392
921
  import { z } from "zod";
393
922
 
394
923
  // src/types/schema.ts
@@ -422,8 +951,8 @@ var SnapshotSchema = z.object({
422
951
  graph: DependencyGraphSchema
423
952
  });
424
953
  async function saveSnapshot(projectRoot, graph) {
425
- const dirPath = join(projectRoot, ARCHTRACKER_DIR);
426
- const filePath = join(dirPath, SNAPSHOT_FILE);
954
+ const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
955
+ const filePath = join4(dirPath, SNAPSHOT_FILE);
427
956
  const snapshot = {
428
957
  version: SCHEMA_VERSION,
429
958
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -435,10 +964,10 @@ async function saveSnapshot(projectRoot, graph) {
435
964
  return snapshot;
436
965
  }
437
966
  async function loadSnapshot(projectRoot) {
438
- const filePath = join(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
967
+ const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
439
968
  let raw;
440
969
  try {
441
- raw = await readFile(filePath, "utf-8");
970
+ raw = await readFile2(filePath, "utf-8");
442
971
  } catch (error) {
443
972
  if (isNodeError(error) && error.code === "ENOENT") {
444
973
  return null;
@@ -1828,9 +2357,9 @@ jobs:
1828
2357
  - run: npx archtracker check --target ${opts.target} --ci
1829
2358
  `;
1830
2359
  try {
1831
- const dir = join2(".github", "workflows");
2360
+ const dir = join5(".github", "workflows");
1832
2361
  await mkdir2(dir, { recursive: true });
1833
- const path = join2(dir, "arch-check.yml");
2362
+ const path = join5(dir, "arch-check.yml");
1834
2363
  await writeFile2(path, workflow, "utf-8");
1835
2364
  console.log(t("ci.generated", { path }));
1836
2365
  } catch (error) {