ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/cli/index.js
CHANGED
|
@@ -14,22 +14,24 @@ 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 {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
17
|
+
const {
|
|
18
|
+
getCliCommandSet,
|
|
19
|
+
resolveCommand,
|
|
20
|
+
suggestCommand,
|
|
21
|
+
v4MigrationHint,
|
|
22
|
+
FLAG_APPLICABILITY,
|
|
23
|
+
toCliName,
|
|
24
|
+
FILE_LOCAL_COMMANDS,
|
|
25
|
+
formatSurfaceMessage,
|
|
26
|
+
getCliFlagsForCommand,
|
|
27
|
+
getCliAcceptedFlags,
|
|
28
|
+
} = require('../core/registry');
|
|
29
|
+
const { buildPublicParams, isPublicCommand } = require('../core/public-command');
|
|
31
30
|
const { execute } = require('../core/execute');
|
|
32
|
-
const {
|
|
31
|
+
const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
|
|
32
|
+
const { clearAllCaches } = require('../core/cache');
|
|
33
|
+
|
|
34
|
+
let activeCanonicalCommand = null;
|
|
33
35
|
|
|
34
36
|
// Sentinel error for command failures that have already printed their message.
|
|
35
37
|
// Thrown instead of process.exit(1) so finally blocks can run (cache save).
|
|
@@ -42,6 +44,35 @@ class FlagValidationError extends Error {
|
|
|
42
44
|
constructor(msg) { super(msg); this.name = 'FlagValidationError'; }
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function unknownCommandMessage(command, { interactive = false } = {}) {
|
|
48
|
+
const migration = v4MigrationHint(command, 'cli');
|
|
49
|
+
if (migration) return `Unknown command: ${command}. ${migration}`;
|
|
50
|
+
const suggestion = suggestCommand(command, 'cli');
|
|
51
|
+
const correction = suggestion ? ` Did you mean "${suggestion}"?` : '';
|
|
52
|
+
const help = !suggestion
|
|
53
|
+
? (interactive
|
|
54
|
+
? ' Type "help" for available commands.'
|
|
55
|
+
: ' Run "ucn --help" for available commands.')
|
|
56
|
+
: '';
|
|
57
|
+
return `Unknown command: ${command}.${correction}${help}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resultExitCode(command, result) {
|
|
61
|
+
if (result && result.ok === false) return 2;
|
|
62
|
+
if (command !== 'check' || !result) return 0;
|
|
63
|
+
if (result.trust) {
|
|
64
|
+
// Target-less pre-commit check distinguishes advisory review from a
|
|
65
|
+
// blocking validation failure. Export/reference review remains
|
|
66
|
+
// machine-readable in trust.status but does not make every ordinary
|
|
67
|
+
// body edit fail CI; incomplete accounts and signature drift do.
|
|
68
|
+
return result.trust.status === 'BLOCKED' ? 1 : 0;
|
|
69
|
+
}
|
|
70
|
+
// Symbol-mode check (verify): a mismatch or unresolved call-site tier is
|
|
71
|
+
// review-required and must fail a CI gate.
|
|
72
|
+
return (Number(result.mismatches || 0) > 0 ||
|
|
73
|
+
Number(result.unverifiedCount || 0) > 0) ? 1 : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
/**
|
|
46
77
|
* Validate that a raw flag value is a positive integer. Returns the parsed
|
|
47
78
|
* number when valid, or throws FlagValidationError. Callers pass `null`/`undefined`
|
|
@@ -61,6 +92,13 @@ function validatePositiveInt(raw, flagName, { allowZero = false, cap = 10000000
|
|
|
61
92
|
if (trimmed === '') {
|
|
62
93
|
throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
|
|
63
94
|
}
|
|
95
|
+
// CLI integer flags use canonical base-10 spelling. Number()/parseInt()
|
|
96
|
+
// accept exponent, decimal and hexadecimal forms (`1e1`, `2.5`, `0x10`),
|
|
97
|
+
// which is especially dangerous for source-line identity because a
|
|
98
|
+
// malformed pin can accidentally select a real definition.
|
|
99
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
100
|
+
throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
|
|
101
|
+
}
|
|
64
102
|
const n = Number(trimmed);
|
|
65
103
|
if (!isFinite(n) || isNaN(n)) {
|
|
66
104
|
throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
|
|
@@ -90,7 +128,7 @@ function validatePositiveInt(raw, flagName, { allowZero = false, cap = 10000000
|
|
|
90
128
|
* Throws FlagValidationError on the first invalid flag.
|
|
91
129
|
*/
|
|
92
130
|
function validateNumericFlags(flags) {
|
|
93
|
-
// --top: positive integer, no zero. Used by
|
|
131
|
+
// --top: positive integer, no zero. Used by show/find/repo/etc.
|
|
94
132
|
if (flags.topRaw != null) {
|
|
95
133
|
flags.top = validatePositiveInt(flags.topRaw, '--top');
|
|
96
134
|
}
|
|
@@ -102,7 +140,10 @@ function validateNumericFlags(flags) {
|
|
|
102
140
|
if (flags.maxFilesRaw != null) {
|
|
103
141
|
flags.maxFiles = validatePositiveInt(flags.maxFilesRaw, '--max-files');
|
|
104
142
|
}
|
|
105
|
-
|
|
143
|
+
if (flags.lineRaw != null) {
|
|
144
|
+
flags.line = validatePositiveInt(flags.lineRaw, '--line');
|
|
145
|
+
}
|
|
146
|
+
// --max-lines: positive integer, no zero. Used by source.
|
|
106
147
|
if (flags.maxLinesRaw != null) {
|
|
107
148
|
flags.maxLines = validatePositiveInt(flags.maxLinesRaw, '--max-lines');
|
|
108
149
|
}
|
|
@@ -118,22 +159,62 @@ function validateNumericFlags(flags) {
|
|
|
118
159
|
if (flags.workersRaw != null) {
|
|
119
160
|
flags.workers = validatePositiveInt(flags.workersRaw, '--workers', { allowZero: true });
|
|
120
161
|
}
|
|
162
|
+
if (flags.maxCharsRaw != null) {
|
|
163
|
+
flags.maxChars = validatePositiveInt(flags.maxCharsRaw, '--max-chars', {
|
|
164
|
+
cap: MAX_OUTPUT_CHARS,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// --min-confidence: number in [0,1] (ordinal evidence weight). Anything
|
|
168
|
+
// else used to coerce to 0 silently — "abc" behaved like no filter.
|
|
169
|
+
if (flags.minConfidenceRaw != null) {
|
|
170
|
+
const value = Number(flags.minConfidenceRaw);
|
|
171
|
+
if (flags.minConfidenceRaw === '' || !Number.isFinite(value) ||
|
|
172
|
+
value < 0 || value > 1) {
|
|
173
|
+
throw new FlagValidationError(
|
|
174
|
+
`Invalid --min-confidence value: "${flags.minConfidenceRaw}" — must be a number between 0 and 1.`);
|
|
175
|
+
}
|
|
176
|
+
flags.minConfidence = value;
|
|
177
|
+
}
|
|
121
178
|
}
|
|
122
179
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
* error envelope to stdout (so JSON-consuming pipelines see structured output)
|
|
126
|
-
* and write the same plain message to stderr (for humans piping to a TTY).
|
|
127
|
-
*/
|
|
128
|
-
function fail(msg) {
|
|
129
|
-
// This helper can run before parsed flags exist, so raw argv is the single
|
|
130
|
-
// reliable source for the output mode.
|
|
180
|
+
/** Emit the one CLI error contract, including failures before main dispatch. */
|
|
181
|
+
function emitCliError(msg, command = activeCanonicalCommand) {
|
|
131
182
|
const wantsJson = process.argv.includes('--json');
|
|
183
|
+
const error = typeof msg === 'string' ? msg : String(msg);
|
|
132
184
|
if (wantsJson) {
|
|
133
|
-
const
|
|
185
|
+
const canonical = command && isPublicCommand(command)
|
|
186
|
+
? command
|
|
187
|
+
: resolveCommand(command, 'cli');
|
|
188
|
+
const surfaceCommand = canonical
|
|
189
|
+
? toCliName(canonical)
|
|
190
|
+
: (command ? String(command) : null);
|
|
191
|
+
const env = {
|
|
192
|
+
meta: {
|
|
193
|
+
ok: false,
|
|
194
|
+
...(surfaceCommand && {
|
|
195
|
+
command: surfaceCommand,
|
|
196
|
+
...(canonical && surfaceCommand !== canonical && {
|
|
197
|
+
canonicalCommand: canonical,
|
|
198
|
+
}),
|
|
199
|
+
...(canonical && {
|
|
200
|
+
contract: output.contractMeta(canonical),
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
data: null,
|
|
205
|
+
error,
|
|
206
|
+
};
|
|
134
207
|
try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) { /* stdout may be closed */ }
|
|
135
208
|
}
|
|
136
|
-
console.error(
|
|
209
|
+
console.error(error);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Print an error message and abort command execution. Throw instead of calling
|
|
214
|
+
* process.exit so index/cache finally blocks still run.
|
|
215
|
+
*/
|
|
216
|
+
function fail(msg, command = activeCanonicalCommand) {
|
|
217
|
+
emitCliError(msg, command);
|
|
137
218
|
throw new CommandError();
|
|
138
219
|
}
|
|
139
220
|
|
|
@@ -191,7 +272,6 @@ function parseFlags(tokens) {
|
|
|
191
272
|
excludeTests: tokens.includes('--exclude-tests') ? true : undefined,
|
|
192
273
|
includeExported: tokens.includes('--include-exported') || undefined,
|
|
193
274
|
includeDecorated: tokens.includes('--include-decorated') || undefined,
|
|
194
|
-
includeUncertain: tokens.includes('--include-uncertain') || undefined,
|
|
195
275
|
expandUnverified: tokens.includes('--expand-unverified') || undefined,
|
|
196
276
|
includeMethods: tokens.some(a => a === '--include-methods=false' || a === '--no-include-methods') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
|
|
197
277
|
detailed: tokens.includes('--detailed') || undefined,
|
|
@@ -202,7 +282,8 @@ function parseFlags(tokens) {
|
|
|
202
282
|
codeOnly: tokens.includes('--code-only') || undefined,
|
|
203
283
|
caseSensitive: tokens.includes('--case-sensitive') || undefined,
|
|
204
284
|
withTypes: tokens.includes('--with-types') || undefined,
|
|
205
|
-
|
|
285
|
+
withSource: tokens.includes('--with-source') || undefined,
|
|
286
|
+
cycles: tokens.includes('--cycles') || undefined,
|
|
206
287
|
depth: getValueFlag('--depth'),
|
|
207
288
|
depthRaw: getValueFlag('--depth'),
|
|
208
289
|
// `top` is the parsed numeric value (NaN/0 default → falsy). `topRaw`
|
|
@@ -213,6 +294,9 @@ function parseFlags(tokens) {
|
|
|
213
294
|
context: parseInt(getValueFlag('--context') || '0'),
|
|
214
295
|
contextRaw: getValueFlag('--context'),
|
|
215
296
|
direction: getValueFlag('--direction'),
|
|
297
|
+
to: getValueFlag('--to'),
|
|
298
|
+
sections: getValueFlag('--sections'),
|
|
299
|
+
range: getValueFlag('--range'),
|
|
216
300
|
addParam: getValueFlag('--add-param'),
|
|
217
301
|
removeParam: getValueFlag('--remove-param'),
|
|
218
302
|
renameTo: getValueFlag('--rename-to'),
|
|
@@ -220,10 +304,14 @@ function parseFlags(tokens) {
|
|
|
220
304
|
base: getValueFlag('--base'),
|
|
221
305
|
staged: tokens.includes('--staged') || undefined,
|
|
222
306
|
deep: tokens.includes('--deep') || undefined,
|
|
223
|
-
compact: tokens.includes('--compact')
|
|
307
|
+
compact: tokens.includes('--no-compact')
|
|
308
|
+
? false
|
|
309
|
+
: (tokens.includes('--compact') ? true : undefined),
|
|
224
310
|
maxLines: getValueFlag('--max-lines') || null,
|
|
225
311
|
maxLinesRaw: getValueFlag('--max-lines'),
|
|
226
|
-
regex: tokens.includes('--
|
|
312
|
+
regex: tokens.includes('--regex')
|
|
313
|
+
? true
|
|
314
|
+
: (tokens.includes('--no-regex') ? false : undefined),
|
|
227
315
|
functions: tokens.includes('--functions') || undefined,
|
|
228
316
|
hot: tokens.includes('--hot') || undefined,
|
|
229
317
|
diverse: tokens.includes('--diverse') || undefined,
|
|
@@ -232,10 +320,13 @@ function parseFlags(tokens) {
|
|
|
232
320
|
// Explicit line pin (fix #249: our own disambiguation notes advertise
|
|
233
321
|
// line= but no surface accepted it).
|
|
234
322
|
line: parseInt(getValueFlag('--line') || '0', 10) || undefined,
|
|
323
|
+
lineRaw: getValueFlag('--line'),
|
|
235
324
|
limit: parseInt(getValueFlag('--limit') || '0') || undefined,
|
|
236
325
|
limitRaw: getValueFlag('--limit'),
|
|
237
326
|
maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
|
|
238
327
|
maxFilesRaw: getValueFlag('--max-files'),
|
|
328
|
+
maxChars: parseInt(getValueFlag('--max-chars') || '0') || undefined,
|
|
329
|
+
maxCharsRaw: getValueFlag('--max-chars'),
|
|
239
330
|
// Structural search flags
|
|
240
331
|
type: getValueFlag('--type'),
|
|
241
332
|
param: getValueFlag('--param'),
|
|
@@ -244,8 +335,10 @@ function parseFlags(tokens) {
|
|
|
244
335
|
decorator: getValueFlag('--decorator'),
|
|
245
336
|
exported: tokens.includes('--exported') || undefined,
|
|
246
337
|
unused: tokens.includes('--unused') || undefined,
|
|
247
|
-
showConfidence: (tokens.includes('--hide-confidence') || tokens.includes('--no-confidence')) ? false
|
|
248
|
-
|
|
338
|
+
showConfidence: (tokens.includes('--hide-confidence') || tokens.includes('--no-confidence')) ? false
|
|
339
|
+
: tokens.includes('--show-confidence') ? true : undefined,
|
|
340
|
+
minConfidence: 0,
|
|
341
|
+
minConfidenceRaw: getValueFlag('--min-confidence'),
|
|
249
342
|
unreachableOnly: tokens.includes('--unreachable-only') || undefined,
|
|
250
343
|
framework: getValueFlag('--framework'),
|
|
251
344
|
// endpoints command flags
|
|
@@ -277,24 +370,7 @@ flags.interactive = args.includes('--interactive') || args.includes('-i');
|
|
|
277
370
|
flags.followSymlinks = !args.includes('--no-follow-symlinks');
|
|
278
371
|
|
|
279
372
|
// Known flags for validation
|
|
280
|
-
const knownFlags =
|
|
281
|
-
'--help', '-h', '--version', '-v', '--mcp',
|
|
282
|
-
'--json', '--verbose', '--no-quiet', '--quiet',
|
|
283
|
-
'--code-only', '--with-types', '--top-level', '--exact', '--case-sensitive',
|
|
284
|
-
'--no-cache', '--clear-cache', '--include-tests', '--exclude-tests',
|
|
285
|
-
'--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--expand-unverified', '--detailed', '--calls-only',
|
|
286
|
-
'--file', '--context', '--exclude', '--not', '--in',
|
|
287
|
-
'--depth', '--direction', '--add-param', '--remove-param', '--rename-to', '--default-value',
|
|
288
|
-
'--default', '--top', '--no-follow-symlinks',
|
|
289
|
-
'--base', '--staged', '--stack',
|
|
290
|
-
'--regex', '--no-regex', '--functions', '--hot', '--diverse', '--git',
|
|
291
|
-
'--max-lines', '--class-name', '--line', '--limit', '--max-files',
|
|
292
|
-
'--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
|
|
293
|
-
'--hide-confidence', '--no-confidence', '--min-confidence', '--unreachable-only',
|
|
294
|
-
'--framework', '--workers', '--deep', '--compact',
|
|
295
|
-
'--bridge', '--server-only', '--client-only', '--unmatched',
|
|
296
|
-
'--method', '--prefix', '--hide-uncertain', '--no-uncertain'
|
|
297
|
-
]);
|
|
373
|
+
const knownFlags = getCliAcceptedFlags();
|
|
298
374
|
|
|
299
375
|
// Handle help flag
|
|
300
376
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -317,8 +393,9 @@ const unknownFlags = args.filter(a => {
|
|
|
317
393
|
});
|
|
318
394
|
|
|
319
395
|
if (unknownFlags.length > 0) {
|
|
320
|
-
|
|
321
|
-
|
|
396
|
+
emitCliError(
|
|
397
|
+
`Unknown flag(s): ${unknownFlags.join(', ')}. Use --help to see available flags.`,
|
|
398
|
+
);
|
|
322
399
|
process.exit(1);
|
|
323
400
|
}
|
|
324
401
|
|
|
@@ -329,11 +406,7 @@ try {
|
|
|
329
406
|
validateNumericFlags(flags);
|
|
330
407
|
} catch (e) {
|
|
331
408
|
if (e instanceof FlagValidationError) {
|
|
332
|
-
|
|
333
|
-
const env = { meta: { ok: false }, error: e.message };
|
|
334
|
-
try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) { /* stdout may be closed */ }
|
|
335
|
-
}
|
|
336
|
-
console.error(e.message);
|
|
409
|
+
emitCliError(e.message);
|
|
337
410
|
process.exit(1);
|
|
338
411
|
}
|
|
339
412
|
throw e;
|
|
@@ -341,11 +414,11 @@ try {
|
|
|
341
414
|
|
|
342
415
|
// Value flags that consume the next token (space form: --flag value)
|
|
343
416
|
const VALUE_FLAGS = new Set([
|
|
344
|
-
'--file', '--depth', '--top', '--context', '--direction',
|
|
417
|
+
'--file', '--depth', '--top', '--context', '--direction', '--to', '--sections', '--range',
|
|
345
418
|
'--add-param', '--remove-param', '--rename-to', '--default', '--default-value',
|
|
346
419
|
'--base', '--exclude', '--not', '--in', '--max-lines', '--class-name', '--line',
|
|
347
420
|
'--type', '--param', '--receiver', '--returns', '--decorator',
|
|
348
|
-
'--limit', '--max-files', '--min-confidence', '--stack', '--framework',
|
|
421
|
+
'--limit', '--max-files', '--max-chars', '--min-confidence', '--stack', '--framework',
|
|
349
422
|
'--workers', '--method', '--prefix'
|
|
350
423
|
]);
|
|
351
424
|
|
|
@@ -378,58 +451,18 @@ function requireArg(arg, usage) {
|
|
|
378
451
|
}
|
|
379
452
|
}
|
|
380
453
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
console.log(text);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Print inline 3-line code previews for context items (--expand support).
|
|
400
|
-
* Used by context in project, interactive, and glob modes. Previews both
|
|
401
|
-
* directions with attribution headers (fix #252: callees only — the flag
|
|
402
|
-
* was a silent no-op for caller-only contexts, and the unlabeled preview
|
|
403
|
-
* rendered detached from the item it expanded).
|
|
404
|
-
*/
|
|
405
|
-
function printInlineExpand(ctx, root) {
|
|
406
|
-
if (!root || !ctx) return;
|
|
407
|
-
const preview = (c, label) => {
|
|
408
|
-
// Caller entries locate the enclosing FUNCTION via callerStartLine;
|
|
409
|
-
// callee entries are definitions with startLine.
|
|
410
|
-
const startLine = label === 'caller' ? c.callerStartLine : c.startLine;
|
|
411
|
-
const endLine = label === 'caller'
|
|
412
|
-
? (c.callerEndLine || (startLine ? startLine + 5 : null))
|
|
413
|
-
: (c.endLine || (startLine ? startLine + 5 : null));
|
|
414
|
-
if (!c.relativePath || !startLine) return;
|
|
415
|
-
try {
|
|
416
|
-
const filePath = path.join(root, c.relativePath);
|
|
417
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
418
|
-
const codeLines = content.split('\n');
|
|
419
|
-
console.log(` ┌ ${label}: ${c.name || c.callerName || '(anonymous)'} — ${c.relativePath}:${startLine}`);
|
|
420
|
-
const previewLines = Math.min(3, endLine - startLine + 1);
|
|
421
|
-
for (let i = 0; i < previewLines && startLine - 1 + i < codeLines.length; i++) {
|
|
422
|
-
console.log(` │ ${codeLines[startLine - 1 + i]}`);
|
|
423
|
-
}
|
|
424
|
-
if (endLine - startLine + 1 > 3) {
|
|
425
|
-
console.log(` │ ... (${endLine - startLine - 2} more lines)`);
|
|
426
|
-
}
|
|
427
|
-
} catch (e) {
|
|
428
|
-
// Skip on error
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
for (const c of ctx.callers || []) preview(c, 'caller');
|
|
432
|
-
for (const c of ctx.callees || []) preview(c, 'callee');
|
|
454
|
+
function formatCliText(command, result, params, execution, displayFlags) {
|
|
455
|
+
const text = output.formatPublicText(command, result, params, {
|
|
456
|
+
...execution,
|
|
457
|
+
surface: 'cli',
|
|
458
|
+
});
|
|
459
|
+
return applyOutputBudget(text, {
|
|
460
|
+
command,
|
|
461
|
+
maxChars: displayFlags?.maxChars,
|
|
462
|
+
all: !!displayFlags?.all,
|
|
463
|
+
surface: 'cli',
|
|
464
|
+
params,
|
|
465
|
+
}).text;
|
|
433
466
|
}
|
|
434
467
|
|
|
435
468
|
// ============================================================================
|
|
@@ -444,18 +477,36 @@ function main() {
|
|
|
444
477
|
let target, command, arg;
|
|
445
478
|
|
|
446
479
|
if (positionalArgs.length === 0) {
|
|
480
|
+
// Standalone `ucn --clear-cache` (the form SKILL.md and README
|
|
481
|
+
// document) clears the current project's cache — falling through to
|
|
482
|
+
// the help banner made it a silent no-op.
|
|
483
|
+
if (flags.clearCache) {
|
|
484
|
+
if (flags.all) {
|
|
485
|
+
const removed = clearAllCaches();
|
|
486
|
+
console.log(removed.length > 0
|
|
487
|
+
? 'All UCN user caches cleared'
|
|
488
|
+
: 'No UCN user caches to clear');
|
|
489
|
+
process.exit(0);
|
|
490
|
+
}
|
|
491
|
+
const index = new ProjectIndex('.');
|
|
492
|
+
const removed = index.clearCache();
|
|
493
|
+
console.log(removed.length > 0
|
|
494
|
+
? `Cache cleared (${index.root})`
|
|
495
|
+
: `No cache to clear (${index.root})`);
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
447
498
|
// No args: show help
|
|
448
499
|
printUsage();
|
|
449
500
|
process.exit(0);
|
|
450
501
|
} else if (positionalArgs.length === 1) {
|
|
451
|
-
// One arg: could be a command (use . as target) or a target (use
|
|
502
|
+
// One arg: could be a command (use . as target) or a target (use repo as command)
|
|
452
503
|
if (COMMANDS.has(positionalArgs[0])) {
|
|
453
504
|
target = '.';
|
|
454
505
|
command = positionalArgs[0];
|
|
455
506
|
arg = undefined;
|
|
456
507
|
} else {
|
|
457
508
|
target = positionalArgs[0];
|
|
458
|
-
command = '
|
|
509
|
+
command = 'repo';
|
|
459
510
|
arg = undefined;
|
|
460
511
|
}
|
|
461
512
|
} else if (COMMANDS.has(positionalArgs[0])) {
|
|
@@ -466,12 +517,10 @@ function main() {
|
|
|
466
517
|
} else {
|
|
467
518
|
// First arg is a target (path/glob)
|
|
468
519
|
target = positionalArgs[0];
|
|
469
|
-
command = positionalArgs[1] || '
|
|
520
|
+
command = positionalArgs[1] || 'repo';
|
|
470
521
|
arg = positionalArgs[2];
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
// demanded a --file the user had plainly given).
|
|
474
|
-
if (command === 'lines' && positionalArgs.length > 3) {
|
|
522
|
+
// source accepts `<file> <range>` as two positionals.
|
|
523
|
+
if (command === 'source' && positionalArgs.length > 3) {
|
|
475
524
|
arg = positionalArgs.slice(2).join(' ');
|
|
476
525
|
}
|
|
477
526
|
}
|
|
@@ -491,54 +540,62 @@ function main() {
|
|
|
491
540
|
// Single file mode
|
|
492
541
|
runFileCommand(target, command, arg);
|
|
493
542
|
} else {
|
|
494
|
-
|
|
495
|
-
|
|
543
|
+
// `ucn missing-dir repo` is a target error. Any other two-token
|
|
544
|
+
// form whose first token is not a path is an unknown command.
|
|
545
|
+
const targetInvocation = positionalArgs.length > 1 &&
|
|
546
|
+
COMMANDS.has(positionalArgs[1]);
|
|
547
|
+
fail(
|
|
548
|
+
targetInvocation
|
|
549
|
+
? `Error: "${target}" not found`
|
|
550
|
+
: unknownCommandMessage(target),
|
|
551
|
+
targetInvocation ? null : target,
|
|
552
|
+
);
|
|
496
553
|
}
|
|
497
554
|
} catch (e) {
|
|
498
555
|
if (!(e instanceof CommandError)) {
|
|
499
|
-
|
|
556
|
+
emitCliError(`Error: ${e.message}`);
|
|
500
557
|
}
|
|
501
558
|
process.exitCode = 1;
|
|
502
559
|
}
|
|
503
560
|
}
|
|
504
561
|
|
|
505
562
|
/**
|
|
506
|
-
*
|
|
507
|
-
* silently dropped the second positional and demanded a --file that was
|
|
508
|
-
* plainly given): `<range>` (+ --file), `<file> <range>`, `<file>:<range>`.
|
|
509
|
-
*/
|
|
510
|
-
function parseLinesTarget(arg, fileFlag) {
|
|
511
|
-
let file = fileFlag;
|
|
512
|
-
let range = arg;
|
|
513
|
-
const parts = String(arg || '').trim().split(/\s+/);
|
|
514
|
-
if (parts.length === 2 && /^\d+(-\d+)?$/.test(parts[1])) {
|
|
515
|
-
file = parts[0];
|
|
516
|
-
range = parts[1];
|
|
517
|
-
} else if (!/^\d+(-\d+)?$/.test(range)) {
|
|
518
|
-
const m = String(range).match(/^(.+):(\d+(?:-\d+)?)$/);
|
|
519
|
-
if (m) { file = m[1]; range = m[2]; }
|
|
520
|
-
}
|
|
521
|
-
return { file, range };
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Tiered-output contract notes: unverified callers are always shown for
|
|
526
|
-
* these commands, so the legacy reveal flags are implied no-ops. Shared by
|
|
527
|
-
* one-shot and interactive mode (fix #250 — interactive printed nothing).
|
|
563
|
+
* Tiered-output contract notes shared by one-shot and interactive mode.
|
|
528
564
|
*/
|
|
529
565
|
function printTieredNoOpNotes(canonical, flags, print) {
|
|
530
|
-
if (!['
|
|
531
|
-
if (flags.
|
|
532
|
-
print(`Note: --include-uncertain has no effect on '${toCliName(canonical)}' — unverified candidates are always shown (tiered).`);
|
|
533
|
-
}
|
|
534
|
-
if (['impact', 'verify', 'blast', 'reverseTrace', 'affectedTests'].includes(canonical) && flags.includeMethods) {
|
|
566
|
+
if (!['show', 'impact', 'trace', 'tests', 'check'].includes(canonical)) return;
|
|
567
|
+
if (['impact', 'trace', 'tests', 'check'].includes(canonical) && flags.includeMethods) {
|
|
535
568
|
print(`Note: --include-methods has no effect on '${toCliName(canonical)}' — method calls are always tiered by receiver evidence.`);
|
|
536
569
|
}
|
|
537
|
-
if (
|
|
570
|
+
if (canonical === 'show' && flags.includeMethods) {
|
|
538
571
|
print(`Note: --include-methods on '${toCliName(canonical)}' affects only method-callee display for standalone-function targets — caller tiers are always evidence-based, and method targets analyze method calls by default.`);
|
|
539
572
|
}
|
|
540
573
|
}
|
|
541
574
|
|
|
575
|
+
const GLOBAL_FLAG_KEYS = new Set([
|
|
576
|
+
'json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles',
|
|
577
|
+
'verbose', 'interactive', '_fileFromFileMode', 'topRaw',
|
|
578
|
+
'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw',
|
|
579
|
+
'workers', 'workersRaw', 'lineRaw', 'maxChars', 'maxCharsRaw', 'minConfidenceRaw',
|
|
580
|
+
]);
|
|
581
|
+
|
|
582
|
+
/** Apply one command/flag policy across project, file, glob, and REPL modes. */
|
|
583
|
+
function warnInapplicableFlags(canonical, parsedFlags, print) {
|
|
584
|
+
const applicableFlags = FLAG_APPLICABILITY[canonical];
|
|
585
|
+
if (!applicableFlags) return;
|
|
586
|
+
const flagToCli = (flag) => '--' + flag.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
587
|
+
for (const [key, value] of Object.entries(parsedFlags)) {
|
|
588
|
+
if (GLOBAL_FLAG_KEYS.has(key)) continue;
|
|
589
|
+
if (value === undefined || value === null || value === 0 ||
|
|
590
|
+
(Array.isArray(value) && value.length === 0)) continue;
|
|
591
|
+
if (key === 'file' && parsedFlags._fileFromFileMode) continue;
|
|
592
|
+
if (!applicableFlags.includes(key)) {
|
|
593
|
+
print(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
printTieredNoOpNotes(canonical, parsedFlags, print);
|
|
597
|
+
}
|
|
598
|
+
|
|
542
599
|
// ============================================================================
|
|
543
600
|
// FILE MODE
|
|
544
601
|
// ============================================================================
|
|
@@ -546,11 +603,11 @@ function printTieredNoOpNotes(canonical, flags, print) {
|
|
|
546
603
|
function runFileCommand(filePath, command, arg) {
|
|
547
604
|
const language = detectLanguage(filePath);
|
|
548
605
|
if (!language) {
|
|
549
|
-
|
|
550
|
-
process.exit(1);
|
|
606
|
+
fail(`Unsupported file type: ${filePath}`, command);
|
|
551
607
|
}
|
|
552
608
|
|
|
553
609
|
const canonical = resolveCommand(command, 'cli') || command;
|
|
610
|
+
activeCanonicalCommand = canonical;
|
|
554
611
|
|
|
555
612
|
// Commands that need full project index — auto-route to project mode
|
|
556
613
|
const fileLocalCommands = FILE_LOCAL_COMMANDS;
|
|
@@ -559,7 +616,7 @@ function runFileCommand(filePath, command, arg) {
|
|
|
559
616
|
// Auto-detect project root and route to project mode
|
|
560
617
|
const projectRoot = findProjectRoot(path.dirname(filePath));
|
|
561
618
|
let effectiveArg = arg;
|
|
562
|
-
if (
|
|
619
|
+
if (canonical === 'deps' && !arg) {
|
|
563
620
|
effectiveArg = filePath;
|
|
564
621
|
}
|
|
565
622
|
// Scope to the target file unless an explicit --file was provided
|
|
@@ -572,78 +629,47 @@ function runFileCommand(filePath, command, arg) {
|
|
|
572
629
|
return;
|
|
573
630
|
}
|
|
574
631
|
|
|
575
|
-
|
|
576
|
-
|
|
632
|
+
if (!isPublicCommand(canonical)) {
|
|
633
|
+
fail(unknownCommandMessage(command));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Require arg for commands that need it.
|
|
637
|
+
const needsArg = {
|
|
638
|
+
show: 'show <name>', find: 'find <name>', usages: 'usages <name>',
|
|
639
|
+
search: 'search <term>', source: 'source <name|range>', trace: 'trace <name>',
|
|
640
|
+
tests: 'tests <name>', plan: 'plan <name>',
|
|
641
|
+
};
|
|
577
642
|
// Structural search doesn't require term
|
|
578
643
|
const isStructural = flags.type || flags.param || flags.receiver || flags.returns || flags.decorator || flags.exported || flags.unused;
|
|
579
|
-
|
|
644
|
+
const hasFlagTarget = canonical === 'source' && flags.range;
|
|
645
|
+
if (needsArg[canonical] && !(canonical === 'search' && isStructural) && !hasFlagTarget) {
|
|
580
646
|
requireArg(arg, `Usage: ucn <file> ${needsArg[canonical]}`);
|
|
581
647
|
}
|
|
582
648
|
|
|
583
|
-
// Build single-file index and route through
|
|
649
|
+
// Build single-file index and route through the same public executor and
|
|
650
|
+
// formatter used by project/glob/MCP modes.
|
|
584
651
|
const index = new ProjectIndex(path.dirname(filePath));
|
|
585
652
|
index.buildSingleFile(filePath);
|
|
586
653
|
const relativePath = path.relative(index.root, path.resolve(filePath));
|
|
587
654
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
fn: { name: arg, file: relativePath, ...flags },
|
|
592
|
-
class: { name: arg, file: relativePath, ...flags },
|
|
593
|
-
find: { name: arg, file: relativePath, ...flags },
|
|
594
|
-
usages: { name: arg, file: relativePath, ...flags },
|
|
595
|
-
search: { term: arg, ...flags },
|
|
596
|
-
lines: { file: relativePath, range: arg },
|
|
597
|
-
typedef: { name: arg, file: relativePath, ...flags },
|
|
598
|
-
api: { file: relativePath, limit: flags.limit },
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
const { ok, result, error, note } = execute(index, canonical, paramsByCommand[canonical]);
|
|
602
|
-
if (!ok) fail(error);
|
|
603
|
-
if (note) console.error(note);
|
|
604
|
-
|
|
605
|
-
// Format output using same formatters as project mode
|
|
606
|
-
switch (canonical) {
|
|
607
|
-
case 'toc':
|
|
608
|
-
printOutput(result, output.formatTocJson, r => output.formatToc(r, {
|
|
609
|
-
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
|
|
610
|
-
uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
|
|
611
|
-
}));
|
|
612
|
-
break;
|
|
613
|
-
case 'find':
|
|
614
|
-
printOutput(result,
|
|
615
|
-
r => output.formatSymbolJson(r, arg),
|
|
616
|
-
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
|
|
617
|
-
);
|
|
618
|
-
break;
|
|
619
|
-
case 'fn':
|
|
620
|
-
printOutput(result, output.formatFnResultJson, output.formatFnResult);
|
|
621
|
-
break;
|
|
622
|
-
case 'class':
|
|
623
|
-
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
624
|
-
break;
|
|
625
|
-
case 'lines':
|
|
626
|
-
printOutput(result, output.formatLinesJson, r => output.formatLines(r));
|
|
627
|
-
break;
|
|
628
|
-
case 'usages':
|
|
629
|
-
printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
|
|
630
|
-
break;
|
|
631
|
-
case 'search':
|
|
632
|
-
if (result && result.meta && result.meta.mode === 'structural') {
|
|
633
|
-
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
634
|
-
} else {
|
|
635
|
-
printOutput(result, r => output.formatSearchJson(r, arg), r => output.formatSearch(r, arg));
|
|
636
|
-
}
|
|
637
|
-
break;
|
|
638
|
-
case 'typedef':
|
|
639
|
-
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
640
|
-
break;
|
|
641
|
-
case 'api': {
|
|
642
|
-
const apiFile = relativePath;
|
|
643
|
-
printOutput(result, r => output.formatApiJson(r, apiFile), r => output.formatApi(r, apiFile));
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
655
|
+
const scopedFlags = { ...flags };
|
|
656
|
+
if (['show', 'find', 'usages', 'source', 'api'].includes(canonical) && !scopedFlags.file) {
|
|
657
|
+
scopedFlags.file = relativePath;
|
|
646
658
|
}
|
|
659
|
+
if (canonical === 'source' && /^\d+(?:-\d+)?$/.test(String(arg || ''))) {
|
|
660
|
+
scopedFlags.range = arg;
|
|
661
|
+
arg = undefined;
|
|
662
|
+
}
|
|
663
|
+
warnInapplicableFlags(canonical, scopedFlags, (message) => console.error(message));
|
|
664
|
+
const params = buildPublicParams(canonical, arg, scopedFlags);
|
|
665
|
+
const execution = execute(index, canonical, params);
|
|
666
|
+
const { ok, result, error } = execution;
|
|
667
|
+
if (!ok) fail(formatSurfaceMessage(error, 'cli'));
|
|
668
|
+
console.log(flags.json
|
|
669
|
+
? output.formatPublicJson(canonical, result, params, {
|
|
670
|
+
...execution, surface: 'cli',
|
|
671
|
+
})
|
|
672
|
+
: formatCliText(canonical, result, params, execution, scopedFlags));
|
|
647
673
|
}
|
|
648
674
|
|
|
649
675
|
// ============================================================================
|
|
@@ -662,12 +688,9 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
662
688
|
|
|
663
689
|
// Clear cache if requested
|
|
664
690
|
if (flags.clearCache) {
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
if (!flags.quiet) {
|
|
669
|
-
console.error('Cache cleared');
|
|
670
|
-
}
|
|
691
|
+
const removed = index.clearCache();
|
|
692
|
+
if (removed.length > 0 && !flags.quiet) {
|
|
693
|
+
console.error('Cache cleared');
|
|
671
694
|
}
|
|
672
695
|
}
|
|
673
696
|
|
|
@@ -676,8 +699,8 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
676
699
|
let cacheWasLoaded = false;
|
|
677
700
|
if (flags.cache && !flags.clearCache) {
|
|
678
701
|
const loaded = index.loadCache();
|
|
679
|
-
|
|
680
|
-
|
|
702
|
+
cacheWasLoaded = !!loaded;
|
|
703
|
+
if (loaded && !flags.maxFiles) {
|
|
681
704
|
if (!index.isCacheStale()) {
|
|
682
705
|
usedCache = true;
|
|
683
706
|
if (!flags.quiet) {
|
|
@@ -693,516 +716,39 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
693
716
|
if (!usedCache) {
|
|
694
717
|
index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers });
|
|
695
718
|
needsCacheSave = flags.cache;
|
|
696
|
-
// Clear stale expand cache — line ranges may have shifted after rebuild
|
|
697
|
-
try {
|
|
698
|
-
const expandPath = path.join(index.root, '.ucn-cache', 'expandable.json');
|
|
699
|
-
if (fs.existsSync(expandPath)) fs.unlinkSync(expandPath);
|
|
700
|
-
} catch (_) { /* best-effort */ }
|
|
701
719
|
}
|
|
702
720
|
|
|
703
721
|
try {
|
|
704
|
-
// Resolve CLI
|
|
722
|
+
// Resolve CLI spelling to the canonical command ID.
|
|
705
723
|
const canonical = resolveCommand(command, 'cli') || command;
|
|
724
|
+
activeCanonicalCommand = canonical;
|
|
706
725
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (applicableFlags) {
|
|
710
|
-
// Map from camelCase flag name to CLI flag string
|
|
711
|
-
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
712
|
-
// Flags that are global (not command-specific) — skip warning for these
|
|
713
|
-
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
|
|
714
|
-
for (const [key, value] of Object.entries(flags)) {
|
|
715
|
-
if (globalFlags.has(key)) continue;
|
|
716
|
-
// Skip unset values (undefined, null, 0, empty array) — but NOT false (explicit negation)
|
|
717
|
-
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
718
|
-
// Skip --file when it was injected by file-mode routing, not user input
|
|
719
|
-
if (key === 'file' && flags._fileFromFileMode) continue;
|
|
720
|
-
if (!applicableFlags.includes(key)) {
|
|
721
|
-
console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// Tiered-output contract notes (shared with interactive mode).
|
|
725
|
-
printTieredNoOpNotes(canonical, flags, (m) => console.error(m));
|
|
726
|
+
if (!isPublicCommand(canonical)) {
|
|
727
|
+
fail(unknownCommandMessage(command));
|
|
726
728
|
}
|
|
729
|
+
warnInapplicableFlags(canonical, flags, (message) => console.error(message));
|
|
727
730
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
|
|
737
|
-
uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
|
|
738
|
-
}));
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
case 'find': {
|
|
743
|
-
const { ok, result, error, note } = execute(index, 'find', { name: arg, ...flags });
|
|
744
|
-
if (!ok) fail(error);
|
|
745
|
-
if (note) console.error(note);
|
|
746
|
-
printOutput(result,
|
|
747
|
-
r => output.formatSymbolJson(r, arg),
|
|
748
|
-
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all, compact: flags.compact })
|
|
749
|
-
);
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
case 'usages': {
|
|
754
|
-
const { ok, result, error, note } = execute(index, 'usages', { name: arg, ...flags });
|
|
755
|
-
if (!ok) fail(error);
|
|
756
|
-
if (note) console.error(note);
|
|
757
|
-
const displayName = nameForDisplay(arg);
|
|
758
|
-
printOutput(result,
|
|
759
|
-
r => output.formatUsagesJson(r, displayName),
|
|
760
|
-
r => output.formatUsages(r, displayName, { compact: flags.compact })
|
|
761
|
-
);
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
case 'example': {
|
|
766
|
-
const { ok, result, error, note } = execute(index, 'example', {
|
|
767
|
-
name: arg,
|
|
768
|
-
file: flags.file,
|
|
769
|
-
className: flags.className,
|
|
770
|
-
diverse: flags.diverse,
|
|
771
|
-
top: flags.top || undefined,
|
|
772
|
-
includeTests: flags.includeTests,
|
|
773
|
-
});
|
|
774
|
-
if (!ok) fail(error);
|
|
775
|
-
if (note) console.error(note);
|
|
776
|
-
const displayName = nameForDisplay(arg);
|
|
777
|
-
printOutput(result,
|
|
778
|
-
r => output.formatExampleJson(r, displayName),
|
|
779
|
-
r => output.formatExample(r, displayName)
|
|
780
|
-
);
|
|
781
|
-
break;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
case 'context': {
|
|
785
|
-
const { ok, result: ctx, error, note } = execute(index, 'context', { name: arg, ...flags });
|
|
786
|
-
if (!ok) fail(error);
|
|
787
|
-
if (flags.json) {
|
|
788
|
-
console.log(output.formatContextJson(ctx));
|
|
789
|
-
} else {
|
|
790
|
-
const { text, expandable } = output.formatContext(ctx, {
|
|
791
|
-
expandHint: 'Use "expand <N>" or --expand to see code for items',
|
|
792
|
-
showConfidence: flags.showConfidence !== false,
|
|
793
|
-
compact: !!flags.compact,
|
|
794
|
-
});
|
|
795
|
-
console.log(text);
|
|
796
|
-
|
|
797
|
-
// Inline expansion of callees when --expand flag is set
|
|
798
|
-
if (flags.expand) {
|
|
799
|
-
printInlineExpand(ctx, index.root);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Save expandable items to cache for 'expand' command
|
|
803
|
-
saveExpandableItems(expandable, index.root);
|
|
804
|
-
if (note) console.error(note);
|
|
805
|
-
}
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
case 'expand': {
|
|
810
|
-
requireArg(arg, 'Usage: ucn . expand <N>\nFirst run "ucn . context <name>" to get numbered items');
|
|
811
|
-
// Whole-token integers only (fix #248: parseInt truncation made
|
|
812
|
-
// `expand 1abc` and `expand 2.5` silently expand items 1 and 2).
|
|
813
|
-
if (!/^\d+$/.test(String(arg).trim())) {
|
|
814
|
-
fail(`Invalid item number: "${arg}"`);
|
|
815
|
-
}
|
|
816
|
-
const expandNum = parseInt(arg, 10);
|
|
817
|
-
const cached = loadExpandableItems(index.root);
|
|
818
|
-
const items = cached?.items || [];
|
|
819
|
-
const match = items.find(i => i.num === expandNum);
|
|
820
|
-
const { ok, result, error } = execute(index, 'expand', {
|
|
821
|
-
match, itemNum: expandNum, itemCount: items.length, validateRoot: true
|
|
822
|
-
});
|
|
823
|
-
if (!ok) fail(error);
|
|
824
|
-
if (flags.json) {
|
|
825
|
-
// Honor --json: structured output with the expanded code + metadata.
|
|
826
|
-
const env = {
|
|
827
|
-
meta: { command: 'expand', item: expandNum },
|
|
828
|
-
data: {
|
|
829
|
-
item: expandNum,
|
|
830
|
-
...(match && {
|
|
831
|
-
name: match.name,
|
|
832
|
-
type: match.type,
|
|
833
|
-
file: match.relativePath || match.file,
|
|
834
|
-
startLine: match.startLine,
|
|
835
|
-
endLine: match.endLine,
|
|
836
|
-
handle: match.relativePath && match.startLine && match.name
|
|
837
|
-
? `${match.relativePath}:${match.startLine}:${match.name}`
|
|
838
|
-
: null,
|
|
839
|
-
}),
|
|
840
|
-
text: result.text,
|
|
841
|
-
},
|
|
842
|
-
};
|
|
843
|
-
console.log(JSON.stringify(env, null, 2));
|
|
844
|
-
} else {
|
|
845
|
-
console.log(result.text);
|
|
846
|
-
}
|
|
847
|
-
break;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
case 'smart': {
|
|
851
|
-
const { ok, result, error, note } = execute(index, 'smart', { name: arg, ...flags });
|
|
852
|
-
if (!ok) fail(error);
|
|
853
|
-
printOutput(result, output.formatSmartJson, r => output.formatSmart(r, {
|
|
854
|
-
uncertainHint: 'unverified callees are listed below with reasons'
|
|
855
|
-
}));
|
|
856
|
-
if (note) console.error(note);
|
|
857
|
-
break;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
case 'about': {
|
|
861
|
-
const { ok, result, error, note } = execute(index, 'about', { name: arg, ...flags });
|
|
862
|
-
if (!ok) fail(error);
|
|
863
|
-
printOutput(result,
|
|
864
|
-
output.formatAboutJson,
|
|
865
|
-
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact, git: !!flags.git })
|
|
866
|
-
);
|
|
867
|
-
if (note) console.error(note);
|
|
868
|
-
break;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
case 'impact': {
|
|
872
|
-
const { ok, result, error, note } = execute(index, 'impact', { name: arg, ...flags });
|
|
873
|
-
if (!ok) fail(error);
|
|
874
|
-
printOutput(result, output.formatImpactJson, r => output.formatImpact(r, { compact: flags.compact }));
|
|
875
|
-
if (note) console.error(note);
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
case 'blast': {
|
|
880
|
-
const { ok, result, error, note } = execute(index, 'blast', { name: arg, ...flags });
|
|
881
|
-
if (!ok) fail(error);
|
|
882
|
-
printOutput(result, output.formatBlastJson, output.formatBlast);
|
|
883
|
-
if (note) console.error(note);
|
|
884
|
-
break;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
case 'plan': {
|
|
888
|
-
const { ok, result, error } = execute(index, 'plan', { name: arg, ...flags });
|
|
889
|
-
if (!ok) fail(error);
|
|
890
|
-
printOutput(result, output.formatPlanJson, output.formatPlan);
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
case 'trace': {
|
|
895
|
-
const { ok, result, error, note } = execute(index, 'trace', { name: arg, ...flags });
|
|
896
|
-
if (!ok) fail(error);
|
|
897
|
-
printOutput(result, output.formatTraceJson, output.formatTrace);
|
|
898
|
-
if (note) console.error(note);
|
|
899
|
-
break;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
case 'reverseTrace': {
|
|
903
|
-
const { ok, result, error, note } = execute(index, 'reverseTrace', { name: arg, ...flags });
|
|
904
|
-
if (!ok) fail(error);
|
|
905
|
-
printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
|
|
906
|
-
if (note) console.error(note);
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
case 'stacktrace': {
|
|
911
|
-
const { ok, result, error } = execute(index, 'stacktrace', { stack: flags.stack || arg });
|
|
912
|
-
if (!ok) fail(error);
|
|
913
|
-
printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
case 'verify': {
|
|
918
|
-
const { ok, result, error } = execute(index, 'verify', { name: arg, ...flags });
|
|
919
|
-
if (!ok) fail(error);
|
|
920
|
-
printOutput(result, output.formatVerifyJson, output.formatVerify);
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
case 'related': {
|
|
925
|
-
const { ok, result, error, note } = execute(index, 'related', { name: arg, ...flags });
|
|
926
|
-
if (!ok) fail(error);
|
|
927
|
-
printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { all: flags.all, top: flags.top }));
|
|
928
|
-
if (note) console.error(note);
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
case 'brief': {
|
|
933
|
-
requireArg(arg, 'Usage: ucn . brief <name>');
|
|
934
|
-
const { ok, result, error } = execute(index, 'brief', { name: arg, file: flags.file, className: flags.className, line: flags.line, git: flags.git });
|
|
935
|
-
if (!ok) fail(error);
|
|
936
|
-
printOutput(result, output.formatBriefJson, output.formatBrief);
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
case 'doctor': {
|
|
941
|
-
const { ok, result, error } = execute(index, 'doctor', {
|
|
942
|
-
file: flags.file, in: flags.in,
|
|
943
|
-
limit: flags.limit, deep: flags.deep,
|
|
944
|
-
});
|
|
945
|
-
if (!ok) fail(error);
|
|
946
|
-
printOutput(result, output.formatDoctorJson, output.formatDoctor);
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
case 'orient': {
|
|
951
|
-
const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
|
|
952
|
-
const { ok, result, error } = execute(index, 'orient', { top: topVal });
|
|
953
|
-
if (!ok) fail(error);
|
|
954
|
-
printOutput(result, output.formatOrientJson, output.formatOrient);
|
|
955
|
-
break;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
case 'check': {
|
|
959
|
-
const { ok, result, error } = execute(index, 'check', {
|
|
960
|
-
base: flags.base, staged: flags.staged,
|
|
961
|
-
file: flags.file, limit: flags.limit,
|
|
962
|
-
});
|
|
963
|
-
if (!ok) fail(error);
|
|
964
|
-
printOutput(result, output.formatCheckJson, output.formatCheck);
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// ── Extraction commands (via execute) ────────────────────────────
|
|
969
|
-
|
|
970
|
-
case 'fn': {
|
|
971
|
-
requireArg(arg, 'Usage: ucn . fn <name>');
|
|
972
|
-
const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all, className: flags.className, line: flags.line });
|
|
973
|
-
if (!ok) fail(error);
|
|
974
|
-
if (note) console.error(note);
|
|
975
|
-
printOutput(result, output.formatFnResultJson, output.formatFnResult);
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
case 'class': {
|
|
980
|
-
requireArg(arg, 'Usage: ucn . class <name>');
|
|
981
|
-
const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines, line: flags.line });
|
|
982
|
-
if (!ok) fail(error);
|
|
983
|
-
if (note) console.error(note);
|
|
984
|
-
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
985
|
-
break;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
case 'lines': {
|
|
989
|
-
requireArg(arg, 'Usage: ucn . lines <range> --file <path> (or: lines <file> <range>)');
|
|
990
|
-
const linesTarget = parseLinesTarget(arg, flags.file);
|
|
991
|
-
const { ok, result, error, note } = execute(index, 'lines', { file: linesTarget.file, range: linesTarget.range });
|
|
992
|
-
if (!ok) fail(error);
|
|
993
|
-
if (note) console.error(note);
|
|
994
|
-
printOutput(result, output.formatLinesJson, r => output.formatLines(r));
|
|
995
|
-
break;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// ── File dependency commands ────────────────────────────────────
|
|
999
|
-
|
|
1000
|
-
case 'imports': {
|
|
1001
|
-
const filePath = arg || flags.file;
|
|
1002
|
-
const { ok, result, error } = execute(index, 'imports', { file: filePath });
|
|
1003
|
-
if (!ok) fail(error);
|
|
1004
|
-
printOutput(result,
|
|
1005
|
-
r => output.formatImportsJson(r, filePath),
|
|
1006
|
-
r => output.formatImports(r, filePath)
|
|
1007
|
-
);
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
case 'exporters': {
|
|
1012
|
-
const filePath = arg || flags.file;
|
|
1013
|
-
const { ok, result, error } = execute(index, 'exporters', { file: filePath });
|
|
1014
|
-
if (!ok) fail(error);
|
|
1015
|
-
printOutput(result,
|
|
1016
|
-
r => output.formatExportersJson(r, filePath),
|
|
1017
|
-
r => output.formatExporters(r, filePath)
|
|
1018
|
-
);
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
case 'fileExports': {
|
|
1023
|
-
const filePath = arg || flags.file;
|
|
1024
|
-
const { ok, result, error } = execute(index, 'fileExports', { file: filePath });
|
|
1025
|
-
if (!ok) fail(error);
|
|
1026
|
-
printOutput(result,
|
|
1027
|
-
r => output.formatFileExportsJson(r, filePath),
|
|
1028
|
-
r => output.formatFileExports(r, filePath)
|
|
1029
|
-
);
|
|
1030
|
-
break;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
case 'graph': {
|
|
1034
|
-
const filePath = arg || flags.file;
|
|
1035
|
-
const { ok, result, error } = execute(index, 'graph', { file: filePath, direction: flags.direction, depth: flags.depth, all: flags.all });
|
|
1036
|
-
if (!ok) fail(error);
|
|
1037
|
-
printOutput(result,
|
|
1038
|
-
output.formatGraphJson,
|
|
1039
|
-
r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: filePath })
|
|
1040
|
-
);
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
case 'circularDeps': {
|
|
1045
|
-
const { ok, result, error } = execute(index, 'circularDeps', { file: flags.file, exclude: flags.exclude });
|
|
1046
|
-
if (!ok) fail(error);
|
|
1047
|
-
printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// ── Remaining commands ──────────────────────────────────────────
|
|
1052
|
-
|
|
1053
|
-
case 'typedef': {
|
|
1054
|
-
const { ok, result, error } = execute(index, 'typedef', { name: arg, exact: flags.exact, file: flags.file, className: flags.className });
|
|
1055
|
-
if (!ok) fail(error);
|
|
1056
|
-
printOutput(result,
|
|
1057
|
-
r => output.formatTypedefJson(r, arg),
|
|
1058
|
-
r => output.formatTypedef(r, arg)
|
|
1059
|
-
);
|
|
1060
|
-
break;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
case 'tests': {
|
|
1064
|
-
const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className, file: flags.file, exclude: flags.exclude });
|
|
1065
|
-
if (!ok) fail(error);
|
|
1066
|
-
const displayName = nameForDisplay(arg);
|
|
1067
|
-
printOutput(result,
|
|
1068
|
-
r => output.formatTestsJson(r, displayName),
|
|
1069
|
-
r => output.formatTests(r, displayName)
|
|
1070
|
-
);
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
case 'affectedTests': {
|
|
1075
|
-
const { ok, result, error, note } = execute(index, 'affectedTests', { name: arg, ...flags });
|
|
1076
|
-
if (!ok) fail(error);
|
|
1077
|
-
printOutput(result, output.formatAffectedTestsJson, r => output.formatAffectedTests(r, { all: flags.all }));
|
|
1078
|
-
if (note) console.error(note);
|
|
1079
|
-
break;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
case 'api': {
|
|
1083
|
-
const filePath = arg || flags.file;
|
|
1084
|
-
const { ok, result, error, note } = execute(index, 'api', { file: filePath, limit: flags.limit });
|
|
1085
|
-
if (!ok) fail(error);
|
|
1086
|
-
if (note) console.error(note);
|
|
1087
|
-
printOutput(result,
|
|
1088
|
-
r => output.formatApiJson(r, filePath),
|
|
1089
|
-
r => output.formatApi(r, filePath)
|
|
1090
|
-
);
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
case 'search': {
|
|
1095
|
-
const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...flags });
|
|
1096
|
-
if (!ok) fail(error);
|
|
1097
|
-
if (structural) {
|
|
1098
|
-
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
1099
|
-
} else {
|
|
1100
|
-
printOutput(result,
|
|
1101
|
-
r => output.formatSearchJson(r, arg),
|
|
1102
|
-
r => output.formatSearch(r, arg)
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
case 'deadcode': {
|
|
1109
|
-
const { ok, result, error, note } = execute(index, 'deadcode', { ...flags, in: flags.in || subdirScope });
|
|
1110
|
-
if (!ok) fail(error);
|
|
1111
|
-
if (note) console.error(note);
|
|
1112
|
-
printOutput(result,
|
|
1113
|
-
output.formatDeadcodeJson,
|
|
1114
|
-
r => output.formatDeadcode(r, {
|
|
1115
|
-
top: flags.top,
|
|
1116
|
-
decoratedHint: !flags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
1117
|
-
exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
|
|
1118
|
-
externalContractHint: !flags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
|
|
1119
|
-
})
|
|
1120
|
-
);
|
|
1121
|
-
break;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
case 'entrypoints': {
|
|
1125
|
-
const { ok, result, error, note } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file, exclude: flags.exclude, includeTests: flags.includeTests, excludeTests: flags.excludeTests, limit: flags.limit });
|
|
1126
|
-
if (!ok) fail(error);
|
|
1127
|
-
if (note) console.error(note);
|
|
1128
|
-
printOutput(result,
|
|
1129
|
-
output.formatEntrypointsJson,
|
|
1130
|
-
r => output.formatEntrypoints(r)
|
|
1131
|
-
);
|
|
1132
|
-
break;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
case 'endpoints': {
|
|
1136
|
-
const { ok, result, error, note } = execute(index, 'endpoints', {
|
|
1137
|
-
file: flags.file,
|
|
1138
|
-
exclude: flags.exclude,
|
|
1139
|
-
limit: flags.limit,
|
|
1140
|
-
framework: flags.framework,
|
|
1141
|
-
bridge: flags.bridge,
|
|
1142
|
-
serverOnly: flags.serverOnly,
|
|
1143
|
-
clientOnly: flags.clientOnly,
|
|
1144
|
-
unmatched: flags.unmatched,
|
|
1145
|
-
method: flags.method,
|
|
1146
|
-
prefix: flags.prefix,
|
|
1147
|
-
hideUncertain: flags.hideUncertain,
|
|
1148
|
-
});
|
|
1149
|
-
if (!ok) fail(error);
|
|
1150
|
-
if (note) console.error(note);
|
|
1151
|
-
printOutput(result,
|
|
1152
|
-
output.formatEndpointsJson,
|
|
1153
|
-
r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched })
|
|
1154
|
-
);
|
|
1155
|
-
break;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
case 'stats': {
|
|
1159
|
-
// MEDIUM-7: pass the raw --top value when present so the executor
|
|
1160
|
-
// can validate it and surface "Invalid --top" errors. Without
|
|
1161
|
-
// this, --top=abc is silently coerced to NaN → undefined and
|
|
1162
|
-
// the user gets the default (10) with no warning.
|
|
1163
|
-
const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
|
|
1164
|
-
const { ok, result, error, note } = execute(index, 'stats', {
|
|
1165
|
-
functions: flags.functions,
|
|
1166
|
-
hot: flags.hot,
|
|
1167
|
-
top: topVal,
|
|
1168
|
-
});
|
|
1169
|
-
if (!ok) fail(error);
|
|
1170
|
-
if (note) console.error(note);
|
|
1171
|
-
printOutput(result,
|
|
1172
|
-
output.formatStatsJson,
|
|
1173
|
-
r => output.formatStats(r, { top: flags.top })
|
|
1174
|
-
);
|
|
1175
|
-
break;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
case 'diffImpact': {
|
|
1179
|
-
const { ok, result, error, note } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file, limit: flags.limit, all: flags.all });
|
|
1180
|
-
if (!ok) fail(error);
|
|
1181
|
-
if (note) console.error(note);
|
|
1182
|
-
printOutput(result, output.formatDiffImpactJson, r => output.formatDiffImpact(r, { all: flags.all }));
|
|
1183
|
-
break;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
case 'auditAsync': {
|
|
1187
|
-
const { ok, result, error, note } = execute(index, 'auditAsync', {
|
|
1188
|
-
file: flags.file,
|
|
1189
|
-
exclude: flags.exclude,
|
|
1190
|
-
limit: flags.limit,
|
|
1191
|
-
});
|
|
1192
|
-
if (!ok) fail(error);
|
|
1193
|
-
if (note) console.error(note);
|
|
1194
|
-
printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
|
|
1195
|
-
break;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
default:
|
|
1199
|
-
console.error(`Unknown command: ${canonical}`);
|
|
1200
|
-
printUsage();
|
|
1201
|
-
throw new CommandError();
|
|
731
|
+
// Public commands share one argument builder, executor, and formatter.
|
|
732
|
+
const publicParams = buildPublicParams(canonical, arg, {
|
|
733
|
+
...flags,
|
|
734
|
+
...(subdirScope && !flags.in ? { in: subdirScope } : {}),
|
|
735
|
+
});
|
|
736
|
+
const publicExecution = execute(index, canonical, publicParams);
|
|
737
|
+
if (!publicExecution.ok) {
|
|
738
|
+
fail(formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
1202
739
|
}
|
|
740
|
+
console.log(flags.json
|
|
741
|
+
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
742
|
+
...publicExecution, surface: 'cli',
|
|
743
|
+
})
|
|
744
|
+
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
|
|
745
|
+
// A gate that could not run (check outside git / bad base ref) must not
|
|
746
|
+
// exit 0 — CI gating on the exit code would read "could not run" as "passed".
|
|
747
|
+
process.exitCode = Math.max(process.exitCode || 0,
|
|
748
|
+
resultExitCode(canonical, publicExecution.result));
|
|
1203
749
|
} catch (e) {
|
|
1204
750
|
if (!(e instanceof CommandError)) {
|
|
1205
|
-
|
|
751
|
+
emitCliError(`Error: ${e.message}`);
|
|
1206
752
|
}
|
|
1207
753
|
process.exitCode = 1;
|
|
1208
754
|
} finally {
|
|
@@ -1211,57 +757,12 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
1211
757
|
// On cache-hit runs, only re-save if callsCache was mutated OR
|
|
1212
758
|
// reachability was computed (MED-1: persists the BFS result so
|
|
1213
759
|
// subsequent cold invocations don't repeat the 7-11s tax).
|
|
1214
|
-
if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty)) {
|
|
760
|
+
if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
|
|
1215
761
|
try { index.saveCache(); } catch (e) { /* best-effort */ }
|
|
1216
762
|
}
|
|
1217
763
|
}
|
|
1218
764
|
}
|
|
1219
765
|
|
|
1220
|
-
// extractFunctionFromProject and extractClassFromProject removed —
|
|
1221
|
-
// all surfaces now use execute(index, 'fn'/'class', params) from core/execute.js
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Save expandable items to cache file
|
|
1226
|
-
*/
|
|
1227
|
-
function saveExpandableItems(items, root) {
|
|
1228
|
-
try {
|
|
1229
|
-
const cacheDir = path.join(root || '.', '.ucn-cache');
|
|
1230
|
-
if (!fs.existsSync(cacheDir)) {
|
|
1231
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1232
|
-
}
|
|
1233
|
-
fs.writeFileSync(
|
|
1234
|
-
path.join(cacheDir, 'expandable.json'),
|
|
1235
|
-
JSON.stringify({ items, root, timestamp: Date.now() }, null, 2)
|
|
1236
|
-
);
|
|
1237
|
-
} catch (e) {
|
|
1238
|
-
// Silently fail - expand feature is optional
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* Load expandable items from cache
|
|
1244
|
-
*/
|
|
1245
|
-
function loadExpandableItems(root) {
|
|
1246
|
-
try {
|
|
1247
|
-
const cachePath = path.join(root || '.', '.ucn-cache', 'expandable.json');
|
|
1248
|
-
if (fs.existsSync(cachePath)) {
|
|
1249
|
-
return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
1250
|
-
}
|
|
1251
|
-
} catch (e) {
|
|
1252
|
-
// Return null on error
|
|
1253
|
-
}
|
|
1254
|
-
return null;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/**
|
|
1258
|
-
* Print expanded code for a cached item
|
|
1259
|
-
*/
|
|
1260
|
-
// printExpandedItem removed — all surfaces now use execute(index, 'expand', ...)
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
766
|
// ============================================================================
|
|
1266
767
|
// GLOB MODE
|
|
1267
768
|
// ============================================================================
|
|
@@ -1270,11 +771,11 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
1270
771
|
const files = expandGlob(pattern);
|
|
1271
772
|
|
|
1272
773
|
if (files.length === 0) {
|
|
1273
|
-
|
|
1274
|
-
process.exit(1);
|
|
774
|
+
fail(`No files match pattern: ${pattern}`, command);
|
|
1275
775
|
}
|
|
1276
776
|
|
|
1277
777
|
const canonical = resolveCommand(command, 'cli') || command;
|
|
778
|
+
activeCanonicalCommand = canonical;
|
|
1278
779
|
|
|
1279
780
|
// Build a temporary index over the matched files and route through execute().
|
|
1280
781
|
// This gives glob mode the same semantics as project mode: test exclusions,
|
|
@@ -1283,223 +784,22 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
1283
784
|
const index = new ProjectIndex(rootDir);
|
|
1284
785
|
index.build(files, { quiet: true });
|
|
1285
786
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
// and interactive-only commands are excluded.
|
|
1289
|
-
const unsupportedGlobCommands = new Set(['expand']);
|
|
1290
|
-
if (unsupportedGlobCommands.has(canonical)) {
|
|
1291
|
-
console.error(`Command "${command}" not supported in glob mode.`);
|
|
1292
|
-
process.exit(1);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Build params — same as project mode
|
|
1296
|
-
const params = {};
|
|
1297
|
-
const needsName = new Set(['find', 'usages', 'fn', 'class', 'typedef', 'about', 'context',
|
|
1298
|
-
'smart', 'impact', 'trace', 'blast', 'reverseTrace', 'tests', 'affectedTests',
|
|
1299
|
-
'example', 'verify', 'plan', 'related']);
|
|
1300
|
-
if (needsName.has(canonical)) {
|
|
1301
|
-
if (!arg) {
|
|
1302
|
-
console.error(`Usage: ucn "pattern" ${command} <name>`);
|
|
1303
|
-
process.exit(1);
|
|
1304
|
-
}
|
|
1305
|
-
params.name = arg;
|
|
1306
|
-
}
|
|
1307
|
-
if (canonical === 'search' || canonical === 'structuralSearch') {
|
|
1308
|
-
if (!arg && !flags.type) {
|
|
1309
|
-
console.error('Usage: ucn "pattern" search <term>');
|
|
1310
|
-
process.exit(1);
|
|
1311
|
-
}
|
|
1312
|
-
params.term = arg;
|
|
1313
|
-
}
|
|
1314
|
-
// Merge flags first, then set positional overrides so they aren't wiped
|
|
1315
|
-
Object.assign(params, flags);
|
|
1316
|
-
|
|
1317
|
-
// Warn about inapplicable flags (same check as project/interactive mode)
|
|
1318
|
-
const applicableFlags = FLAG_APPLICABILITY[canonical];
|
|
1319
|
-
if (applicableFlags) {
|
|
1320
|
-
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
1321
|
-
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
|
|
1322
|
-
for (const [key, value] of Object.entries(flags)) {
|
|
1323
|
-
if (globalFlags.has(key)) continue;
|
|
1324
|
-
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
1325
|
-
if (!applicableFlags.includes(key)) {
|
|
1326
|
-
console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
if (canonical === 'stacktrace' && arg) {
|
|
1331
|
-
params.stack = arg;
|
|
787
|
+
if (!isPublicCommand(canonical)) {
|
|
788
|
+
fail(unknownCommandMessage(command));
|
|
1332
789
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
const { ok, result, error, note, structural } = execute(index, canonical, params);
|
|
1341
|
-
if (!ok) fail(error);
|
|
1342
|
-
if (note) console.error(note);
|
|
1343
|
-
|
|
1344
|
-
// Format output — same formatters as project mode
|
|
1345
|
-
switch (canonical) {
|
|
1346
|
-
case 'toc':
|
|
1347
|
-
printOutput(result, output.formatTocJson, r => output.formatToc(r, {
|
|
1348
|
-
detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
|
|
1349
|
-
}));
|
|
1350
|
-
break;
|
|
1351
|
-
case 'find':
|
|
1352
|
-
printOutput(result,
|
|
1353
|
-
r => output.formatSymbolJson(r, arg),
|
|
1354
|
-
r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
|
|
1355
|
-
);
|
|
1356
|
-
break;
|
|
1357
|
-
case 'search':
|
|
1358
|
-
if (structural) {
|
|
1359
|
-
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
1360
|
-
} else {
|
|
1361
|
-
printOutput(result,
|
|
1362
|
-
r => output.formatSearchJson(r, arg),
|
|
1363
|
-
r => output.formatSearch(r, arg)
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
break;
|
|
1367
|
-
case 'fn':
|
|
1368
|
-
printOutput(result, output.formatFnResultJson, output.formatFnResult);
|
|
1369
|
-
break;
|
|
1370
|
-
case 'class':
|
|
1371
|
-
printOutput(result, output.formatClassResultJson, output.formatClassResult);
|
|
1372
|
-
break;
|
|
1373
|
-
case 'usages':
|
|
1374
|
-
printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
|
|
1375
|
-
break;
|
|
1376
|
-
case 'deadcode':
|
|
1377
|
-
printOutput(result, output.formatDeadcodeJson, r => output.formatDeadcode(r, { top: flags.top }));
|
|
1378
|
-
break;
|
|
1379
|
-
case 'typedef':
|
|
1380
|
-
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
1381
|
-
break;
|
|
1382
|
-
case 'stats':
|
|
1383
|
-
printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
|
|
1384
|
-
break;
|
|
1385
|
-
case 'about':
|
|
1386
|
-
printOutput(result, output.formatAboutJson,
|
|
1387
|
-
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact }));
|
|
1388
|
-
break;
|
|
1389
|
-
case 'context':
|
|
1390
|
-
if (flags.json) {
|
|
1391
|
-
console.log(output.formatContextJson(result));
|
|
1392
|
-
} else {
|
|
1393
|
-
const { text } = output.formatContext(result, {
|
|
1394
|
-
expandHint: 'Use --expand to see inline callee previews',
|
|
1395
|
-
showConfidence: flags.showConfidence !== false,
|
|
1396
|
-
compact: !!flags.compact,
|
|
1397
|
-
});
|
|
1398
|
-
console.log(text);
|
|
1399
|
-
if (flags.expand) {
|
|
1400
|
-
printInlineExpand(result, index.root);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
break;
|
|
1404
|
-
case 'smart':
|
|
1405
|
-
printOutput(result, output.formatSmartJson, output.formatSmart);
|
|
1406
|
-
break;
|
|
1407
|
-
case 'impact':
|
|
1408
|
-
printOutput(result, output.formatImpactJson, output.formatImpact);
|
|
1409
|
-
break;
|
|
1410
|
-
case 'related':
|
|
1411
|
-
printOutput(result, output.formatRelatedJson,
|
|
1412
|
-
r => output.formatRelated(r, { all: flags.all, top: flags.top }));
|
|
1413
|
-
break;
|
|
1414
|
-
case 'brief':
|
|
1415
|
-
printOutput(result, output.formatBriefJson, output.formatBrief);
|
|
1416
|
-
break;
|
|
1417
|
-
case 'doctor':
|
|
1418
|
-
printOutput(result, output.formatDoctorJson, output.formatDoctor);
|
|
1419
|
-
break;
|
|
1420
|
-
case 'check':
|
|
1421
|
-
printOutput(result, output.formatCheckJson, output.formatCheck);
|
|
1422
|
-
break;
|
|
1423
|
-
case 'trace':
|
|
1424
|
-
printOutput(result, output.formatTraceJson, output.formatTrace);
|
|
1425
|
-
break;
|
|
1426
|
-
case 'blast':
|
|
1427
|
-
printOutput(result, output.formatBlastJson, output.formatBlast);
|
|
1428
|
-
break;
|
|
1429
|
-
case 'reverseTrace':
|
|
1430
|
-
printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
|
|
1431
|
-
break;
|
|
1432
|
-
case 'tests':
|
|
1433
|
-
printOutput(result, r => output.formatTestsJson(r, arg), r => output.formatTests(r, arg));
|
|
1434
|
-
break;
|
|
1435
|
-
case 'affectedTests':
|
|
1436
|
-
printOutput(result, output.formatAffectedTestsJson,
|
|
1437
|
-
r => output.formatAffectedTests(r, { all: flags.all }));
|
|
1438
|
-
break;
|
|
1439
|
-
case 'example':
|
|
1440
|
-
printOutput(result, r => output.formatExampleJson(r, arg), r => output.formatExample(r, arg));
|
|
1441
|
-
break;
|
|
1442
|
-
case 'verify':
|
|
1443
|
-
printOutput(result, output.formatVerifyJson, output.formatVerify);
|
|
1444
|
-
break;
|
|
1445
|
-
case 'plan':
|
|
1446
|
-
printOutput(result, output.formatPlanJson, output.formatPlan);
|
|
1447
|
-
break;
|
|
1448
|
-
case 'imports': {
|
|
1449
|
-
const filePath = params.file;
|
|
1450
|
-
printOutput(result, r => output.formatImportsJson(r, filePath), r => output.formatImports(r, filePath));
|
|
1451
|
-
break;
|
|
1452
|
-
}
|
|
1453
|
-
case 'exporters': {
|
|
1454
|
-
const filePath = params.file;
|
|
1455
|
-
printOutput(result, r => output.formatExportersJson(r, filePath), r => output.formatExporters(r, filePath));
|
|
1456
|
-
break;
|
|
1457
|
-
}
|
|
1458
|
-
case 'fileExports': {
|
|
1459
|
-
const filePath = params.file;
|
|
1460
|
-
printOutput(result, r => output.formatFileExportsJson(r, filePath), r => output.formatFileExports(r, filePath));
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
case 'api': {
|
|
1464
|
-
const filePath = params.file;
|
|
1465
|
-
printOutput(result, r => output.formatApiJson(r, filePath), r => output.formatApi(r, filePath));
|
|
1466
|
-
break;
|
|
1467
|
-
}
|
|
1468
|
-
case 'graph':
|
|
1469
|
-
printOutput(result, output.formatGraphJson,
|
|
1470
|
-
r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth }));
|
|
1471
|
-
break;
|
|
1472
|
-
case 'circularDeps':
|
|
1473
|
-
printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
|
|
1474
|
-
break;
|
|
1475
|
-
case 'entrypoints':
|
|
1476
|
-
printOutput(result, output.formatEntrypointsJson, output.formatEntrypoints);
|
|
1477
|
-
break;
|
|
1478
|
-
case 'endpoints':
|
|
1479
|
-
printOutput(result, output.formatEndpointsJson, r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }));
|
|
1480
|
-
break;
|
|
1481
|
-
case 'diffImpact':
|
|
1482
|
-
printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
|
|
1483
|
-
break;
|
|
1484
|
-
case 'auditAsync':
|
|
1485
|
-
printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
|
|
1486
|
-
break;
|
|
1487
|
-
case 'stacktrace':
|
|
1488
|
-
printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
|
|
1489
|
-
break;
|
|
1490
|
-
case 'lines':
|
|
1491
|
-
printOutput(result, output.formatLinesJson, output.formatLines);
|
|
1492
|
-
break;
|
|
1493
|
-
default: {
|
|
1494
|
-
// Fallback: output JSON for any command without a dedicated formatter
|
|
1495
|
-
if (flags.json) {
|
|
1496
|
-
console.log(JSON.stringify({ meta: {}, data: result }, null, 2));
|
|
1497
|
-
} else {
|
|
1498
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1499
|
-
}
|
|
1500
|
-
break;
|
|
1501
|
-
}
|
|
790
|
+
warnInapplicableFlags(canonical, flags, (message) => console.error(message));
|
|
791
|
+
const publicParams = buildPublicParams(canonical, arg, flags);
|
|
792
|
+
const publicExecution = execute(index, canonical, publicParams);
|
|
793
|
+
if (!publicExecution.ok) {
|
|
794
|
+
fail(formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
1502
795
|
}
|
|
796
|
+
console.log(flags.json
|
|
797
|
+
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
798
|
+
...publicExecution, surface: 'cli',
|
|
799
|
+
})
|
|
800
|
+
: formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
|
|
801
|
+
process.exitCode = Math.max(process.exitCode || 0,
|
|
802
|
+
resultExitCode(canonical, publicExecution.result));
|
|
1503
803
|
}
|
|
1504
804
|
|
|
1505
805
|
// ============================================================================
|
|
@@ -1510,153 +810,93 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
1510
810
|
// Single source of truth for the public CLI help. README points here ("Run `ucn --help`")
|
|
1511
811
|
// rather than carrying a copy — keep it that way.
|
|
1512
812
|
function printUsage() {
|
|
813
|
+
const perCommandFlags = [...getCliCommandSet()].map(command => {
|
|
814
|
+
const flags = getCliFlagsForCommand(command);
|
|
815
|
+
return ` ${command.padEnd(14)} ${flags.join(' ')}`;
|
|
816
|
+
}).join('\n');
|
|
1513
817
|
console.log(`UCN - Universal Code Navigator
|
|
1514
818
|
|
|
1515
|
-
Supported: JavaScript
|
|
819
|
+
Supported: JavaScript/TypeScript, Python, Go, Rust, Java, C, C++, C#, HTML
|
|
1516
820
|
|
|
1517
821
|
Usage:
|
|
1518
|
-
ucn [command] [args]
|
|
1519
|
-
ucn <file>
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
trace <
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
--all Show full results: all callers/callees + unverified (about/context), full tree (trace/blast),
|
|
1597
|
-
all names (related/find/fn/class/toc), all changed (diff-impact)
|
|
1598
|
-
--top=N Limit callers/callees (about), similar functions (related), search results
|
|
1599
|
-
--limit=N Limit result count (find, usages, search, deadcode, api, toc, entrypoints, diff-impact)
|
|
1600
|
-
--max-files=N Max files to index (large projects)
|
|
1601
|
-
--context=N Lines of context around matches (search, usages)
|
|
1602
|
-
--json Machine-readable output
|
|
1603
|
-
--compact Token-efficient about/context/impact output
|
|
1604
|
-
--code-only Filter out comments/strings (search, usages)
|
|
1605
|
-
--with-types Include type definitions (about, smart)
|
|
1606
|
-
--detailed Show all symbols in toc (not just counts)
|
|
1607
|
-
--include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
|
|
1608
|
-
--exclude-tests Exclude test files (entrypoints includes tests by default)
|
|
1609
|
-
--class-name=X Scope to specific class (e.g., --class-name=Repository)
|
|
1610
|
-
--include-methods Include method-call (obj.fn) callee expansion in trace/smart
|
|
1611
|
-
(no effect on caller-direction commands: about/context/impact/verify/
|
|
1612
|
-
blast/reverse-trace/affected-tests always tier method calls by evidence)
|
|
1613
|
-
--include-uncertain No effect on tiered commands; unverified candidates are always
|
|
1614
|
-
shown in their own section with reasons
|
|
1615
|
-
--expand-unverified Follow unverified caller edges in blast/reverse-trace trees
|
|
1616
|
-
(downstream nodes marked as possible, not confirmed, impact chains)
|
|
1617
|
-
--hide-confidence Hide confidence scores (shown by default in about, context)
|
|
1618
|
-
--min-confidence=N Filter low-confidence edges (about, context, blast, trace,
|
|
1619
|
-
reverse-trace, smart, affected-tests)
|
|
1620
|
-
--unreachable-only Show only callers/callees that are unreachable from entry points (about, context, impact)
|
|
1621
|
-
--include-exported Include exported symbols in deadcode
|
|
1622
|
-
--no-regex Force plain text search (regex is default)
|
|
1623
|
-
--functions Show per-function line counts (stats command)
|
|
1624
|
-
--hot Show top N most-called functions (stats command, pair with --top=N)
|
|
1625
|
-
--diverse Cluster call sites by argument shape (example command, pair with --top=N)
|
|
1626
|
-
--git Attach git enrichment (last modified, author, recent commits) to about/brief
|
|
1627
|
-
--include-decorated Include decorated/annotated symbols in deadcode
|
|
1628
|
-
--deep Add a stratified evidence profile to doctor (not measured accuracy)
|
|
1629
|
-
--framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
|
|
1630
|
-
--bridge Match server routes to client requests (endpoints command).
|
|
1631
|
-
Confidence tiers: EXACT, PARTIAL, UNCERTAIN
|
|
1632
|
-
--server-only Only list server routes (endpoints command)
|
|
1633
|
-
--client-only Only list client requests (endpoints command)
|
|
1634
|
-
--unmatched Only show routes/requests with no match (endpoints, pair with --bridge)
|
|
1635
|
-
--method=X Filter by HTTP method (endpoints, e.g., --method=POST)
|
|
1636
|
-
--prefix=X Filter routes/requests by path prefix (endpoints, e.g., --prefix=/api)
|
|
1637
|
-
--hide-uncertain Hide UNCERTAIN-confidence bridges (endpoints command)
|
|
1638
|
-
--exact Exact name match only (find, typedef)
|
|
1639
|
-
--calls-only Only show call/test-case matches (tests)
|
|
1640
|
-
--case-sensitive Case-sensitive text search (search)
|
|
1641
|
-
--top-level Show only top-level functions in toc
|
|
1642
|
-
--max-lines=N Max source lines for class (large classes show summary)
|
|
1643
|
-
--workers=N Parallel build workers (auto-detect; 0 to disable, env: UCN_WORKERS)
|
|
1644
|
-
--no-cache Disable caching
|
|
1645
|
-
--clear-cache Clear cache before running
|
|
1646
|
-
--base=<ref> Git ref for diff-impact (default: HEAD)
|
|
1647
|
-
--staged Analyze staged changes (diff-impact)
|
|
1648
|
-
--no-follow-symlinks Don't follow symbolic links
|
|
1649
|
-
--mcp Start the MCP stdio server
|
|
1650
|
-
-i, --interactive Keep index in memory for multiple queries
|
|
1651
|
-
-v, --version Print the UCN version and exit
|
|
1652
|
-
|
|
1653
|
-
Quick Start:
|
|
1654
|
-
ucn orient # First look at a new repo
|
|
1655
|
-
ucn toc # See project structure
|
|
1656
|
-
ucn about handleRequest # Understand a function
|
|
1657
|
-
ucn impact handleRequest # Before modifying
|
|
1658
|
-
ucn fn handleRequest --file api # Extract specific function
|
|
1659
|
-
ucn --interactive # Multiple queries`);
|
|
822
|
+
ucn [command] [args] Current project
|
|
823
|
+
ucn <file|dir|glob> <command> Explicit target
|
|
824
|
+
Add --json for a stable { meta, data } envelope.
|
|
825
|
+
|
|
826
|
+
Commands:
|
|
827
|
+
repo Repository overview
|
|
828
|
+
--sections=summary,files,stats,health Select repository sections
|
|
829
|
+
--deep Include deep readiness evidence
|
|
830
|
+
show <symbol> Symbol summary and relationships
|
|
831
|
+
--sections=summary,callers,callees,source,dependencies,tests,types,example,related
|
|
832
|
+
find <name> Definitions; --type=type, --with-source
|
|
833
|
+
usages <name> Calls, imports, definitions, references
|
|
834
|
+
search [term] Text or structural search
|
|
835
|
+
literal text by default; --regex enables regular-expression syntax
|
|
836
|
+
source <symbol|file:range> Exact function, class, or line extraction
|
|
837
|
+
trace <symbol> Call graph
|
|
838
|
+
--direction=callees|callers Downstream or upstream (default: callees)
|
|
839
|
+
--to=entrypoints Follow callers toward entry points
|
|
840
|
+
impact [symbol] Symbol impact; without symbol, Git-diff impact
|
|
841
|
+
tests <symbol> Direct tests; --depth=N adds transitive impact
|
|
842
|
+
deps <file> File graph; --direction=imports|importers|both
|
|
843
|
+
--detailed Include import declarations
|
|
844
|
+
deps --cycles Report circular dependencies (no file target)
|
|
845
|
+
api [file] Project or file public API
|
|
846
|
+
check [symbol] Signature check; without symbol, precommit check
|
|
847
|
+
plan <symbol> Preview rename or parameter edits
|
|
848
|
+
entrypoints Runtime and framework entry points
|
|
849
|
+
endpoints Server/client HTTP surface
|
|
850
|
+
deadcode Conservative unreachable-symbol candidates
|
|
851
|
+
audit-async Likely missing-await sites
|
|
852
|
+
stacktrace <text> Resolve runtime frames to source
|
|
853
|
+
|
|
854
|
+
Common flags:
|
|
855
|
+
--file=PATH --exclude=a,b --in=PATH --depth=N --top=N --limit=N
|
|
856
|
+
--all --compact --no-compact --json --include-tests --class-name=X --line=N
|
|
857
|
+
--range=N-M (source with --file=PATH)
|
|
858
|
+
--base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
|
|
859
|
+
--max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
|
|
860
|
+
Cache: per-user by default; set UCN_CACHE_DIR to override the cache root.
|
|
861
|
+
|
|
862
|
+
Accepted flags by command:
|
|
863
|
+
${perCommandFlags}
|
|
864
|
+
|
|
865
|
+
Global/build/output flags:
|
|
866
|
+
--help -h --version -v --mcp --json --verbose --no-quiet --quiet
|
|
867
|
+
--interactive -i --no-cache --clear-cache --no-follow-symlinks
|
|
868
|
+
--max-files=N --max-chars=N --workers=N
|
|
869
|
+
--clear-cache --all clears every bounded per-user UCN project cache.
|
|
870
|
+
|
|
871
|
+
Exit codes:
|
|
872
|
+
0 Command completed successfully; check found no blocking issues.
|
|
873
|
+
1 Findings, unsafe changes, validation failures, or invalid user input.
|
|
874
|
+
2 Command could not run (operational/environment failure).
|
|
875
|
+
|
|
876
|
+
Boolean aliases:
|
|
877
|
+
--no-include-methods --no-regex --show-confidence --hide-confidence
|
|
878
|
+
--no-confidence --hide-uncertain --no-uncertain --compact --no-compact
|
|
879
|
+
|
|
880
|
+
Value aliases:
|
|
881
|
+
--not=PATTERN (alias of --exclude) --default=VALUE (alias of --default-value)
|
|
882
|
+
|
|
883
|
+
Trust:
|
|
884
|
+
CONFIRMED means target-identity evidence exists. UNVERIFIED means possible and
|
|
885
|
+
requires review. ACCOUNT conserves observed text lines; it never proves full
|
|
886
|
+
runtime semantics or safe deletion.
|
|
887
|
+
|
|
888
|
+
UCN vs grep:
|
|
889
|
+
Use UCN for definitions, callers/callees, impact, tests, dependencies, APIs,
|
|
890
|
+
entry points, and audits. Use grep/ripgrep for simple literals, messages,
|
|
891
|
+
configuration, filenames, Markdown, and unsupported languages.
|
|
892
|
+
|
|
893
|
+
Quick start:
|
|
894
|
+
ucn repo
|
|
895
|
+
ucn show handleRequest
|
|
896
|
+
ucn trace handleRequest --direction=callers
|
|
897
|
+
ucn impact handleRequest
|
|
898
|
+
ucn tests handleRequest --depth=3
|
|
899
|
+
ucn source handleRequest`);
|
|
1660
900
|
}
|
|
1661
901
|
|
|
1662
902
|
// ============================================================================
|
|
@@ -1669,12 +909,18 @@ function runInteractive(rootDir) {
|
|
|
1669
909
|
|
|
1670
910
|
console.log('Building index...');
|
|
1671
911
|
const index = new ProjectIndex(rootDir);
|
|
912
|
+
if (flags.clearCache) {
|
|
913
|
+
const removed = index.clearCache();
|
|
914
|
+
if (removed.length > 0 && !flags.quiet) {
|
|
915
|
+
console.error('Cache cleared');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
1672
918
|
// Same cache discipline as one-shot mode (fix #250: the REPL fully
|
|
1673
919
|
// re-parsed every session and never consumed cache-persisted state —
|
|
1674
920
|
// the divergence mechanism behind the relocation P1).
|
|
1675
921
|
let iCacheFresh;
|
|
1676
|
-
if (flags.cache
|
|
1677
|
-
const loaded = index.loadCache();
|
|
922
|
+
if (flags.cache) {
|
|
923
|
+
const loaded = !flags.clearCache && index.loadCache();
|
|
1678
924
|
iCacheFresh = loaded && !index.isCacheStale();
|
|
1679
925
|
if (!iCacheFresh && loaded) {
|
|
1680
926
|
index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
|
|
@@ -1687,9 +933,8 @@ function runInteractive(rootDir) {
|
|
|
1687
933
|
} else {
|
|
1688
934
|
index.build(null, { quiet: true, workers: flags.workers });
|
|
1689
935
|
}
|
|
1690
|
-
const iExpandCache = new ExpandCache({ maxSize: 20 });
|
|
1691
936
|
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
|
|
1692
|
-
console.log('Type commands (e.g., "find parseFile", "
|
|
937
|
+
console.log('Type commands (e.g., "find parseFile", "show main", "repo")');
|
|
1693
938
|
console.log('Type "help" for commands, "quit" to exit\n');
|
|
1694
939
|
|
|
1695
940
|
const rl = readline.createInterface({
|
|
@@ -1716,50 +961,29 @@ function runInteractive(rootDir) {
|
|
|
1716
961
|
if (input === 'help') {
|
|
1717
962
|
console.log(`
|
|
1718
963
|
Commands:
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
about <name> Everything about a symbol
|
|
964
|
+
repo Repository overview (--sections=files,stats,health)
|
|
965
|
+
show <name> Symbol summary + relationships (--sections=...)
|
|
966
|
+
find <name> Find definitions (--type=type, --with-source)
|
|
1723
967
|
usages <name> All usages grouped by type
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
trace <name> Call tree (--depth=N)
|
|
1730
|
-
reverse-trace <name> Upward to entry points (--depth=N)
|
|
1731
|
-
example <name> Best usage example
|
|
1732
|
-
related <name> Sibling functions
|
|
1733
|
-
fn <name>[,n2,...] Extract function(s) (--file=)
|
|
1734
|
-
class <name> Extract class code (--file=)
|
|
1735
|
-
lines <range> Extract lines (--file= required)
|
|
1736
|
-
graph <file> File dependency tree (--direction=, --depth=)
|
|
1737
|
-
circular-deps Circular import chains (--file=, --exclude=)
|
|
1738
|
-
file-exports <file> File's exported symbols
|
|
1739
|
-
imports <file> What file imports
|
|
1740
|
-
exporters <file> Who imports file
|
|
1741
|
-
tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
|
|
1742
|
-
affected-tests <n> Tests affected by a change (--depth=N)
|
|
968
|
+
source <target> Extract a symbol or file:line-range
|
|
969
|
+
trace <name> Call tree (--direction=callees|callers, --to=entrypoints)
|
|
970
|
+
impact [name] Symbol impact, or Git diff impact without a name
|
|
971
|
+
tests <name> Direct tests; --depth=N includes transitive impact
|
|
972
|
+
deps <file> File graph (--direction=, --depth=, --detailed, --cycles)
|
|
1743
973
|
search <term> Text search (--context=N, --exclude=, --in=)
|
|
1744
974
|
Structural: --type= --param= --returns= --decorator= --exported --unused
|
|
1745
|
-
typedef <name> Find type definitions
|
|
1746
975
|
deadcode Unreferenced-symbol candidates (review before deletion)
|
|
1747
976
|
entrypoints Detect runtime, framework, task, and test entry points
|
|
1748
977
|
endpoints List server/client HTTP endpoints (--bridge to match)
|
|
1749
|
-
|
|
978
|
+
check [name] Symbol signature check, or pre-commit check without a name
|
|
1750
979
|
plan <name> Preview refactoring (--add-param=, --remove-param=, --rename-to=, --default-value=)
|
|
1751
|
-
check Pre-commit diff, signature, and affected-test checks
|
|
1752
980
|
stacktrace <text> Parse a stack trace
|
|
1753
981
|
api Show public symbols
|
|
1754
|
-
|
|
1755
|
-
stats Index statistics
|
|
1756
|
-
doctor Parse health, blind spots, command proofs, and task readiness
|
|
1757
|
-
orient Repository map and readiness summary
|
|
1758
|
-
audit-async Find likely missing-await calls (JS/TS/Python)
|
|
982
|
+
audit-async Find likely missing-await calls (JS/TS/Python/C#)
|
|
1759
983
|
rebuild Rebuild index
|
|
1760
984
|
quit Exit
|
|
1761
985
|
|
|
1762
|
-
Flags can be added per-command:
|
|
986
|
+
Flags can be added per-command: show myFunc --sections=source,callers
|
|
1763
987
|
`);
|
|
1764
988
|
rl.prompt();
|
|
1765
989
|
return;
|
|
@@ -1768,8 +992,6 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1768
992
|
if (input === 'rebuild') {
|
|
1769
993
|
console.log('Rebuilding index...');
|
|
1770
994
|
index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
|
|
1771
|
-
// Clear expand cache — stale line ranges after rebuild
|
|
1772
|
-
if (iExpandCache) iExpandCache.clearForRoot(index.root);
|
|
1773
995
|
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
|
|
1774
996
|
rl.prompt();
|
|
1775
997
|
return;
|
|
@@ -1779,7 +1001,7 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1779
1001
|
const tokens = input.split(/\s+/);
|
|
1780
1002
|
const command = tokens[0];
|
|
1781
1003
|
// Flags that take a space-separated value (--flag value)
|
|
1782
|
-
const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--line', '--framework', '--method', '--prefix']);
|
|
1004
|
+
const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--to', '--sections', '--range', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--max-chars', '--min-confidence', '--class-name', '--line', '--framework', '--method', '--prefix']);
|
|
1783
1005
|
const flagTokens = [];
|
|
1784
1006
|
const argTokens = [];
|
|
1785
1007
|
const skipNext = new Set();
|
|
@@ -1798,9 +1020,8 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1798
1020
|
}
|
|
1799
1021
|
const arg = argTokens.join(' ');
|
|
1800
1022
|
|
|
1801
|
-
// Unknown flags error instead of folding their
|
|
1802
|
-
//
|
|
1803
|
-
// "AddTask 5"). Same vocabulary as one-shot mode.
|
|
1023
|
+
// Unknown flags error instead of folding their value into the symbol
|
|
1024
|
+
// name. Same vocabulary as one-shot mode.
|
|
1804
1025
|
const unknown = flagTokens.filter(t =>
|
|
1805
1026
|
t.startsWith('--') && !knownFlags.has(t.split('=')[0]));
|
|
1806
1027
|
if (unknown.length > 0) {
|
|
@@ -1820,8 +1041,28 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1820
1041
|
// global CLI mode. MED-2/MED-3/MED-5: bad values are rejected with
|
|
1821
1042
|
// a helpful message instead of being silently coerced.
|
|
1822
1043
|
validateNumericFlags(iflags);
|
|
1044
|
+
// The REPL is an edit/query loop, not a frozen snapshot. Keep the
|
|
1045
|
+
// symbol table and call cache on the same generation by checking
|
|
1046
|
+
// source staleness before every command. getCachedCalls already
|
|
1047
|
+
// refreshes individual call records lazily; without this matching
|
|
1048
|
+
// rebuild a newly added definition was invisible while its calls
|
|
1049
|
+
// appeared in neighbouring answers (UCN5-044).
|
|
1050
|
+
if (index.isCacheStale()) {
|
|
1051
|
+
console.log('Source changed; rebuilding index...');
|
|
1052
|
+
index.build(null, {
|
|
1053
|
+
quiet: true,
|
|
1054
|
+
forceRebuild: true,
|
|
1055
|
+
followSymlinks: flags.followSymlinks,
|
|
1056
|
+
maxFiles: flags.maxFiles,
|
|
1057
|
+
workers: flags.workers,
|
|
1058
|
+
});
|
|
1059
|
+
if (flags.cache) {
|
|
1060
|
+
try { index.saveCache(); } catch (_) { /* best-effort */ }
|
|
1061
|
+
}
|
|
1062
|
+
console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
|
|
1063
|
+
}
|
|
1823
1064
|
const iCanonical = resolveCommand(command, 'cli') || command;
|
|
1824
|
-
executeInteractiveCommand(index, iCanonical, arg, iflags
|
|
1065
|
+
executeInteractiveCommand(index, iCanonical, arg, iflags);
|
|
1825
1066
|
} catch (e) {
|
|
1826
1067
|
if (e instanceof FlagValidationError) {
|
|
1827
1068
|
console.log(e.message);
|
|
@@ -1838,222 +1079,26 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1838
1079
|
});
|
|
1839
1080
|
}
|
|
1840
1081
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
// ── Data-driven interactive command dispatch ─────────────────────────────
|
|
1844
|
-
//
|
|
1845
|
-
// Each entry maps a canonical command name to:
|
|
1846
|
-
// params: (arg, iflags) => execute() params object
|
|
1847
|
-
// format: (result, arg, iflags, index) => formatted string
|
|
1848
|
-
//
|
|
1849
|
-
// The generic handler calls execute(), checks errors, prints notes, and
|
|
1850
|
-
// formats the result. Only commands with truly unique behavior (expand
|
|
1851
|
-
// cache save, file writing, conditional formatters) keep explicit cases.
|
|
1852
|
-
|
|
1853
|
-
const INTERACTIVE_DISPATCH = {
|
|
1854
|
-
// ── Understanding Code ───────────────────────────────────────────
|
|
1855
|
-
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, compact: !!f.compact, git: !!f.git }) },
|
|
1856
|
-
smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'unverified callees are listed below with reasons' }) },
|
|
1857
|
-
impact: { params: 'name', format: (r, _a, f) => output.formatImpact(r, { compact: !!f.compact }) },
|
|
1858
|
-
blast: { params: 'name', format: (r) => output.formatBlast(r) },
|
|
1859
|
-
trace: { params: 'name', format: (r) => output.formatTrace(r) },
|
|
1860
|
-
reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
|
|
1861
|
-
related: { params: 'name', format: (r, _a, f) => output.formatRelated(r, { all: f.all, top: f.top }) },
|
|
1862
|
-
example: { params: (a, f) => ({ name: a, file: f.file, className: f.className, diverse: f.diverse, top: f.top || undefined, includeTests: f.includeTests }), format: (r, a) => output.formatExample(r, a) },
|
|
1863
|
-
brief: { params: 'name', format: (r) => output.formatBrief(r) },
|
|
1864
|
-
|
|
1865
|
-
// ── Finding Code ─────────────────────────────────────────────────
|
|
1866
|
-
find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
|
|
1867
|
-
usages: { params: 'name', format: (r, a, f) => output.formatUsages(r, a, { compact: !!f.compact }) },
|
|
1868
|
-
toc: { params: 'flags', format: (r) => output.formatToc(r, { detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol', uncertainHint: 'run "about <name>" for tiered detail on a specific symbol' }) },
|
|
1869
|
-
tests: { params: 'name', format: (r, a) => output.formatTests(r, a) },
|
|
1870
|
-
affectedTests: { params: 'name', format: (r, _a, f) => output.formatAffectedTests(r, { all: f.all }) },
|
|
1871
|
-
typedef: { params: 'name', format: (r, a) => output.formatTypedef(r, a) },
|
|
1872
|
-
|
|
1873
|
-
// ── File Dependencies ────────────────────────────────────────────
|
|
1874
|
-
imports: { params: 'file', format: (r, a, f) => output.formatImports(r, a || f.file) },
|
|
1875
|
-
exporters: { params: 'file', format: (r, a, f) => output.formatExporters(r, a || f.file) },
|
|
1876
|
-
fileExports: { params: 'file', format: (r, a, f) => output.formatFileExports(r, a || f.file) },
|
|
1877
|
-
graph: { params: (a, f) => ({ file: a || f.file, direction: f.direction, depth: f.depth, all: f.all }), format: (r, a, f) => { const d = f.depth ? parseInt(f.depth) : 2; return output.formatGraph(r, { showAll: f.all || !!f.depth, maxDepth: d, file: a || f.file }); } },
|
|
1878
|
-
circularDeps: { params: (a, f) => ({ file: f.file, exclude: f.exclude }), format: (r) => output.formatCircularDeps(r) },
|
|
1879
|
-
|
|
1880
|
-
// ── Refactoring Helpers ──────────────────────────────────────────
|
|
1881
|
-
plan: { params: 'name', format: (r) => output.formatPlan(r) },
|
|
1882
|
-
verify: { params: 'name', format: (r) => output.formatVerify(r) },
|
|
1883
|
-
diffImpact: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit, all: f.all }), format: (r, _a, f) => output.formatDiffImpact(r, { all: f.all }) },
|
|
1884
|
-
check: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit }), format: (r) => output.formatCheck(r) },
|
|
1885
|
-
entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude, includeTests: f.includeTests, excludeTests: f.excludeTests, limit: f.limit }), format: (r) => output.formatEntrypoints(r) },
|
|
1886
|
-
endpoints: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit, framework: f.framework, bridge: f.bridge, serverOnly: f.serverOnly, clientOnly: f.clientOnly, unmatched: f.unmatched, method: f.method, prefix: f.prefix, hideUncertain: f.hideUncertain }), format: (r) => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }) },
|
|
1887
|
-
|
|
1888
|
-
// ── Other ────────────────────────────────────────────────────────
|
|
1889
|
-
api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file) },
|
|
1890
|
-
stacktrace: { params: (a, f) => ({ stack: f.stack || a }), format: (r) => output.formatStackTrace(r) },
|
|
1891
|
-
doctor: { params: (a, f) => ({ file: f.file, in: f.in, limit: f.limit, deep: f.deep }), format: (r) => output.formatDoctor(r) },
|
|
1892
|
-
orient: { params: (a, f) => ({ top: f.topRaw != null ? f.topRaw : (f.top || undefined) }), format: (r) => output.formatOrient(r) },
|
|
1893
|
-
// MED-2: stats handler in execute.js rejects top<=0; without explicit
|
|
1894
|
-
// coercion, parseFlags's `top: 0` default would surface as
|
|
1895
|
-
// "Invalid --top value" on bare `stats`. Mirror the project-mode top
|
|
1896
|
-
// coercion (topRaw when present, else undefined for default-10).
|
|
1897
|
-
stats: { params: (a, f) => ({ functions: f.functions, hot: f.hot, top: f.topRaw != null ? f.topRaw : (f.top || undefined) }), format: (r, _a, f) => output.formatStats(r, { top: f.top }) },
|
|
1898
|
-
auditAsync: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit }), format: (r) => output.formatAuditAsync(r) },
|
|
1899
|
-
};
|
|
1900
|
-
|
|
1901
|
-
/**
|
|
1902
|
-
* Build execute() params from a dispatch entry's params descriptor.
|
|
1903
|
-
* 'name' → { name: arg, ...iflags }
|
|
1904
|
-
* 'file' → { file: arg }
|
|
1905
|
-
* 'flags' → iflags (no arg)
|
|
1906
|
-
* function → custom builder
|
|
1907
|
-
*/
|
|
1908
|
-
function buildInteractiveParams(descriptor, arg, iflags) {
|
|
1909
|
-
if (typeof descriptor === 'function') return descriptor(arg, iflags);
|
|
1910
|
-
switch (descriptor) {
|
|
1911
|
-
case 'name': return { name: arg, ...iflags };
|
|
1912
|
-
case 'file': return { file: arg || iflags.file };
|
|
1913
|
-
case 'flags': return iflags;
|
|
1914
|
-
default: return { name: arg, ...iflags };
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1082
|
+
function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
1083
|
+
warnInapplicableFlags(command, iflags, (message) => console.log(message));
|
|
1917
1084
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
if (applicableFlags) {
|
|
1922
|
-
const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
1923
|
-
const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
|
|
1924
|
-
for (const [key, value] of Object.entries(iflags)) {
|
|
1925
|
-
if (globalFlags.has(key)) continue;
|
|
1926
|
-
if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
|
|
1927
|
-
if (!applicableFlags.includes(key)) {
|
|
1928
|
-
console.log(`Warning: ${flagToCli(key)} has no effect on '${command}'.`);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
// Tiered-output contract notes (fix #250 — one-shot mode printed
|
|
1932
|
-
// these; interactive silently accepted the flags).
|
|
1933
|
-
printTieredNoOpNotes(command, iflags, (m) => console.log(m));
|
|
1085
|
+
if (!isPublicCommand(command)) {
|
|
1086
|
+
console.log(unknownCommandMessage(command, { interactive: true }));
|
|
1087
|
+
return;
|
|
1934
1088
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>] [--class-name=<class>]'); return; }
|
|
1941
|
-
const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all, className: iflags.className, line: iflags.line });
|
|
1942
|
-
if (!ok) { console.log(error); return; }
|
|
1943
|
-
console.log(output.formatFnResult(result));
|
|
1944
|
-
if (note) console.log(note);
|
|
1945
|
-
break;
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
case 'class': {
|
|
1949
|
-
if (!arg) { console.log('Usage: class <name> [--file=<pattern>]'); return; }
|
|
1950
|
-
const { ok, result, error, note } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines, line: iflags.line });
|
|
1951
|
-
if (!ok) { console.log(error); return; }
|
|
1952
|
-
console.log(output.formatClassResult(result));
|
|
1953
|
-
if (note) console.log(note);
|
|
1954
|
-
break;
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
case 'lines': {
|
|
1958
|
-
if (!arg) { console.log('Usage: lines <range> --file=<file> (or: lines <file> <range>)'); return; }
|
|
1959
|
-
const iLinesTarget = parseLinesTarget(arg, iflags.file);
|
|
1960
|
-
const { ok, result, error } = execute(index, 'lines', { file: iLinesTarget.file, range: iLinesTarget.range });
|
|
1961
|
-
if (!ok) { console.log(error); return; }
|
|
1962
|
-
console.log(output.formatLines(result));
|
|
1963
|
-
break;
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
case 'expand': {
|
|
1967
|
-
if (!arg) {
|
|
1968
|
-
console.log('Usage: expand <number>');
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
// Whole-token integers only (fix #248: parseInt truncation).
|
|
1972
|
-
if (!/^\d+$/.test(String(arg).trim())) {
|
|
1973
|
-
console.log(`Invalid item number: "${arg}"`);
|
|
1974
|
-
return;
|
|
1975
|
-
}
|
|
1976
|
-
const expandNum = parseInt(arg, 10);
|
|
1977
|
-
let match, itemCount, symbolName;
|
|
1978
|
-
if (cache) {
|
|
1979
|
-
const lookup = cache.lookup(index.root, expandNum);
|
|
1980
|
-
match = lookup.match;
|
|
1981
|
-
itemCount = lookup.itemCount;
|
|
1982
|
-
symbolName = lookup.symbolName;
|
|
1983
|
-
} else {
|
|
1984
|
-
const cached = loadExpandableItems(index.root);
|
|
1985
|
-
const items = cached?.items || [];
|
|
1986
|
-
match = items.find(i => i.num === expandNum);
|
|
1987
|
-
itemCount = items.length;
|
|
1988
|
-
}
|
|
1989
|
-
const { ok, result, error } = execute(index, 'expand', {
|
|
1990
|
-
match, itemNum: expandNum, itemCount, symbolName, validateRoot: true
|
|
1991
|
-
});
|
|
1992
|
-
if (!ok) { console.log(error); return; }
|
|
1993
|
-
console.log(result.text);
|
|
1994
|
-
break;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
case 'context': {
|
|
1998
|
-
const { ok, result, error, note } = execute(index, 'context', { name: arg, ...iflags });
|
|
1999
|
-
if (!ok) { console.log(error); return; }
|
|
2000
|
-
const { text, expandable } = output.formatContext(result, {
|
|
2001
|
-
expandHint: 'Use "expand <N>" to see code for item N',
|
|
2002
|
-
showConfidence: iflags.showConfidence !== false,
|
|
2003
|
-
compact: !!iflags.compact,
|
|
2004
|
-
});
|
|
2005
|
-
console.log(text);
|
|
2006
|
-
if (iflags.expand) {
|
|
2007
|
-
printInlineExpand(result, index.root);
|
|
2008
|
-
}
|
|
2009
|
-
if (note) console.log(note);
|
|
2010
|
-
if (cache) {
|
|
2011
|
-
cache.save(index.root, arg, iflags.file, expandable);
|
|
2012
|
-
} else {
|
|
2013
|
-
saveExpandableItems(expandable, index.root);
|
|
2014
|
-
}
|
|
2015
|
-
break;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
case 'deadcode': {
|
|
2019
|
-
const { ok, result, error, note } = execute(index, 'deadcode', iflags);
|
|
2020
|
-
if (!ok) { console.log(error); return; }
|
|
2021
|
-
console.log(output.formatDeadcode(result, {
|
|
2022
|
-
top: iflags.top,
|
|
2023
|
-
decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
2024
|
-
exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
|
|
2025
|
-
externalContractHint: !iflags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
|
|
2026
|
-
}));
|
|
2027
|
-
if (note) console.log(note);
|
|
2028
|
-
break;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
case 'search': {
|
|
2032
|
-
const { ok, result, error, structural, note } = execute(index, 'search', { term: arg, ...iflags });
|
|
2033
|
-
if (!ok) { console.log(error); return; }
|
|
2034
|
-
if (structural) {
|
|
2035
|
-
console.log(output.formatStructuralSearch(result));
|
|
2036
|
-
} else {
|
|
2037
|
-
console.log(output.formatSearch(result, arg));
|
|
2038
|
-
}
|
|
2039
|
-
if (note) console.log(note);
|
|
2040
|
-
break;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
default: {
|
|
2044
|
-
// ── Data-driven dispatch for standard commands ────────────
|
|
2045
|
-
const entry = INTERACTIVE_DISPATCH[command];
|
|
2046
|
-
if (!entry) {
|
|
2047
|
-
console.log(`Unknown command: ${command}. Type "help" for available commands.`);
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
const params = buildInteractiveParams(entry.params, arg, iflags);
|
|
2051
|
-
const { ok, result, error, note } = execute(index, command, params);
|
|
2052
|
-
if (!ok) { console.log(error); return; }
|
|
2053
|
-
console.log(entry.format(result, arg, iflags, index));
|
|
2054
|
-
if (note) console.log(note);
|
|
2055
|
-
}
|
|
1089
|
+
const publicParams = buildPublicParams(command, arg, iflags);
|
|
1090
|
+
const publicExecution = execute(index, command, publicParams);
|
|
1091
|
+
if (!publicExecution.ok) {
|
|
1092
|
+
console.log(formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
1093
|
+
return;
|
|
2056
1094
|
}
|
|
1095
|
+
console.log(formatCliText(
|
|
1096
|
+
command,
|
|
1097
|
+
publicExecution.result,
|
|
1098
|
+
publicParams,
|
|
1099
|
+
publicExecution,
|
|
1100
|
+
iflags,
|
|
1101
|
+
));
|
|
2057
1102
|
}
|
|
2058
1103
|
|
|
2059
1104
|
// ============================================================================
|