ucn 3.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/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
package/cli/index.js
ADDED
|
@@ -0,0 +1,2437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UCN CLI - Universal Code Navigator
|
|
5
|
+
*
|
|
6
|
+
* Unified command model: commands work consistently across file and project modes.
|
|
7
|
+
* Auto-detects mode from target (file path → file mode, directory → project mode).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { parse, parseFile, extractFunction, extractClass, detectLanguage, isSupported } = require('../core/parser');
|
|
14
|
+
const { getParser, getLanguageModule } = require('../languages');
|
|
15
|
+
const { ProjectIndex } = require('../core/project');
|
|
16
|
+
const { expandGlob, findProjectRoot, isTestFile } = require('../core/discovery');
|
|
17
|
+
const output = require('../core/output');
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// ARGUMENT PARSING
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const rawArgs = process.argv.slice(2);
|
|
24
|
+
|
|
25
|
+
// Support -- to separate flags from positional arguments
|
|
26
|
+
const doubleDashIdx = rawArgs.indexOf('--');
|
|
27
|
+
const args = doubleDashIdx === -1 ? rawArgs : rawArgs.slice(0, doubleDashIdx);
|
|
28
|
+
const argsAfterDoubleDash = doubleDashIdx === -1 ? [] : rawArgs.slice(doubleDashIdx + 1);
|
|
29
|
+
|
|
30
|
+
// Parse flags
|
|
31
|
+
const flags = {
|
|
32
|
+
json: args.includes('--json'),
|
|
33
|
+
quiet: !args.includes('--verbose') && !args.includes('--no-quiet'),
|
|
34
|
+
codeOnly: args.includes('--code-only'),
|
|
35
|
+
withTypes: args.includes('--with-types'),
|
|
36
|
+
topLevel: args.includes('--top-level'),
|
|
37
|
+
exact: args.includes('--exact'),
|
|
38
|
+
cache: !args.includes('--no-cache'),
|
|
39
|
+
clearCache: args.includes('--clear-cache'),
|
|
40
|
+
context: parseInt(args.find(a => a.startsWith('--context='))?.split('=')[1] || '0'),
|
|
41
|
+
file: args.find(a => a.startsWith('--file='))?.split('=')[1] || null,
|
|
42
|
+
// Semantic filters (--not is alias for --exclude)
|
|
43
|
+
exclude: args.find(a => a.startsWith('--exclude=') || a.startsWith('--not='))?.split('=')[1]?.split(',') || [],
|
|
44
|
+
in: args.find(a => a.startsWith('--in='))?.split('=')[1] || null,
|
|
45
|
+
// Test file inclusion (by default, tests are excluded from usages/find)
|
|
46
|
+
includeTests: args.includes('--include-tests'),
|
|
47
|
+
// Deadcode options
|
|
48
|
+
includeExported: args.includes('--include-exported'),
|
|
49
|
+
// Output depth
|
|
50
|
+
depth: args.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
|
|
51
|
+
// Inline expansion for callees
|
|
52
|
+
expand: args.includes('--expand'),
|
|
53
|
+
// Interactive REPL mode
|
|
54
|
+
interactive: args.includes('--interactive') || args.includes('-i'),
|
|
55
|
+
// Plan command options
|
|
56
|
+
addParam: args.find(a => a.startsWith('--add-param='))?.split('=')[1] || null,
|
|
57
|
+
removeParam: args.find(a => a.startsWith('--remove-param='))?.split('=')[1] || null,
|
|
58
|
+
renameTo: args.find(a => a.startsWith('--rename-to='))?.split('=')[1] || null,
|
|
59
|
+
defaultValue: args.find(a => a.startsWith('--default='))?.split('=')[1] || null,
|
|
60
|
+
// Smart filtering for find results
|
|
61
|
+
top: parseInt(args.find(a => a.startsWith('--top='))?.split('=')[1] || '0'),
|
|
62
|
+
all: args.includes('--all'),
|
|
63
|
+
// Include method calls in caller/callee analysis
|
|
64
|
+
includeMethods: args.includes('--include-methods'),
|
|
65
|
+
// Symlink handling (follow by default)
|
|
66
|
+
followSymlinks: !args.includes('--no-follow-symlinks')
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Handle --file flag with space
|
|
70
|
+
const fileArgIdx = args.indexOf('--file');
|
|
71
|
+
if (fileArgIdx !== -1 && args[fileArgIdx + 1]) {
|
|
72
|
+
flags.file = args[fileArgIdx + 1];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Known flags for validation
|
|
76
|
+
const knownFlags = new Set([
|
|
77
|
+
'--help', '-h',
|
|
78
|
+
'--json', '--verbose', '--no-quiet', '--quiet',
|
|
79
|
+
'--code-only', '--with-types', '--top-level', '--exact',
|
|
80
|
+
'--no-cache', '--clear-cache', '--include-tests',
|
|
81
|
+
'--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods',
|
|
82
|
+
'--file', '--context', '--exclude', '--not', '--in',
|
|
83
|
+
'--depth', '--add-param', '--remove-param', '--rename-to',
|
|
84
|
+
'--default', '--top', '--no-follow-symlinks'
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Handle help flag
|
|
88
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
89
|
+
printUsage();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate flags
|
|
94
|
+
const unknownFlags = args.filter(a => {
|
|
95
|
+
if (!a.startsWith('-')) return false;
|
|
96
|
+
// Handle --flag=value format
|
|
97
|
+
const flagName = a.includes('=') ? a.split('=')[0] : a;
|
|
98
|
+
return !knownFlags.has(flagName);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (unknownFlags.length > 0) {
|
|
102
|
+
console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
|
|
103
|
+
console.error('Use --help to see available flags');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Remove flags from args, then add args after -- (which are all positional)
|
|
108
|
+
const positionalArgs = [
|
|
109
|
+
...args.filter(a =>
|
|
110
|
+
!a.startsWith('--') &&
|
|
111
|
+
a !== '-i' &&
|
|
112
|
+
!(args.indexOf(a) > 0 && args[args.indexOf(a) - 1] === '--file')
|
|
113
|
+
),
|
|
114
|
+
...argsAfterDoubleDash
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// HELPERS
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add test file patterns to exclusion list
|
|
123
|
+
* Used by find/usages when --include-tests is not specified
|
|
124
|
+
*/
|
|
125
|
+
function addTestExclusions(exclude) {
|
|
126
|
+
const testPatterns = ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'];
|
|
127
|
+
const existing = new Set(exclude.map(e => e.toLowerCase()));
|
|
128
|
+
const additions = testPatterns.filter(p => !existing.has(p));
|
|
129
|
+
return [...exclude, ...additions];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate required argument and exit with usage if missing
|
|
134
|
+
* @param {string} arg - The argument to validate
|
|
135
|
+
* @param {string} usage - Usage message to show on error
|
|
136
|
+
*/
|
|
137
|
+
function requireArg(arg, usage) {
|
|
138
|
+
if (!arg) {
|
|
139
|
+
console.error(usage);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Print result in JSON or text format based on --json flag
|
|
146
|
+
* @param {*} result - The result data
|
|
147
|
+
* @param {Function} jsonFn - Function to format as JSON (receives result)
|
|
148
|
+
* @param {Function} textFn - Function to format as text (receives result)
|
|
149
|
+
*/
|
|
150
|
+
function printOutput(result, jsonFn, textFn) {
|
|
151
|
+
if (flags.json) {
|
|
152
|
+
console.log(jsonFn(result));
|
|
153
|
+
} else {
|
|
154
|
+
const text = textFn(result);
|
|
155
|
+
if (text !== undefined) {
|
|
156
|
+
console.log(text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// MAIN
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
// All valid commands - used to detect if first arg is command vs path
|
|
166
|
+
const COMMANDS = new Set([
|
|
167
|
+
'toc', 'find', 'usages', 'fn', 'class', 'lines', 'search', 'typedef', 'api',
|
|
168
|
+
'context', 'smart', 'about', 'impact', 'trace', 'related', 'example', 'expand',
|
|
169
|
+
'tests', 'verify', 'plan', 'deadcode', 'stats', 'stacktrace', 'stack',
|
|
170
|
+
'imports', 'what-imports', 'exporters', 'who-imports', 'graph', 'file-exports', 'what-exports'
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
function main() {
|
|
174
|
+
// Determine target and command based on positional args
|
|
175
|
+
let target, command, arg;
|
|
176
|
+
|
|
177
|
+
if (positionalArgs.length === 0) {
|
|
178
|
+
// No args: show help
|
|
179
|
+
printUsage();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
} else if (positionalArgs.length === 1) {
|
|
182
|
+
// One arg: could be a command (use . as target) or a target (use toc as command)
|
|
183
|
+
if (COMMANDS.has(positionalArgs[0])) {
|
|
184
|
+
target = '.';
|
|
185
|
+
command = positionalArgs[0];
|
|
186
|
+
arg = undefined;
|
|
187
|
+
} else {
|
|
188
|
+
target = positionalArgs[0];
|
|
189
|
+
command = 'toc';
|
|
190
|
+
arg = undefined;
|
|
191
|
+
}
|
|
192
|
+
} else if (COMMANDS.has(positionalArgs[0])) {
|
|
193
|
+
// First arg is a command, so target defaults to .
|
|
194
|
+
target = '.';
|
|
195
|
+
command = positionalArgs[0];
|
|
196
|
+
arg = positionalArgs[1];
|
|
197
|
+
} else {
|
|
198
|
+
// First arg is a target (path/glob)
|
|
199
|
+
target = positionalArgs[0];
|
|
200
|
+
command = positionalArgs[1] || 'toc';
|
|
201
|
+
arg = positionalArgs[2];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Determine mode: single file, glob pattern, or project
|
|
205
|
+
if (target === '.' || (fs.existsSync(target) && fs.statSync(target).isDirectory())) {
|
|
206
|
+
// Project mode
|
|
207
|
+
runProjectCommand(target, command, arg);
|
|
208
|
+
} else if (target.includes('*') || target.includes('{')) {
|
|
209
|
+
// Glob pattern mode
|
|
210
|
+
runGlobCommand(target, command, arg);
|
|
211
|
+
} else if (fs.existsSync(target)) {
|
|
212
|
+
// Single file mode
|
|
213
|
+
runFileCommand(target, command, arg);
|
|
214
|
+
} else {
|
|
215
|
+
console.error(`Error: "${target}" not found`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// FILE MODE
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
function runFileCommand(filePath, command, arg) {
|
|
225
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
226
|
+
const lines = code.split('\n');
|
|
227
|
+
const language = detectLanguage(filePath);
|
|
228
|
+
|
|
229
|
+
if (!language) {
|
|
230
|
+
console.error(`Unsupported file type: ${filePath}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result = parse(code, language);
|
|
235
|
+
|
|
236
|
+
switch (command) {
|
|
237
|
+
case 'toc':
|
|
238
|
+
printFileToc(result, filePath);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'fn': {
|
|
242
|
+
requireArg(arg, 'Usage: ucn <file> fn <name>');
|
|
243
|
+
const { fn, code: fnCode } = extractFunction(code, language, arg);
|
|
244
|
+
if (fn) {
|
|
245
|
+
printOutput({ fn, fnCode },
|
|
246
|
+
r => output.formatFunctionJson(r.fn, r.fnCode),
|
|
247
|
+
r => {
|
|
248
|
+
console.log(`${output.lineRange(r.fn.startLine, r.fn.endLine)} ${output.formatFunctionSignature(r.fn)}`);
|
|
249
|
+
console.log('─'.repeat(60));
|
|
250
|
+
console.log(r.fnCode);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
console.error(`Function "${arg}" not found`);
|
|
255
|
+
suggestSimilar(arg, result.functions.map(f => f.name));
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'class': {
|
|
261
|
+
requireArg(arg, 'Usage: ucn <file> class <name>');
|
|
262
|
+
const { cls, code: clsCode } = extractClass(code, language, arg);
|
|
263
|
+
if (cls) {
|
|
264
|
+
printOutput({ cls, clsCode },
|
|
265
|
+
r => JSON.stringify({ ...r.cls, code: r.clsCode }, null, 2),
|
|
266
|
+
r => {
|
|
267
|
+
console.log(`${output.lineRange(r.cls.startLine, r.cls.endLine)} ${output.formatClassSignature(r.cls)}`);
|
|
268
|
+
console.log('─'.repeat(60));
|
|
269
|
+
console.log(r.clsCode);
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
console.error(`Class "${arg}" not found`);
|
|
274
|
+
suggestSimilar(arg, result.classes.map(c => c.name));
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'find': {
|
|
280
|
+
requireArg(arg, 'Usage: ucn <file> find <name>');
|
|
281
|
+
findInFile(result, arg, filePath);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'usages': {
|
|
286
|
+
requireArg(arg, 'Usage: ucn <file> usages <name>');
|
|
287
|
+
usagesInFile(code, lines, arg, filePath, result);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'search': {
|
|
292
|
+
requireArg(arg, 'Usage: ucn <file> search <term>');
|
|
293
|
+
searchFile(filePath, lines, arg);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case 'lines': {
|
|
298
|
+
requireArg(arg, 'Usage: ucn <file> lines <start-end>');
|
|
299
|
+
printLines(lines, arg);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'typedef': {
|
|
304
|
+
requireArg(arg, 'Usage: ucn <file> typedef <name>');
|
|
305
|
+
typedefInFile(result, arg, filePath);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'api':
|
|
310
|
+
apiInFile(result, filePath);
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
// Project commands - auto-route to project mode
|
|
314
|
+
case 'smart':
|
|
315
|
+
case 'context':
|
|
316
|
+
case 'tests':
|
|
317
|
+
case 'about':
|
|
318
|
+
case 'impact':
|
|
319
|
+
case 'trace':
|
|
320
|
+
case 'related':
|
|
321
|
+
case 'example':
|
|
322
|
+
case 'graph':
|
|
323
|
+
case 'stats':
|
|
324
|
+
case 'deadcode':
|
|
325
|
+
case 'imports':
|
|
326
|
+
case 'what-imports':
|
|
327
|
+
case 'exporters':
|
|
328
|
+
case 'who-imports': {
|
|
329
|
+
// Auto-detect project root and route to project mode
|
|
330
|
+
const projectRoot = findProjectRoot(path.dirname(filePath));
|
|
331
|
+
|
|
332
|
+
// For file-specific commands (imports/exporters/graph), use the target file as arg if no arg given
|
|
333
|
+
let effectiveArg = arg;
|
|
334
|
+
if ((command === 'imports' || command === 'what-imports' ||
|
|
335
|
+
command === 'exporters' || command === 'who-imports' ||
|
|
336
|
+
command === 'graph') && !arg) {
|
|
337
|
+
effectiveArg = filePath;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// For stats/deadcode, no arg needed
|
|
341
|
+
if (command === 'stats' || command === 'deadcode') {
|
|
342
|
+
effectiveArg = arg; // may be undefined, that's ok
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
runProjectCommand(projectRoot, command, effectiveArg);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
default:
|
|
350
|
+
console.error(`Unknown command: ${command}`);
|
|
351
|
+
printUsage();
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function printFileToc(result, filePath) {
|
|
357
|
+
// Filter for top-level only if flag is set
|
|
358
|
+
let functions = result.functions;
|
|
359
|
+
if (flags.topLevel) {
|
|
360
|
+
functions = functions.filter(fn => !fn.isNested && fn.indent === 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (flags.json) {
|
|
364
|
+
console.log(output.formatTocJson({
|
|
365
|
+
totalFiles: 1,
|
|
366
|
+
totalLines: result.totalLines,
|
|
367
|
+
totalFunctions: functions.length,
|
|
368
|
+
totalClasses: result.classes.length,
|
|
369
|
+
totalState: result.stateObjects.length,
|
|
370
|
+
byFile: [{
|
|
371
|
+
file: filePath,
|
|
372
|
+
language: result.language,
|
|
373
|
+
lines: result.totalLines,
|
|
374
|
+
functions,
|
|
375
|
+
classes: result.classes,
|
|
376
|
+
state: result.stateObjects
|
|
377
|
+
}]
|
|
378
|
+
}));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`FILE: ${filePath} (${result.totalLines} lines)`);
|
|
383
|
+
console.log('═'.repeat(60));
|
|
384
|
+
|
|
385
|
+
if (functions.length > 0) {
|
|
386
|
+
console.log('\nFUNCTIONS:');
|
|
387
|
+
for (const fn of functions) {
|
|
388
|
+
const sig = output.formatFunctionSignature(fn);
|
|
389
|
+
console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${sig}`);
|
|
390
|
+
if (fn.docstring) {
|
|
391
|
+
console.log(` ${fn.docstring}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (result.classes.length > 0) {
|
|
397
|
+
console.log('\nCLASSES:');
|
|
398
|
+
for (const cls of result.classes) {
|
|
399
|
+
console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
400
|
+
if (cls.docstring) {
|
|
401
|
+
console.log(` ${cls.docstring}`);
|
|
402
|
+
}
|
|
403
|
+
if (cls.members && cls.members.length > 0) {
|
|
404
|
+
for (const m of cls.members) {
|
|
405
|
+
console.log(` ${output.lineLoc(m.startLine)} ${output.formatMemberSignature(m)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (result.stateObjects.length > 0) {
|
|
412
|
+
console.log('\nSTATE:');
|
|
413
|
+
for (const s of result.stateObjects) {
|
|
414
|
+
console.log(` ${output.lineRange(s.startLine, s.endLine)} ${s.name}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function findInFile(result, name, filePath) {
|
|
420
|
+
const matches = [];
|
|
421
|
+
const lowerName = name.toLowerCase();
|
|
422
|
+
|
|
423
|
+
for (const fn of result.functions) {
|
|
424
|
+
if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
|
|
425
|
+
matches.push({ ...fn, type: 'function' });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const cls of result.classes) {
|
|
430
|
+
if (flags.exact ? cls.name === name : cls.name.toLowerCase().includes(lowerName)) {
|
|
431
|
+
matches.push({ ...cls });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (flags.json) {
|
|
436
|
+
console.log(output.formatSymbolJson(matches.map(m => ({ ...m, relativePath: filePath })), name));
|
|
437
|
+
} else {
|
|
438
|
+
if (matches.length === 0) {
|
|
439
|
+
console.log(`No symbols found for "${name}" in ${filePath}`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(`Found ${matches.length} match(es) for "${name}" in ${filePath}:`);
|
|
442
|
+
console.log('─'.repeat(60));
|
|
443
|
+
for (const m of matches) {
|
|
444
|
+
const sig = m.params !== undefined
|
|
445
|
+
? output.formatFunctionSignature(m)
|
|
446
|
+
: output.formatClassSignature(m);
|
|
447
|
+
console.log(`${filePath}:${m.startLine} ${sig}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function usagesInFile(code, lines, name, filePath, result) {
|
|
454
|
+
const usages = [];
|
|
455
|
+
|
|
456
|
+
// Get definitions
|
|
457
|
+
const defs = [];
|
|
458
|
+
for (const fn of result.functions) {
|
|
459
|
+
if (fn.name === name) {
|
|
460
|
+
defs.push({ ...fn, type: 'function', isDefinition: true, line: fn.startLine });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const cls of result.classes) {
|
|
464
|
+
if (cls.name === name) {
|
|
465
|
+
defs.push({ ...cls, isDefinition: true, line: cls.startLine });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Try AST-based detection first
|
|
470
|
+
const lang = detectLanguage(filePath);
|
|
471
|
+
const langModule = getLanguageModule(lang);
|
|
472
|
+
|
|
473
|
+
if (langModule && typeof langModule.findUsagesInCode === 'function') {
|
|
474
|
+
try {
|
|
475
|
+
const parser = getParser(lang);
|
|
476
|
+
if (parser) {
|
|
477
|
+
const astUsages = langModule.findUsagesInCode(code, name, parser);
|
|
478
|
+
|
|
479
|
+
for (const u of astUsages) {
|
|
480
|
+
// Skip definition lines
|
|
481
|
+
if (defs.some(d => d.startLine === u.line)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const lineContent = lines[u.line - 1] || '';
|
|
486
|
+
const usage = {
|
|
487
|
+
file: filePath,
|
|
488
|
+
relativePath: filePath,
|
|
489
|
+
line: u.line,
|
|
490
|
+
content: lineContent,
|
|
491
|
+
usageType: u.usageType,
|
|
492
|
+
isDefinition: false
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Add context
|
|
496
|
+
if (flags.context > 0) {
|
|
497
|
+
const idx = u.line - 1;
|
|
498
|
+
const before = [];
|
|
499
|
+
const after = [];
|
|
500
|
+
for (let i = 1; i <= flags.context; i++) {
|
|
501
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
502
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
503
|
+
}
|
|
504
|
+
usage.before = before;
|
|
505
|
+
usage.after = after;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
usages.push(usage);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Add definitions to result and output
|
|
512
|
+
const allUsages = [
|
|
513
|
+
...defs.map(d => ({
|
|
514
|
+
...d,
|
|
515
|
+
relativePath: filePath,
|
|
516
|
+
content: lines[d.startLine - 1],
|
|
517
|
+
signature: d.params !== undefined
|
|
518
|
+
? output.formatFunctionSignature(d)
|
|
519
|
+
: output.formatClassSignature(d)
|
|
520
|
+
})),
|
|
521
|
+
...usages
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
if (flags.json) {
|
|
525
|
+
console.log(output.formatUsagesJson(allUsages, name));
|
|
526
|
+
} else {
|
|
527
|
+
printUsagesText(allUsages, name);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
// Fall through to regex-based detection
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Fallback to regex-based detection (for unsupported languages)
|
|
537
|
+
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b');
|
|
538
|
+
lines.forEach((line, idx) => {
|
|
539
|
+
const lineNum = idx + 1;
|
|
540
|
+
|
|
541
|
+
// Skip definition lines
|
|
542
|
+
if (defs.some(d => d.startLine === lineNum)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (regex.test(line)) {
|
|
547
|
+
if (flags.codeOnly && isCommentOrString(line)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Skip if the match is inside a string literal
|
|
552
|
+
if (isInsideString(line, name)) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const usageType = classifyUsage(line, name);
|
|
557
|
+
const usage = {
|
|
558
|
+
file: filePath,
|
|
559
|
+
relativePath: filePath,
|
|
560
|
+
line: lineNum,
|
|
561
|
+
content: line,
|
|
562
|
+
usageType,
|
|
563
|
+
isDefinition: false
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// Add context
|
|
567
|
+
if (flags.context > 0) {
|
|
568
|
+
const before = [];
|
|
569
|
+
const after = [];
|
|
570
|
+
for (let i = 1; i <= flags.context; i++) {
|
|
571
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
572
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
573
|
+
}
|
|
574
|
+
usage.before = before;
|
|
575
|
+
usage.after = after;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
usages.push(usage);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Add definitions to result
|
|
583
|
+
const allUsages = [
|
|
584
|
+
...defs.map(d => ({
|
|
585
|
+
...d,
|
|
586
|
+
relativePath: filePath,
|
|
587
|
+
content: lines[d.startLine - 1],
|
|
588
|
+
signature: d.params !== undefined
|
|
589
|
+
? output.formatFunctionSignature(d)
|
|
590
|
+
: output.formatClassSignature(d)
|
|
591
|
+
})),
|
|
592
|
+
...usages
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
if (flags.json) {
|
|
596
|
+
console.log(output.formatUsagesJson(allUsages, name));
|
|
597
|
+
} else {
|
|
598
|
+
printUsagesText(allUsages, name);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function typedefInFile(result, name, filePath) {
|
|
603
|
+
const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait'];
|
|
604
|
+
const matches = result.classes.filter(c =>
|
|
605
|
+
typeKinds.includes(c.type) &&
|
|
606
|
+
(flags.exact ? c.name === name : c.name.toLowerCase().includes(name.toLowerCase()))
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
if (flags.json) {
|
|
610
|
+
console.log(output.formatTypedefJson(matches.map(m => ({ ...m, relativePath: filePath })), name));
|
|
611
|
+
} else {
|
|
612
|
+
console.log(output.formatTypedef(matches.map(m => ({ ...m, relativePath: filePath })), name));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function apiInFile(result, filePath) {
|
|
617
|
+
const exported = [];
|
|
618
|
+
|
|
619
|
+
for (const fn of result.functions) {
|
|
620
|
+
if (fn.modifiers && (fn.modifiers.includes('export') || fn.modifiers.includes('public'))) {
|
|
621
|
+
exported.push({
|
|
622
|
+
name: fn.name,
|
|
623
|
+
type: 'function',
|
|
624
|
+
file: filePath,
|
|
625
|
+
startLine: fn.startLine,
|
|
626
|
+
endLine: fn.endLine,
|
|
627
|
+
params: fn.params,
|
|
628
|
+
returnType: fn.returnType,
|
|
629
|
+
signature: output.formatFunctionSignature(fn)
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const cls of result.classes) {
|
|
635
|
+
if (cls.modifiers && (cls.modifiers.includes('export') || cls.modifiers.includes('public'))) {
|
|
636
|
+
exported.push({
|
|
637
|
+
name: cls.name,
|
|
638
|
+
type: cls.type,
|
|
639
|
+
file: filePath,
|
|
640
|
+
startLine: cls.startLine,
|
|
641
|
+
endLine: cls.endLine,
|
|
642
|
+
signature: output.formatClassSignature(cls)
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (flags.json) {
|
|
648
|
+
console.log(output.formatApiJson(exported, filePath));
|
|
649
|
+
} else {
|
|
650
|
+
console.log(output.formatApi(exported, filePath));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============================================================================
|
|
655
|
+
// PROJECT MODE
|
|
656
|
+
// ============================================================================
|
|
657
|
+
|
|
658
|
+
function runProjectCommand(rootDir, command, arg) {
|
|
659
|
+
const index = new ProjectIndex(rootDir);
|
|
660
|
+
|
|
661
|
+
// Clear cache if requested
|
|
662
|
+
if (flags.clearCache) {
|
|
663
|
+
const cacheDir = path.join(index.root, '.ucn-cache');
|
|
664
|
+
if (fs.existsSync(cacheDir)) {
|
|
665
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
666
|
+
if (!flags.quiet) {
|
|
667
|
+
console.error('Cache cleared');
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Try to load cache if enabled
|
|
673
|
+
let usedCache = false;
|
|
674
|
+
let cacheWasLoaded = false;
|
|
675
|
+
if (flags.cache && !flags.clearCache) {
|
|
676
|
+
const loaded = index.loadCache();
|
|
677
|
+
if (loaded) {
|
|
678
|
+
cacheWasLoaded = true;
|
|
679
|
+
if (!index.isCacheStale()) {
|
|
680
|
+
usedCache = true;
|
|
681
|
+
if (!flags.quiet) {
|
|
682
|
+
console.error('Using cached index');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Build/rebuild if cache not used
|
|
689
|
+
// If cache was loaded but stale, force rebuild to avoid duplicates
|
|
690
|
+
if (!usedCache) {
|
|
691
|
+
index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks });
|
|
692
|
+
|
|
693
|
+
// Save cache if enabled
|
|
694
|
+
if (flags.cache) {
|
|
695
|
+
index.saveCache();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
switch (command) {
|
|
700
|
+
case 'toc': {
|
|
701
|
+
const toc = index.getToc();
|
|
702
|
+
printOutput(toc, output.formatTocJson, printProjectToc);
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
case 'find': {
|
|
707
|
+
requireArg(arg, 'Usage: ucn . find <name>');
|
|
708
|
+
const findExclude = flags.includeTests ? flags.exclude : addTestExclusions(flags.exclude);
|
|
709
|
+
const found = index.find(arg, {
|
|
710
|
+
file: flags.file,
|
|
711
|
+
exact: flags.exact,
|
|
712
|
+
exclude: findExclude,
|
|
713
|
+
in: flags.in
|
|
714
|
+
});
|
|
715
|
+
printOutput(found,
|
|
716
|
+
r => output.formatSymbolJson(r, arg),
|
|
717
|
+
r => { printSymbols(r, arg, { depth: flags.depth, top: flags.top, all: flags.all }); }
|
|
718
|
+
);
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
case 'usages': {
|
|
723
|
+
requireArg(arg, 'Usage: ucn . usages <name>');
|
|
724
|
+
const usagesExclude = flags.includeTests ? flags.exclude : addTestExclusions(flags.exclude);
|
|
725
|
+
const usages = index.usages(arg, {
|
|
726
|
+
codeOnly: flags.codeOnly,
|
|
727
|
+
context: flags.context,
|
|
728
|
+
exclude: usagesExclude,
|
|
729
|
+
in: flags.in
|
|
730
|
+
});
|
|
731
|
+
printOutput(usages,
|
|
732
|
+
r => output.formatUsagesJson(r, arg),
|
|
733
|
+
r => { printUsagesText(r, arg); }
|
|
734
|
+
);
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
case 'example':
|
|
739
|
+
if (!arg) {
|
|
740
|
+
console.error('Usage: ucn . example <name>');
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
printBestExample(index, arg);
|
|
744
|
+
break;
|
|
745
|
+
|
|
746
|
+
case 'context': {
|
|
747
|
+
requireArg(arg, 'Usage: ucn . context <name>');
|
|
748
|
+
const ctx = index.context(arg, { includeMethods: flags.includeMethods });
|
|
749
|
+
printOutput(ctx,
|
|
750
|
+
output.formatContextJson,
|
|
751
|
+
r => { printContext(r, { expand: flags.expand, root: index.root }); }
|
|
752
|
+
);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
case 'expand': {
|
|
757
|
+
requireArg(arg, 'Usage: ucn . expand <N>\nFirst run "ucn . context <name>" to get numbered items');
|
|
758
|
+
const expandNum = parseInt(arg);
|
|
759
|
+
if (isNaN(expandNum)) {
|
|
760
|
+
console.error(`Invalid item number: "${arg}"`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
const cached = loadExpandableItems();
|
|
764
|
+
if (!cached || !cached.items || cached.items.length === 0) {
|
|
765
|
+
console.error('No expandable items found. Run "ucn . context <name>" first.');
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
const item = cached.items.find(i => i.num === expandNum);
|
|
769
|
+
if (!item) {
|
|
770
|
+
console.error(`Item ${expandNum} not found. Available: 1-${cached.items.length}`);
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
printExpandedItem(item, cached.root || index.root);
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
case 'smart': {
|
|
778
|
+
requireArg(arg, 'Usage: ucn . smart <name>');
|
|
779
|
+
const smart = index.smart(arg, { withTypes: flags.withTypes, includeMethods: flags.includeMethods });
|
|
780
|
+
if (smart) {
|
|
781
|
+
printOutput(smart, output.formatSmartJson, printSmart);
|
|
782
|
+
} else {
|
|
783
|
+
console.error(`Function "${arg}" not found`);
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
case 'about': {
|
|
789
|
+
requireArg(arg, 'Usage: ucn . about <name>');
|
|
790
|
+
const aboutResult = index.about(arg, { withTypes: flags.withTypes });
|
|
791
|
+
printOutput(aboutResult,
|
|
792
|
+
output.formatAboutJson,
|
|
793
|
+
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
|
|
794
|
+
);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
case 'impact': {
|
|
799
|
+
requireArg(arg, 'Usage: ucn . impact <name>');
|
|
800
|
+
const impactResult = index.impact(arg);
|
|
801
|
+
printOutput(impactResult, output.formatImpactJson, output.formatImpact);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
case 'plan': {
|
|
806
|
+
requireArg(arg, 'Usage: ucn . plan <name> [--add-param=name] [--remove-param=name] [--rename-to=name]');
|
|
807
|
+
if (!flags.addParam && !flags.removeParam && !flags.renameTo) {
|
|
808
|
+
console.error('Plan requires an operation: --add-param, --remove-param, or --rename-to');
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
const planResult = index.plan(arg, {
|
|
812
|
+
addParam: flags.addParam,
|
|
813
|
+
removeParam: flags.removeParam,
|
|
814
|
+
renameTo: flags.renameTo,
|
|
815
|
+
defaultValue: flags.defaultValue
|
|
816
|
+
});
|
|
817
|
+
printOutput(planResult, r => JSON.stringify(r, null, 2), output.formatPlan);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
case 'trace': {
|
|
822
|
+
requireArg(arg, 'Usage: ucn . trace <name>');
|
|
823
|
+
const traceDepth = flags.depth ? parseInt(flags.depth) : 3;
|
|
824
|
+
const traceResult = index.trace(arg, { depth: traceDepth });
|
|
825
|
+
printOutput(traceResult, output.formatTraceJson, output.formatTrace);
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
case 'stacktrace':
|
|
830
|
+
case 'stack': {
|
|
831
|
+
requireArg(arg, 'Usage: ucn . stacktrace "<stack trace text>"\nExample: ucn . stacktrace "Error: failed\\n at parseFile (core/parser.js:90:5)"');
|
|
832
|
+
const stackResult = index.parseStackTrace(arg);
|
|
833
|
+
printOutput(stackResult, r => JSON.stringify(r, null, 2), output.formatStackTrace);
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
case 'verify': {
|
|
838
|
+
requireArg(arg, 'Usage: ucn . verify <name>');
|
|
839
|
+
const verifyResult = index.verify(arg);
|
|
840
|
+
printOutput(verifyResult, r => JSON.stringify(r, null, 2), output.formatVerify);
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
case 'related': {
|
|
845
|
+
requireArg(arg, 'Usage: ucn . related <name>');
|
|
846
|
+
const relatedResult = index.related(arg);
|
|
847
|
+
printOutput(relatedResult, output.formatRelatedJson, output.formatRelated);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
case 'fn': {
|
|
852
|
+
requireArg(arg, 'Usage: ucn . fn <name>');
|
|
853
|
+
extractFunctionFromProject(index, arg);
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
case 'class': {
|
|
858
|
+
requireArg(arg, 'Usage: ucn . class <name>');
|
|
859
|
+
extractClassFromProject(index, arg);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
case 'imports':
|
|
864
|
+
case 'what-imports': {
|
|
865
|
+
requireArg(arg, 'Usage: ucn . imports <file>');
|
|
866
|
+
const imports = index.imports(arg);
|
|
867
|
+
printOutput(imports,
|
|
868
|
+
r => output.formatImportsJson(r, arg),
|
|
869
|
+
r => output.formatImports(r, arg)
|
|
870
|
+
);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
case 'exporters':
|
|
875
|
+
case 'who-imports': {
|
|
876
|
+
requireArg(arg, 'Usage: ucn . exporters <file>');
|
|
877
|
+
const exporters = index.exporters(arg);
|
|
878
|
+
printOutput(exporters,
|
|
879
|
+
r => output.formatExportersJson(r, arg),
|
|
880
|
+
r => output.formatExporters(r, arg)
|
|
881
|
+
);
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
case 'typedef': {
|
|
886
|
+
requireArg(arg, 'Usage: ucn . typedef <name>');
|
|
887
|
+
const typedefs = index.typedef(arg);
|
|
888
|
+
printOutput(typedefs,
|
|
889
|
+
r => output.formatTypedefJson(r, arg),
|
|
890
|
+
r => output.formatTypedef(r, arg)
|
|
891
|
+
);
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
case 'tests': {
|
|
896
|
+
requireArg(arg, 'Usage: ucn . tests <name>');
|
|
897
|
+
const tests = index.tests(arg);
|
|
898
|
+
printOutput(tests,
|
|
899
|
+
r => output.formatTestsJson(r, arg),
|
|
900
|
+
r => output.formatTests(r, arg)
|
|
901
|
+
);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
case 'api': {
|
|
906
|
+
const api = index.api(arg); // arg is optional file path
|
|
907
|
+
printOutput(api,
|
|
908
|
+
r => output.formatApiJson(r, arg),
|
|
909
|
+
r => output.formatApi(r, arg)
|
|
910
|
+
);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
case 'file-exports':
|
|
915
|
+
case 'what-exports': {
|
|
916
|
+
requireArg(arg, 'Usage: ucn . file-exports <file>');
|
|
917
|
+
const fileExports = index.fileExports(arg);
|
|
918
|
+
if (fileExports.length === 0) {
|
|
919
|
+
console.log(`No exports found in ${arg}`);
|
|
920
|
+
} else {
|
|
921
|
+
printOutput(fileExports,
|
|
922
|
+
r => JSON.stringify({ file: arg, exports: r }, null, 2),
|
|
923
|
+
r => {
|
|
924
|
+
console.log(`Exports from ${arg}:\n`);
|
|
925
|
+
for (const exp of r) {
|
|
926
|
+
console.log(` ${output.lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
case 'deadcode': {
|
|
935
|
+
const deadcodeResults = index.deadcode({
|
|
936
|
+
includeExported: flags.includeExported,
|
|
937
|
+
includeTests: flags.includeTests
|
|
938
|
+
});
|
|
939
|
+
if (deadcodeResults.length === 0) {
|
|
940
|
+
console.log('No dead code found');
|
|
941
|
+
} else {
|
|
942
|
+
printOutput(deadcodeResults,
|
|
943
|
+
r => JSON.stringify({ deadcode: r }, null, 2),
|
|
944
|
+
r => {
|
|
945
|
+
console.log(`Dead code: ${r.length} unused symbol(s)\n`);
|
|
946
|
+
let currentFile = null;
|
|
947
|
+
for (const item of r) {
|
|
948
|
+
if (item.file !== currentFile) {
|
|
949
|
+
currentFile = item.file;
|
|
950
|
+
console.log(`${item.file}`);
|
|
951
|
+
}
|
|
952
|
+
const exported = item.isExported ? ' [exported]' : '';
|
|
953
|
+
console.log(` ${output.lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
case 'graph': {
|
|
962
|
+
requireArg(arg, 'Usage: ucn . graph <file>');
|
|
963
|
+
const graphResult = index.graph(arg, { direction: 'both', maxDepth: flags.depth ?? 5 });
|
|
964
|
+
if (graphResult.nodes.length === 0) {
|
|
965
|
+
console.log(`File not found: ${arg}`);
|
|
966
|
+
} else {
|
|
967
|
+
printOutput(graphResult,
|
|
968
|
+
r => JSON.stringify({
|
|
969
|
+
root: path.relative(index.root, r.root),
|
|
970
|
+
nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
|
|
971
|
+
edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
|
|
972
|
+
}, null, 2),
|
|
973
|
+
r => { printGraph(r, index.root); }
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
case 'search': {
|
|
980
|
+
requireArg(arg, 'Usage: ucn . search <term>');
|
|
981
|
+
const searchResults = index.search(arg, { codeOnly: flags.codeOnly, context: flags.context });
|
|
982
|
+
printOutput(searchResults,
|
|
983
|
+
r => output.formatSearchJson(r, arg),
|
|
984
|
+
r => { printSearchResults(r, arg); }
|
|
985
|
+
);
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
case 'lines': {
|
|
990
|
+
if (!arg || !flags.file) {
|
|
991
|
+
console.error('Usage: ucn . lines <range> --file <path>');
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
const filePath = index.findFile(flags.file);
|
|
995
|
+
if (!filePath) {
|
|
996
|
+
console.error(`File not found: ${flags.file}`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
1000
|
+
printLines(fileContent.split('\n'), arg);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
case 'stats': {
|
|
1005
|
+
const stats = index.getStats();
|
|
1006
|
+
printOutput(stats, output.formatStatsJson, printStats);
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
default:
|
|
1011
|
+
console.error(`Unknown command: ${command}`);
|
|
1012
|
+
printUsage();
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function extractFunctionFromProject(index, name) {
|
|
1018
|
+
const matches = index.find(name, { file: flags.file }).filter(m => m.type === 'function' || m.params !== undefined);
|
|
1019
|
+
|
|
1020
|
+
if (matches.length === 0) {
|
|
1021
|
+
console.error(`Function "${name}" not found`);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (matches.length > 1 && !flags.file) {
|
|
1026
|
+
// Disambiguation needed
|
|
1027
|
+
console.log(output.formatDisambiguation(matches, name, 'fn'));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const match = matches[0];
|
|
1032
|
+
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1033
|
+
const language = detectLanguage(match.file);
|
|
1034
|
+
const { fn, code: fnCode } = extractFunction(code, language, match.name);
|
|
1035
|
+
|
|
1036
|
+
if (fn) {
|
|
1037
|
+
if (flags.json) {
|
|
1038
|
+
console.log(output.formatFunctionJson(fn, fnCode));
|
|
1039
|
+
} else {
|
|
1040
|
+
console.log(`${match.relativePath}:${fn.startLine}`);
|
|
1041
|
+
console.log(`${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
|
|
1042
|
+
console.log('─'.repeat(60));
|
|
1043
|
+
console.log(fnCode);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function extractClassFromProject(index, name) {
|
|
1049
|
+
const matches = index.find(name, { file: flags.file }).filter(m =>
|
|
1050
|
+
['class', 'interface', 'type', 'enum', 'struct', 'trait'].includes(m.type)
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
if (matches.length === 0) {
|
|
1054
|
+
console.error(`Class "${name}" not found`);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (matches.length > 1 && !flags.file) {
|
|
1059
|
+
// Disambiguation needed
|
|
1060
|
+
console.log(output.formatDisambiguation(matches, name, 'class'));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const match = matches[0];
|
|
1065
|
+
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1066
|
+
const language = detectLanguage(match.file);
|
|
1067
|
+
const { cls, code: clsCode } = extractClass(code, language, match.name);
|
|
1068
|
+
|
|
1069
|
+
if (cls) {
|
|
1070
|
+
if (flags.json) {
|
|
1071
|
+
console.log(JSON.stringify({ ...cls, code: clsCode }, null, 2));
|
|
1072
|
+
} else {
|
|
1073
|
+
console.log(`${match.relativePath}:${cls.startLine}`);
|
|
1074
|
+
console.log(`${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
1075
|
+
console.log('─'.repeat(60));
|
|
1076
|
+
console.log(clsCode);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function printProjectToc(toc) {
|
|
1082
|
+
console.log(`PROJECT: ${toc.totalFiles} files, ${toc.totalLines} lines`);
|
|
1083
|
+
console.log(` ${toc.totalFunctions} functions, ${toc.totalClasses} classes, ${toc.totalState} state objects`);
|
|
1084
|
+
console.log('═'.repeat(60));
|
|
1085
|
+
|
|
1086
|
+
for (const file of toc.byFile) {
|
|
1087
|
+
// Filter for top-level only if flag is set
|
|
1088
|
+
let functions = file.functions;
|
|
1089
|
+
if (flags.topLevel) {
|
|
1090
|
+
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (functions.length === 0 && file.classes.length === 0) continue;
|
|
1094
|
+
|
|
1095
|
+
console.log(`\n${file.file} (${file.lines} lines)`);
|
|
1096
|
+
|
|
1097
|
+
for (const fn of functions) {
|
|
1098
|
+
console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
for (const cls of file.classes) {
|
|
1102
|
+
console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function printSymbols(symbols, query, options = {}) {
|
|
1108
|
+
const { depth, top, all } = options;
|
|
1109
|
+
const DEFAULT_LIMIT = 5;
|
|
1110
|
+
|
|
1111
|
+
if (symbols.length === 0) {
|
|
1112
|
+
console.log(`No symbols found for "${query}"`);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Determine how many to show
|
|
1117
|
+
const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
|
|
1118
|
+
const showing = Math.min(limit, symbols.length);
|
|
1119
|
+
const hidden = symbols.length - showing;
|
|
1120
|
+
|
|
1121
|
+
if (hidden > 0) {
|
|
1122
|
+
console.log(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
|
|
1123
|
+
} else {
|
|
1124
|
+
console.log(`Found ${symbols.length} match(es) for "${query}":`);
|
|
1125
|
+
}
|
|
1126
|
+
console.log('─'.repeat(60));
|
|
1127
|
+
|
|
1128
|
+
for (let i = 0; i < showing; i++) {
|
|
1129
|
+
const s = symbols[i];
|
|
1130
|
+
// Depth 0: just location
|
|
1131
|
+
if (depth === '0') {
|
|
1132
|
+
console.log(`${s.relativePath}:${s.startLine}`);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Depth 1 (default): location + signature
|
|
1137
|
+
const sig = s.params !== undefined
|
|
1138
|
+
? output.formatFunctionSignature(s)
|
|
1139
|
+
: output.formatClassSignature(s);
|
|
1140
|
+
|
|
1141
|
+
// Compute and display confidence indicator
|
|
1142
|
+
const confidence = computeConfidence(s);
|
|
1143
|
+
const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
|
|
1144
|
+
|
|
1145
|
+
console.log(`${s.relativePath}:${s.startLine} ${sig}${confStr}`);
|
|
1146
|
+
if (s.usageCounts !== undefined) {
|
|
1147
|
+
const c = s.usageCounts;
|
|
1148
|
+
const parts = [];
|
|
1149
|
+
if (c.calls > 0) parts.push(`${c.calls} calls`);
|
|
1150
|
+
if (c.definitions > 0) parts.push(`${c.definitions} def`);
|
|
1151
|
+
if (c.imports > 0) parts.push(`${c.imports} imports`);
|
|
1152
|
+
if (c.references > 0) parts.push(`${c.references} refs`);
|
|
1153
|
+
console.log(` (${c.total} usages: ${parts.join(', ')})`);
|
|
1154
|
+
} else if (s.usageCount !== undefined) {
|
|
1155
|
+
console.log(` (${s.usageCount} usages)`);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Show confidence reason if not high
|
|
1159
|
+
if (confidence.level !== 'high' && confidence.reasons.length > 0) {
|
|
1160
|
+
console.log(` ⚠ ${confidence.reasons.join(', ')}`);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Depth 2: + first 10 lines of code
|
|
1164
|
+
if (depth === '2' || depth === 'full') {
|
|
1165
|
+
try {
|
|
1166
|
+
const content = fs.readFileSync(s.file, 'utf-8');
|
|
1167
|
+
const lines = content.split('\n');
|
|
1168
|
+
const maxLines = depth === 'full' ? (s.endLine - s.startLine + 1) : 10;
|
|
1169
|
+
const endLine = Math.min(s.startLine + maxLines - 1, s.endLine);
|
|
1170
|
+
console.log(' ───');
|
|
1171
|
+
for (let i = s.startLine - 1; i < endLine; i++) {
|
|
1172
|
+
console.log(` ${lines[i]}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (depth === '2' && s.endLine > endLine) {
|
|
1175
|
+
console.log(` ... (${s.endLine - endLine} more lines)`);
|
|
1176
|
+
}
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
// Skip code extraction on error
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
console.log('');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Show hint about hidden results
|
|
1185
|
+
if (hidden > 0) {
|
|
1186
|
+
console.log(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Compute confidence level for a symbol match
|
|
1192
|
+
* @returns {{ level: 'high'|'medium'|'low', reasons: string[] }}
|
|
1193
|
+
*/
|
|
1194
|
+
function computeConfidence(symbol) {
|
|
1195
|
+
const reasons = [];
|
|
1196
|
+
let score = 100;
|
|
1197
|
+
|
|
1198
|
+
// Check function span (very long functions may have incorrect boundaries)
|
|
1199
|
+
const span = (symbol.endLine || symbol.startLine) - symbol.startLine;
|
|
1200
|
+
if (span > 500) {
|
|
1201
|
+
score -= 30;
|
|
1202
|
+
reasons.push('very long function (>500 lines)');
|
|
1203
|
+
} else if (span > 200) {
|
|
1204
|
+
score -= 15;
|
|
1205
|
+
reasons.push('long function (>200 lines)');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Check for complex type annotations (nested generics)
|
|
1209
|
+
const params = Array.isArray(symbol.params) ? symbol.params : [];
|
|
1210
|
+
const signature = params.map(p => p.type || '').join(' ') + (symbol.returnType || '');
|
|
1211
|
+
const genericDepth = countNestedGenerics(signature);
|
|
1212
|
+
if (genericDepth > 3) {
|
|
1213
|
+
score -= 20;
|
|
1214
|
+
reasons.push('complex nested generics');
|
|
1215
|
+
} else if (genericDepth > 2) {
|
|
1216
|
+
score -= 10;
|
|
1217
|
+
reasons.push('nested generics');
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Check file size by checking if file property exists and getting line count
|
|
1221
|
+
if (symbol.file) {
|
|
1222
|
+
try {
|
|
1223
|
+
const stats = fs.statSync(symbol.file);
|
|
1224
|
+
const sizeKB = stats.size / 1024;
|
|
1225
|
+
if (sizeKB > 500) {
|
|
1226
|
+
score -= 20;
|
|
1227
|
+
reasons.push('very large file (>500KB)');
|
|
1228
|
+
} else if (sizeKB > 200) {
|
|
1229
|
+
score -= 10;
|
|
1230
|
+
reasons.push('large file (>200KB)');
|
|
1231
|
+
}
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
// Skip file size check on error
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Determine level
|
|
1238
|
+
let level = 'high';
|
|
1239
|
+
if (score < 50) level = 'low';
|
|
1240
|
+
else if (score < 80) level = 'medium';
|
|
1241
|
+
|
|
1242
|
+
return { level, reasons };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Count depth of nested generic brackets
|
|
1247
|
+
*/
|
|
1248
|
+
function countNestedGenerics(str) {
|
|
1249
|
+
let maxDepth = 0;
|
|
1250
|
+
let depth = 0;
|
|
1251
|
+
for (const char of str) {
|
|
1252
|
+
if (char === '<') {
|
|
1253
|
+
depth++;
|
|
1254
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
1255
|
+
} else if (char === '>') {
|
|
1256
|
+
depth--;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return maxDepth;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function printUsagesText(usages, name) {
|
|
1263
|
+
const defs = usages.filter(u => u.isDefinition);
|
|
1264
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
1265
|
+
const imports = usages.filter(u => u.usageType === 'import');
|
|
1266
|
+
const refs = usages.filter(u => !u.isDefinition && u.usageType === 'reference');
|
|
1267
|
+
|
|
1268
|
+
console.log(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
|
|
1269
|
+
console.log('═'.repeat(60));
|
|
1270
|
+
|
|
1271
|
+
if (defs.length > 0) {
|
|
1272
|
+
console.log('\nDEFINITIONS:');
|
|
1273
|
+
for (const d of defs) {
|
|
1274
|
+
console.log(` ${d.relativePath}:${d.line || d.startLine}`);
|
|
1275
|
+
if (d.signature) console.log(` ${d.signature}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (calls.length > 0) {
|
|
1280
|
+
console.log('\nCALLS:');
|
|
1281
|
+
for (const c of calls) {
|
|
1282
|
+
console.log(` ${c.relativePath}:${c.line}`);
|
|
1283
|
+
printBeforeLines(c);
|
|
1284
|
+
console.log(` ${c.content.trim()}`);
|
|
1285
|
+
printAfterLines(c);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (imports.length > 0) {
|
|
1290
|
+
console.log('\nIMPORTS:');
|
|
1291
|
+
for (const i of imports) {
|
|
1292
|
+
console.log(` ${i.relativePath}:${i.line}`);
|
|
1293
|
+
console.log(` ${i.content.trim()}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (refs.length > 0) {
|
|
1298
|
+
console.log('\nREFERENCES:');
|
|
1299
|
+
for (const r of refs) {
|
|
1300
|
+
console.log(` ${r.relativePath}:${r.line}`);
|
|
1301
|
+
printBeforeLines(r);
|
|
1302
|
+
console.log(` ${r.content.trim()}`);
|
|
1303
|
+
printAfterLines(r);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function printBeforeLines(usage) {
|
|
1309
|
+
if (usage.before && usage.before.length > 0) {
|
|
1310
|
+
for (const line of usage.before) {
|
|
1311
|
+
console.log(` ${line}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function printAfterLines(usage) {
|
|
1317
|
+
if (usage.after && usage.after.length > 0) {
|
|
1318
|
+
for (const line of usage.after) {
|
|
1319
|
+
console.log(` ${line}`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Analyze call site context using AST for better example scoring
|
|
1326
|
+
* @param {string} filePath - Path to the file
|
|
1327
|
+
* @param {number} lineNum - Line number of the call
|
|
1328
|
+
* @param {string} funcName - Function being called
|
|
1329
|
+
* @returns {object} AST-based scoring info
|
|
1330
|
+
*/
|
|
1331
|
+
function analyzeCallSiteAST(filePath, lineNum, funcName) {
|
|
1332
|
+
const result = {
|
|
1333
|
+
isAwait: false,
|
|
1334
|
+
isDestructured: false,
|
|
1335
|
+
isTypedAssignment: false,
|
|
1336
|
+
isInReturn: false,
|
|
1337
|
+
isInCatch: false,
|
|
1338
|
+
isInConditional: false,
|
|
1339
|
+
hasComment: false,
|
|
1340
|
+
isStandalone: false
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
try {
|
|
1344
|
+
const language = detectLanguage(filePath);
|
|
1345
|
+
if (!language) return result;
|
|
1346
|
+
|
|
1347
|
+
const parser = getParser(language);
|
|
1348
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1349
|
+
const { PARSE_OPTIONS } = require('../languages');
|
|
1350
|
+
const tree = parser.parse(content, undefined, PARSE_OPTIONS);
|
|
1351
|
+
|
|
1352
|
+
// Find the node at the call site
|
|
1353
|
+
const row = lineNum - 1; // 0-indexed
|
|
1354
|
+
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
1355
|
+
if (!node) return result;
|
|
1356
|
+
|
|
1357
|
+
// Walk up to find the call expression and its context
|
|
1358
|
+
let current = node;
|
|
1359
|
+
let foundCall = false;
|
|
1360
|
+
|
|
1361
|
+
while (current) {
|
|
1362
|
+
const type = current.type;
|
|
1363
|
+
|
|
1364
|
+
// Check if this is our target call
|
|
1365
|
+
if (!foundCall && (type === 'call_expression' || type === 'call')) {
|
|
1366
|
+
const calleeNode = current.childForFieldName('function') || current.namedChild(0);
|
|
1367
|
+
if (calleeNode && calleeNode.text === funcName) {
|
|
1368
|
+
foundCall = true;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (foundCall) {
|
|
1373
|
+
// Check context of the call
|
|
1374
|
+
if (type === 'await_expression') {
|
|
1375
|
+
result.isAwait = true;
|
|
1376
|
+
}
|
|
1377
|
+
if (type === 'variable_declarator' || type === 'assignment_expression') {
|
|
1378
|
+
const parent = current.parent;
|
|
1379
|
+
if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration')) {
|
|
1380
|
+
result.isTypedAssignment = true;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (type === 'array_pattern' || type === 'object_pattern') {
|
|
1384
|
+
result.isDestructured = true;
|
|
1385
|
+
}
|
|
1386
|
+
if (type === 'return_statement') {
|
|
1387
|
+
result.isInReturn = true;
|
|
1388
|
+
}
|
|
1389
|
+
if (type === 'catch_clause' || type === 'except_clause') {
|
|
1390
|
+
result.isInCatch = true;
|
|
1391
|
+
}
|
|
1392
|
+
if (type === 'if_statement' || type === 'conditional_expression' || type === 'ternary_expression') {
|
|
1393
|
+
result.isInConditional = true;
|
|
1394
|
+
}
|
|
1395
|
+
if (type === 'expression_statement') {
|
|
1396
|
+
// Standalone statement - good example
|
|
1397
|
+
result.isStandalone = true;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
current = current.parent;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Check for preceding comment
|
|
1405
|
+
const lines = content.split('\n');
|
|
1406
|
+
if (lineNum > 1) {
|
|
1407
|
+
const prevLine = lines[lineNum - 2].trim();
|
|
1408
|
+
if (prevLine.startsWith('//') || prevLine.startsWith('#') || prevLine.endsWith('*/')) {
|
|
1409
|
+
result.hasComment = true;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
// Return default result on error
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return result;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Print the best example usage of a function
|
|
1421
|
+
* Selects based on AST analysis: await, destructuring, typed assignment, context
|
|
1422
|
+
*/
|
|
1423
|
+
function printBestExample(index, name) {
|
|
1424
|
+
// Get usages excluding test files
|
|
1425
|
+
const usages = index.usages(name, {
|
|
1426
|
+
codeOnly: true,
|
|
1427
|
+
exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
|
|
1428
|
+
context: 5 // Get 5 lines before/after
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// Filter to only calls (not definitions, imports, or references)
|
|
1432
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
1433
|
+
|
|
1434
|
+
if (calls.length === 0) {
|
|
1435
|
+
console.log(`No call examples found for "${name}"`);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Score each call using both regex and AST analysis
|
|
1440
|
+
const scored = calls.map(call => {
|
|
1441
|
+
let score = 0;
|
|
1442
|
+
const reasons = [];
|
|
1443
|
+
const line = call.content.trim();
|
|
1444
|
+
|
|
1445
|
+
// Get AST-based analysis
|
|
1446
|
+
const astInfo = analyzeCallSiteAST(call.file, call.line, name);
|
|
1447
|
+
|
|
1448
|
+
// AST-based scoring (more accurate)
|
|
1449
|
+
if (astInfo.isTypedAssignment) {
|
|
1450
|
+
score += 15;
|
|
1451
|
+
reasons.push('typed assignment');
|
|
1452
|
+
}
|
|
1453
|
+
if (astInfo.isInReturn) {
|
|
1454
|
+
score += 10;
|
|
1455
|
+
reasons.push('in return');
|
|
1456
|
+
}
|
|
1457
|
+
if (astInfo.isAwait) {
|
|
1458
|
+
score += 10;
|
|
1459
|
+
reasons.push('async usage');
|
|
1460
|
+
}
|
|
1461
|
+
if (astInfo.isDestructured) {
|
|
1462
|
+
score += 8;
|
|
1463
|
+
reasons.push('destructured');
|
|
1464
|
+
}
|
|
1465
|
+
if (astInfo.isStandalone) {
|
|
1466
|
+
score += 5;
|
|
1467
|
+
reasons.push('standalone');
|
|
1468
|
+
}
|
|
1469
|
+
if (astInfo.hasComment) {
|
|
1470
|
+
score += 3;
|
|
1471
|
+
reasons.push('documented');
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Penalties
|
|
1475
|
+
if (astInfo.isInCatch) {
|
|
1476
|
+
score -= 5;
|
|
1477
|
+
reasons.push('in catch block');
|
|
1478
|
+
}
|
|
1479
|
+
if (astInfo.isInConditional) {
|
|
1480
|
+
score -= 3;
|
|
1481
|
+
reasons.push('in conditional');
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Fallback regex-based scoring (for when AST doesn't find much)
|
|
1485
|
+
if (score === 0) {
|
|
1486
|
+
// Return value is used (assigned to variable): +10
|
|
1487
|
+
if (/^(const|let|var|return)\s/.test(line) || /^\w+\s*=/.test(line)) {
|
|
1488
|
+
score += 10;
|
|
1489
|
+
reasons.push('return value used');
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Standalone statement: +5
|
|
1493
|
+
if (line.startsWith(name + '(') || /^(const|let|var)\s+\w+\s*=\s*\w*$/.test(line.split(name)[0])) {
|
|
1494
|
+
score += 5;
|
|
1495
|
+
reasons.push('clear usage');
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Context bonus
|
|
1500
|
+
if (call.before && call.before.length > 0) score += 3;
|
|
1501
|
+
if (call.after && call.after.length > 0) score += 3;
|
|
1502
|
+
if (call.before?.length > 0 && call.after?.length > 0) {
|
|
1503
|
+
reasons.push('has context');
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Not inside another function call: +2
|
|
1507
|
+
const beforeCall = line.split(name + '(')[0];
|
|
1508
|
+
if (!beforeCall.includes('(') || /^\s*(const|let|var|return)?\s*\w+\s*=\s*$/.test(beforeCall)) {
|
|
1509
|
+
score += 2;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Shorter files are often better examples: +1
|
|
1513
|
+
if (call.line < 100) score += 1;
|
|
1514
|
+
|
|
1515
|
+
return { ...call, score, reasons };
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// Sort by score descending
|
|
1519
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1520
|
+
|
|
1521
|
+
const best = scored[0];
|
|
1522
|
+
|
|
1523
|
+
console.log(`Best example of "${name}":`);
|
|
1524
|
+
console.log('═'.repeat(60));
|
|
1525
|
+
console.log(`${best.relativePath}:${best.line}`);
|
|
1526
|
+
console.log('');
|
|
1527
|
+
|
|
1528
|
+
// Print context before
|
|
1529
|
+
if (best.before) {
|
|
1530
|
+
for (let i = 0; i < best.before.length; i++) {
|
|
1531
|
+
const lineNum = best.line - best.before.length + i;
|
|
1532
|
+
console.log(`${lineNum.toString().padStart(4)}│ ${best.before[i]}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Print the call line (highlighted)
|
|
1537
|
+
console.log(`${best.line.toString().padStart(4)}│ ${best.content} ◀──`);
|
|
1538
|
+
|
|
1539
|
+
// Print context after
|
|
1540
|
+
if (best.after) {
|
|
1541
|
+
for (let i = 0; i < best.after.length; i++) {
|
|
1542
|
+
const lineNum = best.line + i + 1;
|
|
1543
|
+
console.log(`${lineNum.toString().padStart(4)}│ ${best.after[i]}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
console.log('');
|
|
1548
|
+
console.log(`Score: ${best.score} (${calls.length} total calls)`);
|
|
1549
|
+
console.log(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function printContext(ctx, options = {}) {
|
|
1553
|
+
console.log(`Context for ${ctx.function}:`);
|
|
1554
|
+
console.log('═'.repeat(60));
|
|
1555
|
+
|
|
1556
|
+
// Display warnings if any
|
|
1557
|
+
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
1558
|
+
console.log('\n⚠️ WARNINGS:');
|
|
1559
|
+
for (const w of ctx.warnings) {
|
|
1560
|
+
console.log(` ${w.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Track expandable items for later use with 'expand' command
|
|
1565
|
+
const expandable = [];
|
|
1566
|
+
let itemNum = 1;
|
|
1567
|
+
|
|
1568
|
+
console.log(`\nCALLERS (${ctx.callers.length}):`);
|
|
1569
|
+
for (const c of ctx.callers) {
|
|
1570
|
+
// All callers are numbered for expand command
|
|
1571
|
+
const callerName = c.callerName || '(module level)';
|
|
1572
|
+
const displayName = c.callerName ? ` [${callerName}]` : '';
|
|
1573
|
+
console.log(` [${itemNum}] ${c.relativePath}:${c.line}${displayName}`);
|
|
1574
|
+
expandable.push({
|
|
1575
|
+
num: itemNum++,
|
|
1576
|
+
type: 'caller',
|
|
1577
|
+
name: callerName,
|
|
1578
|
+
file: c.callerFile || c.file,
|
|
1579
|
+
relativePath: c.relativePath,
|
|
1580
|
+
line: c.line,
|
|
1581
|
+
startLine: c.callerStartLine || c.line,
|
|
1582
|
+
endLine: c.callerEndLine || c.line
|
|
1583
|
+
});
|
|
1584
|
+
console.log(` ${c.content.trim()}`);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
console.log(`\nCALLEES (${ctx.callees.length}):`);
|
|
1588
|
+
for (const c of ctx.callees) {
|
|
1589
|
+
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
1590
|
+
console.log(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
|
|
1591
|
+
expandable.push({
|
|
1592
|
+
num: itemNum++,
|
|
1593
|
+
type: 'callee',
|
|
1594
|
+
name: c.name,
|
|
1595
|
+
file: c.file,
|
|
1596
|
+
relativePath: c.relativePath,
|
|
1597
|
+
startLine: c.startLine,
|
|
1598
|
+
endLine: c.endLine
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
// Inline expansion
|
|
1602
|
+
if (options.expand && options.root && c.relativePath && c.startLine) {
|
|
1603
|
+
try {
|
|
1604
|
+
const filePath = path.join(options.root, c.relativePath);
|
|
1605
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1606
|
+
const lines = content.split('\n');
|
|
1607
|
+
const endLine = c.endLine || c.startLine + 5;
|
|
1608
|
+
const previewLines = Math.min(3, endLine - c.startLine + 1);
|
|
1609
|
+
for (let i = 0; i < previewLines && c.startLine - 1 + i < lines.length; i++) {
|
|
1610
|
+
console.log(` │ ${lines[c.startLine - 1 + i]}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (endLine - c.startLine + 1 > 3) {
|
|
1613
|
+
console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
|
|
1614
|
+
}
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
// Skip on error
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Save expandable items to cache for 'expand' command
|
|
1622
|
+
saveExpandableItems(expandable, options.root);
|
|
1623
|
+
|
|
1624
|
+
if (expandable.length > 0) {
|
|
1625
|
+
console.log(`\nUse "ucn . expand <N>" to see code for item N`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Extract function name from a call expression
|
|
1631
|
+
*/
|
|
1632
|
+
function extractFunctionNameFromContent(content) {
|
|
1633
|
+
if (!content) return null;
|
|
1634
|
+
// Look for common patterns: funcName(, obj.method(, etc.
|
|
1635
|
+
const match = content.match(/(\w+)\s*\(/);
|
|
1636
|
+
return match ? match[1] : null;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Save expandable items to cache file
|
|
1641
|
+
*/
|
|
1642
|
+
function saveExpandableItems(items, root) {
|
|
1643
|
+
try {
|
|
1644
|
+
const cacheDir = path.join(root || '.', '.ucn-cache');
|
|
1645
|
+
if (!fs.existsSync(cacheDir)) {
|
|
1646
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1647
|
+
}
|
|
1648
|
+
fs.writeFileSync(
|
|
1649
|
+
path.join(cacheDir, 'expandable.json'),
|
|
1650
|
+
JSON.stringify({ items, root, timestamp: Date.now() }, null, 2)
|
|
1651
|
+
);
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
// Silently fail - expand feature is optional
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Load expandable items from cache
|
|
1659
|
+
*/
|
|
1660
|
+
function loadExpandableItems() {
|
|
1661
|
+
try {
|
|
1662
|
+
const cachePath = path.join('.ucn-cache', 'expandable.json');
|
|
1663
|
+
if (fs.existsSync(cachePath)) {
|
|
1664
|
+
return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
1665
|
+
}
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
// Return null on error
|
|
1668
|
+
}
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Print expanded code for a cached item
|
|
1674
|
+
*/
|
|
1675
|
+
function printExpandedItem(item, root) {
|
|
1676
|
+
const filePath = item.file || (root && item.relativePath ? path.join(root, item.relativePath) : null);
|
|
1677
|
+
if (!filePath) {
|
|
1678
|
+
console.error(`Cannot locate file for ${item.name}`);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1684
|
+
const lines = content.split('\n');
|
|
1685
|
+
const startLine = item.startLine || item.line || 1;
|
|
1686
|
+
const endLine = item.endLine || startLine + 20;
|
|
1687
|
+
|
|
1688
|
+
console.log(`[${item.num}] ${item.name} (${item.type})`);
|
|
1689
|
+
console.log(`${item.relativePath}:${startLine}-${endLine}`);
|
|
1690
|
+
console.log('═'.repeat(60));
|
|
1691
|
+
|
|
1692
|
+
for (let i = startLine - 1; i < Math.min(endLine, lines.length); i++) {
|
|
1693
|
+
console.log(lines[i]);
|
|
1694
|
+
}
|
|
1695
|
+
} catch (e) {
|
|
1696
|
+
console.error(`Error reading ${filePath}: ${e.message}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function printSmart(smart) {
|
|
1701
|
+
console.log(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
|
|
1702
|
+
console.log('═'.repeat(60));
|
|
1703
|
+
console.log(smart.target.code);
|
|
1704
|
+
|
|
1705
|
+
if (smart.dependencies.length > 0) {
|
|
1706
|
+
console.log('\n─── DEPENDENCIES ───');
|
|
1707
|
+
for (const dep of smart.dependencies) {
|
|
1708
|
+
const weight = dep.weight && dep.weight !== 'normal' ? ` [${dep.weight}]` : '';
|
|
1709
|
+
console.log(`\n// ${dep.name}${weight} (${dep.relativePath}:${dep.startLine})`);
|
|
1710
|
+
console.log(dep.code);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (smart.types && smart.types.length > 0) {
|
|
1715
|
+
console.log('\n─── TYPES ───');
|
|
1716
|
+
for (const t of smart.types) {
|
|
1717
|
+
console.log(`\n// ${t.name} (${t.relativePath}:${t.startLine})`);
|
|
1718
|
+
console.log(t.code);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function printStats(stats) {
|
|
1724
|
+
console.log('PROJECT STATISTICS');
|
|
1725
|
+
console.log('═'.repeat(60));
|
|
1726
|
+
console.log(`Root: ${stats.root}`);
|
|
1727
|
+
console.log(`Files: ${stats.files}`);
|
|
1728
|
+
console.log(`Symbols: ${stats.symbols}`);
|
|
1729
|
+
console.log(`Build time: ${stats.buildTime}ms`);
|
|
1730
|
+
|
|
1731
|
+
console.log('\nBy Language:');
|
|
1732
|
+
for (const [lang, info] of Object.entries(stats.byLanguage)) {
|
|
1733
|
+
console.log(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
console.log('\nBy Type:');
|
|
1737
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
1738
|
+
console.log(` ${type}: ${count}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function printGraph(graph, root) {
|
|
1743
|
+
const rootRelPath = path.relative(root, graph.root);
|
|
1744
|
+
console.log(`Dependency graph for ${rootRelPath}`);
|
|
1745
|
+
console.log('═'.repeat(60));
|
|
1746
|
+
|
|
1747
|
+
const printed = new Set();
|
|
1748
|
+
|
|
1749
|
+
function printNode(file, indent = 0) {
|
|
1750
|
+
const fileEntry = graph.nodes.find(n => n.file === file);
|
|
1751
|
+
const relPath = fileEntry ? fileEntry.relativePath : path.relative(root, file);
|
|
1752
|
+
const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + '├── ';
|
|
1753
|
+
|
|
1754
|
+
if (printed.has(file)) {
|
|
1755
|
+
console.log(`${prefix}${relPath} (circular)`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
printed.add(file);
|
|
1759
|
+
|
|
1760
|
+
console.log(`${prefix}${relPath}`);
|
|
1761
|
+
|
|
1762
|
+
const edges = graph.edges.filter(e => e.from === file);
|
|
1763
|
+
for (const edge of edges) {
|
|
1764
|
+
printNode(edge.to, indent + 1);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
printNode(graph.root);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function printSearchResults(results, term) {
|
|
1772
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
1773
|
+
console.log(`Found ${totalMatches} matches for "${term}" in ${results.length} files:`);
|
|
1774
|
+
console.log('═'.repeat(60));
|
|
1775
|
+
|
|
1776
|
+
for (const result of results) {
|
|
1777
|
+
console.log(`\n${result.file}`);
|
|
1778
|
+
for (const m of result.matches) {
|
|
1779
|
+
console.log(` ${m.line}: ${m.content.trim()}`);
|
|
1780
|
+
if (m.before && m.before.length > 0) {
|
|
1781
|
+
for (const line of m.before) {
|
|
1782
|
+
console.log(` ... ${line.trim()}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (m.after && m.after.length > 0) {
|
|
1786
|
+
for (const line of m.after) {
|
|
1787
|
+
console.log(` ... ${line.trim()}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// ============================================================================
|
|
1795
|
+
// GLOB MODE
|
|
1796
|
+
// ============================================================================
|
|
1797
|
+
|
|
1798
|
+
function runGlobCommand(pattern, command, arg) {
|
|
1799
|
+
const files = expandGlob(pattern);
|
|
1800
|
+
|
|
1801
|
+
if (files.length === 0) {
|
|
1802
|
+
console.error(`No files match pattern: ${pattern}`);
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
switch (command) {
|
|
1807
|
+
case 'toc':
|
|
1808
|
+
let totalFunctions = 0;
|
|
1809
|
+
let totalClasses = 0;
|
|
1810
|
+
let totalState = 0;
|
|
1811
|
+
let totalLines = 0;
|
|
1812
|
+
const byFile = [];
|
|
1813
|
+
|
|
1814
|
+
for (const file of files) {
|
|
1815
|
+
try {
|
|
1816
|
+
const result = parseFile(file);
|
|
1817
|
+
let functions = result.functions;
|
|
1818
|
+
if (flags.topLevel) {
|
|
1819
|
+
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
1820
|
+
}
|
|
1821
|
+
totalFunctions += functions.length;
|
|
1822
|
+
totalClasses += result.classes.length;
|
|
1823
|
+
totalState += result.stateObjects.length;
|
|
1824
|
+
totalLines += result.totalLines;
|
|
1825
|
+
byFile.push({
|
|
1826
|
+
file,
|
|
1827
|
+
language: result.language,
|
|
1828
|
+
lines: result.totalLines,
|
|
1829
|
+
functions,
|
|
1830
|
+
classes: result.classes,
|
|
1831
|
+
state: result.stateObjects
|
|
1832
|
+
});
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
// Skip unparseable files
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const toc = { totalFiles: files.length, totalLines, totalFunctions, totalClasses, totalState, byFile };
|
|
1839
|
+
if (flags.json) {
|
|
1840
|
+
console.log(output.formatTocJson(toc));
|
|
1841
|
+
} else {
|
|
1842
|
+
printProjectToc(toc);
|
|
1843
|
+
}
|
|
1844
|
+
break;
|
|
1845
|
+
|
|
1846
|
+
case 'find':
|
|
1847
|
+
if (!arg) {
|
|
1848
|
+
console.error('Usage: ucn "pattern" find <name>');
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
findInGlobFiles(files, arg);
|
|
1852
|
+
break;
|
|
1853
|
+
|
|
1854
|
+
case 'search':
|
|
1855
|
+
if (!arg) {
|
|
1856
|
+
console.error('Usage: ucn "pattern" search <term>');
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
searchGlobFiles(files, arg);
|
|
1860
|
+
break;
|
|
1861
|
+
|
|
1862
|
+
default:
|
|
1863
|
+
console.error(`Command "${command}" not supported in glob mode`);
|
|
1864
|
+
process.exit(1);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function findInGlobFiles(files, name) {
|
|
1869
|
+
const allMatches = [];
|
|
1870
|
+
const lowerName = name.toLowerCase();
|
|
1871
|
+
|
|
1872
|
+
for (const file of files) {
|
|
1873
|
+
try {
|
|
1874
|
+
const result = parseFile(file);
|
|
1875
|
+
|
|
1876
|
+
for (const fn of result.functions) {
|
|
1877
|
+
if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
|
|
1878
|
+
allMatches.push({ ...fn, type: 'function', relativePath: file });
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
for (const cls of result.classes) {
|
|
1883
|
+
if (flags.exact ? cls.name === name : cls.name.toLowerCase().includes(lowerName)) {
|
|
1884
|
+
allMatches.push({ ...cls, relativePath: file });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
// Skip
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (flags.json) {
|
|
1893
|
+
console.log(output.formatSymbolJson(allMatches, name));
|
|
1894
|
+
} else {
|
|
1895
|
+
printSymbols(allMatches, name, { top: flags.top, all: flags.all });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function searchGlobFiles(files, term) {
|
|
1900
|
+
const results = [];
|
|
1901
|
+
const regex = new RegExp(escapeRegExp(term), 'gi');
|
|
1902
|
+
|
|
1903
|
+
for (const file of files) {
|
|
1904
|
+
try {
|
|
1905
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
1906
|
+
const lines = content.split('\n');
|
|
1907
|
+
const matches = [];
|
|
1908
|
+
|
|
1909
|
+
lines.forEach((line, idx) => {
|
|
1910
|
+
if (regex.test(line)) {
|
|
1911
|
+
if (flags.codeOnly && isCommentOrString(line)) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const match = { line: idx + 1, content: line };
|
|
1916
|
+
|
|
1917
|
+
if (flags.context > 0) {
|
|
1918
|
+
const before = [];
|
|
1919
|
+
const after = [];
|
|
1920
|
+
for (let i = 1; i <= flags.context; i++) {
|
|
1921
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
1922
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
1923
|
+
}
|
|
1924
|
+
match.before = before;
|
|
1925
|
+
match.after = after;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
matches.push(match);
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
if (matches.length > 0) {
|
|
1933
|
+
results.push({ file, matches });
|
|
1934
|
+
}
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
// Skip
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (flags.json) {
|
|
1941
|
+
console.log(output.formatSearchJson(results, term));
|
|
1942
|
+
} else {
|
|
1943
|
+
printSearchResults(results, term);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// ============================================================================
|
|
1948
|
+
// HELPERS
|
|
1949
|
+
// ============================================================================
|
|
1950
|
+
|
|
1951
|
+
function searchFile(filePath, lines, term) {
|
|
1952
|
+
const regex = new RegExp(escapeRegExp(term), 'gi');
|
|
1953
|
+
const matches = [];
|
|
1954
|
+
|
|
1955
|
+
lines.forEach((line, idx) => {
|
|
1956
|
+
if (regex.test(line)) {
|
|
1957
|
+
if (flags.codeOnly && isCommentOrString(line)) {
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const match = { line: idx + 1, content: line };
|
|
1962
|
+
|
|
1963
|
+
if (flags.context > 0) {
|
|
1964
|
+
const before = [];
|
|
1965
|
+
const after = [];
|
|
1966
|
+
for (let i = 1; i <= flags.context; i++) {
|
|
1967
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
1968
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
1969
|
+
}
|
|
1970
|
+
match.before = before;
|
|
1971
|
+
match.after = after;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
matches.push(match);
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
if (flags.json) {
|
|
1979
|
+
console.log(output.formatSearchJson([{ file: filePath, matches }], term));
|
|
1980
|
+
} else {
|
|
1981
|
+
console.log(`Found ${matches.length} matches for "${term}" in ${filePath}:`);
|
|
1982
|
+
for (const m of matches) {
|
|
1983
|
+
console.log(` ${m.line}: ${m.content.trim()}`);
|
|
1984
|
+
if (m.before && m.before.length > 0) {
|
|
1985
|
+
for (const line of m.before) {
|
|
1986
|
+
console.log(` ... ${line.trim()}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (m.after && m.after.length > 0) {
|
|
1990
|
+
for (const line of m.after) {
|
|
1991
|
+
console.log(` ... ${line.trim()}`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function printLines(lines, range) {
|
|
1999
|
+
const parts = range.split('-');
|
|
2000
|
+
const start = parseInt(parts[0], 10);
|
|
2001
|
+
const end = parts.length > 1 ? parseInt(parts[1], 10) : start;
|
|
2002
|
+
|
|
2003
|
+
// Validate input
|
|
2004
|
+
if (isNaN(start) || isNaN(end)) {
|
|
2005
|
+
console.error(`Invalid line range: "${range}". Expected format: <start>-<end> or <line>`);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (start < 1) {
|
|
2010
|
+
console.error(`Invalid start line: ${start}. Line numbers must be >= 1`);
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Handle reversed range by swapping
|
|
2015
|
+
const startLine = Math.min(start, end);
|
|
2016
|
+
const endLine = Math.max(start, end);
|
|
2017
|
+
|
|
2018
|
+
// Check for out-of-bounds
|
|
2019
|
+
if (startLine > lines.length) {
|
|
2020
|
+
console.error(`Line ${startLine} is out of bounds. File has ${lines.length} lines.`);
|
|
2021
|
+
process.exit(1);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Print lines (clamping end to file length)
|
|
2025
|
+
const actualEnd = Math.min(endLine, lines.length);
|
|
2026
|
+
for (let i = startLine - 1; i < actualEnd; i++) {
|
|
2027
|
+
console.log(`${output.lineNum(i + 1)} │ ${lines[i]}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function suggestSimilar(query, names) {
|
|
2032
|
+
const lower = query.toLowerCase();
|
|
2033
|
+
const similar = names.filter(n => n.toLowerCase().includes(lower));
|
|
2034
|
+
if (similar.length > 0) {
|
|
2035
|
+
console.log('\nDid you mean:');
|
|
2036
|
+
for (const s of similar.slice(0, 5)) {
|
|
2037
|
+
console.log(` - ${s}`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function escapeRegExp(text) {
|
|
2043
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function classifyUsage(line, name) {
|
|
2047
|
+
// Check if it's an import first
|
|
2048
|
+
if (/^\s*(import|from|require|use)\b/.test(line)) {
|
|
2049
|
+
return 'import';
|
|
2050
|
+
}
|
|
2051
|
+
// Check if it's a function call (but not a method call)
|
|
2052
|
+
if (new RegExp('\\b' + escapeRegExp(name) + '\\s*\\(').test(line)) {
|
|
2053
|
+
// Exclude method calls (obj.name, this.name, JSON.name, etc.)
|
|
2054
|
+
if (!isMethodCall(line, name)) {
|
|
2055
|
+
return 'call';
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return 'reference';
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function isMethodCall(line, name) {
|
|
2062
|
+
// Check if there's a dot or ] immediately before the name
|
|
2063
|
+
const methodPattern = new RegExp('[.\\]]\\s*' + escapeRegExp(name) + '\\s*\\(');
|
|
2064
|
+
return methodPattern.test(line);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function isCommentOrString(line) {
|
|
2068
|
+
const trimmed = line.trim();
|
|
2069
|
+
return trimmed.startsWith('//') ||
|
|
2070
|
+
trimmed.startsWith('#') ||
|
|
2071
|
+
trimmed.startsWith('*') ||
|
|
2072
|
+
trimmed.startsWith('/*');
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function isInsideString(line, name) {
|
|
2076
|
+
// Simple heuristic: check if name appears inside quotes
|
|
2077
|
+
// Find all string regions in the line
|
|
2078
|
+
const stringRegex = /(['"`])(?:(?!\1|\\).|\\.)*\1/g;
|
|
2079
|
+
let match;
|
|
2080
|
+
|
|
2081
|
+
while ((match = stringRegex.exec(line)) !== null) {
|
|
2082
|
+
const stringContent = match[0];
|
|
2083
|
+
const stringStart = match.index;
|
|
2084
|
+
const stringEnd = stringStart + stringContent.length;
|
|
2085
|
+
|
|
2086
|
+
// Find where the name appears in the line
|
|
2087
|
+
const nameRegex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
|
|
2088
|
+
let nameMatch;
|
|
2089
|
+
while ((nameMatch = nameRegex.exec(line)) !== null) {
|
|
2090
|
+
const nameStart = nameMatch.index;
|
|
2091
|
+
// Check if this name occurrence is inside the string
|
|
2092
|
+
if (nameStart > stringStart && nameStart < stringEnd) {
|
|
2093
|
+
return true;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
function printUsage() {
|
|
2101
|
+
console.log(`UCN - Universal Code Navigator
|
|
2102
|
+
|
|
2103
|
+
Supported: JavaScript, TypeScript, Python, Go, Rust, Java
|
|
2104
|
+
|
|
2105
|
+
Usage:
|
|
2106
|
+
ucn [command] [args] Project mode (current directory)
|
|
2107
|
+
ucn <file> [command] [args] Single file mode
|
|
2108
|
+
ucn <dir> [command] [args] Project mode (specific directory)
|
|
2109
|
+
ucn "pattern" [command] [args] Glob pattern mode
|
|
2110
|
+
|
|
2111
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2112
|
+
UNDERSTAND CODE (UCN's strength - semantic analysis)
|
|
2113
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2114
|
+
about <name> RECOMMENDED: Full picture (definition, callers, callees, tests, code)
|
|
2115
|
+
context <name> Who calls this + what it calls (numbered for expand)
|
|
2116
|
+
smart <name> Function + all dependencies inline
|
|
2117
|
+
impact <name> What breaks if changed (call sites grouped by file)
|
|
2118
|
+
trace <name> Call tree visualization (--depth=N)
|
|
2119
|
+
|
|
2120
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2121
|
+
FIND CODE
|
|
2122
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2123
|
+
find <name> Find symbol definitions (top 5 by usage count)
|
|
2124
|
+
usages <name> All usages grouped: definitions, calls, imports, references
|
|
2125
|
+
toc Table of contents (functions, classes, state)
|
|
2126
|
+
search <term> Text search (for simple patterns, consider grep instead)
|
|
2127
|
+
tests <name> Find test files for a function
|
|
2128
|
+
|
|
2129
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2130
|
+
EXTRACT CODE
|
|
2131
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2132
|
+
fn <name> Extract function (--file to disambiguate)
|
|
2133
|
+
class <name> Extract class
|
|
2134
|
+
lines <range> Extract line range (e.g., lines 50-100)
|
|
2135
|
+
expand <N> Show code for item N from context output
|
|
2136
|
+
|
|
2137
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2138
|
+
FILE DEPENDENCIES
|
|
2139
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2140
|
+
imports <file> What does file import
|
|
2141
|
+
exporters <file> Who imports this file
|
|
2142
|
+
file-exports <file> What does file export
|
|
2143
|
+
graph <file> Full dependency tree (--depth=N)
|
|
2144
|
+
|
|
2145
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2146
|
+
REFACTORING HELPERS
|
|
2147
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2148
|
+
plan <name> Preview refactoring (--add-param, --remove-param, --rename-to)
|
|
2149
|
+
verify <name> Check all call sites match signature
|
|
2150
|
+
deadcode Find unused functions/classes
|
|
2151
|
+
related <name> Find similar functions (same file, shared deps)
|
|
2152
|
+
|
|
2153
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2154
|
+
OTHER
|
|
2155
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
2156
|
+
api Show exported/public symbols
|
|
2157
|
+
typedef <name> Find type definitions
|
|
2158
|
+
stats Project statistics
|
|
2159
|
+
stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
|
|
2160
|
+
example <name> Best usage example with context
|
|
2161
|
+
|
|
2162
|
+
Common Flags:
|
|
2163
|
+
--file <pattern> Filter by file path (e.g., --file=routes)
|
|
2164
|
+
--exclude=a,b Exclude patterns (e.g., --exclude=test,mock)
|
|
2165
|
+
--in=<path> Only in path (e.g., --in=src/core)
|
|
2166
|
+
--depth=N Trace/graph depth (default: 3)
|
|
2167
|
+
--context=N Lines of context around matches
|
|
2168
|
+
--json Machine-readable output
|
|
2169
|
+
--code-only Filter out comments and strings
|
|
2170
|
+
--with-types Include type definitions
|
|
2171
|
+
--top=N / --all Limit or show all results
|
|
2172
|
+
--include-tests Include test files
|
|
2173
|
+
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
2174
|
+
--no-cache Disable caching
|
|
2175
|
+
--clear-cache Clear cache before running
|
|
2176
|
+
--no-follow-symlinks Don't follow symbolic links
|
|
2177
|
+
-i, --interactive Keep index in memory for multiple queries
|
|
2178
|
+
|
|
2179
|
+
Quick Start:
|
|
2180
|
+
ucn toc # See project structure
|
|
2181
|
+
ucn about handleRequest # Understand a function
|
|
2182
|
+
ucn impact handleRequest # Before modifying
|
|
2183
|
+
ucn fn handleRequest --file api # Extract specific function
|
|
2184
|
+
ucn --interactive # Multiple queries`);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// ============================================================================
|
|
2188
|
+
// INTERACTIVE MODE
|
|
2189
|
+
// ============================================================================
|
|
2190
|
+
|
|
2191
|
+
function runInteractive(rootDir) {
|
|
2192
|
+
const readline = require('readline');
|
|
2193
|
+
// ProjectIndex already required at top of file
|
|
2194
|
+
|
|
2195
|
+
console.log('Building index...');
|
|
2196
|
+
const index = new ProjectIndex(rootDir);
|
|
2197
|
+
index.build(null, { quiet: true });
|
|
2198
|
+
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
|
|
2199
|
+
console.log('Type commands (e.g., "find parseFile", "about main", "toc")');
|
|
2200
|
+
console.log('Type "help" for commands, "quit" to exit\n');
|
|
2201
|
+
|
|
2202
|
+
const rl = readline.createInterface({
|
|
2203
|
+
input: process.stdin,
|
|
2204
|
+
output: process.stdout,
|
|
2205
|
+
prompt: 'ucn> '
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
rl.prompt();
|
|
2209
|
+
|
|
2210
|
+
rl.on('line', (line) => {
|
|
2211
|
+
const input = line.trim();
|
|
2212
|
+
if (!input) {
|
|
2213
|
+
rl.prompt();
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (input === 'quit' || input === 'exit' || input === 'q') {
|
|
2218
|
+
console.log('Goodbye!');
|
|
2219
|
+
rl.close();
|
|
2220
|
+
process.exit(0);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (input === 'help') {
|
|
2224
|
+
console.log(`
|
|
2225
|
+
Commands:
|
|
2226
|
+
toc Project overview
|
|
2227
|
+
find <name> Find symbol
|
|
2228
|
+
about <name> Everything about a symbol
|
|
2229
|
+
usages <name> All usages grouped by type
|
|
2230
|
+
context <name> Callers + callees
|
|
2231
|
+
smart <name> Function + dependencies
|
|
2232
|
+
impact <name> What breaks if changed
|
|
2233
|
+
trace <name> Call tree
|
|
2234
|
+
imports <file> What file imports
|
|
2235
|
+
exporters <file> Who imports file
|
|
2236
|
+
tests <name> Find tests
|
|
2237
|
+
search <term> Text search
|
|
2238
|
+
typedef <name> Find type definitions
|
|
2239
|
+
api Show public symbols
|
|
2240
|
+
stats Index statistics
|
|
2241
|
+
rebuild Rebuild index
|
|
2242
|
+
quit Exit
|
|
2243
|
+
`);
|
|
2244
|
+
rl.prompt();
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
if (input === 'rebuild') {
|
|
2249
|
+
console.log('Rebuilding index...');
|
|
2250
|
+
index.build(null, { quiet: true });
|
|
2251
|
+
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
|
|
2252
|
+
rl.prompt();
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Parse command
|
|
2257
|
+
const parts = input.split(/\s+/);
|
|
2258
|
+
const command = parts[0];
|
|
2259
|
+
const arg = parts.slice(1).join(' ');
|
|
2260
|
+
|
|
2261
|
+
try {
|
|
2262
|
+
executeInteractiveCommand(index, command, arg);
|
|
2263
|
+
} catch (e) {
|
|
2264
|
+
console.error(`Error: ${e.message}`);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
rl.prompt();
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
rl.on('close', () => {
|
|
2271
|
+
process.exit(0);
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function executeInteractiveCommand(index, command, arg) {
|
|
2276
|
+
switch (command) {
|
|
2277
|
+
case 'toc': {
|
|
2278
|
+
const toc = index.getToc();
|
|
2279
|
+
printProjectToc(toc);
|
|
2280
|
+
break;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
case 'find': {
|
|
2284
|
+
if (!arg) {
|
|
2285
|
+
console.log('Usage: find <name>');
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const found = index.find(arg, {});
|
|
2289
|
+
if (found.length === 0) {
|
|
2290
|
+
console.log(`No symbols found for "${arg}"`);
|
|
2291
|
+
} else {
|
|
2292
|
+
printSymbols(found, arg, {});
|
|
2293
|
+
}
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
case 'about': {
|
|
2298
|
+
if (!arg) {
|
|
2299
|
+
console.log('Usage: about <name>');
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const aboutResult = index.about(arg, {});
|
|
2303
|
+
console.log(output.formatAbout(aboutResult, { expand: flags.expand, root: index.root }));
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
case 'usages': {
|
|
2308
|
+
if (!arg) {
|
|
2309
|
+
console.log('Usage: usages <name>');
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const usages = index.usages(arg, {});
|
|
2313
|
+
printUsagesText(usages, arg);
|
|
2314
|
+
break;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
case 'context': {
|
|
2318
|
+
if (!arg) {
|
|
2319
|
+
console.log('Usage: context <name>');
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const ctx = index.context(arg);
|
|
2323
|
+
printContext(ctx, { expand: flags.expand, root: index.root });
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
case 'smart': {
|
|
2328
|
+
if (!arg) {
|
|
2329
|
+
console.log('Usage: smart <name>');
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
const smart = index.smart(arg, {});
|
|
2333
|
+
if (smart) {
|
|
2334
|
+
printSmart(smart);
|
|
2335
|
+
} else {
|
|
2336
|
+
console.log(`Function "${arg}" not found`);
|
|
2337
|
+
}
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
case 'impact': {
|
|
2342
|
+
if (!arg) {
|
|
2343
|
+
console.log('Usage: impact <name>');
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const impactResult = index.impact(arg);
|
|
2347
|
+
console.log(output.formatImpact(impactResult));
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
case 'trace': {
|
|
2352
|
+
if (!arg) {
|
|
2353
|
+
console.log('Usage: trace <name>');
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const traceResult = index.trace(arg, { depth: 3 });
|
|
2357
|
+
console.log(output.formatTrace(traceResult));
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
case 'imports': {
|
|
2362
|
+
if (!arg) {
|
|
2363
|
+
console.log('Usage: imports <file>');
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
const imports = index.imports(arg);
|
|
2367
|
+
console.log(output.formatImports(imports, arg));
|
|
2368
|
+
break;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
case 'exporters': {
|
|
2372
|
+
if (!arg) {
|
|
2373
|
+
console.log('Usage: exporters <file>');
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const exporters = index.exporters(arg);
|
|
2377
|
+
console.log(output.formatExporters(exporters, arg));
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
case 'tests': {
|
|
2382
|
+
if (!arg) {
|
|
2383
|
+
console.log('Usage: tests <name>');
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const tests = index.tests(arg);
|
|
2387
|
+
console.log(output.formatTests(tests, arg));
|
|
2388
|
+
break;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
case 'search': {
|
|
2392
|
+
if (!arg) {
|
|
2393
|
+
console.log('Usage: search <term>');
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const results = index.search(arg, {});
|
|
2397
|
+
printSearchResults(results, arg);
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
case 'typedef': {
|
|
2402
|
+
if (!arg) {
|
|
2403
|
+
console.log('Usage: typedef <name>');
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const types = index.typedef(arg);
|
|
2407
|
+
console.log(output.formatTypedef(types, arg));
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
case 'api': {
|
|
2412
|
+
const api = index.api();
|
|
2413
|
+
console.log(output.formatApi(api, '.'));
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
case 'stats': {
|
|
2418
|
+
const stats = index.getStats();
|
|
2419
|
+
printStats(stats);
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
default:
|
|
2424
|
+
console.log(`Unknown command: ${command}. Type "help" for available commands.`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// ============================================================================
|
|
2429
|
+
// RUN
|
|
2430
|
+
// ============================================================================
|
|
2431
|
+
|
|
2432
|
+
if (flags.interactive) {
|
|
2433
|
+
const target = positionalArgs[0] || '.';
|
|
2434
|
+
runInteractive(target);
|
|
2435
|
+
} else {
|
|
2436
|
+
main();
|
|
2437
|
+
}
|