@fragno-dev/cli 0.2.1 → 0.2.3

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