archtracker-mcp 0.1.1 → 0.2.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/dist/cli/index.js +1017 -98
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +1017 -98
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1048 -123
- package/dist/mcp/index.js.map +1 -1
- package/package.json +12 -2
package/dist/mcp/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z as z2 } from "zod";
|
|
5
5
|
|
|
6
6
|
// src/analyzer/analyze.ts
|
|
7
|
+
import { resolve as resolve4 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/analyzer/engines/dependency-cruiser.ts
|
|
7
10
|
import { resolve } from "path";
|
|
8
11
|
import { cruise } from "dependency-cruiser";
|
|
9
12
|
var DEFAULT_EXCLUDE = [
|
|
@@ -14,110 +17,1026 @@ var DEFAULT_EXCLUDE = [
|
|
|
14
17
|
"coverage",
|
|
15
18
|
"\\.archtracker"
|
|
16
19
|
];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
var DependencyCruiserEngine = class {
|
|
21
|
+
async analyze(rootDir, options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
exclude = [],
|
|
24
|
+
maxDepth = 0,
|
|
25
|
+
tsConfigPath,
|
|
26
|
+
includeTypeOnly = true
|
|
27
|
+
} = options;
|
|
28
|
+
const absRootDir = resolve(rootDir);
|
|
29
|
+
const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
|
|
30
|
+
const excludePattern = allExclude.join("|");
|
|
31
|
+
const cruiseOptions = {
|
|
32
|
+
baseDir: absRootDir,
|
|
33
|
+
exclude: { path: excludePattern },
|
|
34
|
+
doNotFollow: { path: "node_modules" },
|
|
35
|
+
maxDepth,
|
|
36
|
+
tsPreCompilationDeps: includeTypeOnly ? true : false,
|
|
37
|
+
combinedDependencies: false
|
|
38
|
+
};
|
|
39
|
+
if (tsConfigPath) {
|
|
40
|
+
cruiseOptions.tsConfig = { fileName: tsConfigPath };
|
|
41
|
+
}
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
result = await cruise(["."], cruiseOptions);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
throw new Error(`dependency-cruiser failed: ${message}`, {
|
|
48
|
+
cause: error
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (result.exitCode !== 0 && !result.output) {
|
|
52
|
+
throw new Error(`Analysis exited with code ${result.exitCode}`);
|
|
53
|
+
}
|
|
54
|
+
const cruiseResult = result.output;
|
|
55
|
+
return this.buildGraph(absRootDir, cruiseResult);
|
|
37
56
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
buildGraph(rootDir, cruiseResult) {
|
|
58
|
+
const files = {};
|
|
59
|
+
const edges = [];
|
|
60
|
+
const circularSet = /* @__PURE__ */ new Set();
|
|
61
|
+
const circularDependencies = [];
|
|
62
|
+
for (const mod of cruiseResult.modules) {
|
|
63
|
+
if (this.isExternalModule(mod)) continue;
|
|
64
|
+
files[mod.source] = {
|
|
65
|
+
path: mod.source,
|
|
66
|
+
exists: !mod.couldNotResolve,
|
|
67
|
+
dependencies: [],
|
|
68
|
+
dependents: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
for (const mod of cruiseResult.modules) {
|
|
72
|
+
for (const dep of mod.dependencies) {
|
|
73
|
+
if (dep.couldNotResolve || dep.coreModule) continue;
|
|
74
|
+
if (!files[mod.source] || this.isExternalDep(dep)) continue;
|
|
75
|
+
const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
|
|
76
|
+
edges.push({ source: mod.source, target: dep.resolved, type: edgeType });
|
|
77
|
+
if (files[mod.source]) {
|
|
78
|
+
files[mod.source].dependencies.push(dep.resolved);
|
|
79
|
+
}
|
|
80
|
+
if (files[dep.resolved]) {
|
|
81
|
+
files[dep.resolved].dependents.push(mod.source);
|
|
82
|
+
}
|
|
83
|
+
if (dep.circular && dep.cycle) {
|
|
84
|
+
const cyclePath = dep.cycle.map((c) => c.name);
|
|
85
|
+
const cycleKey = [...cyclePath].sort().join("\u2192");
|
|
86
|
+
if (!circularSet.has(cycleKey)) {
|
|
87
|
+
circularSet.add(cycleKey);
|
|
88
|
+
circularDependencies.push({ cycle: cyclePath });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
rootDir,
|
|
95
|
+
files,
|
|
96
|
+
edges,
|
|
97
|
+
circularDependencies,
|
|
98
|
+
totalFiles: Object.keys(files).length,
|
|
99
|
+
totalEdges: edges.length
|
|
100
|
+
};
|
|
47
101
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
102
|
+
isExternalModule(mod) {
|
|
103
|
+
if (mod.coreModule) return true;
|
|
104
|
+
const depTypes = mod.dependencyTypes ?? [];
|
|
105
|
+
if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
|
|
106
|
+
return isExternalPath(mod.source);
|
|
107
|
+
}
|
|
108
|
+
isExternalDep(dep) {
|
|
109
|
+
if (dep.coreModule) return true;
|
|
110
|
+
if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core"))
|
|
111
|
+
return true;
|
|
112
|
+
return isExternalPath(dep.resolved);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function isExternalPath(source) {
|
|
116
|
+
if (source.startsWith("@")) return true;
|
|
117
|
+
if (!source.includes("/") && !source.includes("\\") && !source.includes("."))
|
|
118
|
+
return true;
|
|
119
|
+
if (source.startsWith("node:")) return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/analyzer/engines/regex-engine.ts
|
|
124
|
+
import { readdir, readFile } from "fs/promises";
|
|
125
|
+
import { join, relative, resolve as resolve2 } from "path";
|
|
126
|
+
|
|
127
|
+
// src/analyzer/engines/cycle.ts
|
|
128
|
+
function detectCycles(edges) {
|
|
129
|
+
const adj = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const edge of edges) {
|
|
131
|
+
if (!adj.has(edge.source)) adj.set(edge.source, []);
|
|
132
|
+
adj.get(edge.source).push(edge.target);
|
|
133
|
+
}
|
|
134
|
+
const visited = /* @__PURE__ */ new Set();
|
|
135
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
136
|
+
const cycles = [];
|
|
137
|
+
const cycleKeys = /* @__PURE__ */ new Set();
|
|
138
|
+
function dfs(node, path) {
|
|
139
|
+
if (inStack.has(node)) {
|
|
140
|
+
const cycleStart = path.indexOf(node);
|
|
141
|
+
if (cycleStart !== -1) {
|
|
142
|
+
const cycle = path.slice(cycleStart);
|
|
143
|
+
const key = [...cycle].sort().join("\u2192");
|
|
144
|
+
if (!cycleKeys.has(key)) {
|
|
145
|
+
cycleKeys.add(key);
|
|
146
|
+
cycles.push({ cycle });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (visited.has(node)) return;
|
|
152
|
+
visited.add(node);
|
|
153
|
+
inStack.add(node);
|
|
154
|
+
path.push(node);
|
|
155
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
156
|
+
dfs(neighbor, path);
|
|
157
|
+
}
|
|
158
|
+
path.pop();
|
|
159
|
+
inStack.delete(node);
|
|
160
|
+
}
|
|
161
|
+
for (const node of adj.keys()) {
|
|
162
|
+
if (!visited.has(node)) {
|
|
163
|
+
dfs(node, []);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return cycles;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/analyzer/engines/strip-comments.ts
|
|
170
|
+
function stripComments(content, style) {
|
|
171
|
+
switch (style) {
|
|
172
|
+
case "c-style":
|
|
173
|
+
return stripCStyle(content);
|
|
174
|
+
case "hash":
|
|
175
|
+
return stripHash(content);
|
|
176
|
+
case "python":
|
|
177
|
+
return stripPython(content);
|
|
178
|
+
case "ruby":
|
|
179
|
+
return stripRuby(content);
|
|
180
|
+
case "php":
|
|
181
|
+
return stripPhp(content);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function stripCStyle(content) {
|
|
185
|
+
let result = "";
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < content.length) {
|
|
188
|
+
if (content[i] === "/" && content[i + 1] === "/") {
|
|
189
|
+
while (i < content.length && content[i] !== "\n") {
|
|
190
|
+
result += " ";
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
} else if (content[i] === "/" && content[i + 1] === "*") {
|
|
194
|
+
result += " ";
|
|
195
|
+
i++;
|
|
196
|
+
result += " ";
|
|
197
|
+
i++;
|
|
198
|
+
while (i < content.length) {
|
|
199
|
+
if (content[i] === "*" && content[i + 1] === "/") {
|
|
200
|
+
result += " ";
|
|
201
|
+
i++;
|
|
202
|
+
result += " ";
|
|
203
|
+
i++;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
} else if (content[i] === '"') {
|
|
210
|
+
result += content[i];
|
|
211
|
+
i++;
|
|
212
|
+
while (i < content.length && content[i] !== '"') {
|
|
213
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
214
|
+
result += content[i];
|
|
215
|
+
i++;
|
|
216
|
+
result += content[i];
|
|
217
|
+
i++;
|
|
218
|
+
} else if (content[i] === "\n") {
|
|
219
|
+
result += "\n";
|
|
220
|
+
i++;
|
|
221
|
+
} else {
|
|
222
|
+
result += content[i];
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (i < content.length) {
|
|
227
|
+
result += content[i];
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
} else if (content[i] === "`") {
|
|
231
|
+
result += " ";
|
|
232
|
+
i++;
|
|
233
|
+
while (i < content.length && content[i] !== "`") {
|
|
234
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
if (i < content.length) {
|
|
238
|
+
result += " ";
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
result += content[i];
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
function stripHash(content) {
|
|
249
|
+
let result = "";
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (i < content.length) {
|
|
252
|
+
if (content[i] === "#") {
|
|
253
|
+
while (i < content.length && content[i] !== "\n") {
|
|
254
|
+
result += " ";
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
} else if (content[i] === '"') {
|
|
258
|
+
result += content[i];
|
|
259
|
+
i++;
|
|
260
|
+
while (i < content.length && content[i] !== '"') {
|
|
261
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
262
|
+
result += content[i++];
|
|
263
|
+
result += content[i++];
|
|
264
|
+
} else {
|
|
265
|
+
result += content[i++];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (i < content.length) {
|
|
269
|
+
result += content[i];
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
} else if (content[i] === "'") {
|
|
273
|
+
result += content[i];
|
|
274
|
+
i++;
|
|
275
|
+
while (i < content.length && content[i] !== "'") {
|
|
276
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
277
|
+
result += content[i++];
|
|
278
|
+
result += content[i++];
|
|
279
|
+
} else {
|
|
280
|
+
result += content[i++];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (i < content.length) {
|
|
284
|
+
result += content[i];
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
result += content[i];
|
|
289
|
+
i++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
function stripPython(content) {
|
|
295
|
+
let result = "";
|
|
296
|
+
let i = 0;
|
|
297
|
+
while (i < content.length) {
|
|
298
|
+
if (content[i] === '"' && content[i + 1] === '"' && content[i + 2] === '"' || content[i] === "'" && content[i + 1] === "'" && content[i + 2] === "'") {
|
|
299
|
+
const quote = content[i];
|
|
300
|
+
const tripleQuote = quote + quote + quote;
|
|
301
|
+
result += " ";
|
|
302
|
+
i += 3;
|
|
303
|
+
while (i < content.length) {
|
|
304
|
+
if (content[i] === quote && content[i + 1] === quote && content[i + 2] === quote) {
|
|
305
|
+
result += " ";
|
|
306
|
+
i += 3;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
} else if (content[i] === "#") {
|
|
313
|
+
while (i < content.length && content[i] !== "\n") {
|
|
314
|
+
result += " ";
|
|
315
|
+
i++;
|
|
316
|
+
}
|
|
317
|
+
} else if (content[i] === '"' || content[i] === "'") {
|
|
318
|
+
const quote = content[i];
|
|
319
|
+
result += content[i];
|
|
320
|
+
i++;
|
|
321
|
+
while (i < content.length && content[i] !== quote) {
|
|
322
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
323
|
+
result += content[i++];
|
|
324
|
+
result += content[i++];
|
|
325
|
+
} else if (content[i] === "\n") {
|
|
326
|
+
result += "\n";
|
|
327
|
+
i++;
|
|
328
|
+
break;
|
|
329
|
+
} else {
|
|
330
|
+
result += content[i++];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (i < content.length && content[i] === quote) {
|
|
334
|
+
result += content[i];
|
|
335
|
+
i++;
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
result += content[i];
|
|
339
|
+
i++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
function stripRuby(content) {
|
|
345
|
+
const lines = content.split("\n");
|
|
346
|
+
const result = [];
|
|
347
|
+
let inBlock = false;
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
if (!inBlock && line.startsWith("=begin")) {
|
|
350
|
+
inBlock = true;
|
|
351
|
+
result.push(" ".repeat(line.length));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (inBlock) {
|
|
355
|
+
if (line.startsWith("=end")) {
|
|
356
|
+
inBlock = false;
|
|
357
|
+
}
|
|
358
|
+
result.push(" ".repeat(line.length));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let processed = "";
|
|
362
|
+
let i = 0;
|
|
363
|
+
while (i < line.length) {
|
|
364
|
+
if (line[i] === "#") {
|
|
365
|
+
processed += " ".repeat(line.length - i);
|
|
366
|
+
break;
|
|
367
|
+
} else if (line[i] === '"') {
|
|
368
|
+
processed += line[i];
|
|
369
|
+
i++;
|
|
370
|
+
while (i < line.length && line[i] !== '"') {
|
|
371
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
372
|
+
processed += line[i++];
|
|
373
|
+
processed += line[i++];
|
|
374
|
+
} else {
|
|
375
|
+
processed += line[i++];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (i < line.length) {
|
|
379
|
+
processed += line[i];
|
|
380
|
+
i++;
|
|
381
|
+
}
|
|
382
|
+
} else if (line[i] === "'") {
|
|
383
|
+
processed += line[i];
|
|
384
|
+
i++;
|
|
385
|
+
while (i < line.length && line[i] !== "'") {
|
|
386
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
387
|
+
processed += line[i++];
|
|
388
|
+
processed += line[i++];
|
|
389
|
+
} else {
|
|
390
|
+
processed += line[i++];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (i < line.length) {
|
|
394
|
+
processed += line[i];
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
processed += line[i];
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
result.push(processed);
|
|
403
|
+
}
|
|
404
|
+
return result.join("\n");
|
|
405
|
+
}
|
|
406
|
+
function stripPhp(content) {
|
|
407
|
+
let result = "";
|
|
408
|
+
let i = 0;
|
|
409
|
+
while (i < content.length) {
|
|
410
|
+
if (content[i] === "/" && content[i + 1] === "/" || content[i] === "#") {
|
|
411
|
+
while (i < content.length && content[i] !== "\n") {
|
|
412
|
+
result += " ";
|
|
413
|
+
i++;
|
|
414
|
+
}
|
|
415
|
+
} else if (content[i] === "/" && content[i + 1] === "*") {
|
|
416
|
+
result += " ";
|
|
417
|
+
i++;
|
|
418
|
+
result += " ";
|
|
419
|
+
i++;
|
|
420
|
+
while (i < content.length) {
|
|
421
|
+
if (content[i] === "*" && content[i + 1] === "/") {
|
|
422
|
+
result += " ";
|
|
423
|
+
i++;
|
|
424
|
+
result += " ";
|
|
425
|
+
i++;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
} else if (content[i] === '"') {
|
|
432
|
+
result += content[i];
|
|
433
|
+
i++;
|
|
434
|
+
while (i < content.length && content[i] !== '"') {
|
|
435
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
436
|
+
result += content[i++];
|
|
437
|
+
result += content[i++];
|
|
438
|
+
} else {
|
|
439
|
+
result += content[i++];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (i < content.length) {
|
|
443
|
+
result += content[i];
|
|
444
|
+
i++;
|
|
445
|
+
}
|
|
446
|
+
} else if (content[i] === "'") {
|
|
447
|
+
result += content[i];
|
|
448
|
+
i++;
|
|
449
|
+
while (i < content.length && content[i] !== "'") {
|
|
450
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
451
|
+
result += content[i++];
|
|
452
|
+
result += content[i++];
|
|
453
|
+
} else {
|
|
454
|
+
result += content[i++];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (i < content.length) {
|
|
458
|
+
result += content[i];
|
|
459
|
+
i++;
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
result += content[i];
|
|
463
|
+
i++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/analyzer/engines/regex-engine.ts
|
|
470
|
+
var RegexEngine = class {
|
|
471
|
+
constructor(config) {
|
|
472
|
+
this.config = config;
|
|
52
473
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
474
|
+
async analyze(rootDir, options = {}) {
|
|
475
|
+
const absRootDir = resolve2(rootDir);
|
|
476
|
+
const excludePatterns = [
|
|
477
|
+
...this.config.defaultExclude ?? [],
|
|
478
|
+
...options.exclude ?? [],
|
|
479
|
+
"\\.archtracker"
|
|
480
|
+
].map((p) => new RegExp(p));
|
|
481
|
+
const projectFiles = await this.collectFiles(
|
|
482
|
+
absRootDir,
|
|
483
|
+
excludePatterns,
|
|
484
|
+
options.maxDepth ?? 0
|
|
485
|
+
);
|
|
486
|
+
const projectFileSet = new Set(projectFiles);
|
|
487
|
+
const files = {};
|
|
488
|
+
const edges = [];
|
|
489
|
+
const edgeSet = /* @__PURE__ */ new Set();
|
|
490
|
+
for (const filePath of projectFiles) {
|
|
491
|
+
const relPath = relative(absRootDir, filePath);
|
|
492
|
+
files[relPath] = {
|
|
493
|
+
path: relPath,
|
|
494
|
+
exists: true,
|
|
495
|
+
dependencies: [],
|
|
496
|
+
dependents: []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
for (const filePath of projectFiles) {
|
|
500
|
+
const relSource = relative(absRootDir, filePath);
|
|
501
|
+
let content;
|
|
502
|
+
try {
|
|
503
|
+
content = await readFile(filePath, "utf-8");
|
|
504
|
+
} catch {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const stripped = stripComments(content, this.config.commentStyle);
|
|
508
|
+
const imports = this.extractImports(stripped);
|
|
509
|
+
for (const importPath of imports) {
|
|
510
|
+
const resolved = this.config.resolveImport(
|
|
511
|
+
importPath,
|
|
512
|
+
filePath,
|
|
513
|
+
absRootDir,
|
|
514
|
+
projectFileSet
|
|
515
|
+
);
|
|
516
|
+
if (!resolved) continue;
|
|
517
|
+
const relTarget = relative(absRootDir, resolved);
|
|
518
|
+
if (!files[relTarget]) continue;
|
|
519
|
+
if (relSource === relTarget) continue;
|
|
520
|
+
const edgeKey = `${relSource}\0${relTarget}`;
|
|
521
|
+
if (edgeSet.has(edgeKey)) continue;
|
|
522
|
+
edgeSet.add(edgeKey);
|
|
523
|
+
edges.push({
|
|
524
|
+
source: relSource,
|
|
525
|
+
target: relTarget,
|
|
526
|
+
type: "static"
|
|
527
|
+
});
|
|
528
|
+
files[relSource].dependencies.push(relTarget);
|
|
529
|
+
files[relTarget].dependents.push(relSource);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const circularDependencies = detectCycles(edges);
|
|
533
|
+
return {
|
|
534
|
+
rootDir: absRootDir,
|
|
535
|
+
files,
|
|
536
|
+
edges,
|
|
537
|
+
circularDependencies,
|
|
538
|
+
totalFiles: Object.keys(files).length,
|
|
539
|
+
totalEdges: edges.length
|
|
68
540
|
};
|
|
69
541
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 });
|
|
542
|
+
extractImports(content) {
|
|
543
|
+
if (this.config.extractImports) {
|
|
544
|
+
return this.config.extractImports(content);
|
|
545
|
+
}
|
|
546
|
+
const imports = [];
|
|
547
|
+
for (const pattern of this.config.importPatterns) {
|
|
548
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
549
|
+
let match;
|
|
550
|
+
while ((match = regex.exec(content)) !== null) {
|
|
551
|
+
if (match[1]) {
|
|
552
|
+
imports.push(match[1]);
|
|
92
553
|
}
|
|
93
554
|
}
|
|
94
555
|
}
|
|
556
|
+
return imports;
|
|
95
557
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
558
|
+
async collectFiles(dir, excludePatterns, maxDepth, currentDepth = 0) {
|
|
559
|
+
if (maxDepth > 0 && currentDepth >= maxDepth) return [];
|
|
560
|
+
const results = [];
|
|
561
|
+
let entries;
|
|
562
|
+
try {
|
|
563
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
564
|
+
} catch {
|
|
565
|
+
return results;
|
|
566
|
+
}
|
|
567
|
+
for (const entry of entries) {
|
|
568
|
+
const fullPath = join(dir, entry.name);
|
|
569
|
+
const relPath = relative(dir, fullPath);
|
|
570
|
+
if (excludePatterns.some(
|
|
571
|
+
(p) => p.test(entry.name) || p.test(relPath) || p.test(fullPath)
|
|
572
|
+
)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (entry.isDirectory()) {
|
|
576
|
+
if (entry.name.startsWith(".")) continue;
|
|
577
|
+
const sub = await this.collectFiles(
|
|
578
|
+
fullPath,
|
|
579
|
+
excludePatterns,
|
|
580
|
+
maxDepth,
|
|
581
|
+
currentDepth + 1
|
|
582
|
+
);
|
|
583
|
+
results.push(...sub);
|
|
584
|
+
} else if (entry.isFile()) {
|
|
585
|
+
const dotIdx = entry.name.lastIndexOf(".");
|
|
586
|
+
if (dotIdx > 0) {
|
|
587
|
+
const ext = entry.name.slice(dotIdx);
|
|
588
|
+
if (this.config.extensions.includes(ext)) {
|
|
589
|
+
results.push(fullPath);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return results;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/analyzer/engines/detect.ts
|
|
599
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
600
|
+
import { join as join2 } from "path";
|
|
601
|
+
var MARKERS = [
|
|
602
|
+
{ file: "Cargo.toml", language: "rust" },
|
|
603
|
+
{ file: "go.mod", language: "go" },
|
|
604
|
+
{ file: "pyproject.toml", language: "python" },
|
|
605
|
+
{ file: "setup.py", language: "python" },
|
|
606
|
+
{ file: "requirements.txt", language: "python" },
|
|
607
|
+
{ file: "Pipfile", language: "python" },
|
|
608
|
+
{ file: "pom.xml", language: "java" },
|
|
609
|
+
{ file: "build.gradle", language: "java" },
|
|
610
|
+
{ file: "build.gradle.kts", language: "kotlin" },
|
|
611
|
+
{ file: "Package.swift", language: "swift" },
|
|
612
|
+
{ file: "Gemfile", language: "ruby" },
|
|
613
|
+
{ file: "composer.json", language: "php" },
|
|
614
|
+
{ file: "CMakeLists.txt", language: "c-cpp" },
|
|
615
|
+
{ file: "Makefile", language: "c-cpp" },
|
|
616
|
+
{ file: "package.json", language: "javascript" },
|
|
617
|
+
{ file: "tsconfig.json", language: "javascript" }
|
|
618
|
+
];
|
|
619
|
+
var EXT_MAP = {
|
|
620
|
+
".ts": "javascript",
|
|
621
|
+
".tsx": "javascript",
|
|
622
|
+
".js": "javascript",
|
|
623
|
+
".jsx": "javascript",
|
|
624
|
+
".mjs": "javascript",
|
|
625
|
+
".cjs": "javascript",
|
|
626
|
+
".py": "python",
|
|
627
|
+
".rs": "rust",
|
|
628
|
+
".go": "go",
|
|
629
|
+
".java": "java",
|
|
630
|
+
".c": "c-cpp",
|
|
631
|
+
".cpp": "c-cpp",
|
|
632
|
+
".cc": "c-cpp",
|
|
633
|
+
".cxx": "c-cpp",
|
|
634
|
+
".h": "c-cpp",
|
|
635
|
+
".hpp": "c-cpp",
|
|
636
|
+
".rb": "ruby",
|
|
637
|
+
".php": "php",
|
|
638
|
+
".swift": "swift",
|
|
639
|
+
".kt": "kotlin",
|
|
640
|
+
".kts": "kotlin"
|
|
641
|
+
};
|
|
642
|
+
async function detectLanguage(rootDir) {
|
|
643
|
+
for (const marker of MARKERS) {
|
|
644
|
+
try {
|
|
645
|
+
const s = await stat2(join2(rootDir, marker.file));
|
|
646
|
+
if (s.isFile() || s.isDirectory()) {
|
|
647
|
+
return marker.language;
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const counts = /* @__PURE__ */ new Map();
|
|
653
|
+
try {
|
|
654
|
+
await scanExtensions(rootDir, counts, 2, 0);
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
if (counts.size > 0) {
|
|
658
|
+
let maxLang = "javascript";
|
|
659
|
+
let maxCount = 0;
|
|
660
|
+
for (const [lang, count] of counts) {
|
|
661
|
+
if (count > maxCount) {
|
|
662
|
+
maxCount = count;
|
|
663
|
+
maxLang = lang;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return maxLang;
|
|
667
|
+
}
|
|
668
|
+
return "javascript";
|
|
104
669
|
}
|
|
105
|
-
function
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
670
|
+
async function scanExtensions(dir, counts, maxDepth, currentDepth) {
|
|
671
|
+
if (currentDepth >= maxDepth) return;
|
|
672
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
673
|
+
for (const entry of entries) {
|
|
674
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
675
|
+
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
676
|
+
await scanExtensions(
|
|
677
|
+
join2(dir, entry.name),
|
|
678
|
+
counts,
|
|
679
|
+
maxDepth,
|
|
680
|
+
currentDepth + 1
|
|
681
|
+
);
|
|
682
|
+
} else if (entry.isFile()) {
|
|
683
|
+
const dotIdx = entry.name.lastIndexOf(".");
|
|
684
|
+
if (dotIdx > 0) {
|
|
685
|
+
const ext = entry.name.slice(dotIdx);
|
|
686
|
+
const lang = EXT_MAP[ext];
|
|
687
|
+
if (lang) {
|
|
688
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
110
693
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
694
|
+
|
|
695
|
+
// src/analyzer/engines/languages.ts
|
|
696
|
+
import { readFileSync } from "fs";
|
|
697
|
+
import { join as join3, dirname, resolve as resolve3 } from "path";
|
|
698
|
+
var python = {
|
|
699
|
+
id: "python",
|
|
700
|
+
extensions: [".py"],
|
|
701
|
+
commentStyle: "python",
|
|
702
|
+
importPatterns: [
|
|
703
|
+
// from package.module import something
|
|
704
|
+
{ regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm },
|
|
705
|
+
// import package.module
|
|
706
|
+
{ regex: /^import\s+([\w.]+)/gm }
|
|
707
|
+
],
|
|
708
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
709
|
+
if (importPath.startsWith(".")) {
|
|
710
|
+
const dots = importPath.match(/^\.+/)?.[0].length ?? 1;
|
|
711
|
+
let base = dirname(sourceFile);
|
|
712
|
+
for (let i = 1; i < dots; i++) base = dirname(base);
|
|
713
|
+
const rest = importPath.slice(dots).replace(/\./g, "/");
|
|
714
|
+
return tryPythonResolve(join3(base, rest), projectFiles);
|
|
715
|
+
}
|
|
716
|
+
const parts = importPath.replace(/\./g, "/");
|
|
717
|
+
return tryPythonResolve(join3(rootDir, parts), projectFiles);
|
|
718
|
+
},
|
|
719
|
+
defaultExclude: ["__pycache__", "\\.venv", "venv", "\\.egg-info", "dist", "build"]
|
|
720
|
+
};
|
|
721
|
+
function tryPythonResolve(base, projectFiles) {
|
|
722
|
+
if (projectFiles.has(base + ".py")) return base + ".py";
|
|
723
|
+
if (projectFiles.has(join3(base, "__init__.py"))) return join3(base, "__init__.py");
|
|
724
|
+
return null;
|
|
115
725
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
726
|
+
var rust = {
|
|
727
|
+
id: "rust",
|
|
728
|
+
extensions: [".rs"],
|
|
729
|
+
commentStyle: "c-style",
|
|
730
|
+
importPatterns: [],
|
|
731
|
+
// handled by extractImports
|
|
732
|
+
extractImports(content) {
|
|
733
|
+
const imports = [];
|
|
734
|
+
const modRegex = /\bmod\s+(\w+)\s*;/gm;
|
|
735
|
+
let match;
|
|
736
|
+
while ((match = modRegex.exec(content)) !== null) {
|
|
737
|
+
imports.push(match[1]);
|
|
738
|
+
}
|
|
739
|
+
const useRegex = /\buse\s+crate::([\s\S]*?);/gm;
|
|
740
|
+
while ((match = useRegex.exec(content)) !== null) {
|
|
741
|
+
const body = match[1].trim();
|
|
742
|
+
extractRustUsePaths(body, "", imports);
|
|
743
|
+
}
|
|
744
|
+
return imports;
|
|
745
|
+
},
|
|
746
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
747
|
+
const srcDir = join3(rootDir, "src");
|
|
748
|
+
if (!importPath.includes("::")) {
|
|
749
|
+
const parentDir = dirname(sourceFile);
|
|
750
|
+
const asFile = join3(parentDir, importPath + ".rs");
|
|
751
|
+
if (projectFiles.has(asFile)) return asFile;
|
|
752
|
+
const asDir = join3(parentDir, importPath, "mod.rs");
|
|
753
|
+
if (projectFiles.has(asDir)) return asDir;
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const segments = importPath.split("::");
|
|
757
|
+
for (let i = segments.length; i > 0; i--) {
|
|
758
|
+
const path = segments.slice(0, i).join("/");
|
|
759
|
+
const asFile = join3(srcDir, path + ".rs");
|
|
760
|
+
if (projectFiles.has(asFile)) return asFile;
|
|
761
|
+
const asDir = join3(srcDir, path, "mod.rs");
|
|
762
|
+
if (projectFiles.has(asDir)) return asDir;
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
},
|
|
766
|
+
defaultExclude: ["target"]
|
|
767
|
+
};
|
|
768
|
+
function extractRustUsePaths(body, prefix, results) {
|
|
769
|
+
const trimmed = body.trim();
|
|
770
|
+
const braceStart = trimmed.indexOf("{");
|
|
771
|
+
if (braceStart === -1) {
|
|
772
|
+
const path = prefix ? `${prefix}::${trimmed}` : trimmed;
|
|
773
|
+
if (path && !path.includes("{")) {
|
|
774
|
+
results.push(path);
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
let pathPrefix = trimmed.slice(0, braceStart).trim();
|
|
779
|
+
if (pathPrefix.endsWith("::")) {
|
|
780
|
+
pathPrefix = pathPrefix.slice(0, -2);
|
|
781
|
+
}
|
|
782
|
+
const fullPrefix = prefix ? `${prefix}::${pathPrefix}` : pathPrefix;
|
|
783
|
+
let depth = 0;
|
|
784
|
+
let braceEnd = -1;
|
|
785
|
+
for (let i = braceStart; i < trimmed.length; i++) {
|
|
786
|
+
if (trimmed[i] === "{") depth++;
|
|
787
|
+
else if (trimmed[i] === "}") {
|
|
788
|
+
depth--;
|
|
789
|
+
if (depth === 0) {
|
|
790
|
+
braceEnd = i;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (braceEnd === -1) return;
|
|
796
|
+
const inner = trimmed.slice(braceStart + 1, braceEnd).trim();
|
|
797
|
+
const items = splitByTopLevelComma(inner);
|
|
798
|
+
for (const item of items) {
|
|
799
|
+
const cleaned = item.trim();
|
|
800
|
+
if (cleaned === "self") {
|
|
801
|
+
results.push(fullPrefix);
|
|
802
|
+
} else if (cleaned) {
|
|
803
|
+
extractRustUsePaths(cleaned, fullPrefix, results);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function splitByTopLevelComma(s) {
|
|
808
|
+
const parts = [];
|
|
809
|
+
let depth = 0;
|
|
810
|
+
let start = 0;
|
|
811
|
+
for (let i = 0; i < s.length; i++) {
|
|
812
|
+
if (s[i] === "{") depth++;
|
|
813
|
+
else if (s[i] === "}") depth--;
|
|
814
|
+
else if (s[i] === "," && depth === 0) {
|
|
815
|
+
parts.push(s.slice(start, i));
|
|
816
|
+
start = i + 1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
parts.push(s.slice(start));
|
|
820
|
+
return parts;
|
|
821
|
+
}
|
|
822
|
+
var go = {
|
|
823
|
+
id: "go",
|
|
824
|
+
extensions: [".go"],
|
|
825
|
+
commentStyle: "c-style",
|
|
826
|
+
importPatterns: [],
|
|
827
|
+
// handled by extractImports
|
|
828
|
+
extractImports(content) {
|
|
829
|
+
const imports = [];
|
|
830
|
+
const singleRegex = /\bimport\s+(?:\w+\s+)?"([^"]+)"/gm;
|
|
831
|
+
let match;
|
|
832
|
+
while ((match = singleRegex.exec(content)) !== null) {
|
|
833
|
+
imports.push(match[1]);
|
|
834
|
+
}
|
|
835
|
+
const blockRegex = /\bimport\s*\(([^)]*)\)/gms;
|
|
836
|
+
while ((match = blockRegex.exec(content)) !== null) {
|
|
837
|
+
const block = match[1];
|
|
838
|
+
const entryRegex = /(?:\w+\s+)?"([^"]+)"/g;
|
|
839
|
+
let entry;
|
|
840
|
+
while ((entry = entryRegex.exec(block)) !== null) {
|
|
841
|
+
imports.push(entry[1]);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return imports;
|
|
845
|
+
},
|
|
846
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
847
|
+
const modPrefix = goModulePrefix(rootDir);
|
|
848
|
+
if (!modPrefix || !importPath.startsWith(modPrefix)) return null;
|
|
849
|
+
const relPath = importPath.slice(modPrefix.length + 1);
|
|
850
|
+
const pkgDir = join3(rootDir, relPath);
|
|
851
|
+
for (const f of projectFiles) {
|
|
852
|
+
if (f.startsWith(pkgDir + "/") && f.endsWith(".go")) return f;
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
},
|
|
856
|
+
defaultExclude: ["vendor"]
|
|
857
|
+
};
|
|
858
|
+
var goModCache = /* @__PURE__ */ new Map();
|
|
859
|
+
function goModulePrefix(rootDir) {
|
|
860
|
+
if (goModCache.has(rootDir)) return goModCache.get(rootDir);
|
|
861
|
+
try {
|
|
862
|
+
const content = readFileSync(join3(rootDir, "go.mod"), "utf-8");
|
|
863
|
+
const match = content.match(/^module\s+(.+)$/m);
|
|
864
|
+
const prefix = match ? match[1].trim() : null;
|
|
865
|
+
goModCache.set(rootDir, prefix);
|
|
866
|
+
return prefix;
|
|
867
|
+
} catch {
|
|
868
|
+
goModCache.set(rootDir, null);
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
var java = {
|
|
873
|
+
id: "java",
|
|
874
|
+
extensions: [".java"],
|
|
875
|
+
commentStyle: "c-style",
|
|
876
|
+
importPatterns: [
|
|
877
|
+
// import com.example.ClassName;
|
|
878
|
+
{ regex: /^import\s+(?:static\s+)?([\w.]+);/gm }
|
|
879
|
+
],
|
|
880
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
881
|
+
const filePath = importPath.replace(/\./g, "/") + ".java";
|
|
882
|
+
for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
|
|
883
|
+
const full = join3(rootDir, srcRoot, filePath);
|
|
884
|
+
if (projectFiles.has(full)) return full;
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
},
|
|
888
|
+
defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
|
|
889
|
+
};
|
|
890
|
+
var cCpp = {
|
|
891
|
+
id: "c-cpp",
|
|
892
|
+
extensions: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"],
|
|
893
|
+
commentStyle: "c-style",
|
|
894
|
+
importPatterns: [
|
|
895
|
+
// #include "file.h" (skip <system> includes)
|
|
896
|
+
{ regex: /^#include\s+"([^"]+)"/gm }
|
|
897
|
+
],
|
|
898
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
899
|
+
const fromSource = resolve3(dirname(sourceFile), importPath);
|
|
900
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
901
|
+
const fromRoot = join3(rootDir, importPath);
|
|
902
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
903
|
+
for (const incDir of ["include", "src"]) {
|
|
904
|
+
const full = join3(rootDir, incDir, importPath);
|
|
905
|
+
if (projectFiles.has(full)) return full;
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
},
|
|
909
|
+
defaultExclude: ["build", "cmake-build", "\\.o$", "\\.obj$"]
|
|
910
|
+
};
|
|
911
|
+
var ruby = {
|
|
912
|
+
id: "ruby",
|
|
913
|
+
extensions: [".rb"],
|
|
914
|
+
commentStyle: "ruby",
|
|
915
|
+
importPatterns: [
|
|
916
|
+
// require_relative 'path'
|
|
917
|
+
{ regex: /\brequire_relative\s+['"]([^'"]+)['"]/gm },
|
|
918
|
+
// require 'path' (for project-internal requires)
|
|
919
|
+
{ regex: /\brequire\s+['"]([^'"]+)['"]/gm }
|
|
920
|
+
],
|
|
921
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
922
|
+
const withExt = importPath.endsWith(".rb") ? importPath : importPath + ".rb";
|
|
923
|
+
const fromSource = resolve3(dirname(sourceFile), withExt);
|
|
924
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
925
|
+
const fromRoot = join3(rootDir, withExt);
|
|
926
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
927
|
+
const fromLib = join3(rootDir, "lib", withExt);
|
|
928
|
+
if (projectFiles.has(fromLib)) return fromLib;
|
|
929
|
+
return null;
|
|
930
|
+
},
|
|
931
|
+
defaultExclude: ["vendor", "\\.bundle"]
|
|
932
|
+
};
|
|
933
|
+
var php = {
|
|
934
|
+
id: "php",
|
|
935
|
+
extensions: [".php"],
|
|
936
|
+
commentStyle: "php",
|
|
937
|
+
importPatterns: [
|
|
938
|
+
// require/include/require_once/include_once 'path'
|
|
939
|
+
{ regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
|
|
940
|
+
// use Namespace\Class (PSR-4 style)
|
|
941
|
+
{ regex: /^use\s+([\w\\]+)/gm }
|
|
942
|
+
],
|
|
943
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
944
|
+
if (importPath.includes("/") || importPath.endsWith(".php")) {
|
|
945
|
+
const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
|
|
946
|
+
const fromSource = resolve3(dirname(sourceFile), withExt);
|
|
947
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
948
|
+
const fromRoot2 = join3(rootDir, withExt);
|
|
949
|
+
if (projectFiles.has(fromRoot2)) return fromRoot2;
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const filePath = importPath.replace(/\\/g, "/") + ".php";
|
|
953
|
+
const fromRoot = join3(rootDir, filePath);
|
|
954
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
955
|
+
const fromSrc = join3(rootDir, "src", filePath);
|
|
956
|
+
if (projectFiles.has(fromSrc)) return fromSrc;
|
|
957
|
+
return null;
|
|
958
|
+
},
|
|
959
|
+
defaultExclude: ["vendor"]
|
|
960
|
+
};
|
|
961
|
+
var swift = {
|
|
962
|
+
id: "swift",
|
|
963
|
+
extensions: [".swift"],
|
|
964
|
+
commentStyle: "c-style",
|
|
965
|
+
importPatterns: [
|
|
966
|
+
// import ModuleName (for cross-module dependencies)
|
|
967
|
+
{ regex: /^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
|
|
968
|
+
],
|
|
969
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
970
|
+
const spmDir = join3(rootDir, "Sources", importPath);
|
|
971
|
+
for (const f of projectFiles) {
|
|
972
|
+
if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
},
|
|
976
|
+
defaultExclude: ["\\.build", "DerivedData"]
|
|
977
|
+
};
|
|
978
|
+
var kotlin = {
|
|
979
|
+
id: "kotlin",
|
|
980
|
+
extensions: [".kt", ".kts"],
|
|
981
|
+
commentStyle: "c-style",
|
|
982
|
+
importPatterns: [
|
|
983
|
+
// import com.example.ClassName
|
|
984
|
+
{ regex: /^import\s+([\w.]+)/gm }
|
|
985
|
+
],
|
|
986
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
987
|
+
const filePath = importPath.replace(/\./g, "/");
|
|
988
|
+
for (const ext of [".kt", ".kts"]) {
|
|
989
|
+
for (const srcRoot of [
|
|
990
|
+
"",
|
|
991
|
+
"src/main/kotlin/",
|
|
992
|
+
"src/main/java/",
|
|
993
|
+
"src/",
|
|
994
|
+
"app/src/main/kotlin/",
|
|
995
|
+
"app/src/main/java/"
|
|
996
|
+
]) {
|
|
997
|
+
const full = join3(rootDir, srcRoot, filePath + ext);
|
|
998
|
+
if (projectFiles.has(full)) return full;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
},
|
|
1003
|
+
defaultExclude: ["build", "\\.gradle", "\\.idea"]
|
|
1004
|
+
};
|
|
1005
|
+
var LANGUAGE_CONFIGS = {
|
|
1006
|
+
javascript: null,
|
|
1007
|
+
// handled by DependencyCruiserEngine
|
|
1008
|
+
python,
|
|
1009
|
+
rust,
|
|
1010
|
+
go,
|
|
1011
|
+
java,
|
|
1012
|
+
"c-cpp": cCpp,
|
|
1013
|
+
ruby,
|
|
1014
|
+
php,
|
|
1015
|
+
swift,
|
|
1016
|
+
kotlin
|
|
1017
|
+
};
|
|
1018
|
+
function getLanguageConfig(id) {
|
|
1019
|
+
return LANGUAGE_CONFIGS[id] ?? null;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/analyzer/analyze.ts
|
|
1023
|
+
async function analyzeProject(rootDir, options = {}) {
|
|
1024
|
+
const absRootDir = resolve4(rootDir);
|
|
1025
|
+
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1026
|
+
try {
|
|
1027
|
+
if (language === "javascript") {
|
|
1028
|
+
return await new DependencyCruiserEngine().analyze(absRootDir, options);
|
|
1029
|
+
}
|
|
1030
|
+
const config = getLanguageConfig(language);
|
|
1031
|
+
if (!config) {
|
|
1032
|
+
throw new AnalyzerError(`No analyzer config for language: ${language}`);
|
|
1033
|
+
}
|
|
1034
|
+
return await new RegexEngine(config).analyze(absRootDir, options);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (error instanceof AnalyzerError) throw error;
|
|
1037
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1038
|
+
throw new AnalyzerError(message, { cause: error });
|
|
1039
|
+
}
|
|
121
1040
|
}
|
|
122
1041
|
var AnalyzerError = class extends Error {
|
|
123
1042
|
constructor(message, options) {
|
|
@@ -436,8 +1355,8 @@ function formatAnalysisReport(graph, options = {}) {
|
|
|
436
1355
|
}
|
|
437
1356
|
|
|
438
1357
|
// src/storage/snapshot.ts
|
|
439
|
-
import { mkdir, writeFile, readFile, access } from "fs/promises";
|
|
440
|
-
import { join } from "path";
|
|
1358
|
+
import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
|
|
1359
|
+
import { join as join4 } from "path";
|
|
441
1360
|
import { z } from "zod";
|
|
442
1361
|
|
|
443
1362
|
// src/types/schema.ts
|
|
@@ -471,8 +1390,8 @@ var SnapshotSchema = z.object({
|
|
|
471
1390
|
graph: DependencyGraphSchema
|
|
472
1391
|
});
|
|
473
1392
|
async function saveSnapshot(projectRoot, graph) {
|
|
474
|
-
const dirPath =
|
|
475
|
-
const filePath =
|
|
1393
|
+
const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
|
|
1394
|
+
const filePath = join4(dirPath, SNAPSHOT_FILE);
|
|
476
1395
|
const snapshot = {
|
|
477
1396
|
version: SCHEMA_VERSION,
|
|
478
1397
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -484,10 +1403,10 @@ async function saveSnapshot(projectRoot, graph) {
|
|
|
484
1403
|
return snapshot;
|
|
485
1404
|
}
|
|
486
1405
|
async function loadSnapshot(projectRoot) {
|
|
487
|
-
const filePath =
|
|
1406
|
+
const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
|
|
488
1407
|
let raw;
|
|
489
1408
|
try {
|
|
490
|
-
raw = await
|
|
1409
|
+
raw = await readFile2(filePath, "utf-8");
|
|
491
1410
|
} catch (error) {
|
|
492
1411
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
493
1412
|
return null;
|
|
@@ -622,10 +1541,10 @@ function arraysEqual(a, b) {
|
|
|
622
1541
|
}
|
|
623
1542
|
|
|
624
1543
|
// src/utils/path-guard.ts
|
|
625
|
-
import { resolve as
|
|
1544
|
+
import { resolve as resolve5 } from "path";
|
|
626
1545
|
function validatePath(inputPath, boundary) {
|
|
627
|
-
const resolved =
|
|
628
|
-
const root = boundary ?
|
|
1546
|
+
const resolved = resolve5(inputPath);
|
|
1547
|
+
const root = boundary ? resolve5(boundary) : process.cwd();
|
|
629
1548
|
if (!resolved.startsWith(root)) {
|
|
630
1549
|
throw new PathTraversalError(
|
|
631
1550
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -643,20 +1562,21 @@ var PathTraversalError = class extends Error {
|
|
|
643
1562
|
// src/mcp/index.ts
|
|
644
1563
|
var server = new McpServer({
|
|
645
1564
|
name: "archtracker",
|
|
646
|
-
version: "0.
|
|
1565
|
+
version: "0.2.0"
|
|
647
1566
|
});
|
|
648
1567
|
server.tool(
|
|
649
1568
|
"generate_map",
|
|
650
|
-
"Analyze dependency graph of a directory and return file import/export structure as JSON",
|
|
1569
|
+
"Analyze dependency graph of a directory and return file import/export structure as JSON. Supports JS/TS, Python, Rust, Go, Java, C/C++, Ruby, PHP, Swift, Kotlin.",
|
|
651
1570
|
{
|
|
652
1571
|
targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
|
|
653
1572
|
exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
|
|
654
|
-
maxDepth: z2.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)")
|
|
1573
|
+
maxDepth: z2.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
|
|
1574
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
655
1575
|
},
|
|
656
|
-
async ({ targetDir, exclude, maxDepth }) => {
|
|
1576
|
+
async ({ targetDir, exclude, maxDepth, language }) => {
|
|
657
1577
|
try {
|
|
658
1578
|
validatePath(targetDir);
|
|
659
|
-
const graph = await analyzeProject(targetDir, { exclude, maxDepth });
|
|
1579
|
+
const graph = await analyzeProject(targetDir, { exclude, maxDepth, language });
|
|
660
1580
|
const summary = [
|
|
661
1581
|
t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
|
|
662
1582
|
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone")
|
|
@@ -674,18 +1594,19 @@ server.tool(
|
|
|
674
1594
|
);
|
|
675
1595
|
server.tool(
|
|
676
1596
|
"analyze_existing_architecture",
|
|
677
|
-
"Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown.",
|
|
1597
|
+
"Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports 10 languages.",
|
|
678
1598
|
{
|
|
679
1599
|
targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
|
|
680
1600
|
exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude"),
|
|
681
1601
|
topN: z2.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
|
|
682
1602
|
saveSnapshot: z2.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
|
|
683
|
-
projectRoot: z2.string().default(".").describe("Project root (needed only when saveSnapshot is true)")
|
|
1603
|
+
projectRoot: z2.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
|
|
1604
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
684
1605
|
},
|
|
685
|
-
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot }) => {
|
|
1606
|
+
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
686
1607
|
try {
|
|
687
1608
|
validatePath(targetDir);
|
|
688
|
-
const graph = await analyzeProject(targetDir, { exclude });
|
|
1609
|
+
const graph = await analyzeProject(targetDir, { exclude, language });
|
|
689
1610
|
const report = formatAnalysisReport(graph, { topN: topN ?? 10 });
|
|
690
1611
|
const content = [
|
|
691
1612
|
{ type: "text", text: report }
|
|
@@ -706,13 +1627,14 @@ server.tool(
|
|
|
706
1627
|
"Save the current dependency graph as a snapshot to .archtracker/snapshot.json",
|
|
707
1628
|
{
|
|
708
1629
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
709
|
-
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)")
|
|
1630
|
+
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
1631
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
710
1632
|
},
|
|
711
|
-
async ({ targetDir, projectRoot }) => {
|
|
1633
|
+
async ({ targetDir, projectRoot, language }) => {
|
|
712
1634
|
try {
|
|
713
1635
|
validatePath(targetDir);
|
|
714
1636
|
validatePath(projectRoot);
|
|
715
|
-
const graph = await analyzeProject(targetDir);
|
|
1637
|
+
const graph = await analyzeProject(targetDir, { language });
|
|
716
1638
|
const snapshot = await saveSnapshot(projectRoot, graph);
|
|
717
1639
|
const keyComponents = Object.values(graph.files).sort((a, b) => b.dependents.length - a.dependents.length).slice(0, 5).map((f) => ` ${t("cli.dependedBy", { path: f.path, count: f.dependents.length })}`);
|
|
718
1640
|
const report = [
|
|
@@ -735,15 +1657,16 @@ server.tool(
|
|
|
735
1657
|
"Compare saved snapshot with current code dependencies and warn about files that may need updates",
|
|
736
1658
|
{
|
|
737
1659
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
738
|
-
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)")
|
|
1660
|
+
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
1661
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
739
1662
|
},
|
|
740
|
-
async ({ targetDir, projectRoot }) => {
|
|
1663
|
+
async ({ targetDir, projectRoot, language }) => {
|
|
741
1664
|
try {
|
|
742
1665
|
validatePath(targetDir);
|
|
743
1666
|
validatePath(projectRoot);
|
|
744
1667
|
const existingSnapshot = await loadSnapshot(projectRoot);
|
|
745
1668
|
if (!existingSnapshot) {
|
|
746
|
-
const graph = await analyzeProject(targetDir);
|
|
1669
|
+
const graph = await analyzeProject(targetDir, { language });
|
|
747
1670
|
await saveSnapshot(projectRoot, graph);
|
|
748
1671
|
return {
|
|
749
1672
|
content: [
|
|
@@ -758,7 +1681,7 @@ server.tool(
|
|
|
758
1681
|
]
|
|
759
1682
|
};
|
|
760
1683
|
}
|
|
761
|
-
const currentGraph = await analyzeProject(targetDir);
|
|
1684
|
+
const currentGraph = await analyzeProject(targetDir, { language });
|
|
762
1685
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
763
1686
|
const report = formatDiffReport(diff);
|
|
764
1687
|
return { content: [{ type: "text", text: report }] };
|
|
@@ -772,13 +1695,14 @@ server.tool(
|
|
|
772
1695
|
"Get current valid file paths and architecture summary for AI session initialization",
|
|
773
1696
|
{
|
|
774
1697
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
775
|
-
projectRoot: z2.string().default(".").describe("Project root")
|
|
1698
|
+
projectRoot: z2.string().default(".").describe("Project root"),
|
|
1699
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
776
1700
|
},
|
|
777
|
-
async ({ targetDir, projectRoot }) => {
|
|
1701
|
+
async ({ targetDir, projectRoot, language }) => {
|
|
778
1702
|
try {
|
|
779
1703
|
let snapshot = await loadSnapshot(projectRoot);
|
|
780
1704
|
if (!snapshot) {
|
|
781
|
-
const graph2 = await analyzeProject(targetDir);
|
|
1705
|
+
const graph2 = await analyzeProject(targetDir, { language });
|
|
782
1706
|
snapshot = await saveSnapshot(projectRoot, graph2);
|
|
783
1707
|
}
|
|
784
1708
|
const graph = snapshot.graph;
|
|
@@ -831,15 +1755,16 @@ server.tool(
|
|
|
831
1755
|
),
|
|
832
1756
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
833
1757
|
projectRoot: z2.string().default(".").describe("Project root"),
|
|
834
|
-
limit: z2.number().int().min(1).max(50).optional().describe("Max results (default: 10)")
|
|
1758
|
+
limit: z2.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
|
|
1759
|
+
language: z2.enum(["javascript", "python", "rust", "go", "java", "c-cpp", "ruby", "php", "swift", "kotlin"]).optional().describe("Target language (auto-detected if omitted)")
|
|
835
1760
|
},
|
|
836
|
-
async ({ query, mode, targetDir, projectRoot, limit }) => {
|
|
1761
|
+
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
837
1762
|
try {
|
|
838
1763
|
validatePath(targetDir);
|
|
839
1764
|
validatePath(projectRoot);
|
|
840
1765
|
let snapshot = await loadSnapshot(projectRoot);
|
|
841
1766
|
if (!snapshot) {
|
|
842
|
-
const graph2 = await analyzeProject(targetDir);
|
|
1767
|
+
const graph2 = await analyzeProject(targetDir, { language });
|
|
843
1768
|
snapshot = await saveSnapshot(projectRoot, graph2);
|
|
844
1769
|
}
|
|
845
1770
|
const graph = snapshot.graph;
|