ucn 3.8.12 → 3.8.14
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 +3 -1
- package/.github/workflows/ci.yml +15 -3
- package/.github/workflows/publish.yml +20 -8
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- package/package.json +8 -3
package/core/output.js
CHANGED
|
@@ -1,3300 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* core/output.js -
|
|
2
|
+
* core/output.js - Re-export facade
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Format dynamic imports note with language-appropriate terminology.
|
|
13
|
-
* Go doesn't have "dynamic imports" — uses "blank/dot imports" instead.
|
|
14
|
-
*/
|
|
15
|
-
function dynamicImportsNote(count, meta) {
|
|
16
|
-
if (!count) return null;
|
|
17
|
-
if (meta?.projectLanguage === 'go') {
|
|
18
|
-
return `${count} blank/dot import(s)`;
|
|
19
|
-
}
|
|
20
|
-
return `${count} dynamic import(s)`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const FILE_ERROR_MESSAGES = {
|
|
24
|
-
'file-not-found': 'File not found in project',
|
|
25
|
-
'file-ambiguous': 'Ambiguous file match'
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function formatFileError(errorObj, fallbackPath) {
|
|
29
|
-
const msg = FILE_ERROR_MESSAGES[errorObj.error] || errorObj.error;
|
|
30
|
-
const file = errorObj.filePath || fallbackPath || '';
|
|
31
|
-
return `Error: ${msg}: ${file}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Normalize parameters for display
|
|
36
|
-
* Collapses multiline params to single line
|
|
37
|
-
* @param {string} params - Raw params string
|
|
38
|
-
* @returns {string} - Normalized params (NO truncation)
|
|
39
|
-
*/
|
|
40
|
-
function normalizeParams(params) {
|
|
41
|
-
if (!params || params === '...') return params || '...';
|
|
42
|
-
// Collapse whitespace (newlines, tabs, multiple spaces) to single space
|
|
43
|
-
return params.replace(/\s+/g, ' ').trim();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Format a line number for display
|
|
48
|
-
* @param {number} line - 1-indexed line number
|
|
49
|
-
* @param {number} width - Padding width
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
function lineNum(line, width = 4) {
|
|
53
|
-
return String(line).padStart(width);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Format a line range
|
|
58
|
-
* @param {number} start - 1-indexed start line
|
|
59
|
-
* @param {number} end - 1-indexed end line
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
function lineRange(start, end) {
|
|
63
|
-
return `[${lineNum(start)}-${lineNum(end)}]`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Format a single line location
|
|
68
|
-
* @param {number} line - 1-indexed line number
|
|
69
|
-
* @returns {string}
|
|
70
|
-
*/
|
|
71
|
-
function lineLoc(line) {
|
|
72
|
-
return `[${lineNum(line)}]`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ============================================================================
|
|
76
|
-
// TEXT FORMATTERS
|
|
77
|
-
// ============================================================================
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Format function signature for TOC display
|
|
81
|
-
* @param {object} fn - Function definition
|
|
82
|
-
* @returns {string}
|
|
83
|
-
*/
|
|
84
|
-
function formatFunctionSignature(fn) {
|
|
85
|
-
const prefix = [];
|
|
86
|
-
|
|
87
|
-
// Modifiers
|
|
88
|
-
if (fn.modifiers && fn.modifiers.length > 0) {
|
|
89
|
-
prefix.push(fn.modifiers.join(' '));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Generator marker
|
|
93
|
-
if (fn.isGenerator) prefix.push('*');
|
|
94
|
-
|
|
95
|
-
// Name + generics + params (concatenated without spaces)
|
|
96
|
-
let sig = fn.name;
|
|
97
|
-
if (fn.generics) sig += fn.generics;
|
|
98
|
-
const params = normalizeParams(fn.params);
|
|
99
|
-
sig += `(${params})`;
|
|
100
|
-
|
|
101
|
-
// Return type
|
|
102
|
-
if (fn.returnType) sig += `: ${fn.returnType}`;
|
|
103
|
-
|
|
104
|
-
// Arrow indicator
|
|
105
|
-
if (fn.isArrow) sig += ' =>';
|
|
106
|
-
|
|
107
|
-
if (prefix.length > 0) {
|
|
108
|
-
return prefix.join(' ') + ' ' + sig;
|
|
109
|
-
}
|
|
110
|
-
return sig;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Format class/type signature for TOC display
|
|
115
|
-
*/
|
|
116
|
-
function formatClassSignature(cls) {
|
|
117
|
-
const parts = [cls.type, cls.name];
|
|
118
|
-
|
|
119
|
-
if (cls.generics) parts.push(cls.generics);
|
|
120
|
-
if (cls.extends) parts.push(`extends ${cls.extends}`);
|
|
121
|
-
if (cls.implements && cls.implements.length > 0) {
|
|
122
|
-
parts.push(`implements ${cls.implements.join(', ')}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return parts.join(' ');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Format class member for TOC display
|
|
130
|
-
*/
|
|
131
|
-
function formatMemberSignature(member) {
|
|
132
|
-
const parts = [];
|
|
133
|
-
|
|
134
|
-
// Member type (static, get, set, private, etc.)
|
|
135
|
-
if (member.memberType && member.memberType !== 'method') {
|
|
136
|
-
parts.push(member.memberType);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Async
|
|
140
|
-
if (member.isAsync) parts.push('async');
|
|
141
|
-
|
|
142
|
-
// Generator
|
|
143
|
-
if (member.isGenerator) parts.push('*');
|
|
144
|
-
|
|
145
|
-
// Name + Parameters (no space between name and parens)
|
|
146
|
-
if (member.params !== undefined) {
|
|
147
|
-
const params = normalizeParams(member.params);
|
|
148
|
-
parts.push(`${member.name}(${params})`);
|
|
149
|
-
} else {
|
|
150
|
-
parts.push(member.name);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Return type
|
|
154
|
-
if (member.returnType) parts.push(`: ${member.returnType}`);
|
|
155
|
-
|
|
156
|
-
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Print section header
|
|
161
|
-
*/
|
|
162
|
-
function header(title, char = '═') {
|
|
163
|
-
console.log(title);
|
|
164
|
-
console.log(char.repeat(60));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Print subheader
|
|
169
|
-
*/
|
|
170
|
-
function subheader(title) {
|
|
171
|
-
console.log(title);
|
|
172
|
-
console.log('─'.repeat(40));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Print a usage/call site - FULL expression, never truncated
|
|
177
|
-
* @param {object} usage - Usage object
|
|
178
|
-
* @param {string} [relativePath] - Relative file path
|
|
179
|
-
*/
|
|
180
|
-
function printUsage(usage, relativePath) {
|
|
181
|
-
const file = relativePath || usage.file;
|
|
182
|
-
// Context before the match
|
|
183
|
-
if (usage.before && usage.before.length > 0) {
|
|
184
|
-
for (const line of usage.before) {
|
|
185
|
-
console.log(` ... ${line.trim()}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// FULL content - this is the key improvement
|
|
189
|
-
console.log(` ${file}:${usage.line}`);
|
|
190
|
-
console.log(` ${usage.content.trim()}`);
|
|
191
|
-
// Context after the match
|
|
192
|
-
if (usage.after && usage.after.length > 0) {
|
|
193
|
-
for (const line of usage.after) {
|
|
194
|
-
console.log(` ... ${line.trim()}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Print definition with full signature
|
|
201
|
-
*/
|
|
202
|
-
function printDefinition(def, relativePath) {
|
|
203
|
-
const file = relativePath || def.file;
|
|
204
|
-
console.log(` ${file}:${def.line}`);
|
|
205
|
-
if (def.signature) {
|
|
206
|
-
console.log(` ${def.signature}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ============================================================================
|
|
211
|
-
// JSON FORMATTERS
|
|
212
|
-
// ============================================================================
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Format TOC data as JSON
|
|
216
|
-
*/
|
|
217
|
-
function formatTocJson(data) {
|
|
218
|
-
const obj = {
|
|
219
|
-
meta: data.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
220
|
-
totals: data.totals,
|
|
221
|
-
summary: data.summary,
|
|
222
|
-
files: data.files
|
|
223
|
-
};
|
|
224
|
-
if (data.hiddenFiles > 0) obj.hiddenFiles = data.hiddenFiles;
|
|
225
|
-
return JSON.stringify(obj);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Format symbol search results as JSON
|
|
230
|
-
*/
|
|
231
|
-
function formatSymbolJson(symbols, query) {
|
|
232
|
-
return JSON.stringify({
|
|
233
|
-
meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
234
|
-
data: {
|
|
235
|
-
query,
|
|
236
|
-
count: symbols.length,
|
|
237
|
-
results: symbols.map(s => ({
|
|
238
|
-
name: s.name,
|
|
239
|
-
type: s.type,
|
|
240
|
-
file: s.relativePath || s.file,
|
|
241
|
-
startLine: s.startLine,
|
|
242
|
-
endLine: s.endLine,
|
|
243
|
-
...(s.params && { params: s.params }), // FULL params
|
|
244
|
-
...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
|
|
245
|
-
...(s.returnType && { returnType: s.returnType }),
|
|
246
|
-
...(s.modifiers && { modifiers: s.modifiers }),
|
|
247
|
-
...(s.usageCount !== undefined && { usageCount: s.usageCount }),
|
|
248
|
-
...(s.usageCounts !== undefined && { usageCounts: s.usageCounts })
|
|
249
|
-
}))
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Format usages as JSON - FULL expressions, never truncated
|
|
256
|
-
*/
|
|
257
|
-
function formatUsagesJson(usages, name) {
|
|
258
|
-
const definitions = usages.filter(u => u.isDefinition);
|
|
259
|
-
const refs = usages.filter(u => !u.isDefinition);
|
|
260
|
-
|
|
261
|
-
const calls = refs.filter(u => u.usageType === 'call');
|
|
262
|
-
const imports = refs.filter(u => u.usageType === 'import');
|
|
263
|
-
const references = refs.filter(u => u.usageType === 'reference');
|
|
264
|
-
|
|
265
|
-
const formatUsage = (u) => ({
|
|
266
|
-
file: u.relativePath || u.file,
|
|
267
|
-
line: u.line,
|
|
268
|
-
expression: u.content, // FULL expression - key improvement
|
|
269
|
-
...(u.args && { args: u.args }), // Parsed arguments
|
|
270
|
-
...(u.before && u.before.length > 0 && { before: u.before }),
|
|
271
|
-
...(u.after && u.after.length > 0 && { after: u.after })
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
return JSON.stringify({
|
|
275
|
-
meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
276
|
-
data: {
|
|
277
|
-
symbol: name,
|
|
278
|
-
definitionCount: definitions.length,
|
|
279
|
-
callCount: calls.length,
|
|
280
|
-
importCount: imports.length,
|
|
281
|
-
referenceCount: references.length,
|
|
282
|
-
totalUsages: refs.length,
|
|
283
|
-
definitions: definitions.map(d => ({
|
|
284
|
-
file: d.relativePath || d.file,
|
|
285
|
-
line: d.line,
|
|
286
|
-
signature: d.signature || null, // FULL signature
|
|
287
|
-
type: d.type || null,
|
|
288
|
-
...(d.returnType && { returnType: d.returnType }),
|
|
289
|
-
...(d.before && d.before.length > 0 && { before: d.before }),
|
|
290
|
-
...(d.after && d.after.length > 0 && { after: d.after })
|
|
291
|
-
})),
|
|
292
|
-
calls: calls.map(formatUsage),
|
|
293
|
-
imports: imports.map(formatUsage),
|
|
294
|
-
references: references.map(formatUsage)
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Format context (callers + callees) as JSON
|
|
301
|
-
*/
|
|
302
|
-
function formatContextJson(context) {
|
|
303
|
-
const meta = context.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
|
|
304
|
-
// Handle struct/interface types differently
|
|
305
|
-
if (context.type && ['class', 'struct', 'interface', 'type'].includes(context.type)) {
|
|
306
|
-
const callers = context.callers || [];
|
|
307
|
-
const methods = context.methods || [];
|
|
308
|
-
return JSON.stringify({
|
|
309
|
-
meta,
|
|
310
|
-
data: {
|
|
311
|
-
type: context.type,
|
|
312
|
-
name: context.name,
|
|
313
|
-
file: context.file,
|
|
314
|
-
startLine: context.startLine,
|
|
315
|
-
endLine: context.endLine,
|
|
316
|
-
methodCount: methods.length,
|
|
317
|
-
usageCount: callers.length,
|
|
318
|
-
methods: methods.map(m => ({
|
|
319
|
-
name: m.name,
|
|
320
|
-
file: m.file,
|
|
321
|
-
line: m.line,
|
|
322
|
-
params: m.params,
|
|
323
|
-
returnType: m.returnType,
|
|
324
|
-
receiver: m.receiver
|
|
325
|
-
})),
|
|
326
|
-
usages: callers.map(c => ({
|
|
327
|
-
file: c.relativePath || c.file,
|
|
328
|
-
line: c.line,
|
|
329
|
-
expression: c.content,
|
|
330
|
-
callerName: c.callerName
|
|
331
|
-
})),
|
|
332
|
-
...(context.warnings && { warnings: context.warnings })
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Standard function/method context
|
|
338
|
-
const callers = context.callers || [];
|
|
339
|
-
const callees = context.callees || [];
|
|
340
|
-
return JSON.stringify({
|
|
341
|
-
meta,
|
|
342
|
-
data: {
|
|
343
|
-
function: context.function,
|
|
344
|
-
file: context.file,
|
|
345
|
-
callerCount: callers.length,
|
|
346
|
-
calleeCount: callees.length,
|
|
347
|
-
callers: callers.map(c => ({
|
|
348
|
-
file: c.relativePath || c.file,
|
|
349
|
-
line: c.line,
|
|
350
|
-
expression: c.content, // FULL expression
|
|
351
|
-
callerName: c.callerName,
|
|
352
|
-
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
353
|
-
})),
|
|
354
|
-
callees: callees.map(c => ({
|
|
355
|
-
name: c.name,
|
|
356
|
-
type: c.type,
|
|
357
|
-
file: c.relativePath || c.file,
|
|
358
|
-
line: c.startLine,
|
|
359
|
-
params: c.params, // FULL params
|
|
360
|
-
weight: c.weight || 'normal', // Dependency weight: core, setup, utility
|
|
361
|
-
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
362
|
-
})),
|
|
363
|
-
...(context.warnings && { warnings: context.warnings })
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Format extracted function as JSON
|
|
370
|
-
*/
|
|
371
|
-
function formatFunctionJson(fn, code) {
|
|
372
|
-
return JSON.stringify({
|
|
373
|
-
name: fn.name,
|
|
374
|
-
params: fn.params, // FULL params
|
|
375
|
-
paramsStructured: fn.paramsStructured || [],
|
|
376
|
-
startLine: fn.startLine,
|
|
377
|
-
endLine: fn.endLine,
|
|
378
|
-
modifiers: fn.modifiers || [],
|
|
379
|
-
...(fn.returnType && { returnType: fn.returnType }),
|
|
380
|
-
...(fn.generics && { generics: fn.generics }),
|
|
381
|
-
...(fn.docstring && { docstring: fn.docstring }),
|
|
382
|
-
...(fn.isArrow && { isArrow: true }),
|
|
383
|
-
...(fn.isGenerator && { isGenerator: true }),
|
|
384
|
-
code // FULL code
|
|
385
|
-
}, null, 2);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Format search results as JSON
|
|
390
|
-
*/
|
|
391
|
-
function formatSearchJson(results, term) {
|
|
392
|
-
const meta = results.meta;
|
|
393
|
-
const obj = {
|
|
394
|
-
term,
|
|
395
|
-
totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
|
|
396
|
-
files: results.map(r => ({
|
|
397
|
-
file: r.file,
|
|
398
|
-
matchCount: r.matches.length,
|
|
399
|
-
matches: r.matches.map(m => ({
|
|
400
|
-
line: m.line,
|
|
401
|
-
content: m.content // FULL content
|
|
402
|
-
}))
|
|
403
|
-
}))
|
|
404
|
-
};
|
|
405
|
-
if (meta) {
|
|
406
|
-
obj.filesScanned = meta.filesScanned;
|
|
407
|
-
obj.filesSkipped = meta.filesSkipped;
|
|
408
|
-
obj.totalFiles = meta.totalFiles;
|
|
409
|
-
if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
|
|
410
|
-
if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
|
|
411
|
-
}
|
|
412
|
-
return JSON.stringify(obj, null, 2);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Format imports as JSON
|
|
417
|
-
*/
|
|
418
|
-
function formatImportsJson(imports, filePath) {
|
|
419
|
-
if (imports?.error) return JSON.stringify({ found: false, error: imports.error, file: imports.filePath || filePath }, null, 2);
|
|
420
|
-
return JSON.stringify({
|
|
421
|
-
file: filePath,
|
|
422
|
-
importCount: imports.length,
|
|
423
|
-
imports: imports.map(i => ({
|
|
424
|
-
module: i.module,
|
|
425
|
-
names: i.names,
|
|
426
|
-
type: i.type,
|
|
427
|
-
resolved: i.resolved || null,
|
|
428
|
-
isDynamic: !!i.isDynamic
|
|
429
|
-
}))
|
|
430
|
-
}, null, 2);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Format project stats as JSON
|
|
435
|
-
*/
|
|
436
|
-
function formatStatsJson(stats) {
|
|
437
|
-
return JSON.stringify(stats, null, 2);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Format dependency graph as JSON
|
|
442
|
-
*/
|
|
443
|
-
function formatGraphJson(graph) {
|
|
444
|
-
if (graph?.error) return JSON.stringify({ found: false, error: graph.error, file: graph.filePath }, null, 2);
|
|
445
|
-
const result = {
|
|
446
|
-
root: graph.root,
|
|
447
|
-
direction: graph.direction,
|
|
448
|
-
nodes: graph.nodes,
|
|
449
|
-
edges: graph.edges
|
|
450
|
-
};
|
|
451
|
-
if (graph.imports) result.imports = graph.imports;
|
|
452
|
-
if (graph.importers) result.importers = graph.importers;
|
|
453
|
-
return JSON.stringify(result, null, 2);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Format smart extraction result as JSON
|
|
458
|
-
* Includes function + all dependencies
|
|
459
|
-
*/
|
|
460
|
-
function formatSmartJson(result) {
|
|
461
|
-
if (!result) return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
462
|
-
const meta = result.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 };
|
|
463
|
-
return JSON.stringify({
|
|
464
|
-
meta,
|
|
465
|
-
data: {
|
|
466
|
-
target: {
|
|
467
|
-
name: result.target.name,
|
|
468
|
-
file: result.target.file,
|
|
469
|
-
startLine: result.target.startLine,
|
|
470
|
-
endLine: result.target.endLine,
|
|
471
|
-
params: result.target.params,
|
|
472
|
-
returnType: result.target.returnType,
|
|
473
|
-
code: result.target.code
|
|
474
|
-
},
|
|
475
|
-
dependencies: result.dependencies.map(d => ({
|
|
476
|
-
name: d.name,
|
|
477
|
-
type: d.type,
|
|
478
|
-
file: d.file,
|
|
479
|
-
startLine: d.startLine,
|
|
480
|
-
endLine: d.endLine,
|
|
481
|
-
params: d.params,
|
|
482
|
-
weight: d.weight, // core, setup, utility
|
|
483
|
-
callCount: d.callCount,
|
|
484
|
-
code: d.code
|
|
485
|
-
})),
|
|
486
|
-
types: result.types || []
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ============================================================================
|
|
492
|
-
// NEW FORMATTERS (v2 Migration)
|
|
493
|
-
// ============================================================================
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Format imports command output - text
|
|
497
|
-
*/
|
|
498
|
-
function formatImports(imports, filePath) {
|
|
499
|
-
if (imports?.error) return formatFileError(imports, filePath);
|
|
500
|
-
const lines = [`Imports in ${filePath}:\n`];
|
|
501
|
-
|
|
502
|
-
const internal = imports.filter(i => !i.isExternal && !i.isDynamic);
|
|
503
|
-
const external = imports.filter(i => i.isExternal && !i.isDynamic);
|
|
504
|
-
const dynamic = imports.filter(i => i.isDynamic);
|
|
505
|
-
|
|
506
|
-
if (internal.length > 0) {
|
|
507
|
-
lines.push('INTERNAL:');
|
|
508
|
-
for (const imp of internal) {
|
|
509
|
-
lines.push(` ${imp.module}`);
|
|
510
|
-
if (imp.resolved) {
|
|
511
|
-
lines.push(` -> ${imp.resolved}`);
|
|
512
|
-
}
|
|
513
|
-
if (imp.names && imp.names.length > 0 && imp.names[0] !== '*') {
|
|
514
|
-
lines.push(` ${imp.names.join(', ')}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (external.length > 0) {
|
|
520
|
-
if (internal.length > 0) lines.push('');
|
|
521
|
-
lines.push('EXTERNAL:');
|
|
522
|
-
for (const imp of external) {
|
|
523
|
-
lines.push(` ${imp.module}`);
|
|
524
|
-
if (imp.names && imp.names.length > 0) {
|
|
525
|
-
lines.push(` ${imp.names.join(', ')}`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (dynamic.length > 0) {
|
|
531
|
-
if (internal.length > 0 || external.length > 0) lines.push('');
|
|
532
|
-
lines.push('DYNAMIC (unresolved):');
|
|
533
|
-
for (const imp of dynamic) {
|
|
534
|
-
lines.push(` ${imp.module || '(variable)'}`);
|
|
535
|
-
if (imp.names && imp.names.length > 0) {
|
|
536
|
-
lines.push(` ${imp.names.join(', ')}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return lines.join('\n');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Format exporters command output - text
|
|
546
|
-
*/
|
|
547
|
-
function formatExporters(exporters, filePath) {
|
|
548
|
-
if (exporters?.error) return formatFileError(exporters, filePath);
|
|
549
|
-
const lines = [`Files that import ${filePath}:\n`];
|
|
550
|
-
|
|
551
|
-
if (exporters.length === 0) {
|
|
552
|
-
lines.push(' (none found)');
|
|
553
|
-
} else {
|
|
554
|
-
for (const exp of exporters) {
|
|
555
|
-
if (exp.importLine) {
|
|
556
|
-
lines.push(` ${exp.file}:${exp.importLine}`);
|
|
557
|
-
} else {
|
|
558
|
-
lines.push(` ${exp.file}`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return lines.join('\n');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Format typedef command output - text
|
|
568
|
-
*/
|
|
569
|
-
function formatTypedef(types, name) {
|
|
570
|
-
const lines = [`Type definitions for "${name}":\n`];
|
|
571
|
-
|
|
572
|
-
if (types.length === 0) {
|
|
573
|
-
lines.push(' (none found)');
|
|
574
|
-
} else {
|
|
575
|
-
for (const t of types) {
|
|
576
|
-
lines.push(`${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
|
|
577
|
-
if (t.usageCount !== undefined) {
|
|
578
|
-
lines.push(` (${t.usageCount} usages)`);
|
|
579
|
-
}
|
|
580
|
-
if (t.code) {
|
|
581
|
-
lines.push('');
|
|
582
|
-
lines.push('─── CODE ───');
|
|
583
|
-
lines.push(t.code);
|
|
584
|
-
lines.push('');
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return lines.join('\n');
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Format tests command output - text
|
|
594
|
-
*/
|
|
595
|
-
function formatTests(tests, name) {
|
|
596
|
-
const lines = [`Tests for "${name}":\n`];
|
|
597
|
-
|
|
598
|
-
if (tests.length === 0) {
|
|
599
|
-
lines.push(' (no tests found)');
|
|
600
|
-
} else {
|
|
601
|
-
const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
|
|
602
|
-
lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
|
|
603
|
-
|
|
604
|
-
for (const testFile of tests) {
|
|
605
|
-
lines.push(testFile.file);
|
|
606
|
-
for (const match of testFile.matches) {
|
|
607
|
-
const typeLabel = match.matchType === 'test-case' ? '[test]' :
|
|
608
|
-
match.matchType === 'import' ? '[import]' :
|
|
609
|
-
match.matchType === 'call' ? '[call]' :
|
|
610
|
-
match.matchType === 'string-ref' ? '[string]' : '[ref]';
|
|
611
|
-
lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
|
|
612
|
-
}
|
|
613
|
-
lines.push('');
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return lines.join('\n');
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Format api command output - text
|
|
622
|
-
*/
|
|
623
|
-
function formatApi(symbols, filePath) {
|
|
624
|
-
const title = filePath
|
|
625
|
-
? `Exports from ${filePath}:`
|
|
626
|
-
: 'Project API (exported symbols):';
|
|
627
|
-
const lines = [title + '\n'];
|
|
628
|
-
|
|
629
|
-
if (symbols.length === 0) {
|
|
630
|
-
lines.push(' (none found)');
|
|
631
|
-
if (filePath && filePath.endsWith('.py')) {
|
|
632
|
-
lines.push('');
|
|
633
|
-
lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
|
|
634
|
-
}
|
|
635
|
-
} else {
|
|
636
|
-
// Group by file
|
|
637
|
-
const byFile = new Map();
|
|
638
|
-
for (const sym of symbols) {
|
|
639
|
-
if (!byFile.has(sym.file)) {
|
|
640
|
-
byFile.set(sym.file, []);
|
|
641
|
-
}
|
|
642
|
-
byFile.get(sym.file).push(sym);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
for (const [file, syms] of byFile) {
|
|
646
|
-
lines.push(file);
|
|
647
|
-
for (const s of syms) {
|
|
648
|
-
const sig = s.signature || `${s.type} ${s.name}`;
|
|
649
|
-
lines.push(` ${lineRange(s.startLine, s.endLine)} ${sig}`);
|
|
650
|
-
}
|
|
651
|
-
lines.push('');
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return lines.join('\n');
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Format disambiguation prompt - text
|
|
660
|
-
*/
|
|
661
|
-
function formatDisambiguation(matches, name, command) {
|
|
662
|
-
const lines = [`Multiple matches for "${name}":\n`];
|
|
663
|
-
|
|
664
|
-
for (const m of matches) {
|
|
665
|
-
const sig = m.params !== undefined
|
|
666
|
-
? formatFunctionSignature(m)
|
|
667
|
-
: formatClassSignature(m);
|
|
668
|
-
lines.push(` ${m.relativePath}:${m.startLine} ${sig}`);
|
|
669
|
-
if (m.usageCount !== undefined) {
|
|
670
|
-
lines.push(` (${m.usageCount} usages)`);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
lines.push('');
|
|
675
|
-
lines.push(`Use: ucn . ${command} ${name} --file <pattern>`);
|
|
676
|
-
|
|
677
|
-
return lines.join('\n');
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// ============================================================================
|
|
681
|
-
// NEW JSON FORMATTERS
|
|
682
|
-
// ============================================================================
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Format exporters as JSON
|
|
686
|
-
*/
|
|
687
|
-
function formatExportersJson(exporters, filePath) {
|
|
688
|
-
if (exporters?.error) return JSON.stringify({ found: false, error: exporters.error, file: exporters.filePath || filePath }, null, 2);
|
|
689
|
-
return JSON.stringify({
|
|
690
|
-
file: filePath,
|
|
691
|
-
importerCount: exporters.length,
|
|
692
|
-
importers: exporters
|
|
693
|
-
}, null, 2);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Format typedef as JSON
|
|
698
|
-
*/
|
|
699
|
-
function formatTypedefJson(types, name) {
|
|
700
|
-
return JSON.stringify({
|
|
701
|
-
query: name,
|
|
702
|
-
count: types.length,
|
|
703
|
-
types: types.map(t => ({
|
|
704
|
-
name: t.name,
|
|
705
|
-
type: t.type,
|
|
706
|
-
file: t.relativePath || t.file,
|
|
707
|
-
startLine: t.startLine,
|
|
708
|
-
endLine: t.endLine,
|
|
709
|
-
...(t.usageCount !== undefined && { usageCount: t.usageCount }),
|
|
710
|
-
...(t.code && { code: t.code })
|
|
711
|
-
}))
|
|
712
|
-
}, null, 2);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Format tests as JSON
|
|
717
|
-
*/
|
|
718
|
-
function formatTestsJson(tests, name) {
|
|
719
|
-
return JSON.stringify({
|
|
720
|
-
query: name,
|
|
721
|
-
testFileCount: tests.length,
|
|
722
|
-
totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
|
|
723
|
-
testFiles: tests
|
|
724
|
-
}, null, 2);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Format api as JSON
|
|
729
|
-
*/
|
|
730
|
-
function formatApiJson(symbols, filePath) {
|
|
731
|
-
return JSON.stringify({
|
|
732
|
-
...(filePath && { file: filePath }),
|
|
733
|
-
exportCount: symbols.length,
|
|
734
|
-
exports: symbols.map(s => ({
|
|
735
|
-
name: s.name,
|
|
736
|
-
type: s.type,
|
|
737
|
-
file: s.file,
|
|
738
|
-
startLine: s.startLine,
|
|
739
|
-
endLine: s.endLine,
|
|
740
|
-
...(s.params && { params: s.params }),
|
|
741
|
-
...(s.returnType && { returnType: s.returnType }),
|
|
742
|
-
...(s.signature && { signature: s.signature })
|
|
743
|
-
}))
|
|
744
|
-
}, null, 2);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Format trace command output - text
|
|
749
|
-
* Shows call tree visualization
|
|
750
|
-
*/
|
|
751
|
-
function formatTrace(trace, options = {}) {
|
|
752
|
-
if (!trace) {
|
|
753
|
-
return 'Function not found.';
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const lines = [];
|
|
757
|
-
|
|
758
|
-
// Header
|
|
759
|
-
lines.push(`Call tree for ${trace.root}`);
|
|
760
|
-
lines.push('═'.repeat(60));
|
|
761
|
-
lines.push(`${trace.file}:${trace.line}`);
|
|
762
|
-
lines.push(`Direction: ${trace.direction}, Max depth: ${trace.maxDepth}`);
|
|
763
|
-
|
|
764
|
-
if (trace.warnings && trace.warnings.length > 0) {
|
|
765
|
-
for (const w of trace.warnings) {
|
|
766
|
-
lines.push(`Note: ${w.message}`);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
lines.push('');
|
|
771
|
-
|
|
772
|
-
// Render tree
|
|
773
|
-
let hasTruncation = false;
|
|
774
|
-
const renderNode = (node, prefix = '', isLast = true) => {
|
|
775
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
776
|
-
const extension = isLast ? ' ' : '│ ';
|
|
777
|
-
|
|
778
|
-
let label = node.name;
|
|
779
|
-
if (node.external) {
|
|
780
|
-
label += ' [external]';
|
|
781
|
-
} else if (node.file) {
|
|
782
|
-
label += ` (${node.file}:${node.line})`;
|
|
783
|
-
}
|
|
784
|
-
if (node.weight && node.weight !== 'normal') {
|
|
785
|
-
label += ` [${node.weight}]`;
|
|
786
|
-
}
|
|
787
|
-
if (node.callCount) {
|
|
788
|
-
label += ` ${node.callCount}x`;
|
|
789
|
-
}
|
|
790
|
-
if (node.alreadyShown) {
|
|
791
|
-
label += ' (see above)';
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
lines.push(prefix + connector + label);
|
|
795
|
-
|
|
796
|
-
if (node.children && !node.alreadyShown) {
|
|
797
|
-
const hasMore = node.truncatedChildren > 0;
|
|
798
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
799
|
-
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
800
|
-
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
801
|
-
}
|
|
802
|
-
if (hasMore) {
|
|
803
|
-
hasTruncation = true;
|
|
804
|
-
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callees`);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
// Root node
|
|
810
|
-
lines.push(trace.root);
|
|
811
|
-
if (trace.tree && trace.tree.children) {
|
|
812
|
-
const rootHasMore = trace.tree.truncatedChildren > 0;
|
|
813
|
-
for (let i = 0; i < trace.tree.children.length; i++) {
|
|
814
|
-
const isLast = !rootHasMore && i === trace.tree.children.length - 1;
|
|
815
|
-
renderNode(trace.tree.children[i], '', isLast);
|
|
816
|
-
}
|
|
817
|
-
if (rootHasMore) {
|
|
818
|
-
hasTruncation = true;
|
|
819
|
-
lines.push(`└── ... and ${trace.tree.truncatedChildren} more callees`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Callers section
|
|
824
|
-
if (trace.callers && trace.callers.length > 0) {
|
|
825
|
-
lines.push('');
|
|
826
|
-
lines.push('CALLED BY:');
|
|
827
|
-
for (const c of trace.callers) {
|
|
828
|
-
lines.push(` ${c.name} - ${c.file}:${c.line}`);
|
|
829
|
-
lines.push(` ${c.expression}`);
|
|
830
|
-
}
|
|
831
|
-
if (trace.truncatedCallers) {
|
|
832
|
-
hasTruncation = true;
|
|
833
|
-
lines.push(` ... and ${trace.truncatedCallers} more callers`);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (hasTruncation) {
|
|
838
|
-
const allHint = options.allHint || 'Use --all to show all.';
|
|
839
|
-
lines.push(`\nSome results truncated. ${allHint}`);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (trace.includeMethods === false) {
|
|
843
|
-
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded — use --include-methods to include them';
|
|
844
|
-
lines.push(`\n${methodsHint}`);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return lines.join('\n');
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Format trace command output - JSON
|
|
852
|
-
*/
|
|
853
|
-
function formatTraceJson(trace) {
|
|
854
|
-
if (!trace) {
|
|
855
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
856
|
-
}
|
|
857
|
-
return JSON.stringify(trace, null, 2);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* Format blast command output - text
|
|
862
|
-
* Shows transitive blast radius (callers of callers)
|
|
863
|
-
*/
|
|
864
|
-
function formatBlast(blast, options = {}) {
|
|
865
|
-
if (!blast) {
|
|
866
|
-
return 'Function not found.';
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const lines = [];
|
|
870
|
-
|
|
871
|
-
// Header
|
|
872
|
-
lines.push(`Blast radius for ${blast.root}`);
|
|
873
|
-
lines.push('═'.repeat(60));
|
|
874
|
-
lines.push(`${blast.file}:${blast.line}`);
|
|
875
|
-
lines.push(`Depth: ${blast.maxDepth}`);
|
|
876
|
-
|
|
877
|
-
if (blast.warnings && blast.warnings.length > 0) {
|
|
878
|
-
for (const w of blast.warnings) {
|
|
879
|
-
lines.push(`Note: ${w.message}`);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
lines.push('');
|
|
884
|
-
|
|
885
|
-
// Render tree (same structure as trace but showing callers)
|
|
886
|
-
let hasTruncation = false;
|
|
887
|
-
const renderNode = (node, prefix = '', isLast = true) => {
|
|
888
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
889
|
-
const extension = isLast ? ' ' : '│ ';
|
|
890
|
-
|
|
891
|
-
let label = node.name;
|
|
892
|
-
if (node.file) {
|
|
893
|
-
label += ` (${node.file}:${node.line})`;
|
|
894
|
-
}
|
|
895
|
-
if (node.callSites && node.callSites > 1) {
|
|
896
|
-
label += ` ${node.callSites}x`;
|
|
897
|
-
}
|
|
898
|
-
if (node.alreadyShown) {
|
|
899
|
-
label += ' (see above)';
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
lines.push(prefix + connector + label);
|
|
903
|
-
|
|
904
|
-
if (node.children && !node.alreadyShown) {
|
|
905
|
-
const hasMore = node.truncatedChildren > 0;
|
|
906
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
907
|
-
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
908
|
-
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
909
|
-
}
|
|
910
|
-
if (hasMore) {
|
|
911
|
-
hasTruncation = true;
|
|
912
|
-
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
|
|
917
|
-
// Root node
|
|
918
|
-
lines.push(blast.root);
|
|
919
|
-
if (blast.tree && blast.tree.children) {
|
|
920
|
-
const rootHasMore = blast.tree.truncatedChildren > 0;
|
|
921
|
-
for (let i = 0; i < blast.tree.children.length; i++) {
|
|
922
|
-
const isLast = !rootHasMore && i === blast.tree.children.length - 1;
|
|
923
|
-
renderNode(blast.tree.children[i], '', isLast);
|
|
924
|
-
}
|
|
925
|
-
if (rootHasMore) {
|
|
926
|
-
hasTruncation = true;
|
|
927
|
-
lines.push(`└── ... and ${blast.tree.truncatedChildren} more callers`);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Summary
|
|
932
|
-
if (blast.summary) {
|
|
933
|
-
lines.push('');
|
|
934
|
-
const { totalAffected, totalFiles } = blast.summary;
|
|
935
|
-
if (totalAffected > 0) {
|
|
936
|
-
lines.push(`Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`);
|
|
937
|
-
} else {
|
|
938
|
-
lines.push('Summary: No callers found — this function is a root/entry point.');
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (hasTruncation) {
|
|
943
|
-
const allHint = options.allHint || 'Use --all to show all.';
|
|
944
|
-
lines.push(`\nSome results truncated. ${allHint}`);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (blast.includeMethods === false) {
|
|
948
|
-
lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
return lines.join('\n');
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* Format blast command output - JSON
|
|
956
|
-
*/
|
|
957
|
-
function formatBlastJson(blast) {
|
|
958
|
-
if (!blast) {
|
|
959
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
960
|
-
}
|
|
961
|
-
return JSON.stringify(blast, null, 2);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Format reverse-trace command output - text
|
|
966
|
-
* Shows upward call chain to entry points
|
|
967
|
-
*/
|
|
968
|
-
function formatReverseTrace(result, options = {}) {
|
|
969
|
-
if (!result) {
|
|
970
|
-
return 'Function not found.';
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const lines = [];
|
|
974
|
-
|
|
975
|
-
// Header
|
|
976
|
-
lines.push(`Reverse trace for ${result.root}`);
|
|
977
|
-
lines.push('═'.repeat(60));
|
|
978
|
-
lines.push(`${result.file}:${result.line}`);
|
|
979
|
-
lines.push(`Depth: ${result.maxDepth}`);
|
|
980
|
-
|
|
981
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
982
|
-
for (const w of result.warnings) {
|
|
983
|
-
lines.push(`Note: ${w.message}`);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
lines.push('');
|
|
988
|
-
|
|
989
|
-
// Render tree
|
|
990
|
-
let hasTruncation = false;
|
|
991
|
-
const renderNode = (node, prefix = '', isLast = true) => {
|
|
992
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
993
|
-
const extension = isLast ? ' ' : '│ ';
|
|
994
|
-
|
|
995
|
-
let label = node.name;
|
|
996
|
-
if (node.file) {
|
|
997
|
-
label += ` (${node.file}:${node.line})`;
|
|
998
|
-
}
|
|
999
|
-
if (node.callSites && node.callSites > 1) {
|
|
1000
|
-
label += ` ${node.callSites}x`;
|
|
1001
|
-
}
|
|
1002
|
-
if (node.entryPoint) {
|
|
1003
|
-
label += ' ★ entry point';
|
|
1004
|
-
}
|
|
1005
|
-
if (node.alreadyShown) {
|
|
1006
|
-
label += ' (see above)';
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
lines.push(prefix + connector + label);
|
|
1010
|
-
|
|
1011
|
-
if (node.children && !node.alreadyShown) {
|
|
1012
|
-
const hasMore = node.truncatedChildren > 0;
|
|
1013
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
1014
|
-
const isChildLast = !hasMore && i === node.children.length - 1;
|
|
1015
|
-
renderNode(node.children[i], prefix + extension, isChildLast);
|
|
1016
|
-
}
|
|
1017
|
-
if (hasMore) {
|
|
1018
|
-
hasTruncation = true;
|
|
1019
|
-
lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
// Root node
|
|
1025
|
-
let rootLabel = result.root;
|
|
1026
|
-
if (result.tree && result.tree.entryPoint) {
|
|
1027
|
-
rootLabel += ' ★ entry point (no callers)';
|
|
1028
|
-
}
|
|
1029
|
-
lines.push(rootLabel);
|
|
1030
|
-
if (result.tree && result.tree.children) {
|
|
1031
|
-
const rootHasMore = result.tree.truncatedChildren > 0;
|
|
1032
|
-
for (let i = 0; i < result.tree.children.length; i++) {
|
|
1033
|
-
const isLast = !rootHasMore && i === result.tree.children.length - 1;
|
|
1034
|
-
renderNode(result.tree.children[i], '', isLast);
|
|
1035
|
-
}
|
|
1036
|
-
if (rootHasMore) {
|
|
1037
|
-
hasTruncation = true;
|
|
1038
|
-
lines.push(`└── ... and ${result.tree.truncatedChildren} more callers`);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Entry points summary
|
|
1043
|
-
if (result.entryPoints && result.entryPoints.length > 0) {
|
|
1044
|
-
lines.push('');
|
|
1045
|
-
lines.push(`Entry points (${result.entryPoints.length}):`);
|
|
1046
|
-
for (const ep of result.entryPoints) {
|
|
1047
|
-
lines.push(` ★ ${ep.name} (${ep.file}:${ep.line})`);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Summary
|
|
1052
|
-
if (result.summary) {
|
|
1053
|
-
lines.push('');
|
|
1054
|
-
const { totalEntryPoints, totalFunctions } = result.summary;
|
|
1055
|
-
if (totalFunctions > 0) {
|
|
1056
|
-
lines.push(`Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`);
|
|
1057
|
-
} else {
|
|
1058
|
-
lines.push('Summary: No callers found — this function is itself an entry point.');
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (hasTruncation) {
|
|
1063
|
-
const allHint = options.allHint || 'Use --all to show all.';
|
|
1064
|
-
lines.push(`\nSome results truncated. ${allHint}`);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (result.includeMethods === false) {
|
|
1068
|
-
lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return lines.join('\n');
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
/**
|
|
1075
|
-
* Format reverse-trace command output - JSON
|
|
1076
|
-
*/
|
|
1077
|
-
function formatReverseTraceJson(result) {
|
|
1078
|
-
if (!result) {
|
|
1079
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
1080
|
-
}
|
|
1081
|
-
return JSON.stringify(result, null, 2);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
/**
|
|
1085
|
-
* Format affected-tests command output - text
|
|
1086
|
-
*/
|
|
1087
|
-
function formatAffectedTests(result, options = {}) {
|
|
1088
|
-
if (!result) return 'Function not found.';
|
|
1089
|
-
|
|
1090
|
-
const lines = [];
|
|
1091
|
-
const { summary } = result;
|
|
1092
|
-
|
|
1093
|
-
lines.push(`affected-tests: ${result.root}`);
|
|
1094
|
-
lines.push('═'.repeat(60));
|
|
1095
|
-
lines.push(`${result.file}:${result.line}`);
|
|
1096
|
-
lines.push(`1 function changed → ${summary.totalAffected} functions affected (depth ${result.depth})`);
|
|
1097
|
-
lines.push('');
|
|
1098
|
-
|
|
1099
|
-
if (result.testFiles.length === 0) {
|
|
1100
|
-
lines.push('No test files found for any affected function.');
|
|
1101
|
-
} else {
|
|
1102
|
-
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
1103
|
-
const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
|
|
1104
|
-
const truncatedFiles = result.testFiles.length - displayFiles.length;
|
|
1105
|
-
lines.push(`Test files to run (${summary.totalTestFiles}):`);
|
|
1106
|
-
lines.push('');
|
|
1107
|
-
for (const tf of displayFiles) {
|
|
1108
|
-
lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
|
|
1109
|
-
// Show up to 5 key matches per file
|
|
1110
|
-
const keyMatches = tf.matches
|
|
1111
|
-
.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
1112
|
-
.slice(0, 5);
|
|
1113
|
-
for (const m of keyMatches) {
|
|
1114
|
-
lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
if (truncatedFiles > 0) {
|
|
1118
|
-
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (result.uncovered.length > 0) {
|
|
1123
|
-
lines.push('');
|
|
1124
|
-
lines.push(`Uncovered (${result.uncovered.length}): ${result.uncovered.join(', ')}`);
|
|
1125
|
-
lines.push(' ⚠ These affected functions have no test references');
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
lines.push('');
|
|
1129
|
-
const pct = summary.totalAffected > 0
|
|
1130
|
-
? Math.round(summary.coveredFunctions / summary.totalAffected * 100)
|
|
1131
|
-
: 0;
|
|
1132
|
-
lines.push(`Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`);
|
|
1133
|
-
|
|
1134
|
-
if (result.warnings?.length > 0) {
|
|
1135
|
-
lines.push('');
|
|
1136
|
-
for (const w of result.warnings) lines.push(`Note: ${w.message}`);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return lines.join('\n');
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function formatAffectedTestsJson(result) {
|
|
1143
|
-
if (!result) {
|
|
1144
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
1145
|
-
}
|
|
1146
|
-
return JSON.stringify(result, null, 2);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Format related command output - text
|
|
1151
|
-
*/
|
|
1152
|
-
function formatRelated(related, options = {}) {
|
|
1153
|
-
if (!related) {
|
|
1154
|
-
return 'Function not found.';
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const lines = [];
|
|
1158
|
-
|
|
1159
|
-
// Header
|
|
1160
|
-
lines.push(`Related to ${related.target.name}`);
|
|
1161
|
-
lines.push('═'.repeat(60));
|
|
1162
|
-
lines.push(`${related.target.file}:${related.target.line}`);
|
|
1163
|
-
lines.push('');
|
|
1164
|
-
|
|
1165
|
-
// Same file
|
|
1166
|
-
let relatedTruncated = false;
|
|
1167
|
-
if (related.sameFile.length > 0) {
|
|
1168
|
-
const maxSameFile = options.top || (options.all ? Infinity : 8);
|
|
1169
|
-
lines.push(`SAME FILE (${related.sameFile.length}):`);
|
|
1170
|
-
for (const f of related.sameFile.slice(0, maxSameFile)) {
|
|
1171
|
-
const params = f.params ? `(${f.params})` : '';
|
|
1172
|
-
lines.push(` :${f.line} ${f.name}${params}`);
|
|
1173
|
-
}
|
|
1174
|
-
if (related.sameFile.length > maxSameFile) {
|
|
1175
|
-
relatedTruncated = true;
|
|
1176
|
-
lines.push(` ... and ${related.sameFile.length - maxSameFile} more`);
|
|
1177
|
-
}
|
|
1178
|
-
lines.push('');
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Similar names
|
|
1182
|
-
if (related.similarNames.length > 0) {
|
|
1183
|
-
lines.push(`SIMILAR NAMES (${related.similarNames.length}):`);
|
|
1184
|
-
for (const s of related.similarNames) {
|
|
1185
|
-
lines.push(` ${s.name} - ${s.file}:${s.line}`);
|
|
1186
|
-
lines.push(` shared: ${s.sharedParts.join(', ')}`);
|
|
1187
|
-
}
|
|
1188
|
-
lines.push('');
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Shared callers
|
|
1192
|
-
if (related.sharedCallers.length > 0) {
|
|
1193
|
-
lines.push(`CALLED BY SAME FUNCTIONS (${related.sharedCallers.length}):`);
|
|
1194
|
-
for (const s of related.sharedCallers) {
|
|
1195
|
-
lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCallerCount} shared callers)`);
|
|
1196
|
-
}
|
|
1197
|
-
lines.push('');
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// Shared callees
|
|
1201
|
-
if (related.sharedCallees.length > 0) {
|
|
1202
|
-
lines.push(`CALLS SAME FUNCTIONS (${related.sharedCallees.length}):`);
|
|
1203
|
-
for (const s of related.sharedCallees) {
|
|
1204
|
-
lines.push(` ${s.name} - ${s.file}:${s.line} (${s.sharedCalleeCount} shared callees)`);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
if (relatedTruncated) {
|
|
1209
|
-
const allHint = options.allHint || 'Use --all to show all.';
|
|
1210
|
-
lines.push(`\nSome sections truncated. ${allHint}`);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
return lines.join('\n');
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
/**
|
|
1217
|
-
* Format related command output - JSON
|
|
1218
|
-
*/
|
|
1219
|
-
function formatRelatedJson(related) {
|
|
1220
|
-
if (!related) {
|
|
1221
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
1222
|
-
}
|
|
1223
|
-
return JSON.stringify(related, null, 2);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
/**
|
|
1227
|
-
* Format impact command output - text
|
|
1228
|
-
* Shows what would need updating if a function signature changes
|
|
1229
|
-
*/
|
|
1230
|
-
function formatImpact(impact, options = {}) {
|
|
1231
|
-
if (!impact) {
|
|
1232
|
-
return 'Function not found.';
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const lines = [];
|
|
1236
|
-
|
|
1237
|
-
// Header
|
|
1238
|
-
lines.push(`Impact analysis for ${impact.function}`);
|
|
1239
|
-
lines.push('═'.repeat(60));
|
|
1240
|
-
lines.push(`${impact.file}:${impact.startLine}`);
|
|
1241
|
-
lines.push(impact.signature);
|
|
1242
|
-
lines.push('');
|
|
1243
|
-
|
|
1244
|
-
// Summary
|
|
1245
|
-
if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
|
|
1246
|
-
lines.push(`CALL SITES: ${impact.shownCallSites} shown of ${impact.totalCallSites} total`);
|
|
1247
|
-
} else {
|
|
1248
|
-
lines.push(`CALL SITES: ${impact.totalCallSites}`);
|
|
1249
|
-
}
|
|
1250
|
-
lines.push(` Files affected: ${impact.byFile.length}`);
|
|
1251
|
-
|
|
1252
|
-
// Patterns
|
|
1253
|
-
const p = impact.patterns;
|
|
1254
|
-
if (p) {
|
|
1255
|
-
const patternParts = [];
|
|
1256
|
-
if (p.constantArgs > 0) patternParts.push(`${p.constantArgs} with literals`);
|
|
1257
|
-
if (p.variableArgs > 0) patternParts.push(`${p.variableArgs} with variables`);
|
|
1258
|
-
if (p.awaitedCalls > 0) patternParts.push(`${p.awaitedCalls} awaited`);
|
|
1259
|
-
if (p.chainedCalls > 0) patternParts.push(`${p.chainedCalls} chained`);
|
|
1260
|
-
if (p.spreadCalls > 0) patternParts.push(`${p.spreadCalls} with spread`);
|
|
1261
|
-
if (patternParts.length > 0) {
|
|
1262
|
-
lines.push(` Patterns: ${patternParts.join(', ')}`);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// Scope pollution warning
|
|
1267
|
-
if (impact.scopeWarning) {
|
|
1268
|
-
lines.push(` Note: ${impact.scopeWarning.hint}`);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
// By file
|
|
1272
|
-
lines.push('');
|
|
1273
|
-
lines.push('BY FILE:');
|
|
1274
|
-
for (const fileGroup of impact.byFile) {
|
|
1275
|
-
lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
|
|
1276
|
-
for (const site of fileGroup.sites) {
|
|
1277
|
-
const caller = site.callerName ? `[${site.callerName}]` : '';
|
|
1278
|
-
lines.push(` :${site.line} ${caller}`);
|
|
1279
|
-
lines.push(` ${site.expression}`);
|
|
1280
|
-
if (site.args && site.args.length > 0) {
|
|
1281
|
-
lines.push(` args: ${site.args.join(', ')}`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
return lines.join('\n');
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Format impact command output - JSON
|
|
1291
|
-
*/
|
|
1292
|
-
function formatImpactJson(impact) {
|
|
1293
|
-
if (!impact) {
|
|
1294
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
1295
|
-
}
|
|
1296
|
-
return JSON.stringify(impact, null, 2);
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
/**
|
|
1300
|
-
* Format plan command output - text
|
|
1301
|
-
* Shows before/after signatures and all changes needed
|
|
1302
|
-
*/
|
|
1303
|
-
function formatPlan(plan, options = {}) {
|
|
1304
|
-
if (!plan) {
|
|
1305
|
-
return 'Function not found.';
|
|
1306
|
-
}
|
|
1307
|
-
if (!plan.found) {
|
|
1308
|
-
return `Function "${plan.function}" not found.`;
|
|
1309
|
-
}
|
|
1310
|
-
if (plan.error) {
|
|
1311
|
-
return `Error: ${plan.error}\nCurrent parameters: ${plan.currentParams?.join(', ') || 'none'}`;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const lines = [];
|
|
1315
|
-
|
|
1316
|
-
// Header
|
|
1317
|
-
lines.push(`Refactoring plan: ${plan.operation}`);
|
|
1318
|
-
lines.push('═'.repeat(60));
|
|
1319
|
-
lines.push(`${plan.file}:${plan.startLine}`);
|
|
1320
|
-
lines.push('');
|
|
1321
|
-
|
|
1322
|
-
// Before/After
|
|
1323
|
-
lines.push('SIGNATURE CHANGE:');
|
|
1324
|
-
lines.push(` Before: ${plan.before.signature}`);
|
|
1325
|
-
lines.push(` After: ${plan.after.signature}`);
|
|
1326
|
-
lines.push('');
|
|
1327
|
-
|
|
1328
|
-
// Summary
|
|
1329
|
-
lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
|
|
1330
|
-
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
1331
|
-
if (plan.scopeWarning) {
|
|
1332
|
-
lines.push(` Note: ${plan.scopeWarning.hint}`);
|
|
1333
|
-
}
|
|
1334
|
-
lines.push('');
|
|
1335
|
-
|
|
1336
|
-
// Group by file
|
|
1337
|
-
const byFile = new Map();
|
|
1338
|
-
for (const change of plan.changes) {
|
|
1339
|
-
if (!byFile.has(change.file)) {
|
|
1340
|
-
byFile.set(change.file, []);
|
|
1341
|
-
}
|
|
1342
|
-
byFile.get(change.file).push(change);
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
lines.push('BY FILE:');
|
|
1346
|
-
for (const [file, changes] of byFile) {
|
|
1347
|
-
lines.push(`\n${file} (${changes.length} changes)`);
|
|
1348
|
-
for (const change of changes) {
|
|
1349
|
-
lines.push(` :${change.line}`);
|
|
1350
|
-
lines.push(` ${change.expression}`);
|
|
1351
|
-
lines.push(` → ${change.suggestion}`);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
return lines.join('\n');
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
* Format stack trace command output - text
|
|
1360
|
-
* Shows code context for each stack frame
|
|
1361
|
-
*/
|
|
1362
|
-
function formatStackTrace(result) {
|
|
1363
|
-
if (!result || result.frameCount === 0) {
|
|
1364
|
-
return 'No stack frames found in input.';
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
const lines = [];
|
|
1368
|
-
lines.push(`Stack trace: ${result.frameCount} frames`);
|
|
1369
|
-
lines.push('═'.repeat(60));
|
|
1370
|
-
|
|
1371
|
-
for (let i = 0; i < result.frames.length; i++) {
|
|
1372
|
-
const frame = result.frames[i];
|
|
1373
|
-
lines.push('');
|
|
1374
|
-
lines.push(`Frame ${i}: ${frame.function || '(anonymous)'}`);
|
|
1375
|
-
lines.push('─'.repeat(40));
|
|
1376
|
-
|
|
1377
|
-
if (frame.found) {
|
|
1378
|
-
lines.push(` ${frame.resolvedFile}:${frame.line}`);
|
|
1379
|
-
|
|
1380
|
-
// Show code context
|
|
1381
|
-
if (frame.context) {
|
|
1382
|
-
lines.push('');
|
|
1383
|
-
for (const ctx of frame.context) {
|
|
1384
|
-
const marker = ctx.isCurrent ? '→ ' : ' ';
|
|
1385
|
-
const lineNum = ctx.line.toString().padStart(4);
|
|
1386
|
-
lines.push(` ${marker}${lineNum} │ ${ctx.code}`);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Show function info if available
|
|
1391
|
-
if (frame.functionInfo) {
|
|
1392
|
-
lines.push('');
|
|
1393
|
-
lines.push(` In: ${frame.functionInfo.name}(${frame.functionInfo.params || ''})`);
|
|
1394
|
-
lines.push(` Range: ${frame.functionInfo.startLine}-${frame.functionInfo.endLine}`);
|
|
1395
|
-
}
|
|
1396
|
-
} else {
|
|
1397
|
-
lines.push(` ${frame.file}:${frame.line} (file not found in project)`);
|
|
1398
|
-
lines.push(` Raw: ${frame.raw}`);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
return lines.join('\n');
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
/**
|
|
1406
|
-
* Format verify command output - text
|
|
1407
|
-
* Shows call site validation results
|
|
1408
|
-
*/
|
|
1409
|
-
function formatVerify(result, options = {}) {
|
|
1410
|
-
if (!result) {
|
|
1411
|
-
return 'Function not found.';
|
|
1412
|
-
}
|
|
1413
|
-
if (!result.found) {
|
|
1414
|
-
return `Function "${result.function}" not found.`;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
const lines = [];
|
|
1418
|
-
|
|
1419
|
-
// Header
|
|
1420
|
-
lines.push(`Verification: ${result.function}`);
|
|
1421
|
-
lines.push('═'.repeat(60));
|
|
1422
|
-
lines.push(`${result.file}:${result.startLine}`);
|
|
1423
|
-
lines.push(result.signature);
|
|
1424
|
-
lines.push('');
|
|
1425
|
-
|
|
1426
|
-
// Expected args
|
|
1427
|
-
const { min, max } = result.expectedArgs;
|
|
1428
|
-
const expectedStr = min === max ? `${min}` : `${min}-${max}`;
|
|
1429
|
-
lines.push(`Expected arguments: ${expectedStr}`);
|
|
1430
|
-
lines.push('');
|
|
1431
|
-
|
|
1432
|
-
// Summary
|
|
1433
|
-
const status = result.mismatches === 0 ? '✓ All calls valid' : '✗ Mismatches found';
|
|
1434
|
-
lines.push(`STATUS: ${status}`);
|
|
1435
|
-
lines.push(` Total calls: ${result.totalCalls}`);
|
|
1436
|
-
lines.push(` Valid: ${result.valid}`);
|
|
1437
|
-
lines.push(` Mismatches: ${result.mismatches}`);
|
|
1438
|
-
lines.push(` Uncertain: ${result.uncertain}`);
|
|
1439
|
-
if (result.scopeWarning) {
|
|
1440
|
-
lines.push(` Note: ${result.scopeWarning.hint}`);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// Show mismatches
|
|
1444
|
-
if (result.mismatchDetails.length > 0) {
|
|
1445
|
-
lines.push('');
|
|
1446
|
-
lines.push('MISMATCHES:');
|
|
1447
|
-
for (const m of result.mismatchDetails) {
|
|
1448
|
-
lines.push(` ${m.file}:${m.line}`);
|
|
1449
|
-
lines.push(` ${m.expression}`);
|
|
1450
|
-
lines.push(` Expected ${m.expected}, got ${m.actual}: [${m.args?.join(', ') || ''}]`);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// Show uncertain
|
|
1455
|
-
if (result.uncertainDetails.length > 0) {
|
|
1456
|
-
lines.push('');
|
|
1457
|
-
lines.push('UNCERTAIN (manual check needed):');
|
|
1458
|
-
for (const u of result.uncertainDetails) {
|
|
1459
|
-
lines.push(` ${u.file}:${u.line}`);
|
|
1460
|
-
lines.push(` ${u.expression}`);
|
|
1461
|
-
lines.push(` Reason: ${u.reason}`);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
return lines.join('\n');
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
/**
|
|
1469
|
-
* Format about command output - text
|
|
1470
|
-
* The "tell me everything" output for AI agents
|
|
1471
|
-
*/
|
|
1472
|
-
function formatAbout(about, options = {}) {
|
|
1473
|
-
if (!about) {
|
|
1474
|
-
return 'Symbol not found.';
|
|
1475
|
-
}
|
|
1476
|
-
if (!about.found) {
|
|
1477
|
-
const lines = ['Symbol not found.\n'];
|
|
1478
|
-
if (about.suggestions && about.suggestions.length > 0) {
|
|
1479
|
-
lines.push('Did you mean:');
|
|
1480
|
-
for (const s of about.suggestions) {
|
|
1481
|
-
lines.push(` ${s.name} (${s.type}) - ${s.file}:${s.line}`);
|
|
1482
|
-
lines.push(` ${s.usageCount} usages`);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
return lines.join('\n');
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
const lines = [];
|
|
1489
|
-
const sym = about.symbol;
|
|
1490
|
-
const { expand, root, depth } = options;
|
|
1491
|
-
|
|
1492
|
-
// Depth=0: location only
|
|
1493
|
-
if (depth !== null && depth !== undefined && Number(depth) === 0) {
|
|
1494
|
-
return `${sym.file}:${sym.startLine}`;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Depth=1: location + signature + usage counts
|
|
1498
|
-
if (depth !== null && depth !== undefined && Number(depth) === 1) {
|
|
1499
|
-
lines.push(`${sym.file}:${sym.startLine}`);
|
|
1500
|
-
if (sym.signature) {
|
|
1501
|
-
lines.push(sym.signature);
|
|
1502
|
-
}
|
|
1503
|
-
lines.push(`(${about.totalUsages} usages: ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} refs)`);
|
|
1504
|
-
return lines.join('\n');
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// Header with signature
|
|
1508
|
-
lines.push(`${sym.name} (${sym.type})`);
|
|
1509
|
-
lines.push('═'.repeat(60));
|
|
1510
|
-
lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}`);
|
|
1511
|
-
if (sym.signature) {
|
|
1512
|
-
lines.push(sym.signature);
|
|
1513
|
-
}
|
|
1514
|
-
if (sym.docstring) {
|
|
1515
|
-
lines.push(`"${sym.docstring}"`);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// Warnings (show early for visibility)
|
|
1519
|
-
if (about.warnings && about.warnings.length > 0) {
|
|
1520
|
-
for (const w of about.warnings) {
|
|
1521
|
-
lines.push(` Note: ${w.message}`);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
if (about.confidenceFiltered) {
|
|
1525
|
-
lines.push(` Note: ${about.confidenceFiltered} edge(s) below confidence threshold hidden`);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// Usage summary
|
|
1529
|
-
lines.push('');
|
|
1530
|
-
lines.push(`USAGES: ${about.totalUsages} total`);
|
|
1531
|
-
lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
|
|
1532
|
-
|
|
1533
|
-
// Callers
|
|
1534
|
-
const showConf = options.showConfidence || false;
|
|
1535
|
-
let aboutTruncated = false;
|
|
1536
|
-
if (about.callers.total > 0) {
|
|
1537
|
-
lines.push('');
|
|
1538
|
-
if (about.callers.total > about.callers.top.length) {
|
|
1539
|
-
lines.push(`CALLERS (showing ${about.callers.top.length} of ${about.callers.total}):`);
|
|
1540
|
-
aboutTruncated = true;
|
|
1541
|
-
} else {
|
|
1542
|
-
lines.push(`CALLERS (${about.callers.total}):`);
|
|
1543
|
-
}
|
|
1544
|
-
for (const c of about.callers.top) {
|
|
1545
|
-
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
1546
|
-
lines.push(` ${c.file}:${c.line} ${caller}`);
|
|
1547
|
-
lines.push(` ${c.expression}`);
|
|
1548
|
-
if (showConf && c.confidence != null) {
|
|
1549
|
-
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// Callees
|
|
1555
|
-
if (about.callees.total > 0) {
|
|
1556
|
-
lines.push('');
|
|
1557
|
-
if (about.callees.total > about.callees.top.length) {
|
|
1558
|
-
lines.push(`CALLEES (showing ${about.callees.top.length} of ${about.callees.total}):`);
|
|
1559
|
-
aboutTruncated = true;
|
|
1560
|
-
} else {
|
|
1561
|
-
lines.push(`CALLEES (${about.callees.total}):`);
|
|
1562
|
-
}
|
|
1563
|
-
for (const c of about.callees.top) {
|
|
1564
|
-
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
1565
|
-
lines.push(` ${c.name}${weight} - ${c.file}:${c.line} (${c.callCount}x)`);
|
|
1566
|
-
if (showConf && c.confidence != null) {
|
|
1567
|
-
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
// Inline expansion: show first 3 lines of callee code
|
|
1571
|
-
if (expand && root && c.file && c.startLine) {
|
|
1572
|
-
try {
|
|
1573
|
-
const filePath = path.isAbsolute(c.file) ? c.file : path.join(root, c.file);
|
|
1574
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1575
|
-
const fileLines = content.split('\n');
|
|
1576
|
-
const endLine = c.endLine || c.startLine + 5;
|
|
1577
|
-
const previewLines = Math.min(3, endLine - c.startLine + 1);
|
|
1578
|
-
for (let i = 0; i < previewLines && c.startLine - 1 + i < fileLines.length; i++) {
|
|
1579
|
-
const codeLine = fileLines[c.startLine - 1 + i];
|
|
1580
|
-
lines.push(` │ ${codeLine}`);
|
|
1581
|
-
}
|
|
1582
|
-
if (endLine - c.startLine + 1 > 3) {
|
|
1583
|
-
lines.push(` │ ... (${endLine - c.startLine - 2} more lines)`);
|
|
1584
|
-
}
|
|
1585
|
-
} catch (e) {
|
|
1586
|
-
// Skip expansion on error
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// Tests
|
|
1593
|
-
if (about.tests.totalMatches > 0) {
|
|
1594
|
-
lines.push('');
|
|
1595
|
-
if (about.tests.fileCount > about.tests.files.length) {
|
|
1596
|
-
lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s), showing ${about.tests.files.length}:`);
|
|
1597
|
-
aboutTruncated = true;
|
|
1598
|
-
} else {
|
|
1599
|
-
lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s)`);
|
|
1600
|
-
}
|
|
1601
|
-
for (const f of about.tests.files) {
|
|
1602
|
-
lines.push(` ${f}`);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// Other definitions
|
|
1607
|
-
if (about.otherDefinitions.length > 0) {
|
|
1608
|
-
lines.push('');
|
|
1609
|
-
lines.push(`OTHER DEFINITIONS (${about.otherDefinitions.length}):`);
|
|
1610
|
-
for (const d of about.otherDefinitions) {
|
|
1611
|
-
lines.push(` ${d.file}:${d.line} (${d.usageCount} usages)`);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// Types
|
|
1616
|
-
if (about.types && about.types.length > 0) {
|
|
1617
|
-
lines.push('');
|
|
1618
|
-
lines.push('TYPES:');
|
|
1619
|
-
for (const t of about.types) {
|
|
1620
|
-
lines.push(` ${t.name} (${t.type}) - ${t.file}:${t.line}`);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Completeness warnings (condensed single line)
|
|
1625
|
-
if (about.completeness && about.completeness.warnings && about.completeness.warnings.length > 0) {
|
|
1626
|
-
const parts = about.completeness.warnings.map(w => `${w.count} ${w.type.replace('_', ' ')}`);
|
|
1627
|
-
lines.push('');
|
|
1628
|
-
lines.push(`Note: Results may be incomplete (${parts.join(', ')} in project)`);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// Code
|
|
1632
|
-
if (about.code) {
|
|
1633
|
-
lines.push('');
|
|
1634
|
-
lines.push('─── CODE ───');
|
|
1635
|
-
lines.push(about.code);
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
if (aboutTruncated) {
|
|
1639
|
-
const allHint = options.allHint || 'Use --all to show all.';
|
|
1640
|
-
lines.push(`\nSome sections truncated. ${allHint}`);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (about.includeMethods === false) {
|
|
1644
|
-
const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
|
|
1645
|
-
lines.push(`\n${methodsHint}`);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
return lines.join('\n');
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
/**
|
|
1652
|
-
* Format about command output - JSON
|
|
1653
|
-
*/
|
|
1654
|
-
function formatAboutJson(about) {
|
|
1655
|
-
if (!about) {
|
|
1656
|
-
return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
|
|
1657
|
-
}
|
|
1658
|
-
return JSON.stringify(about, null, 2);
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
/**
|
|
1662
|
-
* Format example result as text
|
|
1663
|
-
*/
|
|
1664
|
-
function formatExample(result, name) {
|
|
1665
|
-
if (!result || !result.best) return `No call examples found for "${name}"`;
|
|
1666
|
-
|
|
1667
|
-
const best = result.best;
|
|
1668
|
-
const lines = [];
|
|
1669
|
-
lines.push(`Best example of "${name}":`);
|
|
1670
|
-
lines.push('═'.repeat(60));
|
|
1671
|
-
lines.push(`${best.relativePath}:${best.line}`);
|
|
1672
|
-
lines.push('');
|
|
1673
|
-
|
|
1674
|
-
if (best.before) {
|
|
1675
|
-
for (let i = 0; i < best.before.length; i++) {
|
|
1676
|
-
const ln = best.line - best.before.length + i;
|
|
1677
|
-
lines.push(`${ln.toString().padStart(4)}| ${best.before[i]}`);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
lines.push(`${best.line.toString().padStart(4)}| ${best.content} <--`);
|
|
1682
|
-
|
|
1683
|
-
if (best.after) {
|
|
1684
|
-
for (let i = 0; i < best.after.length; i++) {
|
|
1685
|
-
const ln = best.line + i + 1;
|
|
1686
|
-
lines.push(`${ln.toString().padStart(4)}| ${best.after[i]}`);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
lines.push('');
|
|
1691
|
-
lines.push(`Score: ${best.score} (${result.totalCalls} total calls)`);
|
|
1692
|
-
lines.push(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
|
|
1693
|
-
|
|
1694
|
-
return lines.join('\n');
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// ============================================================================
|
|
1698
|
-
// TEXT FORMATTERS (shared between CLI and MCP)
|
|
1699
|
-
// ============================================================================
|
|
1700
|
-
|
|
1701
|
-
/**
|
|
1702
|
-
* Format toc command output
|
|
1703
|
-
* @param {object} toc - TOC data
|
|
1704
|
-
* @param {object} [options] - Formatting options
|
|
1705
|
-
* @param {string} [options.detailedHint] - Custom hint text for non-detailed mode
|
|
1706
|
-
* @param {string} [options.uncertainHint] - Custom hint text for uncertain references
|
|
1707
|
-
*/
|
|
1708
|
-
function formatToc(toc, options = {}) {
|
|
1709
|
-
const lines = [];
|
|
1710
|
-
const t = toc.totals;
|
|
1711
|
-
lines.push(`PROJECT: ${t.files} files, ${t.lines} lines`);
|
|
1712
|
-
lines.push(` ${t.functions} functions, ${t.classes} types (classes/interfaces/enums), ${t.state} state objects`);
|
|
1713
|
-
|
|
1714
|
-
const meta = toc.meta || {};
|
|
1715
|
-
if (meta.filteredBy) {
|
|
1716
|
-
lines.push(` Filtered by: --file=${meta.filteredBy} (${meta.matchedFiles} files matched)`);
|
|
1717
|
-
if (meta.emptyFiles) {
|
|
1718
|
-
lines.push(` Note: ${meta.emptyFiles} file(s) have no detected symbols (may be generated or data files)`);
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
const warnings = [];
|
|
1722
|
-
if (meta.dynamicImports) { const dn = dynamicImportsNote(meta.dynamicImports, meta); if (dn) warnings.push(dn); }
|
|
1723
|
-
if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
|
|
1724
|
-
if (warnings.length) {
|
|
1725
|
-
const uncertainSuffix = meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
|
|
1726
|
-
lines.push(` Note: ${warnings.join(', ')}${uncertainSuffix}`);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
if (toc.summary) {
|
|
1730
|
-
if (toc.summary.topFunctionFiles?.length) {
|
|
1731
|
-
const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
|
|
1732
|
-
lines.push(` Most functions: ${hint}`);
|
|
1733
|
-
}
|
|
1734
|
-
if (toc.summary.topLineFiles?.length) {
|
|
1735
|
-
const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
|
|
1736
|
-
lines.push(` Largest files: ${hint}`);
|
|
1737
|
-
}
|
|
1738
|
-
if (toc.summary.entryFiles?.length) {
|
|
1739
|
-
lines.push(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
lines.push('═'.repeat(60));
|
|
1744
|
-
const hasDetail = toc.files.some(f => f.symbols);
|
|
1745
|
-
for (const file of toc.files) {
|
|
1746
|
-
const parts = [`${file.lines} lines`];
|
|
1747
|
-
if (file.functions) parts.push(`${file.functions} fn`);
|
|
1748
|
-
if (file.classes) parts.push(`${file.classes} types`);
|
|
1749
|
-
if (file.state) parts.push(`${file.state} state`);
|
|
1750
|
-
|
|
1751
|
-
if (hasDetail) {
|
|
1752
|
-
lines.push(`\n${file.file} (${parts.join(', ')})`);
|
|
1753
|
-
if (file.symbols) {
|
|
1754
|
-
for (const fn of file.symbols.functions) {
|
|
1755
|
-
lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${formatFunctionSignature(fn)}`);
|
|
1756
|
-
}
|
|
1757
|
-
for (const cls of file.symbols.classes) {
|
|
1758
|
-
lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
} else {
|
|
1762
|
-
lines.push(` ${file.file} — ${parts.join(', ')}`);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (!hasDetail) {
|
|
1767
|
-
const hint = options.detailedHint || 'Use detailed=true to list all functions and classes.';
|
|
1768
|
-
lines.push(`\n${hint}`);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
if (toc.hiddenFiles > 0) {
|
|
1772
|
-
const topHint = options.topHint || 'Use --top=N or --all to show more.';
|
|
1773
|
-
lines.push(`\n... and ${toc.hiddenFiles} more files. ${topHint}`);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
return lines.join('\n');
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
/**
|
|
1780
|
-
* Format find command output
|
|
1781
|
-
*/
|
|
1782
|
-
function formatFind(symbols, query, top) {
|
|
1783
|
-
if (symbols.length === 0) {
|
|
1784
|
-
return `No symbols found for "${query}"`;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
const lines = [];
|
|
1788
|
-
const limit = (top && top > 0) ? Math.min(symbols.length, top) : Math.min(symbols.length, 10);
|
|
1789
|
-
const hidden = symbols.length - limit;
|
|
1790
|
-
|
|
1791
|
-
if (hidden > 0) {
|
|
1792
|
-
lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${limit}):`);
|
|
1793
|
-
} else {
|
|
1794
|
-
lines.push(`Found ${symbols.length} match(es) for "${query}":`);
|
|
1795
|
-
}
|
|
1796
|
-
lines.push('─'.repeat(60));
|
|
1797
|
-
|
|
1798
|
-
for (let i = 0; i < limit; i++) {
|
|
1799
|
-
const s = symbols[i];
|
|
1800
|
-
const sig = s.params !== undefined
|
|
1801
|
-
? formatFunctionSignature(s)
|
|
1802
|
-
: formatClassSignature(s);
|
|
1803
|
-
lines.push(`${s.relativePath}:${s.startLine} ${sig}`);
|
|
1804
|
-
if (s.usageCounts !== undefined) {
|
|
1805
|
-
const c = s.usageCounts;
|
|
1806
|
-
const parts = [];
|
|
1807
|
-
if (c.calls > 0) parts.push(`${c.calls} calls`);
|
|
1808
|
-
if (c.definitions > 0) parts.push(`${c.definitions} def`);
|
|
1809
|
-
if (c.imports > 0) parts.push(`${c.imports} imports`);
|
|
1810
|
-
if (c.references > 0) parts.push(`${c.references} refs`);
|
|
1811
|
-
lines.push(parts.length > 0
|
|
1812
|
-
? ` (${c.total} usages: ${parts.join(', ')})`
|
|
1813
|
-
: ` (${c.total} usages)`);
|
|
1814
|
-
} else if (s.usageCount !== undefined) {
|
|
1815
|
-
lines.push(` (${s.usageCount} usages)`);
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
if (hidden > 0) {
|
|
1820
|
-
lines.push(`... ${hidden} more result(s).`);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
return lines.join('\n');
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
/**
|
|
1827
|
-
* Format usages command output
|
|
1828
|
-
*/
|
|
1829
|
-
function formatUsages(usages, name) {
|
|
1830
|
-
const defs = usages.filter(u => u.isDefinition);
|
|
1831
|
-
const calls = usages.filter(u => u.usageType === 'call');
|
|
1832
|
-
const imports = usages.filter(u => u.usageType === 'import');
|
|
1833
|
-
const refs = usages.filter(u => !u.isDefinition && u.usageType === 'reference');
|
|
1834
|
-
|
|
1835
|
-
const lines = [];
|
|
1836
|
-
lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
|
|
1837
|
-
lines.push('═'.repeat(60));
|
|
1838
|
-
|
|
1839
|
-
function renderContextLines(usage) {
|
|
1840
|
-
if (usage.before && usage.before.length > 0) {
|
|
1841
|
-
for (const line of usage.before) {
|
|
1842
|
-
lines.push(` ${line}`);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
function renderAfterLines(usage) {
|
|
1848
|
-
if (usage.after && usage.after.length > 0) {
|
|
1849
|
-
for (const line of usage.after) {
|
|
1850
|
-
lines.push(` ${line}`);
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
if (defs.length > 0) {
|
|
1856
|
-
lines.push('\nDEFINITIONS:');
|
|
1857
|
-
for (const d of defs) {
|
|
1858
|
-
lines.push(` ${d.relativePath}:${d.line || d.startLine}`);
|
|
1859
|
-
if (d.signature) lines.push(` ${d.signature}`);
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
if (calls.length > 0) {
|
|
1864
|
-
lines.push('\nCALLS:');
|
|
1865
|
-
for (const c of calls) {
|
|
1866
|
-
lines.push(` ${c.relativePath}:${c.line}`);
|
|
1867
|
-
renderContextLines(c);
|
|
1868
|
-
lines.push(` ${c.content.trim()}`);
|
|
1869
|
-
renderAfterLines(c);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
if (imports.length > 0) {
|
|
1874
|
-
lines.push('\nIMPORTS:');
|
|
1875
|
-
for (const i of imports) {
|
|
1876
|
-
lines.push(` ${i.relativePath}:${i.line}`);
|
|
1877
|
-
lines.push(` ${i.content.trim()}`);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
if (refs.length > 0) {
|
|
1882
|
-
lines.push('\nREFERENCES:');
|
|
1883
|
-
for (const r of refs) {
|
|
1884
|
-
lines.push(` ${r.relativePath}:${r.line}`);
|
|
1885
|
-
renderContextLines(r);
|
|
1886
|
-
lines.push(` ${r.content.trim()}`);
|
|
1887
|
-
renderAfterLines(r);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
return lines.join('\n');
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
/**
|
|
1895
|
-
* Format context command output
|
|
1896
|
-
* Returns { text, expandable } where expandable is an array of items for expand
|
|
1897
|
-
* @param {object} ctx - Context data
|
|
1898
|
-
* @param {object} [options] - Formatting options
|
|
1899
|
-
* @param {string} [options.methodsHint] - Custom hint for excluded method calls
|
|
1900
|
-
* @param {string} [options.expandHint] - Custom hint for expand command
|
|
1901
|
-
* @param {string} [options.uncertainHint] - Custom hint for uncertain calls
|
|
1902
|
-
*/
|
|
1903
|
-
function formatContext(ctx, options = {}) {
|
|
1904
|
-
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
1905
|
-
|
|
1906
|
-
const expandHint = options.expandHint || 'Use ucn_expand with item number to see code for any item.';
|
|
1907
|
-
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
|
|
1908
|
-
|
|
1909
|
-
const lines = [];
|
|
1910
|
-
const expandable = [];
|
|
1911
|
-
let itemNum = 1;
|
|
1912
|
-
|
|
1913
|
-
// Handle struct/interface types
|
|
1914
|
-
if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
|
|
1915
|
-
lines.push(`Context for ${ctx.type} ${ctx.name}:`);
|
|
1916
|
-
lines.push('═'.repeat(60));
|
|
1917
|
-
|
|
1918
|
-
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
1919
|
-
for (const w of ctx.warnings) {
|
|
1920
|
-
lines.push(` Note: ${w.message}`);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
const methods = ctx.methods || [];
|
|
1925
|
-
lines.push(`\nMETHODS (${methods.length}):`);
|
|
1926
|
-
for (const m of methods) {
|
|
1927
|
-
const receiver = m.receiver ? `(${m.receiver}) ` : '';
|
|
1928
|
-
const params = m.params || '...';
|
|
1929
|
-
const returnType = m.returnType ? `: ${m.returnType}` : '';
|
|
1930
|
-
lines.push(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
|
|
1931
|
-
lines.push(` ${m.file}:${m.line}`);
|
|
1932
|
-
expandable.push({
|
|
1933
|
-
num: itemNum++,
|
|
1934
|
-
type: 'method',
|
|
1935
|
-
name: m.name,
|
|
1936
|
-
file: null,
|
|
1937
|
-
relativePath: m.file,
|
|
1938
|
-
startLine: m.line,
|
|
1939
|
-
endLine: m.endLine || m.line
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
const callers = ctx.callers || [];
|
|
1944
|
-
lines.push(`\nCALLERS (${callers.length}):`);
|
|
1945
|
-
for (const c of callers) {
|
|
1946
|
-
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
1947
|
-
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
1948
|
-
lines.push(` ${c.content.trim()}`);
|
|
1949
|
-
expandable.push({
|
|
1950
|
-
num: itemNum++,
|
|
1951
|
-
type: 'caller',
|
|
1952
|
-
name: c.callerName || '(module level)',
|
|
1953
|
-
file: c.callerFile || c.file,
|
|
1954
|
-
relativePath: c.relativePath,
|
|
1955
|
-
line: c.line,
|
|
1956
|
-
startLine: c.callerStartLine || c.line,
|
|
1957
|
-
endLine: c.callerEndLine || c.line
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
if (expandable.length > 0) {
|
|
1962
|
-
lines.push(`\n${expandHint}`);
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
return { text: lines.join('\n'), expandable };
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
// Standard function/method context
|
|
1969
|
-
lines.push(`Context for ${ctx.function}:`);
|
|
1970
|
-
lines.push('═'.repeat(60));
|
|
1971
|
-
|
|
1972
|
-
if (ctx.meta) {
|
|
1973
|
-
const notes = [];
|
|
1974
|
-
if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
|
|
1975
|
-
if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
|
|
1976
|
-
if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below confidence threshold hidden`);
|
|
1977
|
-
if (notes.length) {
|
|
1978
|
-
const uncertainSuffix = ctx.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
|
|
1979
|
-
lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
if (ctx.meta && ctx.meta.includeMethods === false) {
|
|
1984
|
-
lines.push(` ${methodsHint}`);
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
1988
|
-
for (const w of ctx.warnings) {
|
|
1989
|
-
lines.push(` Note: ${w.message}`);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
const showConf = options.showConfidence || false;
|
|
1994
|
-
const callers = ctx.callers || [];
|
|
1995
|
-
lines.push(`\nCALLERS (${callers.length}):`);
|
|
1996
|
-
for (const c of callers) {
|
|
1997
|
-
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
1998
|
-
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
1999
|
-
lines.push(` ${c.content.trim()}`);
|
|
2000
|
-
if (showConf && c.confidence != null) {
|
|
2001
|
-
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
2002
|
-
}
|
|
2003
|
-
expandable.push({
|
|
2004
|
-
num: itemNum++,
|
|
2005
|
-
type: 'caller',
|
|
2006
|
-
name: c.callerName || '(module level)',
|
|
2007
|
-
file: c.callerFile || c.file,
|
|
2008
|
-
relativePath: c.relativePath,
|
|
2009
|
-
line: c.line,
|
|
2010
|
-
startLine: c.callerStartLine || c.line,
|
|
2011
|
-
endLine: c.callerEndLine || c.line
|
|
2012
|
-
});
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
// Structural hint: class methods may have callers through constructed/injected instances
|
|
2016
|
-
// that static analysis can't track. Only show when caller count is low (≤3) to avoid noise.
|
|
2017
|
-
if (ctx.meta && (ctx.meta.isMethod || ctx.meta.className || ctx.meta.receiver) && callers.length <= 3) {
|
|
2018
|
-
lines.push(` Note: ${ctx.function} is a class/struct method — additional callers through constructed or injected instances are not tracked by static analysis.`);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
const callees = ctx.callees || [];
|
|
2022
|
-
lines.push(`\nCALLEES (${callees.length}):`);
|
|
2023
|
-
for (const c of callees) {
|
|
2024
|
-
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
2025
|
-
lines.push(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
|
|
2026
|
-
if (showConf && c.confidence != null) {
|
|
2027
|
-
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
2028
|
-
}
|
|
2029
|
-
expandable.push({
|
|
2030
|
-
num: itemNum++,
|
|
2031
|
-
type: 'callee',
|
|
2032
|
-
name: c.name,
|
|
2033
|
-
file: c.file,
|
|
2034
|
-
relativePath: c.relativePath,
|
|
2035
|
-
startLine: c.startLine,
|
|
2036
|
-
endLine: c.endLine
|
|
2037
|
-
});
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
if (expandable.length > 0) {
|
|
2041
|
-
lines.push(`\n${expandHint}`);
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
return { text: lines.join('\n'), expandable };
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
/**
|
|
2048
|
-
* Format smart command output
|
|
2049
|
-
* @param {object} smart - Smart extraction result
|
|
2050
|
-
* @param {object} [options] - Formatting options
|
|
2051
|
-
* @param {string} [options.uncertainHint] - Custom hint for uncertain calls
|
|
2052
|
-
*/
|
|
2053
|
-
function formatSmart(smart, options = {}) {
|
|
2054
|
-
if (!smart) return 'Function not found.';
|
|
2055
|
-
|
|
2056
|
-
const lines = [];
|
|
2057
|
-
lines.push(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
|
|
2058
|
-
lines.push('═'.repeat(60));
|
|
2059
|
-
|
|
2060
|
-
if (smart.meta) {
|
|
2061
|
-
const notes = [];
|
|
2062
|
-
if (smart.meta.dynamicImports) { const dn = dynamicImportsNote(smart.meta.dynamicImports, smart.meta); if (dn) notes.push(dn); }
|
|
2063
|
-
if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
|
|
2064
|
-
if (notes.length) {
|
|
2065
|
-
const uncertainSuffix = smart.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
|
|
2066
|
-
lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
lines.push(smart.target.code);
|
|
2071
|
-
|
|
2072
|
-
if (smart.dependencies.length > 0) {
|
|
2073
|
-
lines.push('\n─── DEPENDENCIES ───');
|
|
2074
|
-
for (const dep of smart.dependencies) {
|
|
2075
|
-
const weight = dep.weight && dep.weight !== 'normal' ? ` [${dep.weight}]` : '';
|
|
2076
|
-
lines.push(`\n// ${dep.name}${weight} (${dep.relativePath}:${dep.startLine})`);
|
|
2077
|
-
lines.push(dep.code);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
if (smart.types && smart.types.length > 0) {
|
|
2082
|
-
lines.push('\n─── TYPES ───');
|
|
2083
|
-
for (const t of smart.types) {
|
|
2084
|
-
lines.push(`\n// ${t.name} (${t.relativePath}:${t.startLine})`);
|
|
2085
|
-
lines.push(t.code);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
return lines.join('\n');
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
/**
|
|
2093
|
-
* Format deadcode command output
|
|
2094
|
-
* @param {Array} results - Dead code results
|
|
2095
|
-
* @param {object} [options] - Formatting options
|
|
2096
|
-
* @param {string} [options.exportedHint] - Hint about exported symbols exclusion
|
|
2097
|
-
*/
|
|
2098
|
-
function formatDeadcode(results, options = {}) {
|
|
2099
|
-
if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
|
|
2100
|
-
return 'No dead code found.';
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
const lines = [];
|
|
2104
|
-
const top = options.top > 0 ? options.top : 0;
|
|
2105
|
-
const showing = top > 0 ? results.slice(0, top) : results;
|
|
2106
|
-
const hidden = results.length - showing.length;
|
|
2107
|
-
|
|
2108
|
-
if (results.length > 0) {
|
|
2109
|
-
if (hidden > 0) {
|
|
2110
|
-
lines.push(`Dead code: ${results.length} unused symbol(s) (showing ${showing.length})\n`);
|
|
2111
|
-
} else {
|
|
2112
|
-
lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
let currentFile = null;
|
|
2117
|
-
for (const item of showing) {
|
|
2118
|
-
if (item.file !== currentFile) {
|
|
2119
|
-
currentFile = item.file;
|
|
2120
|
-
lines.push(item.file);
|
|
2121
|
-
}
|
|
2122
|
-
const exported = item.isExported ? ' [exported]' : '';
|
|
2123
|
-
// Surface decorators/annotations — structural hint that a framework may invoke this
|
|
2124
|
-
const hints = [];
|
|
2125
|
-
if (item.decorators && item.decorators.length > 0) {
|
|
2126
|
-
hints.push(...item.decorators.map(d => `@${d}`));
|
|
2127
|
-
}
|
|
2128
|
-
if (item.annotations && item.annotations.length > 0) {
|
|
2129
|
-
hints.push(...item.annotations.map(a => `@${a}`));
|
|
2130
|
-
}
|
|
2131
|
-
const hintStr = hints.length > 0 ? ` [has ${hints.join(', ')}]` : '';
|
|
2132
|
-
lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}`);
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
if (hidden > 0) {
|
|
2136
|
-
lines.push(`\n${hidden} more result(s) not shown. Use --top=${results.length} or --all to see all.`);
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
// Show counts of excluded items with expansion hints
|
|
2140
|
-
if (results.length === 0) {
|
|
2141
|
-
lines.push('No dead code found.');
|
|
2142
|
-
}
|
|
2143
|
-
if (results.excludedDecorated > 0) {
|
|
2144
|
-
const decoratedHint = options.decoratedHint || `${results.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.`;
|
|
2145
|
-
lines.push(`\n${decoratedHint}`);
|
|
2146
|
-
}
|
|
2147
|
-
if (results.excludedExported > 0) {
|
|
2148
|
-
const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
|
|
2149
|
-
lines.push(`\n${exportedHint}`);
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
if (lines.length === 0) {
|
|
2153
|
-
return 'No dead code found.';
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
return lines.join('\n');
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
/**
|
|
2160
|
-
* Format fn command output
|
|
2161
|
-
*/
|
|
2162
|
-
function formatFn(match, fnCode) {
|
|
2163
|
-
const lines = [];
|
|
2164
|
-
lines.push(`${match.relativePath}:${match.startLine}`);
|
|
2165
|
-
lines.push(`${lineRange(match.startLine, match.endLine)} ${formatFunctionSignature(match)}`);
|
|
2166
|
-
lines.push('─'.repeat(60));
|
|
2167
|
-
lines.push(fnCode);
|
|
2168
|
-
return lines.join('\n');
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
/**
|
|
2172
|
-
* Format class command output
|
|
2173
|
-
*/
|
|
2174
|
-
function formatClass(cls, clsCode) {
|
|
2175
|
-
const lines = [];
|
|
2176
|
-
lines.push(`${cls.relativePath || cls.file}:${cls.startLine}`);
|
|
2177
|
-
lines.push(`${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
|
|
2178
|
-
lines.push('─'.repeat(60));
|
|
2179
|
-
lines.push(clsCode);
|
|
2180
|
-
return lines.join('\n');
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
/**
|
|
2184
|
-
* Format graph command output
|
|
2185
|
-
* @param {object} graph - Graph data
|
|
2186
|
-
* @param {object} [options] - Formatting options
|
|
2187
|
-
* @param {boolean} [options.showAll] - Show all children (no truncation)
|
|
2188
|
-
* @param {number} [options.maxDepth] - Maximum depth for tree traversal
|
|
2189
|
-
*/
|
|
2190
|
-
function formatGraph(graph, options = {}) {
|
|
2191
|
-
// Support legacy signature: formatGraph(graph, showAll)
|
|
2192
|
-
if (typeof options === 'boolean') {
|
|
2193
|
-
options = { showAll: options };
|
|
2194
|
-
}
|
|
2195
|
-
if (graph?.error) return formatFileError(graph);
|
|
2196
|
-
if (graph.nodes.length === 0) {
|
|
2197
|
-
const file = options.file || graph.root || '';
|
|
2198
|
-
return file ? `File not found: ${file}` : 'File not found.';
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
const rootEntry = graph.nodes.find(n => n.file === graph.root);
|
|
2202
|
-
const rootRelPath = rootEntry ? rootEntry.relativePath : graph.root;
|
|
2203
|
-
const lines = [];
|
|
2204
|
-
|
|
2205
|
-
const showAll = options.showAll || false;
|
|
2206
|
-
const maxChildren = showAll ? Infinity : 8;
|
|
2207
|
-
const maxDepth = options.maxDepth !== undefined ? options.maxDepth : Infinity;
|
|
2208
|
-
|
|
2209
|
-
function printTree(nodes, edges, rootFile) {
|
|
2210
|
-
const visited = new Set(); // all nodes ever printed (for diamond dep detection)
|
|
2211
|
-
const ancestors = new Set(); // current path from root (for true circular detection)
|
|
2212
|
-
let truncatedNodes = 0;
|
|
2213
|
-
let depthLimited = false;
|
|
2214
|
-
|
|
2215
|
-
function printNode(file, indent = 0, isLast = true) {
|
|
2216
|
-
const fileEntry = nodes.find(n => n.file === file);
|
|
2217
|
-
const relPath = fileEntry ? fileEntry.relativePath : file;
|
|
2218
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
2219
|
-
const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + connector;
|
|
2220
|
-
|
|
2221
|
-
if (ancestors.has(file)) {
|
|
2222
|
-
lines.push(`${prefix}${relPath} (circular)`);
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
if (visited.has(file)) {
|
|
2226
|
-
lines.push(`${prefix}${relPath} (already shown)`);
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
visited.add(file);
|
|
2230
|
-
|
|
2231
|
-
if (indent > maxDepth) {
|
|
2232
|
-
depthLimited = true;
|
|
2233
|
-
lines.push(`${prefix}${relPath} ...`);
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
lines.push(`${prefix}${relPath}`);
|
|
2238
|
-
|
|
2239
|
-
ancestors.add(file);
|
|
2240
|
-
const fileEdges = edges.filter(e => e.from === file);
|
|
2241
|
-
const displayEdges = fileEdges.slice(0, maxChildren);
|
|
2242
|
-
const hiddenCount = fileEdges.length - displayEdges.length;
|
|
2243
|
-
|
|
2244
|
-
for (let i = 0; i < displayEdges.length; i++) {
|
|
2245
|
-
const childIsLast = i === displayEdges.length - 1 && hiddenCount === 0;
|
|
2246
|
-
printNode(displayEdges[i].to, indent + 1, childIsLast);
|
|
2247
|
-
}
|
|
2248
|
-
ancestors.delete(file);
|
|
2249
|
-
|
|
2250
|
-
if (hiddenCount > 0) {
|
|
2251
|
-
truncatedNodes += hiddenCount;
|
|
2252
|
-
lines.push(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
printNode(rootFile);
|
|
2257
|
-
return { truncatedNodes, depthLimited };
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
if (graph.direction === 'both' && graph.imports && graph.importers) {
|
|
2261
|
-
const importCount = graph.imports.edges.filter(e => e.from === graph.root).length;
|
|
2262
|
-
const importerCount = graph.importers.edges.filter(e => e.from === graph.root).length;
|
|
2263
|
-
|
|
2264
|
-
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
2265
|
-
lines.push('═'.repeat(60));
|
|
2266
|
-
|
|
2267
|
-
let totalTruncated = 0;
|
|
2268
|
-
let anyDepthLimited = false;
|
|
2269
|
-
|
|
2270
|
-
lines.push(`\nIMPORTS (what this file depends on): ${importCount} files`);
|
|
2271
|
-
if (importCount > 0) {
|
|
2272
|
-
const r = printTree(graph.imports.nodes, graph.imports.edges, graph.root);
|
|
2273
|
-
totalTruncated += r.truncatedNodes;
|
|
2274
|
-
anyDepthLimited = anyDepthLimited || r.depthLimited;
|
|
2275
|
-
} else {
|
|
2276
|
-
lines.push(' (none)');
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
lines.push(`\nIMPORTERS (what depends on this file): ${importerCount} files`);
|
|
2280
|
-
if (importerCount > 0) {
|
|
2281
|
-
const r = printTree(graph.importers.nodes, graph.importers.edges, graph.root);
|
|
2282
|
-
totalTruncated += r.truncatedNodes;
|
|
2283
|
-
anyDepthLimited = anyDepthLimited || r.depthLimited;
|
|
2284
|
-
} else {
|
|
2285
|
-
lines.push(' (none)');
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
if (anyDepthLimited || totalTruncated > 0) {
|
|
2289
|
-
lines.push('\n' + '─'.repeat(60));
|
|
2290
|
-
if (anyDepthLimited) {
|
|
2291
|
-
const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
|
|
2292
|
-
lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
|
|
2293
|
-
}
|
|
2294
|
-
if (totalTruncated > 0) {
|
|
2295
|
-
const allHint = options.allHint || 'Use --all to show all children.';
|
|
2296
|
-
lines.push(`${totalTruncated} nodes hidden. ${allHint}`);
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
} else {
|
|
2300
|
-
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
2301
|
-
lines.push('═'.repeat(60));
|
|
2302
|
-
|
|
2303
|
-
const { truncatedNodes, depthLimited } = printTree(graph.nodes, graph.edges, graph.root);
|
|
2304
|
-
|
|
2305
|
-
if (depthLimited || truncatedNodes > 0) {
|
|
2306
|
-
lines.push('\n' + '─'.repeat(60));
|
|
2307
|
-
if (depthLimited) {
|
|
2308
|
-
const depthHint = options.depthHint || `Use --depth=N for deeper graph.`;
|
|
2309
|
-
lines.push(`Depth limited to ${maxDepth}. ${depthHint}`);
|
|
2310
|
-
}
|
|
2311
|
-
if (truncatedNodes > 0) {
|
|
2312
|
-
const allHint = options.allHint || 'Use --all to show all children.';
|
|
2313
|
-
lines.push(`${truncatedNodes} nodes hidden. ${allHint} Graph has ${graph.nodes.length} total files.`);
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
return lines.join('\n');
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
function formatCircularDeps(result) {
|
|
2322
|
-
if (!result) return 'No results.';
|
|
2323
|
-
const lines = [];
|
|
2324
|
-
|
|
2325
|
-
lines.push('Circular dependencies');
|
|
2326
|
-
lines.push('═'.repeat(60));
|
|
2327
|
-
|
|
2328
|
-
if (result.fileFilter) {
|
|
2329
|
-
lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
const scannedCount = result.filesWithImports != null ? result.filesWithImports : result.totalFiles;
|
|
2333
|
-
|
|
2334
|
-
if (result.cycles.length === 0) {
|
|
2335
|
-
lines.push('');
|
|
2336
|
-
lines.push('No circular dependencies found.');
|
|
2337
|
-
lines.push(`Scanned ${scannedCount} files with import relationships.`);
|
|
2338
|
-
return lines.join('\n');
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
for (let i = 0; i < result.cycles.length; i++) {
|
|
2342
|
-
const cycle = result.cycles[i];
|
|
2343
|
-
lines.push('');
|
|
2344
|
-
lines.push(`Cycle ${i + 1} (${cycle.length} files):`);
|
|
2345
|
-
lines.push(` ${cycle.files.join(' → ')} → ${cycle.files[0]}`);
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
lines.push('');
|
|
2349
|
-
const { totalCycles, filesInCycles } = result.summary;
|
|
2350
|
-
lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''} (${scannedCount} files with imports scanned).`);
|
|
2351
|
-
|
|
2352
|
-
return lines.join('\n');
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
function formatCircularDepsJson(result) {
|
|
2356
|
-
if (!result) return JSON.stringify({ error: 'No results' }, null, 2);
|
|
2357
|
-
return JSON.stringify(result, null, 2);
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
/**
|
|
2361
|
-
* Detect common double-escaping patterns in regex search terms.
|
|
2362
|
-
* When MCP/JSON transport is involved, agents often write \\. when they mean \. (literal dot).
|
|
2363
|
-
* Returns a hint string if double-escaping is suspected, empty string otherwise.
|
|
2364
|
-
*/
|
|
2365
|
-
function detectDoubleEscaping(term) {
|
|
2366
|
-
// Look for \\. \\d \\w \\s \\b \\D \\W \\S \\B \\( \\) \\[ \\] — common double-escaped sequences
|
|
2367
|
-
const doubleEscaped = term.match(/\\\\[.dDwWsSbB()\[\]*+?^${}|]/g);
|
|
2368
|
-
if (!doubleEscaped) return '';
|
|
2369
|
-
const examples = [...new Set(doubleEscaped)].slice(0, 3);
|
|
2370
|
-
const fixed = examples.map(e => e.slice(1)); // remove one backslash
|
|
2371
|
-
return `\nHint: Pattern contains ${examples.join(', ')} which matches literal backslash(es). If you meant ${fixed.join(', ')}, use a single backslash (MCP/JSON parameters are already raw strings).`;
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
/**
|
|
2375
|
-
* Format search command output
|
|
2376
|
-
*/
|
|
2377
|
-
function formatSearch(results, term) {
|
|
2378
|
-
const meta = results.meta;
|
|
2379
|
-
const fallbackNote = meta && meta.regexFallback
|
|
2380
|
-
? `\nNote: Invalid regex (${meta.regexFallback}). Fell back to plain text search.`
|
|
2381
|
-
: '';
|
|
2382
|
-
|
|
2383
|
-
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
2384
|
-
if (totalMatches === 0) {
|
|
2385
|
-
if (meta) {
|
|
2386
|
-
const scope = meta.filesSkipped > 0
|
|
2387
|
-
? `Searched ${meta.filesScanned} of ${meta.totalFiles} file${meta.totalFiles === 1 ? '' : 's'} (${meta.filesSkipped} excluded by filters).`
|
|
2388
|
-
: `Searched ${meta.filesScanned} file${meta.filesScanned === 1 ? '' : 's'}.`;
|
|
2389
|
-
const escapingHint = detectDoubleEscaping(term);
|
|
2390
|
-
return `No matches found for "${term}". ${scope}${fallbackNote}${escapingHint}`;
|
|
2391
|
-
}
|
|
2392
|
-
return `No matches found for "${term}"${fallbackNote}`;
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
const lines = [];
|
|
2396
|
-
const fileWord = results.length === 1 ? 'file' : 'files';
|
|
2397
|
-
lines.push(`Found ${totalMatches} match${totalMatches === 1 ? '' : 'es'} for "${term}" in ${results.length} ${fileWord}:`);
|
|
2398
|
-
if (fallbackNote) lines.push(fallbackNote.trim());
|
|
2399
|
-
lines.push('═'.repeat(60));
|
|
2400
|
-
|
|
2401
|
-
for (const result of results) {
|
|
2402
|
-
lines.push(`\n${result.file}`);
|
|
2403
|
-
for (const m of result.matches) {
|
|
2404
|
-
if (m.before && m.before.length > 0) {
|
|
2405
|
-
for (const line of m.before) {
|
|
2406
|
-
lines.push(` ... ${line.trim()}`);
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
lines.push(` ${m.line}: ${m.content.trim()}`);
|
|
2410
|
-
if (m.after && m.after.length > 0) {
|
|
2411
|
-
for (const line of m.after) {
|
|
2412
|
-
lines.push(` ... ${line.trim()}`);
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
if (meta && meta.truncatedMatches > 0) {
|
|
2419
|
-
lines.push(`\n${results.reduce((s, r) => s + r.matches.length, 0)} shown of ${meta.totalMatches} total matches. Use top= to see more.`);
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
|
|
2423
|
-
lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
return lines.join('\n');
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
/**
|
|
2430
|
-
* Format structural search results (index-based queries)
|
|
2431
|
-
*/
|
|
2432
|
-
function formatStructuralSearch(result) {
|
|
2433
|
-
const { results, meta } = result;
|
|
2434
|
-
const lines = [];
|
|
2435
|
-
|
|
2436
|
-
// Build query description
|
|
2437
|
-
const parts = [];
|
|
2438
|
-
if (meta.query.type) parts.push(`type=${meta.query.type}`);
|
|
2439
|
-
if (meta.query.term) parts.push(`name="${meta.query.term}"`);
|
|
2440
|
-
if (meta.query.param) parts.push(`param="${meta.query.param}"`);
|
|
2441
|
-
if (meta.query.receiver) parts.push(`receiver="${meta.query.receiver}"`);
|
|
2442
|
-
if (meta.query.returns) parts.push(`returns="${meta.query.returns}"`);
|
|
2443
|
-
if (meta.query.decorator) parts.push(`decorator="${meta.query.decorator}"`);
|
|
2444
|
-
if (meta.query.exported) parts.push('exported');
|
|
2445
|
-
if (meta.query.unused) parts.push('unused');
|
|
2446
|
-
const queryStr = parts.join(', ');
|
|
2447
|
-
|
|
2448
|
-
lines.push(`Structural search: ${queryStr}`);
|
|
2449
|
-
lines.push('═'.repeat(60));
|
|
2450
|
-
|
|
2451
|
-
if (results.length === 0) {
|
|
2452
|
-
lines.push('No matches found.');
|
|
2453
|
-
return lines.join('\n');
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
lines.push(`Found ${meta.totalMatched} match${meta.totalMatched === 1 ? '' : 'es'}${meta.shown < meta.totalMatched ? ` (showing ${meta.shown})` : ''}:`);
|
|
2457
|
-
lines.push('');
|
|
2458
|
-
|
|
2459
|
-
// Group by file
|
|
2460
|
-
let currentFile = null;
|
|
2461
|
-
for (const r of results) {
|
|
2462
|
-
if (r.file !== currentFile) {
|
|
2463
|
-
currentFile = r.file;
|
|
2464
|
-
lines.push(`${r.file}`);
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
if (r.kind === 'call') {
|
|
2468
|
-
lines.push(` ${r.line}: ${r.name}()${r.isMethod ? ' [method]' : ''}`);
|
|
2469
|
-
} else {
|
|
2470
|
-
let sig = ` ${r.line}: ${r.kind} ${r.name}`;
|
|
2471
|
-
if (r.params) sig += `(${r.params})`;
|
|
2472
|
-
if (r.returnType) sig += ` → ${r.returnType}`;
|
|
2473
|
-
if (r.className) sig += ` [${r.className}]`;
|
|
2474
|
-
if (r.decorators) sig += ` @${r.decorators.join(', @')}`;
|
|
2475
|
-
lines.push(sig);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
if (meta.shown < meta.totalMatched) {
|
|
2480
|
-
lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more.`);
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
return lines.join('\n');
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
function formatStructuralSearchJson(result) {
|
|
2487
|
-
return JSON.stringify(result, null, 2);
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
/**
|
|
2491
|
-
* Format file-exports command output
|
|
2492
|
-
*/
|
|
2493
|
-
function formatFileExports(exports, filePath) {
|
|
2494
|
-
if (exports?.error) return formatFileError(exports, filePath);
|
|
2495
|
-
if (exports.length === 0) return `No exports found in ${filePath}`;
|
|
2496
|
-
|
|
2497
|
-
const lines = [];
|
|
2498
|
-
lines.push(`Exports from ${filePath}:\n`);
|
|
2499
|
-
for (const exp of exports) {
|
|
2500
|
-
lines.push(` ${lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
|
|
2501
|
-
}
|
|
2502
|
-
return lines.join('\n');
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
/**
|
|
2506
|
-
* Format stats command output
|
|
2507
|
-
*/
|
|
2508
|
-
function formatStats(stats, options = {}) {
|
|
2509
|
-
const lines = [];
|
|
2510
|
-
lines.push('PROJECT STATISTICS');
|
|
2511
|
-
lines.push('═'.repeat(60));
|
|
2512
|
-
lines.push(`Root: ${stats.root}`);
|
|
2513
|
-
if (stats.truncated) {
|
|
2514
|
-
lines.push(`Files: ${stats.files} (truncated at ${stats.truncated.maxFiles} — use --max-files to increase)`);
|
|
2515
|
-
} else {
|
|
2516
|
-
lines.push(`Files: ${stats.files}`);
|
|
2517
|
-
}
|
|
2518
|
-
lines.push(`Symbols: ${stats.symbols}`);
|
|
2519
|
-
lines.push(`Build time: ${stats.buildTime}ms`);
|
|
2520
|
-
|
|
2521
|
-
lines.push('\nBy Language:');
|
|
2522
|
-
for (const [lang, info] of Object.entries(stats.byLanguage)) {
|
|
2523
|
-
lines.push(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
lines.push('\nBy Type:');
|
|
2527
|
-
for (const [type, count] of Object.entries(stats.byType)) {
|
|
2528
|
-
lines.push(` ${type}: ${count}`);
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
if (stats.functions) {
|
|
2532
|
-
const top = options.top || 30;
|
|
2533
|
-
const shown = stats.functions.slice(0, top);
|
|
2534
|
-
lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
|
|
2535
|
-
for (const fn of shown) {
|
|
2536
|
-
const loc = `${fn.file}:${fn.startLine}`;
|
|
2537
|
-
lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
|
|
2538
|
-
}
|
|
2539
|
-
if (stats.functions.length > top) {
|
|
2540
|
-
lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
return lines.join('\n');
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
// ============================================================================
|
|
2548
|
-
// DIFF IMPACT
|
|
2549
|
-
// ============================================================================
|
|
2550
|
-
|
|
2551
|
-
function formatDiffImpact(result, options = {}) {
|
|
2552
|
-
if (!result) return 'No diff data.';
|
|
2553
|
-
|
|
2554
|
-
const lines = [];
|
|
2555
|
-
const MAX_CALLERS_PER_FN = options.all ? Infinity : 30;
|
|
2556
|
-
|
|
2557
|
-
lines.push(`Diff Impact Analysis (vs ${result.base})`);
|
|
2558
|
-
lines.push('═'.repeat(60));
|
|
2559
|
-
|
|
2560
|
-
const s = result.summary || {};
|
|
2561
|
-
const parts = [];
|
|
2562
|
-
if (s.modifiedFunctions > 0) parts.push(`${s.modifiedFunctions} modified`);
|
|
2563
|
-
if (s.deletedFunctions > 0) parts.push(`${s.deletedFunctions} deleted`);
|
|
2564
|
-
if (s.newFunctions > 0) parts.push(`${s.newFunctions} new`);
|
|
2565
|
-
parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
|
|
2566
|
-
lines.push(parts.join(', '));
|
|
2567
|
-
lines.push('');
|
|
2568
|
-
|
|
2569
|
-
// Modified functions
|
|
2570
|
-
if (result.functions.length > 0) {
|
|
2571
|
-
lines.push('MODIFIED FUNCTIONS:');
|
|
2572
|
-
for (const fn of result.functions) {
|
|
2573
|
-
lines.push(`\n ${fn.name}`);
|
|
2574
|
-
lines.push(` ${fn.relativePath}:${fn.startLine}`);
|
|
2575
|
-
lines.push(` ${fn.signature}`);
|
|
2576
|
-
if (fn.addedLines.length > 0) {
|
|
2577
|
-
lines.push(` Lines added: ${formatLineRanges(fn.addedLines)}`);
|
|
2578
|
-
}
|
|
2579
|
-
if (fn.deletedLines.length > 0) {
|
|
2580
|
-
lines.push(` Lines deleted: ${formatLineRanges(fn.deletedLines)}`);
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
if (fn.callers.length > 0) {
|
|
2584
|
-
const displayCallers = fn.callers.slice(0, MAX_CALLERS_PER_FN);
|
|
2585
|
-
const truncated = fn.callers.length - displayCallers.length;
|
|
2586
|
-
lines.push(` Callers (${fn.callers.length}):`);
|
|
2587
|
-
for (const c of displayCallers) {
|
|
2588
|
-
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
2589
|
-
lines.push(` ${c.relativePath}:${c.line} ${caller}`);
|
|
2590
|
-
lines.push(` ${c.content}`);
|
|
2591
|
-
}
|
|
2592
|
-
if (truncated > 0) {
|
|
2593
|
-
lines.push(` ... ${truncated} more callers (use file= to scope diff to specific files, or use impact with class_name= for type-filtered results)`);
|
|
2594
|
-
}
|
|
2595
|
-
} else {
|
|
2596
|
-
lines.push(' Callers: none found');
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
// New functions
|
|
2602
|
-
if (result.newFunctions.length > 0) {
|
|
2603
|
-
lines.push('\nNEW FUNCTIONS:');
|
|
2604
|
-
for (const fn of result.newFunctions) {
|
|
2605
|
-
lines.push(` ${fn.name} — ${fn.relativePath}:${fn.startLine}`);
|
|
2606
|
-
lines.push(` ${fn.signature}`);
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
// Deleted functions
|
|
2611
|
-
if (result.deletedFunctions.length > 0) {
|
|
2612
|
-
lines.push('\nDELETED FUNCTIONS:');
|
|
2613
|
-
for (const fn of result.deletedFunctions) {
|
|
2614
|
-
lines.push(` ${fn.name} — ${fn.relativePath}:${fn.startLine}`);
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
// Module-level changes
|
|
2619
|
-
if (result.moduleLevelChanges.length > 0) {
|
|
2620
|
-
lines.push('\nMODULE-LEVEL CHANGES:');
|
|
2621
|
-
for (const m of result.moduleLevelChanges) {
|
|
2622
|
-
const changeParts = [];
|
|
2623
|
-
if (m.addedLines.length > 0) changeParts.push(`+${m.addedLines.length} lines`);
|
|
2624
|
-
if (m.deletedLines.length > 0) changeParts.push(`-${m.deletedLines.length} lines`);
|
|
2625
|
-
lines.push(` ${m.relativePath}: ${changeParts.join(', ')}`);
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
return lines.join('\n');
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
/**
|
|
2633
|
-
* Compact display of line numbers, collapsing consecutive ranges
|
|
2634
|
-
*/
|
|
2635
|
-
function formatLineRanges(lineNums) {
|
|
2636
|
-
if (lineNums.length === 0) return '';
|
|
2637
|
-
const sorted = [...lineNums].sort((a, b) => a - b);
|
|
2638
|
-
const ranges = [];
|
|
2639
|
-
let start = sorted[0], end = sorted[0];
|
|
2640
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
2641
|
-
if (sorted[i] === end + 1) {
|
|
2642
|
-
end = sorted[i];
|
|
2643
|
-
} else {
|
|
2644
|
-
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
2645
|
-
start = end = sorted[i];
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
2649
|
-
return ranges.join(', ');
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
/**
|
|
2653
|
-
* Format plan command output - JSON
|
|
2654
|
-
*/
|
|
2655
|
-
function formatPlanJson(plan) {
|
|
2656
|
-
if (!plan) {
|
|
2657
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
2658
|
-
}
|
|
2659
|
-
if (!plan.found) {
|
|
2660
|
-
return JSON.stringify({
|
|
2661
|
-
found: false,
|
|
2662
|
-
error: plan.error || `Function "${plan.function}" not found.`,
|
|
2663
|
-
...(plan.currentParams && { currentParams: plan.currentParams })
|
|
2664
|
-
}, null, 2);
|
|
2665
|
-
}
|
|
2666
|
-
if (plan.error) {
|
|
2667
|
-
return JSON.stringify({
|
|
2668
|
-
found: true,
|
|
2669
|
-
error: plan.error,
|
|
2670
|
-
...(plan.currentParams && { currentParams: plan.currentParams })
|
|
2671
|
-
}, null, 2);
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
return JSON.stringify({
|
|
2675
|
-
found: true,
|
|
2676
|
-
function: plan.function,
|
|
2677
|
-
file: plan.file,
|
|
2678
|
-
startLine: plan.startLine,
|
|
2679
|
-
operation: plan.operation,
|
|
2680
|
-
before: { signature: plan.before.signature },
|
|
2681
|
-
after: { signature: plan.after.signature },
|
|
2682
|
-
totalChanges: plan.totalChanges,
|
|
2683
|
-
filesAffected: plan.filesAffected,
|
|
2684
|
-
changes: plan.changes.map(c => ({
|
|
2685
|
-
file: c.file,
|
|
2686
|
-
line: c.line,
|
|
2687
|
-
expression: c.expression,
|
|
2688
|
-
suggestion: c.suggestion
|
|
2689
|
-
}))
|
|
2690
|
-
}, null, 2);
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
/**
|
|
2694
|
-
* Format stack trace command output - JSON
|
|
2695
|
-
*/
|
|
2696
|
-
function formatStackTraceJson(result) {
|
|
2697
|
-
if (!result || result.frameCount === 0) {
|
|
2698
|
-
return JSON.stringify({ frameCount: 0, frames: [] }, null, 2);
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
return JSON.stringify({
|
|
2702
|
-
frameCount: result.frameCount,
|
|
2703
|
-
frames: result.frames.map(f => ({
|
|
2704
|
-
function: f.function || null,
|
|
2705
|
-
file: f.file,
|
|
2706
|
-
line: f.line,
|
|
2707
|
-
found: !!f.found,
|
|
2708
|
-
...(f.resolvedFile && { resolvedFile: f.resolvedFile }),
|
|
2709
|
-
...(f.context && { context: f.context.map(c => ({
|
|
2710
|
-
line: c.line,
|
|
2711
|
-
code: c.code,
|
|
2712
|
-
isCurrent: !!c.isCurrent
|
|
2713
|
-
})) }),
|
|
2714
|
-
...(f.functionInfo && { functionInfo: {
|
|
2715
|
-
name: f.functionInfo.name,
|
|
2716
|
-
params: f.functionInfo.params || null,
|
|
2717
|
-
startLine: f.functionInfo.startLine,
|
|
2718
|
-
endLine: f.functionInfo.endLine
|
|
2719
|
-
} }),
|
|
2720
|
-
...(f.raw && { raw: f.raw })
|
|
2721
|
-
}))
|
|
2722
|
-
}, null, 2);
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
/**
|
|
2726
|
-
* Format verify command output - JSON
|
|
2727
|
-
*/
|
|
2728
|
-
function formatVerifyJson(result) {
|
|
2729
|
-
if (!result) {
|
|
2730
|
-
return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
|
|
2731
|
-
}
|
|
2732
|
-
if (!result.found) {
|
|
2733
|
-
return JSON.stringify({ found: false, error: `Function "${result.function}" not found.` }, null, 2);
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
return JSON.stringify({
|
|
2737
|
-
found: true,
|
|
2738
|
-
function: result.function,
|
|
2739
|
-
file: result.file,
|
|
2740
|
-
startLine: result.startLine,
|
|
2741
|
-
signature: result.signature,
|
|
2742
|
-
expectedArgs: result.expectedArgs,
|
|
2743
|
-
totalCalls: result.totalCalls,
|
|
2744
|
-
valid: result.valid,
|
|
2745
|
-
mismatches: result.mismatches,
|
|
2746
|
-
uncertain: result.uncertain,
|
|
2747
|
-
mismatchDetails: result.mismatchDetails.map(m => ({
|
|
2748
|
-
file: m.file,
|
|
2749
|
-
line: m.line,
|
|
2750
|
-
expression: m.expression,
|
|
2751
|
-
expected: m.expected,
|
|
2752
|
-
actual: m.actual,
|
|
2753
|
-
args: m.args || []
|
|
2754
|
-
})),
|
|
2755
|
-
uncertainDetails: result.uncertainDetails.map(u => ({
|
|
2756
|
-
file: u.file,
|
|
2757
|
-
line: u.line,
|
|
2758
|
-
expression: u.expression,
|
|
2759
|
-
reason: u.reason
|
|
2760
|
-
}))
|
|
2761
|
-
}, null, 2);
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
/**
|
|
2765
|
-
* Format example command output - JSON
|
|
2766
|
-
*/
|
|
2767
|
-
function formatExampleJson(result, name) {
|
|
2768
|
-
if (!result || !result.best) {
|
|
2769
|
-
return JSON.stringify({ found: false, query: name, error: `No call examples found for "${name}"` }, null, 2);
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
const best = result.best;
|
|
2773
|
-
return JSON.stringify({
|
|
2774
|
-
found: true,
|
|
2775
|
-
query: name,
|
|
2776
|
-
totalCalls: result.totalCalls,
|
|
2777
|
-
best: {
|
|
2778
|
-
file: best.relativePath || best.file,
|
|
2779
|
-
line: best.line,
|
|
2780
|
-
content: best.content,
|
|
2781
|
-
score: best.score,
|
|
2782
|
-
reasons: best.reasons || [],
|
|
2783
|
-
...(best.before && best.before.length > 0 && { before: best.before }),
|
|
2784
|
-
...(best.after && best.after.length > 0 && { after: best.after })
|
|
2785
|
-
}
|
|
2786
|
-
}, null, 2);
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
/**
|
|
2790
|
-
* Format deadcode command output - JSON
|
|
2791
|
-
*/
|
|
2792
|
-
function formatDeadcodeJson(results) {
|
|
2793
|
-
return JSON.stringify({
|
|
2794
|
-
count: results.length,
|
|
2795
|
-
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
2796
|
-
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
2797
|
-
symbols: results.map(item => ({
|
|
2798
|
-
name: item.name,
|
|
2799
|
-
type: item.type,
|
|
2800
|
-
file: item.file,
|
|
2801
|
-
startLine: item.startLine,
|
|
2802
|
-
endLine: item.endLine,
|
|
2803
|
-
...(item.isExported && { isExported: true }),
|
|
2804
|
-
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
2805
|
-
...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
|
|
2806
|
-
}))
|
|
2807
|
-
}, null, 2);
|
|
2808
|
-
}
|
|
2809
|
-
|
|
2810
|
-
function formatDiffImpactJson(result) {
|
|
2811
|
-
return JSON.stringify(result, null, 2);
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
// ============================================================================
|
|
2815
|
-
// Extraction command formatters (fn, class, lines)
|
|
2816
|
-
// ============================================================================
|
|
2817
|
-
|
|
2818
|
-
/**
|
|
2819
|
-
* Format fn handler result (from execute.js).
|
|
2820
|
-
* Notes are NOT included — surfaces render those separately (e.g. stderr for CLI).
|
|
2821
|
-
* @param {{ entries: Array<{match, code}>, notes: string[] }} result
|
|
2822
|
-
*/
|
|
2823
|
-
function formatFnResult(result) {
|
|
2824
|
-
const parts = [];
|
|
2825
|
-
for (const { match, code } of result.entries) {
|
|
2826
|
-
parts.push(formatFn(match, code));
|
|
2827
|
-
}
|
|
2828
|
-
const separator = result.entries.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '';
|
|
2829
|
-
return parts.join(separator);
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
/**
|
|
2833
|
-
* Format fn handler result as JSON.
|
|
2834
|
-
*/
|
|
2835
|
-
function formatFnResultJson(result) {
|
|
2836
|
-
if (result.entries.length === 1) {
|
|
2837
|
-
return formatFunctionJson(result.entries[0].match, result.entries[0].code);
|
|
2838
|
-
}
|
|
2839
|
-
const arr = result.entries.map(({ match, code }) => ({
|
|
2840
|
-
name: match.name,
|
|
2841
|
-
params: match.params,
|
|
2842
|
-
paramsStructured: match.paramsStructured || [],
|
|
2843
|
-
startLine: match.startLine,
|
|
2844
|
-
endLine: match.endLine,
|
|
2845
|
-
modifiers: match.modifiers || [],
|
|
2846
|
-
...(match.returnType && { returnType: match.returnType }),
|
|
2847
|
-
...(match.generics && { generics: match.generics }),
|
|
2848
|
-
...(match.docstring && { docstring: match.docstring }),
|
|
2849
|
-
...(match.isArrow && { isArrow: true }),
|
|
2850
|
-
...(match.isGenerator && { isGenerator: true }),
|
|
2851
|
-
file: match.relativePath || match.file,
|
|
2852
|
-
code,
|
|
2853
|
-
}));
|
|
2854
|
-
return JSON.stringify(arr, null, 2);
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
/**
|
|
2858
|
-
* Format class handler result (from execute.js).
|
|
2859
|
-
* @param {{ entries: Array<{match, code, methods?, summaryMode, truncated, totalLines, maxLines?}>, notes: string[] }} result
|
|
2860
|
-
*/
|
|
2861
|
-
function formatClassResult(result) {
|
|
2862
|
-
const parts = [];
|
|
2863
|
-
for (const entry of result.entries) {
|
|
2864
|
-
if (entry.summaryMode) {
|
|
2865
|
-
// Large class summary
|
|
2866
|
-
const lines = [];
|
|
2867
|
-
lines.push(`${entry.match.relativePath}:${entry.match.startLine}`);
|
|
2868
|
-
lines.push(`${lineRange(entry.match.startLine, entry.match.endLine)} ${formatClassSignature(entry.match)}`);
|
|
2869
|
-
lines.push('\u2500'.repeat(60));
|
|
2870
|
-
if (entry.methods && entry.methods.length > 0) {
|
|
2871
|
-
lines.push(`\nMethods (${entry.methods.length}):`);
|
|
2872
|
-
for (const m of entry.methods) {
|
|
2873
|
-
lines.push(` ${formatFunctionSignature(m)} [line ${m.startLine}]`);
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
lines.push(`\nClass is ${entry.totalLines} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
|
|
2877
|
-
parts.push(lines.join('\n'));
|
|
2878
|
-
} else if (entry.truncated) {
|
|
2879
|
-
parts.push(formatClass(entry.match, entry.code) + `\n\n... showing ${entry.maxLines} of ${entry.totalLines} lines`);
|
|
2880
|
-
} else {
|
|
2881
|
-
parts.push(formatClass(entry.match, entry.code));
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
return parts.join('\n\n');
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
/**
|
|
2889
|
-
* Format class handler result as JSON.
|
|
2890
|
-
*/
|
|
2891
|
-
function formatClassResultJson(result) {
|
|
2892
|
-
if (result.entries.length === 1) {
|
|
2893
|
-
const entry = result.entries[0];
|
|
2894
|
-
return JSON.stringify({
|
|
2895
|
-
...entry.match,
|
|
2896
|
-
code: entry.code,
|
|
2897
|
-
...(entry.summaryMode && { summaryMode: true }),
|
|
2898
|
-
...(entry.methods && { methods: entry.methods }),
|
|
2899
|
-
...(entry.truncated && { truncated: true }),
|
|
2900
|
-
totalLines: entry.totalLines,
|
|
2901
|
-
}, null, 2);
|
|
2902
|
-
}
|
|
2903
|
-
const arr = result.entries.map(entry => ({
|
|
2904
|
-
...entry.match,
|
|
2905
|
-
code: entry.code,
|
|
2906
|
-
...(entry.summaryMode && { summaryMode: true }),
|
|
2907
|
-
...(entry.methods && { methods: entry.methods }),
|
|
2908
|
-
...(entry.truncated && { truncated: true }),
|
|
2909
|
-
totalLines: entry.totalLines,
|
|
2910
|
-
}));
|
|
2911
|
-
return JSON.stringify(arr, null, 2);
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
/**
|
|
2915
|
-
* Format lines handler result (from execute.js).
|
|
2916
|
-
* @param {{ relativePath: string, lines: string[], startLine: number, endLine: number }} result
|
|
2917
|
-
*/
|
|
2918
|
-
function formatLines(result) {
|
|
2919
|
-
const lines = [];
|
|
2920
|
-
lines.push(`${result.relativePath}:${result.startLine}-${result.endLine}`);
|
|
2921
|
-
lines.push('\u2500'.repeat(60));
|
|
2922
|
-
for (let i = 0; i < result.lines.length; i++) {
|
|
2923
|
-
lines.push(`${lineNum(result.startLine + i)} \u2502 ${result.lines[i]}`);
|
|
2924
|
-
}
|
|
2925
|
-
return lines.join('\n');
|
|
2926
|
-
}
|
|
2927
|
-
|
|
2928
|
-
// ============================================================================
|
|
2929
|
-
// FIND DETAILED (moved from CLI — depth/confidence features)
|
|
2930
|
-
// ============================================================================
|
|
2931
|
-
|
|
2932
|
-
/**
|
|
2933
|
-
* Count depth of nested generic brackets.
|
|
2934
|
-
*/
|
|
2935
|
-
function countNestedGenerics(str) {
|
|
2936
|
-
let maxDepth = 0;
|
|
2937
|
-
let depth = 0;
|
|
2938
|
-
for (const char of str) {
|
|
2939
|
-
if (char === '<') {
|
|
2940
|
-
depth++;
|
|
2941
|
-
maxDepth = Math.max(maxDepth, depth);
|
|
2942
|
-
} else if (char === '>') {
|
|
2943
|
-
depth--;
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
return maxDepth;
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
/**
|
|
2950
|
-
* Compute confidence level for a symbol match.
|
|
2951
|
-
* @returns {{ level: 'high'|'medium'|'low', reasons: string[] }}
|
|
2952
|
-
*/
|
|
2953
|
-
function computeConfidence(symbol) {
|
|
2954
|
-
const reasons = [];
|
|
2955
|
-
let score = 100;
|
|
2956
|
-
|
|
2957
|
-
const span = (symbol.endLine || symbol.startLine) - symbol.startLine;
|
|
2958
|
-
if (span > 500) {
|
|
2959
|
-
score -= 30;
|
|
2960
|
-
reasons.push('very long function (>500 lines)');
|
|
2961
|
-
} else if (span > 200) {
|
|
2962
|
-
score -= 15;
|
|
2963
|
-
reasons.push('long function (>200 lines)');
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
const params = Array.isArray(symbol.params) ? symbol.params : [];
|
|
2967
|
-
const signature = params.map(p => p.type || '').join(' ') + (symbol.returnType || '');
|
|
2968
|
-
const genericDepth = countNestedGenerics(signature);
|
|
2969
|
-
if (genericDepth > 3) {
|
|
2970
|
-
score -= 20;
|
|
2971
|
-
reasons.push('complex nested generics');
|
|
2972
|
-
} else if (genericDepth > 2) {
|
|
2973
|
-
score -= 10;
|
|
2974
|
-
reasons.push('nested generics');
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
if (symbol.file) {
|
|
2978
|
-
try {
|
|
2979
|
-
const stats = fs.statSync(symbol.file);
|
|
2980
|
-
const sizeKB = stats.size / 1024;
|
|
2981
|
-
if (sizeKB > 500) {
|
|
2982
|
-
score -= 20;
|
|
2983
|
-
reasons.push('very large file (>500KB)');
|
|
2984
|
-
} else if (sizeKB > 200) {
|
|
2985
|
-
score -= 10;
|
|
2986
|
-
reasons.push('large file (>200KB)');
|
|
2987
|
-
}
|
|
2988
|
-
} catch (e) {
|
|
2989
|
-
// Skip file size check on error
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
let level = 'high';
|
|
2994
|
-
if (score < 50) level = 'low';
|
|
2995
|
-
else if (score < 80) level = 'medium';
|
|
2996
|
-
|
|
2997
|
-
return { level, reasons };
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
/**
|
|
3001
|
-
* Format find results with depth/confidence features (detailed view).
|
|
3002
|
-
* Returns a string. Used by CLI and interactive mode.
|
|
4
|
+
* All formatters are split into domain files under core/output/.
|
|
5
|
+
* This file re-exports everything so consumers don't need to change.
|
|
3003
6
|
*
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
* @param {object} options - { depth, top, all }
|
|
3007
|
-
*/
|
|
3008
|
-
function formatFindDetailed(symbols, query, options = {}) {
|
|
3009
|
-
const { depth, top, all } = options;
|
|
3010
|
-
const DEFAULT_LIMIT = 5;
|
|
3011
|
-
|
|
3012
|
-
if (symbols.length === 0) {
|
|
3013
|
-
return `No symbols found for "${query}"`;
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
const lines = [];
|
|
3017
|
-
const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
|
|
3018
|
-
const showing = Math.min(limit, symbols.length);
|
|
3019
|
-
const hidden = symbols.length - showing;
|
|
3020
|
-
|
|
3021
|
-
if (hidden > 0) {
|
|
3022
|
-
lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
|
|
3023
|
-
} else {
|
|
3024
|
-
lines.push(`Found ${symbols.length} match(es) for "${query}":`);
|
|
3025
|
-
}
|
|
3026
|
-
lines.push('─'.repeat(60));
|
|
3027
|
-
|
|
3028
|
-
for (let i = 0; i < showing; i++) {
|
|
3029
|
-
const s = symbols[i];
|
|
3030
|
-
// Depth 0: just location
|
|
3031
|
-
if (depth === '0') {
|
|
3032
|
-
lines.push(`${s.relativePath}:${s.startLine}`);
|
|
3033
|
-
continue;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
// Depth 1 (default): location + signature
|
|
3037
|
-
const sig = s.params !== undefined
|
|
3038
|
-
? formatFunctionSignature(s)
|
|
3039
|
-
: formatClassSignature(s);
|
|
3040
|
-
|
|
3041
|
-
const confidence = computeConfidence(s);
|
|
3042
|
-
const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
|
|
3043
|
-
|
|
3044
|
-
lines.push(`${s.relativePath}:${s.startLine} ${sig}${confStr}`);
|
|
3045
|
-
if (s.usageCounts !== undefined) {
|
|
3046
|
-
const c = s.usageCounts;
|
|
3047
|
-
const parts = [];
|
|
3048
|
-
if (c.calls > 0) parts.push(`${c.calls} calls`);
|
|
3049
|
-
if (c.definitions > 0) parts.push(`${c.definitions} def`);
|
|
3050
|
-
if (c.imports > 0) parts.push(`${c.imports} imports`);
|
|
3051
|
-
if (c.references > 0) parts.push(`${c.references} refs`);
|
|
3052
|
-
lines.push(` (${c.total} usages: ${parts.join(', ')})`);
|
|
3053
|
-
} else if (s.usageCount !== undefined) {
|
|
3054
|
-
lines.push(` (${s.usageCount} usages)`);
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
if (confidence.level !== 'high' && confidence.reasons.length > 0) {
|
|
3058
|
-
lines.push(` ⚠ ${confidence.reasons.join(', ')}`);
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
// Depth 2: + first 10 lines of code
|
|
3062
|
-
if (depth === '2' || depth === 'full') {
|
|
3063
|
-
try {
|
|
3064
|
-
const content = fs.readFileSync(s.file, 'utf-8');
|
|
3065
|
-
const fileLines = content.split('\n');
|
|
3066
|
-
const maxLines = depth === 'full' ? (s.endLine - s.startLine + 1) : 10;
|
|
3067
|
-
const endLine = Math.min(s.startLine + maxLines - 1, s.endLine);
|
|
3068
|
-
lines.push(' ───');
|
|
3069
|
-
for (let j = s.startLine - 1; j < endLine; j++) {
|
|
3070
|
-
lines.push(` ${fileLines[j]}`);
|
|
3071
|
-
}
|
|
3072
|
-
if (depth === '2' && s.endLine > endLine) {
|
|
3073
|
-
lines.push(` ... (${s.endLine - endLine} more lines)`);
|
|
3074
|
-
}
|
|
3075
|
-
} catch (e) {
|
|
3076
|
-
// Skip code extraction on error
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
lines.push('');
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
if (hidden > 0) {
|
|
3083
|
-
lines.push(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
return lines.join('\n');
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
/**
|
|
3090
|
-
* Format lines handler result as JSON.
|
|
3091
|
-
*/
|
|
3092
|
-
function formatLinesJson(result) {
|
|
3093
|
-
return JSON.stringify({
|
|
3094
|
-
file: result.relativePath,
|
|
3095
|
-
startLine: result.startLine,
|
|
3096
|
-
endLine: result.endLine,
|
|
3097
|
-
lines: result.lines,
|
|
3098
|
-
}, null, 2);
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// ============================================================================
|
|
3102
|
-
// Entrypoints command formatters
|
|
3103
|
-
// ============================================================================
|
|
3104
|
-
|
|
3105
|
-
/**
|
|
3106
|
-
* Format entrypoints command output (text)
|
|
3107
|
-
*/
|
|
3108
|
-
function formatEntrypoints(results, options = {}) {
|
|
3109
|
-
if (!results || results.length === 0) {
|
|
3110
|
-
return 'No framework entry points detected.';
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
const lines = [];
|
|
3114
|
-
lines.push(`Framework Entry Points: ${results.length} detected\n`);
|
|
3115
|
-
|
|
3116
|
-
// Group by type
|
|
3117
|
-
const byType = new Map();
|
|
3118
|
-
for (const ep of results) {
|
|
3119
|
-
if (!byType.has(ep.type)) byType.set(ep.type, []);
|
|
3120
|
-
byType.get(ep.type).push(ep);
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
const typeLabels = {
|
|
3124
|
-
http: 'HTTP Routes',
|
|
3125
|
-
cli: 'CLI Handlers',
|
|
3126
|
-
di: 'Dependency Injection',
|
|
3127
|
-
jobs: 'Job Schedulers',
|
|
3128
|
-
test: 'Test Fixtures',
|
|
3129
|
-
runtime: 'Runtime Entry Points',
|
|
3130
|
-
ui: 'UI Handlers',
|
|
3131
|
-
events: 'Event Handlers',
|
|
3132
|
-
};
|
|
3133
|
-
|
|
3134
|
-
let itemNum = 0;
|
|
3135
|
-
for (const [type, entries] of byType) {
|
|
3136
|
-
const label = typeLabels[type] || type;
|
|
3137
|
-
lines.push(`${label} (${entries.length}):`);
|
|
3138
|
-
|
|
3139
|
-
let currentFile = null;
|
|
3140
|
-
for (const ep of entries) {
|
|
3141
|
-
if (ep.file !== currentFile) {
|
|
3142
|
-
currentFile = ep.file;
|
|
3143
|
-
lines.push(` ${ep.file}`);
|
|
3144
|
-
}
|
|
3145
|
-
itemNum++;
|
|
3146
|
-
const evidence = ep.evidence.join(', ');
|
|
3147
|
-
lines.push(` [${itemNum}] ${ep.name} (${ep.framework}) — ${evidence}${' '.repeat(Math.max(0, 40 - ep.name.length - ep.framework.length - evidence.length))}:${ep.line}`);
|
|
3148
|
-
}
|
|
3149
|
-
lines.push('');
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
return lines.join('\n').trimEnd();
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
/**
|
|
3156
|
-
* Format entrypoints command output (JSON)
|
|
7
|
+
* KEY PRINCIPLE: Never truncate critical information.
|
|
8
|
+
* Full expressions, full signatures, full context.
|
|
3157
9
|
*/
|
|
3158
|
-
function formatEntrypointsJson(results) {
|
|
3159
|
-
return JSON.stringify({
|
|
3160
|
-
meta: { total: results.length },
|
|
3161
|
-
data: {
|
|
3162
|
-
entrypoints: results.map(ep => ({
|
|
3163
|
-
name: ep.name,
|
|
3164
|
-
file: ep.file,
|
|
3165
|
-
line: ep.line,
|
|
3166
|
-
type: ep.type,
|
|
3167
|
-
framework: ep.framework,
|
|
3168
|
-
patternId: ep.patternId,
|
|
3169
|
-
evidence: ep.evidence,
|
|
3170
|
-
confidence: ep.confidence,
|
|
3171
|
-
}))
|
|
3172
|
-
}
|
|
3173
|
-
}, null, 2);
|
|
3174
|
-
}
|
|
3175
10
|
|
|
3176
11
|
module.exports = {
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
header,
|
|
3188
|
-
subheader,
|
|
3189
|
-
printUsage,
|
|
3190
|
-
printDefinition,
|
|
3191
|
-
|
|
3192
|
-
// JSON formatters
|
|
3193
|
-
formatTocJson,
|
|
3194
|
-
formatSymbolJson,
|
|
3195
|
-
formatUsagesJson,
|
|
3196
|
-
formatContextJson,
|
|
3197
|
-
formatFunctionJson,
|
|
3198
|
-
formatSearchJson,
|
|
3199
|
-
formatImportsJson,
|
|
3200
|
-
formatStatsJson,
|
|
3201
|
-
formatGraphJson,
|
|
3202
|
-
formatSmartJson,
|
|
3203
|
-
|
|
3204
|
-
// New formatters (v2 migration)
|
|
3205
|
-
formatImports,
|
|
3206
|
-
formatExporters,
|
|
3207
|
-
formatTypedef,
|
|
3208
|
-
formatTests,
|
|
3209
|
-
formatApi,
|
|
3210
|
-
formatDisambiguation,
|
|
3211
|
-
formatExportersJson,
|
|
3212
|
-
formatTypedefJson,
|
|
3213
|
-
formatTestsJson,
|
|
3214
|
-
formatApiJson,
|
|
3215
|
-
|
|
3216
|
-
// About command
|
|
3217
|
-
formatAbout,
|
|
3218
|
-
formatAboutJson,
|
|
3219
|
-
|
|
3220
|
-
// Impact command
|
|
3221
|
-
formatImpact,
|
|
3222
|
-
formatImpactJson,
|
|
3223
|
-
|
|
3224
|
-
// Plan command
|
|
3225
|
-
formatPlan,
|
|
3226
|
-
formatPlanJson,
|
|
3227
|
-
|
|
3228
|
-
// Stack trace command
|
|
3229
|
-
formatStackTrace,
|
|
3230
|
-
formatStackTraceJson,
|
|
3231
|
-
|
|
3232
|
-
// Verify command
|
|
3233
|
-
formatVerify,
|
|
3234
|
-
formatVerifyJson,
|
|
3235
|
-
|
|
3236
|
-
// Trace command
|
|
3237
|
-
formatTrace,
|
|
3238
|
-
formatTraceJson,
|
|
3239
|
-
|
|
3240
|
-
// Blast command
|
|
3241
|
-
formatBlast,
|
|
3242
|
-
formatBlastJson,
|
|
3243
|
-
|
|
3244
|
-
// Reverse trace command
|
|
3245
|
-
formatReverseTrace,
|
|
3246
|
-
formatReverseTraceJson,
|
|
3247
|
-
|
|
3248
|
-
// Affected tests command
|
|
3249
|
-
formatAffectedTests,
|
|
3250
|
-
formatAffectedTestsJson,
|
|
3251
|
-
|
|
3252
|
-
// Related command
|
|
3253
|
-
formatRelated,
|
|
3254
|
-
formatRelatedJson,
|
|
3255
|
-
|
|
3256
|
-
// Example command
|
|
3257
|
-
formatExample,
|
|
3258
|
-
formatExampleJson,
|
|
3259
|
-
|
|
3260
|
-
// Deadcode command
|
|
3261
|
-
formatDeadcodeJson,
|
|
3262
|
-
|
|
3263
|
-
// Shared text formatters (CLI + MCP)
|
|
3264
|
-
formatToc,
|
|
3265
|
-
formatFind,
|
|
3266
|
-
formatUsages,
|
|
3267
|
-
formatContext,
|
|
3268
|
-
formatSmart,
|
|
3269
|
-
formatDeadcode,
|
|
3270
|
-
formatFn,
|
|
3271
|
-
formatClass,
|
|
3272
|
-
formatGraph,
|
|
3273
|
-
formatCircularDeps,
|
|
3274
|
-
formatCircularDepsJson,
|
|
3275
|
-
formatSearch,
|
|
3276
|
-
formatStructuralSearch,
|
|
3277
|
-
formatStructuralSearchJson,
|
|
3278
|
-
detectDoubleEscaping,
|
|
3279
|
-
formatFileExports,
|
|
3280
|
-
formatStats,
|
|
3281
|
-
|
|
3282
|
-
// Diff impact command
|
|
3283
|
-
formatDiffImpact,
|
|
3284
|
-
formatDiffImpactJson,
|
|
3285
|
-
|
|
3286
|
-
// Find detailed (depth/confidence)
|
|
3287
|
-
formatFindDetailed,
|
|
3288
|
-
|
|
3289
|
-
// Extraction commands (fn, class, lines)
|
|
3290
|
-
formatFnResult,
|
|
3291
|
-
formatFnResultJson,
|
|
3292
|
-
formatClassResult,
|
|
3293
|
-
formatClassResultJson,
|
|
3294
|
-
formatLines,
|
|
3295
|
-
formatLinesJson,
|
|
3296
|
-
|
|
3297
|
-
// Entrypoints command
|
|
3298
|
-
formatEntrypoints,
|
|
3299
|
-
formatEntrypointsJson,
|
|
12
|
+
...require('./output/shared'),
|
|
13
|
+
...require('./output/tracing'),
|
|
14
|
+
...require('./output/analysis'),
|
|
15
|
+
...require('./output/analysis-ext'),
|
|
16
|
+
...require('./output/find'),
|
|
17
|
+
...require('./output/search'),
|
|
18
|
+
...require('./output/graph'),
|
|
19
|
+
...require('./output/extraction'),
|
|
20
|
+
...require('./output/reporting'),
|
|
21
|
+
...require('./output/refactoring'),
|
|
3300
22
|
};
|