@fragno-dev/cli 0.1.13 → 0.1.15
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +44 -0
- package/dist/cli.d.ts +52 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +573 -51
- package/dist/cli.js.map +1 -1
- package/package.json +6 -3
- package/src/cli.ts +95 -76
- package/src/commands/corpus.ts +581 -0
- package/src/commands/search.ts +105 -0
- package/src/utils/format-search-results.ts +121 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { define } from "gunshi";
|
|
2
|
+
import {
|
|
3
|
+
getSubjects,
|
|
4
|
+
getSubject,
|
|
5
|
+
getAllSubjects,
|
|
6
|
+
getSubjectParent,
|
|
7
|
+
getSubjectChildren,
|
|
8
|
+
} from "@fragno-dev/corpus";
|
|
9
|
+
import type { Subject, Example } from "@fragno-dev/corpus";
|
|
10
|
+
import { marked } from "marked";
|
|
11
|
+
// @ts-expect-error - marked-terminal types are outdated for v7
|
|
12
|
+
import { markedTerminal } from "marked-terminal";
|
|
13
|
+
import { stripVTControlCharacters } from "node:util";
|
|
14
|
+
|
|
15
|
+
// Always configure marked to use terminal renderer
|
|
16
|
+
marked.use(markedTerminal());
|
|
17
|
+
|
|
18
|
+
interface PrintOptions {
|
|
19
|
+
showLineNumbers: boolean;
|
|
20
|
+
startLine?: number;
|
|
21
|
+
endLine?: number;
|
|
22
|
+
headingsOnly: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build markdown content for multiple subjects
|
|
27
|
+
*/
|
|
28
|
+
function buildSubjectsMarkdown(subjects: Subject[]): string {
|
|
29
|
+
let fullMarkdown = "";
|
|
30
|
+
|
|
31
|
+
for (const subject of subjects) {
|
|
32
|
+
fullMarkdown += `# ${subject.title}\n\n`;
|
|
33
|
+
|
|
34
|
+
if (subject.description) {
|
|
35
|
+
fullMarkdown += `${subject.description}\n\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add imports block if present
|
|
39
|
+
if (subject.imports) {
|
|
40
|
+
fullMarkdown += `### Imports\n\n\`\`\`typescript\n${subject.imports}\n\`\`\`\n\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add prelude blocks if present
|
|
44
|
+
if (subject.prelude.length > 0) {
|
|
45
|
+
fullMarkdown += `### Prelude\n\n`;
|
|
46
|
+
for (const block of subject.prelude) {
|
|
47
|
+
// Don't include the directive in the displayed code fence
|
|
48
|
+
fullMarkdown += `\`\`\`typescript\n${block.code}\n\`\`\`\n\n`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add all sections
|
|
53
|
+
for (const section of subject.sections) {
|
|
54
|
+
fullMarkdown += `## ${section.heading}\n\n${section.content}\n\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return fullMarkdown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add line numbers to content
|
|
63
|
+
*/
|
|
64
|
+
function addLineNumbers(content: string, startFrom: number = 1): string {
|
|
65
|
+
const lines = content.split("\n");
|
|
66
|
+
const maxDigits = String(startFrom + lines.length - 1).length;
|
|
67
|
+
|
|
68
|
+
return lines
|
|
69
|
+
.map((line, index) => {
|
|
70
|
+
const lineNum = startFrom + index;
|
|
71
|
+
const paddedNum = String(lineNum).padStart(maxDigits, " ");
|
|
72
|
+
return `${paddedNum}│ ${line}`;
|
|
73
|
+
})
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Filter content by line range
|
|
79
|
+
*/
|
|
80
|
+
function filterByLineRange(content: string, startLine: number, endLine: number): string {
|
|
81
|
+
const lines = content.split("\n");
|
|
82
|
+
// Convert to 0-based index
|
|
83
|
+
const start = Math.max(0, startLine - 1);
|
|
84
|
+
const end = Math.min(lines.length, endLine);
|
|
85
|
+
return lines.slice(start, end).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extract headings and code block information with line numbers
|
|
90
|
+
*/
|
|
91
|
+
function extractHeadingsAndBlocks(subjects: Subject[]): string {
|
|
92
|
+
let output = "";
|
|
93
|
+
let currentLine = 1;
|
|
94
|
+
let lastOutputLine = 0;
|
|
95
|
+
|
|
96
|
+
// Helper to add a gap indicator if we skipped lines
|
|
97
|
+
const addGapIfNeeded = () => {
|
|
98
|
+
if (lastOutputLine > 0 && currentLine > lastOutputLine + 1) {
|
|
99
|
+
output += ` │\n`;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Add instruction header
|
|
104
|
+
output += "Use --start N --end N flags to show specific line ranges\n\n";
|
|
105
|
+
|
|
106
|
+
for (const subject of subjects) {
|
|
107
|
+
// Title
|
|
108
|
+
addGapIfNeeded();
|
|
109
|
+
output += `${currentLine.toString().padStart(4, " ")}│ # ${subject.title}\n`;
|
|
110
|
+
lastOutputLine = currentLine;
|
|
111
|
+
currentLine += 1;
|
|
112
|
+
|
|
113
|
+
// Empty line after title - SHOW IT
|
|
114
|
+
output += `${currentLine.toString().padStart(4, " ")}│\n`;
|
|
115
|
+
lastOutputLine = currentLine;
|
|
116
|
+
currentLine += 1;
|
|
117
|
+
|
|
118
|
+
// Description - show full text
|
|
119
|
+
if (subject.description) {
|
|
120
|
+
const descLines = subject.description.split("\n");
|
|
121
|
+
for (const line of descLines) {
|
|
122
|
+
output += `${currentLine.toString().padStart(4, " ")}│ ${line}\n`;
|
|
123
|
+
lastOutputLine = currentLine;
|
|
124
|
+
currentLine += 1;
|
|
125
|
+
}
|
|
126
|
+
// Empty line after description - SHOW IT
|
|
127
|
+
output += `${currentLine.toString().padStart(4, " ")}│\n`;
|
|
128
|
+
lastOutputLine = currentLine;
|
|
129
|
+
currentLine += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Imports block - show full code
|
|
133
|
+
if (subject.imports) {
|
|
134
|
+
addGapIfNeeded();
|
|
135
|
+
output += `${currentLine.toString().padStart(4, " ")}│ ### Imports\n`;
|
|
136
|
+
lastOutputLine = currentLine;
|
|
137
|
+
currentLine += 1;
|
|
138
|
+
// Empty line after heading - SHOW IT
|
|
139
|
+
output += `${currentLine.toString().padStart(4, " ")}│\n`;
|
|
140
|
+
lastOutputLine = currentLine;
|
|
141
|
+
currentLine += 1;
|
|
142
|
+
output += `${currentLine.toString().padStart(4, " ")}│ \`\`\`typescript\n`;
|
|
143
|
+
lastOutputLine = currentLine;
|
|
144
|
+
currentLine += 1;
|
|
145
|
+
const importLines = subject.imports.split("\n");
|
|
146
|
+
for (const line of importLines) {
|
|
147
|
+
output += `${currentLine.toString().padStart(4, " ")}│ ${line}\n`;
|
|
148
|
+
lastOutputLine = currentLine;
|
|
149
|
+
currentLine += 1;
|
|
150
|
+
}
|
|
151
|
+
output += `${currentLine.toString().padStart(4, " ")}│ \`\`\`\n`;
|
|
152
|
+
lastOutputLine = currentLine;
|
|
153
|
+
currentLine += 1;
|
|
154
|
+
// Empty line after code block - SHOW IT
|
|
155
|
+
output += `${currentLine.toString().padStart(4, " ")}│\n`;
|
|
156
|
+
lastOutputLine = currentLine;
|
|
157
|
+
currentLine += 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Prelude blocks - show as list
|
|
161
|
+
if (subject.prelude.length > 0) {
|
|
162
|
+
addGapIfNeeded();
|
|
163
|
+
output += `${currentLine.toString().padStart(4, " ")}│ ### Prelude\n`;
|
|
164
|
+
lastOutputLine = currentLine;
|
|
165
|
+
currentLine += 1;
|
|
166
|
+
// Empty line after heading
|
|
167
|
+
output += `${currentLine.toString().padStart(4, " ")}│\n`;
|
|
168
|
+
lastOutputLine = currentLine;
|
|
169
|
+
currentLine += 1;
|
|
170
|
+
|
|
171
|
+
for (const block of subject.prelude) {
|
|
172
|
+
const id = block.id || "(no-id)";
|
|
173
|
+
const blockStartLine = currentLine + 1; // +1 for opening ```
|
|
174
|
+
const codeLines = block.code.split("\n").length;
|
|
175
|
+
const blockEndLine = currentLine + 1 + codeLines; // opening ``` + code lines
|
|
176
|
+
output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`, L${blockStartLine}-${blockEndLine}\n`;
|
|
177
|
+
lastOutputLine = currentLine;
|
|
178
|
+
currentLine += codeLines + 3; // opening ```, code, closing ```, blank line
|
|
179
|
+
}
|
|
180
|
+
// Update lastOutputLine to current position to avoid gap indicator
|
|
181
|
+
lastOutputLine = currentLine - 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sections - show headings and any example IDs that belong to them
|
|
185
|
+
const sectionToExamples = new Map<string, Example[]>();
|
|
186
|
+
|
|
187
|
+
// Group examples by their rough section (based on heading appearance in explanations)
|
|
188
|
+
for (const example of subject.examples) {
|
|
189
|
+
// Try to match the example to a section based on context
|
|
190
|
+
// For now, we'll list all example IDs under the sections where they appear
|
|
191
|
+
for (const section of subject.sections) {
|
|
192
|
+
// Check if the section contains references to this example
|
|
193
|
+
if (
|
|
194
|
+
section.content.includes(example.code.substring(0, Math.min(50, example.code.length)))
|
|
195
|
+
) {
|
|
196
|
+
if (!sectionToExamples.has(section.heading)) {
|
|
197
|
+
sectionToExamples.set(section.heading, []);
|
|
198
|
+
}
|
|
199
|
+
sectionToExamples.get(section.heading)!.push(example);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const section of subject.sections) {
|
|
206
|
+
addGapIfNeeded();
|
|
207
|
+
output += `${currentLine.toString().padStart(4, " ")}│ ## ${section.heading}\n`;
|
|
208
|
+
lastOutputLine = currentLine;
|
|
209
|
+
currentLine += 1;
|
|
210
|
+
|
|
211
|
+
// Show code block IDs as a list if any examples match this section
|
|
212
|
+
const examples = sectionToExamples.get(section.heading) || [];
|
|
213
|
+
if (examples.length > 0) {
|
|
214
|
+
// We need to parse the section content to find where each example appears
|
|
215
|
+
const sectionStartLine = currentLine;
|
|
216
|
+
const lines = section.content.split("\n");
|
|
217
|
+
|
|
218
|
+
for (const example of examples) {
|
|
219
|
+
const id = example.id || "(no-id)";
|
|
220
|
+
// Find the code block in section content
|
|
221
|
+
let blockStartLine = sectionStartLine;
|
|
222
|
+
let blockEndLine = sectionStartLine;
|
|
223
|
+
let inCodeBlock = false;
|
|
224
|
+
let foundBlock = false;
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
const line = lines[i];
|
|
228
|
+
if (line.trim().startsWith("```") && !inCodeBlock) {
|
|
229
|
+
// Check if next lines match the example
|
|
230
|
+
const codeStart = i + 1;
|
|
231
|
+
let matches = true;
|
|
232
|
+
const exampleLines = example.code.split("\n");
|
|
233
|
+
for (let j = 0; j < Math.min(3, exampleLines.length); j++) {
|
|
234
|
+
if (lines[codeStart + j]?.trim() !== exampleLines[j]?.trim()) {
|
|
235
|
+
matches = false;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (matches) {
|
|
240
|
+
blockStartLine = sectionStartLine + i + 1; // +1 to skip opening ```
|
|
241
|
+
blockEndLine = sectionStartLine + i + exampleLines.length;
|
|
242
|
+
foundBlock = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (foundBlock) {
|
|
249
|
+
output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`, L${blockStartLine}-${blockEndLine}\n`;
|
|
250
|
+
} else {
|
|
251
|
+
output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`\n`;
|
|
252
|
+
}
|
|
253
|
+
lastOutputLine = currentLine;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Count lines
|
|
258
|
+
const sectionLines = section.content.split("\n");
|
|
259
|
+
for (const _line of sectionLines) {
|
|
260
|
+
currentLine += 1;
|
|
261
|
+
}
|
|
262
|
+
currentLine += 1; // blank line after section
|
|
263
|
+
// Update lastOutputLine to current position to avoid gap indicator
|
|
264
|
+
lastOutputLine = currentLine - 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return output;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Print subjects with the given options
|
|
273
|
+
*/
|
|
274
|
+
async function printSubjects(subjects: Subject[], options: PrintOptions): Promise<void> {
|
|
275
|
+
if (options.headingsOnly) {
|
|
276
|
+
// Show only headings and code block IDs
|
|
277
|
+
const headingsOutput = extractHeadingsAndBlocks(subjects);
|
|
278
|
+
console.log(headingsOutput);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Build the full markdown content
|
|
283
|
+
const markdown = buildSubjectsMarkdown(subjects);
|
|
284
|
+
|
|
285
|
+
// Render markdown to terminal for nice formatting
|
|
286
|
+
let output = await marked.parse(markdown);
|
|
287
|
+
|
|
288
|
+
// Apply line range filter if specified (after rendering)
|
|
289
|
+
const startLine = options.startLine ?? 1;
|
|
290
|
+
if (options.startLine !== undefined || options.endLine !== undefined) {
|
|
291
|
+
const end = options.endLine ?? output.split("\n").length;
|
|
292
|
+
output = filterByLineRange(output, startLine, end);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add line numbers after rendering (if requested)
|
|
296
|
+
// Line numbers correspond to the rendered output that agents interact with
|
|
297
|
+
if (options.showLineNumbers) {
|
|
298
|
+
output = addLineNumbers(output, startLine);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(output);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Find and print code blocks by ID
|
|
306
|
+
*/
|
|
307
|
+
async function printCodeBlockById(
|
|
308
|
+
id: string,
|
|
309
|
+
topics: string[],
|
|
310
|
+
showLineNumbers: boolean,
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
// If topics are specified, search only those; otherwise search all subjects
|
|
313
|
+
const subjects = topics.length > 0 ? getSubject(...topics) : getAllSubjects();
|
|
314
|
+
|
|
315
|
+
interface CodeBlockMatch {
|
|
316
|
+
subjectId: string;
|
|
317
|
+
subjectTitle: string;
|
|
318
|
+
section: string;
|
|
319
|
+
code: string;
|
|
320
|
+
type: "prelude" | "example";
|
|
321
|
+
startLine?: number;
|
|
322
|
+
endLine?: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const matches: CodeBlockMatch[] = [];
|
|
326
|
+
|
|
327
|
+
for (const subject of subjects) {
|
|
328
|
+
// Build the rendered markdown to get correct line numbers (matching --start/--end behavior)
|
|
329
|
+
const fullMarkdown = buildSubjectsMarkdown([subject]);
|
|
330
|
+
const renderedOutput = await marked.parse(fullMarkdown);
|
|
331
|
+
const renderedLines = renderedOutput.split("\n");
|
|
332
|
+
|
|
333
|
+
// Search in prelude blocks
|
|
334
|
+
for (const block of subject.prelude) {
|
|
335
|
+
if (block.id === id) {
|
|
336
|
+
// Find line numbers in the rendered output
|
|
337
|
+
let startLine: number | undefined;
|
|
338
|
+
let endLine: number | undefined;
|
|
339
|
+
|
|
340
|
+
// Search for the prelude code in the rendered output
|
|
341
|
+
const codeLines = block.code.split("\n");
|
|
342
|
+
const firstCodeLine = codeLines[0].trim();
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < renderedLines.length; i++) {
|
|
345
|
+
// Strip ANSI codes before comparing
|
|
346
|
+
if (stripVTControlCharacters(renderedLines[i]).trim() === firstCodeLine) {
|
|
347
|
+
// Found the start of the code
|
|
348
|
+
startLine = i + 1; // 1-based line numbers
|
|
349
|
+
endLine = i + codeLines.length;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
matches.push({
|
|
355
|
+
subjectId: subject.id,
|
|
356
|
+
subjectTitle: subject.title,
|
|
357
|
+
section: "Prelude",
|
|
358
|
+
code: block.code,
|
|
359
|
+
type: "prelude",
|
|
360
|
+
startLine,
|
|
361
|
+
endLine,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Search in examples
|
|
367
|
+
for (const example of subject.examples) {
|
|
368
|
+
if (example.id === id) {
|
|
369
|
+
// Try to find which section this example belongs to
|
|
370
|
+
let sectionName = "Unknown Section";
|
|
371
|
+
let startLine: number | undefined;
|
|
372
|
+
let endLine: number | undefined;
|
|
373
|
+
|
|
374
|
+
for (const section of subject.sections) {
|
|
375
|
+
if (
|
|
376
|
+
section.content.includes(example.code.substring(0, Math.min(50, example.code.length)))
|
|
377
|
+
) {
|
|
378
|
+
sectionName = section.heading;
|
|
379
|
+
|
|
380
|
+
// Find line numbers in the rendered output
|
|
381
|
+
const codeLines = example.code.split("\n");
|
|
382
|
+
const firstCodeLine = codeLines[0].trim();
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < renderedLines.length; i++) {
|
|
385
|
+
// Strip ANSI codes before comparing
|
|
386
|
+
if (stripVTControlCharacters(renderedLines[i]).trim() === firstCodeLine) {
|
|
387
|
+
// Found the start of the code
|
|
388
|
+
startLine = i + 1; // 1-based line numbers
|
|
389
|
+
endLine = i + codeLines.length;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
matches.push({
|
|
398
|
+
subjectId: subject.id,
|
|
399
|
+
subjectTitle: subject.title,
|
|
400
|
+
section: sectionName,
|
|
401
|
+
code: example.code,
|
|
402
|
+
type: "example",
|
|
403
|
+
startLine,
|
|
404
|
+
endLine,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (matches.length === 0) {
|
|
411
|
+
console.error(`Error: No code block found with id "${id}"`);
|
|
412
|
+
if (topics.length > 0) {
|
|
413
|
+
console.error(`Searched in topics: ${topics.join(", ")}`);
|
|
414
|
+
} else {
|
|
415
|
+
console.error("Searched in all available topics");
|
|
416
|
+
}
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Build markdown output
|
|
421
|
+
for (let i = 0; i < matches.length; i++) {
|
|
422
|
+
const match = matches[i];
|
|
423
|
+
|
|
424
|
+
if (matches.length > 1 && i > 0) {
|
|
425
|
+
console.log("\n---\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Build markdown for this match
|
|
429
|
+
let matchMarkdown = `# ${match.subjectTitle}\n\n`;
|
|
430
|
+
matchMarkdown += `## ${match.section}\n\n`;
|
|
431
|
+
|
|
432
|
+
// Add line number info if available and requested (as plain text, not in markdown)
|
|
433
|
+
if (showLineNumbers && match.startLine && match.endLine) {
|
|
434
|
+
console.log(`Lines ${match.startLine}-${match.endLine} (use with --start/--end)\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
matchMarkdown += `\`\`\`typescript\n${match.code}\n\`\`\`\n`;
|
|
438
|
+
|
|
439
|
+
// Render the markdown
|
|
440
|
+
const rendered = await marked.parse(matchMarkdown);
|
|
441
|
+
console.log(rendered);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Print information about the corpus command
|
|
447
|
+
*/
|
|
448
|
+
function printCorpusHelp(): void {
|
|
449
|
+
console.log("Fragno Corpus - Code examples and documentation (similar to LLMs.txt");
|
|
450
|
+
console.log("");
|
|
451
|
+
console.log("Usage: fragno-cli corpus [options] [topic...]");
|
|
452
|
+
console.log("");
|
|
453
|
+
console.log("Options:");
|
|
454
|
+
console.log(" -n, --no-line-numbers Hide line numbers (shown by default)");
|
|
455
|
+
console.log(" -s, --start N Starting line number to display from");
|
|
456
|
+
console.log(" -e, --end N Ending line number to display to");
|
|
457
|
+
console.log(" --headings Show only headings and code block IDs");
|
|
458
|
+
console.log(" --id <id> Retrieve a specific code block by ID");
|
|
459
|
+
console.log("");
|
|
460
|
+
console.log("Examples:");
|
|
461
|
+
console.log(" fragno-cli corpus # List all available topics");
|
|
462
|
+
console.log(" fragno-cli corpus defining-routes # Show route definition examples");
|
|
463
|
+
console.log(" fragno-cli corpus --headings database-querying");
|
|
464
|
+
console.log(" # Show structure overview");
|
|
465
|
+
console.log(" fragno-cli corpus --start 10 --end 50 database-querying");
|
|
466
|
+
console.log(" # Show specific lines");
|
|
467
|
+
console.log(" fragno-cli corpus --id create-user # Get code block by ID");
|
|
468
|
+
console.log(" fragno-cli corpus database-adapters kysely-adapter");
|
|
469
|
+
console.log(" # Show multiple topics");
|
|
470
|
+
console.log("");
|
|
471
|
+
console.log("Available topics:");
|
|
472
|
+
|
|
473
|
+
const subjects = getSubjects();
|
|
474
|
+
|
|
475
|
+
// Group subjects by their tree structure
|
|
476
|
+
const rootSubjects: Array<{
|
|
477
|
+
id: string;
|
|
478
|
+
title: string;
|
|
479
|
+
children: Array<{ id: string; title: string }>;
|
|
480
|
+
}> = [];
|
|
481
|
+
const subjectMap = new Map(subjects.map((s) => [s.id, s]));
|
|
482
|
+
|
|
483
|
+
for (const subject of subjects) {
|
|
484
|
+
const parent = getSubjectParent(subject.id);
|
|
485
|
+
if (!parent) {
|
|
486
|
+
// This is a root subject
|
|
487
|
+
const children = getSubjectChildren(subject.id);
|
|
488
|
+
rootSubjects.push({
|
|
489
|
+
id: subject.id,
|
|
490
|
+
title: subject.title,
|
|
491
|
+
children: children.map((childId) => ({
|
|
492
|
+
id: childId,
|
|
493
|
+
title: subjectMap.get(childId)?.title || childId,
|
|
494
|
+
})),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Display in tree format
|
|
500
|
+
for (const root of rootSubjects) {
|
|
501
|
+
console.log(` ${root.id.padEnd(30)} ${root.title}`);
|
|
502
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
503
|
+
const child = root.children[i];
|
|
504
|
+
const isLast = i === root.children.length - 1;
|
|
505
|
+
const connector = isLast ? "└─" : "├─";
|
|
506
|
+
console.log(` ${connector} ${child.id.padEnd(26)} ${child.title}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export const corpusCommand = define({
|
|
512
|
+
name: "corpus",
|
|
513
|
+
description: "View code examples and documentation for Fragno",
|
|
514
|
+
args: {
|
|
515
|
+
"no-line-numbers": {
|
|
516
|
+
type: "boolean",
|
|
517
|
+
short: "n",
|
|
518
|
+
description: "Hide line numbers (line numbers are shown by default)",
|
|
519
|
+
},
|
|
520
|
+
start: {
|
|
521
|
+
type: "number",
|
|
522
|
+
short: "s",
|
|
523
|
+
description: "Starting line number (1-based) to display from",
|
|
524
|
+
},
|
|
525
|
+
end: {
|
|
526
|
+
type: "number",
|
|
527
|
+
short: "e",
|
|
528
|
+
description: "Ending line number (1-based) to display to",
|
|
529
|
+
},
|
|
530
|
+
headings: {
|
|
531
|
+
type: "boolean",
|
|
532
|
+
description: "Show only section headings and code block IDs with line numbers",
|
|
533
|
+
},
|
|
534
|
+
id: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "Retrieve a specific code block by ID",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
run: async (ctx) => {
|
|
540
|
+
const topics = ctx.positionals;
|
|
541
|
+
const showLineNumbers = !(ctx.values["no-line-numbers"] ?? false);
|
|
542
|
+
const startLine = ctx.values.start;
|
|
543
|
+
const endLine = ctx.values.end;
|
|
544
|
+
const headingsOnly = ctx.values.headings ?? false;
|
|
545
|
+
const codeBlockId = ctx.values.id;
|
|
546
|
+
|
|
547
|
+
// Handle --id flag
|
|
548
|
+
if (codeBlockId) {
|
|
549
|
+
await printCodeBlockById(codeBlockId, topics, showLineNumbers);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// No topics provided - show help
|
|
554
|
+
if (topics.length === 0) {
|
|
555
|
+
printCorpusHelp();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Validate line range
|
|
560
|
+
if (startLine !== undefined && endLine !== undefined && startLine > endLine) {
|
|
561
|
+
console.error("Error: --start must be less than or equal to --end");
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Load and display requested topics
|
|
566
|
+
try {
|
|
567
|
+
const subjects = getSubject(...topics);
|
|
568
|
+
|
|
569
|
+
await printSubjects(subjects, {
|
|
570
|
+
showLineNumbers,
|
|
571
|
+
startLine,
|
|
572
|
+
endLine,
|
|
573
|
+
headingsOnly,
|
|
574
|
+
});
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error("Error loading topics:", error instanceof Error ? error.message : error);
|
|
577
|
+
console.log("\nRun 'fragno-cli corpus' to see available topics.");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { define } from "gunshi";
|
|
2
|
+
import {
|
|
3
|
+
mergeResultsByUrl,
|
|
4
|
+
formatAsMarkdown,
|
|
5
|
+
formatAsJson,
|
|
6
|
+
} from "../utils/format-search-results.js";
|
|
7
|
+
|
|
8
|
+
interface SearchResult {
|
|
9
|
+
id: string;
|
|
10
|
+
type: "page" | "heading" | "text";
|
|
11
|
+
content: string;
|
|
12
|
+
breadcrumbs?: string[];
|
|
13
|
+
contentWithHighlights?: Array<{
|
|
14
|
+
type: string;
|
|
15
|
+
content: string;
|
|
16
|
+
styles?: { highlight?: boolean };
|
|
17
|
+
}>;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const searchCommand = define({
|
|
22
|
+
name: "search",
|
|
23
|
+
description: "Search the Fragno documentation",
|
|
24
|
+
args: {
|
|
25
|
+
limit: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Maximum number of results to show",
|
|
28
|
+
default: 10,
|
|
29
|
+
},
|
|
30
|
+
json: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
description: "Output results in JSON format",
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
markdown: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Output results in Markdown format (default)",
|
|
38
|
+
default: true,
|
|
39
|
+
},
|
|
40
|
+
"base-url": {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Base URL for the documentation site",
|
|
43
|
+
default: "fragno.dev",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
run: async (ctx) => {
|
|
47
|
+
const query = ctx.positionals.join(" ");
|
|
48
|
+
|
|
49
|
+
if (!query || query.trim().length === 0) {
|
|
50
|
+
throw new Error("Please provide a search query");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Determine output mode
|
|
54
|
+
const jsonMode = ctx.values.json as boolean;
|
|
55
|
+
const baseUrl = ctx.values["base-url"] as string;
|
|
56
|
+
|
|
57
|
+
if (!jsonMode) {
|
|
58
|
+
console.log(`Searching for: "${query}"\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Make request to the docs search API
|
|
63
|
+
const encodedQuery = encodeURIComponent(query);
|
|
64
|
+
const response = await fetch(`https://${baseUrl}/api/search?query=${encodedQuery}`);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`API request failed with status ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const results = (await response.json()) as SearchResult[];
|
|
71
|
+
|
|
72
|
+
// Apply limit
|
|
73
|
+
const limit = ctx.values.limit as number;
|
|
74
|
+
const limitedResults = results.slice(0, limit);
|
|
75
|
+
|
|
76
|
+
if (limitedResults.length === 0) {
|
|
77
|
+
if (jsonMode) {
|
|
78
|
+
console.log("[]");
|
|
79
|
+
} else {
|
|
80
|
+
console.log("No results found.");
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Merge results by URL
|
|
86
|
+
const mergedResults = mergeResultsByUrl(limitedResults, baseUrl);
|
|
87
|
+
|
|
88
|
+
// Output based on mode
|
|
89
|
+
if (jsonMode) {
|
|
90
|
+
console.log(formatAsJson(mergedResults));
|
|
91
|
+
} else {
|
|
92
|
+
// Markdown mode (default)
|
|
93
|
+
console.log(
|
|
94
|
+
`Found ${results.length} result${results.length === 1 ? "" : "s"}${results.length > limit ? ` (showing ${limit})` : ""}\n`,
|
|
95
|
+
);
|
|
96
|
+
console.log(formatAsMarkdown(mergedResults));
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
throw new Error("Search failed: An unknown error occurred");
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|