agentsys 5.6.4 → 5.8.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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +30 -19
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.kiro/agents/exploration-agent.json +1 -1
  4. package/.kiro/agents/implementation-agent.json +1 -1
  5. package/.kiro/agents/map-validator.json +2 -2
  6. package/.kiro/agents/perf-orchestrator.json +1 -1
  7. package/.kiro/agents/planning-agent.json +1 -1
  8. package/.kiro/skills/perf-code-paths/SKILL.md +1 -1
  9. package/.kiro/skills/perf-theory-gatherer/SKILL.md +1 -1
  10. package/.kiro/skills/repo-intel/SKILL.md +63 -0
  11. package/AGENTS.md +10 -8
  12. package/CHANGELOG.md +37 -0
  13. package/README.md +152 -98
  14. package/lib/binary/version.js +1 -1
  15. package/lib/repo-map/converter.js +130 -0
  16. package/lib/repo-map/index.js +117 -74
  17. package/lib/repo-map/installer.js +38 -172
  18. package/lib/repo-map/updater.js +16 -474
  19. package/meta/skills/maintain-cross-platform/SKILL.md +7 -6
  20. package/package.json +3 -3
  21. package/scripts/fix-graduated-repos.js +2 -2
  22. package/scripts/generate-docs.js +22 -16
  23. package/scripts/graduate-plugin.js +1 -1
  24. package/scripts/plugins.txt +7 -1
  25. package/scripts/preflight.js +4 -4
  26. package/scripts/validate-cross-platform-docs.js +2 -2
  27. package/site/content.json +40 -23
  28. package/site/index.html +44 -12
  29. package/site/ux-spec.md +6 -6
  30. package/.kiro/skills/repo-mapping/SKILL.md +0 -83
  31. package/lib/repo-map/concurrency.js +0 -29
  32. package/lib/repo-map/queries/go.js +0 -27
  33. package/lib/repo-map/queries/index.js +0 -100
  34. package/lib/repo-map/queries/java.js +0 -38
  35. package/lib/repo-map/queries/javascript.js +0 -55
  36. package/lib/repo-map/queries/python.js +0 -24
  37. package/lib/repo-map/queries/rust.js +0 -73
  38. package/lib/repo-map/queries/typescript.js +0 -38
  39. package/lib/repo-map/runner.js +0 -1364
  40. package/lib/repo-map/usage-analyzer.js +0 -407
