ucn 3.7.24 → 3.7.25

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/cli/index.js CHANGED
@@ -10,15 +10,19 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
- const { parse, parseFile, extractFunction, extractClass, cleanHtmlScriptTags, detectLanguage, isSupported } = require('../core/parser');
14
- const { getParser, getLanguageModule } = require('../languages');
13
+ const { parseFile, detectLanguage } = require('../core/parser');
15
14
  const { ProjectIndex } = require('../core/project');
16
15
  const { expandGlob, findProjectRoot } = require('../core/discovery');
17
16
  const output = require('../core/output');
18
- const { pickBestDefinition } = require('../core/shared');
17
+ // pickBestDefinition moved to execute.js — no longer needed here
19
18
  const { getCliCommandSet, resolveCommand } = require('../core/registry');
20
19
  const { execute } = require('../core/execute');
21
- const { ExpandCache, renderExpandItem } = require('../core/expand-cache');
20
+ const { ExpandCache } = require('../core/expand-cache');
21
+
22
+ // Sentinel error for command failures that have already printed their message.
23
+ // Thrown instead of process.exit(1) so finally blocks can run (cache save).
24
+ class CommandError extends Error { constructor() { super(); } }
25
+ function fail(msg) { console.error(msg); throw new CommandError(); }
22
26
 
23
27
  // ============================================================================
24
28
  // ARGUMENT PARSING
@@ -36,70 +40,79 @@ const args = doubleDashIdx === -1 ? rawArgs : rawArgs.slice(0, doubleDashIdx);
36
40
  const argsAfterDoubleDash = doubleDashIdx === -1 ? [] : rawArgs.slice(doubleDashIdx + 1);
37
41
 
38
42
  // Parse flags
