code-survey 1.0.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 +191 -0
- package/dist/bin/code-survey.js +1620 -0
- package/dist/cache/cache.d.ts +9 -0
- package/dist/cli/cli-parser.d.ts +24 -0
- package/dist/cli/token-estimator.d.ts +4 -0
- package/dist/cli/watcher.d.ts +2 -0
- package/dist/config/config.d.ts +2 -0
- package/dist/git/git.d.ts +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +941 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/package-parser.d.ts +2 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parsers/base.d.ts +9 -0
- package/dist/parsers/csharp.d.ts +5 -0
- package/dist/parsers/go.d.ts +5 -0
- package/dist/parsers/java.d.ts +5 -0
- package/dist/parsers/javascript.d.ts +5 -0
- package/dist/parsers/python.d.ts +5 -0
- package/dist/parsers/rust.d.ts +5 -0
- package/dist/parsers/typescript.d.ts +5 -0
- package/dist/resolver.d.ts +5 -0
- package/dist/scanner.d.ts +7 -0
- package/dist/types.d.ts +74 -0
- package/dist/wasm/wasm-loader.d.ts +1 -0
- package/dist/writer.d.ts +21 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import * as fs6 from "node:fs";
|
|
7
|
+
import * as path8 from "node:path";
|
|
8
|
+
import * as os2 from "node:os";
|
|
9
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// src/scanner.ts
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as crypto from "node:crypto";
|
|
15
|
+
var DEFAULT_IGNORED_DIRS = new Set([
|
|
16
|
+
".git",
|
|
17
|
+
".code-survey",
|
|
18
|
+
"node_modules",
|
|
19
|
+
".venv",
|
|
20
|
+
"venv",
|
|
21
|
+
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"build",
|
|
24
|
+
"target",
|
|
25
|
+
"obj"
|
|
26
|
+
]);
|
|
27
|
+
function calculateHash(filePath) {
|
|
28
|
+
const content = fs.readFileSync(filePath);
|
|
29
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
function scanDirectory(rootDir, customExcludes = [], maxDepth) {
|
|
32
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
33
|
+
const excludeSet = new Set(customExcludes);
|
|
34
|
+
const results = [];
|
|
35
|
+
function walk(currentDir, currentDepth = 0) {
|
|
36
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const entryName = entry.name;
|
|
39
|
+
const fullPath = path.join(currentDir, entryName);
|
|
40
|
+
const relativePath = path.relative(absoluteRoot, fullPath);
|
|
41
|
+
if (DEFAULT_IGNORED_DIRS.has(entryName) || excludeSet.has(entryName) || excludeSet.has(relativePath)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (maxDepth === undefined || currentDepth < maxDepth) {
|
|
46
|
+
walk(fullPath, currentDepth + 1);
|
|
47
|
+
}
|
|
48
|
+
} else if (entry.isFile()) {
|
|
49
|
+
const hash = calculateHash(fullPath);
|
|
50
|
+
results.push({
|
|
51
|
+
relativePath,
|
|
52
|
+
absolutePath: fullPath,
|
|
53
|
+
hash
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
walk(absoluteRoot);
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/parser.ts
|
|
63
|
+
import * as path3 from "node:path";
|
|
64
|
+
|
|
65
|
+
// src/wasm/wasm-loader.ts
|
|
66
|
+
import * as fs2 from "node:fs";
|
|
67
|
+
import * as path2 from "node:path";
|
|
68
|
+
import * as os from "node:os";
|
|
69
|
+
import { fileURLToPath } from "node:url";
|
|
70
|
+
var WASM_FILE_MAP = {
|
|
71
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
72
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
73
|
+
python: "tree-sitter-python.wasm",
|
|
74
|
+
go: "tree-sitter-go.wasm",
|
|
75
|
+
java: "tree-sitter-java.wasm",
|
|
76
|
+
csharp: "tree-sitter-c_sharp.wasm",
|
|
77
|
+
rust: "tree-sitter-rust.wasm"
|
|
78
|
+
};
|
|
79
|
+
var CDN_BASE_URL = "https://cdn.jsdelivr.net/npm/tree-sitter-wasms@0.1.13/out";
|
|
80
|
+
async function getWasmPath(language) {
|
|
81
|
+
const fileName = WASM_FILE_MAP[language];
|
|
82
|
+
if (!fileName) {
|
|
83
|
+
throw new Error(`Unsupported tree-sitter language: ${language}`);
|
|
84
|
+
}
|
|
85
|
+
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
86
|
+
const nodeModulesPath = path2.resolve(currentDir, "..", "..", "node_modules", "tree-sitter-wasms", "out", fileName);
|
|
87
|
+
if (fs2.existsSync(nodeModulesPath)) {
|
|
88
|
+
return nodeModulesPath;
|
|
89
|
+
}
|
|
90
|
+
const localPath = path2.resolve(process.cwd(), "node_modules", "tree-sitter-wasms", "out", fileName);
|
|
91
|
+
if (fs2.existsSync(localPath)) {
|
|
92
|
+
return localPath;
|
|
93
|
+
}
|
|
94
|
+
const cacheDir = path2.resolve(os.homedir(), ".code-survey", "grammars");
|
|
95
|
+
const cachePath = path2.resolve(cacheDir, fileName);
|
|
96
|
+
if (fs2.existsSync(cachePath)) {
|
|
97
|
+
return cachePath;
|
|
98
|
+
}
|
|
99
|
+
if (!fs2.existsSync(cacheDir)) {
|
|
100
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
const url = `${CDN_BASE_URL}/${fileName}`;
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(url);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch WASM from CDN: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
109
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
110
|
+
fs2.writeFileSync(cachePath, buffer);
|
|
111
|
+
return cachePath;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw new Error(`Could not resolve or download WASM grammar for ${language}: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/parser.ts
|
|
118
|
+
class CodebaseParser {
|
|
119
|
+
parsers = {};
|
|
120
|
+
async initialize(fileExtensions) {
|
|
121
|
+
const extensions = fileExtensions ? new Set(fileExtensions) : null;
|
|
122
|
+
const loadParser = async (exts, importPath, className, wasmName) => {
|
|
123
|
+
if (extensions) {
|
|
124
|
+
const needsLoad = exts.some((ext) => extensions.has(ext));
|
|
125
|
+
if (!needsLoad)
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const module = await import(importPath);
|
|
130
|
+
const constructor = module[className];
|
|
131
|
+
const parser = new constructor;
|
|
132
|
+
await parser.initialize(await getWasmPath(wasmName));
|
|
133
|
+
for (const ext of exts) {
|
|
134
|
+
this.parsers[ext] = parser;
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`Failed to initialize parser for ${wasmName}: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
await loadParser([".ts", ".tsx"], "./parsers/typescript.ts", "TypeScriptParser", "typescript");
|
|
141
|
+
await loadParser([".js", ".jsx"], "./parsers/javascript.ts", "JavaScriptParser", "javascript");
|
|
142
|
+
await loadParser([".py"], "./parsers/python.ts", "PythonParser", "python");
|
|
143
|
+
await loadParser([".go"], "./parsers/go.ts", "GoParser", "go");
|
|
144
|
+
await loadParser([".java"], "./parsers/java.ts", "JavaParser", "java");
|
|
145
|
+
await loadParser([".cs"], "./parsers/csharp.ts", "CSharpParser", "csharp");
|
|
146
|
+
await loadParser([".rs"], "./parsers/rust.ts", "RustParser", "rust");
|
|
147
|
+
}
|
|
148
|
+
getParserForFile(filePath) {
|
|
149
|
+
const ext = path3.extname(filePath);
|
|
150
|
+
return this.parsers[ext] || null;
|
|
151
|
+
}
|
|
152
|
+
parseFile(filePath, code, options) {
|
|
153
|
+
const parser = this.getParserForFile(filePath);
|
|
154
|
+
if (!parser) {
|
|
155
|
+
return { imports: [], exports: [], symbols: [] };
|
|
156
|
+
}
|
|
157
|
+
return parser.parse(code, options);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/package-parser.ts
|
|
162
|
+
import * as fs3 from "node:fs";
|
|
163
|
+
import * as path4 from "node:path";
|
|
164
|
+
function parsePackageDependencies(dir) {
|
|
165
|
+
const deps = {};
|
|
166
|
+
const pkgJsonPath = path4.join(dir, "package.json");
|
|
167
|
+
if (fs3.existsSync(pkgJsonPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const content = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
|
|
170
|
+
deps.npm = {
|
|
171
|
+
...content.dependencies || {},
|
|
172
|
+
...content.devDependencies || {}
|
|
173
|
+
};
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
const goModPath = path4.join(dir, "go.mod");
|
|
177
|
+
if (fs3.existsSync(goModPath)) {
|
|
178
|
+
try {
|
|
179
|
+
const content = fs3.readFileSync(goModPath, "utf-8");
|
|
180
|
+
const goDeps = {};
|
|
181
|
+
const requireBlockRegex = /require\s*\(([\s\S]*?)\)/g;
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = requireBlockRegex.exec(content)) !== null) {
|
|
184
|
+
const lines = match[1].split(`
|
|
185
|
+
`);
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const parts = line.trim().split(/\s+/);
|
|
188
|
+
if (parts.length >= 2 && !parts[0].startsWith("//")) {
|
|
189
|
+
goDeps[parts[0]] = parts[1];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const singleRequireRegex = /^\s*require\s+([a-zA-Z0-9.\-_\/]+)\s+([^\s]+)/gm;
|
|
194
|
+
while ((match = singleRequireRegex.exec(content)) !== null) {
|
|
195
|
+
goDeps[match[1]] = match[2];
|
|
196
|
+
}
|
|
197
|
+
if (Object.keys(goDeps).length > 0) {
|
|
198
|
+
deps.go = goDeps;
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
const pomPath = path4.join(dir, "pom.xml");
|
|
203
|
+
if (fs3.existsSync(pomPath)) {
|
|
204
|
+
try {
|
|
205
|
+
const content = fs3.readFileSync(pomPath, "utf-8");
|
|
206
|
+
const mavenDeps = {};
|
|
207
|
+
const depRegex = /<dependency>([\s\S]*?)<\/dependency>/g;
|
|
208
|
+
let match;
|
|
209
|
+
while ((match = depRegex.exec(content)) !== null) {
|
|
210
|
+
const block = match[1];
|
|
211
|
+
const groupId = block.match(/<groupId>(.*?)<\/groupId>/)?.[1]?.trim();
|
|
212
|
+
const artifactId = block.match(/<artifactId>(.*?)<\/artifactId>/)?.[1]?.trim();
|
|
213
|
+
const version = block.match(/<version>(.*?)<\/version>/)?.[1]?.trim();
|
|
214
|
+
if (groupId && artifactId && version) {
|
|
215
|
+
mavenDeps[`${groupId}:${artifactId}`] = version;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (Object.keys(mavenDeps).length > 0) {
|
|
219
|
+
deps.maven = mavenDeps;
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
const files = fs3.readdirSync(dir);
|
|
224
|
+
const csprojFile = files.find((f) => f.endsWith(".csproj"));
|
|
225
|
+
if (csprojFile) {
|
|
226
|
+
try {
|
|
227
|
+
const content = fs3.readFileSync(path4.join(dir, csprojFile), "utf-8");
|
|
228
|
+
const nugetDeps = {};
|
|
229
|
+
const packageRefRegex = /<PackageReference\s+([\s\S]*?)\/>/g;
|
|
230
|
+
let match;
|
|
231
|
+
while ((match = packageRefRegex.exec(content)) !== null) {
|
|
232
|
+
const attrs = match[1];
|
|
233
|
+
const include = attrs.match(/Include="([^"]+)"/)?.[1];
|
|
234
|
+
const version = attrs.match(/Version="([^"]+)"/)?.[1];
|
|
235
|
+
if (include && version) {
|
|
236
|
+
nugetDeps[include] = version;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(nugetDeps).length > 0) {
|
|
240
|
+
deps.nuget = nugetDeps;
|
|
241
|
+
}
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
const reqPath = path4.join(dir, "requirements.txt");
|
|
245
|
+
if (fs3.existsSync(reqPath)) {
|
|
246
|
+
try {
|
|
247
|
+
const lines = fs3.readFileSync(reqPath, "utf-8").split(`
|
|
248
|
+
`);
|
|
249
|
+
const pipDeps = {};
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
253
|
+
const match = trimmed.match(/^([^>=<#\s]+)\s*(>=|<=|==|!=|<|>)\s*([^\s]+)/);
|
|
254
|
+
if (match) {
|
|
255
|
+
pipDeps[match[1]] = match[2] + match[3];
|
|
256
|
+
} else {
|
|
257
|
+
pipDeps[trimmed] = "latest";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(pipDeps).length > 0) {
|
|
262
|
+
deps.pip = pipDeps;
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
return deps;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/resolver.ts
|
|
270
|
+
import * as path5 from "node:path";
|
|
271
|
+
function resolveImports(files, namespaces, packageDeps) {
|
|
272
|
+
const externalImports = {};
|
|
273
|
+
const internalLinks = {};
|
|
274
|
+
const filePaths = Object.keys(files);
|
|
275
|
+
const addExternalImport = (pkg, file) => {
|
|
276
|
+
if (!externalImports[pkg])
|
|
277
|
+
externalImports[pkg] = [];
|
|
278
|
+
if (!externalImports[pkg].includes(file)) {
|
|
279
|
+
externalImports[pkg].push(file);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const addInternalLink = (targetFile, symbol, sourceFile) => {
|
|
283
|
+
const key = symbol ? `${targetFile}:${symbol}` : targetFile;
|
|
284
|
+
if (!internalLinks[key])
|
|
285
|
+
internalLinks[key] = [];
|
|
286
|
+
if (!internalLinks[key].includes(sourceFile)) {
|
|
287
|
+
internalLinks[key].push(sourceFile);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const npmPackages = new Set(Object.keys(packageDeps.npm || {}));
|
|
291
|
+
const pipPackages = new Set(Object.keys(packageDeps.pip || {}));
|
|
292
|
+
const nugetPackages = new Set(Object.keys(packageDeps.nuget || {}));
|
|
293
|
+
const mavenPackages = new Set(Object.keys(packageDeps.maven || {}));
|
|
294
|
+
const goPackages = new Set(Object.keys(packageDeps.go || {}));
|
|
295
|
+
function isExternal(source) {
|
|
296
|
+
return npmPackages.has(source) || pipPackages.has(source) || nugetPackages.has(source) || mavenPackages.has(source) || goPackages.has(source);
|
|
297
|
+
}
|
|
298
|
+
for (const sourceFile of filePaths) {
|
|
299
|
+
const entry = files[sourceFile];
|
|
300
|
+
const sourceDir = path5.dirname(sourceFile);
|
|
301
|
+
for (const imp of entry.imports) {
|
|
302
|
+
const { source, symbols } = imp;
|
|
303
|
+
if (isExternal(source)) {
|
|
304
|
+
addExternalImport(source, sourceFile);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (source.startsWith(".")) {
|
|
308
|
+
const resolvedBase = path5.normalize(path5.join(sourceDir, source));
|
|
309
|
+
const candidates = [
|
|
310
|
+
resolvedBase,
|
|
311
|
+
resolvedBase + ".ts",
|
|
312
|
+
resolvedBase + ".tsx",
|
|
313
|
+
resolvedBase + ".js",
|
|
314
|
+
resolvedBase + ".jsx",
|
|
315
|
+
resolvedBase + ".d.ts",
|
|
316
|
+
path5.join(resolvedBase, "index.ts"),
|
|
317
|
+
path5.join(resolvedBase, "index.tsx"),
|
|
318
|
+
path5.join(resolvedBase, "index.js")
|
|
319
|
+
];
|
|
320
|
+
let resolvedFile = null;
|
|
321
|
+
for (const cand of candidates) {
|
|
322
|
+
const normalizedCand = cand.replace(/\\/g, "/");
|
|
323
|
+
if (files[normalizedCand]) {
|
|
324
|
+
resolvedFile = normalizedCand;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (resolvedFile) {
|
|
329
|
+
if (symbols.length === 0) {
|
|
330
|
+
addInternalLink(resolvedFile, null, sourceFile);
|
|
331
|
+
} else {
|
|
332
|
+
for (const sym of symbols) {
|
|
333
|
+
addInternalLink(resolvedFile, sym, sourceFile);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const isPythonDotted = sourceFile.endsWith(".py") && !source.includes("/") && (source.includes(".") || source.startsWith("."));
|
|
340
|
+
if (isPythonDotted) {
|
|
341
|
+
let normalizedSource = source;
|
|
342
|
+
if (source.startsWith(".")) {
|
|
343
|
+
const dots = source.match(/^\.+/)?.[0] || "";
|
|
344
|
+
const remainder = source.substring(dots.length);
|
|
345
|
+
const dotCount = dots.length;
|
|
346
|
+
let targetDir = sourceDir;
|
|
347
|
+
for (let i = 1;i < dotCount; i++) {
|
|
348
|
+
targetDir = path5.dirname(targetDir);
|
|
349
|
+
}
|
|
350
|
+
normalizedSource = remainder ? path5.join(targetDir, remainder.replace(/\./g, "/")) : targetDir;
|
|
351
|
+
} else {
|
|
352
|
+
normalizedSource = source.replace(/\./g, "/");
|
|
353
|
+
}
|
|
354
|
+
const candidate = (normalizedSource + ".py").replace(/\\/g, "/");
|
|
355
|
+
if (files[candidate]) {
|
|
356
|
+
if (symbols.length === 0) {
|
|
357
|
+
addInternalLink(candidate, null, sourceFile);
|
|
358
|
+
} else {
|
|
359
|
+
for (const sym of symbols) {
|
|
360
|
+
addInternalLink(candidate, sym, sourceFile);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (namespaces[source]) {
|
|
367
|
+
const ns = namespaces[source];
|
|
368
|
+
for (const targetFile of ns.files) {
|
|
369
|
+
if (symbols.length === 0) {
|
|
370
|
+
addInternalLink(targetFile, null, sourceFile);
|
|
371
|
+
} else {
|
|
372
|
+
for (const sym of symbols) {
|
|
373
|
+
const targetEntry = files[targetFile];
|
|
374
|
+
if (targetEntry && targetEntry.exports.some((e) => e.name === sym)) {
|
|
375
|
+
addInternalLink(targetFile, sym, sourceFile);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (source.includes(".")) {
|
|
383
|
+
const lastDot = source.lastIndexOf(".");
|
|
384
|
+
const nsName = source.substring(0, lastDot);
|
|
385
|
+
const className = source.substring(lastDot + 1);
|
|
386
|
+
if (namespaces[nsName]) {
|
|
387
|
+
const ns = namespaces[nsName];
|
|
388
|
+
for (const targetFile of ns.files) {
|
|
389
|
+
const targetEntry = files[targetFile];
|
|
390
|
+
if (targetEntry && targetEntry.exports.some((e) => e.name === className)) {
|
|
391
|
+
addInternalLink(targetFile, className, sourceFile);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const simpleCandidate = source.replace(/\\/g, "/");
|
|
398
|
+
if (files[simpleCandidate]) {
|
|
399
|
+
addInternalLink(simpleCandidate, null, sourceFile);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { externalImports, internalLinks };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/writer.ts
|
|
407
|
+
import { stringify } from "yaml";
|
|
408
|
+
import * as fs4 from "node:fs";
|
|
409
|
+
import * as path6 from "node:path";
|
|
410
|
+
function sortObjectKeys(obj) {
|
|
411
|
+
if (obj === null || typeof obj !== "object") {
|
|
412
|
+
return obj;
|
|
413
|
+
}
|
|
414
|
+
if (Array.isArray(obj)) {
|
|
415
|
+
return obj.map(sortObjectKeys);
|
|
416
|
+
}
|
|
417
|
+
const sortedObj = {};
|
|
418
|
+
const keys = Object.keys(obj).sort();
|
|
419
|
+
for (const key of keys) {
|
|
420
|
+
sortedObj[key] = sortObjectKeys(obj[key]);
|
|
421
|
+
}
|
|
422
|
+
return sortedObj;
|
|
423
|
+
}
|
|
424
|
+
function sortCodeSurveyData(data) {
|
|
425
|
+
const clone = JSON.parse(JSON.stringify(data));
|
|
426
|
+
if (clone.project?.languages) {
|
|
427
|
+
clone.project.languages.sort();
|
|
428
|
+
}
|
|
429
|
+
if (clone.files) {
|
|
430
|
+
for (const fileKey of Object.keys(clone.files)) {
|
|
431
|
+
const fileEntry = clone.files[fileKey];
|
|
432
|
+
if (fileEntry.imports) {
|
|
433
|
+
fileEntry.imports.sort((a, b) => a.source.localeCompare(b.source));
|
|
434
|
+
for (const imp of fileEntry.imports) {
|
|
435
|
+
if (imp.symbols)
|
|
436
|
+
imp.symbols.sort();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (fileEntry.exports) {
|
|
440
|
+
fileEntry.exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
441
|
+
}
|
|
442
|
+
if (fileEntry.symbols) {
|
|
443
|
+
fileEntry.symbols.sort((a, b) => a.name.localeCompare(b.name));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (clone.namespaces) {
|
|
448
|
+
for (const nsKey of Object.keys(clone.namespaces)) {
|
|
449
|
+
const nsEntry = clone.namespaces[nsKey];
|
|
450
|
+
if (nsEntry.files)
|
|
451
|
+
nsEntry.files.sort();
|
|
452
|
+
if (nsEntry.exports)
|
|
453
|
+
nsEntry.exports.sort();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (clone.externalImports) {
|
|
457
|
+
for (const extKey of Object.keys(clone.externalImports)) {
|
|
458
|
+
clone.externalImports[extKey].sort();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (clone.internalLinks) {
|
|
462
|
+
for (const linkKey of Object.keys(clone.internalLinks)) {
|
|
463
|
+
clone.internalLinks[linkKey].sort();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return sortObjectKeys(clone);
|
|
467
|
+
}
|
|
468
|
+
function generateDeterministicJson(data) {
|
|
469
|
+
const sortedData = sortCodeSurveyData(data);
|
|
470
|
+
return JSON.stringify(sortedData, null, 2);
|
|
471
|
+
}
|
|
472
|
+
function generateDeterministicYaml(data) {
|
|
473
|
+
const sortedData = sortCodeSurveyData(data);
|
|
474
|
+
return stringify(sortedData, { sortMapEntries: true });
|
|
475
|
+
}
|
|
476
|
+
function generateDeterministicMermaid(data) {
|
|
477
|
+
const sortedData = sortCodeSurveyData(data);
|
|
478
|
+
const lines = ["graph TD"];
|
|
479
|
+
const fileKeys = Object.keys(sortedData.files).sort();
|
|
480
|
+
if (fileKeys.length === 0) {
|
|
481
|
+
return `graph TD
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
const fileToId = {};
|
|
485
|
+
fileKeys.forEach((fileKey, index) => {
|
|
486
|
+
fileToId[fileKey] = `node_${index}`;
|
|
487
|
+
lines.push(` node_${index}["${fileKey}"]`);
|
|
488
|
+
});
|
|
489
|
+
const linkSet = new Set;
|
|
490
|
+
const linkKeys = Object.keys(sortedData.internalLinks).sort();
|
|
491
|
+
for (const linkKey of linkKeys) {
|
|
492
|
+
const sourceFile = linkKey.split(":")[0];
|
|
493
|
+
const sourceId = fileToId[sourceFile];
|
|
494
|
+
if (!sourceId)
|
|
495
|
+
continue;
|
|
496
|
+
const targets = sortedData.internalLinks[linkKey];
|
|
497
|
+
for (const targetFile of targets) {
|
|
498
|
+
const targetId = fileToId[targetFile];
|
|
499
|
+
if (targetId && sourceId !== targetId) {
|
|
500
|
+
const edge = ` ${targetId} --> ${sourceId}`;
|
|
501
|
+
linkSet.add(edge);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const sortedEdges = Array.from(linkSet).sort();
|
|
506
|
+
lines.push(...sortedEdges);
|
|
507
|
+
return lines.join(`
|
|
508
|
+
`) + `
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
function generateProjectMarkdown(data) {
|
|
512
|
+
return `# Project: ${data.project.name}
|
|
513
|
+
Languages: ${data.project.languages.join(", ")}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
function generateDependenciesMarkdown(data) {
|
|
517
|
+
const lines = [];
|
|
518
|
+
const depKeys = Object.keys(data.packageDependencies).sort();
|
|
519
|
+
if (depKeys.length > 0) {
|
|
520
|
+
lines.push("## Dependencies");
|
|
521
|
+
for (const key of depKeys) {
|
|
522
|
+
const deps = data.packageDependencies[key];
|
|
523
|
+
const items = Object.keys(deps).sort().map((d) => `${d} (${deps[d]})`);
|
|
524
|
+
if (items.length > 0) {
|
|
525
|
+
lines.push(`- **${key}**: ${items.join(", ")}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return lines.length > 0 ? lines.join(`
|
|
530
|
+
`) + `
|
|
531
|
+
` : "";
|
|
532
|
+
}
|
|
533
|
+
function generateFilesMarkdown(data, options) {
|
|
534
|
+
const lines = [];
|
|
535
|
+
const fileKeys = Object.keys(data.files).sort();
|
|
536
|
+
if (fileKeys.length > 0) {
|
|
537
|
+
lines.push("## Files");
|
|
538
|
+
for (const fileKey of fileKeys) {
|
|
539
|
+
const fileEntry = data.files[fileKey];
|
|
540
|
+
lines.push(`
|
|
541
|
+
### \`${fileKey}\``);
|
|
542
|
+
lines.push(`Hash: ${fileEntry.hash}`);
|
|
543
|
+
if (fileEntry.imports.length > 0) {
|
|
544
|
+
lines.push("- **Imports**:");
|
|
545
|
+
for (const imp of fileEntry.imports) {
|
|
546
|
+
const syms = imp.symbols.length > 0 ? `: ${imp.symbols.join(", ")}` : "";
|
|
547
|
+
lines.push(` - \`${imp.source}\`${syms}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (fileEntry.exports.length > 0) {
|
|
551
|
+
lines.push("- **Exports**:");
|
|
552
|
+
for (const exp of fileEntry.exports) {
|
|
553
|
+
const docStr = exp.doc ? ` - ${exp.doc}` : "";
|
|
554
|
+
const dispName = exp.signature ? `${exp.name}${exp.signature}` : exp.name;
|
|
555
|
+
lines.push(` - \`${dispName}\` (${exp.type}, ${exp.location})${docStr}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (fileEntry.symbols.length > 0) {
|
|
559
|
+
lines.push("- **Symbols**:");
|
|
560
|
+
for (const sym of fileEntry.symbols) {
|
|
561
|
+
const docStr = sym.doc ? ` - ${sym.doc}` : "";
|
|
562
|
+
const dispName = sym.signature ? `${sym.name}${sym.signature}` : sym.name;
|
|
563
|
+
lines.push(` - \`${dispName}\` (${sym.type}, ${sym.location})${docStr}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (options?.includeToc) {
|
|
567
|
+
lines.push(`
|
|
568
|
+
[Back to Table of Contents](#table-of-contents)`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return lines.length > 0 ? lines.join(`
|
|
573
|
+
`) + `
|
|
574
|
+
` : "";
|
|
575
|
+
}
|
|
576
|
+
function generateNamespacesMarkdown(data) {
|
|
577
|
+
const lines = [];
|
|
578
|
+
const nsKeys = Object.keys(data.namespaces).sort();
|
|
579
|
+
if (nsKeys.length > 0) {
|
|
580
|
+
lines.push("## Namespaces");
|
|
581
|
+
for (const nsKey of nsKeys) {
|
|
582
|
+
const nsEntry = data.namespaces[nsKey];
|
|
583
|
+
lines.push(`
|
|
584
|
+
### ${nsKey}`);
|
|
585
|
+
lines.push(`- **Files**: ${nsEntry.files.join(", ")}`);
|
|
586
|
+
lines.push(`- **Exports**: ${nsEntry.exports.join(", ")}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return lines.length > 0 ? lines.join(`
|
|
590
|
+
`) + `
|
|
591
|
+
` : "";
|
|
592
|
+
}
|
|
593
|
+
function generateLinksMarkdown(data) {
|
|
594
|
+
const lines = [];
|
|
595
|
+
const linkKeys = Object.keys(data.internalLinks).sort();
|
|
596
|
+
if (linkKeys.length > 0) {
|
|
597
|
+
lines.push("## Links");
|
|
598
|
+
for (const linkKey of linkKeys) {
|
|
599
|
+
const targets = data.internalLinks[linkKey];
|
|
600
|
+
lines.push(`- \`${linkKey}\` -> ${targets.map((t) => `\`${t}\``).join(", ")}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return lines.length > 0 ? lines.join(`
|
|
604
|
+
`) + `
|
|
605
|
+
` : "";
|
|
606
|
+
}
|
|
607
|
+
function generateDeterministicMarkdown(data, options) {
|
|
608
|
+
const sortedData = sortCodeSurveyData(data);
|
|
609
|
+
const parts = [];
|
|
610
|
+
parts.push(generateProjectMarkdown(sortedData));
|
|
611
|
+
if (options?.includeToc) {
|
|
612
|
+
const tocLines = [];
|
|
613
|
+
tocLines.push("## Table of Contents");
|
|
614
|
+
tocLines.push("- [Dependencies](#dependencies)");
|
|
615
|
+
tocLines.push("- [Files](#files)");
|
|
616
|
+
const fileKeys = Object.keys(sortedData.files).sort();
|
|
617
|
+
for (const fileKey of fileKeys) {
|
|
618
|
+
const anchor = fileKey.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
619
|
+
tocLines.push(` - [${fileKey}](#${anchor})`);
|
|
620
|
+
}
|
|
621
|
+
const nsKeys = Object.keys(sortedData.namespaces).sort();
|
|
622
|
+
if (nsKeys.length > 0) {
|
|
623
|
+
tocLines.push("- [Namespaces](#namespaces)");
|
|
624
|
+
}
|
|
625
|
+
const linkKeys = Object.keys(sortedData.internalLinks).sort();
|
|
626
|
+
if (linkKeys.length > 0) {
|
|
627
|
+
tocLines.push("- [Links](#links)");
|
|
628
|
+
}
|
|
629
|
+
parts.push(tocLines.join(`
|
|
630
|
+
`) + `
|
|
631
|
+
`);
|
|
632
|
+
}
|
|
633
|
+
const depsMd = generateDependenciesMarkdown(sortedData);
|
|
634
|
+
if (depsMd)
|
|
635
|
+
parts.push(depsMd);
|
|
636
|
+
const filesMd = generateFilesMarkdown(sortedData, options);
|
|
637
|
+
if (filesMd)
|
|
638
|
+
parts.push(filesMd);
|
|
639
|
+
const nsMd = generateNamespacesMarkdown(sortedData);
|
|
640
|
+
if (nsMd)
|
|
641
|
+
parts.push(nsMd);
|
|
642
|
+
const linksMd = generateLinksMarkdown(sortedData);
|
|
643
|
+
if (linksMd)
|
|
644
|
+
parts.push(linksMd);
|
|
645
|
+
return parts.join(`
|
|
646
|
+
`);
|
|
647
|
+
}
|
|
648
|
+
function writeSurveyOutput(schemaObj, options) {
|
|
649
|
+
if (!options.output)
|
|
650
|
+
return;
|
|
651
|
+
let format = options.format;
|
|
652
|
+
if (!format) {
|
|
653
|
+
const ext = path6.extname(options.output).toLowerCase();
|
|
654
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
655
|
+
format = "yaml";
|
|
656
|
+
} else if (ext === ".md" || ext === ".markdown") {
|
|
657
|
+
format = "markdown";
|
|
658
|
+
} else if (ext === ".mermaid" || ext === ".mmd") {
|
|
659
|
+
format = "mermaid";
|
|
660
|
+
} else {
|
|
661
|
+
format = "json";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (options.split) {
|
|
665
|
+
if (!fs4.existsSync(options.output)) {
|
|
666
|
+
fs4.mkdirSync(options.output, { recursive: true });
|
|
667
|
+
}
|
|
668
|
+
const ext = format === "yaml" ? ".yaml" : format === "markdown" ? ".md" : format === "mermaid" ? ".mermaid" : ".json";
|
|
669
|
+
if (format === "json") {
|
|
670
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateDeterministicJson({ version: schemaObj.version, project: schemaObj.project }), "utf-8");
|
|
671
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDeterministicJson({ packageDependencies: schemaObj.packageDependencies }), "utf-8");
|
|
672
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateDeterministicJson({ files: schemaObj.files }), "utf-8");
|
|
673
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateDeterministicJson({ namespaces: schemaObj.namespaces }), "utf-8");
|
|
674
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicJson({ externalImports: schemaObj.externalImports, internalLinks: schemaObj.internalLinks }), "utf-8");
|
|
675
|
+
} else if (format === "yaml") {
|
|
676
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateDeterministicYaml({ version: schemaObj.version, project: schemaObj.project }), "utf-8");
|
|
677
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDeterministicYaml({ packageDependencies: schemaObj.packageDependencies }), "utf-8");
|
|
678
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateDeterministicYaml({ files: schemaObj.files }), "utf-8");
|
|
679
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateDeterministicYaml({ namespaces: schemaObj.namespaces }), "utf-8");
|
|
680
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicYaml({ externalImports: schemaObj.externalImports, internalLinks: schemaObj.internalLinks }), "utf-8");
|
|
681
|
+
} else if (format === "markdown") {
|
|
682
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateProjectMarkdown(schemaObj), "utf-8");
|
|
683
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDependenciesMarkdown(schemaObj), "utf-8");
|
|
684
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateFilesMarkdown(schemaObj), "utf-8");
|
|
685
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateNamespacesMarkdown(schemaObj), "utf-8");
|
|
686
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateLinksMarkdown(schemaObj), "utf-8");
|
|
687
|
+
} else if (format === "mermaid") {
|
|
688
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicMermaid(schemaObj), "utf-8");
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
const outputStr = format === "yaml" ? generateDeterministicYaml(schemaObj) : format === "markdown" ? generateDeterministicMarkdown(schemaObj, { includeToc: options.includeToc }) : format === "mermaid" ? generateDeterministicMermaid(schemaObj) : generateDeterministicJson(schemaObj);
|
|
692
|
+
const outputDir = path6.dirname(options.output);
|
|
693
|
+
if (!fs4.existsSync(outputDir)) {
|
|
694
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
695
|
+
}
|
|
696
|
+
fs4.writeFileSync(options.output, outputStr, "utf-8");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/cache/cache.ts
|
|
701
|
+
import * as fs5 from "node:fs";
|
|
702
|
+
import * as path7 from "node:path";
|
|
703
|
+
import * as crypto2 from "node:crypto";
|
|
704
|
+
function loadCache(rootDir, options) {
|
|
705
|
+
const optionsHash = crypto2.createHash("md5").update(JSON.stringify({
|
|
706
|
+
includeInternalVars: !!options.includeInternalVars,
|
|
707
|
+
includeDocs: !!options.includeDocs,
|
|
708
|
+
includeSignatures: !!options.includeSignatures
|
|
709
|
+
})).digest("hex");
|
|
710
|
+
const cacheDir = path7.join(rootDir, ".code-survey");
|
|
711
|
+
const cacheFile = path7.join(cacheDir, "cache.json");
|
|
712
|
+
let cache = {
|
|
713
|
+
optionsHash,
|
|
714
|
+
files: {}
|
|
715
|
+
};
|
|
716
|
+
if (fs5.existsSync(cacheFile)) {
|
|
717
|
+
try {
|
|
718
|
+
const rawCache = JSON.parse(fs5.readFileSync(cacheFile, "utf-8"));
|
|
719
|
+
if (rawCache && rawCache.optionsHash === optionsHash && rawCache.files) {
|
|
720
|
+
cache = rawCache;
|
|
721
|
+
}
|
|
722
|
+
} catch {}
|
|
723
|
+
}
|
|
724
|
+
return { cache, cacheDir, cacheFile };
|
|
725
|
+
}
|
|
726
|
+
function saveCache(cacheDir, cacheFile, cache) {
|
|
727
|
+
try {
|
|
728
|
+
if (!fs5.existsSync(cacheDir)) {
|
|
729
|
+
fs5.mkdirSync(cacheDir, { recursive: true });
|
|
730
|
+
}
|
|
731
|
+
fs5.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
732
|
+
} catch {}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/git/git.ts
|
|
736
|
+
import { execSync } from "node:child_process";
|
|
737
|
+
function getGitChangedFiles(rootDir, diffTarget) {
|
|
738
|
+
const changed = new Set;
|
|
739
|
+
try {
|
|
740
|
+
const target = typeof diffTarget === "string" ? diffTarget : "HEAD";
|
|
741
|
+
const diffOutput = execSync(`git diff --name-only ${target}`, { cwd: rootDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
742
|
+
for (const line of diffOutput.split(`
|
|
743
|
+
`)) {
|
|
744
|
+
const trimmed = line.trim();
|
|
745
|
+
if (trimmed)
|
|
746
|
+
changed.add(trimmed);
|
|
747
|
+
}
|
|
748
|
+
const statusOutput = execSync("git status --porcelain", { cwd: rootDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
749
|
+
for (const line of statusOutput.split(`
|
|
750
|
+
`)) {
|
|
751
|
+
const trimmed = line.trim();
|
|
752
|
+
if (trimmed) {
|
|
753
|
+
const filePath = trimmed.substring(3).trim();
|
|
754
|
+
if (filePath)
|
|
755
|
+
changed.add(filePath);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch {}
|
|
759
|
+
return changed;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/index.ts
|
|
763
|
+
async function createCodeSurvey(options) {
|
|
764
|
+
let rootDir = path8.resolve(options.root);
|
|
765
|
+
let tempDir = null;
|
|
766
|
+
try {
|
|
767
|
+
if (options.remote) {
|
|
768
|
+
tempDir = fs6.mkdtempSync(path8.join(os2.tmpdir(), "code-survey-remote-"));
|
|
769
|
+
const refArg = options.ref ? `--branch ${options.ref} ` : "";
|
|
770
|
+
execSync2(`git clone --depth 1 ${refArg}${options.remote} "${tempDir}"`, { stdio: ["ignore", "ignore", "pipe"] });
|
|
771
|
+
rootDir = tempDir;
|
|
772
|
+
}
|
|
773
|
+
const ignoreFile = path8.join(rootDir, ".codesurveyignore");
|
|
774
|
+
const ignoreExcludes = [];
|
|
775
|
+
if (fs6.existsSync(ignoreFile)) {
|
|
776
|
+
const content = fs6.readFileSync(ignoreFile, "utf-8");
|
|
777
|
+
for (const line of content.split(`
|
|
778
|
+
`)) {
|
|
779
|
+
const trimmed = line.trim();
|
|
780
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
781
|
+
ignoreExcludes.push(trimmed);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const excludes = [...options.excludes || [], ...ignoreExcludes];
|
|
786
|
+
const scannedFiles = scanDirectory(rootDir, excludes, options.maxDepth);
|
|
787
|
+
let filesToProcess = scannedFiles;
|
|
788
|
+
if (options.diff) {
|
|
789
|
+
const changedFiles = getGitChangedFiles(rootDir, options.diff);
|
|
790
|
+
filesToProcess = scannedFiles.filter((f) => changedFiles.has(f.relativePath));
|
|
791
|
+
}
|
|
792
|
+
const symbolsCount = {
|
|
793
|
+
class: 0,
|
|
794
|
+
interface: 0,
|
|
795
|
+
struct: 0,
|
|
796
|
+
method: 0,
|
|
797
|
+
function: 0,
|
|
798
|
+
variable: 0,
|
|
799
|
+
type: 0
|
|
800
|
+
};
|
|
801
|
+
const parser = new CodebaseParser;
|
|
802
|
+
const extensionsToInitialize = new Set(filesToProcess.map((f) => path8.extname(f.relativePath)));
|
|
803
|
+
await parser.initialize(extensionsToInitialize);
|
|
804
|
+
const files = {};
|
|
805
|
+
const namespaces = {};
|
|
806
|
+
const detectedLanguages = new Set;
|
|
807
|
+
const { cache, cacheDir, cacheFile } = loadCache(rootDir, options);
|
|
808
|
+
const supportedExtensions = new Set([
|
|
809
|
+
".ts",
|
|
810
|
+
".tsx",
|
|
811
|
+
".js",
|
|
812
|
+
".jsx",
|
|
813
|
+
".py",
|
|
814
|
+
".go",
|
|
815
|
+
".java",
|
|
816
|
+
".cs",
|
|
817
|
+
".rs"
|
|
818
|
+
]);
|
|
819
|
+
for (const file of filesToProcess) {
|
|
820
|
+
const ext = path8.extname(file.relativePath);
|
|
821
|
+
if (!supportedExtensions.has(ext)) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
let lang = "";
|
|
825
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
826
|
+
lang = "typescript";
|
|
827
|
+
} else if (ext === ".js" || ext === ".jsx") {
|
|
828
|
+
lang = "javascript";
|
|
829
|
+
} else if (ext === ".py") {
|
|
830
|
+
lang = "python";
|
|
831
|
+
} else if (ext === ".go") {
|
|
832
|
+
lang = "go";
|
|
833
|
+
} else if (ext === ".java") {
|
|
834
|
+
lang = "java";
|
|
835
|
+
} else if (ext === ".cs") {
|
|
836
|
+
lang = "csharp";
|
|
837
|
+
} else if (ext === ".rs") {
|
|
838
|
+
lang = "rust";
|
|
839
|
+
}
|
|
840
|
+
if (lang) {
|
|
841
|
+
detectedLanguages.add(lang);
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
let parseResult;
|
|
845
|
+
const cached = cache.files[file.relativePath];
|
|
846
|
+
if (cached && cached.hash === file.hash) {
|
|
847
|
+
parseResult = cached;
|
|
848
|
+
} else {
|
|
849
|
+
const code = fs6.readFileSync(file.absolutePath, "utf-8");
|
|
850
|
+
parseResult = parser.parseFile(file.relativePath, code, {
|
|
851
|
+
includeInternalVars: options.includeInternalVars,
|
|
852
|
+
includeDocs: options.includeDocs,
|
|
853
|
+
includeSignatures: options.includeSignatures
|
|
854
|
+
});
|
|
855
|
+
cache.files[file.relativePath] = {
|
|
856
|
+
hash: file.hash,
|
|
857
|
+
imports: parseResult.imports,
|
|
858
|
+
exports: parseResult.exports,
|
|
859
|
+
symbols: parseResult.symbols,
|
|
860
|
+
namespace: parseResult.namespace
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const filterSymbols = (list) => {
|
|
864
|
+
if (!options.symbolsFilter)
|
|
865
|
+
return list;
|
|
866
|
+
return list.filter((s) => options.symbolsFilter.includes(s.type));
|
|
867
|
+
};
|
|
868
|
+
const filteredExports = filterSymbols(parseResult.exports);
|
|
869
|
+
const filteredSymbols = filterSymbols(parseResult.symbols);
|
|
870
|
+
for (const exp of filteredExports) {
|
|
871
|
+
if (exp.type in symbolsCount) {
|
|
872
|
+
symbolsCount[exp.type]++;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
for (const sym of filteredSymbols) {
|
|
876
|
+
if (sym.type in symbolsCount) {
|
|
877
|
+
symbolsCount[sym.type]++;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
files[file.relativePath] = {
|
|
881
|
+
hash: file.hash,
|
|
882
|
+
imports: parseResult.imports,
|
|
883
|
+
exports: filteredExports,
|
|
884
|
+
symbols: filteredSymbols
|
|
885
|
+
};
|
|
886
|
+
if (parseResult.namespace) {
|
|
887
|
+
const ns = parseResult.namespace;
|
|
888
|
+
if (!namespaces[ns]) {
|
|
889
|
+
namespaces[ns] = { files: [], exports: [] };
|
|
890
|
+
}
|
|
891
|
+
if (!namespaces[ns].files.includes(file.relativePath)) {
|
|
892
|
+
namespaces[ns].files.push(file.relativePath);
|
|
893
|
+
}
|
|
894
|
+
for (const exp of filteredExports) {
|
|
895
|
+
if (!namespaces[ns].exports.includes(exp.name)) {
|
|
896
|
+
namespaces[ns].exports.push(exp.name);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
} catch {}
|
|
901
|
+
}
|
|
902
|
+
if (!options.remote) {
|
|
903
|
+
saveCache(cacheDir, cacheFile, cache);
|
|
904
|
+
}
|
|
905
|
+
const packageDependencies = parsePackageDependencies(rootDir);
|
|
906
|
+
const { externalImports, internalLinks } = resolveImports(files, namespaces, packageDependencies);
|
|
907
|
+
let projectName = path8.basename(rootDir) || "project";
|
|
908
|
+
if (options.remote) {
|
|
909
|
+
const parts = options.remote.split("/");
|
|
910
|
+
const last = parts[parts.length - 1];
|
|
911
|
+
if (last) {
|
|
912
|
+
projectName = last.replace(/\.git$/, "");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const schemaObj = {
|
|
916
|
+
version: "1.0.0",
|
|
917
|
+
$schema: "https://raw.githubusercontent.com/username/code-survey/main/schema.json",
|
|
918
|
+
project: {
|
|
919
|
+
name: projectName,
|
|
920
|
+
languages: Array.from(detectedLanguages),
|
|
921
|
+
root: "."
|
|
922
|
+
},
|
|
923
|
+
packageDependencies,
|
|
924
|
+
files,
|
|
925
|
+
namespaces,
|
|
926
|
+
externalImports,
|
|
927
|
+
internalLinks
|
|
928
|
+
};
|
|
929
|
+
writeSurveyOutput(schemaObj, options);
|
|
930
|
+
return {
|
|
931
|
+
filesCount: Object.keys(files).length,
|
|
932
|
+
symbolsCount,
|
|
933
|
+
data: schemaObj
|
|
934
|
+
};
|
|
935
|
+
} finally {
|
|
936
|
+
if (tempDir && fs6.existsSync(tempDir)) {
|
|
937
|
+
try {
|
|
938
|
+
fs6.rmSync(tempDir, { recursive: true, force: true });
|
|
939
|
+
} catch {}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/mcp-server.ts
|
|
945
|
+
import * as path9 from "node:path";
|
|
946
|
+
import * as readline from "node:readline";
|
|
947
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
948
|
+
function runMcpServer() {
|
|
949
|
+
const rl = readline.createInterface({
|
|
950
|
+
input: process.stdin,
|
|
951
|
+
output: process.stdout,
|
|
952
|
+
terminal: false
|
|
953
|
+
});
|
|
954
|
+
console.error("Code Survey MCP Server started.");
|
|
955
|
+
rl.on("line", async (line) => {
|
|
956
|
+
if (!line.trim())
|
|
957
|
+
return;
|
|
958
|
+
try {
|
|
959
|
+
const message = JSON.parse(line);
|
|
960
|
+
const { jsonrpc, id, method, params } = message;
|
|
961
|
+
if (jsonrpc !== "2.0") {
|
|
962
|
+
sendError(id, -32600, "Invalid Request");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
switch (method) {
|
|
966
|
+
case "initialize": {
|
|
967
|
+
sendResponse(id, {
|
|
968
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
969
|
+
capabilities: {
|
|
970
|
+
tools: {}
|
|
971
|
+
},
|
|
972
|
+
serverInfo: {
|
|
973
|
+
name: "code-survey-mcp",
|
|
974
|
+
version: "1.0.0"
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
case "notifications/initialized": {
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
case "tools/list": {
|
|
983
|
+
sendResponse(id, {
|
|
984
|
+
tools: [
|
|
985
|
+
{
|
|
986
|
+
name: "get_codebase_survey",
|
|
987
|
+
description: "Generates and returns the complete codebase survey data, including project details, file mappings, dependencies, and syntax symbols.",
|
|
988
|
+
inputSchema: {
|
|
989
|
+
type: "object",
|
|
990
|
+
properties: {
|
|
991
|
+
root: { type: "string", description: "The absolute or relative path to the codebase root (defaults to current working directory)." },
|
|
992
|
+
includeSignatures: { type: "boolean", description: "Include function parameter signatures and return types (defaults to false)." },
|
|
993
|
+
includeDocs: { type: "boolean", description: "Extract docstrings/JSDocs for symbols (defaults to false)." },
|
|
994
|
+
includeInternalVars: { type: "boolean", description: "Include local/internal variables (defaults to false)." }
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "search_symbols",
|
|
1000
|
+
description: "Searches for matching symbols (classes, functions, methods, variables, types) across the codebase.",
|
|
1001
|
+
inputSchema: {
|
|
1002
|
+
type: "object",
|
|
1003
|
+
properties: {
|
|
1004
|
+
query: { type: "string", description: "The search term/substring to look for (case-insensitive)." },
|
|
1005
|
+
root: { type: "string", description: "The codebase root directory path." }
|
|
1006
|
+
},
|
|
1007
|
+
required: ["query"]
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
name: "get_file_symbols",
|
|
1012
|
+
description: "Retrieves parsed symbol structure, imports, and exports for a specific file relative to codebase root.",
|
|
1013
|
+
inputSchema: {
|
|
1014
|
+
type: "object",
|
|
1015
|
+
properties: {
|
|
1016
|
+
file: { type: "string", description: "The relative file path (e.g. src/index.ts)." },
|
|
1017
|
+
root: { type: "string", description: "The codebase root directory path." }
|
|
1018
|
+
},
|
|
1019
|
+
required: ["file"]
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "get_dependencies",
|
|
1024
|
+
description: "Retrieves project-level external/npm and package dependencies.",
|
|
1025
|
+
inputSchema: {
|
|
1026
|
+
type: "object",
|
|
1027
|
+
properties: {
|
|
1028
|
+
root: { type: "string", description: "The codebase root directory path." }
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
});
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
case "tools/call": {
|
|
1037
|
+
const { name, arguments: args } = params || {};
|
|
1038
|
+
const result = await handleToolCall(name, args || {});
|
|
1039
|
+
sendResponse(id, result);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
default: {
|
|
1043
|
+
if (id !== undefined) {
|
|
1044
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
sendError(null, -32700, `Parse error: ${err.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
function sendResponse(id, result) {
|
|
1054
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + `
|
|
1055
|
+
`);
|
|
1056
|
+
}
|
|
1057
|
+
function sendError(id, code, message) {
|
|
1058
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + `
|
|
1059
|
+
`);
|
|
1060
|
+
}
|
|
1061
|
+
async function handleToolCall(name, args) {
|
|
1062
|
+
const root = args.root ? path9.resolve(args.root) : process.cwd();
|
|
1063
|
+
try {
|
|
1064
|
+
switch (name) {
|
|
1065
|
+
case "get_codebase_survey": {
|
|
1066
|
+
const resultObj = await createCodeSurvey({
|
|
1067
|
+
root,
|
|
1068
|
+
includeSignatures: !!args.includeSignatures,
|
|
1069
|
+
includeDocs: !!args.includeDocs,
|
|
1070
|
+
includeInternalVars: !!args.includeInternalVars
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
content: [
|
|
1074
|
+
{
|
|
1075
|
+
type: "text",
|
|
1076
|
+
text: JSON.stringify(resultObj.data, null, 2)
|
|
1077
|
+
}
|
|
1078
|
+
],
|
|
1079
|
+
isError: false
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
case "search_symbols": {
|
|
1083
|
+
const resultObj = await createCodeSurvey({
|
|
1084
|
+
root,
|
|
1085
|
+
includeSignatures: true,
|
|
1086
|
+
includeDocs: true
|
|
1087
|
+
});
|
|
1088
|
+
const query = String(args.query).toLowerCase();
|
|
1089
|
+
const matches = [];
|
|
1090
|
+
for (const [filePath, fileEntry] of Object.entries(resultObj.data.files || {})) {
|
|
1091
|
+
const allSymbols = [...fileEntry.exports || [], ...fileEntry.symbols || []];
|
|
1092
|
+
for (const sym of allSymbols) {
|
|
1093
|
+
if (sym.name.toLowerCase().includes(query)) {
|
|
1094
|
+
matches.push({
|
|
1095
|
+
file: filePath,
|
|
1096
|
+
name: sym.name,
|
|
1097
|
+
type: sym.type,
|
|
1098
|
+
location: sym.location,
|
|
1099
|
+
signature: sym.signature,
|
|
1100
|
+
doc: sym.doc
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
content: [
|
|
1107
|
+
{
|
|
1108
|
+
type: "text",
|
|
1109
|
+
text: JSON.stringify(matches, null, 2)
|
|
1110
|
+
}
|
|
1111
|
+
],
|
|
1112
|
+
isError: false
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
case "get_file_symbols": {
|
|
1116
|
+
const resultObj = await createCodeSurvey({
|
|
1117
|
+
root,
|
|
1118
|
+
includeSignatures: true,
|
|
1119
|
+
includeDocs: true,
|
|
1120
|
+
includeInternalVars: true
|
|
1121
|
+
});
|
|
1122
|
+
const relativeFile = String(args.file).replace(/\\/g, "/");
|
|
1123
|
+
const fileEntry = resultObj.data.files?.[relativeFile];
|
|
1124
|
+
if (!fileEntry) {
|
|
1125
|
+
return {
|
|
1126
|
+
content: [
|
|
1127
|
+
{
|
|
1128
|
+
type: "text",
|
|
1129
|
+
text: `File not found or has no symbols in survey: ${relativeFile}`
|
|
1130
|
+
}
|
|
1131
|
+
],
|
|
1132
|
+
isError: true
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
content: [
|
|
1137
|
+
{
|
|
1138
|
+
type: "text",
|
|
1139
|
+
text: JSON.stringify(fileEntry, null, 2)
|
|
1140
|
+
}
|
|
1141
|
+
],
|
|
1142
|
+
isError: false
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
case "get_dependencies": {
|
|
1146
|
+
const resultObj = await createCodeSurvey({
|
|
1147
|
+
root
|
|
1148
|
+
});
|
|
1149
|
+
const deps = {
|
|
1150
|
+
packageDependencies: resultObj.data.packageDependencies || {},
|
|
1151
|
+
externalImports: resultObj.data.externalImports || []
|
|
1152
|
+
};
|
|
1153
|
+
return {
|
|
1154
|
+
content: [
|
|
1155
|
+
{
|
|
1156
|
+
type: "text",
|
|
1157
|
+
text: JSON.stringify(deps, null, 2)
|
|
1158
|
+
}
|
|
1159
|
+
],
|
|
1160
|
+
isError: false
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
default:
|
|
1164
|
+
return {
|
|
1165
|
+
content: [
|
|
1166
|
+
{
|
|
1167
|
+
type: "text",
|
|
1168
|
+
text: `Unknown tool: ${name}`
|
|
1169
|
+
}
|
|
1170
|
+
],
|
|
1171
|
+
isError: true
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
return {
|
|
1176
|
+
content: [
|
|
1177
|
+
{
|
|
1178
|
+
type: "text",
|
|
1179
|
+
text: `Error executing tool: ${err.message}`
|
|
1180
|
+
}
|
|
1181
|
+
],
|
|
1182
|
+
isError: true
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/cli/cli-parser.ts
|
|
1189
|
+
import * as path11 from "node:path";
|
|
1190
|
+
|
|
1191
|
+
// src/config/config.ts
|
|
1192
|
+
import * as fs7 from "node:fs";
|
|
1193
|
+
import * as path10 from "node:path";
|
|
1194
|
+
async function resolveConfigOptions(configFilePath) {
|
|
1195
|
+
const ext = path10.extname(configFilePath).toLowerCase();
|
|
1196
|
+
const content = fs7.readFileSync(configFilePath, "utf-8");
|
|
1197
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
1198
|
+
try {
|
|
1199
|
+
const { parse: parseYaml } = await import("yaml");
|
|
1200
|
+
return parseYaml(content);
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
console.error(`Error parsing YAML config at ${configFilePath}: ${err.message}`);
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
try {
|
|
1207
|
+
return JSON.parse(content);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
console.error(`Error parsing JSON config at ${configFilePath}: ${err.message}`);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async function loadConfig(configPath, absoluteRoot) {
|
|
1215
|
+
if (configPath) {
|
|
1216
|
+
const absoluteConfig = path10.resolve(configPath);
|
|
1217
|
+
if (!fs7.existsSync(absoluteConfig)) {
|
|
1218
|
+
console.error(`Error: Configuration file not found at ${absoluteConfig}`);
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
return resolveConfigOptions(absoluteConfig);
|
|
1222
|
+
}
|
|
1223
|
+
const jsonConfig = path10.join(absoluteRoot, "code-survey.config.json");
|
|
1224
|
+
const yamlConfig = path10.join(absoluteRoot, "code-survey.config.yaml");
|
|
1225
|
+
const ymlConfig = path10.join(absoluteRoot, "code-survey.config.yml");
|
|
1226
|
+
if (fs7.existsSync(jsonConfig)) {
|
|
1227
|
+
return resolveConfigOptions(jsonConfig);
|
|
1228
|
+
} else if (fs7.existsSync(yamlConfig)) {
|
|
1229
|
+
return resolveConfigOptions(yamlConfig);
|
|
1230
|
+
} else if (fs7.existsSync(ymlConfig)) {
|
|
1231
|
+
return resolveConfigOptions(ymlConfig);
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/cli/cli-parser.ts
|
|
1237
|
+
function printHelp() {
|
|
1238
|
+
console.log(`
|
|
1239
|
+
Code Survey - Deterministically maps codebases for AI coding agents.
|
|
1240
|
+
|
|
1241
|
+
Usage:
|
|
1242
|
+
code-survey [options]
|
|
1243
|
+
|
|
1244
|
+
Options:
|
|
1245
|
+
--root, -r The root directory of the codebase to map (default: current directory)
|
|
1246
|
+
--output, -o The output path for the mapped artifact (default: <root>/code-survey.json)
|
|
1247
|
+
--config, -c Path to a configuration file containing survey option profiles (JSON or YAML)
|
|
1248
|
+
--exclude, -e Comma-separated list of additional file/folder patterns to exclude
|
|
1249
|
+
--format, -f The output format: 'json', 'yaml', 'markdown', or 'mermaid' (default: inferred from output path)
|
|
1250
|
+
--internal-vars Include internal/local variables in the mapping (default: false)
|
|
1251
|
+
--include-docs Extract docstring/JSDoc summaries for class/function symbols (default: false)
|
|
1252
|
+
--include-signatures Extract function/method parameters and return types (default: false)
|
|
1253
|
+
--include-toc Generate a Table of Contents and navigation links in Markdown format (default: false)
|
|
1254
|
+
--split Split the output into multiple modular files (default: false)
|
|
1255
|
+
--max-depth <n> Maximum directory traversal depth (default: unlimited)
|
|
1256
|
+
--symbols-filter Comma-separated list of symbol types to keep (e.g. class,method) (default: all)
|
|
1257
|
+
--diff <target> Generate map only for files changed since <target> (default: HEAD). Staged/unstaged files are always included.
|
|
1258
|
+
--remote <url> Clone and map a remote Git repository (url/ssh path)
|
|
1259
|
+
--ref <name> The branch, tag, or commit hash to check out when mapping a remote repository
|
|
1260
|
+
--watch, -w Watch for file changes and automatically regenerate surveys (default: false)
|
|
1261
|
+
--mcp Run the built-in MCP (Model Context Protocol) stdio server (default: false)
|
|
1262
|
+
--help, -h Show this help message
|
|
1263
|
+
`);
|
|
1264
|
+
}
|
|
1265
|
+
async function parseCliArgs(args, cwd) {
|
|
1266
|
+
let root = cwd;
|
|
1267
|
+
let configPath = null;
|
|
1268
|
+
for (let i = 0;i < args.length; i++) {
|
|
1269
|
+
if (args[i] === "--root" || args[i] === "-r") {
|
|
1270
|
+
if (args[i + 1])
|
|
1271
|
+
root = args[i + 1];
|
|
1272
|
+
} else if (args[i] === "--config" || args[i] === "-c") {
|
|
1273
|
+
if (args[i + 1])
|
|
1274
|
+
configPath = args[i + 1];
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const absoluteRoot = path11.resolve(root);
|
|
1278
|
+
const loadedConfig = await loadConfig(configPath, absoluteRoot);
|
|
1279
|
+
let isMultiSurvey = false;
|
|
1280
|
+
let surveysList = [];
|
|
1281
|
+
let configOptions = {};
|
|
1282
|
+
if (loadedConfig) {
|
|
1283
|
+
if (Array.isArray(loadedConfig)) {
|
|
1284
|
+
isMultiSurvey = true;
|
|
1285
|
+
surveysList = loadedConfig;
|
|
1286
|
+
} else if (Array.isArray(loadedConfig.surveys)) {
|
|
1287
|
+
isMultiSurvey = true;
|
|
1288
|
+
surveysList = loadedConfig.surveys;
|
|
1289
|
+
} else {
|
|
1290
|
+
configOptions = loadedConfig;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
let output = configOptions.output;
|
|
1294
|
+
let excludes = configOptions.excludes || [];
|
|
1295
|
+
let format = configOptions.format;
|
|
1296
|
+
let includeInternalVars = configOptions.includeInternalVars ?? false;
|
|
1297
|
+
let includeDocs = configOptions.includeDocs ?? false;
|
|
1298
|
+
let includeSignatures = configOptions.includeSignatures ?? false;
|
|
1299
|
+
let includeToc = configOptions.includeToc ?? false;
|
|
1300
|
+
let split = configOptions.split ?? false;
|
|
1301
|
+
let maxDepth = configOptions.maxDepth;
|
|
1302
|
+
let symbolsFilter = configOptions.symbolsFilter;
|
|
1303
|
+
let diff = configOptions.diff;
|
|
1304
|
+
let remote = configOptions.remote;
|
|
1305
|
+
let ref = configOptions.ref;
|
|
1306
|
+
let watchMode = configOptions.watch ?? false;
|
|
1307
|
+
let mcpMode = configOptions.mcp ?? false;
|
|
1308
|
+
let hasCliOverrides = false;
|
|
1309
|
+
for (let i = 0;i < args.length; i++) {
|
|
1310
|
+
const arg = args[i];
|
|
1311
|
+
if (arg === "--help" || arg === "-h") {
|
|
1312
|
+
printHelp();
|
|
1313
|
+
process.exit(0);
|
|
1314
|
+
} else if (arg === "--root" || arg === "-r") {
|
|
1315
|
+
i++;
|
|
1316
|
+
} else if (arg === "--config" || arg === "-c") {
|
|
1317
|
+
i++;
|
|
1318
|
+
} else {
|
|
1319
|
+
hasCliOverrides = true;
|
|
1320
|
+
if (arg === "--output" || arg === "-o") {
|
|
1321
|
+
output = args[++i];
|
|
1322
|
+
} else if (arg === "--exclude" || arg === "-e") {
|
|
1323
|
+
const patterns = args[++i];
|
|
1324
|
+
if (patterns) {
|
|
1325
|
+
excludes = [
|
|
1326
|
+
...excludes,
|
|
1327
|
+
...patterns.split(",").map((p) => p.trim())
|
|
1328
|
+
];
|
|
1329
|
+
}
|
|
1330
|
+
} else if (arg === "--format" || arg === "-f") {
|
|
1331
|
+
const val = args[++i];
|
|
1332
|
+
if (val === "json" || val === "yaml" || val === "markdown" || val === "mermaid") {
|
|
1333
|
+
format = val;
|
|
1334
|
+
} else {
|
|
1335
|
+
console.error(`Error: format must be 'json', 'yaml', 'markdown', or 'mermaid', got '${val}'.`);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
} else if (arg === "--internal-vars") {
|
|
1339
|
+
includeInternalVars = true;
|
|
1340
|
+
} else if (arg === "--include-docs") {
|
|
1341
|
+
includeDocs = true;
|
|
1342
|
+
} else if (arg === "--include-signatures") {
|
|
1343
|
+
includeSignatures = true;
|
|
1344
|
+
} else if (arg === "--include-toc") {
|
|
1345
|
+
includeToc = true;
|
|
1346
|
+
} else if (arg === "--split") {
|
|
1347
|
+
split = true;
|
|
1348
|
+
} else if (arg === "--watch" || arg === "-w") {
|
|
1349
|
+
watchMode = true;
|
|
1350
|
+
} else if (arg === "--mcp") {
|
|
1351
|
+
mcpMode = true;
|
|
1352
|
+
} else if (arg === "--max-depth") {
|
|
1353
|
+
const val = args[++i];
|
|
1354
|
+
maxDepth = parseInt(val, 10);
|
|
1355
|
+
if (isNaN(maxDepth)) {
|
|
1356
|
+
console.error(`Error: --max-depth must be a number, got '${val}'.`);
|
|
1357
|
+
process.exit(1);
|
|
1358
|
+
}
|
|
1359
|
+
} else if (arg === "--symbols-filter") {
|
|
1360
|
+
const val = args[++i];
|
|
1361
|
+
if (val) {
|
|
1362
|
+
symbolsFilter = val.split(",").map((s) => s.trim());
|
|
1363
|
+
}
|
|
1364
|
+
} else if (arg === "--diff") {
|
|
1365
|
+
const nextArg = args[i + 1];
|
|
1366
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
1367
|
+
diff = nextArg;
|
|
1368
|
+
i++;
|
|
1369
|
+
} else {
|
|
1370
|
+
diff = true;
|
|
1371
|
+
}
|
|
1372
|
+
} else if (arg === "--remote") {
|
|
1373
|
+
remote = args[++i];
|
|
1374
|
+
} else if (arg === "--ref") {
|
|
1375
|
+
ref = args[++i];
|
|
1376
|
+
} else {
|
|
1377
|
+
console.error(`Unknown argument: ${arg}`);
|
|
1378
|
+
printHelp();
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
absoluteRoot,
|
|
1385
|
+
isMultiSurvey,
|
|
1386
|
+
surveysList,
|
|
1387
|
+
hasCliOverrides,
|
|
1388
|
+
options: {
|
|
1389
|
+
output,
|
|
1390
|
+
excludes,
|
|
1391
|
+
format,
|
|
1392
|
+
includeInternalVars,
|
|
1393
|
+
includeDocs,
|
|
1394
|
+
includeSignatures,
|
|
1395
|
+
includeToc,
|
|
1396
|
+
split,
|
|
1397
|
+
maxDepth,
|
|
1398
|
+
symbolsFilter,
|
|
1399
|
+
diff,
|
|
1400
|
+
remote,
|
|
1401
|
+
ref,
|
|
1402
|
+
watchMode,
|
|
1403
|
+
mcpMode
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/cli/watcher.ts
|
|
1409
|
+
import * as fs8 from "node:fs";
|
|
1410
|
+
import * as path12 from "node:path";
|
|
1411
|
+
function startWatcher(absoluteRoot, isMultiSurvey, hasCliOverrides, surveysList, output, split, excludes, executeSurveys) {
|
|
1412
|
+
const allOutputPaths = new Set;
|
|
1413
|
+
if (isMultiSurvey && !hasCliOverrides) {
|
|
1414
|
+
for (const s of surveysList) {
|
|
1415
|
+
const resolvedRoot = s.root ? path12.resolve(absoluteRoot, s.root) : absoluteRoot;
|
|
1416
|
+
const out = s.output ? path12.resolve(resolvedRoot, s.output) : s.split ? path12.join(resolvedRoot, "code-survey-results") : path12.join(resolvedRoot, "code-survey.json");
|
|
1417
|
+
allOutputPaths.add(out);
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
const out = output ? path12.resolve(output) : split ? path12.join(absoluteRoot, "code-survey-results") : path12.join(absoluteRoot, "code-survey.json");
|
|
1421
|
+
allOutputPaths.add(out);
|
|
1422
|
+
}
|
|
1423
|
+
console.error(`
|
|
1424
|
+
Watching for changes in ${absoluteRoot}... (Press Ctrl+C to stop)`);
|
|
1425
|
+
let debounceTimeout = null;
|
|
1426
|
+
let isRunning = false;
|
|
1427
|
+
return fs8.watch(absoluteRoot, { recursive: true }, (eventType, filename) => {
|
|
1428
|
+
if (!filename)
|
|
1429
|
+
return;
|
|
1430
|
+
const relPath = filename.replace(/\\/g, "/");
|
|
1431
|
+
if (relPath.startsWith(".git/") || relPath === ".git" || relPath.startsWith(".code-survey/") || relPath === ".code-survey" || relPath.startsWith("node_modules/") || relPath === "node_modules") {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
for (const pattern of excludes) {
|
|
1435
|
+
if (relPath.includes(pattern) || relPath.startsWith(pattern)) {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const absChangedPath = path12.resolve(absoluteRoot, filename);
|
|
1440
|
+
for (const outPath of allOutputPaths) {
|
|
1441
|
+
if (absChangedPath === outPath || absChangedPath.startsWith(outPath + path12.sep)) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (isRunning)
|
|
1446
|
+
return;
|
|
1447
|
+
if (debounceTimeout)
|
|
1448
|
+
clearTimeout(debounceTimeout);
|
|
1449
|
+
debounceTimeout = setTimeout(async () => {
|
|
1450
|
+
isRunning = true;
|
|
1451
|
+
console.error(`
|
|
1452
|
+
File change detected: ${relPath}. Regenerating surveys...`);
|
|
1453
|
+
try {
|
|
1454
|
+
await executeSurveys();
|
|
1455
|
+
console.error(`Regeneration complete.`);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
console.error(`Error regenerating: ${err.message}`);
|
|
1458
|
+
} finally {
|
|
1459
|
+
isRunning = false;
|
|
1460
|
+
}
|
|
1461
|
+
}, 300);
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/cli/token-estimator.ts
|
|
1466
|
+
import * as fs9 from "node:fs";
|
|
1467
|
+
import * as path13 from "node:path";
|
|
1468
|
+
function reportTokenBudget(absoluteOutput, format, split, result) {
|
|
1469
|
+
if (!fs9.existsSync(absoluteOutput))
|
|
1470
|
+
return;
|
|
1471
|
+
const isDir = fs9.statSync(absoluteOutput).isDirectory();
|
|
1472
|
+
let sizeBytes = 0;
|
|
1473
|
+
let totalLength = 0;
|
|
1474
|
+
const actualExt = isDir ? "" : path13.extname(absoluteOutput).toLowerCase();
|
|
1475
|
+
const actualFormat = format || (isDir ? "json" : actualExt === ".yaml" || actualExt === ".yml" ? "yaml" : actualExt === ".md" || actualExt === ".markdown" ? "markdown" : actualExt === ".mermaid" || actualExt === ".mmd" ? "mermaid" : "json");
|
|
1476
|
+
if (isDir) {
|
|
1477
|
+
const files = fs9.readdirSync(absoluteOutput);
|
|
1478
|
+
for (const f of files) {
|
|
1479
|
+
const fPath = path13.join(absoluteOutput, f);
|
|
1480
|
+
if (fs9.statSync(fPath).isFile()) {
|
|
1481
|
+
sizeBytes += fs9.statSync(fPath).size;
|
|
1482
|
+
totalLength += fs9.readFileSync(fPath, "utf-8").length;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
sizeBytes = fs9.statSync(absoluteOutput).size;
|
|
1487
|
+
totalLength = fs9.readFileSync(absoluteOutput, "utf-8").length;
|
|
1488
|
+
}
|
|
1489
|
+
const sizeKb = (sizeBytes / 1024).toFixed(1);
|
|
1490
|
+
let charToTokenRatio = 3.2;
|
|
1491
|
+
if (actualFormat === "yaml") {
|
|
1492
|
+
charToTokenRatio = 3.7;
|
|
1493
|
+
} else if (actualFormat === "markdown") {
|
|
1494
|
+
charToTokenRatio = 4.2;
|
|
1495
|
+
} else if (actualFormat === "mermaid") {
|
|
1496
|
+
charToTokenRatio = 3.8;
|
|
1497
|
+
}
|
|
1498
|
+
const estTokens = Math.round(totalLength / charToTokenRatio);
|
|
1499
|
+
process.stderr.write(`
|
|
1500
|
+
--- Code Survey Token Budget Estimator ---
|
|
1501
|
+
`);
|
|
1502
|
+
process.stderr.write(`Output Format: ${actualFormat}${isDir ? " (split)" : ""}
|
|
1503
|
+
`);
|
|
1504
|
+
process.stderr.write(`Files Mapped: ${result.filesCount}
|
|
1505
|
+
`);
|
|
1506
|
+
const symbolLines = [];
|
|
1507
|
+
for (const [key, val] of Object.entries(result.symbolsCount)) {
|
|
1508
|
+
if (val > 0) {
|
|
1509
|
+
symbolLines.push(` - ${key}: ${val}`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (symbolLines.length > 0) {
|
|
1513
|
+
process.stderr.write(`Symbol Breakdown:
|
|
1514
|
+
`);
|
|
1515
|
+
process.stderr.write(symbolLines.join(`
|
|
1516
|
+
`) + `
|
|
1517
|
+
`);
|
|
1518
|
+
}
|
|
1519
|
+
process.stderr.write(`File Size: ${sizeKb} KB (${sizeBytes} bytes)
|
|
1520
|
+
`);
|
|
1521
|
+
process.stderr.write(`Estimated Tokens: ~${estTokens.toLocaleString()} tokens
|
|
1522
|
+
`);
|
|
1523
|
+
process.stderr.write(`--------------------------------------
|
|
1524
|
+
|
|
1525
|
+
`);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// bin/code-survey.ts
|
|
1529
|
+
import * as path14 from "node:path";
|
|
1530
|
+
async function runSurvey(opts) {
|
|
1531
|
+
const absoluteRoot = path14.resolve(opts.root);
|
|
1532
|
+
const absoluteOutput = opts.output ? path14.resolve(opts.output) : opts.split ? path14.join(absoluteRoot, "code-survey-results") : path14.join(absoluteRoot, "code-survey.json");
|
|
1533
|
+
if (opts.remote) {
|
|
1534
|
+
console.error(`Remote repository: ${opts.remote}`);
|
|
1535
|
+
if (opts.ref) {
|
|
1536
|
+
console.error(`Ref/Branch: ${opts.ref}`);
|
|
1537
|
+
}
|
|
1538
|
+
} else {
|
|
1539
|
+
console.error(`Mapping codebase at: ${absoluteRoot}`);
|
|
1540
|
+
}
|
|
1541
|
+
console.error(`Output target: ${absoluteOutput}`);
|
|
1542
|
+
const result = await createCodeSurvey({
|
|
1543
|
+
root: absoluteRoot,
|
|
1544
|
+
output: absoluteOutput,
|
|
1545
|
+
excludes: opts.excludes,
|
|
1546
|
+
format: opts.format,
|
|
1547
|
+
includeInternalVars: opts.includeInternalVars,
|
|
1548
|
+
includeDocs: opts.includeDocs,
|
|
1549
|
+
includeSignatures: opts.includeSignatures,
|
|
1550
|
+
includeToc: opts.includeToc,
|
|
1551
|
+
maxDepth: opts.maxDepth,
|
|
1552
|
+
symbolsFilter: opts.symbolsFilter,
|
|
1553
|
+
diff: opts.diff,
|
|
1554
|
+
remote: opts.remote,
|
|
1555
|
+
ref: opts.ref,
|
|
1556
|
+
split: opts.split
|
|
1557
|
+
});
|
|
1558
|
+
console.error("Code Survey generated successfully!");
|
|
1559
|
+
reportTokenBudget(absoluteOutput, opts.format, opts.split, result);
|
|
1560
|
+
}
|
|
1561
|
+
async function main() {
|
|
1562
|
+
const { absoluteRoot, isMultiSurvey, surveysList, hasCliOverrides, options } = await parseCliArgs(process.argv.slice(2), process.cwd());
|
|
1563
|
+
const executeSurveys = async () => {
|
|
1564
|
+
if (isMultiSurvey && !hasCliOverrides) {
|
|
1565
|
+
console.error(`Running batch code surveys from configuration (${surveysList.length} targets)...`);
|
|
1566
|
+
for (const survey of surveysList) {
|
|
1567
|
+
console.error(`
|
|
1568
|
+
----------------------------------------`);
|
|
1569
|
+
const resolvedRoot = survey.root ? path14.resolve(absoluteRoot, survey.root) : absoluteRoot;
|
|
1570
|
+
await runSurvey({
|
|
1571
|
+
root: resolvedRoot,
|
|
1572
|
+
output: survey.output ? path14.resolve(resolvedRoot, survey.output) : "",
|
|
1573
|
+
excludes: survey.excludes || [],
|
|
1574
|
+
format: survey.format,
|
|
1575
|
+
includeInternalVars: survey.includeInternalVars ?? false,
|
|
1576
|
+
includeDocs: survey.includeDocs ?? false,
|
|
1577
|
+
includeSignatures: survey.includeSignatures ?? false,
|
|
1578
|
+
includeToc: survey.includeToc ?? false,
|
|
1579
|
+
maxDepth: survey.maxDepth,
|
|
1580
|
+
symbolsFilter: survey.symbolsFilter,
|
|
1581
|
+
diff: survey.diff,
|
|
1582
|
+
remote: survey.remote,
|
|
1583
|
+
ref: survey.ref,
|
|
1584
|
+
split: survey.split ?? false
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
} else {
|
|
1588
|
+
await runSurvey({
|
|
1589
|
+
root: absoluteRoot,
|
|
1590
|
+
output: options.output || "",
|
|
1591
|
+
excludes: options.excludes,
|
|
1592
|
+
format: options.format,
|
|
1593
|
+
includeInternalVars: options.includeInternalVars,
|
|
1594
|
+
includeDocs: options.includeDocs,
|
|
1595
|
+
includeSignatures: options.includeSignatures,
|
|
1596
|
+
includeToc: options.includeToc,
|
|
1597
|
+
maxDepth: options.maxDepth,
|
|
1598
|
+
symbolsFilter: options.symbolsFilter,
|
|
1599
|
+
diff: options.diff,
|
|
1600
|
+
remote: options.remote,
|
|
1601
|
+
ref: options.ref,
|
|
1602
|
+
split: options.split
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
try {
|
|
1607
|
+
if (options.mcpMode) {
|
|
1608
|
+
runMcpServer();
|
|
1609
|
+
} else {
|
|
1610
|
+
await executeSurveys();
|
|
1611
|
+
if (options.watchMode) {
|
|
1612
|
+
startWatcher(absoluteRoot, isMultiSurvey, hasCliOverrides, surveysList, options.output, options.split, options.excludes, executeSurveys);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
console.error(`Error generating code-survey: ${err.message}`);
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
main();
|