@@ -1,1364 +0,0 @@
1
- /**
2
- * ast-grep execution and result parsing
3
- *
4
- * @module lib/repo-map/runner
5
- */
6
-
7
- 'use strict';
8
-
9
- const { execFileSync, spawnSync, spawn } = require('child_process');
10
- const path = require('path');
11
- const fs = require('fs');
12
- const fsPromises = require('fs').promises;
13
- const crypto = require('crypto');
14
-
15
- const installer = require('./installer');
16
- const queries = require('./queries');
17
- const slopAnalyzers = require('../patterns/slop-analyzers');
18
- const { runWithConcurrency } = require('./concurrency');
19
-
20
- // Language file extensions mapping
21
- const LANGUAGE_EXTENSIONS = {
22
- javascript: ['.js', '.jsx', '.mjs', '.cjs'],
23
- typescript: ['.ts', '.tsx', '.mts', '.cts'],
24
- python: ['.py', '.pyw'],
25
- rust: ['.rs'],
26
- go: ['.go'],
27
- java: ['.java']
28
- };
29
-
30
- // Directories to exclude from scanning (extend base list)
31
- const EXCLUDE_DIRS = Array.from(new Set([
32
- ...slopAnalyzers.EXCLUDE_DIRS,
33
- '.claude', '.opencode', '.codex', '.venv', 'venv', 'env'
34
- ]));
35
-
36
- const AST_GREP_BATCH_SIZE = 100;
37
- const AST_GREP_CONCURRENCY = 4;
38
- const LANGUAGE_EXTENSION_SCAN_LIMIT = 500;
39
- const FILE_READ_BATCH_SIZE = 50; // Concurrent file reads
40
-
41
- /**
42
- * Detect languages in a repository
43
- * @param {string} basePath - Repository root
44
- * @returns {Promise<string[]>} - List of detected languages
45
- */
46
- async function detectLanguages(basePath) {
47
- const detected = new Set();
48
-
49
- // Check for config files first (faster)
50
- const configIndicators = {
51
- javascript: ['package.json', 'jsconfig.json'],
52
- typescript: ['tsconfig.json', 'tsconfig.base.json'],
53
- python: ['pyproject.toml', 'setup.py', 'requirements.txt', 'Pipfile'],
54
- rust: ['Cargo.toml'],
55
- go: ['go.mod', 'go.sum'],
56
- java: ['pom.xml', 'build.gradle', 'build.gradle.kts']
57
- };
58
-
59
- for (const [lang, files] of Object.entries(configIndicators)) {
60
- for (const file of files) {
61
- if (fs.existsSync(path.join(basePath, file))) {
62
- detected.add(lang);
63
- break;
64
- }
65
- }
66
- }
67
-
68
- // Supplement with extension scan to catch mixed-language repos
69
- const extensions = scanForExtensions(basePath, LANGUAGE_EXTENSION_SCAN_LIMIT);
70
- for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
71
- if (exts.some(ext => extensions.has(ext))) {
72
- detected.add(lang);
73
- }
74
- }
75
-
76
- return Array.from(detected);
77
- }
78
-
79
- /**
80
- * Scan repository for file extensions (sampling)
81
- * @param {string} basePath - Repository root
82
- * @param {number} maxFiles - Maximum files to check
83
- * @returns {Set<string>} - Set of extensions found
84
- */
85
- function scanForExtensions(basePath, maxFiles = 100) {
86
- const extensions = new Set();
87
- let count = 0;
88
- const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
89
-
90
- function scan(dir) {
91
- if (count >= maxFiles) return;
92
-
93
- try {
94
- const entries = fs.readdirSync(dir, { withFileTypes: true });
95
- for (const entry of entries) {
96
- if (count >= maxFiles) break;
97
-
98
- if (entry.isDirectory()) {
99
- const relativePath = path.relative(basePath, path.join(dir, entry.name));
100
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
101
- if (isIgnored && isIgnored(relativePath, true)) continue;
102
- if (!entry.name.startsWith('.')) {
103
- scan(path.join(dir, entry.name));
104
- }
105
- } else if (entry.isFile()) {
106
- const relativePath = path.relative(basePath, path.join(dir, entry.name));
107
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
108
- if (isIgnored && isIgnored(relativePath, false)) continue;
109
- const ext = path.extname(entry.name).toLowerCase();
110
- if (ext) {
111
- extensions.add(ext);
112
- count++;
113
- }
114
- }
115
- }
116
- } catch {
117
- // Skip directories we can't read
118
- }
119
- }
120
-
121
- scan(basePath);
122
- return extensions;
123
- }
124
-
125
- /**
126
- * Run a full scan of the repository
127
- * @param {string} basePath - Repository root
128
- * @param {string[]} languages - Languages to scan
129
- * @returns {Promise<Object>} - The generated map
130
- */
131
- async function fullScan(basePath, languages, options = {}) {
132
- const cmd = installer.getCommand();
133
- if (!cmd) {
134
- throw new Error('ast-grep not found');
135
- }
136
- const fileLimit = Number.isFinite(options.fileLimit) ? Math.max(0, Math.floor(options.fileLimit)) : null;
137
- const filesByLanguage = collectFilesByLanguage(basePath, languages, {
138
- maxFiles: fileLimit
139
- });
140
-
141
- const map = {
142
- version: '1.0.0',
143
- generated: new Date().toISOString(),
144
- updated: null,
145
- git: getGitInfo(basePath),
146
- project: {
147
- type: detectProjectType(languages),
148
- languages,
149
- frameworks: [] // Could be enhanced later
150
- },
151
- stats: {
152
- totalFiles: 0,
153
- totalSymbols: 0,
154
- scanDurationMs: 0,
155
- errors: []
156
- },
157
- files: {},
158
- dependencies: {}
159
- };
160
-
161
- // Run queries for each language
162
- for (const lang of languages) {
163
- const langQueries = queries.getQueriesForLanguage(lang);
164
- if (!langQueries) continue;
165
-
166
- const files = filesByLanguage.get(lang) || [];
167
- if (files.length === 0) continue;
168
-
169
- const fileEntries = [];
170
- const symbolMapsByFile = new Map();
171
- const importStateByFile = new Map();
172
- const contentByFile = new Map();
173
-
174
- // Filter out already processed files first
175
- const filesToProcess = files.filter(file => {
176
- const relativePath = path.relative(basePath, file).replace(/\\/g, '/');
177
- return !map.files[relativePath];
178
- });
179
-
180
- // Batch read all files asynchronously
181
- const fileContents = await batchReadFiles(filesToProcess);
182
-
183
- for (const file of filesToProcess) {
184
- const relativePath = path.relative(basePath, file).replace(/\\/g, '/');
185
- const readResult = fileContents.get(file);
186
-
187
- if (readResult.error || readResult.content === null) {
188
- map.stats.errors.push({
189
- file: relativePath,
190
- error: readResult.error?.message || 'Failed to read file'
191
- });
192
- continue;
193
- }
194
-
195
- const content = readResult.content;
196
- const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
197
-
198
- map.files[relativePath] = {
199
- hash,
200
- language: lang,
201
- size: content.length,
202
- symbols: {
203
- exports: [],
204
- functions: [],
205
- classes: [],
206
- types: [],
207
- constants: []
208
- },
209
- imports: []
210
- };
211
-
212
- map.stats.totalFiles++;
213
- fileEntries.push({ file, relativePath });
214
- symbolMapsByFile.set(relativePath, createSymbolMaps());
215
- importStateByFile.set(relativePath, { items: [], seen: new Set() });
216
- contentByFile.set(relativePath, content);
217
- }
218
-
219
- if (fileEntries.length === 0) continue;
220
-
221
- const filesBySgLang = new Map();
222
- for (const entry of fileEntries) {
223
- const sgLang = queries.getSgLanguageForFile(entry.file, lang);
224
- if (!filesBySgLang.has(sgLang)) {
225
- filesBySgLang.set(sgLang, []);
226
- }
227
- filesBySgLang.get(sgLang).push(entry);
228
- }
229
-
230
- for (const [sgLang, entries] of filesBySgLang) {
231
- const filePaths = entries.map(entry => entry.file);
232
- const experimentBatchSize = process.env.PERF_EXPERIMENT === '1'
233
- ? Number(process.env.REPO_MAP_AST_GREP_BATCH_SIZE)
234
- : null;
235
- const batchSize = Number.isFinite(options.astGrepBatchSize)
236
- ? Math.max(1, Math.floor(options.astGrepBatchSize))
237
- : Number.isFinite(experimentBatchSize) && experimentBatchSize > 0
238
- ? Math.max(1, Math.floor(experimentBatchSize))
239
- : AST_GREP_BATCH_SIZE;
240
- const chunks = chunkArray(filePaths, batchSize);
241
-
242
- const patternGroups = [
243
- { category: 'exports', patterns: langQueries.exports, defaultKind: 'export' },
244
- { category: 'functions', patterns: langQueries.functions, defaultKind: 'function' },
245
- { category: 'classes', patterns: langQueries.classes, defaultKind: 'class' },
246
- { category: 'types', patterns: langQueries.types, defaultKind: 'type' },
247
- { category: 'constants', patterns: langQueries.constants, defaultKind: 'constant' },
248
- { category: 'imports', patterns: langQueries.imports, defaultKind: 'import' }
249
- ];
250
-
251
- for (const group of patternGroups) {
252
- if (!group.patterns || group.patterns.length === 0) continue;
253
-
254
- for (const patternDef of group.patterns) {
255
- const pattern = typeof patternDef === 'string' ? patternDef : patternDef.pattern;
256
- if (!pattern) continue;
257
-
258
- const matchesByChunk = await runAstGrepPatternBatches(cmd, pattern, sgLang, basePath, chunks, {
259
- onError: (error) => map.stats.errors.push(error),
260
- concurrency: options.astGrepConcurrency
261
- });
262
-
263
- for (const matches of matchesByChunk) {
264
- for (const match of matches) {
265
- const matchedPath = normalizeMatchPath(match.file, basePath);
266
- if (!matchedPath) continue;
267
-
268
- const symbolMaps = symbolMapsByFile.get(matchedPath);
269
- const importState = importStateByFile.get(matchedPath);
270
- if (!symbolMaps || !importState) continue;
271
-
272
- if (group.category === 'imports') {
273
- const sourceResult = extractSourceFromMatch(match, patternDef);
274
- const sources = Array.isArray(sourceResult) ? sourceResult : [sourceResult];
275
- for (const source of sources) {
276
- if (!source) continue;
277
- const kind = patternDef.kind || 'import';
278
- const key = `${source}:${kind}`;
279
- if (importState.seen.has(key)) continue;
280
- importState.seen.add(key);
281
- importState.items.push({
282
- source,
283
- kind,
284
- line: getLine(match)
285
- });
286
- }
287
- continue;
288
- }
289
-
290
- const names = extractNamesFromMatch(match, patternDef);
291
- const targetMap = symbolMaps[group.category];
292
- if (!targetMap) continue;
293
- for (const name of names) {
294
- const kind = patternDef.kind || group.defaultKind;
295
- addSymbolToMap(targetMap, name, match, kind, patternDef.extra);
296
- }
297
- }
298
- }
299
- }
300
- }
301
- }
302
-
303
- for (const entry of fileEntries) {
304
- const relativePath = entry.relativePath;
305
- const symbolMaps = symbolMapsByFile.get(relativePath);
306
- const importState = importStateByFile.get(relativePath);
307
- if (!symbolMaps || !importState) continue;
308
-
309
- const exportNames = new Set(symbolMaps.exports.keys());
310
- const content = contentByFile.get(relativePath) || '';
311
- applyLanguageExportRules(lang, content, exportNames, symbolMaps.functions, symbolMaps.classes, symbolMaps.types, symbolMaps.constants);
312
- ensureExportEntries(symbolMaps.exports, exportNames, symbolMaps.functions, symbolMaps.classes, symbolMaps.types, symbolMaps.constants);
313
-
314
- const symbols = {
315
- exports: mapToSortedArray(symbolMaps.exports),
316
- functions: mapToSortedArray(symbolMaps.functions, exportNames),
317
- classes: mapToSortedArray(symbolMaps.classes, exportNames),
318
- types: mapToSortedArray(symbolMaps.types, exportNames),
319
- constants: mapToSortedArray(symbolMaps.constants, exportNames)
320
- };
321
-
322
- map.files[relativePath].symbols = symbols;
323
- map.files[relativePath].imports = importState.items;
324
-
325
- if (importState.items.length > 0) {
326
- map.dependencies[relativePath] = Array.from(new Set(importState.items.map(imp => imp.source)));
327
- }
328
-
329
- map.stats.totalSymbols +=
330
- (symbols.functions?.length || 0) +
331
- (symbols.classes?.length || 0) +
332
- (symbols.types?.length || 0) +
333
- (symbols.constants?.length || 0);
334
- }
335
- }
336
-
337
- return map;
338
- }
339
-
340
- /**
341
- * Find all files for a language
342
- * @param {string} basePath - Repository root
343
- * @param {string} language - Language name
344
- * @returns {string[]} - Array of file paths
345
- */
346
- function findFilesForLanguage(basePath, language, options = {}) {
347
- const extensions = LANGUAGE_EXTENSIONS[language] || [];
348
- const files = [];
349
- const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
350
- const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(0, Math.floor(options.maxFiles)) : null;
351
-
352
- function scan(dir) {
353
- if (maxFiles !== null && files.length >= maxFiles) return;
354
- try {
355
- const entries = fs.readdirSync(dir, { withFileTypes: true });
356
- for (const entry of entries) {
357
- if (maxFiles !== null && files.length >= maxFiles) break;
358
- const fullPath = path.join(dir, entry.name);
359
-
360
- if (entry.isDirectory()) {
361
- const relativePath = path.relative(basePath, fullPath);
362
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
363
- if (isIgnored && isIgnored(relativePath, true)) continue;
364
- if (!entry.name.startsWith('.')) {
365
- scan(fullPath);
366
- }
367
- } else if (entry.isFile()) {
368
- const relativePath = path.relative(basePath, fullPath);
369
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
370
- if (isIgnored && isIgnored(relativePath, false)) continue;
371
- const ext = path.extname(entry.name).toLowerCase();
372
- if (extensions.includes(ext)) {
373
- files.push(fullPath);
374
- if (maxFiles !== null && files.length >= maxFiles) break;
375
- }
376
- }
377
- }
378
- } catch {
379
- // Skip directories we can't read
380
- }
381
- }
382
-
383
- scan(basePath);
384
- return files;
385
- }
386
-
387
- /**
388
- * Collect files for all languages in a single walk.
389
- * @param {string} basePath - Repository root
390
- * @param {string[]} languages - Languages to collect
391
- * @param {Object} options
392
- * @param {number} [options.maxFiles] - Global file limit
393
- * @returns {Map<string, string[]>} - Map of language -> file paths
394
- */
395
- function collectFilesByLanguage(basePath, languages, options = {}) {
396
- const langList = Array.isArray(languages) ? languages : [];
397
- const filesByLanguage = new Map();
398
- for (const lang of langList) {
399
- filesByLanguage.set(lang, []);
400
- }
401
-
402
- const extensionToLang = new Map();
403
- for (const lang of langList) {
404
- const extensions = LANGUAGE_EXTENSIONS[lang] || [];
405
- for (const ext of extensions) {
406
- if (!extensionToLang.has(ext)) {
407
- extensionToLang.set(ext, lang);
408
- }
409
- }
410
- }
411
-
412
- const isIgnored = slopAnalyzers.parseGitignore(basePath, fs, path);
413
- const maxFiles = Number.isFinite(options.maxFiles) ? Math.max(0, Math.floor(options.maxFiles)) : null;
414
- let count = 0;
415
-
416
- function scan(dir) {
417
- if (maxFiles !== null && count >= maxFiles) return;
418
- try {
419
- const entries = fs.readdirSync(dir, { withFileTypes: true });
420
- for (const entry of entries) {
421
- if (maxFiles !== null && count >= maxFiles) break;
422
- const fullPath = path.join(dir, entry.name);
423
-
424
- if (entry.isDirectory()) {
425
- const relativePath = path.relative(basePath, fullPath);
426
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
427
- if (isIgnored && isIgnored(relativePath, true)) continue;
428
- if (!entry.name.startsWith('.')) {
429
- scan(fullPath);
430
- }
431
- } else if (entry.isFile()) {
432
- const relativePath = path.relative(basePath, fullPath);
433
- if (slopAnalyzers.shouldExclude(relativePath, EXCLUDE_DIRS)) continue;
434
- if (isIgnored && isIgnored(relativePath, false)) continue;
435
- const ext = path.extname(entry.name).toLowerCase();
436
- const lang = extensionToLang.get(ext);
437
- if (lang) {
438
- const bucket = filesByLanguage.get(lang);
439
- if (bucket) {
440
- bucket.push(fullPath);
441
- count++;
442
- }
443
- }
444
- }
445
- }
446
- } catch {
447
- // Skip directories we can't read
448
- }
449
- }
450
-
451
- scan(basePath);
452
- return filesByLanguage;
453
- }
454
-
455
- function createSymbolMaps() {
456
- return {
457
- exports: new Map(),
458
- functions: new Map(),
459
- classes: new Map(),
460
- types: new Map(),
461
- constants: new Map()
462
- };
463
- }
464
-
465
- /**
466
- * Read multiple files asynchronously in batches
467
- * @param {string[]} files - Array of file paths
468
- * @param {number} batchSize - Concurrent reads per batch
469
- * @returns {Promise<Map<string, {content: string, error: Error|null}>>}
470
- */
471
- async function batchReadFiles(files, batchSize = FILE_READ_BATCH_SIZE) {
472
- const results = new Map();
473
-
474
- for (let i = 0; i < files.length; i += batchSize) {
475
- const batch = files.slice(i, i + batchSize);
476
- const batchResults = await Promise.all(
477
- batch.map(async (file) => {
478
- try {
479
- const content = await fsPromises.readFile(file, 'utf8');
480
- return { file, content, error: null };
481
- } catch (err) {
482
- return { file, content: null, error: err };
483
- }
484
- })
485
- );
486
-
487
- for (const result of batchResults) {
488
- results.set(result.file, { content: result.content, error: result.error });
489
- }
490
- }
491
-
492
- return results;
493
- }
494
-
495
- function chunkArray(items, size) {
496
- if (!items || items.length === 0) return [];
497
- const chunks = [];
498
- for (let i = 0; i < items.length; i += size) {
499
- chunks.push(items.slice(i, i + size));
500
- }
501
- return chunks;
502
- }
503
-
504
- function truncatePattern(pattern, max = 120) {
505
- if (typeof pattern !== 'string') return '';
506
- if (pattern.length <= max) return pattern;
507
- return `${pattern.slice(0, max - 3)}...`;
508
- }
509
-
510
- function buildAstGrepError({ reason, pattern, lang, filePaths, basePath, stderr }) {
511
- const batchLabel = Array.isArray(filePaths) && filePaths.length === 1
512
- ? normalizeMatchPath(filePaths[0], basePath)
513
- : '[batch]';
514
-
515
- const details = stderr && String(stderr).trim()
516
- ? ` (${String(stderr).trim()})`
517
- : '';
518
-
519
- return {
520
- file: batchLabel,
521
- error: `ast-grep ${reason} for ${lang}${details}`,
522
- pattern: truncatePattern(pattern)
523
- };
524
- }
525
-
526
- function runAstGrepPatternAsync(cmd, pattern, lang, basePath, filePaths, options = {}) {
527
- if (!pattern || !filePaths || filePaths.length === 0) {
528
- return Promise.resolve([]);
529
- }
530
-
531
- return new Promise((resolve) => {
532
- const child = spawn(cmd, [
533
- 'run',
534
- '--pattern', pattern,
535
- '--lang', lang,
536
- '--json=stream',
537
- ...filePaths
538
- ], {
539
- cwd: basePath,
540
- windowsHide: true,
541
- stdio: ['ignore', 'pipe', 'pipe']
542
- });
543
-
544
- let stdout = '';
545
- let stderr = '';
546
- let settled = false;
547
-
548
- const timeoutHandle = setTimeout(() => {
549
- if (settled) return;
550
- settled = true;
551
- child.kill();
552
- if (typeof options.onError === 'function') {
553
- options.onError(buildAstGrepError({
554
- reason: 'timed out after 300000ms',
555
- pattern,
556
- lang,
557
- filePaths,
558
- basePath,
559
- stderr
560
- }));
561
- }
562
- resolve([]);
563
- }, 300000);
564
-
565
- child.stdout.on('data', (chunk) => {
566
- stdout += chunk.toString();
567
- });
568
-
569
- child.stderr.on('data', (chunk) => {
570
- stderr += chunk.toString();
571
- });
572
-
573
- child.on('error', (error) => {
574
- if (settled) return;
575
- settled = true;
576
- clearTimeout(timeoutHandle);
577
- if (typeof options.onError === 'function') {
578
- options.onError(buildAstGrepError({
579
- reason: 'execution failed',
580
- pattern,
581
- lang,
582
- filePaths,
583
- basePath,
584
- stderr: error.message
585
- }));
586
- }
587
- resolve([]);
588
- });
589
-
590
- child.on('close', (code) => {
591
- if (settled) return;
592
- settled = true;
593
- clearTimeout(timeoutHandle);
594
-
595
- if (typeof code === 'number' && code > 1) {
596
- if (typeof options.onError === 'function') {
597
- options.onError(buildAstGrepError({
598
- reason: `returned exit code ${code}`,
599
- pattern,
600
- lang,
601
- filePaths,
602
- basePath,
603
- stderr
604
- }));
605
- }
606
- resolve([]);
607
- return;
608
- }
609
-
610
- resolve(parseNdjson(stdout));
611
- });
612
- });
613
- }
614
-
615
- async function runAstGrepPatternBatches(cmd, pattern, lang, basePath, chunks, options = {}) {
616
- const concurrency = Number.isFinite(options.concurrency)
617
- ? Math.max(1, Math.floor(options.concurrency))
618
- : AST_GREP_CONCURRENCY;
619
-
620
- return runWithConcurrency(chunks, concurrency, async (chunk) => {
621
- return runAstGrepPatternAsync(cmd, pattern, lang, basePath, chunk, options);
622
- });
623
- }
624
-
625
- function normalizeMatchPath(matchFile, basePath) {
626
- if (!matchFile) return null;
627
- const absolutePath = path.isAbsolute(matchFile) ? matchFile : path.join(basePath, matchFile);
628
- return path.relative(basePath, absolutePath).replace(/\\/g, '/');
629
- }
630
-
631
- function addSymbolToMap(map, name, match, kind, extra = {}) {
632
- if (!name) return;
633
- if (!map.has(name)) {
634
- map.set(name, {
635
- name,
636
- line: getLine(match),
637
- kind,
638
- ...extra
639
- });
640
- }
641
- }
642
-
643
- function runAstGrepPattern(cmd, pattern, lang, basePath, filePaths, options = {}) {
644
- if (!pattern || !filePaths || filePaths.length === 0) return [];
645
-
646
- try {
647
- const result = spawnSync(cmd, [
648
- 'run',
649
- '--pattern', pattern,
650
- '--lang', lang,
651
- '--json=stream',
652
- ...filePaths
653
- ], {
654
- cwd: basePath,
655
- encoding: 'utf8',
656
- timeout: 300000,
657
- windowsHide: true,
658
- stdio: ['pipe', 'pipe', 'pipe']
659
- });
660
-
661
- if (result.error) {
662
- if (typeof options.onError === 'function') {
663
- options.onError(buildAstGrepError({
664
- reason: 'execution failed',
665
- pattern,
666
- lang,
667
- filePaths,
668
- basePath,
669
- stderr: result.error.message
670
- }));
671
- }
672
- return [];
673
- }
674
-
675
- if (typeof result.status === 'number' && result.status > 1) {
676
- if (typeof options.onError === 'function') {
677
- options.onError(buildAstGrepError({
678
- reason: `returned exit code ${result.status}`,
679
- pattern,
680
- lang,
681
- filePaths,
682
- basePath,
683
- stderr: result.stderr
684
- }));
685
- }
686
- return [];
687
- }
688
-
689
- return parseNdjson(result.stdout);
690
- } catch (error) {
691
- if (typeof options.onError === 'function') {
692
- options.onError(buildAstGrepError({
693
- reason: 'threw an exception',
694
- pattern,
695
- lang,
696
- filePaths,
697
- basePath,
698
- stderr: error.message
699
- }));
700
- }
701
- return [];
702
- }
703
- }
704
-
705
- /**
706
- * Extract symbols from a file using ast-grep
707
- * @param {string} cmd - ast-grep command
708
- * @param {string} file - File path
709
- * @param {string} language - Language name
710
- * @param {Object} langQueries - Query patterns for this language
711
- * @param {string} basePath - Repository root (for cwd)
712
- * @param {string} content - File content
713
- * @returns {Object} - Extracted symbols
714
- */
715
- function extractSymbols(cmd, file, language, langQueries, basePath, content, options = {}) {
716
- const symbols = {
717
- exports: [],
718
- functions: [],
719
- classes: [],
720
- types: [],
721
- constants: []
722
- };
723
-
724
- const sgLang = queries.getSgLanguageForFile(file, language);
725
-
726
- const exportMap = new Map();
727
- const functionMap = new Map();
728
- const classMap = new Map();
729
- const typeMap = new Map();
730
- const constMap = new Map();
731
-
732
- const addSymbol = (map, name, match, kind, extra = {}) => {
733
- if (!name) return;
734
- if (!map.has(name)) {
735
- map.set(name, {
736
- name,
737
- line: getLine(match),
738
- kind,
739
- ...extra
740
- });
741
- }
742
- };
743
-
744
- const runPatternSet = (patterns, targetMap, defaultKind) => {
745
- if (!patterns) return;
746
- for (const patternDef of patterns) {
747
- const pattern = patternDef.pattern || patternDef;
748
- const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
749
- for (const match of results) {
750
- const names = extractNamesFromMatch(match, patternDef);
751
- for (const name of names) {
752
- const kind = patternDef.kind || defaultKind;
753
- addSymbol(targetMap, name, match, kind, patternDef.extra);
754
- }
755
- }
756
- }
757
- };
758
-
759
- // Extract exports
760
- runPatternSet(langQueries.exports, exportMap, 'export');
761
-
762
- // Extract functions
763
- runPatternSet(langQueries.functions, functionMap, 'function');
764
-
765
- // Extract classes
766
- runPatternSet(langQueries.classes, classMap, 'class');
767
-
768
- // Extract types
769
- runPatternSet(langQueries.types, typeMap, 'type');
770
-
771
- // Extract constants
772
- runPatternSet(langQueries.constants, constMap, 'constant');
773
-
774
- // Infer exports for languages with implicit public rules
775
- const exportNames = new Set(exportMap.keys());
776
- applyLanguageExportRules(language, content, exportNames, functionMap, classMap, typeMap, constMap);
777
-
778
- // Ensure export entries exist for inferred exports
779
- ensureExportEntries(exportMap, exportNames, functionMap, classMap, typeMap, constMap);
780
-
781
- // Convert maps to arrays and mark exported flags
782
- symbols.exports = mapToSortedArray(exportMap);
783
- symbols.functions = mapToSortedArray(functionMap, exportNames);
784
- symbols.classes = mapToSortedArray(classMap, exportNames);
785
- symbols.types = mapToSortedArray(typeMap, exportNames);
786
- symbols.constants = mapToSortedArray(constMap, exportNames);
787
-
788
- return symbols;
789
- }
790
-
791
- /**
792
- * Extract imports from a file using ast-grep
793
- * @param {string} cmd - ast-grep command
794
- * @param {string} file - File path
795
- * @param {string} language - Language name
796
- * @param {Object} langQueries - Query patterns for this language
797
- * @param {string} basePath - Repository root (for cwd)
798
- * @returns {Array} - Extracted imports
799
- */
800
- function extractImports(cmd, file, language, langQueries, basePath, options = {}) {
801
- const imports = [];
802
-
803
- if (!langQueries.imports) return imports;
804
-
805
- const sgLang = queries.getSgLanguageForFile(file, language);
806
- const seen = new Set();
807
-
808
- for (const patternDef of langQueries.imports) {
809
- const pattern = patternDef.pattern || patternDef;
810
- const results = runAstGrep(cmd, file, pattern, sgLang, basePath, options);
811
- for (const match of results) {
812
- const sourceResult = extractSourceFromMatch(match, patternDef);
813
- const sources = Array.isArray(sourceResult) ? sourceResult : [sourceResult];
814
- for (const source of sources) {
815
- if (!source) continue;
816
- const key = `${source}:${patternDef.kind || 'import'}`;
817
- if (seen.has(key)) continue;
818
- seen.add(key);
819
- imports.push({
820
- source,
821
- kind: patternDef.kind || 'import',
822
- line: getLine(match)
823
- });
824
- }
825
- }
826
- }
827
-
828
- return imports;
829
- }
830
-
831
- /**
832
- * Run ast-grep with a pattern
833
- * @param {string} cmd - ast-grep command
834
- * @param {string} file - File to scan
835
- * @param {string} pattern - Pattern to match
836
- * @param {string} lang - ast-grep language identifier
837
- * @param {string} basePath - Working directory
838
- * @returns {Array} - Match results
839
- */
840
- function runAstGrep(cmd, file, pattern, lang, basePath, options = {}) {
841
- try {
842
- const result = spawnSync(cmd, [
843
- 'run',
844
- '--pattern', pattern,
845
- '--lang', lang,
846
- '--json=stream',
847
- file
848
- ], {
849
- cwd: basePath,
850
- encoding: 'utf8',
851
- timeout: 30000,
852
- windowsHide: true,
853
- stdio: ['pipe', 'pipe', 'pipe']
854
- });
855
-
856
- if (result.error) {
857
- if (typeof options.onError === 'function') {
858
- options.onError(buildAstGrepError({
859
- reason: 'execution failed',
860
- pattern,
861
- lang,
862
- filePaths: [file],
863
- basePath,
864
- stderr: result.error.message
865
- }));
866
- }
867
- return [];
868
- }
869
-
870
- // ast-grep exits with 1 when no matches
871
- if (typeof result.status === 'number' && result.status > 1) {
872
- if (typeof options.onError === 'function') {
873
- options.onError(buildAstGrepError({
874
- reason: `returned exit code ${result.status}`,
875
- pattern,
876
- lang,
877
- filePaths: [file],
878
- basePath,
879
- stderr: result.stderr
880
- }));
881
- }
882
- return [];
883
- }
884
-
885
- return parseNdjson(result.stdout);
886
- } catch (error) {
887
- if (typeof options.onError === 'function') {
888
- options.onError(buildAstGrepError({
889
- reason: 'threw an exception',
890
- pattern,
891
- lang,
892
- filePaths: [file],
893
- basePath,
894
- stderr: error.message
895
- }));
896
- }
897
- return [];
898
- }
899
- }
900
-
901
- function parseNdjson(output) {
902
- const matches = [];
903
- const lines = (output || '').split('\n').filter(Boolean);
904
- for (const line of lines) {
905
- try {
906
- matches.push(JSON.parse(line));
907
- } catch {
908
- // Skip malformed lines
909
- }
910
- }
911
- return matches;
912
- }
913
-
914
- /**
915
- * Extract names from an ast-grep match, based on pattern metadata
916
- * @param {Object} match - ast-grep match result
917
- * @param {Object|string} patternDef - Pattern definition or string
918
- * @returns {string[]} - List of names
919
- */
920
- function extractNamesFromMatch(match, patternDef) {
921
- const def = typeof patternDef === 'string' ? {} : (patternDef || {});
922
-
923
- if (def.multi === 'exportList') {
924
- return extractNamesFromExportList(match.text || '');
925
- }
926
-
927
- if (def.multi === 'objectLiteral') {
928
- return extractNamesFromObjectLiteral(match.text || '');
929
- }
930
-
931
- const name = extractNameFromMatch(match, def.nameVar);
932
- if (name) return [name];
933
- if (def.fallbackName) return [def.fallbackName];
934
- return [];
935
- }
936
-
937
- function getMetaVariable(match, key) {
938
- if (!match || !match.metaVariables) return null;
939
- if (match.metaVariables[key]) return match.metaVariables[key];
940
- if (match.metaVariables.single && match.metaVariables.single[key]) {
941
- return match.metaVariables.single[key];
942
- }
943
- return null;
944
- }
945
-
946
- /**
947
- * Extract a single name from ast-grep match
948
- * @param {Object} match - ast-grep match result
949
- * @param {string|string[]} nameVar - Preferred meta variable name(s)
950
- * @returns {string|null}
951
- */
952
- function extractNameFromMatch(match, nameVar) {
953
- const vars = [];
954
- if (Array.isArray(nameVar)) {
955
- vars.push(...nameVar);
956
- } else if (nameVar) {
957
- vars.push(nameVar);
958
- }
959
- vars.push('NAME', 'FUNC', 'CLASS', 'IDENT', 'N');
960
-
961
- for (const key of vars) {
962
- const variable = getMetaVariable(match, key);
963
- if (variable && variable.text) {
964
- return variable.text;
965
- }
966
- }
967
-
968
- // Fallback: extract from matched text
969
- if (match.text) {
970
- const nameMatch = match.text.match(/(?:function|class|const|let|var|def|fn|pub\s+fn|type|struct|enum|trait|interface|record)\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
971
- if (nameMatch) {
972
- return nameMatch[1];
973
- }
974
- }
975
-
976
- return null;
977
- }
978
-
979
- /**
980
- * Extract import source from ast-grep match
981
- * @param {Object} match - ast-grep match result
982
- * @param {Object|string} patternDef - Pattern definition or string
983
- * @returns {string|null}
984
- */
985
- function extractSourceFromMatch(match, patternDef) {
986
- const def = typeof patternDef === 'string' ? {} : (patternDef || {});
987
- const sourceVar = def.sourceVar || 'SOURCE';
988
-
989
- const variable = getMetaVariable(match, sourceVar);
990
- if (variable && variable.text) {
991
- const raw = variable.text.replace(/^['"]|['"]$/g, '');
992
- if (def.multiSource) {
993
- return splitMultiSource(raw);
994
- }
995
- return raw;
996
- }
997
-
998
- // Fallback: extract quoted string from match
999
- if (match.text) {
1000
- const sourceMatch = match.text.match(/['"]([^'"]+)['"]/);
1001
- if (sourceMatch) {
1002
- return sourceMatch[1];
1003
- }
1004
- }
1005
-
1006
- return null;
1007
- }
1008
-
1009
- /**
1010
- * Extract export names from `export { ... }`
1011
- * @param {string} text - Match text
1012
- * @returns {string[]}
1013
- */
1014
- function extractNamesFromExportList(text) {
1015
- const match = text.match(/\{([^}]+)\}/);
1016
- if (!match) return [];
1017
-
1018
- const names = new Set();
1019
- const parts = match[1].split(',');
1020
- for (const part of parts) {
1021
- const trimmed = part.trim();
1022
- if (!trimmed) continue;
1023
- const aliasMatch = trimmed.split(/\s+as\s+/i).map(s => s.trim());
1024
- const name = (aliasMatch[1] || aliasMatch[0]).replace(/[^a-zA-Z0-9_\$]/g, '');
1025
- if (isValidIdentifier(name)) names.add(name);
1026
- }
1027
-
1028
- return Array.from(names);
1029
- }
1030
-
1031
- /**
1032
- * Extract property names from object literal
1033
- * @param {string} text - Match text
1034
- * @returns {string[]}
1035
- */
1036
- function extractNamesFromObjectLiteral(text) {
1037
- const match = text.match(/\{([\s\S]*?)\}/);
1038
- if (!match) return [];
1039
-
1040
- const body = match[1];
1041
- const names = new Set();
1042
-
1043
- // Match shorthand properties and key: value pairs
1044
- const propRegex = /\b([A-Za-z_$][\w$]*)\b\s*(?=,|\}|:)/g;
1045
- let propMatch;
1046
- while ((propMatch = propRegex.exec(body)) !== null) {
1047
- const name = propMatch[1];
1048
- if (isValidIdentifier(name)) names.add(name);
1049
- }
1050
-
1051
- return Array.from(names);
1052
- }
1053
-
1054
- /**
1055
- * Split comma-separated import sources
1056
- * @param {string} raw - Raw source text
1057
- * @returns {string[]}
1058
- */
1059
- function splitMultiSource(raw) {
1060
- if (!raw) return [];
1061
- const parts = raw.split(',').map(p => p.trim()).filter(Boolean);
1062
- const results = [];
1063
- for (const part of parts) {
1064
- const [name] = part.split(/\s+as\s+/i);
1065
- const cleaned = name.trim().replace(/^['"]|['"]$/g, '');
1066
- if (cleaned) results.push(cleaned);
1067
- }
1068
- return results;
1069
- }
1070
-
1071
- /**
1072
- * Determine if a name is a valid identifier
1073
- * @param {string} name - Name to check
1074
- * @returns {boolean}
1075
- */
1076
- function isValidIdentifier(name) {
1077
- return Boolean(name) && /^[A-Za-z_$][\w$]*$/.test(name);
1078
- }
1079
-
1080
- /**
1081
- * Get 1-based line number from ast-grep match
1082
- * @param {Object} match - ast-grep match
1083
- * @returns {number|null}
1084
- */
1085
- function getLine(match) {
1086
- const line = match?.range?.start?.line;
1087
- return typeof line === 'number' ? line + 1 : null;
1088
- }
1089
-
1090
- /**
1091
- * Apply language-specific export rules
1092
- * @param {string} language - Language name
1093
- * @param {string} content - File content
1094
- * @param {Set<string>} exportNames - Export name set (in-place)
1095
- * @param {Map} functionMap - Function symbols
1096
- * @param {Map} classMap - Class symbols
1097
- * @param {Map} typeMap - Type symbols
1098
- * @param {Map} constMap - Constant symbols
1099
- */
1100
- function applyLanguageExportRules(language, content, exportNames, functionMap, classMap, typeMap, constMap) {
1101
- if (language === 'python') {
1102
- const explicit = extractPythonAll(content);
1103
- if (explicit.length > 0) {
1104
- for (const name of explicit) exportNames.add(name);
1105
- } else {
1106
- addPublicNames(exportNames, functionMap, classMap, typeMap, constMap, name => !name.startsWith('_'));
1107
- }
1108
- return;
1109
- }
1110
-
1111
- if (language === 'go') {
1112
- addPublicNames(exportNames, functionMap, classMap, typeMap, constMap, name => isExportedGoName(name));
1113
- }
1114
- }
1115
-
1116
- /**
1117
- * Extract __all__ exports from Python content
1118
- * @param {string} content - File content
1119
- * @returns {string[]}
1120
- */
1121
- function extractPythonAll(content) {
1122
- if (!content) return [];
1123
- const match = content.match(/__all__\s*=\s*[\[(]([\s\S]*?)[\])]\s*/m);
1124
- if (!match) return [];
1125
-
1126
- const body = match[1];
1127
- const names = [];
1128
- const stringRegex = /['"]([^'"]+)['"]/g;
1129
- let m;
1130
- while ((m = stringRegex.exec(body)) !== null) {
1131
- if (m[1]) names.push(m[1]);
1132
- }
1133
- return names;
1134
- }
1135
-
1136
- /**
1137
- * Add public names from symbol maps based on predicate
1138
- * @param {Set<string>} exportNames - Export name set
1139
- * @param {...Map} maps - Symbol maps
1140
- * @param {Function} predicate - Function(name) => boolean
1141
- */
1142
- function addPublicNames(exportNames, ...args) {
1143
- const predicate = args.pop();
1144
- const maps = args;
1145
- for (const map of maps) {
1146
- for (const name of map.keys()) {
1147
- if (predicate(name)) exportNames.add(name);
1148
- }
1149
- }
1150
- }
1151
-
1152
- /**
1153
- * Determine if a Go identifier is exported
1154
- * @param {string} name - Identifier
1155
- * @returns {boolean}
1156
- */
1157
- function isExportedGoName(name) {
1158
- if (!name) return false;
1159
- const first = name[0];
1160
- return first.toUpperCase() === first && first.toLowerCase() !== first;
1161
- }
1162
-
1163
- /**
1164
- * Ensure export entries exist for inferred exports
1165
- * @param {Map} exportMap - Export map to populate
1166
- * @param {Set<string>} exportNames - Names to ensure
1167
- * @param {Map} functionMap - Function map
1168
- * @param {Map} classMap - Class map
1169
- * @param {Map} typeMap - Type map
1170
- * @param {Map} constMap - Constant map
1171
- */
1172
- function ensureExportEntries(exportMap, exportNames, functionMap, classMap, typeMap, constMap) {
1173
- const sources = [functionMap, classMap, typeMap, constMap];
1174
-
1175
- for (const name of exportNames) {
1176
- if (exportMap.has(name)) continue;
1177
-
1178
- let entry = null;
1179
- for (const map of sources) {
1180
- if (map.has(name)) {
1181
- const item = map.get(name);
1182
- entry = { name, line: item.line, kind: item.kind };
1183
- break;
1184
- }
1185
- }
1186
-
1187
- if (!entry) {
1188
- entry = { name, line: null, kind: 'export' };
1189
- }
1190
-
1191
- exportMap.set(name, entry);
1192
- }
1193
- }
1194
-
1195
- /**
1196
- * Convert symbol map to sorted array and mark exported flags
1197
- * @param {Map} map - Symbol map
1198
- * @param {Set<string>} [exportNames] - Export name set
1199
- * @returns {Array}
1200
- */
1201
- function mapToSortedArray(map, exportNames) {
1202
- const list = Array.from(map.values());
1203
- if (exportNames) {
1204
- for (const item of list) {
1205
- item.exported = exportNames.has(item.name);
1206
- }
1207
- }
1208
- list.sort((a, b) => a.name.localeCompare(b.name));
1209
- return list;
1210
- }
1211
-
1212
- /**
1213
- * Get git info for the repository
1214
- * @param {string} basePath - Repository root
1215
- * @returns {Object|null}
1216
- */
1217
- function getGitInfo(basePath) {
1218
- try {
1219
- const commit = execFileSync('git', ['rev-parse', 'HEAD'], {
1220
- cwd: basePath,
1221
- encoding: 'utf8',
1222
- stdio: ['pipe', 'pipe', 'pipe']
1223
- }).trim();
1224
-
1225
- const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
1226
- cwd: basePath,
1227
- encoding: 'utf8',
1228
- stdio: ['pipe', 'pipe', 'pipe']
1229
- }).trim();
1230
-
1231
- return { commit, branch };
1232
- } catch {
1233
- return null;
1234
- }
1235
- }
1236
-
1237
- /**
1238
- * Detect primary project type from languages
1239
- * @param {string[]} languages - Detected languages
1240
- * @returns {string}
1241
- */
1242
- function detectProjectType(languages) {
1243
- // Priority order
1244
- const priority = ['typescript', 'javascript', 'python', 'rust', 'go', 'java'];
1245
- for (const lang of priority) {
1246
- if (languages.includes(lang)) {
1247
- return lang === 'typescript' ? 'node' : lang;
1248
- }
1249
- }
1250
- return languages[0] || 'unknown';
1251
- }
1252
-
1253
- /**
1254
- * Scan a single file (for incremental updates)
1255
- * @param {string} cmd - ast-grep command
1256
- * @param {string} file - File path
1257
- * @param {string} basePath - Repository root
1258
- * @returns {Object|null} - File data or null if failed
1259
- */
1260
- function scanSingleFile(cmd, file, basePath, options = {}) {
1261
- const ext = path.extname(file).toLowerCase();
1262
-
1263
- // Find language for this extension
1264
- let language = null;
1265
- for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
1266
- if (exts.includes(ext)) {
1267
- language = lang;
1268
- break;
1269
- }
1270
- }
1271
-
1272
- if (!language) return null;
1273
-
1274
- const langQueries = queries.getQueriesForLanguage(language);
1275
- if (!langQueries) return null;
1276
-
1277
- try {
1278
- const content = fs.readFileSync(file, 'utf8');
1279
- const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1280
-
1281
- const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
1282
- const imports = extractImports(cmd, file, language, langQueries, basePath, options);
1283
-
1284
- return {
1285
- hash,
1286
- language,
1287
- size: content.length,
1288
- symbols,
1289
- imports
1290
- };
1291
- } catch (error) {
1292
- if (typeof options.onError === 'function') {
1293
- options.onError({
1294
- file: normalizeMatchPath(file, basePath) || file,
1295
- error: `Failed to scan file: ${error.message}`
1296
- });
1297
- }
1298
- return null;
1299
- }
1300
- }
1301
-
1302
- /**
1303
- * Scan a single file asynchronously (for incremental updates)
1304
- * Uses async file read, but ast-grep subprocess remains synchronous
1305
- * @param {string} cmd - ast-grep command
1306
- * @param {string} file - File path
1307
- * @param {string} basePath - Repository root
1308
- * @returns {Promise<Object|null>} - File data or null if failed
1309
- */
1310
- async function scanSingleFileAsync(cmd, file, basePath, options = {}) {
1311
- const ext = path.extname(file).toLowerCase();
1312
-
1313
- // Find language for this extension
1314
- let language = null;
1315
- for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
1316
- if (exts.includes(ext)) {
1317
- language = lang;
1318
- break;
1319
- }
1320
- }
1321
-
1322
- if (!language) return null;
1323
-
1324
- const langQueries = queries.getQueriesForLanguage(language);
1325
- if (!langQueries) return null;
1326
-
1327
- try {
1328
- const content = await fsPromises.readFile(file, 'utf8');
1329
- const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1330
-
1331
- const symbols = extractSymbols(cmd, file, language, langQueries, basePath, content, options);
1332
- const imports = extractImports(cmd, file, language, langQueries, basePath, options);
1333
-
1334
- return {
1335
- hash,
1336
- language,
1337
- size: content.length,
1338
- symbols,
1339
- imports
1340
- };
1341
- } catch (error) {
1342
- if (typeof options.onError === 'function') {
1343
- options.onError({
1344
- file: normalizeMatchPath(file, basePath) || file,
1345
- error: `Failed to scan file: ${error.message}`
1346
- });
1347
- }
1348
- return null;
1349
- }
1350
- }
1351
-
1352
- module.exports = {
1353
- detectLanguages,
1354
- fullScan,
1355
- findFilesForLanguage,
1356
- collectFilesByLanguage,
1357
- scanSingleFile,
1358
- scanSingleFileAsync,
1359
- runAstGrep,
1360
- getGitInfo,
1361
- batchReadFiles,
1362
- LANGUAGE_EXTENSIONS,
1363
- EXCLUDE_DIRS
1364
- };