39
- const flags = {
40
- json: args.includes('--json'),
41
- quiet: !args.includes('--verbose') && !args.includes('--no-quiet'),
42
- codeOnly: args.includes('--code-only'),
43
- caseSensitive: args.includes('--case-sensitive'),
44
- withTypes: args.includes('--with-types'),
45
- topLevel: args.includes('--top-level'),
46
- exact: args.includes('--exact'),
47
- cache: !args.includes('--no-cache'),
48
- clearCache: args.includes('--clear-cache'),
49
- context: parseInt(args.find(a => a.startsWith('--context='))?.split('=')[1] || '0'),
50
- file: args.find(a => a.startsWith('--file='))?.split('=')[1] || null,
51
- // Semantic filters (--not is alias for --exclude)
52
- exclude: args.find(a => a.startsWith('--exclude=') || a.startsWith('--not='))?.split('=')[1]?.split(',') || [],
53
- in: args.find(a => a.startsWith('--in='))?.split('=')[1] || null,
54
- // Test file inclusion (by default, tests are excluded from usages/find)
55
- includeTests: args.includes('--include-tests'),
56
- // Deadcode options
57
- includeExported: args.includes('--include-exported'),
58
- includeDecorated: args.includes('--include-decorated'),
59
- // Uncertain matches (off by default)
60
- includeUncertain: args.includes('--include-uncertain'),
61
- // Detailed listing (e.g. toc with all symbols)
62
- detailed: args.includes('--detailed'),
63
- // Output depth
64
- depth: args.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
65
- // Inline expansion for callees
66
- expand: args.includes('--expand'),
67
- // Interactive REPL mode
68
- interactive: args.includes('--interactive') || args.includes('-i'),
69
- // Plan command options
70
- addParam: args.find(a => a.startsWith('--add-param='))?.split('=')[1] || null,
71
- removeParam: args.find(a => a.startsWith('--remove-param='))?.split('=')[1] || null,
72
- renameTo: args.find(a => a.startsWith('--rename-to='))?.split('=')[1] || null,
73
- defaultValue: args.find(a => a.startsWith('--default='))?.split('=')[1] || null,
74
- // Smart filtering for find results
75
- top: parseInt(args.find(a => a.startsWith('--top='))?.split('=')[1] || '0'),
76
- all: args.includes('--all'),
77
- // Include method calls in caller/callee analysis
78
- // Tri-state: true (--include-methods), false (--include-methods=false), undefined (let command decide default)
79
- includeMethods: args.includes('--include-methods=false') ? false : args.includes('--include-methods') ? true : undefined,
80
- // Tests: only show call/test-case matches
81
- callsOnly: args.includes('--calls-only'),
82
- // Graph direction (imports/importers/both)
83
- direction: args.find(a => a.startsWith('--direction='))?.split('=')[1] || null,
84
- // Symlink handling (follow by default)
85
- followSymlinks: !args.includes('--no-follow-symlinks'),
86
- // Diff-impact options
87
- base: args.find(a => a.startsWith('--base='))?.split('=')[1] || null,
88
- staged: args.includes('--staged'),
89
- // Regex search mode (default: ON; --no-regex to force plain text)
90
- regex: args.includes('--no-regex') ? false : undefined,
91
- // Stats: per-function line counts
92
- functions: args.includes('--functions'),
93
- // Class: max lines to show (0 = no limit)
94
- maxLines: parseInt(args.find(a => a.startsWith('--max-lines='))?.split('=')[1] || '0') || null
95
- };
96
-
97
- // Handle --file flag with space
98
- const fileArgIdx = args.indexOf('--file');
99
- if (fileArgIdx !== -1 && args[fileArgIdx + 1]) {
100
- flags.file = args[fileArgIdx + 1];
43
+ /**
44
+ * Parse flags from an array of tokens. Supports both --flag=value and --flag value forms.
45
+ * Shared between global CLI mode and interactive mode.
46
+ */
47
+ function parseFlags(tokens) {
48
+ function getValueFlag(flagName) {
49
+ const eqForm = tokens.find(a => a.startsWith(flagName + '='));
50
+ if (eqForm) return eqForm.split('=').slice(1).join('=');
51
+ const idx = tokens.indexOf(flagName);
52
+ if (idx !== -1 && idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
53
+ return tokens[idx + 1];
54
+ }
55
+ return null;
56
+ }
57
+ function parseExclude() {
58
+ const result = [];
59
+ for (const a of tokens) {
60
+ if (a.startsWith('--exclude=') || a.startsWith('--not=')) {
61
+ result.push(...a.split('=').slice(1).join('=').split(','));
62
+ }
63
+ }
64
+ for (const flag of ['--exclude', '--not']) {
65
+ for (let i = 0; i < tokens.length; i++) {
66
+ if (tokens[i] === flag && i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
67
+ result.push(...tokens[i + 1].split(','));
68
+ }
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+ return {
74
+ file: getValueFlag('--file'),
75
+ exclude: parseExclude(),
76
+ in: getValueFlag('--in'),
77
+ includeTests: tokens.includes('--include-tests'),
78
+ includeExported: tokens.includes('--include-exported'),
79
+ includeDecorated: tokens.includes('--include-decorated'),
80
+ includeUncertain: tokens.includes('--include-uncertain'),
81
+ includeMethods: tokens.some(a => a === '--include-methods=false') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
82
+ detailed: tokens.includes('--detailed'),
83
+ topLevel: tokens.includes('--top-level'),
84
+ all: tokens.includes('--all'),
85
+ exact: tokens.includes('--exact'),
86
+ callsOnly: tokens.includes('--calls-only'),
87
+ codeOnly: tokens.includes('--code-only'),
88
+ caseSensitive: tokens.includes('--case-sensitive'),
89
+ withTypes: tokens.includes('--with-types'),
90
+ expand: tokens.includes('--expand'),
91
+ depth: getValueFlag('--depth'),
92
+ top: parseInt(getValueFlag('--top') || '0'),
93
+ context: parseInt(getValueFlag('--context') || '0'),
94
+ direction: getValueFlag('--direction'),
95
+ addParam: getValueFlag('--add-param'),
96
+ removeParam: getValueFlag('--remove-param'),
97
+ renameTo: getValueFlag('--rename-to'),
98
+ defaultValue: getValueFlag('--default'),
99
+ base: getValueFlag('--base'),
100
+ staged: tokens.includes('--staged'),
101
+ maxLines: getValueFlag('--max-lines') || null,
102
+ regex: tokens.includes('--no-regex') ? false : undefined,
103
+ functions: tokens.includes('--functions'),
104
+ };
101
105
  }
102
106
 
107
+ // Parse shared flags from CLI args, then add global-only flags
108
+ const flags = parseFlags(args);
109
+ flags.json = args.includes('--json');
110
+ flags.quiet = !args.includes('--verbose') && !args.includes('--no-quiet');
111
+ flags.cache = !args.includes('--no-cache');
112
+ flags.clearCache = args.includes('--clear-cache');
113
+ flags.interactive = args.includes('--interactive') || args.includes('-i');
114
+ flags.followSymlinks = !args.includes('--no-follow-symlinks');
115
+
103
116
  // Known flags for validation
104
117
  const knownFlags = new Set([
105
118
  '--help', '-h', '--mcp',
@@ -110,7 +123,7 @@ const knownFlags = new Set([
110
123
  '--file', '--context', '--exclude', '--not', '--in',
111
124
  '--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
112
125
  '--default', '--top', '--no-follow-symlinks',
113
- '--base', '--staged',
126
+ '--base', '--staged', '--stack',
114
127
  '--regex', '--no-regex', '--functions',
115
128
  '--max-lines'
116
129
  ]);
@@ -135,12 +148,19 @@ if (unknownFlags.length > 0) {
135
148
  process.exit(1);
136
149
  }
137
150
 
151
+ // Value flags that consume the next token (space form: --flag value)
152
+ const VALUE_FLAGS = new Set([
153
+ '--file', '--depth', '--top', '--context', '--direction',
154
+ '--add-param', '--remove-param', '--rename-to', '--default',
155
+ '--base', '--exclude', '--not', '--in', '--max-lines'
156
+ ]);
157
+
138
158
  // Remove flags from args, then add args after -- (which are all positional)
139
159
  const positionalArgs = [
140
160
  ...args.filter((a, idx) =>
141
161
  !a.startsWith('--') &&
142
162
  a !== '-i' &&
143
- !(idx > 0 && args[idx - 1] === '--file')
163
+ !(idx > 0 && VALUE_FLAGS.has(args[idx - 1]) && !args[idx - 1].includes('='))
144
164
  ),
145
165
  ...argsAfterDoubleDash
146
166
  ];
@@ -160,8 +180,7 @@ const positionalArgs = [
160
180
  */
161
181
  function requireArg(arg, usage) {
162
182
  if (!arg) {
163
- console.error(usage);
164
- process.exit(1);
183
+ fail(usage);
165
184
  }
166
185
  }
167
186
 
@@ -241,406 +260,92 @@ function main() {
241
260
  // ============================================================================
242
261
 
243
262
  function runFileCommand(filePath, command, arg) {
244
- const code = fs.readFileSync(filePath, 'utf-8');
245
- const lines = code.split('\n');
246
263
  const language = detectLanguage(filePath);
247
-
248
264
  if (!language) {
249
265
  console.error(`Unsupported file type: ${filePath}`);
250
266
  process.exit(1);
251
267
  }
252
268
 
253
- const result = parse(code, language);
269
+ const canonical = resolveCommand(command, 'cli') || command;
254
270
 
255
- switch (command) {
256
- case 'toc':
257
- printFileToc(result, filePath);
258
- break;
271
+ // Commands that need full project index — auto-route to project mode
272
+ const fileLocalCommands = new Set(['toc', 'fn', 'class', 'find', 'usages', 'search', 'lines', 'typedef', 'api']);
259
273
 
260
- case 'fn': {
261
- requireArg(arg, 'Usage: ucn <file> fn <name>');
262
- const { fn, code: fnCode } = extractFunction(code, language, arg);
263
- if (fn) {
264
- printOutput({ fn, fnCode },
265
- r => output.formatFunctionJson(r.fn, r.fnCode),
266
- r => {
267
- console.log(`${output.lineRange(r.fn.startLine, r.fn.endLine)} ${output.formatFunctionSignature(r.fn)}`);
268
- console.log('─'.repeat(60));
269
- console.log(r.fnCode);
270
- }
271
- );
272
- } else {
273
- console.error(`Function "${arg}" not found`);
274
- suggestSimilar(arg, result.functions.map(f => f.name));
275
- }
276
- break;
274
+ if (!fileLocalCommands.has(canonical)) {
275
+ // Auto-detect project root and route to project mode
276
+ const projectRoot = findProjectRoot(path.dirname(filePath));
277
+ let effectiveArg = arg;
278
+ if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
279
+ effectiveArg = filePath;
277
280
  }
281
+ runProjectCommand(projectRoot, command, effectiveArg);
282
+ return;
283
+ }
278
284
 
279
- case 'class': {
280
- requireArg(arg, 'Usage: ucn <file> class <name>');
281
- const { cls, code: clsCode } = extractClass(code, language, arg);
282
- if (cls) {
283
- printOutput({ cls, clsCode },
284
- r => JSON.stringify({ ...r.cls, code: r.clsCode }, null, 2),
285
- r => {
286
- console.log(`${output.lineRange(r.cls.startLine, r.cls.endLine)} ${output.formatClassSignature(r.cls)}`);
287
- console.log('─'.repeat(60));
288
- console.log(r.clsCode);
289
- }
290
- );
291
- } else {
292
- console.error(`Class "${arg}" not found`);
293
- suggestSimilar(arg, result.classes.map(c => c.name));
294
- }
295
- break;
296
- }
285
+ // Require arg for commands that need it
286
+ const needsArg = { fn: 'fn <name>', class: 'class <name>', find: 'find <name>', usages: 'usages <name>', search: 'search <term>', lines: 'lines <start-end>', typedef: 'typedef <name>' };
287
+ if (needsArg[canonical]) {
288
+ requireArg(arg, `Usage: ucn <file> ${needsArg[canonical]}`);
289
+ }
290
+
291
+ // Build single-file index and route through execute()
292
+ const index = new ProjectIndex(path.dirname(filePath));
293
+ index.buildSingleFile(filePath);
294
+ const relativePath = path.relative(index.root, path.resolve(filePath));
295
+
296
+ // Map command args to execute() params
297
+ const paramsByCommand = {
298
+ toc: { ...flags },
299
+ fn: { name: arg, file: relativePath, ...flags },
300
+ class: { name: arg, file: relativePath, ...flags },
301
+ find: { name: arg, file: relativePath, ...flags },
302
+ usages: { name: arg, ...flags },
303
+ search: { term: arg, ...flags },
304
+ lines: { file: relativePath, range: arg },
305
+ typedef: { name: arg, ...flags },
306
+ api: { file: relativePath },
307
+ };
297
308
 
298
- case 'find': {
299
- requireArg(arg, 'Usage: ucn <file> find <name>');
300
- findInFile(result, arg, filePath);
301
- break;
302
- }
309
+ const { ok, result, error } = execute(index, canonical, paramsByCommand[canonical]);
310
+ if (!ok) fail(error);
303
311
 
304
- case 'usages': {
305
- requireArg(arg, 'Usage: ucn <file> usages <name>');
306
- usagesInFile(code, lines, arg, filePath, result);
312
+ // Format output using same formatters as project mode
313
+ switch (canonical) {
314
+ case 'toc':
315
+ printOutput(result, output.formatTocJson, r => output.formatToc(r, {
316
+ detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
317
+ uncertainHint: 'use --include-uncertain to include all'
318
+ }));
307
319
  break;
308
- }
309
-
310
- case 'search': {
311
- requireArg(arg, 'Usage: ucn <file> search <term>');
312
- searchFile(filePath, lines, arg);
320
+ case 'find':
321
+ printOutput(result,
322
+ r => output.formatSymbolJson(r, arg),
323
+ r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
324
+ );
313
325
  break;
314
- }
315
-
316
- case 'lines': {
317
- requireArg(arg, 'Usage: ucn <file> lines <start-end>');
318
- printLines(lines, arg);
326
+ case 'fn':
327
+ if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
328
+ printOutput(result, output.formatFnResultJson, output.formatFnResult);
319
329
  break;
320
- }
321
-
322
- case 'typedef': {
323
- requireArg(arg, 'Usage: ucn <file> typedef <name>');
324
- typedefInFile(result, arg, filePath);
330
+ case 'class':
331
+ if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
332
+ printOutput(result, output.formatClassResultJson, output.formatClassResult);
333
+ break;
334
+ case 'lines':
335
+ printOutput(result, output.formatLinesJson, r => output.formatLines(r));
336
+ break;
337
+ case 'usages':
338
+ printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
339
+ break;
340
+ case 'search':
341
+ printOutput(result, r => output.formatSearchJson(r, arg), r => output.formatSearch(r, arg));
342
+ break;
343
+ case 'typedef':
344
+ printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
325
345
  break;
326
- }
327
-
328
346
  case 'api':
329
- apiInFile(result, filePath);
330
- break;
331
-
332
- // Project commands - auto-route to project mode
333
- case 'smart':
334
- case 'context':
335
- case 'tests':
336
- case 'about':
337
- case 'impact':
338
- case 'trace':
339
- case 'related':
340
- case 'example':
341
- case 'graph':
342
- case 'stats':
343
- case 'deadcode':
344
- case 'imports':
345
- case 'what-imports':
346
- case 'exporters':
347
- case 'who-imports':
348
- case 'verify':
349
- case 'plan':
350
- case 'expand':
351
- case 'stacktrace':
352
- case 'stack':
353
- case 'diff-impact':
354
- case 'file-exports':
355
- case 'what-exports': {
356
- // Auto-detect project root and route to project mode
357
- const projectRoot = findProjectRoot(path.dirname(filePath));
358
-
359
- // For file-specific commands (imports/exporters/graph), use the target file as arg if no arg given
360
- const fileCanonical = resolveCommand(command, 'cli') || command;
361
- let effectiveArg = arg;
362
- if ((fileCanonical === 'imports' || fileCanonical === 'exporters' ||
363
- fileCanonical === 'fileExports' || fileCanonical === 'graph') && !arg) {
364
- effectiveArg = filePath;
365
- }
366
-
367
- // For stats/deadcode, no arg needed
368
- if (fileCanonical === 'stats' || fileCanonical === 'deadcode') {
369
- effectiveArg = arg; // may be undefined, that's ok
370
- }
371
-
372
- runProjectCommand(projectRoot, command, effectiveArg);
347
+ printOutput(result, r => output.formatApiJson(r, arg), r => output.formatApi(r, arg));
373
348
  break;
374
- }
375
-
376
- default:
377
- console.error(`Unknown command: ${command}`);
378
- printUsage();
379
- process.exit(1);
380
- }
381
- }
382
-
383
- function printFileToc(result, filePath) {
384
- // Filter for top-level only if flag is set
385
- let functions = result.functions;
386
- if (flags.topLevel) {
387
- functions = functions.filter(fn => !fn.isNested && fn.indent === 0);
388
- }
389
-
390
- if (flags.json) {
391
- console.log(output.formatTocJson({
392
- totalFiles: 1,
393
- totalLines: result.totalLines,
394
- totalFunctions: functions.length,
395
- totalClasses: result.classes.length,
396
- totalState: result.stateObjects.length,
397
- byFile: [{
398
- file: filePath,
399
- language: result.language,
400
- lines: result.totalLines,
401
- functions,
402
- classes: result.classes,
403
- state: result.stateObjects
404
- }]
405
- }));
406
- return;
407
- }
408
-
409
- console.log(`FILE: ${filePath} (${result.totalLines} lines)`);
410
- console.log('═'.repeat(60));
411
-
412
- if (functions.length > 0) {
413
- console.log('\nFUNCTIONS:');
414
- for (const fn of functions) {
415
- const sig = output.formatFunctionSignature(fn);
416
- console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${sig}`);
417
- if (fn.docstring) {
418
- console.log(` ${fn.docstring}`);
419
- }
420
- }
421
- }
422
-
423
- if (result.classes.length > 0) {
424
- console.log('\nCLASSES:');
425
- for (const cls of result.classes) {
426
- console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
427
- if (cls.docstring) {
428
- console.log(` ${cls.docstring}`);
429
- }
430
- if (cls.members && cls.members.length > 0) {
431
- for (const m of cls.members) {
432
- console.log(` ${output.lineLoc(m.startLine)} ${output.formatMemberSignature(m)}`);
433
- }
434
- }
435
- }
436
- }
437
-
438
- if (result.stateObjects.length > 0) {
439
- console.log('\nSTATE:');
440
- for (const s of result.stateObjects) {
441
- console.log(` ${output.lineRange(s.startLine, s.endLine)} ${s.name}`);
442
- }
443
- }
444
- }
445
-
446
- function findInFile(result, name, filePath) {
447
- const matches = [];
448
- const lowerName = name.toLowerCase();
449
-
450
- for (const fn of result.functions) {
451
- if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
452
- matches.push({ ...fn, type: 'function' });
453
- }
454
- }
455
-
456
- for (const cls of result.classes) {
457
- if (flags.exact ? cls.name === name : cls.name.toLowerCase().includes(lowerName)) {
458
- matches.push({ ...cls });
459
- }
460
- }
461
-
462
- if (flags.json) {
463
- console.log(output.formatSymbolJson(matches.map(m => ({ ...m, relativePath: filePath })), name));
464
- } else {
465
- if (matches.length === 0) {
466
- console.log(`No symbols found for "${name}" in ${filePath}`);
467
- } else {
468
- console.log(`Found ${matches.length} match(es) for "${name}" in ${filePath}:`);
469
- console.log('─'.repeat(60));
470
- for (const m of matches) {
471
- const sig = m.params !== undefined
472
- ? output.formatFunctionSignature(m)
473
- : output.formatClassSignature(m);
474
- console.log(`${filePath}:${m.startLine} ${sig}`);
475
- }
476
- }
477
- }
478
- }
479
-
480
- function usagesInFile(code, lines, name, filePath, result) {
481
- const usages = [];
482
-
483
- // Get definitions
484
- const defs = [];
485
- for (const fn of result.functions) {
486
- if (fn.name === name) {
487
- defs.push({ ...fn, type: 'function', isDefinition: true, line: fn.startLine });
488
- }
489
- }
490
- for (const cls of result.classes) {
491
- if (cls.name === name) {
492
- defs.push({ ...cls, isDefinition: true, line: cls.startLine });
493
- }
494
- }
495
-
496
- // Try AST-based detection first
497
- const lang = detectLanguage(filePath);
498
- const langModule = getLanguageModule(lang);
499
-
500
- if (langModule && typeof langModule.findUsagesInCode === 'function') {
501
- try {
502
- const parser = getParser(lang);
503
- if (parser) {
504
- const astUsages = langModule.findUsagesInCode(code, name, parser);
505
-
506
- for (const u of astUsages) {
507
- // Skip definition lines
508
- if (defs.some(d => d.startLine === u.line)) {
509
- continue;
510
- }
511
-
512
- const lineContent = lines[u.line - 1] || '';
513
- const usage = {
514
- file: filePath,
515
- relativePath: filePath,
516
- line: u.line,
517
- content: lineContent,
518
- usageType: u.usageType,
519
- isDefinition: false
520
- };
521
-
522
- // Add context
523
- if (flags.context > 0) {
524
- const idx = u.line - 1;
525
- const before = [];
526
- const after = [];
527
- for (let i = 1; i <= flags.context; i++) {
528
- if (idx - i >= 0) before.unshift(lines[idx - i]);
529
- if (idx + i < lines.length) after.push(lines[idx + i]);
530
- }
531
- usage.before = before;
532
- usage.after = after;
533
- }
534
-
535
- usages.push(usage);
536
- }
537
-
538
- // Add definitions to result and output
539
- const allUsages = [
540
- ...defs.map(d => ({
541
- ...d,
542
- relativePath: filePath,
543
- content: lines[d.startLine - 1],
544
- signature: d.params !== undefined
545
- ? output.formatFunctionSignature(d)
546
- : output.formatClassSignature(d)
547
- })),
548
- ...usages
549
- ];
550
-
551
- if (flags.json) {
552
- console.log(output.formatUsagesJson(allUsages, name));
553
- } else {
554
- console.log(output.formatUsages(allUsages, name));
555
- }
556
- return;
557
- }
558
- } catch (e) {
559
- // AST parsing failed — usages will be empty, only definitions shown
560
- }
561
- }
562
-
563
- // Output definitions + any usages found via AST
564
- const allUsages = [
565
- ...defs.map(d => ({
566
- ...d,
567
- relativePath: filePath,
568
- content: lines[d.startLine - 1],
569
- signature: d.params !== undefined
570
- ? output.formatFunctionSignature(d)
571
- : output.formatClassSignature(d)
572
- })),
573
- ...usages
574
- ];
575
-
576
- if (flags.json) {
577
- console.log(output.formatUsagesJson(allUsages, name));
578
- } else {
579
- console.log(output.formatUsages(allUsages, name));
580
- }
581
- }
582
-
583
- function typedefInFile(result, name, filePath) {
584
- const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class'];
585
- const matches = result.classes.filter(c =>
586
- typeKinds.includes(c.type) &&
587
- (flags.exact ? c.name === name : c.name.toLowerCase().includes(name.toLowerCase()))
588
- );
589
-
590
- // Extract source code for each match
591
- const absPath = path.resolve(filePath);
592
- let fileLines = null;
593
- try { fileLines = fs.readFileSync(absPath, 'utf-8').split('\n'); } catch (e) { /* ignore */ }
594
- const enriched = matches.map(m => {
595
- const obj = { ...m, relativePath: filePath };
596
- if (fileLines && m.startLine && m.endLine) {
597
- obj.code = fileLines.slice(m.startLine - 1, m.endLine).join('\n');
598
- }
599
- return obj;
600
- });
601
-
602
- if (flags.json) {
603
- console.log(output.formatTypedefJson(enriched, name));
604
- } else {
605
- console.log(output.formatTypedef(enriched, name));
606
- }
607
- }
608
-
609
- function apiInFile(result, filePath) {
610
- const exported = [];
611
-
612
- for (const fn of result.functions) {
613
- if (fn.modifiers && (fn.modifiers.includes('export') || fn.modifiers.includes('public'))) {
614
- exported.push({
615
- name: fn.name,
616
- type: 'function',
617
- file: filePath,
618
- startLine: fn.startLine,
619
- endLine: fn.endLine,
620
- params: fn.params,
621
- returnType: fn.returnType,
622
- signature: output.formatFunctionSignature(fn)
623
- });
624
- }
625
- }
626
-
627
- for (const cls of result.classes) {
628
- if (cls.modifiers && (cls.modifiers.includes('export') || cls.modifiers.includes('public'))) {
629
- exported.push({
630
- name: cls.name,
631
- type: cls.type,
632
- file: filePath,
633
- startLine: cls.startLine,
634
- endLine: cls.endLine,
635
- signature: output.formatClassSignature(cls)
636
- });
637
- }
638
- }
639
-
640
- if (flags.json) {
641
- console.log(output.formatApiJson(exported, filePath));
642
- } else {
643
- console.log(output.formatApi(exported, filePath));
644
349
  }
645
350
  }
646
351
 
@@ -687,13 +392,10 @@ function runProjectCommand(rootDir, command, arg) {
687
392
 
688
393
  // Build/rebuild if cache not used
689
394
  // If cache was loaded but stale, force rebuild to avoid duplicates
395
+ let needsCacheSave = false;
690
396
  if (!usedCache) {
691
397
  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
- }
398
+ needsCacheSave = flags.cache;
697
399
  }
698
400
 
699
401
  try {
@@ -705,7 +407,7 @@ function runProjectCommand(rootDir, command, arg) {
705
407
 
706
408
  case 'toc': {
707
409
  const { ok, result, error } = execute(index, 'toc', flags);
708
- if (!ok) { console.error(error); process.exit(1); }
410
+ if (!ok) fail(error);
709
411
  printOutput(result, output.formatTocJson, r => output.formatToc(r, {
710
412
  detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
711
413
  uncertainHint: 'use --include-uncertain to include all'
@@ -715,17 +417,17 @@ function runProjectCommand(rootDir, command, arg) {
715
417
 
716
418
  case 'find': {
717
419
  const { ok, result, error } = execute(index, 'find', { name: arg, ...flags });
718
- if (!ok) { console.error(error); process.exit(1); }
420
+ if (!ok) fail(error);
719
421
  printOutput(result,
720
422
  r => output.formatSymbolJson(r, arg),
721
- r => { printSymbols(r, arg, { depth: flags.depth, top: flags.top, all: flags.all }); }
423
+ r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
722
424
  );
723
425
  break;
724
426
  }
725
427
 
726
428
  case 'usages': {
727
429
  const { ok, result, error } = execute(index, 'usages', { name: arg, ...flags });
728
- if (!ok) { console.error(error); process.exit(1); }
430
+ if (!ok) fail(error);
729
431
  printOutput(result,
730
432
  r => output.formatUsagesJson(r, arg),
731
433
  r => output.formatUsages(r, arg)
@@ -735,7 +437,7 @@ function runProjectCommand(rootDir, command, arg) {
735
437
 
736
438
  case 'example': {
737
439
  const { ok, result, error } = execute(index, 'example', { name: arg });
738
- if (!ok) { console.error(error); process.exit(1); }
440
+ if (!ok) fail(error);
739
441
  printOutput(result,
740
442
  r => output.formatExampleJson(r, arg),
741
443
  r => output.formatExample(r, arg)
@@ -745,7 +447,7 @@ function runProjectCommand(rootDir, command, arg) {
745
447
 
746
448
  case 'context': {
747
449
  const { ok, result: ctx, error } = execute(index, 'context', { name: arg, ...flags });
748
- if (!ok) { console.log(error); break; }
450
+ if (!ok) fail(error);
749
451
  if (flags.json) {
750
452
  console.log(output.formatContextJson(ctx));
751
453
  } else {
@@ -789,26 +491,22 @@ function runProjectCommand(rootDir, command, arg) {
789
491
  requireArg(arg, 'Usage: ucn . expand <N>\nFirst run "ucn . context <name>" to get numbered items');
790
492
  const expandNum = parseInt(arg);
791
493
  if (isNaN(expandNum)) {
792
- console.error(`Invalid item number: "${arg}"`);
793
- process.exit(1);
494
+ fail(`Invalid item number: "${arg}"`);
794
495
  }
795
496
  const cached = loadExpandableItems(index.root);
796
- if (!cached || !cached.items || cached.items.length === 0) {
797
- console.error('No expandable items found. Run "ucn . context <name>" first.');
798
- process.exit(1);
799
- }
800
- const item = cached.items.find(i => i.num === expandNum);
801
- if (!item) {
802
- console.error(`Item ${expandNum} not found. Available: 1-${cached.items.length}`);
803
- process.exit(1);
804
- }
805
- printExpandedItem(item, cached.root || index.root);
497
+ const items = cached?.items || [];
498
+ const match = items.find(i => i.num === expandNum);
499
+ const { ok, result, error } = execute(index, 'expand', {
500
+ match, itemNum: expandNum, itemCount: items.length
501
+ });
502
+ if (!ok) fail(error);
503
+ console.log(result.text);
806
504
  break;
807
505
  }
808
506
 
809
507
  case 'smart': {
810
508
  const { ok, result, error } = execute(index, 'smart', { name: arg, ...flags });
811
- if (!ok) { console.error(error); break; }
509
+ if (!ok) fail(error);
812
510
  printOutput(result, output.formatSmartJson, r => output.formatSmart(r, {
813
511
  uncertainHint: 'use --include-uncertain to include all'
814
512
  }));
@@ -817,7 +515,7 @@ function runProjectCommand(rootDir, command, arg) {
817
515
 
818
516
  case 'about': {
819
517
  const { ok, result, error } = execute(index, 'about', { name: arg, ...flags });
820
- if (!ok) { console.error(error); process.exit(1); }
518
+ if (!ok) fail(error);
821
519
  printOutput(result,
822
520
  output.formatAboutJson,
823
521
  r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
@@ -827,80 +525,71 @@ function runProjectCommand(rootDir, command, arg) {
827
525
 
828
526
  case 'impact': {
829
527
  const { ok, result, error } = execute(index, 'impact', { name: arg, ...flags });
830
- if (!ok) { console.error(error); process.exit(1); }
528
+ if (!ok) fail(error);
831
529
  printOutput(result, output.formatImpactJson, output.formatImpact);
832
530
  break;
833
531
  }
834
532
 
835
533
  case 'plan': {
836
534
  const { ok, result, error } = execute(index, 'plan', { name: arg, ...flags });
837
- if (!ok) { console.error(error); process.exit(1); }
535
+ if (!ok) fail(error);
838
536
  printOutput(result, output.formatPlanJson, output.formatPlan);
839
537
  break;
840
538
  }
841
539
 
842
540
  case 'trace': {
843
541
  const { ok, result, error } = execute(index, 'trace', { name: arg, ...flags });
844
- if (!ok) { console.error(error); process.exit(1); }
542
+ if (!ok) fail(error);
845
543
  printOutput(result, output.formatTraceJson, output.formatTrace);
846
544
  break;
847
545
  }
848
546
 
849
547
  case 'stacktrace': {
850
548
  const { ok, result, error } = execute(index, 'stacktrace', { stack: arg });
851
- if (!ok) { console.error(error); process.exit(1); }
549
+ if (!ok) fail(error);
852
550
  printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
853
551
  break;
854
552
  }
855
553
 
856
554
  case 'verify': {
857
555
  const { ok, result, error } = execute(index, 'verify', { name: arg, file: flags.file });
858
- if (!ok) { console.error(error); process.exit(1); }
556
+ if (!ok) fail(error);
859
557
  printOutput(result, output.formatVerifyJson, output.formatVerify);
860
558
  break;
861
559
  }
862
560
 
863
561
  case 'related': {
864
562
  const { ok, result, error } = execute(index, 'related', { name: arg, ...flags });
865
- if (!ok) { console.error(error); process.exit(1); }
563
+ if (!ok) fail(error);
866
564
  printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { showAll: flags.all, top: flags.top }));
867
565
  break;
868
566
  }
869
567
 
870
- // ── Commands staying in adapter (complex I/O) ───────────────────
568
+ // ── Extraction commands (via execute) ────────────────────────────
871
569
 
872
570
  case 'fn': {
873
571
  requireArg(arg, 'Usage: ucn . fn <name>');
874
- if (arg.includes(',')) {
875
- const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
876
- for (let i = 0; i < fnNames.length; i++) {
877
- if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
878
- extractFunctionFromProject(index, fnNames[i]);
879
- }
880
- } else {
881
- extractFunctionFromProject(index, arg);
882
- }
572
+ const { ok, result, error } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all });
573
+ if (!ok) fail(error);
574
+ if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
575
+ printOutput(result, output.formatFnResultJson, output.formatFnResult);
883
576
  break;
884
577
  }
885
578
 
886
579
  case 'class': {
887
580
  requireArg(arg, 'Usage: ucn . class <name>');
888
- extractClassFromProject(index, arg);
581
+ const { ok, result, error } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
582
+ if (!ok) fail(error);
583
+ if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
584
+ printOutput(result, output.formatClassResultJson, output.formatClassResult);
889
585
  break;
890
586
  }
891
587
 
892
588
  case 'lines': {
893
- if (!arg || !flags.file) {
894
- console.error('Usage: ucn . lines <range> --file <path>');
895
- process.exit(1);
896
- }
897
- const filePath = index.findFile(flags.file);
898
- if (!filePath) {
899
- console.error(`File not found: ${flags.file}`);
900
- process.exit(1);
901
- }
902
- const fileContent = fs.readFileSync(filePath, 'utf-8');
903
- printLines(fileContent.split('\n'), arg);
589
+ requireArg(arg, 'Usage: ucn . lines <range> --file <path>');
590
+ const { ok, result, error } = execute(index, 'lines', { file: flags.file, range: arg });
591
+ if (!ok) fail(error);
592
+ printOutput(result, output.formatLinesJson, r => output.formatLines(r));
904
593
  break;
905
594
  }
906
595
 
@@ -908,7 +597,7 @@ function runProjectCommand(rootDir, command, arg) {
908
597
 
909
598
  case 'imports': {
910
599
  const { ok, result, error } = execute(index, 'imports', { file: arg });
911
- if (!ok) { console.error(error); process.exit(1); }
600
+ if (!ok) fail(error);
912
601
  printOutput(result,
913
602
  r => output.formatImportsJson(r, arg),
914
603
  r => output.formatImports(r, arg)
@@ -918,7 +607,7 @@ function runProjectCommand(rootDir, command, arg) {
918
607
 
919
608
  case 'exporters': {
920
609
  const { ok, result, error } = execute(index, 'exporters', { file: arg });
921
- if (!ok) { console.error(error); process.exit(1); }
610
+ if (!ok) fail(error);
922
611
  printOutput(result,
923
612
  r => output.formatExportersJson(r, arg),
924
613
  r => output.formatExporters(r, arg)
@@ -928,7 +617,7 @@ function runProjectCommand(rootDir, command, arg) {
928
617
 
929
618
  case 'fileExports': {
930
619
  const { ok, result, error } = execute(index, 'fileExports', { file: arg });
931
- if (!ok) { console.error(error); process.exit(1); }
620
+ if (!ok) fail(error);
932
621
  printOutput(result,
933
622
  r => JSON.stringify({ file: arg, exports: r }, null, 2),
934
623
  r => output.formatFileExports(r, arg)
@@ -938,14 +627,14 @@ function runProjectCommand(rootDir, command, arg) {
938
627
 
939
628
  case 'graph': {
940
629
  const { ok, result, error } = execute(index, 'graph', { file: arg, direction: flags.direction, depth: flags.depth, all: flags.all });
941
- if (!ok) { console.error(error); process.exit(1); }
630
+ if (!ok) fail(error);
942
631
  printOutput(result,
943
632
  r => JSON.stringify({
944
633
  root: path.relative(index.root, r.root),
945
634
  nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
946
635
  edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
947
636
  }, null, 2),
948
- r => output.formatGraph(r, { showAll: flags.all || flags.depth !== undefined, maxDepth: flags.depth ?? 2, file: arg })
637
+ r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: arg })
949
638
  );
950
639
  break;
951
640
  }
@@ -954,7 +643,7 @@ function runProjectCommand(rootDir, command, arg) {
954
643
 
955
644
  case 'typedef': {
956
645
  const { ok, result, error } = execute(index, 'typedef', { name: arg, exact: flags.exact });
957
- if (!ok) { console.error(error); process.exit(1); }
646
+ if (!ok) fail(error);
958
647
  printOutput(result,
959
648
  r => output.formatTypedefJson(r, arg),
960
649
  r => output.formatTypedef(r, arg)
@@ -964,7 +653,7 @@ function runProjectCommand(rootDir, command, arg) {
964
653
 
965
654
  case 'tests': {
966
655
  const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly });
967
- if (!ok) { console.error(error); process.exit(1); }
656
+ if (!ok) fail(error);
968
657
  printOutput(result,
969
658
  r => output.formatTestsJson(r, arg),
970
659
  r => output.formatTests(r, arg)
@@ -974,7 +663,7 @@ function runProjectCommand(rootDir, command, arg) {
974
663
 
975
664
  case 'api': {
976
665
  const { ok, result, error } = execute(index, 'api', { file: arg });
977
- if (!ok) { console.error(error); process.exit(1); }
666
+ if (!ok) fail(error);
978
667
  printOutput(result,
979
668
  r => output.formatApiJson(r, arg),
980
669
  r => output.formatApi(r, arg)
@@ -984,7 +673,7 @@ function runProjectCommand(rootDir, command, arg) {
984
673
 
985
674
  case 'search': {
986
675
  const { ok, result, error } = execute(index, 'search', { term: arg, ...flags });
987
- if (!ok) { console.error(error); process.exit(1); }
676
+ if (!ok) fail(error);
988
677
  printOutput(result,
989
678
  r => output.formatSearchJson(r, arg),
990
679
  r => output.formatSearch(r, arg)
@@ -994,7 +683,7 @@ function runProjectCommand(rootDir, command, arg) {
994
683
 
995
684
  case 'deadcode': {
996
685
  const { ok, result, error } = execute(index, 'deadcode', { ...flags, in: flags.in || subdirScope });
997
- if (!ok) { console.error(error); process.exit(1); }
686
+ if (!ok) fail(error);
998
687
  printOutput(result,
999
688
  output.formatDeadcodeJson,
1000
689
  r => output.formatDeadcode(r, {
@@ -1008,7 +697,7 @@ function runProjectCommand(rootDir, command, arg) {
1008
697
 
1009
698
  case 'stats': {
1010
699
  const { ok, result, error } = execute(index, 'stats', { functions: flags.functions });
1011
- if (!ok) { console.error(error); process.exit(1); }
700
+ if (!ok) fail(error);
1012
701
  printOutput(result,
1013
702
  output.formatStatsJson,
1014
703
  r => output.formatStats(r, { top: flags.top })
@@ -1018,7 +707,7 @@ function runProjectCommand(rootDir, command, arg) {
1018
707
 
1019
708
  case 'diffImpact': {
1020
709
  const { ok, result, error } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file });
1021
- if (!ok) { console.error(error); process.exit(1); }
710
+ if (!ok) fail(error);
1022
711
  printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
1023
712
  break;
1024
713
  }
@@ -1026,310 +715,25 @@ function runProjectCommand(rootDir, command, arg) {
1026
715
  default:
1027
716
  console.error(`Unknown command: ${canonical}`);
1028
717
  printUsage();
1029
- process.exit(1);
718
+ throw new CommandError();
1030
719
  }
1031
720
  } catch (e) {
1032
- console.error(`Error: ${e.message}`);
1033
- process.exit(1);
1034
- }
1035
- }
1036
-
1037
- function extractFunctionFromProject(index, name, overrideFlags) {
1038
- const f = overrideFlags || flags;
1039
- const matches = index.find(name, { file: f.file }).filter(m => m.type === 'function' || m.params !== undefined);
1040
-
1041
- if (matches.length === 0) {
1042
- console.error(`Function "${name}" not found`);
1043
- return;
1044
- }
1045
-
1046
- if (matches.length > 1 && !f.file && f.all) {
1047
- // Show all definitions
1048
- for (let i = 0; i < matches.length; i++) {
1049
- const m = matches[i];
1050
- const code = fs.readFileSync(m.file, 'utf-8');
1051
- const lines = code.split('\n');
1052
- const extracted = lines.slice(m.startLine - 1, m.endLine);
1053
- const fnCode = cleanHtmlScriptTags(extracted, detectLanguage(m.file)).join('\n');
1054
- if (i > 0) console.log('');
1055
- if (f.json) {
1056
- console.log(output.formatFunctionJson(m, fnCode));
1057
- } else {
1058
- console.log(output.formatFn(m, fnCode));
1059
- }
1060
- }
1061
- return;
1062
- }
1063
-
1064
- let match;
1065
- if (matches.length > 1 && !f.file) {
1066
- // Auto-select best match using same scoring as resolveSymbol
1067
- match = pickBestDefinition(matches);
1068
- const others = matches.filter(m => m !== match).map(m => `${m.relativePath}:${m.startLine}`).join(', ');
1069
- console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Also in: ${others}. Use --file to disambiguate or --all to show all.`);
1070
- } else {
1071
- match = matches[0];
1072
- }
1073
-
1074
- // Extract code directly using symbol index location (works for class methods and overloads)
1075
- const code = fs.readFileSync(match.file, 'utf-8');
1076
- const lines = code.split('\n');
1077
- const extracted = lines.slice(match.startLine - 1, match.endLine);
1078
- const fnCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
1079
-
1080
- if (f.json) {
1081
- console.log(output.formatFunctionJson(match, fnCode));
1082
- } else {
1083
- console.log(output.formatFn(match, fnCode));
1084
- }
1085
- }
1086
-
1087
- function extractClassFromProject(index, name, overrideFlags) {
1088
- const f = overrideFlags || flags;
1089
- const matches = index.find(name, { file: f.file }).filter(m =>
1090
- ['class', 'interface', 'type', 'enum', 'struct', 'trait'].includes(m.type)
1091
- );
1092
-
1093
- if (matches.length === 0) {
1094
- console.error(`Class "${name}" not found`);
1095
- return;
1096
- }
1097
-
1098
- if (matches.length > 1 && !f.file && f.all) {
1099
- // Show all definitions using index data (no re-parsing)
1100
- for (let i = 0; i < matches.length; i++) {
1101
- const m = matches[i];
1102
- const code = fs.readFileSync(m.file, 'utf-8');
1103
- const codeLines = code.split('\n');
1104
- const extracted = codeLines.slice(m.startLine - 1, m.endLine);
1105
- const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(m.file)).join('\n');
1106
- if (i > 0) console.log('');
1107
- if (f.json) {
1108
- console.log(JSON.stringify({ ...m, code: clsCode }, null, 2));
1109
- } else {
1110
- console.log(output.formatClass(m, clsCode));
1111
- }
1112
- }
1113
- return;
1114
- }
1115
-
1116
- let match;
1117
- if (matches.length > 1 && !f.file) {
1118
- // Auto-select best match using same scoring as resolveSymbol
1119
- match = pickBestDefinition(matches);
1120
- const others = matches.filter(m => m !== match).map(m => `${m.relativePath}:${m.startLine}`).join(', ');
1121
- console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Also in: ${others}. Use --file to disambiguate or --all to show all.`);
1122
- } else {
1123
- match = matches[0];
1124
- }
1125
-
1126
- // Use index data directly instead of re-parsing the file
1127
- const code = fs.readFileSync(match.file, 'utf-8');
1128
- const codeLines = code.split('\n');
1129
- const classLineCount = match.endLine - match.startLine + 1;
1130
-
1131
- // Large class summary (>200 lines) when no --max-lines specified
1132
- if (classLineCount > 200 && !f.maxLines) {
1133
- if (f.json) {
1134
- const extracted = codeLines.slice(match.startLine - 1, match.endLine);
1135
- const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
1136
- console.log(JSON.stringify({ ...match, code: clsCode }, null, 2));
1137
- } else {
1138
- const lines = [];
1139
- lines.push(`${match.relativePath}:${match.startLine}`);
1140
- lines.push(`${output.lineRange(match.startLine, match.endLine)} ${output.formatClassSignature(match)}`);
1141
- lines.push('\u2500'.repeat(60));
1142
- const methods = index.findMethodsForType(match.name);
1143
- if (methods.length > 0) {
1144
- lines.push(`\nMethods (${methods.length}):`);
1145
- for (const m of methods) {
1146
- lines.push(` ${output.formatFunctionSignature(m)} [line ${m.startLine}]`);
1147
- }
1148
- }
1149
- lines.push(`\nClass is ${classLineCount} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
1150
- console.log(lines.join('\n'));
1151
- }
1152
- return;
1153
- }
1154
-
1155
- // Truncated source with --max-lines
1156
- if (f.maxLines && classLineCount > f.maxLines) {
1157
- const truncated = codeLines.slice(match.startLine - 1, match.startLine - 1 + f.maxLines);
1158
- const truncatedCode = cleanHtmlScriptTags(truncated, detectLanguage(match.file)).join('\n');
1159
- if (f.json) {
1160
- console.log(JSON.stringify({ ...match, code: truncatedCode, truncated: true, totalLines: classLineCount }, null, 2));
1161
- } else {
1162
- console.log(output.formatClass(match, truncatedCode));
1163
- console.log(`\n... showing ${f.maxLines} of ${classLineCount} lines`);
1164
- }
1165
- return;
1166
- }
1167
-
1168
- const extracted = codeLines.slice(match.startLine - 1, match.endLine);
1169
- const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
1170
-
1171
- if (f.json) {
1172
- console.log(JSON.stringify({ ...match, code: clsCode }, null, 2));
1173
- } else {
1174
- console.log(output.formatClass(match, clsCode));
1175
- }
1176
- }
1177
-
1178
-
1179
- function printSymbols(symbols, query, options = {}) {
1180
- const { depth, top, all } = options;
1181
- const DEFAULT_LIMIT = 5;
1182
-
1183
- if (symbols.length === 0) {
1184
- console.log(`No symbols found for "${query}"`);
1185
- return;
1186
- }
1187
-
1188
- // Determine how many to show
1189
- const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
1190
- const showing = Math.min(limit, symbols.length);
1191
- const hidden = symbols.length - showing;
1192
-
1193
- if (hidden > 0) {
1194
- console.log(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
1195
- } else {
1196
- console.log(`Found ${symbols.length} match(es) for "${query}":`);
1197
- }
1198
- console.log('─'.repeat(60));
1199
-
1200
- for (let i = 0; i < showing; i++) {
1201
- const s = symbols[i];
1202
- // Depth 0: just location
1203
- if (depth === '0') {
1204
- console.log(`${s.relativePath}:${s.startLine}`);
1205
- continue;
1206
- }
1207
-
1208
- // Depth 1 (default): location + signature
1209
- const sig = s.params !== undefined
1210
- ? output.formatFunctionSignature(s)
1211
- : output.formatClassSignature(s);
1212
-
1213
- // Compute and display confidence indicator
1214
- const confidence = computeConfidence(s);
1215
- const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
1216
-
1217
- console.log(`${s.relativePath}:${s.startLine} ${sig}${confStr}`);
1218
- if (s.usageCounts !== undefined) {
1219
- const c = s.usageCounts;
1220
- const parts = [];
1221
- if (c.calls > 0) parts.push(`${c.calls} calls`);
1222
- if (c.definitions > 0) parts.push(`${c.definitions} def`);
1223
- if (c.imports > 0) parts.push(`${c.imports} imports`);
1224
- if (c.references > 0) parts.push(`${c.references} refs`);
1225
- console.log(` (${c.total} usages: ${parts.join(', ')})`);
1226
- } else if (s.usageCount !== undefined) {
1227
- console.log(` (${s.usageCount} usages)`);
1228
- }
1229
-
1230
- // Show confidence reason if not high
1231
- if (confidence.level !== 'high' && confidence.reasons.length > 0) {
1232
- console.log(` ⚠ ${confidence.reasons.join(', ')}`);
1233
- }
1234
-
1235
- // Depth 2: + first 10 lines of code
1236
- if (depth === '2' || depth === 'full') {
1237
- try {
1238
- const content = fs.readFileSync(s.file, 'utf-8');
1239
- const lines = content.split('\n');
1240
- const maxLines = depth === 'full' ? (s.endLine - s.startLine + 1) : 10;
1241
- const endLine = Math.min(s.startLine + maxLines - 1, s.endLine);
1242
- console.log(' ───');
1243
- for (let i = s.startLine - 1; i < endLine; i++) {
1244
- console.log(` ${lines[i]}`);
1245
- }
1246
- if (depth === '2' && s.endLine > endLine) {
1247
- console.log(` ... (${s.endLine - endLine} more lines)`);
1248
- }
1249
- } catch (e) {
1250
- // Skip code extraction on error
1251
- }
721
+ if (!(e instanceof CommandError)) {
722
+ console.error(`Error: ${e.message}`);
1252
723
  }
1253
- console.log('');
1254
- }
1255
-
1256
- // Show hint about hidden results
1257
- if (hidden > 0) {
1258
- console.log(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
1259
- }
1260
- }
1261
-
1262
- /**
1263
- * Compute confidence level for a symbol match
1264
- * @returns {{ level: 'high'|'medium'|'low', reasons: string[] }}
1265
- */
1266
- function computeConfidence(symbol) {
1267
- const reasons = [];
1268
- let score = 100;
1269
-
1270
- // Check function span (very long functions may have incorrect boundaries)
1271
- const span = (symbol.endLine || symbol.startLine) - symbol.startLine;
1272
- if (span > 500) {
1273
- score -= 30;
1274
- reasons.push('very long function (>500 lines)');
1275
- } else if (span > 200) {
1276
- score -= 15;
1277
- reasons.push('long function (>200 lines)');
1278
- }
1279
-
1280
- // Check for complex type annotations (nested generics)
1281
- const params = Array.isArray(symbol.params) ? symbol.params : [];
1282
- const signature = params.map(p => p.type || '').join(' ') + (symbol.returnType || '');
1283
- const genericDepth = countNestedGenerics(signature);
1284
- if (genericDepth > 3) {
1285
- score -= 20;
1286
- reasons.push('complex nested generics');
1287
- } else if (genericDepth > 2) {
1288
- score -= 10;
1289
- reasons.push('nested generics');
1290
- }
1291
-
1292
- // Check file size by checking if file property exists and getting line count
1293
- if (symbol.file) {
1294
- try {
1295
- const stats = fs.statSync(symbol.file);
1296
- const sizeKB = stats.size / 1024;
1297
- if (sizeKB > 500) {
1298
- score -= 20;
1299
- reasons.push('very large file (>500KB)');
1300
- } else if (sizeKB > 200) {
1301
- score -= 10;
1302
- reasons.push('large file (>200KB)');
1303
- }
1304
- } catch (e) {
1305
- // Skip file size check on error
724
+ process.exitCode = 1;
725
+ } finally {
726
+ // Save cache after command execution so callsCache populated
727
+ // by findCallers/findCallees gets persisted to disk.
728
+ // On cache-hit runs, only re-save if callsCache was mutated.
729
+ if (flags.cache && (needsCacheSave || index.callsCacheDirty)) {
730
+ try { index.saveCache(); } catch (e) { /* best-effort */ }
1306
731
  }
1307
732
  }
1308
-
1309
- // Determine level
1310
- let level = 'high';
1311
- if (score < 50) level = 'low';
1312
- else if (score < 80) level = 'medium';
1313
-
1314
- return { level, reasons };
1315
733
  }
1316
734
 
1317
- /**
1318
- * Count depth of nested generic brackets
1319
- */
1320
- function countNestedGenerics(str) {
1321
- let maxDepth = 0;
1322
- let depth = 0;
1323
- for (const char of str) {
1324
- if (char === '<') {
1325
- depth++;
1326
- maxDepth = Math.max(maxDepth, depth);
1327
- } else if (char === '>') {
1328
- depth--;
1329
- }
1330
- }
1331
- return maxDepth;
1332
- }
735
+ // extractFunctionFromProject and extractClassFromProject removed —
736
+ // all surfaces now use execute(index, 'fn'/'class', params) from core/execute.js
1333
737
 
1334
738
 
1335
739
  /**
@@ -1368,30 +772,7 @@ function loadExpandableItems(root) {
1368
772
  /**
1369
773
  * Print expanded code for a cached item
1370
774
  */
1371
- function printExpandedItem(item, root) {
1372
- const filePath = item.file || (root && item.relativePath ? path.join(root, item.relativePath) : null);
1373
- if (!filePath) {
1374
- console.error(`Cannot locate file for ${item.name}`);
1375
- return;
1376
- }
1377
-
1378
- try {
1379
- const content = fs.readFileSync(filePath, 'utf-8');
1380
- const lines = content.split('\n');
1381
- const startLine = item.startLine || item.line || 1;
1382
- const endLine = item.endLine || startLine + 20;
1383
-
1384
- console.log(`[${item.num}] ${item.name} (${item.type})`);
1385
- console.log(`${item.relativePath}:${startLine}-${endLine}`);
1386
- console.log('═'.repeat(60));
1387
-
1388
- for (let i = startLine - 1; i < Math.min(endLine, lines.length); i++) {
1389
- console.log(lines[i]);
1390
- }
1391
- } catch (e) {
1392
- console.error(`Error reading ${filePath}: ${e.message}`);
1393
- }
1394
- }
775
+ // printExpandedItem removed — all surfaces now use execute(index, 'expand', ...)
1395
776
 
1396
777
 
1397
778
 
@@ -1440,22 +821,21 @@ function runGlobCommand(pattern, command, arg) {
1440
821
  }
1441
822
  }
1442
823
 
1443
- const tocRaw = { totalFiles: files.length, totalLines, totalFunctions, totalClasses, totalState, byFile };
824
+ // Convert glob toc to shared formatter format
825
+ const toc = {
826
+ totals: { files: files.length, lines: totalLines, functions: totalFunctions, classes: totalClasses, state: totalState },
827
+ files: byFile.map(f => ({
828
+ file: f.file,
829
+ lines: f.lines,
830
+ functions: f.functions.length,
831
+ classes: f.classes.length,
832
+ state: f.stateObjects ? f.stateObjects.length : (f.state ? f.state.length : 0)
833
+ })),
834
+ meta: {}
835
+ };
1444
836
  if (flags.json) {
1445
- console.log(output.formatTocJson(tocRaw));
837
+ console.log(output.formatTocJson(toc));
1446
838
  } else {
1447
- // Convert glob toc to shared formatter format
1448
- const toc = {
1449
- totals: { files: files.length, lines: totalLines, functions: totalFunctions, classes: totalClasses, state: totalState },
1450
- files: byFile.map(f => ({
1451
- file: f.file,
1452
- lines: f.lines,
1453
- functions: f.functions.length,
1454
- classes: f.classes.length,
1455
- state: f.stateObjects ? f.stateObjects.length : (f.state ? f.state.length : 0)
1456
- })),
1457
- meta: {}
1458
- };
1459
839
  console.log(output.formatToc(toc, {
1460
840
  detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
1461
841
  }));
@@ -1511,7 +891,7 @@ function findInGlobFiles(files, name) {
1511
891
  if (flags.json) {
1512
892
  console.log(output.formatSymbolJson(allMatches, name));
1513
893
  } else {
1514
- printSymbols(allMatches, name, { depth: flags.depth, top: flags.top, all: flags.all });
894
+ console.log(output.formatFindDetailed(allMatches, name, { depth: flags.depth, top: flags.top, all: flags.all }));
1515
895
  }
1516
896
  }
1517
897
 
@@ -1573,103 +953,6 @@ function searchGlobFiles(files, term) {
1573
953
  // HELPERS
1574
954
  // ============================================================================
1575
955
 
1576
- function searchFile(filePath, lines, term) {
1577
- const useRegex = flags.regex !== false;
1578
- let regex;
1579
- if (useRegex) {
1580
- try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
1581
- } else {
1582
- regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
1583
- }
1584
- const matches = [];
1585
-
1586
- lines.forEach((line, idx) => {
1587
- if (regex.test(line)) {
1588
- if (flags.codeOnly && isCommentOrString(line)) {
1589
- return;
1590
- }
1591
-
1592
- const match = { line: idx + 1, content: line };
1593
-
1594
- if (flags.context > 0) {
1595
- const before = [];
1596
- const after = [];
1597
- for (let i = 1; i <= flags.context; i++) {
1598
- if (idx - i >= 0) before.unshift(lines[idx - i]);
1599
- if (idx + i < lines.length) after.push(lines[idx + i]);
1600
- }
1601
- match.before = before;
1602
- match.after = after;
1603
- }
1604
-
1605
- matches.push(match);
1606
- }
1607
- });
1608
-
1609
- if (flags.json) {
1610
- console.log(output.formatSearchJson([{ file: filePath, matches }], term));
1611
- } else {
1612
- console.log(`Found ${matches.length} matches for "${term}" in ${filePath}:`);
1613
- for (const m of matches) {
1614
- console.log(` ${m.line}: ${m.content.trim()}`);
1615
- if (m.before && m.before.length > 0) {
1616
- for (const line of m.before) {
1617
- console.log(` ... ${line.trim()}`);
1618
- }
1619
- }
1620
- if (m.after && m.after.length > 0) {
1621
- for (const line of m.after) {
1622
- console.log(` ... ${line.trim()}`);
1623
- }
1624
- }
1625
- }
1626
- }
1627
- }
1628
-
1629
- function printLines(lines, range) {
1630
- const parts = range.split('-');
1631
- const start = parseInt(parts[0], 10);
1632
- const end = parts.length > 1 ? parseInt(parts[1], 10) : start;
1633
-
1634
- // Validate input
1635
- if (isNaN(start) || isNaN(end)) {
1636
- console.error(`Invalid line range: "${range}". Expected format: <start>-<end> or <line>`);
1637
- process.exit(1);
1638
- }
1639
-
1640
- if (start < 1) {
1641
- console.error(`Invalid start line: ${start}. Line numbers must be >= 1`);
1642
- process.exit(1);
1643
- }
1644
-
1645
- // Handle reversed range by swapping
1646
- const startLine = Math.min(start, end);
1647
- const endLine = Math.max(start, end);
1648
-
1649
- // Check for out-of-bounds
1650
- if (startLine > lines.length) {
1651
- console.error(`Line ${startLine} is out of bounds. File has ${lines.length} lines.`);
1652
- process.exit(1);
1653
- }
1654
-
1655
- // Print lines (clamping end to file length)
1656
- const actualEnd = Math.min(endLine, lines.length);
1657
- for (let i = startLine - 1; i < actualEnd; i++) {
1658
- console.log(`${output.lineNum(i + 1)} │ ${lines[i]}`);
1659
- }
1660
- }
1661
-
1662
- function suggestSimilar(query, names) {
1663
- const lower = query.toLowerCase();
1664
- const similar = names.filter(n => n.toLowerCase().includes(lower));
1665
- if (similar.length > 0) {
1666
- console.log('\nDid you mean:');
1667
- for (const s of similar.slice(0, 5)) {
1668
- console.log(` - ${s}`);
1669
- }
1670
- }
1671
- }
1672
-
1673
956
  function escapeRegExp(text) {
1674
957
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1675
958
  }
@@ -1874,10 +1157,26 @@ Flags can be added per-command: context myFunc --include-methods
1874
1157
  // Parse command, flags, and arg from interactive input
1875
1158
  const tokens = input.split(/\s+/);
1876
1159
  const command = tokens[0];
1877
- const flagTokens = tokens.filter(t => t.startsWith('--'));
1878
- const argTokens = tokens.slice(1).filter(t => !t.startsWith('--'));
1160
+ // Flags that take a space-separated value (--flag value)
1161
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack']);
1162
+ const flagTokens = [];
1163
+ const argTokens = [];
1164
+ const skipNext = new Set();
1165
+ for (let i = 1; i < tokens.length; i++) {
1166
+ if (skipNext.has(i)) { continue; }
1167
+ if (tokens[i].startsWith('--')) {
1168
+ flagTokens.push(tokens[i]);
1169
+ // If it's a value-flag without = and next token exists and isn't a flag, consume it too
1170
+ if (valueFlagNames.has(tokens[i]) && !tokens[i].includes('=') && i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
1171
+ flagTokens.push(tokens[i + 1]);
1172
+ skipNext.add(i + 1);
1173
+ }
1174
+ } else {
1175
+ argTokens.push(tokens[i]);
1176
+ }
1177
+ }
1879
1178
  const arg = argTokens.join(' ');
1880
- const iflags = parseInteractiveFlags(flagTokens);
1179
+ const iflags = parseFlags(flagTokens);
1881
1180
 
1882
1181
  try {
1883
1182
  const iCanonical = resolveCommand(command, 'cli') || command;
@@ -1894,89 +1193,36 @@ Flags can be added per-command: context myFunc --include-methods
1894
1193
  });
1895
1194
  }
1896
1195
 
1897
- /**
1898
- * Parse flags from interactive command tokens.
1899
- * Returns a flags object similar to the global flags but scoped to this command.
1900
- */
1901
- function parseInteractiveFlags(tokens) {
1902
- return {
1903
- file: tokens.find(a => a.startsWith('--file='))?.split('=')[1] || null,
1904
- exclude: tokens.find(a => a.startsWith('--exclude=') || a.startsWith('--not='))?.split('=')[1]?.split(',') || [],
1905
- in: tokens.find(a => a.startsWith('--in='))?.split('=')[1] || null,
1906
- includeTests: tokens.includes('--include-tests'),
1907
- includeExported: tokens.includes('--include-exported'),
1908
- includeDecorated: tokens.includes('--include-decorated'),
1909
- includeUncertain: tokens.includes('--include-uncertain'),
1910
- includeMethods: tokens.includes('--include-methods=false') ? false : tokens.includes('--include-methods') ? true : undefined,
1911
- detailed: tokens.includes('--detailed'),
1912
- topLevel: tokens.includes('--top-level'),
1913
- all: tokens.includes('--all'),
1914
- exact: tokens.includes('--exact'),
1915
- callsOnly: tokens.includes('--calls-only'),
1916
- codeOnly: tokens.includes('--code-only'),
1917
- caseSensitive: tokens.includes('--case-sensitive'),
1918
- withTypes: tokens.includes('--with-types'),
1919
- expand: tokens.includes('--expand'),
1920
- depth: tokens.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
1921
- top: parseInt(tokens.find(a => a.startsWith('--top='))?.split('=')[1] || '0'),
1922
- context: parseInt(tokens.find(a => a.startsWith('--context='))?.split('=')[1] || '0'),
1923
- direction: tokens.find(a => a.startsWith('--direction='))?.split('=')[1] || null,
1924
- addParam: tokens.find(a => a.startsWith('--add-param='))?.split('=')[1] || null,
1925
- removeParam: tokens.find(a => a.startsWith('--remove-param='))?.split('=')[1] || null,
1926
- renameTo: tokens.find(a => a.startsWith('--rename-to='))?.split('=')[1] || null,
1927
- defaultValue: tokens.find(a => a.startsWith('--default='))?.split('=')[1] || null,
1928
- base: tokens.find(a => a.startsWith('--base='))?.split('=')[1] || null,
1929
- staged: tokens.includes('--staged'),
1930
- maxLines: parseInt(tokens.find(a => a.startsWith('--max-lines='))?.split('=')[1] || '0') || null,
1931
- regex: tokens.includes('--no-regex') ? false : undefined,
1932
- functions: tokens.includes('--functions'),
1933
- };
1934
- }
1196
+ // parseInteractiveFlags removed — both global and interactive mode now use parseFlags()
1935
1197
 
1936
1198
  function executeInteractiveCommand(index, command, arg, iflags = {}, cache = null) {
1937
1199
  switch (command) {
1938
1200
 
1939
- // ── Special commands (complex I/O, stay in adapter) ──────────────
1201
+ // ── Extraction commands (via execute) ────────────────────────────
1940
1202
 
1941
1203
  case 'fn': {
1942
- if (!arg) {
1943
- console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]');
1944
- return;
1945
- }
1946
- // Support comma-separated names for bulk extraction
1947
- if (arg.includes(',')) {
1948
- const fnNames = arg.split(',').map(n => n.trim()).filter(Boolean);
1949
- for (let i = 0; i < fnNames.length; i++) {
1950
- if (i > 0) console.log('\n' + '═'.repeat(60) + '\n');
1951
- extractFunctionFromProject(index, fnNames[i], iflags);
1952
- }
1953
- } else {
1954
- extractFunctionFromProject(index, arg, iflags);
1955
- }
1204
+ if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]'); return; }
1205
+ const { ok, result, error } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all });
1206
+ if (!ok) { console.log(error); return; }
1207
+ if (result.notes.length) result.notes.forEach(n => console.log('Note: ' + n));
1208
+ console.log(output.formatFnResult(result));
1956
1209
  break;
1957
1210
  }
1958
1211
 
1959
1212
  case 'class': {
1960
- if (!arg) {
1961
- console.log('Usage: class <name> [--file=<pattern>]');
1962
- return;
1963
- }
1964
- extractClassFromProject(index, arg, iflags);
1213
+ if (!arg) { console.log('Usage: class <name> [--file=<pattern>]'); return; }
1214
+ const { ok, result, error } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines });
1215
+ if (!ok) { console.log(error); return; }
1216
+ if (result.notes.length) result.notes.forEach(n => console.log('Note: ' + n));
1217
+ console.log(output.formatClassResult(result));
1965
1218
  break;
1966
1219
  }
1967
1220
 
1968
1221
  case 'lines': {
1969
- if (!arg || !iflags.file) {
1970
- console.log('Usage: lines <range> --file=<file>');
1971
- return;
1972
- }
1973
- const filePath = index.findFile(iflags.file);
1974
- if (!filePath) {
1975
- console.log(`File not found: ${iflags.file}`);
1976
- return;
1977
- }
1978
- const fileContent = fs.readFileSync(filePath, 'utf-8');
1979
- printLines(fileContent.split('\n'), arg);
1222
+ if (!arg) { console.log('Usage: lines <range> --file=<file>'); return; }
1223
+ const { ok, result, error } = execute(index, 'lines', { file: iflags.file, range: arg });
1224
+ if (!ok) { console.log(error); return; }
1225
+ console.log(output.formatLines(result));
1980
1226
  break;
1981
1227
  }
1982
1228
 
@@ -1990,46 +1236,30 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1990
1236
  console.log(`Invalid item number: "${arg}"`);
1991
1237
  return;
1992
1238
  }
1239
+ let match, itemCount, symbolName;
1993
1240
  if (cache) {
1994
- const { match, itemCount } = cache.lookup(index.root, expandNum);
1995
- if (!match && itemCount === 0) {
1996
- console.log('No expandable items. Run context first.');
1997
- return;
1998
- }
1999
- if (!match) {
2000
- console.log(`Item ${expandNum} not found. Available: 1-${itemCount}`);
2001
- return;
2002
- }
2003
- const rendered = renderExpandItem(match, index.root);
2004
- if (!rendered.ok) { console.log(rendered.error); return; }
2005
- console.log(rendered.text);
1241
+ const lookup = cache.lookup(index.root, expandNum);
1242
+ match = lookup.match;
1243
+ itemCount = lookup.itemCount;
1244
+ symbolName = lookup.symbolName;
2006
1245
  } else {
2007
- // Fallback to file-based cache (CLI one-shot)
2008
1246
  const cached = loadExpandableItems(index.root);
2009
- if (!cached || !cached.items || cached.items.length === 0) {
2010
- console.log('No expandable items. Run context first.');
2011
- return;
2012
- }
2013
- const expandMatch = cached.items.find(i => i.num === expandNum);
2014
- if (!expandMatch) {
2015
- console.log(`Item ${expandNum} not found. Available: 1-${cached.items.length}`);
2016
- return;
2017
- }
2018
- printExpandedItem(expandMatch, cached.root || index.root);
1247
+ const items = cached?.items || [];
1248
+ match = items.find(i => i.num === expandNum);
1249
+ itemCount = items.length;
2019
1250
  }
1251
+ const { ok, result, error } = execute(index, 'expand', {
1252
+ match, itemNum: expandNum, itemCount, symbolName
1253
+ });
1254
+ if (!ok) { console.log(error); return; }
1255
+ console.log(result.text);
2020
1256
  break;
2021
1257
  }
2022
1258
 
2023
- // ── find: uses printSymbols (interactive-only formatter) ─────────
2024
-
2025
1259
  case 'find': {
2026
1260
  const { ok, result, error } = execute(index, 'find', { name: arg, ...iflags });
2027
1261
  if (!ok) { console.log(error); return; }
2028
- if (result.length === 0) {
2029
- console.log(`No symbols found for "${arg}"`);
2030
- } else {
2031
- printSymbols(result, arg, { top: iflags.top });
2032
- }
1262
+ console.log(output.formatFindDetailed(result, arg, { depth: iflags.depth, top: iflags.top, all: iflags.all }));
2033
1263
  break;
2034
1264
  }
2035
1265
 
@@ -2080,7 +1310,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
2080
1310
  case 'about': {
2081
1311
  const { ok, result, error } = execute(index, 'about', { name: arg, ...iflags });
2082
1312
  if (!ok) { console.log(error); return; }
2083
- console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all }));
1313
+ console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth }));
2084
1314
  break;
2085
1315
  }
2086
1316
 
@@ -2230,7 +1460,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
2230
1460
  // ============================================================================
2231
1461
 
2232
1462
  if (flags.interactive) {
2233
- const target = positionalArgs[0] || '.';
1463
+ let target = positionalArgs[0] || '.';
1464
+ if (COMMANDS.has(target)) target = '.';
2234
1465
  runInteractive(target);
2235
1466
  } else {
2236
1467
  main();