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.
Files changed (45) hide show
  1. package/.claude/skills/ucn/SKILL.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/cli/index.js +2437 -0
  5. package/core/discovery.js +513 -0
  6. package/core/imports.js +558 -0
  7. package/core/output.js +1274 -0
  8. package/core/parser.js +279 -0
  9. package/core/project.js +3261 -0
  10. package/index.js +52 -0
  11. package/languages/go.js +653 -0
  12. package/languages/index.js +267 -0
  13. package/languages/java.js +826 -0
  14. package/languages/javascript.js +1346 -0
  15. package/languages/python.js +667 -0
  16. package/languages/rust.js +950 -0
  17. package/languages/utils.js +457 -0
  18. package/package.json +42 -0
  19. package/test/fixtures/go/go.mod +3 -0
  20. package/test/fixtures/go/main.go +257 -0
  21. package/test/fixtures/go/service.go +187 -0
  22. package/test/fixtures/java/DataService.java +279 -0
  23. package/test/fixtures/java/Main.java +287 -0
  24. package/test/fixtures/java/Utils.java +199 -0
  25. package/test/fixtures/java/pom.xml +6 -0
  26. package/test/fixtures/javascript/main.js +109 -0
  27. package/test/fixtures/javascript/package.json +1 -0
  28. package/test/fixtures/javascript/service.js +88 -0
  29. package/test/fixtures/javascript/utils.js +67 -0
  30. package/test/fixtures/python/main.py +198 -0
  31. package/test/fixtures/python/pyproject.toml +3 -0
  32. package/test/fixtures/python/service.py +166 -0
  33. package/test/fixtures/python/utils.py +118 -0
  34. package/test/fixtures/rust/Cargo.toml +3 -0
  35. package/test/fixtures/rust/main.rs +253 -0
  36. package/test/fixtures/rust/service.rs +210 -0
  37. package/test/fixtures/rust/utils.rs +154 -0
  38. package/test/fixtures/typescript/main.ts +154 -0
  39. package/test/fixtures/typescript/package.json +1 -0
  40. package/test/fixtures/typescript/repository.ts +149 -0
  41. package/test/fixtures/typescript/types.ts +114 -0
  42. package/test/parser.test.js +3661 -0
  43. package/test/public-repos-test.js +477 -0
  44. package/test/systematic-test.js +619 -0
  45. 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
+ }