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 +285 -1054
- package/core/cache.js +193 -0
- package/core/callers.js +817 -0
- package/core/deadcode.js +320 -0
- package/core/discovery.js +1 -1
- package/core/execute.js +207 -10
- package/core/expand-cache.js +16 -5
- package/core/imports.js +21 -15
- package/core/output.js +370 -35
- package/core/project.js +365 -2272
- package/core/shared.js +11 -1
- package/core/stacktrace.js +313 -0
- package/core/verify.js +533 -0
- package/languages/go.js +57 -21
- package/languages/html.js +14 -3
- package/languages/java.js +4 -2
- package/languages/javascript.js +36 -9
- package/languages/rust.js +49 -17
- package/mcp/server.js +39 -172
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
269
|
+
const canonical = resolveCommand(command, 'cli') || command;
|
|
254
270
|
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
420
|
+
if (!ok) fail(error);
|
|
719
421
|
printOutput(result,
|
|
720
422
|
r => output.formatSymbolJson(r, arg),
|
|
721
|
-
r =>
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
793
|
-
process.exit(1);
|
|
494
|
+
fail(`Invalid item number: "${arg}"`);
|
|
794
495
|
}
|
|
795
496
|
const cached = loadExpandableItems(index.root);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (!
|
|
802
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
// ──
|
|
568
|
+
// ── Extraction commands (via execute) ────────────────────────────
|
|
871
569
|
|
|
872
570
|
case 'fn': {
|
|
873
571
|
requireArg(arg, 'Usage: ucn . fn <name>');
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
718
|
+
throw new CommandError();
|
|
1030
719
|
}
|
|
1031
720
|
} catch (e) {
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1878
|
-
const
|
|
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 =
|
|
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
|
-
// ──
|
|
1201
|
+
// ── Extraction commands (via execute) ────────────────────────────
|
|
1940
1202
|
|
|
1941
1203
|
case 'fn': {
|
|
1942
|
-
if (!arg) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
+
let target = positionalArgs[0] || '.';
|
|
1464
|
+
if (COMMANDS.has(target)) target = '.';
|
|
2234
1465
|
runInteractive(target);
|
|
2235
1466
|
} else {
|
|
2236
1467
|
main();
|