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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/reporting.js - Stats/TOC/deadcode/entrypoints formatters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
lineRange,
|
|
7
|
+
dynamicImportsNote,
|
|
8
|
+
formatFunctionSignature,
|
|
9
|
+
formatClassSignature,
|
|
10
|
+
} = require('./shared');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format toc command output
|
|
14
|
+
* @param {object} toc - TOC data
|
|
15
|
+
* @param {object} [options] - Formatting options
|
|
16
|
+
* @param {string} [options.detailedHint] - Custom hint text for non-detailed mode
|
|
17
|
+
* @param {string} [options.uncertainHint] - Custom hint text for uncertain references
|
|
18
|
+
*/
|
|
19
|
+
function formatToc(toc, options = {}) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
const t = toc.totals;
|
|
22
|
+
lines.push(`PROJECT: ${t.files} files, ${t.lines} lines`);
|
|
23
|
+
lines.push(` ${t.functions} functions, ${t.classes} types (classes/interfaces/enums), ${t.state} state objects`);
|
|
24
|
+
|
|
25
|
+
const meta = toc.meta || {};
|
|
26
|
+
if (meta.filteredBy) {
|
|
27
|
+
lines.push(` Filtered by: --file=${meta.filteredBy} (${meta.matchedFiles} files matched)`);
|
|
28
|
+
if (meta.emptyFiles) {
|
|
29
|
+
lines.push(` Note: ${meta.emptyFiles} file(s) have no detected symbols (may be generated or data files)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const warnings = [];
|
|
33
|
+
if (meta.dynamicImports) { const dn = dynamicImportsNote(meta.dynamicImports, meta); if (dn) warnings.push(dn); }
|
|
34
|
+
if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
|
|
35
|
+
if (warnings.length) {
|
|
36
|
+
const uncertainSuffix = meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
|
|
37
|
+
lines.push(` Note: ${warnings.join(', ')}${uncertainSuffix}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (toc.summary) {
|
|
41
|
+
if (toc.summary.topFunctionFiles?.length) {
|
|
42
|
+
const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
|
|
43
|
+
lines.push(` Most functions: ${hint}`);
|
|
44
|
+
}
|
|
45
|
+
if (toc.summary.topLineFiles?.length) {
|
|
46
|
+
const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
|
|
47
|
+
lines.push(` Largest files: ${hint}`);
|
|
48
|
+
}
|
|
49
|
+
if (toc.summary.entryFiles?.length) {
|
|
50
|
+
lines.push(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push('═'.repeat(60));
|
|
55
|
+
const hasDetail = toc.files.some(f => f.symbols);
|
|
56
|
+
for (const file of toc.files) {
|
|
57
|
+
const parts = [`${file.lines} lines`];
|
|
58
|
+
if (file.functions) parts.push(`${file.functions} fn`);
|
|
59
|
+
if (file.classes) parts.push(`${file.classes} types`);
|
|
60
|
+
if (file.state) parts.push(`${file.state} state`);
|
|
61
|
+
|
|
62
|
+
if (hasDetail) {
|
|
63
|
+
lines.push(`\n${file.file} (${parts.join(', ')})`);
|
|
64
|
+
if (file.symbols) {
|
|
65
|
+
for (const fn of file.symbols.functions) {
|
|
66
|
+
lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${formatFunctionSignature(fn)}`);
|
|
67
|
+
}
|
|
68
|
+
for (const cls of file.symbols.classes) {
|
|
69
|
+
lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(` ${file.file} — ${parts.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!hasDetail) {
|
|
78
|
+
const hint = options.detailedHint || 'Use detailed=true to list all functions and classes.';
|
|
79
|
+
lines.push(`\n${hint}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (toc.hiddenFiles > 0) {
|
|
83
|
+
const topHint = options.topHint || 'Use --top=N or --all to show more.';
|
|
84
|
+
lines.push(`\n... and ${toc.hiddenFiles} more files. ${topHint}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format TOC data as JSON
|
|
92
|
+
*/
|
|
93
|
+
function formatTocJson(data) {
|
|
94
|
+
const obj = {
|
|
95
|
+
meta: data.meta || { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
96
|
+
totals: data.totals,
|
|
97
|
+
summary: data.summary,
|
|
98
|
+
files: data.files
|
|
99
|
+
};
|
|
100
|
+
if (data.hiddenFiles > 0) obj.hiddenFiles = data.hiddenFiles;
|
|
101
|
+
return JSON.stringify(obj);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Format stats command output
|
|
106
|
+
*/
|
|
107
|
+
function formatStats(stats, options = {}) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push('PROJECT STATISTICS');
|
|
110
|
+
lines.push('═'.repeat(60));
|
|
111
|
+
lines.push(`Root: ${stats.root}`);
|
|
112
|
+
if (stats.truncated) {
|
|
113
|
+
lines.push(`Files: ${stats.files} (truncated at ${stats.truncated.maxFiles} — use --max-files to increase)`);
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(`Files: ${stats.files}`);
|
|
116
|
+
}
|
|
117
|
+
lines.push(`Symbols: ${stats.symbols}`);
|
|
118
|
+
lines.push(`Build time: ${stats.buildTime}ms`);
|
|
119
|
+
|
|
120
|
+
lines.push('\nBy Language:');
|
|
121
|
+
for (const [lang, info] of Object.entries(stats.byLanguage)) {
|
|
122
|
+
lines.push(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lines.push('\nBy Type:');
|
|
126
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
127
|
+
lines.push(` ${type}: ${count}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (stats.warnings) {
|
|
131
|
+
lines.push(`\nWarnings: ${stats.warnings.count} file(s) failed to parse:`);
|
|
132
|
+
for (const f of stats.warnings.failedFiles.slice(0, 10)) {
|
|
133
|
+
lines.push(` ${f}`);
|
|
134
|
+
}
|
|
135
|
+
if (stats.warnings.count > 10) {
|
|
136
|
+
lines.push(` ... and ${stats.warnings.count - 10} more`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (stats.functions) {
|
|
141
|
+
const top = options.top || 30;
|
|
142
|
+
const shown = stats.functions.slice(0, top);
|
|
143
|
+
lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
|
|
144
|
+
for (const fn of shown) {
|
|
145
|
+
const loc = `${fn.file}:${fn.startLine}`;
|
|
146
|
+
lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
|
|
147
|
+
}
|
|
148
|
+
if (stats.functions.length > top) {
|
|
149
|
+
lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Format project stats as JSON
|
|
158
|
+
*/
|
|
159
|
+
function formatStatsJson(stats) {
|
|
160
|
+
return JSON.stringify(stats, null, 2);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format deadcode command output
|
|
165
|
+
* @param {Array} results - Dead code results
|
|
166
|
+
* @param {object} [options] - Formatting options
|
|
167
|
+
* @param {string} [options.exportedHint] - Hint about exported symbols exclusion
|
|
168
|
+
*/
|
|
169
|
+
function formatDeadcode(results, options = {}) {
|
|
170
|
+
if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
|
|
171
|
+
return 'No dead code found.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lines = [];
|
|
175
|
+
const top = options.top > 0 ? options.top : 0;
|
|
176
|
+
const showing = top > 0 ? results.slice(0, top) : results;
|
|
177
|
+
const hidden = results.length - showing.length;
|
|
178
|
+
|
|
179
|
+
if (results.length > 0) {
|
|
180
|
+
if (hidden > 0) {
|
|
181
|
+
lines.push(`Dead code: ${results.length} unused symbol(s) (showing ${showing.length})\n`);
|
|
182
|
+
} else {
|
|
183
|
+
lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let currentFile = null;
|
|
188
|
+
for (const item of showing) {
|
|
189
|
+
if (item.file !== currentFile) {
|
|
190
|
+
currentFile = item.file;
|
|
191
|
+
lines.push(item.file);
|
|
192
|
+
}
|
|
193
|
+
const exported = item.isExported ? ' [exported]' : '';
|
|
194
|
+
// Surface decorators/annotations — structural hint that a framework may invoke this
|
|
195
|
+
const hints = [];
|
|
196
|
+
if (item.decorators && item.decorators.length > 0) {
|
|
197
|
+
hints.push(...item.decorators.map(d => `@${d}`));
|
|
198
|
+
}
|
|
199
|
+
if (item.annotations && item.annotations.length > 0) {
|
|
200
|
+
hints.push(...item.annotations.map(a => `@${a}`));
|
|
201
|
+
}
|
|
202
|
+
const hintStr = hints.length > 0 ? ` [has ${hints.join(', ')}]` : '';
|
|
203
|
+
lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (hidden > 0) {
|
|
207
|
+
lines.push(`\n${hidden} more result(s) not shown. Use --top=${results.length} or --all to see all.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Show counts of excluded items with expansion hints
|
|
211
|
+
if (results.length === 0) {
|
|
212
|
+
lines.push('No dead code found.');
|
|
213
|
+
}
|
|
214
|
+
if (results.excludedDecorated > 0) {
|
|
215
|
+
const decoratedHint = options.decoratedHint || `${results.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.`;
|
|
216
|
+
lines.push(`\n${decoratedHint}`);
|
|
217
|
+
}
|
|
218
|
+
if (results.excludedExported > 0) {
|
|
219
|
+
const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
|
|
220
|
+
lines.push(`\n${exportedHint}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (lines.length === 0) {
|
|
224
|
+
return 'No dead code found.';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Format deadcode command output - JSON
|
|
232
|
+
*/
|
|
233
|
+
function formatDeadcodeJson(results) {
|
|
234
|
+
return JSON.stringify({
|
|
235
|
+
count: results.length,
|
|
236
|
+
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
237
|
+
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
238
|
+
symbols: results.map(item => ({
|
|
239
|
+
name: item.name,
|
|
240
|
+
type: item.type,
|
|
241
|
+
file: item.file,
|
|
242
|
+
startLine: item.startLine,
|
|
243
|
+
endLine: item.endLine,
|
|
244
|
+
...(item.isExported && { isExported: true }),
|
|
245
|
+
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
246
|
+
...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
|
|
247
|
+
}))
|
|
248
|
+
}, null, 2);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Format entrypoints command output (text)
|
|
253
|
+
*/
|
|
254
|
+
function formatEntrypoints(results, options = {}) {
|
|
255
|
+
if (!results || results.length === 0) {
|
|
256
|
+
return 'No framework entry points detected.';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const lines = [];
|
|
260
|
+
lines.push(`Framework Entry Points: ${results.length} detected\n`);
|
|
261
|
+
|
|
262
|
+
// Group by type
|
|
263
|
+
const byType = new Map();
|
|
264
|
+
for (const ep of results) {
|
|
265
|
+
if (!byType.has(ep.type)) byType.set(ep.type, []);
|
|
266
|
+
byType.get(ep.type).push(ep);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const typeLabels = {
|
|
270
|
+
http: 'HTTP Routes',
|
|
271
|
+
cli: 'CLI Handlers',
|
|
272
|
+
di: 'Dependency Injection',
|
|
273
|
+
jobs: 'Job Schedulers',
|
|
274
|
+
test: 'Test Fixtures',
|
|
275
|
+
runtime: 'Runtime Entry Points',
|
|
276
|
+
ui: 'UI Handlers',
|
|
277
|
+
events: 'Event Handlers',
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
let itemNum = 0;
|
|
281
|
+
for (const [type, entries] of byType) {
|
|
282
|
+
const label = typeLabels[type] || type;
|
|
283
|
+
lines.push(`${label} (${entries.length}):`);
|
|
284
|
+
|
|
285
|
+
let currentFile = null;
|
|
286
|
+
for (const ep of entries) {
|
|
287
|
+
if (ep.file !== currentFile) {
|
|
288
|
+
currentFile = ep.file;
|
|
289
|
+
lines.push(` ${ep.file}`);
|
|
290
|
+
}
|
|
291
|
+
itemNum++;
|
|
292
|
+
const evidence = ep.evidence.join(', ');
|
|
293
|
+
lines.push(` [${itemNum}] ${ep.name} (${ep.framework}) — ${evidence}${' '.repeat(Math.max(0, 40 - ep.name.length - ep.framework.length - evidence.length))}:${ep.line}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return lines.join('\n').trimEnd();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Format entrypoints command output (JSON)
|
|
303
|
+
*/
|
|
304
|
+
function formatEntrypointsJson(results) {
|
|
305
|
+
return JSON.stringify({
|
|
306
|
+
meta: { total: results.length },
|
|
307
|
+
data: {
|
|
308
|
+
entrypoints: results.map(ep => ({
|
|
309
|
+
name: ep.name,
|
|
310
|
+
file: ep.file,
|
|
311
|
+
line: ep.line,
|
|
312
|
+
type: ep.type,
|
|
313
|
+
framework: ep.framework,
|
|
314
|
+
patternId: ep.patternId,
|
|
315
|
+
evidence: ep.evidence,
|
|
316
|
+
confidence: ep.confidence,
|
|
317
|
+
}))
|
|
318
|
+
}
|
|
319
|
+
}, null, 2);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
formatToc,
|
|
324
|
+
formatTocJson,
|
|
325
|
+
formatStats,
|
|
326
|
+
formatStatsJson,
|
|
327
|
+
formatDeadcode,
|
|
328
|
+
formatDeadcodeJson,
|
|
329
|
+
formatEntrypoints,
|
|
330
|
+
formatEntrypointsJson,
|
|
331
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/search.js - Text search, structural search, example, typedef, tests formatters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { detectDoubleEscaping } = require('./shared');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format search command output
|
|
9
|
+
*/
|
|
10
|
+
function formatSearch(results, term) {
|
|
11
|
+
const meta = results.meta;
|
|
12
|
+
const fallbackNote = meta && meta.regexFallback
|
|
13
|
+
? `\nNote: Invalid regex (${meta.regexFallback}). Fell back to plain text search.`
|
|
14
|
+
: '';
|
|
15
|
+
|
|
16
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
17
|
+
if (totalMatches === 0) {
|
|
18
|
+
if (meta) {
|
|
19
|
+
const scope = meta.filesSkipped > 0
|
|
20
|
+
? `Searched ${meta.filesScanned} of ${meta.totalFiles} file${meta.totalFiles === 1 ? '' : 's'} (${meta.filesSkipped} excluded by filters).`
|
|
21
|
+
: `Searched ${meta.filesScanned} file${meta.filesScanned === 1 ? '' : 's'}.`;
|
|
22
|
+
const escapingHint = detectDoubleEscaping(term);
|
|
23
|
+
return `No matches found for "${term}". ${scope}${fallbackNote}${escapingHint}`;
|
|
24
|
+
}
|
|
25
|
+
return `No matches found for "${term}"${fallbackNote}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lines = [];
|
|
29
|
+
const fileWord = results.length === 1 ? 'file' : 'files';
|
|
30
|
+
lines.push(`Found ${totalMatches} match${totalMatches === 1 ? '' : 'es'} for "${term}" in ${results.length} ${fileWord}:`);
|
|
31
|
+
if (fallbackNote) lines.push(fallbackNote.trim());
|
|
32
|
+
lines.push('═'.repeat(60));
|
|
33
|
+
|
|
34
|
+
for (const result of results) {
|
|
35
|
+
lines.push(`\n${result.file}`);
|
|
36
|
+
for (const m of result.matches) {
|
|
37
|
+
if (m.before && m.before.length > 0) {
|
|
38
|
+
for (const line of m.before) {
|
|
39
|
+
lines.push(` ... ${line.trim()}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push(` ${m.line}: ${m.content.trim()}`);
|
|
43
|
+
if (m.after && m.after.length > 0) {
|
|
44
|
+
for (const line of m.after) {
|
|
45
|
+
lines.push(` ... ${line.trim()}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (meta && meta.truncatedMatches > 0) {
|
|
52
|
+
lines.push(`\n${results.reduce((s, r) => s + r.matches.length, 0)} shown of ${meta.totalMatches} total matches. Use top= to see more.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
|
|
56
|
+
lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format search results as JSON
|
|
64
|
+
*/
|
|
65
|
+
function formatSearchJson(results, term) {
|
|
66
|
+
const meta = results.meta;
|
|
67
|
+
const obj = {
|
|
68
|
+
term,
|
|
69
|
+
totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
|
|
70
|
+
files: results.map(r => ({
|
|
71
|
+
file: r.file,
|
|
72
|
+
matchCount: r.matches.length,
|
|
73
|
+
matches: r.matches.map(m => ({
|
|
74
|
+
line: m.line,
|
|
75
|
+
content: m.content // FULL content
|
|
76
|
+
}))
|
|
77
|
+
}))
|
|
78
|
+
};
|
|
79
|
+
if (meta) {
|
|
80
|
+
obj.filesScanned = meta.filesScanned;
|
|
81
|
+
obj.filesSkipped = meta.filesSkipped;
|
|
82
|
+
obj.totalFiles = meta.totalFiles;
|
|
83
|
+
if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
|
|
84
|
+
if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(obj, null, 2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format structural search results (index-based queries)
|
|
91
|
+
*/
|
|
92
|
+
function formatStructuralSearch(result) {
|
|
93
|
+
const { results, meta } = result;
|
|
94
|
+
const lines = [];
|
|
95
|
+
|
|
96
|
+
// Build query description
|
|
97
|
+
const parts = [];
|
|
98
|
+
if (meta.query.type) parts.push(`type=${meta.query.type}`);
|
|
99
|
+
if (meta.query.term) parts.push(`name="${meta.query.term}"`);
|
|
100
|
+
if (meta.query.param) parts.push(`param="${meta.query.param}"`);
|
|
101
|
+
if (meta.query.receiver) parts.push(`receiver="${meta.query.receiver}"`);
|
|
102
|
+
if (meta.query.returns) parts.push(`returns="${meta.query.returns}"`);
|
|
103
|
+
if (meta.query.decorator) parts.push(`decorator="${meta.query.decorator}"`);
|
|
104
|
+
if (meta.query.exported) parts.push('exported');
|
|
105
|
+
if (meta.query.unused) parts.push('unused');
|
|
106
|
+
const queryStr = parts.join(', ');
|
|
107
|
+
|
|
108
|
+
lines.push(`Structural search: ${queryStr}`);
|
|
109
|
+
lines.push('═'.repeat(60));
|
|
110
|
+
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
lines.push('No matches found.');
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines.push(`Found ${meta.totalMatched} match${meta.totalMatched === 1 ? '' : 'es'}${meta.shown < meta.totalMatched ? ` (showing ${meta.shown})` : ''}:`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
|
|
119
|
+
// Group by file
|
|
120
|
+
let currentFile = null;
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
if (r.file !== currentFile) {
|
|
123
|
+
currentFile = r.file;
|
|
124
|
+
lines.push(`${r.file}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (r.kind === 'call') {
|
|
128
|
+
lines.push(` ${r.line}: ${r.name}()${r.isMethod ? ' [method]' : ''}`);
|
|
129
|
+
} else {
|
|
130
|
+
let sig = ` ${r.line}: ${r.kind} ${r.name}`;
|
|
131
|
+
if (r.params) sig += `(${r.params})`;
|
|
132
|
+
if (r.returnType) sig += ` → ${r.returnType}`;
|
|
133
|
+
if (r.className) sig += ` [${r.className}]`;
|
|
134
|
+
if (r.decorators) sig += ` @${r.decorators.join(', @')}`;
|
|
135
|
+
lines.push(sig);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (meta.shown < meta.totalMatched) {
|
|
140
|
+
lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatStructuralSearchJson(result) {
|
|
147
|
+
return JSON.stringify(result, null, 2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format example result as text
|
|
152
|
+
*/
|
|
153
|
+
function formatExample(result, name) {
|
|
154
|
+
if (!result || !result.best) return `No call examples found for "${name}"`;
|
|
155
|
+
|
|
156
|
+
const best = result.best;
|
|
157
|
+
const lines = [];
|
|
158
|
+
lines.push(`Best example of "${name}":`);
|
|
159
|
+
lines.push('═'.repeat(60));
|
|
160
|
+
lines.push(`${best.relativePath}:${best.line}`);
|
|
161
|
+
lines.push('');
|
|
162
|
+
|
|
163
|
+
if (best.before) {
|
|
164
|
+
for (let i = 0; i < best.before.length; i++) {
|
|
165
|
+
const ln = best.line - best.before.length + i;
|
|
166
|
+
lines.push(`${ln.toString().padStart(4)}| ${best.before[i]}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push(`${best.line.toString().padStart(4)}| ${best.content} <--`);
|
|
171
|
+
|
|
172
|
+
if (best.after) {
|
|
173
|
+
for (let i = 0; i < best.after.length; i++) {
|
|
174
|
+
const ln = best.line + i + 1;
|
|
175
|
+
lines.push(`${ln.toString().padStart(4)}| ${best.after[i]}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(`Score: ${best.score} (${result.totalCalls} total calls)`);
|
|
181
|
+
lines.push(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
|
|
182
|
+
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format example command output - JSON
|
|
188
|
+
*/
|
|
189
|
+
function formatExampleJson(result, name) {
|
|
190
|
+
if (!result || !result.best) {
|
|
191
|
+
return JSON.stringify({ found: false, query: name, error: `No call examples found for "${name}"` }, null, 2);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const best = result.best;
|
|
195
|
+
return JSON.stringify({
|
|
196
|
+
found: true,
|
|
197
|
+
query: name,
|
|
198
|
+
totalCalls: result.totalCalls,
|
|
199
|
+
best: {
|
|
200
|
+
file: best.relativePath || best.file,
|
|
201
|
+
line: best.line,
|
|
202
|
+
content: best.content,
|
|
203
|
+
score: best.score,
|
|
204
|
+
reasons: best.reasons || [],
|
|
205
|
+
...(best.before && best.before.length > 0 && { before: best.before }),
|
|
206
|
+
...(best.after && best.after.length > 0 && { after: best.after })
|
|
207
|
+
}
|
|
208
|
+
}, null, 2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Format typedef command output - text
|
|
213
|
+
*/
|
|
214
|
+
function formatTypedef(types, name) {
|
|
215
|
+
const lines = [`Type definitions for "${name}":\n`];
|
|
216
|
+
|
|
217
|
+
if (types.length === 0) {
|
|
218
|
+
lines.push(' (none found)');
|
|
219
|
+
} else {
|
|
220
|
+
for (const t of types) {
|
|
221
|
+
lines.push(`${t.relativePath}:${t.startLine} ${t.type} ${t.name}`);
|
|
222
|
+
if (t.usageCount !== undefined) {
|
|
223
|
+
lines.push(` (${t.usageCount} usages)`);
|
|
224
|
+
}
|
|
225
|
+
if (t.code) {
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push('─── CODE ───');
|
|
228
|
+
lines.push(t.code);
|
|
229
|
+
lines.push('');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format typedef as JSON
|
|
239
|
+
*/
|
|
240
|
+
function formatTypedefJson(types, name) {
|
|
241
|
+
return JSON.stringify({
|
|
242
|
+
query: name,
|
|
243
|
+
count: types.length,
|
|
244
|
+
types: types.map(t => ({
|
|
245
|
+
name: t.name,
|
|
246
|
+
type: t.type,
|
|
247
|
+
file: t.relativePath || t.file,
|
|
248
|
+
startLine: t.startLine,
|
|
249
|
+
endLine: t.endLine,
|
|
250
|
+
...(t.usageCount !== undefined && { usageCount: t.usageCount }),
|
|
251
|
+
...(t.code && { code: t.code })
|
|
252
|
+
}))
|
|
253
|
+
}, null, 2);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format tests command output - text
|
|
258
|
+
*/
|
|
259
|
+
function formatTests(tests, name) {
|
|
260
|
+
const lines = [`Tests for "${name}":\n`];
|
|
261
|
+
|
|
262
|
+
if (tests.length === 0) {
|
|
263
|
+
lines.push(' (no tests found)');
|
|
264
|
+
} else {
|
|
265
|
+
const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
|
|
266
|
+
lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
|
|
267
|
+
|
|
268
|
+
for (const testFile of tests) {
|
|
269
|
+
lines.push(testFile.file);
|
|
270
|
+
for (const match of testFile.matches) {
|
|
271
|
+
const typeLabel = match.matchType === 'test-case' ? '[test]' :
|
|
272
|
+
match.matchType === 'import' ? '[import]' :
|
|
273
|
+
match.matchType === 'call' ? '[call]' :
|
|
274
|
+
match.matchType === 'string-ref' ? '[string]' : '[ref]';
|
|
275
|
+
lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
|
|
276
|
+
}
|
|
277
|
+
lines.push('');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Format tests as JSON
|
|
286
|
+
*/
|
|
287
|
+
function formatTestsJson(tests, name) {
|
|
288
|
+
return JSON.stringify({
|
|
289
|
+
query: name,
|
|
290
|
+
testFileCount: tests.length,
|
|
291
|
+
totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
|
|
292
|
+
testFiles: tests
|
|
293
|
+
}, null, 2);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
formatSearch,
|
|
298
|
+
formatSearchJson,
|
|
299
|
+
formatStructuralSearch,
|
|
300
|
+
formatStructuralSearchJson,
|
|
301
|
+
formatExample,
|
|
302
|
+
formatExampleJson,
|
|
303
|
+
formatTypedef,
|
|
304
|
+
formatTypedefJson,
|
|
305
|
+
formatTests,
|
|
306
|
+
formatTestsJson,
|
|
307
|
+
};
|