depwire-cli 0.1.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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/chunk-2XOJSBSD.js +3302 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +243 -0
- package/dist/mcpb-entry.d.ts +1 -0
- package/dist/mcpb-entry.js +70 -0
- package/dist/viz/public/arc.js +579 -0
- package/dist/viz/public/index.html +48 -0
- package/dist/viz/public/style.css +326 -0
- package/icon.png +0 -0
- package/package.json +77 -0
|
@@ -0,0 +1,3302 @@
|
|
|
1
|
+
// src/parser/index.ts
|
|
2
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
3
|
+
import { join as join6 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/files.ts
|
|
6
|
+
import { readdirSync, statSync, existsSync, lstatSync } from "fs";
|
|
7
|
+
import { join, relative } from "path";
|
|
8
|
+
function scanDirectory(rootDir, baseDir = rootDir) {
|
|
9
|
+
const files = [];
|
|
10
|
+
try {
|
|
11
|
+
const entries = readdirSync(baseDir);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const fullPath = join(baseDir, entry);
|
|
14
|
+
if (entry.startsWith(".")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (entry === "node_modules" || entry === "vendor" || entry === "dist" || entry === "build") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const stats2 = lstatSync(fullPath);
|
|
22
|
+
if (stats2.isSymbolicLink()) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const stats = statSync(fullPath);
|
|
29
|
+
if (stats.isDirectory()) {
|
|
30
|
+
files.push(...scanDirectory(rootDir, fullPath));
|
|
31
|
+
} else if (stats.isFile()) {
|
|
32
|
+
const isTypeScript = (entry.endsWith(".ts") || entry.endsWith(".tsx")) && !entry.endsWith(".d.ts");
|
|
33
|
+
const isJavaScript = entry.endsWith(".js") || entry.endsWith(".jsx") || entry.endsWith(".mjs") || entry.endsWith(".cjs");
|
|
34
|
+
const isPython = entry.endsWith(".py");
|
|
35
|
+
const isGo = entry.endsWith(".go") && !entry.endsWith("_test.go");
|
|
36
|
+
if (isTypeScript || isJavaScript || isPython || isGo) {
|
|
37
|
+
files.push(relative(rootDir, fullPath));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`Error scanning directory ${baseDir}:`, err);
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
function fileExists(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
return existsSync(filePath) && statSync(filePath).isFile();
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/parser/detect.ts
|
|
55
|
+
import { extname as extname3 } from "path";
|
|
56
|
+
|
|
57
|
+
// src/parser/typescript.ts
|
|
58
|
+
import Parser from "tree-sitter";
|
|
59
|
+
import TypeScript from "tree-sitter-typescript";
|
|
60
|
+
|
|
61
|
+
// src/parser/resolver.ts
|
|
62
|
+
import { join as join2, dirname, resolve, relative as relative2 } from "path";
|
|
63
|
+
import { readFileSync } from "fs";
|
|
64
|
+
var tsconfigCache = /* @__PURE__ */ new Map();
|
|
65
|
+
function loadTsConfig(projectRoot) {
|
|
66
|
+
if (tsconfigCache.has(projectRoot)) {
|
|
67
|
+
return tsconfigCache.get(projectRoot);
|
|
68
|
+
}
|
|
69
|
+
let config = {};
|
|
70
|
+
let currentDir = projectRoot;
|
|
71
|
+
while (currentDir !== dirname(currentDir)) {
|
|
72
|
+
const tsconfigPath = join2(currentDir, "tsconfig.json");
|
|
73
|
+
try {
|
|
74
|
+
const raw = readFileSync(tsconfigPath, "utf-8");
|
|
75
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([\]}])/g, "$1");
|
|
76
|
+
const parsed = JSON.parse(stripped);
|
|
77
|
+
if (parsed.compilerOptions) {
|
|
78
|
+
config.baseUrl = parsed.compilerOptions.baseUrl;
|
|
79
|
+
config.paths = parsed.compilerOptions.paths;
|
|
80
|
+
if (config.baseUrl) {
|
|
81
|
+
config.baseUrl = resolve(currentDir, config.baseUrl);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
currentDir = dirname(currentDir);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
tsconfigCache.set(projectRoot, config);
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
function expandPathAlias(importPath, tsconfig) {
|
|
93
|
+
if (!tsconfig.paths) return null;
|
|
94
|
+
for (const [pattern, mappings] of Object.entries(tsconfig.paths)) {
|
|
95
|
+
const patternRegex = new RegExp(
|
|
96
|
+
"^" + pattern.replace(/\*/g, "(.*)") + "$"
|
|
97
|
+
);
|
|
98
|
+
const match = importPath.match(patternRegex);
|
|
99
|
+
if (match) {
|
|
100
|
+
const captured = match[1] || "";
|
|
101
|
+
for (const mapping of mappings) {
|
|
102
|
+
const expanded = mapping.replace(/\*/g, captured);
|
|
103
|
+
const baseUrl = tsconfig.baseUrl || ".";
|
|
104
|
+
return join2(baseUrl, expanded);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function tryResolve(basePath, projectRoot) {
|
|
111
|
+
const candidates = [];
|
|
112
|
+
if (basePath.endsWith(".js")) {
|
|
113
|
+
candidates.push(basePath.replace(/\.js$/, ".ts"));
|
|
114
|
+
candidates.push(basePath.replace(/\.js$/, ".tsx"));
|
|
115
|
+
candidates.push(basePath);
|
|
116
|
+
} else if (basePath.endsWith(".jsx")) {
|
|
117
|
+
candidates.push(basePath.replace(/\.jsx$/, ".tsx"));
|
|
118
|
+
candidates.push(basePath);
|
|
119
|
+
} else if (basePath.endsWith(".ts") || basePath.endsWith(".tsx")) {
|
|
120
|
+
candidates.push(basePath);
|
|
121
|
+
} else {
|
|
122
|
+
candidates.push(basePath + ".ts");
|
|
123
|
+
candidates.push(basePath + ".tsx");
|
|
124
|
+
candidates.push(join2(basePath, "index.ts"));
|
|
125
|
+
candidates.push(join2(basePath, "index.tsx"));
|
|
126
|
+
candidates.push(basePath);
|
|
127
|
+
}
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
if (fileExists(candidate)) {
|
|
130
|
+
return relative2(projectRoot, candidate);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function resolveImportPath(importPath, fromFile, projectRoot) {
|
|
136
|
+
const tsconfig = loadTsConfig(projectRoot);
|
|
137
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
138
|
+
const expanded = expandPathAlias(importPath, tsconfig);
|
|
139
|
+
if (expanded) {
|
|
140
|
+
return tryResolve(expanded, projectRoot);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const fromDir = dirname(join2(projectRoot, fromFile));
|
|
145
|
+
let resolvedPath;
|
|
146
|
+
if (importPath.startsWith(".")) {
|
|
147
|
+
resolvedPath = resolve(fromDir, importPath);
|
|
148
|
+
} else {
|
|
149
|
+
resolvedPath = resolve(projectRoot, importPath.substring(1));
|
|
150
|
+
}
|
|
151
|
+
return tryResolve(resolvedPath, projectRoot);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/parser/typescript.ts
|
|
155
|
+
var tsParser = new Parser();
|
|
156
|
+
tsParser.setLanguage(TypeScript.typescript);
|
|
157
|
+
var tsxParser = new Parser();
|
|
158
|
+
tsxParser.setLanguage(TypeScript.tsx);
|
|
159
|
+
function parseTypeScriptFile(filePath, sourceCode, projectRoot) {
|
|
160
|
+
const parser2 = filePath.endsWith(".tsx") ? tsxParser : tsParser;
|
|
161
|
+
const tree = parser2.parse(sourceCode);
|
|
162
|
+
const context = {
|
|
163
|
+
filePath,
|
|
164
|
+
projectRoot,
|
|
165
|
+
sourceCode,
|
|
166
|
+
symbols: [],
|
|
167
|
+
edges: [],
|
|
168
|
+
currentScope: [],
|
|
169
|
+
imports: /* @__PURE__ */ new Map()
|
|
170
|
+
};
|
|
171
|
+
walkNode(tree.rootNode, context);
|
|
172
|
+
return {
|
|
173
|
+
filePath,
|
|
174
|
+
symbols: context.symbols,
|
|
175
|
+
edges: context.edges
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function walkNode(node, context) {
|
|
179
|
+
processNode(node, context);
|
|
180
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
181
|
+
const child = node.child(i);
|
|
182
|
+
if (child) {
|
|
183
|
+
walkNode(child, context);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function processNode(node, context) {
|
|
188
|
+
const type = node.type;
|
|
189
|
+
switch (type) {
|
|
190
|
+
case "function_declaration":
|
|
191
|
+
processFunctionDeclaration(node, context);
|
|
192
|
+
break;
|
|
193
|
+
case "class_declaration":
|
|
194
|
+
processClassDeclaration(node, context);
|
|
195
|
+
break;
|
|
196
|
+
case "variable_declaration":
|
|
197
|
+
case "lexical_declaration":
|
|
198
|
+
processVariableDeclaration(node, context);
|
|
199
|
+
break;
|
|
200
|
+
case "type_alias_declaration":
|
|
201
|
+
processTypeAliasDeclaration(node, context);
|
|
202
|
+
break;
|
|
203
|
+
case "interface_declaration":
|
|
204
|
+
processInterfaceDeclaration(node, context);
|
|
205
|
+
break;
|
|
206
|
+
case "enum_declaration":
|
|
207
|
+
processEnumDeclaration(node, context);
|
|
208
|
+
break;
|
|
209
|
+
case "import_statement":
|
|
210
|
+
processImportStatement(node, context);
|
|
211
|
+
break;
|
|
212
|
+
case "export_statement":
|
|
213
|
+
processExportStatement(node, context);
|
|
214
|
+
break;
|
|
215
|
+
case "call_expression":
|
|
216
|
+
processCallExpression(node, context);
|
|
217
|
+
break;
|
|
218
|
+
case "new_expression":
|
|
219
|
+
processNewExpression(node, context);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function processFunctionDeclaration(node, context) {
|
|
224
|
+
const nameNode = node.childForFieldName("name");
|
|
225
|
+
if (!nameNode) return;
|
|
226
|
+
const name = nameNode.text;
|
|
227
|
+
const exported = isExported(node);
|
|
228
|
+
const startLine = node.startPosition.row + 1;
|
|
229
|
+
const endLine = node.endPosition.row + 1;
|
|
230
|
+
const scope = context.currentScope.length > 0 ? context.currentScope.join(".") : void 0;
|
|
231
|
+
const symbolId = `${context.filePath}::${scope ? scope + "." : ""}${name}`;
|
|
232
|
+
context.symbols.push({
|
|
233
|
+
id: symbolId,
|
|
234
|
+
name,
|
|
235
|
+
kind: "function",
|
|
236
|
+
filePath: context.filePath,
|
|
237
|
+
startLine,
|
|
238
|
+
endLine,
|
|
239
|
+
exported,
|
|
240
|
+
scope
|
|
241
|
+
});
|
|
242
|
+
context.currentScope.push(name);
|
|
243
|
+
const body = node.childForFieldName("body");
|
|
244
|
+
if (body) {
|
|
245
|
+
walkNode(body, context);
|
|
246
|
+
}
|
|
247
|
+
context.currentScope.pop();
|
|
248
|
+
}
|
|
249
|
+
function processClassDeclaration(node, context) {
|
|
250
|
+
const nameNode = node.childForFieldName("name");
|
|
251
|
+
if (!nameNode) return;
|
|
252
|
+
const name = nameNode.text;
|
|
253
|
+
const exported = isExported(node);
|
|
254
|
+
const startLine = node.startPosition.row + 1;
|
|
255
|
+
const endLine = node.endPosition.row + 1;
|
|
256
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
257
|
+
context.symbols.push({
|
|
258
|
+
id: symbolId,
|
|
259
|
+
name,
|
|
260
|
+
kind: "class",
|
|
261
|
+
filePath: context.filePath,
|
|
262
|
+
startLine,
|
|
263
|
+
endLine,
|
|
264
|
+
exported
|
|
265
|
+
});
|
|
266
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
267
|
+
const child = node.child(i);
|
|
268
|
+
if (child && child.type === "class_heritage") {
|
|
269
|
+
const extendsClause = child.childForFieldName("extends");
|
|
270
|
+
if (extendsClause) {
|
|
271
|
+
for (let j = 0; j < extendsClause.childCount; j++) {
|
|
272
|
+
const typeNode = extendsClause.child(j);
|
|
273
|
+
if (typeNode && typeNode.type === "identifier") {
|
|
274
|
+
const targetName = typeNode.text;
|
|
275
|
+
const targetId = `${context.filePath}::${targetName}`;
|
|
276
|
+
context.edges.push({
|
|
277
|
+
source: symbolId,
|
|
278
|
+
target: targetId,
|
|
279
|
+
kind: "extends",
|
|
280
|
+
filePath: context.filePath,
|
|
281
|
+
line: typeNode.startPosition.row + 1
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const implementsClause = child.childForFieldName("implements");
|
|
287
|
+
if (implementsClause) {
|
|
288
|
+
for (let j = 0; j < implementsClause.childCount; j++) {
|
|
289
|
+
const typeNode = implementsClause.child(j);
|
|
290
|
+
if (typeNode && typeNode.type === "type_identifier") {
|
|
291
|
+
const targetName = typeNode.text;
|
|
292
|
+
const targetId = `${context.filePath}::${targetName}`;
|
|
293
|
+
context.edges.push({
|
|
294
|
+
source: symbolId,
|
|
295
|
+
target: targetId,
|
|
296
|
+
kind: "implements",
|
|
297
|
+
filePath: context.filePath,
|
|
298
|
+
line: typeNode.startPosition.row + 1
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
context.currentScope.push(name);
|
|
306
|
+
const body = node.childForFieldName("body");
|
|
307
|
+
if (body) {
|
|
308
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
309
|
+
const child = body.child(i);
|
|
310
|
+
if (child) {
|
|
311
|
+
if (child.type === "method_definition") {
|
|
312
|
+
processMethodDefinition(child, context);
|
|
313
|
+
} else if (child.type === "public_field_definition" || child.type === "field_definition") {
|
|
314
|
+
processPropertyDefinition(child, context);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
context.currentScope.pop();
|
|
320
|
+
}
|
|
321
|
+
function processMethodDefinition(node, context) {
|
|
322
|
+
const nameNode = node.childForFieldName("name");
|
|
323
|
+
if (!nameNode) return;
|
|
324
|
+
const name = nameNode.text;
|
|
325
|
+
const className = context.currentScope[context.currentScope.length - 1];
|
|
326
|
+
const startLine = node.startPosition.row + 1;
|
|
327
|
+
const endLine = node.endPosition.row + 1;
|
|
328
|
+
const symbolId = `${context.filePath}::${className}.${name}`;
|
|
329
|
+
context.symbols.push({
|
|
330
|
+
id: symbolId,
|
|
331
|
+
name,
|
|
332
|
+
kind: "method",
|
|
333
|
+
filePath: context.filePath,
|
|
334
|
+
startLine,
|
|
335
|
+
endLine,
|
|
336
|
+
exported: false,
|
|
337
|
+
scope: className
|
|
338
|
+
});
|
|
339
|
+
context.currentScope.push(name);
|
|
340
|
+
const body = node.childForFieldName("body");
|
|
341
|
+
if (body) {
|
|
342
|
+
walkNode(body, context);
|
|
343
|
+
}
|
|
344
|
+
context.currentScope.pop();
|
|
345
|
+
}
|
|
346
|
+
function processPropertyDefinition(node, context) {
|
|
347
|
+
const nameNode = node.childForFieldName("name");
|
|
348
|
+
if (!nameNode) return;
|
|
349
|
+
const name = nameNode.text;
|
|
350
|
+
const className = context.currentScope[context.currentScope.length - 1];
|
|
351
|
+
const startLine = node.startPosition.row + 1;
|
|
352
|
+
const endLine = node.endPosition.row + 1;
|
|
353
|
+
const symbolId = `${context.filePath}::${className}.${name}`;
|
|
354
|
+
context.symbols.push({
|
|
355
|
+
id: symbolId,
|
|
356
|
+
name,
|
|
357
|
+
kind: "property",
|
|
358
|
+
filePath: context.filePath,
|
|
359
|
+
startLine,
|
|
360
|
+
endLine,
|
|
361
|
+
exported: false,
|
|
362
|
+
scope: className
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function processVariableDeclaration(node, context) {
|
|
366
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
367
|
+
const child = node.child(i);
|
|
368
|
+
if (child && child.type === "variable_declarator") {
|
|
369
|
+
const nameNode = child.childForFieldName("name");
|
|
370
|
+
if (!nameNode) continue;
|
|
371
|
+
const name = nameNode.text;
|
|
372
|
+
const exported = isExported(node.parent);
|
|
373
|
+
const startLine = child.startPosition.row + 1;
|
|
374
|
+
const endLine = child.endPosition.row + 1;
|
|
375
|
+
const scope = context.currentScope.length > 0 ? context.currentScope.join(".") : void 0;
|
|
376
|
+
const value = child.childForFieldName("value");
|
|
377
|
+
const kind = value && value.type === "arrow_function" ? "function" : "variable";
|
|
378
|
+
const symbolId = `${context.filePath}::${scope ? scope + "." : ""}${name}`;
|
|
379
|
+
context.symbols.push({
|
|
380
|
+
id: symbolId,
|
|
381
|
+
name,
|
|
382
|
+
kind,
|
|
383
|
+
filePath: context.filePath,
|
|
384
|
+
startLine,
|
|
385
|
+
endLine,
|
|
386
|
+
exported,
|
|
387
|
+
scope
|
|
388
|
+
});
|
|
389
|
+
if (kind === "function" && value) {
|
|
390
|
+
context.currentScope.push(name);
|
|
391
|
+
walkNode(value, context);
|
|
392
|
+
context.currentScope.pop();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function processTypeAliasDeclaration(node, context) {
|
|
398
|
+
const nameNode = node.childForFieldName("name");
|
|
399
|
+
if (!nameNode) return;
|
|
400
|
+
const name = nameNode.text;
|
|
401
|
+
const exported = isExported(node);
|
|
402
|
+
const startLine = node.startPosition.row + 1;
|
|
403
|
+
const endLine = node.endPosition.row + 1;
|
|
404
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
405
|
+
context.symbols.push({
|
|
406
|
+
id: symbolId,
|
|
407
|
+
name,
|
|
408
|
+
kind: "type_alias",
|
|
409
|
+
filePath: context.filePath,
|
|
410
|
+
startLine,
|
|
411
|
+
endLine,
|
|
412
|
+
exported
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function processInterfaceDeclaration(node, context) {
|
|
416
|
+
const nameNode = node.childForFieldName("name");
|
|
417
|
+
if (!nameNode) return;
|
|
418
|
+
const name = nameNode.text;
|
|
419
|
+
const exported = isExported(node);
|
|
420
|
+
const startLine = node.startPosition.row + 1;
|
|
421
|
+
const endLine = node.endPosition.row + 1;
|
|
422
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
423
|
+
context.symbols.push({
|
|
424
|
+
id: symbolId,
|
|
425
|
+
name,
|
|
426
|
+
kind: "interface",
|
|
427
|
+
filePath: context.filePath,
|
|
428
|
+
startLine,
|
|
429
|
+
endLine,
|
|
430
|
+
exported
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function processEnumDeclaration(node, context) {
|
|
434
|
+
const nameNode = node.childForFieldName("name");
|
|
435
|
+
if (!nameNode) return;
|
|
436
|
+
const name = nameNode.text;
|
|
437
|
+
const exported = isExported(node);
|
|
438
|
+
const startLine = node.startPosition.row + 1;
|
|
439
|
+
const endLine = node.endPosition.row + 1;
|
|
440
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
441
|
+
context.symbols.push({
|
|
442
|
+
id: symbolId,
|
|
443
|
+
name,
|
|
444
|
+
kind: "enum",
|
|
445
|
+
filePath: context.filePath,
|
|
446
|
+
startLine,
|
|
447
|
+
endLine,
|
|
448
|
+
exported
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function processImportStatement(node, context) {
|
|
452
|
+
const source = node.childForFieldName("source");
|
|
453
|
+
if (!source) return;
|
|
454
|
+
const importPath = source.text.slice(1, -1);
|
|
455
|
+
const resolvedPath = resolveImportPath(importPath, context.filePath, context.projectRoot);
|
|
456
|
+
const importClause = node.child(1);
|
|
457
|
+
if (!importClause) return;
|
|
458
|
+
const importedNames = [];
|
|
459
|
+
const namedImports = findChildByType(importClause, "named_imports");
|
|
460
|
+
if (namedImports) {
|
|
461
|
+
for (let i = 0; i < namedImports.childCount; i++) {
|
|
462
|
+
const child = namedImports.child(i);
|
|
463
|
+
if (child && child.type === "import_specifier") {
|
|
464
|
+
const identifier2 = findChildByType(child, "identifier");
|
|
465
|
+
if (identifier2) {
|
|
466
|
+
importedNames.push(identifier2.text);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const identifier = findChildByType(importClause, "identifier");
|
|
472
|
+
if (identifier) {
|
|
473
|
+
importedNames.push(identifier.text);
|
|
474
|
+
}
|
|
475
|
+
const namespaceImport = findChildByType(importClause, "namespace_import");
|
|
476
|
+
if (namespaceImport) {
|
|
477
|
+
const alias = findChildByType(namespaceImport, "identifier");
|
|
478
|
+
if (alias) {
|
|
479
|
+
importedNames.push(alias.text);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (resolvedPath) {
|
|
483
|
+
const currentSymbolId = getCurrentSymbolId(context);
|
|
484
|
+
for (const importedName of importedNames) {
|
|
485
|
+
const targetId = `${resolvedPath}::${importedName}`;
|
|
486
|
+
context.imports.set(importedName, targetId);
|
|
487
|
+
context.edges.push({
|
|
488
|
+
source: currentSymbolId || `${context.filePath}::__file__`,
|
|
489
|
+
target: targetId,
|
|
490
|
+
kind: "imports",
|
|
491
|
+
filePath: context.filePath,
|
|
492
|
+
line: node.startPosition.row + 1
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function processExportStatement(node, context) {
|
|
498
|
+
const source = node.childForFieldName("source");
|
|
499
|
+
if (source) {
|
|
500
|
+
const importPath = source.text.slice(1, -1);
|
|
501
|
+
const resolvedPath = resolveImportPath(importPath, context.filePath, context.projectRoot);
|
|
502
|
+
const exportClause = node.child(1);
|
|
503
|
+
if (exportClause && resolvedPath) {
|
|
504
|
+
const exportedNames = [];
|
|
505
|
+
for (let i = 0; i < exportClause.childCount; i++) {
|
|
506
|
+
const child = exportClause.child(i);
|
|
507
|
+
if (child && child.type === "export_specifier") {
|
|
508
|
+
const identifier = findChildByType(child, "identifier");
|
|
509
|
+
if (identifier) {
|
|
510
|
+
exportedNames.push(identifier.text);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const currentSymbolId = getCurrentSymbolId(context);
|
|
515
|
+
const startLine = node.startPosition.row + 1;
|
|
516
|
+
const endLine = node.endPosition.row + 1;
|
|
517
|
+
for (const exportedName of exportedNames) {
|
|
518
|
+
const symbolId = `${context.filePath}::${exportedName}`;
|
|
519
|
+
context.symbols.push({
|
|
520
|
+
id: symbolId,
|
|
521
|
+
name: exportedName,
|
|
522
|
+
kind: "export",
|
|
523
|
+
filePath: context.filePath,
|
|
524
|
+
startLine,
|
|
525
|
+
endLine,
|
|
526
|
+
exported: true
|
|
527
|
+
});
|
|
528
|
+
const targetId = `${resolvedPath}::${exportedName}`;
|
|
529
|
+
context.edges.push({
|
|
530
|
+
source: symbolId,
|
|
531
|
+
target: targetId,
|
|
532
|
+
kind: "imports",
|
|
533
|
+
filePath: context.filePath,
|
|
534
|
+
line: startLine
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function processCallExpression(node, context) {
|
|
541
|
+
const functionNode = node.childForFieldName("function");
|
|
542
|
+
if (!functionNode) return;
|
|
543
|
+
let functionName = null;
|
|
544
|
+
if (functionNode.type === "identifier") {
|
|
545
|
+
functionName = functionNode.text;
|
|
546
|
+
} else if (functionNode.type === "member_expression") {
|
|
547
|
+
const property = functionNode.childForFieldName("property");
|
|
548
|
+
if (property) {
|
|
549
|
+
functionName = property.text;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (functionName) {
|
|
553
|
+
const currentSymbolId = getCurrentSymbolId(context);
|
|
554
|
+
if (currentSymbolId) {
|
|
555
|
+
let targetId;
|
|
556
|
+
if (context.imports.has(functionName)) {
|
|
557
|
+
targetId = context.imports.get(functionName);
|
|
558
|
+
} else {
|
|
559
|
+
targetId = `${context.filePath}::${functionName}`;
|
|
560
|
+
}
|
|
561
|
+
context.edges.push({
|
|
562
|
+
source: currentSymbolId,
|
|
563
|
+
target: targetId,
|
|
564
|
+
kind: "calls",
|
|
565
|
+
filePath: context.filePath,
|
|
566
|
+
line: node.startPosition.row + 1
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function processNewExpression(node, context) {
|
|
572
|
+
const classNode = node.child(1);
|
|
573
|
+
if (!classNode || classNode.type !== "identifier") return;
|
|
574
|
+
const className = classNode.text;
|
|
575
|
+
const currentSymbolId = getCurrentSymbolId(context);
|
|
576
|
+
if (currentSymbolId) {
|
|
577
|
+
const targetId = `${context.filePath}::${className}`;
|
|
578
|
+
context.edges.push({
|
|
579
|
+
source: currentSymbolId,
|
|
580
|
+
target: targetId,
|
|
581
|
+
kind: "calls",
|
|
582
|
+
filePath: context.filePath,
|
|
583
|
+
line: node.startPosition.row + 1
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function isExported(node) {
|
|
588
|
+
if (!node) return false;
|
|
589
|
+
if (node.type === "export_statement") return true;
|
|
590
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
591
|
+
const child = node.child(i);
|
|
592
|
+
if (child && child.type === "export") return true;
|
|
593
|
+
}
|
|
594
|
+
return isExported(node.parent);
|
|
595
|
+
}
|
|
596
|
+
function findChildByType(node, type) {
|
|
597
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
598
|
+
const child = node.child(i);
|
|
599
|
+
if (child && child.type === type) {
|
|
600
|
+
return child;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
function getCurrentSymbolId(context) {
|
|
606
|
+
if (context.currentScope.length === 0) return null;
|
|
607
|
+
return `${context.filePath}::${context.currentScope.join(".")}`;
|
|
608
|
+
}
|
|
609
|
+
var typescriptParser = {
|
|
610
|
+
name: "typescript",
|
|
611
|
+
extensions: [".ts", ".tsx"],
|
|
612
|
+
parseFile: parseTypeScriptFile
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/parser/python.ts
|
|
616
|
+
import Parser2 from "tree-sitter";
|
|
617
|
+
import Python from "tree-sitter-python";
|
|
618
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
619
|
+
import { existsSync as existsSync2 } from "fs";
|
|
620
|
+
var pyParser = new Parser2();
|
|
621
|
+
pyParser.setLanguage(Python);
|
|
622
|
+
function parsePythonFile(filePath, sourceCode, projectRoot) {
|
|
623
|
+
const tree = pyParser.parse(sourceCode);
|
|
624
|
+
const context = {
|
|
625
|
+
filePath,
|
|
626
|
+
projectRoot,
|
|
627
|
+
sourceCode,
|
|
628
|
+
symbols: [],
|
|
629
|
+
edges: [],
|
|
630
|
+
currentScope: [],
|
|
631
|
+
currentClass: null,
|
|
632
|
+
imports: /* @__PURE__ */ new Map()
|
|
633
|
+
};
|
|
634
|
+
walkNode2(tree.rootNode, context);
|
|
635
|
+
return {
|
|
636
|
+
filePath,
|
|
637
|
+
symbols: context.symbols,
|
|
638
|
+
edges: context.edges
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function walkNode2(node, context) {
|
|
642
|
+
processNode2(node, context);
|
|
643
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
644
|
+
const child = node.child(i);
|
|
645
|
+
if (child) {
|
|
646
|
+
walkNode2(child, context);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function processNode2(node, context) {
|
|
651
|
+
const type = node.type;
|
|
652
|
+
switch (type) {
|
|
653
|
+
case "function_definition":
|
|
654
|
+
processFunctionDefinition(node, context);
|
|
655
|
+
break;
|
|
656
|
+
case "class_definition":
|
|
657
|
+
processClassDefinition(node, context);
|
|
658
|
+
break;
|
|
659
|
+
case "expression_statement":
|
|
660
|
+
processExpressionStatement(node, context);
|
|
661
|
+
break;
|
|
662
|
+
case "import_statement":
|
|
663
|
+
processImportStatement2(node, context);
|
|
664
|
+
break;
|
|
665
|
+
case "import_from_statement":
|
|
666
|
+
processImportFromStatement(node, context);
|
|
667
|
+
break;
|
|
668
|
+
case "decorated_definition":
|
|
669
|
+
processDecoratedDefinition(node, context);
|
|
670
|
+
break;
|
|
671
|
+
case "call":
|
|
672
|
+
processCallExpression2(node, context);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function processFunctionDefinition(node, context) {
|
|
677
|
+
const nameNode = findChildByType2(node, "identifier");
|
|
678
|
+
if (!nameNode) return;
|
|
679
|
+
const name = nodeText(nameNode, context);
|
|
680
|
+
const isAsync = node.text.startsWith("async ");
|
|
681
|
+
const kind = context.currentClass ? "method" : "function";
|
|
682
|
+
const scope = context.currentClass || void 0;
|
|
683
|
+
const exported = context.currentScope.length === 0 && !context.currentClass;
|
|
684
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
685
|
+
context.symbols.push({
|
|
686
|
+
id: symbolId,
|
|
687
|
+
name,
|
|
688
|
+
kind,
|
|
689
|
+
filePath: context.filePath,
|
|
690
|
+
startLine: node.startPosition.row + 1,
|
|
691
|
+
endLine: node.endPosition.row + 1,
|
|
692
|
+
exported,
|
|
693
|
+
scope
|
|
694
|
+
});
|
|
695
|
+
context.currentScope.push(name);
|
|
696
|
+
const body = findChildByType2(node, "block");
|
|
697
|
+
if (body) {
|
|
698
|
+
walkNode2(body, context);
|
|
699
|
+
}
|
|
700
|
+
context.currentScope.pop();
|
|
701
|
+
}
|
|
702
|
+
function processClassDefinition(node, context) {
|
|
703
|
+
const nameNode = findChildByType2(node, "identifier");
|
|
704
|
+
if (!nameNode) return;
|
|
705
|
+
const name = nodeText(nameNode, context);
|
|
706
|
+
const exported = context.currentScope.length === 0;
|
|
707
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
708
|
+
context.symbols.push({
|
|
709
|
+
id: symbolId,
|
|
710
|
+
name,
|
|
711
|
+
kind: "class",
|
|
712
|
+
filePath: context.filePath,
|
|
713
|
+
startLine: node.startPosition.row + 1,
|
|
714
|
+
endLine: node.endPosition.row + 1,
|
|
715
|
+
exported
|
|
716
|
+
});
|
|
717
|
+
const argumentList = findChildByType2(node, "argument_list");
|
|
718
|
+
if (argumentList) {
|
|
719
|
+
for (let i = 0; i < argumentList.childCount; i++) {
|
|
720
|
+
const arg = argumentList.child(i);
|
|
721
|
+
if (arg && (arg.type === "identifier" || arg.type === "attribute")) {
|
|
722
|
+
const baseName = nodeText(arg, context);
|
|
723
|
+
const baseId = resolveSymbol(baseName, context);
|
|
724
|
+
if (baseId) {
|
|
725
|
+
context.edges.push({
|
|
726
|
+
source: symbolId,
|
|
727
|
+
target: baseId,
|
|
728
|
+
kind: "inherits",
|
|
729
|
+
filePath: context.filePath,
|
|
730
|
+
line: arg.startPosition.row + 1
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const oldClass = context.currentClass;
|
|
737
|
+
context.currentClass = name;
|
|
738
|
+
context.currentScope.push(name);
|
|
739
|
+
const body = findChildByType2(node, "block");
|
|
740
|
+
if (body) {
|
|
741
|
+
walkNode2(body, context);
|
|
742
|
+
}
|
|
743
|
+
context.currentScope.pop();
|
|
744
|
+
context.currentClass = oldClass;
|
|
745
|
+
}
|
|
746
|
+
function processExpressionStatement(node, context) {
|
|
747
|
+
if (context.currentScope.length > 0) return;
|
|
748
|
+
const assignment = findChildByType2(node, "assignment");
|
|
749
|
+
if (!assignment) return;
|
|
750
|
+
const left = assignment.child(0);
|
|
751
|
+
if (!left || left.type !== "identifier") return;
|
|
752
|
+
const name = nodeText(left, context);
|
|
753
|
+
const isConstant = name === name.toUpperCase() && name.length > 1;
|
|
754
|
+
const kind = isConstant ? "constant" : "variable";
|
|
755
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
756
|
+
context.symbols.push({
|
|
757
|
+
id: symbolId,
|
|
758
|
+
name,
|
|
759
|
+
kind,
|
|
760
|
+
filePath: context.filePath,
|
|
761
|
+
startLine: node.startPosition.row + 1,
|
|
762
|
+
endLine: node.endPosition.row + 1,
|
|
763
|
+
exported: true
|
|
764
|
+
// Module-level variables are exported
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function processImportStatement2(node, context) {
|
|
768
|
+
const dottedName = findChildByType2(node, "dotted_name");
|
|
769
|
+
const identifier = findChildByType2(node, "identifier");
|
|
770
|
+
const moduleName = dottedName ? nodeText(dottedName, context) : identifier ? nodeText(identifier, context) : null;
|
|
771
|
+
if (!moduleName) return;
|
|
772
|
+
const aliasedImport = findChildByType2(node, "aliased_import");
|
|
773
|
+
let importedName = moduleName;
|
|
774
|
+
if (aliasedImport) {
|
|
775
|
+
const asNode = aliasedImport.childForFieldName("alias");
|
|
776
|
+
if (asNode) {
|
|
777
|
+
importedName = nodeText(asNode, context);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const resolvedPath = resolveImportPath2(moduleName, context.filePath, context.projectRoot);
|
|
781
|
+
if (resolvedPath) {
|
|
782
|
+
const targetId = `${resolvedPath}::__module__`;
|
|
783
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
784
|
+
context.imports.set(importedName, targetId);
|
|
785
|
+
context.edges.push({
|
|
786
|
+
source: sourceId,
|
|
787
|
+
target: targetId,
|
|
788
|
+
kind: "imports",
|
|
789
|
+
filePath: context.filePath,
|
|
790
|
+
line: node.startPosition.row + 1
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function processImportFromStatement(node, context) {
|
|
795
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
796
|
+
if (!moduleNode) return;
|
|
797
|
+
const moduleName = nodeText(moduleNode, context);
|
|
798
|
+
const importedNames = [];
|
|
799
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
800
|
+
const child = node.child(i);
|
|
801
|
+
if (!child) continue;
|
|
802
|
+
if (child.type === "dotted_name" || child.type === "identifier") {
|
|
803
|
+
const prevSibling = node.child(i - 1);
|
|
804
|
+
if (prevSibling && prevSibling.text === "import") {
|
|
805
|
+
importedNames.push(nodeText(child, context));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (child.type === "aliased_import") {
|
|
809
|
+
const nameNode = child.childForFieldName("name");
|
|
810
|
+
if (nameNode) {
|
|
811
|
+
importedNames.push(nodeText(nameNode, context));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const resolvedPath = resolveImportPath2(moduleName, context.filePath, context.projectRoot);
|
|
816
|
+
if (resolvedPath) {
|
|
817
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
818
|
+
for (const importedName of importedNames) {
|
|
819
|
+
if (importedName === "*") continue;
|
|
820
|
+
const targetId = `${resolvedPath}::${importedName}`;
|
|
821
|
+
context.imports.set(importedName, targetId);
|
|
822
|
+
context.edges.push({
|
|
823
|
+
source: sourceId,
|
|
824
|
+
target: targetId,
|
|
825
|
+
kind: "imports",
|
|
826
|
+
filePath: context.filePath,
|
|
827
|
+
line: node.startPosition.row + 1
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function processDecoratedDefinition(node, context) {
|
|
833
|
+
const decorators = [];
|
|
834
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
835
|
+
const child = node.child(i);
|
|
836
|
+
if (child && child.type === "decorator") {
|
|
837
|
+
const decoratorName = extractDecoratorName(child, context);
|
|
838
|
+
if (decoratorName) {
|
|
839
|
+
decorators.push(decoratorName);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const definition = findChildByType2(node, "function_definition") || findChildByType2(node, "class_definition");
|
|
844
|
+
if (definition) {
|
|
845
|
+
processNode2(definition, context);
|
|
846
|
+
const nameNode = findChildByType2(definition, "identifier");
|
|
847
|
+
if (nameNode) {
|
|
848
|
+
const targetName = nodeText(nameNode, context);
|
|
849
|
+
const targetId = `${context.filePath}::${targetName}`;
|
|
850
|
+
for (const decoratorName of decorators) {
|
|
851
|
+
const decoratorId = resolveSymbol(decoratorName, context);
|
|
852
|
+
if (decoratorId) {
|
|
853
|
+
context.edges.push({
|
|
854
|
+
source: decoratorId,
|
|
855
|
+
target: targetId,
|
|
856
|
+
kind: "decorates",
|
|
857
|
+
filePath: context.filePath,
|
|
858
|
+
line: node.startPosition.row + 1
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function processCallExpression2(node, context) {
|
|
866
|
+
const functionNode = node.childForFieldName("function");
|
|
867
|
+
if (!functionNode) return;
|
|
868
|
+
let calleeName;
|
|
869
|
+
if (functionNode.type === "identifier") {
|
|
870
|
+
calleeName = nodeText(functionNode, context);
|
|
871
|
+
} else if (functionNode.type === "attribute") {
|
|
872
|
+
const attrNode = functionNode.childForFieldName("attribute");
|
|
873
|
+
if (!attrNode) return;
|
|
874
|
+
calleeName = nodeText(attrNode, context);
|
|
875
|
+
} else {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const builtins = ["print", "len", "str", "int", "float", "list", "dict", "set", "tuple", "range", "enumerate", "zip", "map", "filter", "open", "type", "isinstance", "hasattr", "getattr", "setattr"];
|
|
879
|
+
if (builtins.includes(calleeName)) return;
|
|
880
|
+
const callerId = getCurrentSymbolId2(context);
|
|
881
|
+
if (!callerId) return;
|
|
882
|
+
const calleeId = resolveSymbol(calleeName, context);
|
|
883
|
+
if (calleeId) {
|
|
884
|
+
context.edges.push({
|
|
885
|
+
source: callerId,
|
|
886
|
+
target: calleeId,
|
|
887
|
+
kind: "calls",
|
|
888
|
+
filePath: context.filePath,
|
|
889
|
+
line: node.startPosition.row + 1
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function resolveImportPath2(moduleName, currentFile, projectRoot) {
|
|
894
|
+
if (moduleName.startsWith(".")) {
|
|
895
|
+
const currentDir = dirname2(join3(projectRoot, currentFile));
|
|
896
|
+
let level = 0;
|
|
897
|
+
while (moduleName[level] === ".") level++;
|
|
898
|
+
let targetDir = currentDir;
|
|
899
|
+
for (let i = 0; i < level - 1; i++) {
|
|
900
|
+
targetDir = dirname2(targetDir);
|
|
901
|
+
}
|
|
902
|
+
const relativeModule = moduleName.substring(level);
|
|
903
|
+
if (relativeModule) {
|
|
904
|
+
const modulePath2 = relativeModule.replace(/\./g, "/");
|
|
905
|
+
const candidates2 = [
|
|
906
|
+
join3(targetDir, `${modulePath2}.py`),
|
|
907
|
+
join3(targetDir, modulePath2, "__init__.py")
|
|
908
|
+
];
|
|
909
|
+
for (const candidate of candidates2) {
|
|
910
|
+
if (existsSync2(candidate)) {
|
|
911
|
+
return candidate.substring(projectRoot.length + 1);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
const initPath = join3(targetDir, "__init__.py");
|
|
916
|
+
if (existsSync2(initPath)) {
|
|
917
|
+
return initPath.substring(projectRoot.length + 1);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const modulePath = moduleName.replace(/\./g, "/");
|
|
923
|
+
const candidates = [
|
|
924
|
+
join3(projectRoot, `${modulePath}.py`),
|
|
925
|
+
join3(projectRoot, modulePath, "__init__.py")
|
|
926
|
+
];
|
|
927
|
+
for (const candidate of candidates) {
|
|
928
|
+
if (existsSync2(candidate)) {
|
|
929
|
+
return candidate.substring(projectRoot.length + 1);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function resolveSymbol(name, context) {
|
|
935
|
+
if (context.imports.has(name)) {
|
|
936
|
+
return context.imports.get(name) || null;
|
|
937
|
+
}
|
|
938
|
+
const currentFileId = `${context.filePath}::${name}`;
|
|
939
|
+
const symbol = context.symbols.find((s) => s.id === currentFileId);
|
|
940
|
+
if (symbol) {
|
|
941
|
+
return currentFileId;
|
|
942
|
+
}
|
|
943
|
+
if (context.currentClass) {
|
|
944
|
+
const classMethodId = `${context.filePath}::${context.currentClass}.${name}`;
|
|
945
|
+
const classMethod = context.symbols.find((s) => s.id === classMethodId);
|
|
946
|
+
if (classMethod) {
|
|
947
|
+
return classMethodId;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
function extractDecoratorName(node, context) {
|
|
953
|
+
const identifier = findChildByType2(node, "identifier");
|
|
954
|
+
const attribute = findChildByType2(node, "attribute");
|
|
955
|
+
if (attribute) {
|
|
956
|
+
return nodeText(attribute, context);
|
|
957
|
+
} else if (identifier) {
|
|
958
|
+
return nodeText(identifier, context);
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
function findChildByType2(node, type) {
|
|
963
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
964
|
+
const child = node.child(i);
|
|
965
|
+
if (child && child.type === type) {
|
|
966
|
+
return child;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
function nodeText(node, context) {
|
|
972
|
+
return context.sourceCode.substring(node.startIndex, node.endIndex);
|
|
973
|
+
}
|
|
974
|
+
function getCurrentSymbolId2(context) {
|
|
975
|
+
if (context.currentScope.length === 0) return null;
|
|
976
|
+
return `${context.filePath}::${context.currentScope.join(".")}`;
|
|
977
|
+
}
|
|
978
|
+
var pythonParser = {
|
|
979
|
+
name: "python",
|
|
980
|
+
extensions: [".py"],
|
|
981
|
+
parseFile: parsePythonFile
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// src/parser/javascript.ts
|
|
985
|
+
import Parser3 from "tree-sitter";
|
|
986
|
+
import JavaScript from "tree-sitter-javascript";
|
|
987
|
+
import { existsSync as existsSync3 } from "fs";
|
|
988
|
+
import { join as join4, dirname as dirname3, extname as extname2 } from "path";
|
|
989
|
+
var jsParser = new Parser3();
|
|
990
|
+
jsParser.setLanguage(JavaScript);
|
|
991
|
+
function parseJavaScriptFile(filePath, sourceCode, projectRoot) {
|
|
992
|
+
const tree = jsParser.parse(sourceCode);
|
|
993
|
+
const context = {
|
|
994
|
+
filePath,
|
|
995
|
+
projectRoot,
|
|
996
|
+
sourceCode,
|
|
997
|
+
symbols: [],
|
|
998
|
+
edges: [],
|
|
999
|
+
currentScope: [],
|
|
1000
|
+
imports: /* @__PURE__ */ new Map(),
|
|
1001
|
+
isJSX: filePath.endsWith(".jsx")
|
|
1002
|
+
};
|
|
1003
|
+
walkNode3(tree.rootNode, context);
|
|
1004
|
+
return {
|
|
1005
|
+
filePath,
|
|
1006
|
+
symbols: context.symbols,
|
|
1007
|
+
edges: context.edges
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function walkNode3(node, context) {
|
|
1011
|
+
processNode3(node, context);
|
|
1012
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1013
|
+
const child = node.child(i);
|
|
1014
|
+
if (child) {
|
|
1015
|
+
walkNode3(child, context);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function processNode3(node, context) {
|
|
1020
|
+
const type = node.type;
|
|
1021
|
+
switch (type) {
|
|
1022
|
+
case "function_declaration":
|
|
1023
|
+
processFunctionDeclaration2(node, context);
|
|
1024
|
+
break;
|
|
1025
|
+
case "function":
|
|
1026
|
+
processFunctionExpression(node, context);
|
|
1027
|
+
break;
|
|
1028
|
+
case "class_declaration":
|
|
1029
|
+
processClassDeclaration2(node, context);
|
|
1030
|
+
break;
|
|
1031
|
+
case "method_definition":
|
|
1032
|
+
processMethodDefinition2(node, context);
|
|
1033
|
+
break;
|
|
1034
|
+
case "lexical_declaration":
|
|
1035
|
+
case "variable_declaration":
|
|
1036
|
+
processVariableDeclaration2(node, context);
|
|
1037
|
+
break;
|
|
1038
|
+
case "import_statement":
|
|
1039
|
+
processImportStatement3(node, context);
|
|
1040
|
+
break;
|
|
1041
|
+
case "export_statement":
|
|
1042
|
+
processExportStatement2(node, context);
|
|
1043
|
+
break;
|
|
1044
|
+
case "call_expression":
|
|
1045
|
+
processCallExpression3(node, context);
|
|
1046
|
+
break;
|
|
1047
|
+
case "new_expression":
|
|
1048
|
+
processNewExpression2(node, context);
|
|
1049
|
+
break;
|
|
1050
|
+
case "jsx_element":
|
|
1051
|
+
case "jsx_self_closing_element":
|
|
1052
|
+
if (context.isJSX) {
|
|
1053
|
+
processJSXElement(node, context);
|
|
1054
|
+
}
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function processFunctionDeclaration2(node, context) {
|
|
1059
|
+
const nameNode = findChildByType3(node, "identifier");
|
|
1060
|
+
if (!nameNode) return;
|
|
1061
|
+
const name = nodeText2(nameNode, context);
|
|
1062
|
+
const exported = isExported2(node.parent);
|
|
1063
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1064
|
+
context.symbols.push({
|
|
1065
|
+
id: symbolId,
|
|
1066
|
+
name,
|
|
1067
|
+
kind: "function",
|
|
1068
|
+
filePath: context.filePath,
|
|
1069
|
+
startLine: node.startPosition.row + 1,
|
|
1070
|
+
endLine: node.endPosition.row + 1,
|
|
1071
|
+
exported
|
|
1072
|
+
});
|
|
1073
|
+
context.currentScope.push(name);
|
|
1074
|
+
const body = findChildByType3(node, "statement_block");
|
|
1075
|
+
if (body) {
|
|
1076
|
+
walkNode3(body, context);
|
|
1077
|
+
}
|
|
1078
|
+
context.currentScope.pop();
|
|
1079
|
+
}
|
|
1080
|
+
function processFunctionExpression(node, context) {
|
|
1081
|
+
if (node.parent && node.parent.type === "variable_declarator") {
|
|
1082
|
+
const nameNode = node.parent.childForFieldName("name");
|
|
1083
|
+
if (nameNode && nameNode.type === "identifier") {
|
|
1084
|
+
const name = nodeText2(nameNode, context);
|
|
1085
|
+
const exported = isExported2(node.parent.parent?.parent || null);
|
|
1086
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1087
|
+
context.symbols.push({
|
|
1088
|
+
id: symbolId,
|
|
1089
|
+
name,
|
|
1090
|
+
kind: "function",
|
|
1091
|
+
filePath: context.filePath,
|
|
1092
|
+
startLine: node.startPosition.row + 1,
|
|
1093
|
+
endLine: node.endPosition.row + 1,
|
|
1094
|
+
exported
|
|
1095
|
+
});
|
|
1096
|
+
context.currentScope.push(name);
|
|
1097
|
+
const body = findChildByType3(node, "statement_block");
|
|
1098
|
+
if (body) {
|
|
1099
|
+
walkNode3(body, context);
|
|
1100
|
+
}
|
|
1101
|
+
context.currentScope.pop();
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function processClassDeclaration2(node, context) {
|
|
1106
|
+
const nameNode = findChildByType3(node, "identifier");
|
|
1107
|
+
if (!nameNode) return;
|
|
1108
|
+
const name = nodeText2(nameNode, context);
|
|
1109
|
+
const exported = isExported2(node.parent);
|
|
1110
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1111
|
+
context.symbols.push({
|
|
1112
|
+
id: symbolId,
|
|
1113
|
+
name,
|
|
1114
|
+
kind: "class",
|
|
1115
|
+
filePath: context.filePath,
|
|
1116
|
+
startLine: node.startPosition.row + 1,
|
|
1117
|
+
endLine: node.endPosition.row + 1,
|
|
1118
|
+
exported
|
|
1119
|
+
});
|
|
1120
|
+
const heritage = node.childForFieldName("heritage");
|
|
1121
|
+
if (heritage) {
|
|
1122
|
+
for (let i = 0; i < heritage.childCount; i++) {
|
|
1123
|
+
const child = heritage.child(i);
|
|
1124
|
+
if (child && child.type === "extends_clause") {
|
|
1125
|
+
const baseClass = findChildByType3(child, "identifier");
|
|
1126
|
+
if (baseClass) {
|
|
1127
|
+
const baseName = nodeText2(baseClass, context);
|
|
1128
|
+
const baseId = resolveSymbol2(baseName, context);
|
|
1129
|
+
if (baseId) {
|
|
1130
|
+
context.edges.push({
|
|
1131
|
+
source: symbolId,
|
|
1132
|
+
target: baseId,
|
|
1133
|
+
kind: "extends",
|
|
1134
|
+
filePath: context.filePath,
|
|
1135
|
+
line: child.startPosition.row + 1
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
context.currentScope.push(name);
|
|
1143
|
+
const body = findChildByType3(node, "class_body");
|
|
1144
|
+
if (body) {
|
|
1145
|
+
walkNode3(body, context);
|
|
1146
|
+
}
|
|
1147
|
+
context.currentScope.pop();
|
|
1148
|
+
}
|
|
1149
|
+
function processMethodDefinition2(node, context) {
|
|
1150
|
+
const nameNode = node.childForFieldName("name");
|
|
1151
|
+
if (!nameNode) return;
|
|
1152
|
+
const name = nodeText2(nameNode, context);
|
|
1153
|
+
const scope = context.currentScope.length > 0 ? context.currentScope[context.currentScope.length - 1] : void 0;
|
|
1154
|
+
const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
|
|
1155
|
+
context.symbols.push({
|
|
1156
|
+
id: symbolId,
|
|
1157
|
+
name,
|
|
1158
|
+
kind: "method",
|
|
1159
|
+
filePath: context.filePath,
|
|
1160
|
+
startLine: node.startPosition.row + 1,
|
|
1161
|
+
endLine: node.endPosition.row + 1,
|
|
1162
|
+
exported: false,
|
|
1163
|
+
scope
|
|
1164
|
+
});
|
|
1165
|
+
context.currentScope.push(name);
|
|
1166
|
+
const body = findChildByType3(node, "statement_block");
|
|
1167
|
+
if (body) {
|
|
1168
|
+
walkNode3(body, context);
|
|
1169
|
+
}
|
|
1170
|
+
context.currentScope.pop();
|
|
1171
|
+
}
|
|
1172
|
+
function processVariableDeclaration2(node, context) {
|
|
1173
|
+
const declarators = node.children.filter((c) => c.type === "variable_declarator");
|
|
1174
|
+
for (const declarator of declarators) {
|
|
1175
|
+
const nameNode = declarator.childForFieldName("name");
|
|
1176
|
+
const valueNode = declarator.childForFieldName("value");
|
|
1177
|
+
if (!nameNode) continue;
|
|
1178
|
+
if (valueNode && valueNode.type === "call_expression") {
|
|
1179
|
+
const functionNode = valueNode.childForFieldName("function");
|
|
1180
|
+
if (functionNode && nodeText2(functionNode, context) === "require") {
|
|
1181
|
+
processRequireCall(declarator, valueNode, context);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (context.currentScope.length === 0) {
|
|
1186
|
+
const name = extractIdentifierName(nameNode, context);
|
|
1187
|
+
if (name) {
|
|
1188
|
+
const exported = isExported2(node.parent);
|
|
1189
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1190
|
+
context.symbols.push({
|
|
1191
|
+
id: symbolId,
|
|
1192
|
+
name,
|
|
1193
|
+
kind: "variable",
|
|
1194
|
+
filePath: context.filePath,
|
|
1195
|
+
startLine: node.startPosition.row + 1,
|
|
1196
|
+
endLine: node.endPosition.row + 1,
|
|
1197
|
+
exported
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function processRequireCall(declarator, callNode, context) {
|
|
1204
|
+
const nameNode = declarator.childForFieldName("name");
|
|
1205
|
+
const args = callNode.childForFieldName("arguments");
|
|
1206
|
+
if (!args) return;
|
|
1207
|
+
const stringArg = findChildByType3(args, "string");
|
|
1208
|
+
if (!stringArg) return;
|
|
1209
|
+
const modulePath = nodeText2(stringArg, context).slice(1, -1);
|
|
1210
|
+
const resolvedPath = resolveJavaScriptImport(modulePath, context.filePath, context.projectRoot);
|
|
1211
|
+
if (!resolvedPath) return;
|
|
1212
|
+
if (nameNode) {
|
|
1213
|
+
if (nameNode.type === "identifier") {
|
|
1214
|
+
const name = nodeText2(nameNode, context);
|
|
1215
|
+
const targetId = `${resolvedPath}::${name}`;
|
|
1216
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
1217
|
+
context.imports.set(name, targetId);
|
|
1218
|
+
context.edges.push({
|
|
1219
|
+
source: sourceId,
|
|
1220
|
+
target: targetId,
|
|
1221
|
+
kind: "imports",
|
|
1222
|
+
filePath: context.filePath,
|
|
1223
|
+
line: callNode.startPosition.row + 1
|
|
1224
|
+
});
|
|
1225
|
+
} else if (nameNode.type === "object_pattern") {
|
|
1226
|
+
const properties = nameNode.children.filter((c) => c.type === "pair_pattern" || c.type === "shorthand_property_identifier_pattern");
|
|
1227
|
+
for (const prop of properties) {
|
|
1228
|
+
let importedName;
|
|
1229
|
+
if (prop.type === "shorthand_property_identifier_pattern") {
|
|
1230
|
+
importedName = nodeText2(prop, context);
|
|
1231
|
+
} else {
|
|
1232
|
+
const keyNode = prop.childForFieldName("key");
|
|
1233
|
+
if (keyNode) {
|
|
1234
|
+
importedName = nodeText2(keyNode, context);
|
|
1235
|
+
} else {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const targetId = `${resolvedPath}::${importedName}`;
|
|
1240
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
1241
|
+
context.imports.set(importedName, targetId);
|
|
1242
|
+
context.edges.push({
|
|
1243
|
+
source: sourceId,
|
|
1244
|
+
target: targetId,
|
|
1245
|
+
kind: "imports",
|
|
1246
|
+
filePath: context.filePath,
|
|
1247
|
+
line: callNode.startPosition.row + 1
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function processImportStatement3(node, context) {
|
|
1254
|
+
const source = node.childForFieldName("source");
|
|
1255
|
+
if (!source) return;
|
|
1256
|
+
const importPath = nodeText2(source, context).slice(1, -1);
|
|
1257
|
+
const resolvedPath = resolveJavaScriptImport(importPath, context.filePath, context.projectRoot);
|
|
1258
|
+
if (!resolvedPath) return;
|
|
1259
|
+
const importClause = findChildByType3(node, "import_clause");
|
|
1260
|
+
if (!importClause) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const namedImports = findChildByType3(importClause, "named_imports");
|
|
1264
|
+
const defaultImport = findChildByType3(importClause, "identifier");
|
|
1265
|
+
const namespaceImport = findChildByType3(importClause, "namespace_import");
|
|
1266
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
1267
|
+
if (defaultImport) {
|
|
1268
|
+
const name = nodeText2(defaultImport, context);
|
|
1269
|
+
const targetId = `${resolvedPath}::default`;
|
|
1270
|
+
context.imports.set(name, targetId);
|
|
1271
|
+
context.edges.push({
|
|
1272
|
+
source: sourceId,
|
|
1273
|
+
target: targetId,
|
|
1274
|
+
kind: "imports",
|
|
1275
|
+
filePath: context.filePath,
|
|
1276
|
+
line: node.startPosition.row + 1
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
if (namedImports) {
|
|
1280
|
+
const specifiers = namedImports.children.filter((c) => c.type === "import_specifier");
|
|
1281
|
+
for (const specifier of specifiers) {
|
|
1282
|
+
const nameNode = specifier.childForFieldName("name");
|
|
1283
|
+
const aliasNode = specifier.childForFieldName("alias");
|
|
1284
|
+
if (nameNode) {
|
|
1285
|
+
const importedName = nodeText2(nameNode, context);
|
|
1286
|
+
const localName = aliasNode ? nodeText2(aliasNode, context) : importedName;
|
|
1287
|
+
const targetId = `${resolvedPath}::${importedName}`;
|
|
1288
|
+
context.imports.set(localName, targetId);
|
|
1289
|
+
context.edges.push({
|
|
1290
|
+
source: sourceId,
|
|
1291
|
+
target: targetId,
|
|
1292
|
+
kind: "imports",
|
|
1293
|
+
filePath: context.filePath,
|
|
1294
|
+
line: node.startPosition.row + 1
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (namespaceImport) {
|
|
1300
|
+
const aliasNode = findChildByType3(namespaceImport, "identifier");
|
|
1301
|
+
if (aliasNode) {
|
|
1302
|
+
const localName = nodeText2(aliasNode, context);
|
|
1303
|
+
const targetId = `${resolvedPath}::*`;
|
|
1304
|
+
context.imports.set(localName, targetId);
|
|
1305
|
+
context.edges.push({
|
|
1306
|
+
source: sourceId,
|
|
1307
|
+
target: targetId,
|
|
1308
|
+
kind: "imports",
|
|
1309
|
+
filePath: context.filePath,
|
|
1310
|
+
line: node.startPosition.row + 1
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function processExportStatement2(node, context) {
|
|
1316
|
+
const declaration = findChildByType3(node, "lexical_declaration") || findChildByType3(node, "variable_declaration") || findChildByType3(node, "function_declaration") || findChildByType3(node, "class_declaration");
|
|
1317
|
+
if (declaration) {
|
|
1318
|
+
processNode3(declaration, context);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function processCallExpression3(node, context) {
|
|
1322
|
+
const functionNode = node.childForFieldName("function");
|
|
1323
|
+
if (!functionNode) return;
|
|
1324
|
+
let calleeName = null;
|
|
1325
|
+
if (functionNode.type === "identifier") {
|
|
1326
|
+
calleeName = nodeText2(functionNode, context);
|
|
1327
|
+
} else if (functionNode.type === "member_expression") {
|
|
1328
|
+
const property = functionNode.childForFieldName("property");
|
|
1329
|
+
if (property) {
|
|
1330
|
+
calleeName = nodeText2(property, context);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (!calleeName) return;
|
|
1334
|
+
const builtins = ["console", "require", "setTimeout", "setInterval", "parseInt", "parseFloat", "JSON", "Object", "Array", "String", "Number", "Boolean"];
|
|
1335
|
+
if (builtins.includes(calleeName)) return;
|
|
1336
|
+
const callerId = getCurrentSymbolId3(context);
|
|
1337
|
+
if (!callerId) return;
|
|
1338
|
+
const calleeId = resolveSymbol2(calleeName, context);
|
|
1339
|
+
if (calleeId) {
|
|
1340
|
+
context.edges.push({
|
|
1341
|
+
source: callerId,
|
|
1342
|
+
target: calleeId,
|
|
1343
|
+
kind: "calls",
|
|
1344
|
+
filePath: context.filePath,
|
|
1345
|
+
line: node.startPosition.row + 1
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function processNewExpression2(node, context) {
|
|
1350
|
+
const constructorNode = findChildByType3(node, "identifier");
|
|
1351
|
+
if (!constructorNode) return;
|
|
1352
|
+
const className = nodeText2(constructorNode, context);
|
|
1353
|
+
const callerId = getCurrentSymbolId3(context);
|
|
1354
|
+
if (!callerId) return;
|
|
1355
|
+
const classId = resolveSymbol2(className, context);
|
|
1356
|
+
if (classId) {
|
|
1357
|
+
context.edges.push({
|
|
1358
|
+
source: callerId,
|
|
1359
|
+
target: classId,
|
|
1360
|
+
kind: "calls",
|
|
1361
|
+
filePath: context.filePath,
|
|
1362
|
+
line: node.startPosition.row + 1
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function processJSXElement(node, context) {
|
|
1367
|
+
let tagName = null;
|
|
1368
|
+
if (node.type === "jsx_self_closing_element") {
|
|
1369
|
+
const nameNode = node.childForFieldName("name");
|
|
1370
|
+
if (nameNode) {
|
|
1371
|
+
tagName = nodeText2(nameNode, context);
|
|
1372
|
+
}
|
|
1373
|
+
} else if (node.type === "jsx_element") {
|
|
1374
|
+
const openingElement = findChildByType3(node, "jsx_opening_element");
|
|
1375
|
+
if (openingElement) {
|
|
1376
|
+
const nameNode = openingElement.childForFieldName("name");
|
|
1377
|
+
if (nameNode) {
|
|
1378
|
+
tagName = nodeText2(nameNode, context);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (!tagName) return;
|
|
1383
|
+
if (!/^[A-Z]/.test(tagName)) return;
|
|
1384
|
+
const callerId = getCurrentSymbolId3(context);
|
|
1385
|
+
if (!callerId) return;
|
|
1386
|
+
const componentId = resolveSymbol2(tagName, context);
|
|
1387
|
+
if (componentId) {
|
|
1388
|
+
context.edges.push({
|
|
1389
|
+
source: callerId,
|
|
1390
|
+
target: componentId,
|
|
1391
|
+
kind: "references",
|
|
1392
|
+
filePath: context.filePath,
|
|
1393
|
+
line: node.startPosition.row + 1
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function resolveJavaScriptImport(importPath, currentFile, projectRoot) {
|
|
1398
|
+
if (importPath.startsWith(".")) {
|
|
1399
|
+
const currentDir = dirname3(join4(projectRoot, currentFile));
|
|
1400
|
+
const targetPath = join4(currentDir, importPath);
|
|
1401
|
+
const extensions = [".js", ".jsx", ".mjs", ".cjs"];
|
|
1402
|
+
const indexFiles = ["index.js", "index.jsx", "index.mjs"];
|
|
1403
|
+
if (extname2(importPath)) {
|
|
1404
|
+
const fullPath = targetPath;
|
|
1405
|
+
if (existsSync3(fullPath)) {
|
|
1406
|
+
return fullPath.substring(projectRoot.length + 1);
|
|
1407
|
+
}
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
for (const ext of extensions) {
|
|
1411
|
+
const candidate = `${targetPath}${ext}`;
|
|
1412
|
+
if (existsSync3(candidate)) {
|
|
1413
|
+
return candidate.substring(projectRoot.length + 1);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
for (const indexFile of indexFiles) {
|
|
1417
|
+
const candidate = join4(targetPath, indexFile);
|
|
1418
|
+
if (existsSync3(candidate)) {
|
|
1419
|
+
return candidate.substring(projectRoot.length + 1);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
function resolveSymbol2(name, context) {
|
|
1427
|
+
if (context.imports.has(name)) {
|
|
1428
|
+
return context.imports.get(name) || null;
|
|
1429
|
+
}
|
|
1430
|
+
const currentFileId = `${context.filePath}::${name}`;
|
|
1431
|
+
const symbol = context.symbols.find((s) => s.id === currentFileId);
|
|
1432
|
+
if (symbol) {
|
|
1433
|
+
return currentFileId;
|
|
1434
|
+
}
|
|
1435
|
+
if (context.currentScope.length > 0) {
|
|
1436
|
+
const scopedId = `${context.filePath}::${context.currentScope.join(".")}.${name}`;
|
|
1437
|
+
const scopedSymbol = context.symbols.find((s) => s.id === scopedId);
|
|
1438
|
+
if (scopedSymbol) {
|
|
1439
|
+
return scopedId;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
function isExported2(node) {
|
|
1445
|
+
if (!node) return false;
|
|
1446
|
+
let current = node;
|
|
1447
|
+
while (current) {
|
|
1448
|
+
if (current.type === "export_statement") {
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
current = current.parent;
|
|
1452
|
+
}
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
function extractIdentifierName(node, context) {
|
|
1456
|
+
if (node.type === "identifier") {
|
|
1457
|
+
return nodeText2(node, context);
|
|
1458
|
+
} else if (node.type === "object_pattern") {
|
|
1459
|
+
const properties = node.children.filter((c) => c.type === "shorthand_property_identifier_pattern");
|
|
1460
|
+
if (properties.length > 0) {
|
|
1461
|
+
return nodeText2(properties[0], context);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
function findChildByType3(node, type) {
|
|
1467
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1468
|
+
const child = node.child(i);
|
|
1469
|
+
if (child && child.type === type) {
|
|
1470
|
+
return child;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
function nodeText2(node, context) {
|
|
1476
|
+
return context.sourceCode.substring(node.startIndex, node.endIndex);
|
|
1477
|
+
}
|
|
1478
|
+
function getCurrentSymbolId3(context) {
|
|
1479
|
+
if (context.currentScope.length === 0) return null;
|
|
1480
|
+
return `${context.filePath}::${context.currentScope.join(".")}`;
|
|
1481
|
+
}
|
|
1482
|
+
var javascriptParser = {
|
|
1483
|
+
name: "javascript",
|
|
1484
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
1485
|
+
parseFile: parseJavaScriptFile
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// src/parser/go.ts
|
|
1489
|
+
import Parser4 from "tree-sitter";
|
|
1490
|
+
import Go from "tree-sitter-go";
|
|
1491
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
1492
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1493
|
+
var parser = new Parser4();
|
|
1494
|
+
parser.setLanguage(Go);
|
|
1495
|
+
function parseGoFile(filePath, sourceCode, projectRoot) {
|
|
1496
|
+
const tree = parser.parse(sourceCode);
|
|
1497
|
+
const moduleName = readGoModuleName(projectRoot);
|
|
1498
|
+
const context = {
|
|
1499
|
+
filePath,
|
|
1500
|
+
projectRoot,
|
|
1501
|
+
sourceCode,
|
|
1502
|
+
symbols: [],
|
|
1503
|
+
edges: [],
|
|
1504
|
+
currentScope: [],
|
|
1505
|
+
packageName: "",
|
|
1506
|
+
imports: /* @__PURE__ */ new Map(),
|
|
1507
|
+
moduleName
|
|
1508
|
+
};
|
|
1509
|
+
extractPackageName(tree.rootNode, context);
|
|
1510
|
+
walkNode4(tree.rootNode, context);
|
|
1511
|
+
return {
|
|
1512
|
+
filePath,
|
|
1513
|
+
symbols: context.symbols,
|
|
1514
|
+
edges: context.edges
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function extractPackageName(node, context) {
|
|
1518
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1519
|
+
const child = node.child(i);
|
|
1520
|
+
if (child && child.type === "package_clause") {
|
|
1521
|
+
const pkgIdentifier = findChildByType4(child, "package_identifier");
|
|
1522
|
+
if (pkgIdentifier) {
|
|
1523
|
+
context.packageName = nodeText3(pkgIdentifier, context);
|
|
1524
|
+
}
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function walkNode4(node, context) {
|
|
1530
|
+
processNode4(node, context);
|
|
1531
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1532
|
+
const child = node.child(i);
|
|
1533
|
+
if (child) {
|
|
1534
|
+
walkNode4(child, context);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function processNode4(node, context) {
|
|
1539
|
+
const type = node.type;
|
|
1540
|
+
switch (type) {
|
|
1541
|
+
case "function_declaration":
|
|
1542
|
+
processFunctionDeclaration3(node, context);
|
|
1543
|
+
break;
|
|
1544
|
+
case "method_declaration":
|
|
1545
|
+
processMethodDeclaration(node, context);
|
|
1546
|
+
break;
|
|
1547
|
+
case "type_declaration":
|
|
1548
|
+
processTypeDeclaration(node, context);
|
|
1549
|
+
break;
|
|
1550
|
+
case "const_declaration":
|
|
1551
|
+
processConstDeclaration(node, context);
|
|
1552
|
+
break;
|
|
1553
|
+
case "var_declaration":
|
|
1554
|
+
processVarDeclaration(node, context);
|
|
1555
|
+
break;
|
|
1556
|
+
case "import_declaration":
|
|
1557
|
+
processImportDeclaration(node, context);
|
|
1558
|
+
break;
|
|
1559
|
+
case "call_expression":
|
|
1560
|
+
processCallExpression4(node, context);
|
|
1561
|
+
break;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function processFunctionDeclaration3(node, context) {
|
|
1565
|
+
const nameNode = node.childForFieldName("name");
|
|
1566
|
+
if (!nameNode) return;
|
|
1567
|
+
const name = nodeText3(nameNode, context);
|
|
1568
|
+
const exported = isExported3(name);
|
|
1569
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1570
|
+
context.symbols.push({
|
|
1571
|
+
id: symbolId,
|
|
1572
|
+
name,
|
|
1573
|
+
kind: "function",
|
|
1574
|
+
filePath: context.filePath,
|
|
1575
|
+
startLine: node.startPosition.row + 1,
|
|
1576
|
+
endLine: node.endPosition.row + 1,
|
|
1577
|
+
exported
|
|
1578
|
+
});
|
|
1579
|
+
context.currentScope.push(name);
|
|
1580
|
+
const body = node.childForFieldName("body");
|
|
1581
|
+
if (body) {
|
|
1582
|
+
walkNode4(body, context);
|
|
1583
|
+
}
|
|
1584
|
+
context.currentScope.pop();
|
|
1585
|
+
}
|
|
1586
|
+
function processMethodDeclaration(node, context) {
|
|
1587
|
+
const nameNode = node.childForFieldName("name");
|
|
1588
|
+
const receiverNode = node.childForFieldName("receiver");
|
|
1589
|
+
if (!nameNode || !receiverNode) return;
|
|
1590
|
+
const name = nodeText3(nameNode, context);
|
|
1591
|
+
const receiverType = extractReceiverType(receiverNode, context);
|
|
1592
|
+
if (!receiverType) return;
|
|
1593
|
+
const exported = isExported3(name);
|
|
1594
|
+
const symbolId = `${context.filePath}::${receiverType}.${name}`;
|
|
1595
|
+
context.symbols.push({
|
|
1596
|
+
id: symbolId,
|
|
1597
|
+
name,
|
|
1598
|
+
kind: "method",
|
|
1599
|
+
filePath: context.filePath,
|
|
1600
|
+
startLine: node.startPosition.row + 1,
|
|
1601
|
+
endLine: node.endPosition.row + 1,
|
|
1602
|
+
exported,
|
|
1603
|
+
scope: receiverType
|
|
1604
|
+
});
|
|
1605
|
+
context.currentScope.push(`${receiverType}.${name}`);
|
|
1606
|
+
const body = node.childForFieldName("body");
|
|
1607
|
+
if (body) {
|
|
1608
|
+
walkNode4(body, context);
|
|
1609
|
+
}
|
|
1610
|
+
context.currentScope.pop();
|
|
1611
|
+
}
|
|
1612
|
+
function processTypeDeclaration(node, context) {
|
|
1613
|
+
const typeSpecs = findChildrenByType(node, "type_spec");
|
|
1614
|
+
for (const typeSpec of typeSpecs) {
|
|
1615
|
+
const nameNode = typeSpec.childForFieldName("name");
|
|
1616
|
+
const typeNode = typeSpec.childForFieldName("type");
|
|
1617
|
+
if (!nameNode || !typeNode) continue;
|
|
1618
|
+
const name = nodeText3(nameNode, context);
|
|
1619
|
+
const exported = isExported3(name);
|
|
1620
|
+
let kind = "type_alias";
|
|
1621
|
+
if (typeNode.type === "struct_type") {
|
|
1622
|
+
kind = "class";
|
|
1623
|
+
const fieldList = findChildByType4(typeNode, "field_declaration_list");
|
|
1624
|
+
if (fieldList) {
|
|
1625
|
+
for (let i = 0; i < fieldList.childCount; i++) {
|
|
1626
|
+
const field = fieldList.child(i);
|
|
1627
|
+
if (field && field.type === "field_declaration") {
|
|
1628
|
+
const fieldName = field.childForFieldName("name");
|
|
1629
|
+
const fieldType = field.childForFieldName("type");
|
|
1630
|
+
if (!fieldName && fieldType) {
|
|
1631
|
+
const embeddedTypeName = extractTypeName(fieldType, context);
|
|
1632
|
+
if (embeddedTypeName) {
|
|
1633
|
+
const embeddedId = resolveSymbol3(embeddedTypeName, context);
|
|
1634
|
+
if (embeddedId) {
|
|
1635
|
+
const symbolId2 = `${context.filePath}::${name}`;
|
|
1636
|
+
context.edges.push({
|
|
1637
|
+
source: symbolId2,
|
|
1638
|
+
target: embeddedId,
|
|
1639
|
+
kind: "inherits",
|
|
1640
|
+
filePath: context.filePath,
|
|
1641
|
+
line: field.startPosition.row + 1
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
} else if (typeNode.type === "interface_type") {
|
|
1650
|
+
kind = "interface";
|
|
1651
|
+
}
|
|
1652
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1653
|
+
context.symbols.push({
|
|
1654
|
+
id: symbolId,
|
|
1655
|
+
name,
|
|
1656
|
+
kind,
|
|
1657
|
+
filePath: context.filePath,
|
|
1658
|
+
startLine: typeSpec.startPosition.row + 1,
|
|
1659
|
+
endLine: typeSpec.endPosition.row + 1,
|
|
1660
|
+
exported
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function processConstDeclaration(node, context) {
|
|
1665
|
+
const constSpecs = findChildrenByType(node, "const_spec");
|
|
1666
|
+
for (const constSpec of constSpecs) {
|
|
1667
|
+
const nameNode = constSpec.childForFieldName("name");
|
|
1668
|
+
if (!nameNode) continue;
|
|
1669
|
+
const names = extractIdentifierNames(nameNode, context);
|
|
1670
|
+
for (const name of names) {
|
|
1671
|
+
const exported = isExported3(name);
|
|
1672
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1673
|
+
context.symbols.push({
|
|
1674
|
+
id: symbolId,
|
|
1675
|
+
name,
|
|
1676
|
+
kind: "constant",
|
|
1677
|
+
filePath: context.filePath,
|
|
1678
|
+
startLine: constSpec.startPosition.row + 1,
|
|
1679
|
+
endLine: constSpec.endPosition.row + 1,
|
|
1680
|
+
exported
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function processVarDeclaration(node, context) {
|
|
1686
|
+
if (context.currentScope.length > 0) return;
|
|
1687
|
+
const varSpecs = findChildrenByType(node, "var_spec");
|
|
1688
|
+
for (const varSpec of varSpecs) {
|
|
1689
|
+
const nameNode = varSpec.childForFieldName("name");
|
|
1690
|
+
if (!nameNode) continue;
|
|
1691
|
+
const names = extractIdentifierNames(nameNode, context);
|
|
1692
|
+
for (const name of names) {
|
|
1693
|
+
const exported = isExported3(name);
|
|
1694
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
1695
|
+
context.symbols.push({
|
|
1696
|
+
id: symbolId,
|
|
1697
|
+
name,
|
|
1698
|
+
kind: "variable",
|
|
1699
|
+
filePath: context.filePath,
|
|
1700
|
+
startLine: varSpec.startPosition.row + 1,
|
|
1701
|
+
endLine: varSpec.endPosition.row + 1,
|
|
1702
|
+
exported
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function processImportDeclaration(node, context) {
|
|
1708
|
+
let importSpecs = [];
|
|
1709
|
+
const importSpecList = findChildByType4(node, "import_spec_list");
|
|
1710
|
+
if (importSpecList) {
|
|
1711
|
+
importSpecs = findChildrenByType(importSpecList, "import_spec");
|
|
1712
|
+
} else {
|
|
1713
|
+
importSpecs = findChildrenByType(node, "import_spec");
|
|
1714
|
+
}
|
|
1715
|
+
for (const importSpec of importSpecs) {
|
|
1716
|
+
const pathNode = importSpec.childForFieldName("path");
|
|
1717
|
+
if (!pathNode) continue;
|
|
1718
|
+
const importPath = nodeText3(pathNode, context).slice(1, -1);
|
|
1719
|
+
const nameNode = importSpec.childForFieldName("name");
|
|
1720
|
+
let alias = "";
|
|
1721
|
+
if (nameNode) {
|
|
1722
|
+
alias = nodeText3(nameNode, context);
|
|
1723
|
+
} else {
|
|
1724
|
+
const segments = importPath.split("/");
|
|
1725
|
+
alias = segments[segments.length - 1];
|
|
1726
|
+
}
|
|
1727
|
+
context.imports.set(alias, importPath);
|
|
1728
|
+
const resolvedFiles = resolveGoImport(importPath, context.projectRoot, context.moduleName);
|
|
1729
|
+
if (resolvedFiles.length > 0) {
|
|
1730
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
1731
|
+
for (const targetFile of resolvedFiles) {
|
|
1732
|
+
const targetId = `${targetFile}::__file__`;
|
|
1733
|
+
context.edges.push({
|
|
1734
|
+
source: sourceId,
|
|
1735
|
+
target: targetId,
|
|
1736
|
+
kind: "imports",
|
|
1737
|
+
filePath: context.filePath,
|
|
1738
|
+
line: importSpec.startPosition.row + 1
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function processCallExpression4(node, context) {
|
|
1745
|
+
const functionNode = node.childForFieldName("function");
|
|
1746
|
+
if (!functionNode) return;
|
|
1747
|
+
let calleeName = null;
|
|
1748
|
+
if (functionNode.type === "identifier") {
|
|
1749
|
+
calleeName = nodeText3(functionNode, context);
|
|
1750
|
+
} else if (functionNode.type === "selector_expression") {
|
|
1751
|
+
const field = functionNode.childForFieldName("field");
|
|
1752
|
+
if (field) {
|
|
1753
|
+
calleeName = nodeText3(field, context);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (!calleeName) return;
|
|
1757
|
+
const builtins = ["make", "len", "cap", "append", "copy", "delete", "panic", "recover", "print", "println", "new"];
|
|
1758
|
+
if (builtins.includes(calleeName)) return;
|
|
1759
|
+
const callerId = getCurrentSymbolId4(context);
|
|
1760
|
+
if (!callerId) return;
|
|
1761
|
+
const calleeId = resolveSymbol3(calleeName, context);
|
|
1762
|
+
if (calleeId) {
|
|
1763
|
+
context.edges.push({
|
|
1764
|
+
source: callerId,
|
|
1765
|
+
target: calleeId,
|
|
1766
|
+
kind: "calls",
|
|
1767
|
+
filePath: context.filePath,
|
|
1768
|
+
line: node.startPosition.row + 1
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function readGoModuleName(projectRoot) {
|
|
1773
|
+
let currentDir = projectRoot;
|
|
1774
|
+
for (let i = 0; i < 5; i++) {
|
|
1775
|
+
const goModPath = join5(currentDir, "go.mod");
|
|
1776
|
+
if (existsSync4(goModPath)) {
|
|
1777
|
+
try {
|
|
1778
|
+
const content = readFileSync2(goModPath, "utf-8");
|
|
1779
|
+
const lines = content.split("\n");
|
|
1780
|
+
for (const line of lines) {
|
|
1781
|
+
const trimmed = line.trim();
|
|
1782
|
+
if (trimmed.startsWith("module ")) {
|
|
1783
|
+
return trimmed.substring(7).trim();
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
console.error(`Error reading go.mod: ${error}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const parentDir = dirname4(currentDir);
|
|
1791
|
+
if (parentDir === currentDir) break;
|
|
1792
|
+
currentDir = parentDir;
|
|
1793
|
+
}
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
function resolveGoImport(importPath, projectRoot, moduleName) {
|
|
1797
|
+
if (!importPath.includes(".") && !importPath.includes("/")) {
|
|
1798
|
+
return [];
|
|
1799
|
+
}
|
|
1800
|
+
if (moduleName && importPath.startsWith(moduleName)) {
|
|
1801
|
+
const relativePath = importPath.substring(moduleName.length + 1);
|
|
1802
|
+
const packageDir2 = join5(projectRoot, relativePath);
|
|
1803
|
+
return findGoFilesInDir(packageDir2, projectRoot);
|
|
1804
|
+
}
|
|
1805
|
+
const segments = importPath.split("/");
|
|
1806
|
+
const packageDir = join5(projectRoot, ...segments);
|
|
1807
|
+
if (existsSync4(packageDir)) {
|
|
1808
|
+
return findGoFilesInDir(packageDir, projectRoot);
|
|
1809
|
+
}
|
|
1810
|
+
return [];
|
|
1811
|
+
}
|
|
1812
|
+
function findGoFilesInDir(dir, projectRoot) {
|
|
1813
|
+
if (!existsSync4(dir)) return [];
|
|
1814
|
+
try {
|
|
1815
|
+
const files = readdirSync2(dir);
|
|
1816
|
+
const goFiles = files.filter((f) => f.endsWith(".go") && !f.endsWith("_test.go"));
|
|
1817
|
+
return goFiles.map((f) => {
|
|
1818
|
+
const fullPath = join5(dir, f);
|
|
1819
|
+
return fullPath.substring(projectRoot.length + 1);
|
|
1820
|
+
});
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
console.error(`[findGoFilesInDir] Error:`, error);
|
|
1823
|
+
return [];
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function isExported3(name) {
|
|
1827
|
+
return name.length > 0 && name[0] === name[0].toUpperCase();
|
|
1828
|
+
}
|
|
1829
|
+
function extractReceiverType(receiverNode, context) {
|
|
1830
|
+
const paramDecl = findChildByType4(receiverNode, "parameter_declaration");
|
|
1831
|
+
if (!paramDecl) return null;
|
|
1832
|
+
const typeNode = paramDecl.childForFieldName("type");
|
|
1833
|
+
if (!typeNode) return null;
|
|
1834
|
+
return extractTypeName(typeNode, context);
|
|
1835
|
+
}
|
|
1836
|
+
function extractTypeName(typeNode, context) {
|
|
1837
|
+
if (typeNode.type === "pointer_type") {
|
|
1838
|
+
for (let i = 0; i < typeNode.childCount; i++) {
|
|
1839
|
+
const child = typeNode.child(i);
|
|
1840
|
+
if (child && child.type === "type_identifier") {
|
|
1841
|
+
return nodeText3(child, context);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return null;
|
|
1845
|
+
} else if (typeNode.type === "type_identifier") {
|
|
1846
|
+
return nodeText3(typeNode, context);
|
|
1847
|
+
}
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
function extractIdentifierNames(node, context) {
|
|
1851
|
+
if (node.type === "identifier") {
|
|
1852
|
+
return [nodeText3(node, context)];
|
|
1853
|
+
} else if (node.type === "identifier_list") {
|
|
1854
|
+
const names = [];
|
|
1855
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1856
|
+
const child = node.child(i);
|
|
1857
|
+
if (child && child.type === "identifier") {
|
|
1858
|
+
names.push(nodeText3(child, context));
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return names;
|
|
1862
|
+
}
|
|
1863
|
+
return [];
|
|
1864
|
+
}
|
|
1865
|
+
function resolveSymbol3(name, context) {
|
|
1866
|
+
const currentFileId = `${context.filePath}::${name}`;
|
|
1867
|
+
const symbol = context.symbols.find((s) => s.id === currentFileId);
|
|
1868
|
+
if (symbol) {
|
|
1869
|
+
return currentFileId;
|
|
1870
|
+
}
|
|
1871
|
+
if (context.currentScope.length > 0) {
|
|
1872
|
+
for (let i = context.currentScope.length - 1; i >= 0; i--) {
|
|
1873
|
+
const scopedId = `${context.filePath}::${context.currentScope[i]}.${name}`;
|
|
1874
|
+
const scopedSymbol = context.symbols.find((s) => s.id === scopedId);
|
|
1875
|
+
if (scopedSymbol) {
|
|
1876
|
+
return scopedId;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
function findChildByType4(node, type) {
|
|
1883
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1884
|
+
const child = node.child(i);
|
|
1885
|
+
if (child && child.type === type) {
|
|
1886
|
+
return child;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
function findChildrenByType(node, type) {
|
|
1892
|
+
const results = [];
|
|
1893
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1894
|
+
const child = node.child(i);
|
|
1895
|
+
if (child && child.type === type) {
|
|
1896
|
+
results.push(child);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return results;
|
|
1900
|
+
}
|
|
1901
|
+
function nodeText3(node, context) {
|
|
1902
|
+
return context.sourceCode.substring(node.startIndex, node.endIndex);
|
|
1903
|
+
}
|
|
1904
|
+
function getCurrentSymbolId4(context) {
|
|
1905
|
+
if (context.currentScope.length === 0) return null;
|
|
1906
|
+
return `${context.filePath}::${context.currentScope[context.currentScope.length - 1]}`;
|
|
1907
|
+
}
|
|
1908
|
+
var goParser = {
|
|
1909
|
+
name: "go",
|
|
1910
|
+
extensions: [".go"],
|
|
1911
|
+
parseFile: parseGoFile
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
// src/parser/detect.ts
|
|
1915
|
+
var parsers = [
|
|
1916
|
+
typescriptParser,
|
|
1917
|
+
pythonParser,
|
|
1918
|
+
javascriptParser,
|
|
1919
|
+
goParser
|
|
1920
|
+
];
|
|
1921
|
+
function getParserForFile(filePath) {
|
|
1922
|
+
const ext = extname3(filePath).toLowerCase();
|
|
1923
|
+
return parsers.find((p) => p.extensions.includes(ext)) || null;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/parser/index.ts
|
|
1927
|
+
var MAX_FILE_SIZE = 1e6;
|
|
1928
|
+
function shouldParseFile(fullPath) {
|
|
1929
|
+
try {
|
|
1930
|
+
const stats = statSync2(fullPath);
|
|
1931
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1932
|
+
console.error(`[Parser] Skipping ${fullPath} \u2014 file too large (${(stats.size / 1024).toFixed(0)}KB)`);
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
return true;
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function parseProject(projectRoot) {
|
|
1941
|
+
const files = scanDirectory(projectRoot);
|
|
1942
|
+
const parsedFiles = [];
|
|
1943
|
+
for (const file of files) {
|
|
1944
|
+
try {
|
|
1945
|
+
const fullPath = join6(projectRoot, file);
|
|
1946
|
+
if (!shouldParseFile(fullPath)) {
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
const parser2 = getParserForFile(file);
|
|
1950
|
+
if (!parser2) {
|
|
1951
|
+
console.error(`No parser found for file: ${file}`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const sourceCode = readFileSync3(fullPath, "utf-8");
|
|
1955
|
+
const parsed = parser2.parseFile(file, sourceCode, projectRoot);
|
|
1956
|
+
parsedFiles.push(parsed);
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
console.error(`Error parsing file ${file}:`, err instanceof Error ? err.message : err);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return parsedFiles;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// src/graph/index.ts
|
|
1965
|
+
import { DirectedGraph } from "graphology";
|
|
1966
|
+
function buildGraph(parsedFiles) {
|
|
1967
|
+
const graph = new DirectedGraph();
|
|
1968
|
+
for (const file of parsedFiles) {
|
|
1969
|
+
for (const symbol of file.symbols) {
|
|
1970
|
+
if (!graph.hasNode(symbol.id)) {
|
|
1971
|
+
graph.addNode(symbol.id, {
|
|
1972
|
+
name: symbol.name,
|
|
1973
|
+
kind: symbol.kind,
|
|
1974
|
+
filePath: symbol.filePath,
|
|
1975
|
+
startLine: symbol.startLine,
|
|
1976
|
+
endLine: symbol.endLine,
|
|
1977
|
+
exported: symbol.exported,
|
|
1978
|
+
scope: symbol.scope
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const fileNodes = /* @__PURE__ */ new Set();
|
|
1984
|
+
for (const file of parsedFiles) {
|
|
1985
|
+
for (const edge of file.edges) {
|
|
1986
|
+
if (edge.source.endsWith("::__file__") && !fileNodes.has(edge.source)) {
|
|
1987
|
+
fileNodes.add(edge.source);
|
|
1988
|
+
const filePath = edge.source.replace("::__file__", "");
|
|
1989
|
+
graph.addNode(edge.source, {
|
|
1990
|
+
name: "__file__",
|
|
1991
|
+
kind: "import",
|
|
1992
|
+
filePath,
|
|
1993
|
+
startLine: 1,
|
|
1994
|
+
endLine: 1,
|
|
1995
|
+
exported: false
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
for (const file of parsedFiles) {
|
|
2001
|
+
for (const edge of file.edges) {
|
|
2002
|
+
if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
|
|
2003
|
+
graph.mergeEdge(edge.source, edge.target, {
|
|
2004
|
+
kind: edge.kind,
|
|
2005
|
+
filePath: edge.filePath,
|
|
2006
|
+
line: edge.line
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return graph;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/graph/queries.ts
|
|
2015
|
+
function getDependencies(graph, symbolId) {
|
|
2016
|
+
if (!graph.hasNode(symbolId)) return [];
|
|
2017
|
+
const dependencies = [];
|
|
2018
|
+
const neighbors = graph.outNeighbors(symbolId);
|
|
2019
|
+
for (const neighborId of neighbors) {
|
|
2020
|
+
const attrs = graph.getNodeAttributes(neighborId);
|
|
2021
|
+
dependencies.push({
|
|
2022
|
+
id: neighborId,
|
|
2023
|
+
name: attrs.name,
|
|
2024
|
+
kind: attrs.kind,
|
|
2025
|
+
filePath: attrs.filePath,
|
|
2026
|
+
startLine: attrs.startLine,
|
|
2027
|
+
endLine: attrs.endLine,
|
|
2028
|
+
exported: attrs.exported,
|
|
2029
|
+
scope: attrs.scope
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
return dependencies;
|
|
2033
|
+
}
|
|
2034
|
+
function getDependents(graph, symbolId) {
|
|
2035
|
+
if (!graph.hasNode(symbolId)) return [];
|
|
2036
|
+
const dependents = [];
|
|
2037
|
+
const neighbors = graph.inNeighbors(symbolId);
|
|
2038
|
+
for (const neighborId of neighbors) {
|
|
2039
|
+
const attrs = graph.getNodeAttributes(neighborId);
|
|
2040
|
+
dependents.push({
|
|
2041
|
+
id: neighborId,
|
|
2042
|
+
name: attrs.name,
|
|
2043
|
+
kind: attrs.kind,
|
|
2044
|
+
filePath: attrs.filePath,
|
|
2045
|
+
startLine: attrs.startLine,
|
|
2046
|
+
endLine: attrs.endLine,
|
|
2047
|
+
exported: attrs.exported,
|
|
2048
|
+
scope: attrs.scope
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
return dependents;
|
|
2052
|
+
}
|
|
2053
|
+
function getImpact(graph, symbolId) {
|
|
2054
|
+
if (!graph.hasNode(symbolId)) {
|
|
2055
|
+
return {
|
|
2056
|
+
directDependents: [],
|
|
2057
|
+
transitiveDependents: [],
|
|
2058
|
+
affectedFiles: []
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const directDependents = getDependents(graph, symbolId);
|
|
2062
|
+
const visited = /* @__PURE__ */ new Set([symbolId]);
|
|
2063
|
+
const queue = [symbolId];
|
|
2064
|
+
const allDependents = [];
|
|
2065
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
2066
|
+
while (queue.length > 0) {
|
|
2067
|
+
const current = queue.shift();
|
|
2068
|
+
const neighbors = graph.inNeighbors(current);
|
|
2069
|
+
for (const neighborId of neighbors) {
|
|
2070
|
+
if (!visited.has(neighborId)) {
|
|
2071
|
+
visited.add(neighborId);
|
|
2072
|
+
queue.push(neighborId);
|
|
2073
|
+
const attrs = graph.getNodeAttributes(neighborId);
|
|
2074
|
+
allDependents.push({
|
|
2075
|
+
id: neighborId,
|
|
2076
|
+
name: attrs.name,
|
|
2077
|
+
kind: attrs.kind,
|
|
2078
|
+
filePath: attrs.filePath,
|
|
2079
|
+
startLine: attrs.startLine,
|
|
2080
|
+
endLine: attrs.endLine,
|
|
2081
|
+
exported: attrs.exported,
|
|
2082
|
+
scope: attrs.scope
|
|
2083
|
+
});
|
|
2084
|
+
fileSet.add(attrs.filePath);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
directDependents,
|
|
2090
|
+
transitiveDependents: allDependents,
|
|
2091
|
+
affectedFiles: Array.from(fileSet).sort()
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function getCrossFileEdges(graph) {
|
|
2095
|
+
const crossFileEdges = [];
|
|
2096
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2097
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2098
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2099
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
2100
|
+
crossFileEdges.push({
|
|
2101
|
+
source,
|
|
2102
|
+
target,
|
|
2103
|
+
sourceFile: sourceAttrs.filePath,
|
|
2104
|
+
targetFile: targetAttrs.filePath,
|
|
2105
|
+
kind: attrs.kind
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
return crossFileEdges;
|
|
2110
|
+
}
|
|
2111
|
+
function getFileSummary(graph) {
|
|
2112
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
2113
|
+
graph.forEachNode((node, attrs) => {
|
|
2114
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
2115
|
+
fileMap.set(attrs.filePath, {
|
|
2116
|
+
symbolCount: 0,
|
|
2117
|
+
incomingRefs: /* @__PURE__ */ new Set(),
|
|
2118
|
+
outgoingRefs: /* @__PURE__ */ new Set()
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
fileMap.get(attrs.filePath).symbolCount++;
|
|
2122
|
+
});
|
|
2123
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2124
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2125
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2126
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
2127
|
+
const sourceFile = fileMap.get(sourceAttrs.filePath);
|
|
2128
|
+
const targetFile = fileMap.get(targetAttrs.filePath);
|
|
2129
|
+
if (sourceFile) {
|
|
2130
|
+
sourceFile.outgoingRefs.add(targetAttrs.filePath);
|
|
2131
|
+
}
|
|
2132
|
+
if (targetFile) {
|
|
2133
|
+
targetFile.incomingRefs.add(sourceAttrs.filePath);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
const result = [];
|
|
2138
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
2139
|
+
result.push({
|
|
2140
|
+
filePath,
|
|
2141
|
+
symbolCount: data.symbolCount,
|
|
2142
|
+
incomingRefs: data.incomingRefs.size,
|
|
2143
|
+
outgoingRefs: data.outgoingRefs.size
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
return result.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
2147
|
+
}
|
|
2148
|
+
function searchSymbols(graph, query) {
|
|
2149
|
+
const queryLower = query.toLowerCase();
|
|
2150
|
+
const results = [];
|
|
2151
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
2152
|
+
if (attrs.name.toLowerCase().includes(queryLower)) {
|
|
2153
|
+
results.push({
|
|
2154
|
+
id: nodeId,
|
|
2155
|
+
name: attrs.name,
|
|
2156
|
+
kind: attrs.kind,
|
|
2157
|
+
filePath: attrs.filePath,
|
|
2158
|
+
startLine: attrs.startLine,
|
|
2159
|
+
endLine: attrs.endLine,
|
|
2160
|
+
exported: attrs.exported,
|
|
2161
|
+
scope: attrs.scope
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
return results;
|
|
2166
|
+
}
|
|
2167
|
+
function getArchitectureSummary(graph) {
|
|
2168
|
+
const fileSummary = getFileSummary(graph);
|
|
2169
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
2170
|
+
graph.forEachNode((node, attrs) => {
|
|
2171
|
+
fileSet.add(attrs.filePath);
|
|
2172
|
+
});
|
|
2173
|
+
const fileConnections = fileSummary.map((f) => ({
|
|
2174
|
+
filePath: f.filePath,
|
|
2175
|
+
connections: f.incomingRefs + f.outgoingRefs
|
|
2176
|
+
}));
|
|
2177
|
+
fileConnections.sort((a, b) => b.connections - a.connections);
|
|
2178
|
+
const orphanFiles = fileSummary.filter((f) => f.incomingRefs === 0 && f.outgoingRefs === 0).map((f) => f.filePath);
|
|
2179
|
+
return {
|
|
2180
|
+
fileCount: fileSet.size,
|
|
2181
|
+
symbolCount: graph.order,
|
|
2182
|
+
edgeCount: graph.size,
|
|
2183
|
+
mostConnectedFiles: fileConnections.slice(0, 5),
|
|
2184
|
+
orphanFiles
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/viz/data.ts
|
|
2189
|
+
import { basename } from "path";
|
|
2190
|
+
function prepareVizData(graph, projectRoot) {
|
|
2191
|
+
const fileSummary = getFileSummary(graph);
|
|
2192
|
+
const crossFileEdges = getCrossFileEdges(graph);
|
|
2193
|
+
const files = fileSummary.map((f) => ({
|
|
2194
|
+
path: f.filePath,
|
|
2195
|
+
directory: f.filePath.includes("/") ? f.filePath.substring(0, f.filePath.lastIndexOf("/")) : ".",
|
|
2196
|
+
symbolCount: f.symbolCount,
|
|
2197
|
+
incomingCount: f.incomingRefs,
|
|
2198
|
+
outgoingCount: f.outgoingRefs
|
|
2199
|
+
}));
|
|
2200
|
+
files.sort((a, b) => {
|
|
2201
|
+
if (a.directory !== b.directory) {
|
|
2202
|
+
return a.directory.localeCompare(b.directory);
|
|
2203
|
+
}
|
|
2204
|
+
return a.path.localeCompare(b.path);
|
|
2205
|
+
});
|
|
2206
|
+
const arcMap = /* @__PURE__ */ new Map();
|
|
2207
|
+
for (const edge of crossFileEdges) {
|
|
2208
|
+
const key = `${edge.sourceFile}::${edge.targetFile}`;
|
|
2209
|
+
if (arcMap.has(key)) {
|
|
2210
|
+
const arc = arcMap.get(key);
|
|
2211
|
+
arc.edgeCount++;
|
|
2212
|
+
if (!arc.edgeKinds.includes(edge.kind)) {
|
|
2213
|
+
arc.edgeKinds.push(edge.kind);
|
|
2214
|
+
}
|
|
2215
|
+
} else {
|
|
2216
|
+
arcMap.set(key, {
|
|
2217
|
+
sourceFile: edge.sourceFile,
|
|
2218
|
+
targetFile: edge.targetFile,
|
|
2219
|
+
edgeCount: 1,
|
|
2220
|
+
edgeKinds: [edge.kind]
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const arcs = Array.from(arcMap.values());
|
|
2225
|
+
const projectName = basename(projectRoot);
|
|
2226
|
+
return {
|
|
2227
|
+
files,
|
|
2228
|
+
arcs,
|
|
2229
|
+
stats: {
|
|
2230
|
+
totalFiles: files.length,
|
|
2231
|
+
totalSymbols: graph.order,
|
|
2232
|
+
totalEdges: graph.size,
|
|
2233
|
+
totalCrossFileEdges: arcs.reduce((sum, arc) => sum + arc.edgeCount, 0)
|
|
2234
|
+
},
|
|
2235
|
+
projectName
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// src/watcher.ts
|
|
2240
|
+
import chokidar from "chokidar";
|
|
2241
|
+
function watchProject(projectRoot, callbacks) {
|
|
2242
|
+
console.error(`[Watcher] Creating watcher for: ${projectRoot}`);
|
|
2243
|
+
const watcher = chokidar.watch(projectRoot, {
|
|
2244
|
+
ignored: [
|
|
2245
|
+
"**/node_modules/**",
|
|
2246
|
+
"**/vendor/**",
|
|
2247
|
+
// Go dependencies
|
|
2248
|
+
"**/.git/**",
|
|
2249
|
+
"**/dist/**",
|
|
2250
|
+
"**/build/**",
|
|
2251
|
+
"**/coverage/**",
|
|
2252
|
+
"**/.next/**",
|
|
2253
|
+
"**/.turbo/**",
|
|
2254
|
+
"**/.*"
|
|
2255
|
+
// Hidden files and directories
|
|
2256
|
+
],
|
|
2257
|
+
ignoreInitial: true,
|
|
2258
|
+
// Don't fire events for existing files
|
|
2259
|
+
persistent: true,
|
|
2260
|
+
followSymlinks: false,
|
|
2261
|
+
awaitWriteFinish: {
|
|
2262
|
+
stabilityThreshold: 300,
|
|
2263
|
+
// Wait 300ms after last change before firing
|
|
2264
|
+
pollInterval: 100
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
console.error("[Watcher] Attaching event listeners...");
|
|
2268
|
+
watcher.on("change", (absolutePath) => {
|
|
2269
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2270
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2271
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
2272
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
2273
|
+
console.error(`[Watcher] Change event: ${relativePath}`);
|
|
2274
|
+
callbacks.onFileChanged(relativePath);
|
|
2275
|
+
});
|
|
2276
|
+
watcher.on("add", (absolutePath) => {
|
|
2277
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2278
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2279
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
2280
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
2281
|
+
console.error(`[Watcher] Add event: ${relativePath}`);
|
|
2282
|
+
callbacks.onFileAdded(relativePath);
|
|
2283
|
+
});
|
|
2284
|
+
watcher.on("unlink", (absolutePath) => {
|
|
2285
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2286
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2287
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
2288
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
2289
|
+
console.error(`[Watcher] Unlink event: ${relativePath}`);
|
|
2290
|
+
callbacks.onFileDeleted(relativePath);
|
|
2291
|
+
});
|
|
2292
|
+
watcher.on("error", (error) => {
|
|
2293
|
+
console.error("[Watcher] Error:", error);
|
|
2294
|
+
});
|
|
2295
|
+
watcher.on("ready", () => {
|
|
2296
|
+
console.error("[Watcher] Ready \u2014 watching for changes");
|
|
2297
|
+
const watched = watcher.getWatched();
|
|
2298
|
+
const dirs = Object.keys(watched);
|
|
2299
|
+
let fileCount = 0;
|
|
2300
|
+
for (const dir of dirs) {
|
|
2301
|
+
const files = watched[dir];
|
|
2302
|
+
fileCount += files.filter(
|
|
2303
|
+
(f) => f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx") || f.endsWith(".mjs") || f.endsWith(".cjs") || f.endsWith(".py") || f.endsWith(".go") && !f.endsWith("_test.go")
|
|
2304
|
+
).length;
|
|
2305
|
+
}
|
|
2306
|
+
console.error(`[Watcher] Watching ${fileCount} TypeScript/JavaScript/Python/Go files in ${dirs.length} directories`);
|
|
2307
|
+
});
|
|
2308
|
+
watcher.on("all", (event, path) => {
|
|
2309
|
+
console.error(`[Watcher] ALL event: ${event} ${path}`);
|
|
2310
|
+
});
|
|
2311
|
+
return watcher;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/viz/server.ts
|
|
2315
|
+
import express from "express";
|
|
2316
|
+
import open from "open";
|
|
2317
|
+
import { fileURLToPath } from "url";
|
|
2318
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2319
|
+
import { WebSocketServer } from "ws";
|
|
2320
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2321
|
+
var __dirname = dirname5(__filename);
|
|
2322
|
+
var activeServer = null;
|
|
2323
|
+
function startVizServer(initialVizData, graph, projectRoot, port = 3333, shouldOpen = true) {
|
|
2324
|
+
if (activeServer) {
|
|
2325
|
+
console.error(`Visualization server already running at ${activeServer.url}`);
|
|
2326
|
+
return {
|
|
2327
|
+
server: activeServer.server,
|
|
2328
|
+
url: activeServer.url,
|
|
2329
|
+
alreadyRunning: true
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
const app = express();
|
|
2333
|
+
let vizData = initialVizData;
|
|
2334
|
+
const publicDir = join7(__dirname, "viz", "public");
|
|
2335
|
+
app.use(express.static(publicDir));
|
|
2336
|
+
app.get("/api/graph", (req, res) => {
|
|
2337
|
+
res.json(vizData);
|
|
2338
|
+
});
|
|
2339
|
+
const server = app.listen(port, "127.0.0.1", () => {
|
|
2340
|
+
const url2 = `http://127.0.0.1:${port}`;
|
|
2341
|
+
console.error(`
|
|
2342
|
+
Depwire visualization running at ${url2}`);
|
|
2343
|
+
console.error("Press Ctrl+C to stop\n");
|
|
2344
|
+
activeServer = { server, port, url: url2 };
|
|
2345
|
+
if (shouldOpen) {
|
|
2346
|
+
open(url2);
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
const wss = new WebSocketServer({ server });
|
|
2350
|
+
wss.on("connection", (ws) => {
|
|
2351
|
+
console.error("Browser connected to WebSocket");
|
|
2352
|
+
ws.on("close", () => {
|
|
2353
|
+
console.error("Browser disconnected from WebSocket");
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
function broadcastRefresh() {
|
|
2357
|
+
wss.clients.forEach((client) => {
|
|
2358
|
+
if (client.readyState === 1) {
|
|
2359
|
+
client.send(JSON.stringify({ type: "refresh" }));
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
console.error("Starting file watcher...");
|
|
2364
|
+
const watcher = watchProject(projectRoot, {
|
|
2365
|
+
onFileChanged: async (filePath) => {
|
|
2366
|
+
console.error(`File changed: ${filePath} \u2014 re-parsing project...`);
|
|
2367
|
+
try {
|
|
2368
|
+
const parsedFiles = parseProject(projectRoot);
|
|
2369
|
+
const newGraph = buildGraph(parsedFiles);
|
|
2370
|
+
graph.clear();
|
|
2371
|
+
newGraph.forEachNode((node, attrs) => {
|
|
2372
|
+
graph.addNode(node, attrs);
|
|
2373
|
+
});
|
|
2374
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
2375
|
+
graph.addEdge(source, target, attrs);
|
|
2376
|
+
});
|
|
2377
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
2378
|
+
broadcastRefresh();
|
|
2379
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
console.error(`Failed to update graph for ${filePath}:`, error);
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
onFileAdded: async (filePath) => {
|
|
2385
|
+
console.error(`File added: ${filePath} \u2014 re-parsing project...`);
|
|
2386
|
+
try {
|
|
2387
|
+
const parsedFiles = parseProject(projectRoot);
|
|
2388
|
+
const newGraph = buildGraph(parsedFiles);
|
|
2389
|
+
graph.clear();
|
|
2390
|
+
newGraph.forEachNode((node, attrs) => {
|
|
2391
|
+
graph.addNode(node, attrs);
|
|
2392
|
+
});
|
|
2393
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
2394
|
+
graph.addEdge(source, target, attrs);
|
|
2395
|
+
});
|
|
2396
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
2397
|
+
broadcastRefresh();
|
|
2398
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
console.error(`Failed to update graph for ${filePath}:`, error);
|
|
2401
|
+
}
|
|
2402
|
+
},
|
|
2403
|
+
onFileDeleted: (filePath) => {
|
|
2404
|
+
console.error(`File deleted: ${filePath} \u2014 re-parsing project...`);
|
|
2405
|
+
try {
|
|
2406
|
+
const parsedFiles = parseProject(projectRoot);
|
|
2407
|
+
const newGraph = buildGraph(parsedFiles);
|
|
2408
|
+
graph.clear();
|
|
2409
|
+
newGraph.forEachNode((node, attrs) => {
|
|
2410
|
+
graph.addNode(node, attrs);
|
|
2411
|
+
});
|
|
2412
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
2413
|
+
graph.addEdge(source, target, attrs);
|
|
2414
|
+
});
|
|
2415
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
2416
|
+
broadcastRefresh();
|
|
2417
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
console.error(`Failed to remove ${filePath} from graph:`, error);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
process.on("SIGINT", () => {
|
|
2424
|
+
console.error("\nShutting down visualization server...");
|
|
2425
|
+
activeServer = null;
|
|
2426
|
+
watcher.close();
|
|
2427
|
+
wss.close();
|
|
2428
|
+
server.close(() => {
|
|
2429
|
+
process.exit(0);
|
|
2430
|
+
});
|
|
2431
|
+
});
|
|
2432
|
+
const url = `http://127.0.0.1:${port}`;
|
|
2433
|
+
return { server, url, alreadyRunning: false };
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/mcp/state.ts
|
|
2437
|
+
function createEmptyState() {
|
|
2438
|
+
return {
|
|
2439
|
+
graph: null,
|
|
2440
|
+
projectRoot: null,
|
|
2441
|
+
projectName: null,
|
|
2442
|
+
watcher: null
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
function isProjectLoaded(state) {
|
|
2446
|
+
return state.graph !== null && state.projectRoot !== null;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// src/graph/updater.ts
|
|
2450
|
+
import { join as join8 } from "path";
|
|
2451
|
+
function removeFileFromGraph(graph, filePath) {
|
|
2452
|
+
const nodesToRemove = [];
|
|
2453
|
+
graph.forEachNode((node, attrs) => {
|
|
2454
|
+
if (attrs.filePath === filePath) {
|
|
2455
|
+
nodesToRemove.push(node);
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
nodesToRemove.forEach((node) => {
|
|
2459
|
+
try {
|
|
2460
|
+
graph.dropNode(node);
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
function addFileToGraph(graph, parsedFile) {
|
|
2466
|
+
for (const symbol of parsedFile.symbols) {
|
|
2467
|
+
const nodeId = `${parsedFile.filePath}::${symbol.name}`;
|
|
2468
|
+
try {
|
|
2469
|
+
graph.addNode(nodeId, {
|
|
2470
|
+
name: symbol.name,
|
|
2471
|
+
kind: symbol.kind,
|
|
2472
|
+
filePath: parsedFile.filePath,
|
|
2473
|
+
startLine: symbol.location.startLine,
|
|
2474
|
+
endLine: symbol.location.endLine,
|
|
2475
|
+
exported: symbol.exported,
|
|
2476
|
+
scope: symbol.scope
|
|
2477
|
+
});
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
for (const edge of parsedFile.edges) {
|
|
2482
|
+
try {
|
|
2483
|
+
graph.mergeEdge(edge.source, edge.target, {
|
|
2484
|
+
kind: edge.kind,
|
|
2485
|
+
sourceFile: edge.sourceFile,
|
|
2486
|
+
targetFile: edge.targetFile
|
|
2487
|
+
});
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
2493
|
+
removeFileFromGraph(graph, relativeFilePath);
|
|
2494
|
+
const absolutePath = join8(projectRoot, relativeFilePath);
|
|
2495
|
+
try {
|
|
2496
|
+
const parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
|
|
2497
|
+
addFileToGraph(graph, parsedFile);
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
console.error(`Failed to parse file ${relativeFilePath}:`, error);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/mcp/server.ts
|
|
2504
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2505
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2506
|
+
|
|
2507
|
+
// src/mcp/tools.ts
|
|
2508
|
+
import { dirname as dirname6 } from "path";
|
|
2509
|
+
|
|
2510
|
+
// src/mcp/connect.ts
|
|
2511
|
+
import simpleGit from "simple-git";
|
|
2512
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2513
|
+
import { join as join9, basename as basename2, resolve as resolve2 } from "path";
|
|
2514
|
+
import { tmpdir, homedir } from "os";
|
|
2515
|
+
function validateProjectPath(source) {
|
|
2516
|
+
const resolved = resolve2(source);
|
|
2517
|
+
const blockedPaths = [
|
|
2518
|
+
"/etc",
|
|
2519
|
+
"/var",
|
|
2520
|
+
"/usr",
|
|
2521
|
+
"/bin",
|
|
2522
|
+
"/sbin",
|
|
2523
|
+
"/boot",
|
|
2524
|
+
"/proc",
|
|
2525
|
+
"/sys",
|
|
2526
|
+
join9(homedir(), ".ssh"),
|
|
2527
|
+
join9(homedir(), ".gnupg"),
|
|
2528
|
+
join9(homedir(), ".aws"),
|
|
2529
|
+
join9(homedir(), ".config"),
|
|
2530
|
+
join9(homedir(), ".env")
|
|
2531
|
+
];
|
|
2532
|
+
for (const blocked of blockedPaths) {
|
|
2533
|
+
if (resolved.startsWith(blocked)) {
|
|
2534
|
+
return { valid: false, error: `Access denied: ${blocked} is a protected path` };
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return { valid: true };
|
|
2538
|
+
}
|
|
2539
|
+
async function connectToRepo(source, subdirectory, state) {
|
|
2540
|
+
try {
|
|
2541
|
+
let projectRoot;
|
|
2542
|
+
let projectName;
|
|
2543
|
+
const isGitHub = source.startsWith("https://github.com/") || source.startsWith("git@github.com:");
|
|
2544
|
+
if (isGitHub) {
|
|
2545
|
+
const match = source.match(/[\/:]([^\/]+?)(?:\.git)?$/);
|
|
2546
|
+
if (!match) {
|
|
2547
|
+
return {
|
|
2548
|
+
error: "Invalid GitHub URL",
|
|
2549
|
+
message: "Could not parse repository name from URL"
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
projectName = match[1];
|
|
2553
|
+
const reposDir = join9(tmpdir(), "depwire-repos");
|
|
2554
|
+
const cloneDir = join9(reposDir, projectName);
|
|
2555
|
+
console.error(`Connecting to GitHub repo: ${source}`);
|
|
2556
|
+
const git = simpleGit();
|
|
2557
|
+
if (existsSync5(cloneDir)) {
|
|
2558
|
+
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
2559
|
+
try {
|
|
2560
|
+
await git.cwd(cloneDir).pull();
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
console.error(`Pull failed, using existing clone: ${error}`);
|
|
2563
|
+
}
|
|
2564
|
+
} else {
|
|
2565
|
+
console.error(`Cloning ${source} to ${cloneDir}...`);
|
|
2566
|
+
try {
|
|
2567
|
+
await git.clone(source, cloneDir, ["--depth", "1", "--no-recurse-submodules", "--single-branch"]);
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
return {
|
|
2570
|
+
error: "Failed to clone repository",
|
|
2571
|
+
message: `Git clone failed: ${error}. Ensure git is installed and the URL is correct.`
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
projectRoot = subdirectory ? join9(cloneDir, subdirectory) : cloneDir;
|
|
2576
|
+
} else {
|
|
2577
|
+
const validation2 = validateProjectPath(source);
|
|
2578
|
+
if (!validation2.valid) {
|
|
2579
|
+
return {
|
|
2580
|
+
error: "Access denied",
|
|
2581
|
+
message: validation2.error
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
if (!existsSync5(source)) {
|
|
2585
|
+
return {
|
|
2586
|
+
error: "Directory not found",
|
|
2587
|
+
message: `Directory does not exist: ${source}`
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
projectRoot = subdirectory ? join9(source, subdirectory) : source;
|
|
2591
|
+
projectName = basename2(projectRoot);
|
|
2592
|
+
}
|
|
2593
|
+
const validation = validateProjectPath(projectRoot);
|
|
2594
|
+
if (!validation.valid) {
|
|
2595
|
+
return {
|
|
2596
|
+
error: "Access denied",
|
|
2597
|
+
message: validation.error
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
if (!existsSync5(projectRoot)) {
|
|
2601
|
+
return {
|
|
2602
|
+
error: "Project root not found",
|
|
2603
|
+
message: `Directory does not exist: ${projectRoot}`
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
console.error(`Parsing project at ${projectRoot}...`);
|
|
2607
|
+
if (state.watcher) {
|
|
2608
|
+
console.error("Stopping previous file watcher...");
|
|
2609
|
+
await state.watcher.close();
|
|
2610
|
+
state.watcher = null;
|
|
2611
|
+
}
|
|
2612
|
+
const parsedFiles = await parseProject(projectRoot);
|
|
2613
|
+
if (parsedFiles.length === 0) {
|
|
2614
|
+
return {
|
|
2615
|
+
error: "No source files found",
|
|
2616
|
+
message: `No supported source files (.ts, .tsx, .js, .jsx, .py, .go) found in ${projectRoot}`
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
const graph = buildGraph(parsedFiles);
|
|
2620
|
+
state.graph = graph;
|
|
2621
|
+
state.projectRoot = projectRoot;
|
|
2622
|
+
state.projectName = projectName;
|
|
2623
|
+
console.error(`Parsed ${parsedFiles.length} files`);
|
|
2624
|
+
console.error("Starting file watcher...");
|
|
2625
|
+
state.watcher = watchProject(projectRoot, {
|
|
2626
|
+
onFileChanged: async (filePath) => {
|
|
2627
|
+
console.error(`File changed: ${filePath}`);
|
|
2628
|
+
try {
|
|
2629
|
+
await updateFileInGraph(state.graph, projectRoot, filePath);
|
|
2630
|
+
console.error(`Graph updated for ${filePath}`);
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
console.error(`Failed to update graph for ${filePath}: ${error}`);
|
|
2633
|
+
}
|
|
2634
|
+
},
|
|
2635
|
+
onFileAdded: async (filePath) => {
|
|
2636
|
+
console.error(`File added: ${filePath}`);
|
|
2637
|
+
try {
|
|
2638
|
+
await updateFileInGraph(state.graph, projectRoot, filePath);
|
|
2639
|
+
console.error(`Graph updated for ${filePath}`);
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
console.error(`Failed to update graph for ${filePath}: ${error}`);
|
|
2642
|
+
}
|
|
2643
|
+
},
|
|
2644
|
+
onFileDeleted: (filePath) => {
|
|
2645
|
+
console.error(`File deleted: ${filePath}`);
|
|
2646
|
+
try {
|
|
2647
|
+
const fileNodes = state.graph.filterNodes(
|
|
2648
|
+
(node, attrs) => attrs.filePath === filePath
|
|
2649
|
+
);
|
|
2650
|
+
fileNodes.forEach((node) => state.graph.dropNode(node));
|
|
2651
|
+
console.error(`Removed ${filePath} from graph`);
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
console.error(`Failed to remove ${filePath} from graph: ${error}`);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
const summary = getArchitectureSummary(graph);
|
|
2658
|
+
const mostConnected = summary.mostConnectedFiles.slice(0, 3);
|
|
2659
|
+
const languageBreakdown = {};
|
|
2660
|
+
parsedFiles.forEach((file) => {
|
|
2661
|
+
const ext = file.filePath.toLowerCase();
|
|
2662
|
+
let lang;
|
|
2663
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
2664
|
+
lang = "typescript";
|
|
2665
|
+
} else if (ext.endsWith(".py")) {
|
|
2666
|
+
lang = "python";
|
|
2667
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
2668
|
+
lang = "javascript";
|
|
2669
|
+
} else if (ext.endsWith(".go")) {
|
|
2670
|
+
lang = "go";
|
|
2671
|
+
} else {
|
|
2672
|
+
lang = "other";
|
|
2673
|
+
}
|
|
2674
|
+
languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
|
|
2675
|
+
});
|
|
2676
|
+
return {
|
|
2677
|
+
connected: true,
|
|
2678
|
+
projectRoot,
|
|
2679
|
+
projectName,
|
|
2680
|
+
stats: {
|
|
2681
|
+
files: summary.totalFiles,
|
|
2682
|
+
symbols: summary.totalSymbols,
|
|
2683
|
+
edges: summary.totalEdges,
|
|
2684
|
+
crossFileEdges: summary.crossFileEdges,
|
|
2685
|
+
languages: languageBreakdown
|
|
2686
|
+
},
|
|
2687
|
+
mostConnectedFiles: mostConnected.map((f) => ({
|
|
2688
|
+
path: f.filePath,
|
|
2689
|
+
connections: f.incomingCount + f.outgoingCount
|
|
2690
|
+
})),
|
|
2691
|
+
summary: `Connected to ${projectName}. Found ${summary.totalFiles} files with ${summary.totalSymbols} symbols and ${summary.crossFileEdges} cross-file edges.`
|
|
2692
|
+
};
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
console.error("Error in connectToRepo:", error);
|
|
2695
|
+
return {
|
|
2696
|
+
error: "Connection failed",
|
|
2697
|
+
message: String(error)
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/mcp/tools.ts
|
|
2703
|
+
function getToolsList() {
|
|
2704
|
+
return [
|
|
2705
|
+
{
|
|
2706
|
+
name: "connect_repo",
|
|
2707
|
+
description: "Connect Depwire to a codebase for analysis. Accepts a local directory path or a GitHub repository URL. If a GitHub URL is provided, the repo will be cloned automatically. This replaces the currently loaded project.",
|
|
2708
|
+
inputSchema: {
|
|
2709
|
+
type: "object",
|
|
2710
|
+
properties: {
|
|
2711
|
+
source: {
|
|
2712
|
+
type: "string",
|
|
2713
|
+
description: "Local directory path (e.g., '/Users/me/project') or GitHub URL (e.g., 'https://github.com/vercel/next.js')"
|
|
2714
|
+
},
|
|
2715
|
+
subdirectory: {
|
|
2716
|
+
type: "string",
|
|
2717
|
+
description: "Subdirectory within the repo to analyze (optional, e.g., 'packages/core/src')"
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
required: ["source"]
|
|
2721
|
+
}
|
|
2722
|
+
},
|
|
2723
|
+
{
|
|
2724
|
+
name: "get_symbol_info",
|
|
2725
|
+
description: "Look up detailed information about a symbol (function, class, variable, type, etc.) by name. Returns file location, type, line numbers, and export status.",
|
|
2726
|
+
inputSchema: {
|
|
2727
|
+
type: "object",
|
|
2728
|
+
properties: {
|
|
2729
|
+
name: {
|
|
2730
|
+
type: "string",
|
|
2731
|
+
description: "The symbol name to look up (e.g., 'UserService', 'handleAuth')"
|
|
2732
|
+
}
|
|
2733
|
+
},
|
|
2734
|
+
required: ["name"]
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
name: "get_dependencies",
|
|
2739
|
+
description: "Get all symbols that a given symbol depends on (what does this symbol use/import/call?).",
|
|
2740
|
+
inputSchema: {
|
|
2741
|
+
type: "object",
|
|
2742
|
+
properties: {
|
|
2743
|
+
symbol: {
|
|
2744
|
+
type: "string",
|
|
2745
|
+
description: "Symbol name or ID to analyze"
|
|
2746
|
+
}
|
|
2747
|
+
},
|
|
2748
|
+
required: ["symbol"]
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: "get_dependents",
|
|
2753
|
+
description: "Get all symbols that depend on a given symbol (what uses this symbol?).",
|
|
2754
|
+
inputSchema: {
|
|
2755
|
+
type: "object",
|
|
2756
|
+
properties: {
|
|
2757
|
+
symbol: {
|
|
2758
|
+
type: "string",
|
|
2759
|
+
description: "Symbol name or ID to analyze"
|
|
2760
|
+
}
|
|
2761
|
+
},
|
|
2762
|
+
required: ["symbol"]
|
|
2763
|
+
}
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
name: "impact_analysis",
|
|
2767
|
+
description: "Analyze what would break if a symbol is changed, renamed, or removed. Shows direct dependents, transitive dependents (chain reaction), and all affected files. Use this before making changes to understand the blast radius.",
|
|
2768
|
+
inputSchema: {
|
|
2769
|
+
type: "object",
|
|
2770
|
+
properties: {
|
|
2771
|
+
symbol: {
|
|
2772
|
+
type: "string",
|
|
2773
|
+
description: "The symbol name or ID to analyze"
|
|
2774
|
+
}
|
|
2775
|
+
},
|
|
2776
|
+
required: ["symbol"]
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
name: "get_file_context",
|
|
2781
|
+
description: "Get complete context about a file \u2014 all symbols defined in it, all imports, all exports, and all files that import from it.",
|
|
2782
|
+
inputSchema: {
|
|
2783
|
+
type: "object",
|
|
2784
|
+
properties: {
|
|
2785
|
+
filePath: {
|
|
2786
|
+
type: "string",
|
|
2787
|
+
description: "Relative file path (e.g., 'services/UserService.ts')"
|
|
2788
|
+
}
|
|
2789
|
+
},
|
|
2790
|
+
required: ["filePath"]
|
|
2791
|
+
}
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
name: "search_symbols",
|
|
2795
|
+
description: "Search for symbols by name across the entire codebase. Supports partial matching.",
|
|
2796
|
+
inputSchema: {
|
|
2797
|
+
type: "object",
|
|
2798
|
+
properties: {
|
|
2799
|
+
query: {
|
|
2800
|
+
type: "string",
|
|
2801
|
+
description: "Search query (case-insensitive substring match)"
|
|
2802
|
+
},
|
|
2803
|
+
limit: {
|
|
2804
|
+
type: "number",
|
|
2805
|
+
description: "Maximum results to return (default: 20)"
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
required: ["query"]
|
|
2809
|
+
}
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: "get_architecture_summary",
|
|
2813
|
+
description: "Get a high-level overview of the project's architecture \u2014 file count, symbol count, most connected files, dependency hotspots, and orphan files.",
|
|
2814
|
+
inputSchema: {
|
|
2815
|
+
type: "object",
|
|
2816
|
+
properties: {}
|
|
2817
|
+
}
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: "list_files",
|
|
2821
|
+
description: "List all files in the project with basic stats.",
|
|
2822
|
+
inputSchema: {
|
|
2823
|
+
type: "object",
|
|
2824
|
+
properties: {
|
|
2825
|
+
directory: {
|
|
2826
|
+
type: "string",
|
|
2827
|
+
description: "Filter to a specific subdirectory (optional)"
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
name: "visualize_graph",
|
|
2834
|
+
description: "Render an interactive arc diagram visualization of the current codebase's cross-reference graph. Shows files as bars along the bottom and dependency arcs connecting them, colored by distance. The visualization appears inline in the conversation.",
|
|
2835
|
+
inputSchema: {
|
|
2836
|
+
type: "object",
|
|
2837
|
+
properties: {
|
|
2838
|
+
highlight: {
|
|
2839
|
+
type: "string",
|
|
2840
|
+
description: "File or symbol name to highlight in the visualization (optional)"
|
|
2841
|
+
},
|
|
2842
|
+
maxFiles: {
|
|
2843
|
+
type: "number",
|
|
2844
|
+
description: "Limit to top N most connected files (optional, default: all)"
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
];
|
|
2850
|
+
}
|
|
2851
|
+
async function handleToolCall(name, args, state) {
|
|
2852
|
+
try {
|
|
2853
|
+
let result;
|
|
2854
|
+
if (name === "connect_repo") {
|
|
2855
|
+
result = await connectToRepo(args.source, args.subdirectory, state);
|
|
2856
|
+
} else if (name === "get_architecture_summary") {
|
|
2857
|
+
if (!isProjectLoaded(state)) {
|
|
2858
|
+
result = {
|
|
2859
|
+
status: "no_project",
|
|
2860
|
+
message: "No project loaded. Use connect_repo to analyze a codebase."
|
|
2861
|
+
};
|
|
2862
|
+
} else {
|
|
2863
|
+
result = handleGetArchitectureSummary(state.graph);
|
|
2864
|
+
}
|
|
2865
|
+
} else if (name === "visualize_graph") {
|
|
2866
|
+
if (!isProjectLoaded(state)) {
|
|
2867
|
+
result = {
|
|
2868
|
+
error: "No project loaded",
|
|
2869
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
2870
|
+
};
|
|
2871
|
+
} else {
|
|
2872
|
+
result = await handleVisualizeGraph(args.highlight, args.maxFiles, state);
|
|
2873
|
+
}
|
|
2874
|
+
} else {
|
|
2875
|
+
if (!isProjectLoaded(state)) {
|
|
2876
|
+
result = {
|
|
2877
|
+
error: "No project loaded",
|
|
2878
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
2879
|
+
};
|
|
2880
|
+
} else {
|
|
2881
|
+
const graph = state.graph;
|
|
2882
|
+
switch (name) {
|
|
2883
|
+
case "get_symbol_info":
|
|
2884
|
+
result = handleGetSymbolInfo(args.name, graph);
|
|
2885
|
+
break;
|
|
2886
|
+
case "get_dependencies":
|
|
2887
|
+
result = handleGetDependencies(args.symbol, graph);
|
|
2888
|
+
break;
|
|
2889
|
+
case "get_dependents":
|
|
2890
|
+
result = handleGetDependents(args.symbol, graph);
|
|
2891
|
+
break;
|
|
2892
|
+
case "impact_analysis":
|
|
2893
|
+
result = handleImpactAnalysis(args.symbol, graph);
|
|
2894
|
+
break;
|
|
2895
|
+
case "get_file_context":
|
|
2896
|
+
result = handleGetFileContext(args.filePath, graph);
|
|
2897
|
+
break;
|
|
2898
|
+
case "search_symbols":
|
|
2899
|
+
result = handleSearchSymbols(args.query, args.limit || 20, graph);
|
|
2900
|
+
break;
|
|
2901
|
+
case "list_files":
|
|
2902
|
+
result = handleListFiles(args.directory, graph);
|
|
2903
|
+
break;
|
|
2904
|
+
default:
|
|
2905
|
+
result = { error: `Unknown tool: ${name}` };
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (result && typeof result === "object" && "_mcpAppResponse" in result) {
|
|
2910
|
+
const appResult = result;
|
|
2911
|
+
return {
|
|
2912
|
+
content: [
|
|
2913
|
+
{
|
|
2914
|
+
type: "text",
|
|
2915
|
+
text: appResult.text
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
type: "resource",
|
|
2919
|
+
resource: {
|
|
2920
|
+
uri: "ui://depwire/arc-diagram",
|
|
2921
|
+
mimeType: "text/html;profile=mcp-app",
|
|
2922
|
+
text: appResult.html
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
]
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
if (result && typeof result === "object" && "content" in result && Array.isArray(result.content)) {
|
|
2929
|
+
return result;
|
|
2930
|
+
}
|
|
2931
|
+
return {
|
|
2932
|
+
content: [
|
|
2933
|
+
{
|
|
2934
|
+
type: "text",
|
|
2935
|
+
text: JSON.stringify(result, null, 2)
|
|
2936
|
+
}
|
|
2937
|
+
]
|
|
2938
|
+
};
|
|
2939
|
+
} catch (error) {
|
|
2940
|
+
console.error("Error handling tool call:", error);
|
|
2941
|
+
return {
|
|
2942
|
+
content: [
|
|
2943
|
+
{
|
|
2944
|
+
type: "text",
|
|
2945
|
+
text: JSON.stringify({ error: String(error) }, null, 2)
|
|
2946
|
+
}
|
|
2947
|
+
]
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function handleGetSymbolInfo(name, graph) {
|
|
2952
|
+
const matches = searchSymbols(graph, name);
|
|
2953
|
+
const exactMatches = matches.filter((m) => m.name.toLowerCase() === name.toLowerCase());
|
|
2954
|
+
const results = exactMatches.length > 0 ? exactMatches : matches.slice(0, 10);
|
|
2955
|
+
return {
|
|
2956
|
+
matches: results.map((m) => ({
|
|
2957
|
+
id: m.id,
|
|
2958
|
+
name: m.name,
|
|
2959
|
+
kind: m.kind,
|
|
2960
|
+
filePath: m.filePath,
|
|
2961
|
+
startLine: m.startLine,
|
|
2962
|
+
endLine: m.endLine,
|
|
2963
|
+
exported: m.exported,
|
|
2964
|
+
scope: m.scope
|
|
2965
|
+
})),
|
|
2966
|
+
count: results.length
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function handleGetDependencies(symbol, graph) {
|
|
2970
|
+
const matches = searchSymbols(graph, symbol);
|
|
2971
|
+
if (matches.length === 0) {
|
|
2972
|
+
return {
|
|
2973
|
+
error: `Symbol '${symbol}' not found`,
|
|
2974
|
+
suggestion: "Try using search_symbols to find available symbols"
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
const target = matches[0];
|
|
2978
|
+
const deps = getDependencies(graph, target.id);
|
|
2979
|
+
const grouped = {};
|
|
2980
|
+
graph.forEachOutEdge(target.id, (edge, attrs, source, targetNode) => {
|
|
2981
|
+
const kind = attrs.kind;
|
|
2982
|
+
if (!grouped[kind]) {
|
|
2983
|
+
grouped[kind] = [];
|
|
2984
|
+
}
|
|
2985
|
+
const targetAttrs = graph.getNodeAttributes(targetNode);
|
|
2986
|
+
grouped[kind].push({
|
|
2987
|
+
name: targetAttrs.name,
|
|
2988
|
+
filePath: targetAttrs.filePath,
|
|
2989
|
+
kind: targetAttrs.kind
|
|
2990
|
+
});
|
|
2991
|
+
});
|
|
2992
|
+
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
2993
|
+
return {
|
|
2994
|
+
symbol: `${target.filePath}::${target.name}`,
|
|
2995
|
+
dependencies: grouped,
|
|
2996
|
+
totalCount
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
function handleGetDependents(symbol, graph) {
|
|
3000
|
+
const matches = searchSymbols(graph, symbol);
|
|
3001
|
+
if (matches.length === 0) {
|
|
3002
|
+
return {
|
|
3003
|
+
error: `Symbol '${symbol}' not found`,
|
|
3004
|
+
suggestion: "Try using search_symbols to find available symbols"
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
const target = matches[0];
|
|
3008
|
+
const deps = getDependents(graph, target.id);
|
|
3009
|
+
const grouped = {};
|
|
3010
|
+
graph.forEachInEdge(target.id, (edge, attrs, source, targetNode) => {
|
|
3011
|
+
const kind = attrs.kind;
|
|
3012
|
+
if (!grouped[kind]) {
|
|
3013
|
+
grouped[kind] = [];
|
|
3014
|
+
}
|
|
3015
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3016
|
+
grouped[kind].push({
|
|
3017
|
+
name: sourceAttrs.name,
|
|
3018
|
+
filePath: sourceAttrs.filePath,
|
|
3019
|
+
kind: sourceAttrs.kind
|
|
3020
|
+
});
|
|
3021
|
+
});
|
|
3022
|
+
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
3023
|
+
return {
|
|
3024
|
+
symbol: `${target.filePath}::${target.name}`,
|
|
3025
|
+
dependents: grouped,
|
|
3026
|
+
totalCount
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
function handleImpactAnalysis(symbol, graph) {
|
|
3030
|
+
const matches = searchSymbols(graph, symbol);
|
|
3031
|
+
if (matches.length === 0) {
|
|
3032
|
+
return {
|
|
3033
|
+
error: `Symbol '${symbol}' not found`,
|
|
3034
|
+
suggestion: "Try using search_symbols to find available symbols"
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
const target = matches[0];
|
|
3038
|
+
const impact = getImpact(graph, target.id);
|
|
3039
|
+
const directWithKinds = impact.directDependents.map((dep) => {
|
|
3040
|
+
let relationship = "unknown";
|
|
3041
|
+
graph.forEachEdge(dep.id, target.id, (edge, attrs) => {
|
|
3042
|
+
relationship = attrs.kind;
|
|
3043
|
+
});
|
|
3044
|
+
return {
|
|
3045
|
+
name: dep.name,
|
|
3046
|
+
filePath: dep.filePath,
|
|
3047
|
+
kind: dep.kind,
|
|
3048
|
+
relationship
|
|
3049
|
+
};
|
|
3050
|
+
});
|
|
3051
|
+
const transitiveFormatted = impact.transitiveDependents.filter((dep) => !impact.directDependents.some((d) => d.id === dep.id)).map((dep) => ({
|
|
3052
|
+
name: dep.name,
|
|
3053
|
+
filePath: dep.filePath,
|
|
3054
|
+
kind: dep.kind
|
|
3055
|
+
}));
|
|
3056
|
+
const summary = `Changing ${target.name} would directly affect ${impact.directDependents.length} symbol(s) and transitively affect ${transitiveFormatted.length} more, across ${impact.affectedFiles.length} file(s).`;
|
|
3057
|
+
return {
|
|
3058
|
+
symbol: {
|
|
3059
|
+
name: target.name,
|
|
3060
|
+
filePath: target.filePath,
|
|
3061
|
+
kind: target.kind
|
|
3062
|
+
},
|
|
3063
|
+
impact: {
|
|
3064
|
+
directDependents: directWithKinds,
|
|
3065
|
+
transitiveDependents: transitiveFormatted,
|
|
3066
|
+
affectedFiles: impact.affectedFiles,
|
|
3067
|
+
summary
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
function handleGetFileContext(filePath, graph) {
|
|
3072
|
+
const fileSymbols = [];
|
|
3073
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
3074
|
+
if (attrs.filePath === filePath) {
|
|
3075
|
+
fileSymbols.push({
|
|
3076
|
+
name: attrs.name,
|
|
3077
|
+
kind: attrs.kind,
|
|
3078
|
+
exported: attrs.exported,
|
|
3079
|
+
startLine: attrs.startLine,
|
|
3080
|
+
endLine: attrs.endLine,
|
|
3081
|
+
scope: attrs.scope
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
if (fileSymbols.length === 0) {
|
|
3086
|
+
return {
|
|
3087
|
+
error: `File '${filePath}' not found`,
|
|
3088
|
+
suggestion: "Use list_files to see available files"
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
const importsMap = /* @__PURE__ */ new Map();
|
|
3092
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
3093
|
+
if (attrs.filePath === filePath) {
|
|
3094
|
+
graph.forEachOutEdge(nodeId, (edge, edgeAttrs, source, target) => {
|
|
3095
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3096
|
+
if (targetAttrs.filePath !== filePath) {
|
|
3097
|
+
if (!importsMap.has(targetAttrs.filePath)) {
|
|
3098
|
+
importsMap.set(targetAttrs.filePath, /* @__PURE__ */ new Set());
|
|
3099
|
+
}
|
|
3100
|
+
importsMap.get(targetAttrs.filePath).add(targetAttrs.name);
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
const imports = Array.from(importsMap.entries()).map(([file, symbols]) => ({
|
|
3106
|
+
from: file,
|
|
3107
|
+
symbols: Array.from(symbols)
|
|
3108
|
+
}));
|
|
3109
|
+
const importedByMap = /* @__PURE__ */ new Map();
|
|
3110
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
3111
|
+
if (attrs.filePath === filePath) {
|
|
3112
|
+
graph.forEachInEdge(nodeId, (edge, edgeAttrs, source, target) => {
|
|
3113
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3114
|
+
if (sourceAttrs.filePath !== filePath) {
|
|
3115
|
+
if (!importedByMap.has(sourceAttrs.filePath)) {
|
|
3116
|
+
importedByMap.set(sourceAttrs.filePath, /* @__PURE__ */ new Set());
|
|
3117
|
+
}
|
|
3118
|
+
importedByMap.get(sourceAttrs.filePath).add(attrs.name);
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
const importedBy = Array.from(importedByMap.entries()).map(([file, symbols]) => ({
|
|
3124
|
+
file,
|
|
3125
|
+
symbols: Array.from(symbols)
|
|
3126
|
+
}));
|
|
3127
|
+
const summary = `${filePath} defines ${fileSymbols.length} symbol(s), imports from ${imports.length} file(s), and is imported by ${importedBy.length} file(s).`;
|
|
3128
|
+
return {
|
|
3129
|
+
filePath,
|
|
3130
|
+
symbols: fileSymbols,
|
|
3131
|
+
imports,
|
|
3132
|
+
importedBy,
|
|
3133
|
+
summary
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
function handleSearchSymbols(query, limit, graph) {
|
|
3137
|
+
const results = searchSymbols(graph, query);
|
|
3138
|
+
const queryLower = query.toLowerCase();
|
|
3139
|
+
results.sort((a, b) => {
|
|
3140
|
+
const aName = a.name.toLowerCase();
|
|
3141
|
+
const bName = b.name.toLowerCase();
|
|
3142
|
+
if (aName === queryLower && bName !== queryLower) return -1;
|
|
3143
|
+
if (bName === queryLower && aName !== queryLower) return 1;
|
|
3144
|
+
const aStarts = aName.startsWith(queryLower);
|
|
3145
|
+
const bStarts = bName.startsWith(queryLower);
|
|
3146
|
+
if (aStarts && !bStarts) return -1;
|
|
3147
|
+
if (bStarts && !aStarts) return 1;
|
|
3148
|
+
return aName.localeCompare(bName);
|
|
3149
|
+
});
|
|
3150
|
+
const showing = Math.min(limit, results.length);
|
|
3151
|
+
return {
|
|
3152
|
+
query,
|
|
3153
|
+
results: results.slice(0, limit).map((r) => ({
|
|
3154
|
+
name: r.name,
|
|
3155
|
+
kind: r.kind,
|
|
3156
|
+
filePath: r.filePath,
|
|
3157
|
+
exported: r.exported,
|
|
3158
|
+
scope: r.scope
|
|
3159
|
+
})),
|
|
3160
|
+
totalMatches: results.length,
|
|
3161
|
+
showing
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function handleGetArchitectureSummary(graph) {
|
|
3165
|
+
const summary = getArchitectureSummary(graph);
|
|
3166
|
+
const fileSummary = getFileSummary(graph);
|
|
3167
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
3168
|
+
const languageBreakdown = {};
|
|
3169
|
+
fileSummary.forEach((f) => {
|
|
3170
|
+
const dir = f.filePath.includes("/") ? dirname6(f.filePath) : ".";
|
|
3171
|
+
if (!dirMap.has(dir)) {
|
|
3172
|
+
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
3173
|
+
}
|
|
3174
|
+
const entry = dirMap.get(dir);
|
|
3175
|
+
entry.fileCount++;
|
|
3176
|
+
entry.symbolCount += f.symbolCount;
|
|
3177
|
+
const ext = f.filePath.toLowerCase();
|
|
3178
|
+
let lang;
|
|
3179
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
3180
|
+
lang = "typescript";
|
|
3181
|
+
} else if (ext.endsWith(".py")) {
|
|
3182
|
+
lang = "python";
|
|
3183
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
3184
|
+
lang = "javascript";
|
|
3185
|
+
} else {
|
|
3186
|
+
lang = "other";
|
|
3187
|
+
}
|
|
3188
|
+
languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
|
|
3189
|
+
});
|
|
3190
|
+
const directories = Array.from(dirMap.entries()).map(([name, stats]) => ({ name, ...stats })).sort((a, b) => b.symbolCount - a.symbolCount);
|
|
3191
|
+
const summaryText = `Project has ${summary.fileCount} files with ${summary.symbolCount} symbols and ${summary.edgeCount} edges. The most connected file is ${summary.mostConnectedFiles[0]?.filePath || "N/A"} with ${summary.mostConnectedFiles[0]?.connections || 0} connections.`;
|
|
3192
|
+
return {
|
|
3193
|
+
overview: {
|
|
3194
|
+
totalFiles: summary.fileCount,
|
|
3195
|
+
totalSymbols: summary.symbolCount,
|
|
3196
|
+
totalEdges: summary.edgeCount,
|
|
3197
|
+
languages: languageBreakdown
|
|
3198
|
+
},
|
|
3199
|
+
mostConnectedFiles: summary.mostConnectedFiles.slice(0, 10),
|
|
3200
|
+
directories: directories.slice(0, 10),
|
|
3201
|
+
orphanFiles: summary.orphanFiles,
|
|
3202
|
+
summary: summaryText
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function handleListFiles(directory, graph) {
|
|
3206
|
+
const fileSummary = getFileSummary(graph);
|
|
3207
|
+
let filtered = fileSummary;
|
|
3208
|
+
if (directory) {
|
|
3209
|
+
filtered = fileSummary.filter((f) => f.filePath.startsWith(directory));
|
|
3210
|
+
}
|
|
3211
|
+
const files = filtered.map((f) => ({
|
|
3212
|
+
path: f.filePath,
|
|
3213
|
+
symbolCount: f.symbolCount,
|
|
3214
|
+
connections: f.incomingRefs + f.outgoingRefs
|
|
3215
|
+
}));
|
|
3216
|
+
return {
|
|
3217
|
+
files,
|
|
3218
|
+
totalFiles: files.length
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
async function handleVisualizeGraph(highlight, maxFiles, state) {
|
|
3222
|
+
const vizData = prepareVizData(state.graph, state.projectRoot);
|
|
3223
|
+
const { url, alreadyRunning } = startVizServer(
|
|
3224
|
+
vizData,
|
|
3225
|
+
state.graph,
|
|
3226
|
+
state.projectRoot,
|
|
3227
|
+
3456,
|
|
3228
|
+
// Use different port from CLI default to avoid conflicts
|
|
3229
|
+
false
|
|
3230
|
+
// Don't auto-open browser from MCP
|
|
3231
|
+
);
|
|
3232
|
+
const fileCount = maxFiles && maxFiles < vizData.files.length ? maxFiles : vizData.files.length;
|
|
3233
|
+
const arcCount = vizData.arcs.filter((a) => {
|
|
3234
|
+
if (!maxFiles || maxFiles >= vizData.files.length) return true;
|
|
3235
|
+
const topFiles = vizData.files.sort((a2, b) => b.incomingCount + b.outgoingCount - (a2.incomingCount + a2.outgoingCount)).slice(0, maxFiles).map((f) => f.path);
|
|
3236
|
+
return topFiles.includes(a.sourceFile) && topFiles.includes(a.targetFile);
|
|
3237
|
+
}).length;
|
|
3238
|
+
const statusMessage = alreadyRunning ? "Visualization server is already running." : "Visualization server started.";
|
|
3239
|
+
const message = `${statusMessage}
|
|
3240
|
+
|
|
3241
|
+
Interactive arc diagram: ${url}
|
|
3242
|
+
|
|
3243
|
+
The diagram shows ${fileCount} files and ${arcCount} cross-file dependencies.${highlight ? ` Highlighted: ${highlight}` : ""}
|
|
3244
|
+
|
|
3245
|
+
Features:
|
|
3246
|
+
\u2022 Hover over arcs to see source \u2192 target details
|
|
3247
|
+
\u2022 Click files to filter connections
|
|
3248
|
+
\u2022 Search for specific files
|
|
3249
|
+
\u2022 Export as SVG or PNG
|
|
3250
|
+
|
|
3251
|
+
The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
3252
|
+
return {
|
|
3253
|
+
content: [{ type: "text", text: message }]
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/mcp/server.ts
|
|
3258
|
+
async function startMcpServer(state) {
|
|
3259
|
+
const server = new Server(
|
|
3260
|
+
{
|
|
3261
|
+
name: "depwire",
|
|
3262
|
+
version: "0.1.0"
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
capabilities: {
|
|
3266
|
+
tools: {}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
);
|
|
3270
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
3271
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3272
|
+
return {
|
|
3273
|
+
tools: getToolsList()
|
|
3274
|
+
};
|
|
3275
|
+
});
|
|
3276
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3277
|
+
const { name, arguments: args } = request.params;
|
|
3278
|
+
return await handleToolCall(name, args || {}, state);
|
|
3279
|
+
});
|
|
3280
|
+
const transport = new StdioServerTransport();
|
|
3281
|
+
await server.connect(transport);
|
|
3282
|
+
console.error("Depwire MCP server started");
|
|
3283
|
+
if (state.projectRoot) {
|
|
3284
|
+
console.error(`Project: ${state.projectRoot}`);
|
|
3285
|
+
} else {
|
|
3286
|
+
console.error("No project loaded. Use connect_repo to connect to a codebase.");
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
export {
|
|
3291
|
+
parseProject,
|
|
3292
|
+
buildGraph,
|
|
3293
|
+
getImpact,
|
|
3294
|
+
searchSymbols,
|
|
3295
|
+
getArchitectureSummary,
|
|
3296
|
+
prepareVizData,
|
|
3297
|
+
watchProject,
|
|
3298
|
+
startVizServer,
|
|
3299
|
+
createEmptyState,
|
|
3300
|
+
updateFileInGraph,
|
|
3301
|
+
startMcpServer
|
|
3302
|
+
};
|