ucn 3.8.17 → 3.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +2 -2
- package/cli/index.js +286 -205
- package/core/analysis.js +6 -2
- package/core/execute.js +17 -0
- package/core/output/analysis.js +1 -1
- package/core/project.js +24 -17
- package/core/registry.js +37 -4
- package/core/search.js +487 -74
- package/core/tracing.js +251 -39
- package/languages/javascript.js +19 -2
- package/languages/rust.js +40 -0
- package/mcp/server.js +75 -44
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { detectLanguage } = require('../core/parser');
|
|
14
14
|
const { ProjectIndex } = require('../core/project');
|
|
15
15
|
const { expandGlob, findProjectRoot } = require('../core/discovery');
|
|
16
16
|
const output = require('../core/output');
|
|
17
|
-
const { escapeRegExp } = require('../core/shared');
|
|
18
17
|
const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
|
|
19
18
|
const { execute } = require('../core/execute');
|
|
20
19
|
const { ExpandCache } = require('../core/expand-cache');
|
|
@@ -75,19 +74,19 @@ function parseFlags(tokens) {
|
|
|
75
74
|
exclude: parseExclude(),
|
|
76
75
|
in: getValueFlag('--in'),
|
|
77
76
|
includeTests: tokens.includes('--include-tests') ? true : undefined,
|
|
78
|
-
includeExported: tokens.includes('--include-exported'),
|
|
79
|
-
includeDecorated: tokens.includes('--include-decorated'),
|
|
80
|
-
includeUncertain: tokens.includes('--include-uncertain'),
|
|
77
|
+
includeExported: tokens.includes('--include-exported') || undefined,
|
|
78
|
+
includeDecorated: tokens.includes('--include-decorated') || undefined,
|
|
79
|
+
includeUncertain: tokens.includes('--include-uncertain') || undefined,
|
|
81
80
|
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'),
|
|
81
|
+
detailed: tokens.includes('--detailed') || undefined,
|
|
82
|
+
topLevel: tokens.includes('--top-level') || undefined,
|
|
83
|
+
all: tokens.includes('--all') || undefined,
|
|
84
|
+
exact: tokens.includes('--exact') || undefined,
|
|
85
|
+
callsOnly: tokens.includes('--calls-only') || undefined,
|
|
86
|
+
codeOnly: tokens.includes('--code-only') || undefined,
|
|
87
|
+
caseSensitive: tokens.includes('--case-sensitive') || undefined,
|
|
88
|
+
withTypes: tokens.includes('--with-types') || undefined,
|
|
89
|
+
expand: tokens.includes('--expand') || undefined,
|
|
91
90
|
depth: getValueFlag('--depth'),
|
|
92
91
|
top: parseInt(getValueFlag('--top') || '0'),
|
|
93
92
|
context: parseInt(getValueFlag('--context') || '0'),
|
|
@@ -97,10 +96,10 @@ function parseFlags(tokens) {
|
|
|
97
96
|
renameTo: getValueFlag('--rename-to'),
|
|
98
97
|
defaultValue: getValueFlag('--default'),
|
|
99
98
|
base: getValueFlag('--base'),
|
|
100
|
-
staged: tokens.includes('--staged'),
|
|
99
|
+
staged: tokens.includes('--staged') || undefined,
|
|
101
100
|
maxLines: getValueFlag('--max-lines') || null,
|
|
102
101
|
regex: tokens.includes('--no-regex') ? false : undefined,
|
|
103
|
-
functions: tokens.includes('--functions'),
|
|
102
|
+
functions: tokens.includes('--functions') || undefined,
|
|
104
103
|
className: getValueFlag('--class-name'),
|
|
105
104
|
limit: parseInt(getValueFlag('--limit') || '0') || undefined,
|
|
106
105
|
maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
|
|
@@ -110,9 +109,9 @@ function parseFlags(tokens) {
|
|
|
110
109
|
receiver: getValueFlag('--receiver'),
|
|
111
110
|
returns: getValueFlag('--returns'),
|
|
112
111
|
decorator: getValueFlag('--decorator'),
|
|
113
|
-
exported: tokens.includes('--exported'),
|
|
114
|
-
unused: tokens.includes('--unused'),
|
|
115
|
-
showConfidence:
|
|
112
|
+
exported: tokens.includes('--exported') || undefined,
|
|
113
|
+
unused: tokens.includes('--unused') || undefined,
|
|
114
|
+
showConfidence: tokens.includes('--no-confidence') ? false : undefined,
|
|
116
115
|
minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
|
|
117
116
|
framework: getValueFlag('--framework'),
|
|
118
117
|
stack: getValueFlag('--stack'),
|
|
@@ -228,6 +227,33 @@ function printOutput(result, jsonFn, textFn) {
|
|
|
228
227
|
}
|
|
229
228
|
}
|
|
230
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Print inline 3-line code previews for each callee (--expand support).
|
|
232
|
+
* Used by context in project, interactive, and glob modes.
|
|
233
|
+
*/
|
|
234
|
+
function printInlineExpand(ctx, root) {
|
|
235
|
+
if (!root || !ctx || !ctx.callees) return;
|
|
236
|
+
for (const c of ctx.callees) {
|
|
237
|
+
if (c.relativePath && c.startLine) {
|
|
238
|
+
try {
|
|
239
|
+
const filePath = path.join(root, c.relativePath);
|
|
240
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
241
|
+
const codeLines = content.split('\n');
|
|
242
|
+
const endLine = c.endLine || c.startLine + 5;
|
|
243
|
+
const previewLines = Math.min(3, endLine - c.startLine + 1);
|
|
244
|
+
for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
|
|
245
|
+
console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
|
|
246
|
+
}
|
|
247
|
+
if (endLine - c.startLine + 1 > 3) {
|
|
248
|
+
console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Skip on error
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
231
257
|
// ============================================================================
|
|
232
258
|
// MAIN
|
|
233
259
|
// ============================================================================
|
|
@@ -305,6 +331,12 @@ function runFileCommand(filePath, command, arg) {
|
|
|
305
331
|
if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
|
|
306
332
|
effectiveArg = filePath;
|
|
307
333
|
}
|
|
334
|
+
// Scope to the target file unless an explicit --file was provided
|
|
335
|
+
if (!flags.file) {
|
|
336
|
+
const relPath = path.relative(projectRoot, path.resolve(filePath));
|
|
337
|
+
flags.file = relPath;
|
|
338
|
+
flags._fileFromFileMode = true; // suppress inapplicable-flag warning
|
|
339
|
+
}
|
|
308
340
|
runProjectCommand(projectRoot, command, effectiveArg);
|
|
309
341
|
return;
|
|
310
342
|
}
|
|
@@ -375,9 +407,11 @@ function runFileCommand(filePath, command, arg) {
|
|
|
375
407
|
case 'typedef':
|
|
376
408
|
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
377
409
|
break;
|
|
378
|
-
case 'api':
|
|
379
|
-
|
|
410
|
+
case 'api': {
|
|
411
|
+
const apiFile = relativePath;
|
|
412
|
+
printOutput(result, r => output.formatApiJson(r, apiFile), r => output.formatApi(r, apiFile));
|
|
380
413
|
break;
|
|
414
|
+
}
|
|
381
415
|
}
|
|
382
416
|
}
|
|
383
417
|
|
|
@@ -444,12 +478,14 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
444
478
|
if (applicableFlags) {
|
|
445
479
|
// Map from camelCase flag name to CLI flag string
|
|
446
480
|
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
447
|
-
// Flags that are global (not command-specific)
|
|
448
|
-
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '
|
|
481
|
+
// Flags that are global (not command-specific) — skip warning for these
|
|
482
|
+
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
|
|
449
483
|
for (const [key, value] of Object.entries(flags)) {
|
|
450
484
|
if (globalFlags.has(key)) continue;
|
|
451
|
-
// Skip
|
|
452
|
-
if (
|
|
485
|
+
// Skip unset values (undefined, null, 0, empty array) — but NOT false (explicit negation)
|
|
486
|
+
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
487
|
+
// Skip --file when it was injected by file-mode routing, not user input
|
|
488
|
+
if (key === 'file' && flags._fileFromFileMode) continue;
|
|
453
489
|
if (!applicableFlags.includes(key)) {
|
|
454
490
|
console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
455
491
|
}
|
|
@@ -510,33 +546,15 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
510
546
|
} else {
|
|
511
547
|
const { text, expandable } = output.formatContext(ctx, {
|
|
512
548
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
513
|
-
expandHint: 'Use "
|
|
549
|
+
expandHint: 'Use "expand <N>" or --expand to see code for items',
|
|
514
550
|
uncertainHint: 'use --include-uncertain to include all',
|
|
515
|
-
showConfidence: flags.showConfidence,
|
|
551
|
+
showConfidence: flags.showConfidence !== false,
|
|
516
552
|
});
|
|
517
553
|
console.log(text);
|
|
518
554
|
|
|
519
555
|
// Inline expansion of callees when --expand flag is set
|
|
520
|
-
if (flags.expand
|
|
521
|
-
|
|
522
|
-
if (c.relativePath && c.startLine) {
|
|
523
|
-
try {
|
|
524
|
-
const filePath = path.join(index.root, c.relativePath);
|
|
525
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
526
|
-
const codeLines = content.split('\n');
|
|
527
|
-
const endLine = c.endLine || c.startLine + 5;
|
|
528
|
-
const previewLines = Math.min(3, endLine - c.startLine + 1);
|
|
529
|
-
for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
|
|
530
|
-
console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
|
|
531
|
-
}
|
|
532
|
-
if (endLine - c.startLine + 1 > 3) {
|
|
533
|
-
console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
|
|
534
|
-
}
|
|
535
|
-
} catch (e) {
|
|
536
|
-
// Skip on error
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
556
|
+
if (flags.expand) {
|
|
557
|
+
printInlineExpand(ctx, index.root);
|
|
540
558
|
}
|
|
541
559
|
|
|
542
560
|
// Save expandable items to cache for 'expand' command
|
|
@@ -578,7 +596,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
578
596
|
if (!ok) fail(error);
|
|
579
597
|
printOutput(result,
|
|
580
598
|
output.formatAboutJson,
|
|
581
|
-
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence })
|
|
599
|
+
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false })
|
|
582
600
|
);
|
|
583
601
|
if (note) console.error(note);
|
|
584
602
|
break;
|
|
@@ -740,7 +758,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
740
758
|
}
|
|
741
759
|
|
|
742
760
|
case 'tests': {
|
|
743
|
-
const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className });
|
|
761
|
+
const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className, file: flags.file, exclude: flags.exclude });
|
|
744
762
|
if (!ok) fail(error);
|
|
745
763
|
printOutput(result,
|
|
746
764
|
r => output.formatTestsJson(r, arg),
|
|
@@ -904,164 +922,217 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
904
922
|
process.exit(1);
|
|
905
923
|
}
|
|
906
924
|
|
|
907
|
-
|
|
908
|
-
case 'toc': {
|
|
909
|
-
let totalFunctions = 0;
|
|
910
|
-
let totalClasses = 0;
|
|
911
|
-
let totalState = 0;
|
|
912
|
-
let totalLines = 0;
|
|
913
|
-
const byFile = [];
|
|
914
|
-
|
|
915
|
-
for (const file of files) {
|
|
916
|
-
try {
|
|
917
|
-
const result = parseFile(file);
|
|
918
|
-
let functions = result.functions;
|
|
919
|
-
if (flags.topLevel) {
|
|
920
|
-
functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
|
|
921
|
-
}
|
|
922
|
-
totalFunctions += functions.length;
|
|
923
|
-
totalClasses += result.classes.length;
|
|
924
|
-
totalState += result.stateObjects.length;
|
|
925
|
-
totalLines += result.totalLines;
|
|
926
|
-
byFile.push({
|
|
927
|
-
file,
|
|
928
|
-
language: result.language,
|
|
929
|
-
lines: result.totalLines,
|
|
930
|
-
functions,
|
|
931
|
-
classes: result.classes,
|
|
932
|
-
state: result.stateObjects
|
|
933
|
-
});
|
|
934
|
-
} catch (e) {
|
|
935
|
-
// Skip unparseable files
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Convert glob toc to shared formatter format
|
|
940
|
-
const toc = {
|
|
941
|
-
totals: { files: files.length, lines: totalLines, functions: totalFunctions, classes: totalClasses, state: totalState },
|
|
942
|
-
files: byFile.map(f => ({
|
|
943
|
-
file: f.file,
|
|
944
|
-
lines: f.lines,
|
|
945
|
-
functions: f.functions.length,
|
|
946
|
-
classes: f.classes.length,
|
|
947
|
-
state: f.stateObjects ? f.stateObjects.length : (f.state ? f.state.length : 0)
|
|
948
|
-
})),
|
|
949
|
-
meta: {}
|
|
950
|
-
};
|
|
951
|
-
if (flags.json) {
|
|
952
|
-
console.log(output.formatTocJson(toc));
|
|
953
|
-
} else {
|
|
954
|
-
console.log(output.formatToc(toc, {
|
|
955
|
-
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
|
|
956
|
-
}));
|
|
957
|
-
}
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
case 'find':
|
|
962
|
-
if (!arg) {
|
|
963
|
-
console.error('Usage: ucn "pattern" find <name>');
|
|
964
|
-
process.exit(1);
|
|
965
|
-
}
|
|
966
|
-
findInGlobFiles(files, arg);
|
|
967
|
-
break;
|
|
925
|
+
const canonical = resolveCommand(command, 'cli') || command;
|
|
968
926
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
927
|
+
// Build a temporary index over the matched files and route through execute().
|
|
928
|
+
// This gives glob mode the same semantics as project mode: test exclusions,
|
|
929
|
+
// limit, all flags — no bespoke logic, no parity drift.
|
|
930
|
+
const rootDir = findProjectRoot(path.dirname(files[0]));
|
|
931
|
+
const index = new ProjectIndex(rootDir);
|
|
932
|
+
index.build(files, { quiet: true });
|
|
933
|
+
|
|
934
|
+
// Supported commands — anything that works with an index.
|
|
935
|
+
// All execute() commands are supported; only expand (requires cached state)
|
|
936
|
+
// and interactive-only commands are excluded.
|
|
937
|
+
const unsupportedGlobCommands = new Set(['expand']);
|
|
938
|
+
if (unsupportedGlobCommands.has(canonical)) {
|
|
939
|
+
console.error(`Command "${command}" not supported in glob mode.`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
976
942
|
|
|
977
|
-
|
|
978
|
-
|
|
943
|
+
// Build params — same as project mode
|
|
944
|
+
const params = {};
|
|
945
|
+
const needsName = new Set(['find', 'usages', 'fn', 'class', 'typedef', 'about', 'context',
|
|
946
|
+
'smart', 'impact', 'trace', 'blast', 'reverseTrace', 'tests', 'affectedTests',
|
|
947
|
+
'example', 'verify', 'plan', 'related']);
|
|
948
|
+
if (needsName.has(canonical)) {
|
|
949
|
+
if (!arg) {
|
|
950
|
+
console.error(`Usage: ucn "pattern" ${command} <name>`);
|
|
979
951
|
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
params.name = arg;
|
|
980
954
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
for (const fn of result.functions) {
|
|
992
|
-
if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
|
|
993
|
-
allMatches.push({ ...fn, type: 'function', relativePath: file });
|
|
994
|
-
}
|
|
995
|
-
}
|
|
955
|
+
if (canonical === 'search' || canonical === 'structuralSearch') {
|
|
956
|
+
if (!arg && !flags.type) {
|
|
957
|
+
console.error('Usage: ucn "pattern" search <term>');
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
params.term = arg;
|
|
961
|
+
}
|
|
962
|
+
// Merge flags first, then set positional overrides so they aren't wiped
|
|
963
|
+
Object.assign(params, flags);
|
|
996
964
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
965
|
+
// Warn about inapplicable flags (same check as project/interactive mode)
|
|
966
|
+
const applicableFlags = FLAG_APPLICABILITY[canonical];
|
|
967
|
+
if (applicableFlags) {
|
|
968
|
+
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
969
|
+
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
|
|
970
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
971
|
+
if (globalFlags.has(key)) continue;
|
|
972
|
+
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
973
|
+
if (!applicableFlags.includes(key)) {
|
|
974
|
+
console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
1001
975
|
}
|
|
1002
|
-
} catch (e) {
|
|
1003
|
-
// Skip
|
|
1004
976
|
}
|
|
1005
977
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
console.log(output.formatSymbolJson(allMatches, name));
|
|
1009
|
-
} else {
|
|
1010
|
-
console.log(output.formatFindDetailed(allMatches, name, { depth: flags.depth, top: flags.top, all: flags.all }));
|
|
978
|
+
if (canonical === 'stacktrace' && arg) {
|
|
979
|
+
params.stack = arg;
|
|
1011
980
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
let regex;
|
|
1018
|
-
if (useRegex) {
|
|
1019
|
-
try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
|
|
1020
|
-
} else {
|
|
1021
|
-
regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
|
|
981
|
+
if (canonical === 'lines' && arg) {
|
|
982
|
+
params.range = arg;
|
|
983
|
+
}
|
|
984
|
+
if (['imports', 'exporters', 'fileExports', 'graph', 'api'].includes(canonical)) {
|
|
985
|
+
if (arg) params.file = arg;
|
|
1022
986
|
}
|
|
1023
987
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const lines = content.split('\n');
|
|
1028
|
-
const matches = [];
|
|
1029
|
-
|
|
1030
|
-
lines.forEach((line, idx) => {
|
|
1031
|
-
if (regex.test(line)) {
|
|
1032
|
-
if (flags.codeOnly && isCommentOrString(line)) {
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
const match = { line: idx + 1, content: line };
|
|
1037
|
-
|
|
1038
|
-
if (flags.context > 0) {
|
|
1039
|
-
const before = [];
|
|
1040
|
-
const after = [];
|
|
1041
|
-
for (let i = 1; i <= flags.context; i++) {
|
|
1042
|
-
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
1043
|
-
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
1044
|
-
}
|
|
1045
|
-
match.before = before;
|
|
1046
|
-
match.after = after;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
matches.push(match);
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
988
|
+
const { ok, result, error, note, structural } = execute(index, canonical, params);
|
|
989
|
+
if (!ok) fail(error);
|
|
990
|
+
if (note) console.error(note);
|
|
1052
991
|
|
|
1053
|
-
|
|
1054
|
-
|
|
992
|
+
// Format output — same formatters as project mode
|
|
993
|
+
switch (canonical) {
|
|
994
|
+
case 'toc':
|
|
995
|
+
printOutput(result, output.formatTocJson, r => output.formatToc(r, {
|
|
996
|
+
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
|
|
997
|
+
}));
|
|
998
|
+
break;
|
|
999
|
+
case 'find':
|
|
1000
|
+
printOutput(result,
|
|
1001
|
+
r => output.formatSymbolJson(r, arg),
|
|
1002
|
+
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
|
|
1003
|
+
);
|
|
1004
|
+
break;
|
|
1005
|
+
case 'search':
|
|
1006
|
+
if (structural) {
|
|
1007
|
+
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
1008
|
+
} else {
|
|
1009
|
+
printOutput(result,
|
|
1010
|
+
r => output.formatSearchJson(r, arg),
|
|
1011
|
+
r => output.formatSearch(r, arg)
|
|
1012
|
+
);
|
|
1055
1013
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1014
|
+
break;
|
|
1015
|
+
case 'fn':
|
|
1016
|
+
printOutput(result, output.formatFnResultJson, output.formatFnResult);
|
|
1017
|
+
break;
|
|
1018
|
+
case 'class':
|
|
1019
|
+
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
1020
|
+
break;
|
|
1021
|
+
case 'usages':
|
|
1022
|
+
printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
|
|
1023
|
+
break;
|
|
1024
|
+
case 'deadcode':
|
|
1025
|
+
printOutput(result, output.formatDeadcodeJson, r => output.formatDeadcode(r, { top: flags.top }));
|
|
1026
|
+
break;
|
|
1027
|
+
case 'typedef':
|
|
1028
|
+
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
1029
|
+
break;
|
|
1030
|
+
case 'stats':
|
|
1031
|
+
printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
|
|
1032
|
+
break;
|
|
1033
|
+
case 'about':
|
|
1034
|
+
printOutput(result, output.formatAboutJson,
|
|
1035
|
+
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false }));
|
|
1036
|
+
break;
|
|
1037
|
+
case 'context':
|
|
1038
|
+
if (flags.json) {
|
|
1039
|
+
console.log(output.formatContextJson(result));
|
|
1040
|
+
} else {
|
|
1041
|
+
const { text } = output.formatContext(result, {
|
|
1042
|
+
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
1043
|
+
uncertainHint: 'use --include-uncertain to include all',
|
|
1044
|
+
expandHint: 'Use --expand to see inline callee previews',
|
|
1045
|
+
showConfidence: flags.showConfidence !== false,
|
|
1046
|
+
});
|
|
1047
|
+
console.log(text);
|
|
1048
|
+
if (flags.expand) {
|
|
1049
|
+
printInlineExpand(result, index.root);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
break;
|
|
1053
|
+
case 'smart':
|
|
1054
|
+
printOutput(result, output.formatSmartJson, output.formatSmart);
|
|
1055
|
+
break;
|
|
1056
|
+
case 'impact':
|
|
1057
|
+
printOutput(result, output.formatImpactJson, output.formatImpact);
|
|
1058
|
+
break;
|
|
1059
|
+
case 'related':
|
|
1060
|
+
printOutput(result, output.formatRelatedJson,
|
|
1061
|
+
r => output.formatRelated(r, { all: flags.all, top: flags.top }));
|
|
1062
|
+
break;
|
|
1063
|
+
case 'trace':
|
|
1064
|
+
printOutput(result, output.formatTraceJson, output.formatTrace);
|
|
1065
|
+
break;
|
|
1066
|
+
case 'blast':
|
|
1067
|
+
printOutput(result, output.formatBlastJson, output.formatBlast);
|
|
1068
|
+
break;
|
|
1069
|
+
case 'reverseTrace':
|
|
1070
|
+
printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
|
|
1071
|
+
break;
|
|
1072
|
+
case 'tests':
|
|
1073
|
+
printOutput(result, r => output.formatTestsJson(r, arg), r => output.formatTests(r, arg));
|
|
1074
|
+
break;
|
|
1075
|
+
case 'affectedTests':
|
|
1076
|
+
printOutput(result, output.formatAffectedTestsJson,
|
|
1077
|
+
r => output.formatAffectedTests(r, { all: flags.all }));
|
|
1078
|
+
break;
|
|
1079
|
+
case 'example':
|
|
1080
|
+
printOutput(result, r => output.formatExampleJson(r, arg), r => output.formatExample(r, arg));
|
|
1081
|
+
break;
|
|
1082
|
+
case 'verify':
|
|
1083
|
+
printOutput(result, output.formatVerifyJson, output.formatVerify);
|
|
1084
|
+
break;
|
|
1085
|
+
case 'plan':
|
|
1086
|
+
printOutput(result, output.formatPlanJson, output.formatPlan);
|
|
1087
|
+
break;
|
|
1088
|
+
case 'imports': {
|
|
1089
|
+
const filePath = params.file;
|
|
1090
|
+
printOutput(result, r => output.formatImportsJson(r, filePath), r => output.formatImports(r, filePath));
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case 'exporters': {
|
|
1094
|
+
const filePath = params.file;
|
|
1095
|
+
printOutput(result, r => output.formatExportersJson(r, filePath), r => output.formatExporters(r, filePath));
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
case 'fileExports': {
|
|
1099
|
+
const filePath = params.file;
|
|
1100
|
+
printOutput(result, r => output.formatFileExportsJson(r, filePath), r => output.formatFileExports(r, filePath));
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case 'api': {
|
|
1104
|
+
const filePath = params.file;
|
|
1105
|
+
printOutput(result, r => output.formatApiJson(r, filePath), r => output.formatApi(r, filePath));
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case 'graph':
|
|
1109
|
+
printOutput(result, output.formatGraphJson,
|
|
1110
|
+
r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth }));
|
|
1111
|
+
break;
|
|
1112
|
+
case 'circularDeps':
|
|
1113
|
+
printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
|
|
1114
|
+
break;
|
|
1115
|
+
case 'entrypoints':
|
|
1116
|
+
printOutput(result, output.formatEntrypointsJson, output.formatEntrypoints);
|
|
1117
|
+
break;
|
|
1118
|
+
case 'diffImpact':
|
|
1119
|
+
printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
|
|
1120
|
+
break;
|
|
1121
|
+
case 'stacktrace':
|
|
1122
|
+
printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
|
|
1123
|
+
break;
|
|
1124
|
+
case 'lines':
|
|
1125
|
+
printOutput(result, output.formatLinesJson, output.formatLines);
|
|
1126
|
+
break;
|
|
1127
|
+
default: {
|
|
1128
|
+
// Fallback: output JSON for any command without a dedicated formatter
|
|
1129
|
+
if (flags.json) {
|
|
1130
|
+
console.log(JSON.stringify({ meta: {}, data: result }, null, 2));
|
|
1131
|
+
} else {
|
|
1132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1133
|
+
}
|
|
1134
|
+
break;
|
|
1058
1135
|
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (flags.json) {
|
|
1062
|
-
console.log(output.formatSearchJson(results, term));
|
|
1063
|
-
} else {
|
|
1064
|
-
console.log(output.formatSearch(results, term));
|
|
1065
1136
|
}
|
|
1066
1137
|
}
|
|
1067
1138
|
|
|
@@ -1069,13 +1140,6 @@ function searchGlobFiles(files, term) {
|
|
|
1069
1140
|
// HELPERS
|
|
1070
1141
|
// ============================================================================
|
|
1071
1142
|
|
|
1072
|
-
function isCommentOrString(line) {
|
|
1073
|
-
const trimmed = line.trim();
|
|
1074
|
-
return trimmed.startsWith('//') ||
|
|
1075
|
-
trimmed.startsWith('#') ||
|
|
1076
|
-
trimmed.startsWith('*') ||
|
|
1077
|
-
trimmed.startsWith('/*');
|
|
1078
|
-
}
|
|
1079
1143
|
|
|
1080
1144
|
function printUsage() {
|
|
1081
1145
|
console.log(`UCN - Universal Code Navigator
|
|
@@ -1110,7 +1174,7 @@ FIND CODE
|
|
|
1110
1174
|
toc Table of contents (compact; --detailed lists all symbols)
|
|
1111
1175
|
search <term> Text search (regex default, --context=N, --exclude=, --in=)
|
|
1112
1176
|
Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
|
|
1113
|
-
tests <name> Find test files for a function
|
|
1177
|
+
tests <name> Find test files for a function (--file, --class-name, --exclude, --calls-only)
|
|
1114
1178
|
affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
|
|
1115
1179
|
|
|
1116
1180
|
═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1163,7 +1227,7 @@ Common Flags:
|
|
|
1163
1227
|
--code-only Filter out comments/strings (search, usages)
|
|
1164
1228
|
--with-types Include type definitions (about, smart)
|
|
1165
1229
|
--detailed Show all symbols in toc (not just counts)
|
|
1166
|
-
--include-tests Include test files
|
|
1230
|
+
--include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
|
|
1167
1231
|
--class-name=X Scope to specific class (e.g., --class-name=Repository)
|
|
1168
1232
|
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
1169
1233
|
--include-uncertain Include ambiguous/uncertain matches
|
|
@@ -1258,7 +1322,7 @@ Commands:
|
|
|
1258
1322
|
file-exports <file> File's exported symbols
|
|
1259
1323
|
imports <file> What file imports
|
|
1260
1324
|
exporters <file> Who imports file
|
|
1261
|
-
tests <name> Find tests (--calls-only)
|
|
1325
|
+
tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
|
|
1262
1326
|
affected-tests <n> Tests affected by a change (--depth=N)
|
|
1263
1327
|
search <term> Text search (--context=N, --exclude=, --in=)
|
|
1264
1328
|
Structural: --type= --param= --returns= --decorator= --exported --unused
|
|
@@ -1342,7 +1406,7 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1342
1406
|
|
|
1343
1407
|
const INTERACTIVE_DISPATCH = {
|
|
1344
1408
|
// ── Understanding Code ───────────────────────────────────────────
|
|
1345
|
-
about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence }) },
|
|
1409
|
+
about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false }) },
|
|
1346
1410
|
smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
|
|
1347
1411
|
impact: { params: 'name', format: (r) => output.formatImpact(r) },
|
|
1348
1412
|
blast: { params: 'name', format: (r) => output.formatBlast(r) },
|
|
@@ -1396,6 +1460,20 @@ function buildInteractiveParams(descriptor, arg, iflags) {
|
|
|
1396
1460
|
}
|
|
1397
1461
|
|
|
1398
1462
|
function executeInteractiveCommand(index, command, arg, iflags = {}, cache = null) {
|
|
1463
|
+
// Warn about inapplicable flags (same check as project mode)
|
|
1464
|
+
const applicableFlags = FLAG_APPLICABILITY[command];
|
|
1465
|
+
if (applicableFlags) {
|
|
1466
|
+
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
1467
|
+
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
|
|
1468
|
+
for (const [key, value] of Object.entries(iflags)) {
|
|
1469
|
+
if (globalFlags.has(key)) continue;
|
|
1470
|
+
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
1471
|
+
if (!applicableFlags.includes(key)) {
|
|
1472
|
+
console.log(`Warning: ${flagToCli(key)} has no effect on '${command}'.`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1399
1477
|
// ── Commands with unique behavior (not data-driven) ──────────────
|
|
1400
1478
|
switch (command) {
|
|
1401
1479
|
|
|
@@ -1462,9 +1540,12 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1462
1540
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
1463
1541
|
expandHint: 'Use "expand <N>" to see code for item N',
|
|
1464
1542
|
uncertainHint: 'use --include-uncertain to include all',
|
|
1465
|
-
showConfidence: iflags.showConfidence,
|
|
1543
|
+
showConfidence: iflags.showConfidence !== false,
|
|
1466
1544
|
});
|
|
1467
1545
|
console.log(text);
|
|
1546
|
+
if (iflags.expand) {
|
|
1547
|
+
printInlineExpand(result, index.root);
|
|
1548
|
+
}
|
|
1468
1549
|
if (note) console.log(note);
|
|
1469
1550
|
if (cache) {
|
|
1470
1551
|
cache.save(index.root, arg, iflags.file, expandable);
|
package/core/analysis.js
CHANGED
|
@@ -836,8 +836,12 @@ function about(index, name, options = {}) {
|
|
|
836
836
|
}));
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
// Find tests
|
|
840
|
-
const tests = index.tests(symbolName
|
|
839
|
+
// Find tests — scope to the same file/class as the primary definition
|
|
840
|
+
const tests = index.tests(symbolName, {
|
|
841
|
+
file: options.file,
|
|
842
|
+
className: options.className || primary.className,
|
|
843
|
+
exclude: options.exclude,
|
|
844
|
+
});
|
|
841
845
|
const testSummary = {
|
|
842
846
|
fileCount: tests.length,
|
|
843
847
|
totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
|