ucn 3.8.18 → 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 +226 -58
- package/core/analysis.js +6 -2
- package/core/execute.js +17 -0
- package/core/output/analysis.js +1 -1
- package/core/registry.js +37 -4
- package/core/search.js +482 -127
- 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
|
@@ -133,7 +133,7 @@ ucn entrypoints --file=routes/ # Scoped to files
|
|
|
133
133
|
| File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Setting `--depth=N` expands all children. Can be noisy — use depth=1 for large projects. For function-level flow, use `trace` instead |
|
|
134
134
|
| Are there circular dependencies? | `ucn circular-deps` | Detect circular import chains. `--file=<pattern>` filters to cycles involving a file. `--exclude=test` skips test files |
|
|
135
135
|
| What are the framework entry points? | `ucn entrypoints` | Lists all detected routes, DI beans, tasks, etc. Filter: `--type=http`, `--framework=express` |
|
|
136
|
-
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
|
|
136
|
+
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names. Scope with `--file`, `--class-name`, `--exclude`, `--calls-only` |
|
|
137
137
|
| Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
|
|
138
138
|
| Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
|
|
139
139
|
| See a project's public API | `ucn api` or `ucn api --file=<file>` | All exported/public symbols with signatures |
|
|
@@ -163,7 +163,7 @@ ucn [target] <command> [name] [--flags]
|
|
|
163
163
|
| `--in=src/core` | Limit search to a subdirectory |
|
|
164
164
|
| `--depth=N` | Control tree depth for `trace`, `graph`, and detail level for `find` (default 3). Also expands all children — no breadth limit |
|
|
165
165
|
| `--all` | Expand truncated sections in `about`, `trace`, `graph`, `related` |
|
|
166
|
-
| `--include-tests` | Include test files in results (
|
|
166
|
+
| `--include-tests` | Include test files in usage counts (`about`) and results (`find`, `usages`, `deadcode`). Callers always include tests. |
|
|
167
167
|
| `--include-methods` | Include `obj.method()` calls in `context`/`smart` (only direct calls shown by default) |
|
|
168
168
|
| `--base=<ref>` | Git ref for diff-impact (default: HEAD) |
|
|
169
169
|
| `--staged` | Analyze staged changes (diff-impact) |
|
package/cli/index.js
CHANGED
|
@@ -74,19 +74,19 @@ function parseFlags(tokens) {
|
|
|
74
74
|
exclude: parseExclude(),
|
|
75
75
|
in: getValueFlag('--in'),
|
|
76
76
|
includeTests: tokens.includes('--include-tests') ? true : undefined,
|
|
77
|
-
includeExported: tokens.includes('--include-exported'),
|
|
78
|
-
includeDecorated: tokens.includes('--include-decorated'),
|
|
79
|
-
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,
|
|
80
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,
|
|
81
|
-
detailed: tokens.includes('--detailed'),
|
|
82
|
-
topLevel: tokens.includes('--top-level'),
|
|
83
|
-
all: tokens.includes('--all'),
|
|
84
|
-
exact: tokens.includes('--exact'),
|
|
85
|
-
callsOnly: tokens.includes('--calls-only'),
|
|
86
|
-
codeOnly: tokens.includes('--code-only'),
|
|
87
|
-
caseSensitive: tokens.includes('--case-sensitive'),
|
|
88
|
-
withTypes: tokens.includes('--with-types'),
|
|
89
|
-
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,
|
|
90
90
|
depth: getValueFlag('--depth'),
|
|
91
91
|
top: parseInt(getValueFlag('--top') || '0'),
|
|
92
92
|
context: parseInt(getValueFlag('--context') || '0'),
|
|
@@ -96,10 +96,10 @@ function parseFlags(tokens) {
|
|
|
96
96
|
renameTo: getValueFlag('--rename-to'),
|
|
97
97
|
defaultValue: getValueFlag('--default'),
|
|
98
98
|
base: getValueFlag('--base'),
|
|
99
|
-
staged: tokens.includes('--staged'),
|
|
99
|
+
staged: tokens.includes('--staged') || undefined,
|
|
100
100
|
maxLines: getValueFlag('--max-lines') || null,
|
|
101
101
|
regex: tokens.includes('--no-regex') ? false : undefined,
|
|
102
|
-
functions: tokens.includes('--functions'),
|
|
102
|
+
functions: tokens.includes('--functions') || undefined,
|
|
103
103
|
className: getValueFlag('--class-name'),
|
|
104
104
|
limit: parseInt(getValueFlag('--limit') || '0') || undefined,
|
|
105
105
|
maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
|
|
@@ -109,9 +109,9 @@ function parseFlags(tokens) {
|
|
|
109
109
|
receiver: getValueFlag('--receiver'),
|
|
110
110
|
returns: getValueFlag('--returns'),
|
|
111
111
|
decorator: getValueFlag('--decorator'),
|
|
112
|
-
exported: tokens.includes('--exported'),
|
|
113
|
-
unused: tokens.includes('--unused'),
|
|
114
|
-
showConfidence:
|
|
112
|
+
exported: tokens.includes('--exported') || undefined,
|
|
113
|
+
unused: tokens.includes('--unused') || undefined,
|
|
114
|
+
showConfidence: tokens.includes('--no-confidence') ? false : undefined,
|
|
115
115
|
minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
|
|
116
116
|
framework: getValueFlag('--framework'),
|
|
117
117
|
stack: getValueFlag('--stack'),
|
|
@@ -227,6 +227,33 @@ function printOutput(result, jsonFn, textFn) {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
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
|
+
|
|
230
257
|
// ============================================================================
|
|
231
258
|
// MAIN
|
|
232
259
|
// ============================================================================
|
|
@@ -304,6 +331,12 @@ function runFileCommand(filePath, command, arg) {
|
|
|
304
331
|
if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
|
|
305
332
|
effectiveArg = filePath;
|
|
306
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
|
+
}
|
|
307
340
|
runProjectCommand(projectRoot, command, effectiveArg);
|
|
308
341
|
return;
|
|
309
342
|
}
|
|
@@ -374,9 +407,11 @@ function runFileCommand(filePath, command, arg) {
|
|
|
374
407
|
case 'typedef':
|
|
375
408
|
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
376
409
|
break;
|
|
377
|
-
case 'api':
|
|
378
|
-
|
|
410
|
+
case 'api': {
|
|
411
|
+
const apiFile = relativePath;
|
|
412
|
+
printOutput(result, r => output.formatApiJson(r, apiFile), r => output.formatApi(r, apiFile));
|
|
379
413
|
break;
|
|
414
|
+
}
|
|
380
415
|
}
|
|
381
416
|
}
|
|
382
417
|
|
|
@@ -443,12 +478,14 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
443
478
|
if (applicableFlags) {
|
|
444
479
|
// Map from camelCase flag name to CLI flag string
|
|
445
480
|
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
446
|
-
// Flags that are global (not command-specific)
|
|
447
|
-
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']);
|
|
448
483
|
for (const [key, value] of Object.entries(flags)) {
|
|
449
484
|
if (globalFlags.has(key)) continue;
|
|
450
|
-
// Skip
|
|
451
|
-
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;
|
|
452
489
|
if (!applicableFlags.includes(key)) {
|
|
453
490
|
console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
454
491
|
}
|
|
@@ -509,33 +546,15 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
509
546
|
} else {
|
|
510
547
|
const { text, expandable } = output.formatContext(ctx, {
|
|
511
548
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
512
|
-
expandHint: 'Use "
|
|
549
|
+
expandHint: 'Use "expand <N>" or --expand to see code for items',
|
|
513
550
|
uncertainHint: 'use --include-uncertain to include all',
|
|
514
|
-
showConfidence: flags.showConfidence,
|
|
551
|
+
showConfidence: flags.showConfidence !== false,
|
|
515
552
|
});
|
|
516
553
|
console.log(text);
|
|
517
554
|
|
|
518
555
|
// Inline expansion of callees when --expand flag is set
|
|
519
|
-
if (flags.expand
|
|
520
|
-
|
|
521
|
-
if (c.relativePath && c.startLine) {
|
|
522
|
-
try {
|
|
523
|
-
const filePath = path.join(index.root, c.relativePath);
|
|
524
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
525
|
-
const codeLines = content.split('\n');
|
|
526
|
-
const endLine = c.endLine || c.startLine + 5;
|
|
527
|
-
const previewLines = Math.min(3, endLine - c.startLine + 1);
|
|
528
|
-
for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
|
|
529
|
-
console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
|
|
530
|
-
}
|
|
531
|
-
if (endLine - c.startLine + 1 > 3) {
|
|
532
|
-
console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
|
|
533
|
-
}
|
|
534
|
-
} catch (e) {
|
|
535
|
-
// Skip on error
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
556
|
+
if (flags.expand) {
|
|
557
|
+
printInlineExpand(ctx, index.root);
|
|
539
558
|
}
|
|
540
559
|
|
|
541
560
|
// Save expandable items to cache for 'expand' command
|
|
@@ -577,7 +596,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
577
596
|
if (!ok) fail(error);
|
|
578
597
|
printOutput(result,
|
|
579
598
|
output.formatAboutJson,
|
|
580
|
-
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 })
|
|
581
600
|
);
|
|
582
601
|
if (note) console.error(note);
|
|
583
602
|
break;
|
|
@@ -739,7 +758,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
739
758
|
}
|
|
740
759
|
|
|
741
760
|
case 'tests': {
|
|
742
|
-
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 });
|
|
743
762
|
if (!ok) fail(error);
|
|
744
763
|
printOutput(result,
|
|
745
764
|
r => output.formatTestsJson(r, arg),
|
|
@@ -912,31 +931,60 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
912
931
|
const index = new ProjectIndex(rootDir);
|
|
913
932
|
index.build(files, { quiet: true });
|
|
914
933
|
|
|
915
|
-
// Supported commands — anything that works with an index
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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.`);
|
|
919
940
|
process.exit(1);
|
|
920
941
|
}
|
|
921
942
|
|
|
922
943
|
// Build params — same as project mode
|
|
923
944
|
const params = {};
|
|
924
|
-
|
|
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)) {
|
|
925
949
|
if (!arg) {
|
|
926
950
|
console.error(`Usage: ucn "pattern" ${command} <name>`);
|
|
927
951
|
process.exit(1);
|
|
928
952
|
}
|
|
929
953
|
params.name = arg;
|
|
930
954
|
}
|
|
931
|
-
if (canonical === 'search') {
|
|
955
|
+
if (canonical === 'search' || canonical === 'structuralSearch') {
|
|
932
956
|
if (!arg && !flags.type) {
|
|
933
957
|
console.error('Usage: ucn "pattern" search <term>');
|
|
934
958
|
process.exit(1);
|
|
935
959
|
}
|
|
936
960
|
params.term = arg;
|
|
937
961
|
}
|
|
962
|
+
// Merge flags first, then set positional overrides so they aren't wiped
|
|
938
963
|
Object.assign(params, flags);
|
|
939
964
|
|
|
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)}'.`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (canonical === 'stacktrace' && arg) {
|
|
979
|
+
params.stack = arg;
|
|
980
|
+
}
|
|
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;
|
|
986
|
+
}
|
|
987
|
+
|
|
940
988
|
const { ok, result, error, note, structural } = execute(index, canonical, params);
|
|
941
989
|
if (!ok) fail(error);
|
|
942
990
|
if (note) console.error(note);
|
|
@@ -982,6 +1030,109 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
982
1030
|
case 'stats':
|
|
983
1031
|
printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
|
|
984
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;
|
|
1135
|
+
}
|
|
985
1136
|
}
|
|
986
1137
|
}
|
|
987
1138
|
|
|
@@ -1023,7 +1174,7 @@ FIND CODE
|
|
|
1023
1174
|
toc Table of contents (compact; --detailed lists all symbols)
|
|
1024
1175
|
search <term> Text search (regex default, --context=N, --exclude=, --in=)
|
|
1025
1176
|
Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
|
|
1026
|
-
tests <name> Find test files for a function
|
|
1177
|
+
tests <name> Find test files for a function (--file, --class-name, --exclude, --calls-only)
|
|
1027
1178
|
affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
|
|
1028
1179
|
|
|
1029
1180
|
═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1076,7 +1227,7 @@ Common Flags:
|
|
|
1076
1227
|
--code-only Filter out comments/strings (search, usages)
|
|
1077
1228
|
--with-types Include type definitions (about, smart)
|
|
1078
1229
|
--detailed Show all symbols in toc (not just counts)
|
|
1079
|
-
--include-tests Include test files
|
|
1230
|
+
--include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
|
|
1080
1231
|
--class-name=X Scope to specific class (e.g., --class-name=Repository)
|
|
1081
1232
|
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
1082
1233
|
--include-uncertain Include ambiguous/uncertain matches
|
|
@@ -1171,7 +1322,7 @@ Commands:
|
|
|
1171
1322
|
file-exports <file> File's exported symbols
|
|
1172
1323
|
imports <file> What file imports
|
|
1173
1324
|
exporters <file> Who imports file
|
|
1174
|
-
tests <name> Find tests (--calls-only)
|
|
1325
|
+
tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
|
|
1175
1326
|
affected-tests <n> Tests affected by a change (--depth=N)
|
|
1176
1327
|
search <term> Text search (--context=N, --exclude=, --in=)
|
|
1177
1328
|
Structural: --type= --param= --returns= --decorator= --exported --unused
|
|
@@ -1255,7 +1406,7 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1255
1406
|
|
|
1256
1407
|
const INTERACTIVE_DISPATCH = {
|
|
1257
1408
|
// ── Understanding Code ───────────────────────────────────────────
|
|
1258
|
-
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 }) },
|
|
1259
1410
|
smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
|
|
1260
1411
|
impact: { params: 'name', format: (r) => output.formatImpact(r) },
|
|
1261
1412
|
blast: { params: 'name', format: (r) => output.formatBlast(r) },
|
|
@@ -1309,6 +1460,20 @@ function buildInteractiveParams(descriptor, arg, iflags) {
|
|
|
1309
1460
|
}
|
|
1310
1461
|
|
|
1311
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
|
+
|
|
1312
1477
|
// ── Commands with unique behavior (not data-driven) ──────────────
|
|
1313
1478
|
switch (command) {
|
|
1314
1479
|
|
|
@@ -1375,9 +1540,12 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1375
1540
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
1376
1541
|
expandHint: 'Use "expand <N>" to see code for item N',
|
|
1377
1542
|
uncertainHint: 'use --include-uncertain to include all',
|
|
1378
|
-
showConfidence: iflags.showConfidence,
|
|
1543
|
+
showConfidence: iflags.showConfidence !== false,
|
|
1379
1544
|
});
|
|
1380
1545
|
console.log(text);
|
|
1546
|
+
if (iflags.expand) {
|
|
1547
|
+
printInlineExpand(result, index.root);
|
|
1548
|
+
}
|
|
1381
1549
|
if (note) console.log(note);
|
|
1382
1550
|
if (cache) {
|
|
1383
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),
|
package/core/execute.js
CHANGED
|
@@ -96,6 +96,7 @@ function buildCallerOptions(p) {
|
|
|
96
96
|
className: p.className,
|
|
97
97
|
includeMethods: p.includeMethods,
|
|
98
98
|
includeUncertain: p.includeUncertain || false,
|
|
99
|
+
includeTests: p.includeTests,
|
|
99
100
|
exclude: toExcludeArray(p.exclude),
|
|
100
101
|
minConfidence: num(p.minConfidence, 0),
|
|
101
102
|
};
|
|
@@ -592,11 +593,27 @@ const HANDLERS = {
|
|
|
592
593
|
const err = requireName(p.name);
|
|
593
594
|
if (err) return { ok: false, error: err };
|
|
594
595
|
applyClassMethodSyntax(p);
|
|
596
|
+
if (p.file) {
|
|
597
|
+
const fileErr = checkFilePatternMatch(index, p.file);
|
|
598
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
599
|
+
// Validate that the symbol exists in the target file
|
|
600
|
+
const defs = index.find(p.name, { exact: true, file: p.file, className: p.className });
|
|
601
|
+
if (defs.length === 0) {
|
|
602
|
+
const allDefs = index.find(p.name, { exact: true });
|
|
603
|
+
if (allDefs.length > 0) {
|
|
604
|
+
const files = allDefs.map(d => d.relativePath).join(', ');
|
|
605
|
+
return { ok: false, error: `Symbol "${p.name}" not found in files matching "${p.file}". Defined in: ${files}` };
|
|
606
|
+
}
|
|
607
|
+
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
595
610
|
const classErr = validateClassName(index, p.name, p.className);
|
|
596
611
|
if (classErr) return { ok: false, error: classErr };
|
|
597
612
|
const result = index.tests(p.name, {
|
|
598
613
|
callsOnly: p.callsOnly || false,
|
|
599
614
|
className: p.className,
|
|
615
|
+
file: p.file,
|
|
616
|
+
exclude: toExcludeArray(p.exclude),
|
|
600
617
|
});
|
|
601
618
|
return { ok: true, result };
|
|
602
619
|
},
|
package/core/output/analysis.js
CHANGED
|
@@ -80,7 +80,7 @@ function formatContextJson(context) {
|
|
|
80
80
|
function formatContext(ctx, options = {}) {
|
|
81
81
|
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
82
82
|
|
|
83
|
-
const expandHint = options.expandHint
|
|
83
|
+
const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
|
|
84
84
|
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
|
|
85
85
|
|
|
86
86
|
const lines = [];
|
package/core/registry.js
CHANGED
|
@@ -92,11 +92,11 @@ const PARAM_MAP = {
|
|
|
92
92
|
// ============================================================================
|
|
93
93
|
|
|
94
94
|
// Per-command list of accepted flag names (camelCase). Source of truth for help text,
|
|
95
|
-
// MCP
|
|
95
|
+
// MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
|
|
96
96
|
// file* = file is the command subject (required), not a filter pattern.
|
|
97
97
|
const FLAG_APPLICABILITY = {
|
|
98
98
|
// Understanding code
|
|
99
|
-
about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
|
|
99
|
+
about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
|
|
100
100
|
context: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
|
|
101
101
|
impact: ['file', 'exclude', 'className', 'top'],
|
|
102
102
|
blast: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
|
|
@@ -110,7 +110,7 @@ const FLAG_APPLICABILITY = {
|
|
|
110
110
|
usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
|
|
111
111
|
toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
|
|
112
112
|
search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
|
|
113
|
-
tests: ['className', 'callsOnly'],
|
|
113
|
+
tests: ['file', 'exclude', 'className', 'callsOnly'],
|
|
114
114
|
affectedTests:['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
|
|
115
115
|
deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
|
|
116
116
|
entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
|
|
@@ -123,7 +123,7 @@ const FLAG_APPLICABILITY = {
|
|
|
123
123
|
imports: ['file'],
|
|
124
124
|
exporters: ['file'],
|
|
125
125
|
fileExports: ['file'],
|
|
126
|
-
graph: ['file', 'depth', 'direction'],
|
|
126
|
+
graph: ['file', 'depth', 'direction', 'all'],
|
|
127
127
|
circularDeps: ['file', 'exclude'],
|
|
128
128
|
// Refactoring
|
|
129
129
|
verify: ['file', 'className'],
|
|
@@ -224,11 +224,43 @@ function toCliName(canonical) {
|
|
|
224
224
|
return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Build a reverse map: camelCase → snake_case from PARAM_MAP.
|
|
229
|
+
* Flags not in PARAM_MAP are already snake_case-safe (single words).
|
|
230
|
+
*/
|
|
231
|
+
function buildReverseParamMap() {
|
|
232
|
+
const rev = {};
|
|
233
|
+
for (const [snake, camel] of Object.entries(PARAM_MAP)) {
|
|
234
|
+
rev[camel] = snake;
|
|
235
|
+
}
|
|
236
|
+
return rev;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const REVERSE_PARAM_MAP = buildReverseParamMap();
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate per-command parameter listing for the MCP tool description.
|
|
243
|
+
* Maps camelCase flags back to snake_case for MCP clients.
|
|
244
|
+
* One line per command: `about: file, exclude, class_name, ...`
|
|
245
|
+
*/
|
|
246
|
+
function generateMcpParamSection() {
|
|
247
|
+
const lines = ['', 'ACCEPTED FLAGS PER COMMAND (name, term, stack, range, base, staged, max_chars always accepted; flags not listed below are ignored):'];
|
|
248
|
+
for (const cmd of CANONICAL_COMMANDS) {
|
|
249
|
+
const flags = FLAG_APPLICABILITY[cmd];
|
|
250
|
+
if (!flags || flags.length === 0) continue;
|
|
251
|
+
const mcpCmd = toMcpName(cmd);
|
|
252
|
+
const mcpFlags = flags.map(f => REVERSE_PARAM_MAP[f] || f);
|
|
253
|
+
lines.push(` ${mcpCmd}: ${mcpFlags.join(', ')}`);
|
|
254
|
+
}
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
227
258
|
module.exports = {
|
|
228
259
|
CANONICAL_COMMANDS,
|
|
229
260
|
CLI_ALIASES,
|
|
230
261
|
MCP_ALIASES,
|
|
231
262
|
PARAM_MAP,
|
|
263
|
+
REVERSE_PARAM_MAP,
|
|
232
264
|
FLAG_APPLICABILITY,
|
|
233
265
|
BROAD_COMMANDS,
|
|
234
266
|
resolveCommand,
|
|
@@ -237,4 +269,5 @@ module.exports = {
|
|
|
237
269
|
getMcpCommandEnum,
|
|
238
270
|
toMcpName,
|
|
239
271
|
toCliName,
|
|
272
|
+
generateMcpParamSection,
|
|
240
273
|
};
|