@fragno-dev/cli 0.2.0 → 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.
package/dist/cli.js CHANGED
@@ -1,186 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { n as importFragmentFiles } from "./find-fragno-databases-Depht1jV.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
2
6
  import { cli, define } from "gunshi";
3
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { dirname, join, relative, resolve } from "node:path";
7
+ import { mkdir, writeFile } from "node:fs/promises";
5
8
  import { executeMigrations, generateSchemaArtifacts } from "@fragno-dev/db/generation-engine";
6
- import { FragnoDatabase, isFragnoDatabase } from "@fragno-dev/db";
7
- import { fragnoDatabaseAdapterNameFakeSymbol, fragnoDatabaseAdapterVersionFakeSymbol } from "@fragno-dev/db/adapters";
8
- import { instantiatedFragmentFakeSymbol } from "@fragno-dev/core/internal/symbols";
9
- import "@fragno-dev/core";
10
- import { loadConfig } from "c12";
11
- import { constants, readFileSync } from "node:fs";
12
- import { getAllSubjectIdsInOrder, getAllSubjects, getCategoryTitle, getSubject, getSubjectChildren, getSubjectParent, getSubjects, isCategory } from "@fragno-dev/corpus";
13
- import { marked } from "marked";
14
- import { markedTerminal } from "marked-terminal";
15
- import { stripVTControlCharacters } from "node:util";
16
- import { fileURLToPath } from "node:url";
17
-
18
- //#region src/utils/load-config.ts
19
- /**
20
- * Checks if a file exists using async API.
21
- */
22
- async function fileExists(path) {
23
- try {
24
- await access(path, constants.F_OK);
25
- return true;
26
- } catch {
27
- return false;
28
- }
29
- }
30
- /**
31
- * Walks up the directory tree from the target path to find a tsconfig.json file.
32
- */
33
- async function findTsconfig(startPath) {
34
- let currentDir = dirname(startPath);
35
- const root = resolve("/");
36
- while (currentDir !== root) {
37
- const tsconfigPath = join(currentDir, "tsconfig.json");
38
- if (await fileExists(tsconfigPath)) return tsconfigPath;
39
- currentDir = dirname(currentDir);
40
- }
41
- return null;
42
- }
43
- /**
44
- * Strips comments from JSONC (JSON with Comments) content.
45
- */
46
- function stripJsonComments(jsonc) {
47
- let result = jsonc.replace(/\/\/[^\n]*/g, "");
48
- result = result.replace(/\/\*[\s\S]*?\*\//g, "");
49
- return result;
50
- }
51
- /**
52
- * Converts TypeScript path aliases to jiti alias format.
53
- * Strips trailing '*' from aliases and paths, and resolves paths relative to baseUrl.
54
- */
55
- function convertTsconfigPathsToJitiAlias(tsconfigPaths, baseUrlResolved) {
56
- return Object.fromEntries(Object.entries(tsconfigPaths).map(([_alias, paths]) => {
57
- const pathsArray = paths;
58
- return [_alias.endsWith("*") ? _alias.slice(0, -1) : _alias, resolve(baseUrlResolved, pathsArray[0].endsWith("*") ? pathsArray[0].slice(0, -1) : pathsArray[0])];
59
- }));
60
- }
61
- /**
62
- * Resolves tsconfig path aliases for use with jiti.
63
- */
64
- async function resolveTsconfigAliases(targetPath) {
65
- const tsconfigPath = await findTsconfig(targetPath);
66
- if (!tsconfigPath) return {};
67
- try {
68
- const jsonContent = stripJsonComments(await readFile(tsconfigPath, "utf-8"));
69
- const tsconfig = JSON.parse(jsonContent);
70
- const tsconfigPaths = tsconfig?.compilerOptions?.paths;
71
- if (!tsconfigPaths || typeof tsconfigPaths !== "object") return {};
72
- return convertTsconfigPathsToJitiAlias(tsconfigPaths, resolve(dirname(tsconfigPath), tsconfig?.compilerOptions?.baseUrl || "."));
73
- } catch (error) {
74
- console.warn(`Warning: Failed to parse tsconfig at ${tsconfigPath}:`, error);
75
- return {};
76
- }
77
- }
78
- /**
79
- * Loads a config file using c12 with automatic tsconfig path alias resolution.
80
- */
81
- async function loadConfig$1(path) {
82
- const { config } = await loadConfig({
83
- configFile: path,
84
- jitiOptions: { alias: await resolveTsconfigAliases(path) }
85
- });
86
- return config;
87
- }
88
-
89
- //#endregion
90
- //#region src/utils/find-fragno-databases.ts
91
- async function importFragmentFile(path) {
92
- process.env["FRAGNO_INIT_DRY_RUN"] = "true";
93
- try {
94
- const databases = findFragnoDatabases(await loadConfig$1(path));
95
- const adapterNames = databases.map((db) => `${db.adapter[fragnoDatabaseAdapterNameFakeSymbol]}@${db.adapter[fragnoDatabaseAdapterVersionFakeSymbol]}`);
96
- if ([...new Set(adapterNames)].length > 1) throw new Error(`All Fragno databases must use the same adapter name and version. Found mismatch: (${adapterNames.join(", ")})`);
97
- return {
98
- adapter: databases[0].adapter,
99
- databases
100
- };
101
- } finally {
102
- delete process.env["FRAGNO_INIT_DRY_RUN"];
103
- }
104
- }
105
- /**
106
- * Imports multiple fragment files and validates they all use the same adapter.
107
- * Returns the combined databases from all files.
108
- */
109
- async function importFragmentFiles(paths) {
110
- const uniquePaths = Array.from(new Set(paths));
111
- if (uniquePaths.length === 0) throw new Error("No fragment files provided");
112
- const allDatabases = [];
113
- let adapter;
114
- let firstAdapterFile;
115
- const cwd = process.cwd();
116
- for (const path of uniquePaths) {
117
- const relativePath = relative(cwd, path);
118
- try {
119
- const result = await importFragmentFile(path);
120
- const databases = result["databases"];
121
- const fileAdapter = result["adapter"];
122
- if (databases.length === 0) {
123
- console.warn(`Warning: No FragnoDatabase instances found in ${relativePath}.\nMake sure you export either:\n - A FragnoDatabase instance created with .create(adapter)\n - An instantiated fragment with embedded database definition\n`);
124
- continue;
125
- }
126
- if (!adapter) {
127
- adapter = fileAdapter;
128
- firstAdapterFile = relativePath;
129
- }
130
- const firstAdapterName = adapter[fragnoDatabaseAdapterNameFakeSymbol];
131
- const firstAdapterVersion = adapter[fragnoDatabaseAdapterVersionFakeSymbol];
132
- const fileAdapterName = fileAdapter[fragnoDatabaseAdapterNameFakeSymbol];
133
- const fileAdapterVersion = fileAdapter[fragnoDatabaseAdapterVersionFakeSymbol];
134
- if (firstAdapterName !== fileAdapterName || firstAdapterVersion !== fileAdapterVersion) {
135
- const firstAdapterInfo = `${firstAdapterName}@${firstAdapterVersion}`;
136
- const fileAdapterInfo = `${fileAdapterName}@${fileAdapterVersion}`;
137
- throw new Error(`All fragments must use the same database adapter. Mixed adapters found:\n - ${firstAdapterFile}: ${firstAdapterInfo}\n - ${relativePath}: ${fileAdapterInfo}\n\nMake sure all fragments use the same adapter name and version.`);
138
- }
139
- allDatabases.push(...databases);
140
- console.log(` Found ${databases.length} database(s) in ${relativePath}`);
141
- } catch (error) {
142
- throw new Error(`Failed to import fragment file ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
143
- }
144
- }
145
- if (allDatabases.length === 0) throw new Error("No FragnoDatabase instances found in any of the target files.\nMake sure your files export either:\n - A FragnoDatabase instance created with .create(adapter)\n - An instantiated fragment with embedded database definition\n");
146
- if (!adapter) throw new Error("No adapter found in any of the fragment files");
147
- return {
148
- adapter,
149
- databases: allDatabases
150
- };
151
- }
152
- function isNewFragnoInstantiatedFragment(value) {
153
- return typeof value === "object" && value !== null && instantiatedFragmentFakeSymbol in value && value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol;
154
- }
155
- /**
156
- * Finds all FragnoDatabase instances in a module, including those embedded
157
- * in instantiated fragments.
158
- */
159
- function findFragnoDatabases(targetModule) {
160
- const fragnoDatabases = [];
161
- for (const [_key, value] of Object.entries(targetModule)) if (isFragnoDatabase(value)) fragnoDatabases.push(value);
162
- else if (isNewFragnoInstantiatedFragment(value)) {
163
- const internal = value.$internal;
164
- const deps = internal.deps;
165
- const options = internal.options;
166
- if (!deps["db"] || !deps["schema"]) continue;
167
- const schema = deps["schema"];
168
- const namespace = deps["namespace"];
169
- const databaseAdapter = options["databaseAdapter"];
170
- if (!databaseAdapter) {
171
- console.warn(`Warning: Fragment '${value.name}' appears to be a database fragment but no databaseAdapter found in options.`);
172
- continue;
173
- }
174
- fragnoDatabases.push(new FragnoDatabase({
175
- namespace,
176
- schema,
177
- adapter: databaseAdapter
178
- }));
179
- }
180
- return fragnoDatabases;
181
- }
182
9
 
183
- //#endregion
184
10
  //#region src/commands/db/generate.ts
185
11
  const generateCommand = define({
186
12
  name: "generate",
@@ -264,48 +90,6 @@ const generateCommand = define({
264
90
  }
265
91
  });
266
92
 
267
- //#endregion
268
- //#region src/commands/db/migrate.ts
269
- const migrateCommand = define({
270
- name: "migrate",
271
- description: "Run SQL database migrations for all fragments to their latest versions",
272
- args: {},
273
- run: async (ctx) => {
274
- const targets = ctx.positionals;
275
- if (targets.length === 0) throw new Error("At least one target file path is required");
276
- const { databases: allFragnoDatabases } = await importFragmentFiles(targets.map((target) => resolve(process.cwd(), target)));
277
- console.log("\nMigrating all fragments to their latest versions...\n");
278
- let results;
279
- try {
280
- results = await executeMigrations(allFragnoDatabases);
281
- } catch (error) {
282
- throw new Error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
283
- }
284
- for (const result of results) {
285
- console.log(`Fragment: ${result.namespace}`);
286
- console.log(` Current version: ${result.fromVersion}`);
287
- console.log(` Target version: ${result.toVersion}`);
288
- if (result.didMigrate) console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\n`);
289
- else console.log(` ✓ Already at latest version. No migration needed.\n`);
290
- }
291
- console.log("═══════════════════════════════════════");
292
- console.log("Migration Summary");
293
- console.log("═══════════════════════════════════════");
294
- const migrated = results.filter((r) => r.didMigrate);
295
- const skipped = results.filter((r) => !r.didMigrate);
296
- if (migrated.length > 0) {
297
- console.log(`\n✓ Migrated ${migrated.length} fragment(s):`);
298
- for (const r of migrated) console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);
299
- }
300
- if (skipped.length > 0) {
301
- console.log(`\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);
302
- for (const r of skipped) console.log(` - ${r.namespace}: v${r.toVersion}`);
303
- }
304
- for (const db of allFragnoDatabases) await db.adapter.close();
305
- console.log("\n✓ All migrations completed successfully");
306
- }
307
- });
308
-
309
93
  //#endregion
310
94
  //#region src/commands/db/info.ts
311
95
  const infoCommand = define({
@@ -368,6 +152,48 @@ const infoCommand = define({
368
152
  }
369
153
  });
370
154
 
155
+ //#endregion
156
+ //#region src/commands/db/migrate.ts
157
+ const migrateCommand = define({
158
+ name: "migrate",
159
+ description: "Run SQL database migrations for all fragments to their latest versions",
160
+ args: {},
161
+ run: async (ctx) => {
162
+ const targets = ctx.positionals;
163
+ if (targets.length === 0) throw new Error("At least one target file path is required");
164
+ const { databases: allFragnoDatabases } = await importFragmentFiles(targets.map((target) => resolve(process.cwd(), target)));
165
+ console.log("\nMigrating all fragments to their latest versions...\n");
166
+ let results;
167
+ try {
168
+ results = await executeMigrations(allFragnoDatabases);
169
+ } catch (error) {
170
+ throw new Error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
171
+ }
172
+ for (const result of results) {
173
+ console.log(`Fragment: ${result.namespace}`);
174
+ console.log(` Current version: ${result.fromVersion}`);
175
+ console.log(` Target version: ${result.toVersion}`);
176
+ if (result.didMigrate) console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\n`);
177
+ else console.log(` ✓ Already at latest version. No migration needed.\n`);
178
+ }
179
+ console.log("═══════════════════════════════════════");
180
+ console.log("Migration Summary");
181
+ console.log("═══════════════════════════════════════");
182
+ const migrated = results.filter((r) => r.didMigrate);
183
+ const skipped = results.filter((r) => !r.didMigrate);
184
+ if (migrated.length > 0) {
185
+ console.log(`\n✓ Migrated ${migrated.length} fragment(s):`);
186
+ for (const r of migrated) console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);
187
+ }
188
+ if (skipped.length > 0) {
189
+ console.log(`\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);
190
+ for (const r of skipped) console.log(` - ${r.namespace}: v${r.toVersion}`);
191
+ }
192
+ for (const db of allFragnoDatabases) await db.adapter.close();
193
+ console.log("\n✓ All migrations completed successfully");
194
+ }
195
+ });
196
+
371
197
  //#endregion
372
198
  //#region src/utils/format-search-results.ts
373
199
  /**
@@ -498,403 +324,6 @@ const searchCommand = define({
498
324
  }
499
325
  });
500
326
 
501
- //#endregion
502
- //#region src/commands/corpus.ts
503
- marked.use(markedTerminal());
504
- /**
505
- * Build markdown content for multiple subjects
506
- */
507
- function buildSubjectsMarkdown(subjects) {
508
- let fullMarkdown = "";
509
- for (const subject of subjects) {
510
- fullMarkdown += `# ${subject.title}\n\n`;
511
- if (subject.description) fullMarkdown += `${subject.description}\n\n`;
512
- if (subject.imports) fullMarkdown += `### Imports\n\n\`\`\`typescript\n${subject.imports}\n\`\`\`\n\n`;
513
- if (subject.prelude.length > 0) {
514
- fullMarkdown += `### Prelude\n\n`;
515
- for (const block of subject.prelude) fullMarkdown += `\`\`\`typescript\n${block.code}\n\`\`\`\n\n`;
516
- }
517
- for (const section of subject.sections) fullMarkdown += `## ${section.heading}\n\n${section.content}\n\n`;
518
- }
519
- return fullMarkdown;
520
- }
521
- /**
522
- * Add line numbers to content
523
- */
524
- function addLineNumbers(content, startFrom = 1) {
525
- const lines = content.split("\n");
526
- const maxDigits = String(startFrom + lines.length - 1).length;
527
- return lines.map((line, index) => {
528
- const lineNum = startFrom + index;
529
- return `${String(lineNum).padStart(maxDigits, " ")}│ ${line}`;
530
- }).join("\n");
531
- }
532
- /**
533
- * Filter content by line range
534
- */
535
- function filterByLineRange(content, startLine, endLine) {
536
- const lines = content.split("\n");
537
- const start = Math.max(0, startLine - 1);
538
- const end = Math.min(lines.length, endLine);
539
- return lines.slice(start, end).join("\n");
540
- }
541
- /**
542
- * Extract headings and code block information with line numbers
543
- */
544
- function extractHeadingsAndBlocks(subjects) {
545
- let output = "";
546
- let currentLine = 1;
547
- let lastOutputLine = 0;
548
- const addGapIfNeeded = () => {
549
- if (lastOutputLine > 0 && currentLine > lastOutputLine + 1) output += ` │\n`;
550
- };
551
- output += "Use --start N --end N flags to show specific line ranges\n\n";
552
- for (const subject of subjects) {
553
- addGapIfNeeded();
554
- output += `${currentLine.toString().padStart(4, " ")}│ # ${subject.title}\n`;
555
- lastOutputLine = currentLine;
556
- currentLine += 1;
557
- output += `${currentLine.toString().padStart(4, " ")}│\n`;
558
- lastOutputLine = currentLine;
559
- currentLine += 1;
560
- if (subject.description) {
561
- const descLines = subject.description.split("\n");
562
- for (const line of descLines) {
563
- output += `${currentLine.toString().padStart(4, " ")}│ ${line}\n`;
564
- lastOutputLine = currentLine;
565
- currentLine += 1;
566
- }
567
- output += `${currentLine.toString().padStart(4, " ")}│\n`;
568
- lastOutputLine = currentLine;
569
- currentLine += 1;
570
- }
571
- if (subject.imports) {
572
- addGapIfNeeded();
573
- output += `${currentLine.toString().padStart(4, " ")}│ ### Imports\n`;
574
- lastOutputLine = currentLine;
575
- currentLine += 1;
576
- output += `${currentLine.toString().padStart(4, " ")}│\n`;
577
- lastOutputLine = currentLine;
578
- currentLine += 1;
579
- output += `${currentLine.toString().padStart(4, " ")}│ \`\`\`typescript\n`;
580
- lastOutputLine = currentLine;
581
- currentLine += 1;
582
- const importLines = subject.imports.split("\n");
583
- for (const line of importLines) {
584
- output += `${currentLine.toString().padStart(4, " ")}│ ${line}\n`;
585
- lastOutputLine = currentLine;
586
- currentLine += 1;
587
- }
588
- output += `${currentLine.toString().padStart(4, " ")}│ \`\`\`\n`;
589
- lastOutputLine = currentLine;
590
- currentLine += 1;
591
- output += `${currentLine.toString().padStart(4, " ")}│\n`;
592
- lastOutputLine = currentLine;
593
- currentLine += 1;
594
- }
595
- if (subject.prelude.length > 0) {
596
- addGapIfNeeded();
597
- output += `${currentLine.toString().padStart(4, " ")}│ ### Prelude\n`;
598
- lastOutputLine = currentLine;
599
- currentLine += 1;
600
- output += `${currentLine.toString().padStart(4, " ")}│\n`;
601
- lastOutputLine = currentLine;
602
- currentLine += 1;
603
- for (const block of subject.prelude) {
604
- const id = block.id || "(no-id)";
605
- const blockStartLine = currentLine + 1;
606
- const codeLines = block.code.split("\n").length;
607
- const blockEndLine = currentLine + 1 + codeLines;
608
- output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`, L${blockStartLine}-${blockEndLine}\n`;
609
- lastOutputLine = currentLine;
610
- currentLine += codeLines + 3;
611
- }
612
- lastOutputLine = currentLine - 1;
613
- }
614
- const sectionToExamples = /* @__PURE__ */ new Map();
615
- for (const example of subject.examples) for (const section of subject.sections) if (section.content.includes(example.code.substring(0, Math.min(50, example.code.length)))) {
616
- if (!sectionToExamples.has(section.heading)) sectionToExamples.set(section.heading, []);
617
- sectionToExamples.get(section.heading).push(example);
618
- break;
619
- }
620
- for (const section of subject.sections) {
621
- addGapIfNeeded();
622
- output += `${currentLine.toString().padStart(4, " ")}│ ## ${section.heading}\n`;
623
- lastOutputLine = currentLine;
624
- currentLine += 1;
625
- const examples = sectionToExamples.get(section.heading) || [];
626
- if (examples.length > 0) {
627
- const sectionStartLine = currentLine;
628
- const lines = section.content.split("\n");
629
- for (const example of examples) {
630
- const id = example.id || "(no-id)";
631
- let blockStartLine = sectionStartLine;
632
- let blockEndLine = sectionStartLine;
633
- let foundBlock = false;
634
- for (let i = 0; i < lines.length; i++) if (lines[i].trim().startsWith("```") && true) {
635
- const codeStart = i + 1;
636
- let matches = true;
637
- const exampleLines = example.code.split("\n");
638
- for (let j = 0; j < Math.min(3, exampleLines.length); j++) if (lines[codeStart + j]?.trim() !== exampleLines[j]?.trim()) {
639
- matches = false;
640
- break;
641
- }
642
- if (matches) {
643
- blockStartLine = sectionStartLine + i + 1;
644
- blockEndLine = sectionStartLine + i + exampleLines.length;
645
- foundBlock = true;
646
- break;
647
- }
648
- }
649
- if (foundBlock) output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`, L${blockStartLine}-${blockEndLine}\n`;
650
- else output += `${currentLine.toString().padStart(4, " ")}│ - id: \`${id}\`\n`;
651
- lastOutputLine = currentLine;
652
- }
653
- }
654
- const sectionLines = section.content.split("\n");
655
- for (const _line of sectionLines) currentLine += 1;
656
- currentLine += 1;
657
- lastOutputLine = currentLine - 1;
658
- }
659
- }
660
- return output;
661
- }
662
- /**
663
- * Print subjects with the given options
664
- */
665
- async function printSubjects(subjects, options) {
666
- if (options.headingsOnly) {
667
- const headingsOutput = extractHeadingsAndBlocks(subjects);
668
- console.log(headingsOutput);
669
- return;
670
- }
671
- const markdown = buildSubjectsMarkdown(subjects);
672
- let output = await marked.parse(markdown);
673
- const startLine = options.startLine ?? 1;
674
- if (options.startLine !== void 0 || options.endLine !== void 0) {
675
- const end = options.endLine ?? output.split("\n").length;
676
- output = filterByLineRange(output, startLine, end);
677
- }
678
- if (options.showLineNumbers) output = addLineNumbers(output, startLine);
679
- console.log(output);
680
- }
681
- /**
682
- * Find and print code blocks by ID
683
- */
684
- async function printCodeBlockById(id, topics, showLineNumbers) {
685
- const subjects = topics.length > 0 ? getSubject(...topics) : getAllSubjects();
686
- const matches = [];
687
- for (const subject of subjects) {
688
- const fullMarkdown = buildSubjectsMarkdown([subject]);
689
- const renderedLines = (await marked.parse(fullMarkdown)).split("\n");
690
- for (const block of subject.prelude) if (block.id === id) {
691
- let startLine;
692
- let endLine;
693
- const codeLines = block.code.split("\n");
694
- const firstCodeLine = codeLines[0].trim();
695
- for (let i = 0; i < renderedLines.length; i++) if (stripVTControlCharacters(renderedLines[i]).trim() === firstCodeLine) {
696
- startLine = i + 1;
697
- endLine = i + codeLines.length;
698
- break;
699
- }
700
- matches.push({
701
- subjectId: subject.id,
702
- subjectTitle: subject.title,
703
- section: "Prelude",
704
- code: block.code,
705
- type: "prelude",
706
- startLine,
707
- endLine
708
- });
709
- }
710
- for (const example of subject.examples) if (example.id === id) {
711
- let sectionName = "Unknown Section";
712
- let startLine;
713
- let endLine;
714
- for (const section of subject.sections) if (section.content.includes(example.code.substring(0, Math.min(50, example.code.length)))) {
715
- sectionName = section.heading;
716
- const codeLines = example.code.split("\n");
717
- const firstCodeLine = codeLines[0].trim();
718
- for (let i = 0; i < renderedLines.length; i++) if (stripVTControlCharacters(renderedLines[i]).trim() === firstCodeLine) {
719
- startLine = i + 1;
720
- endLine = i + codeLines.length;
721
- break;
722
- }
723
- break;
724
- }
725
- matches.push({
726
- subjectId: subject.id,
727
- subjectTitle: subject.title,
728
- section: sectionName,
729
- code: example.code,
730
- type: "example",
731
- startLine,
732
- endLine
733
- });
734
- }
735
- }
736
- if (matches.length === 0) {
737
- console.error(`Error: No code block found with id "${id}"`);
738
- if (topics.length > 0) console.error(`Searched in topics: ${topics.join(", ")}`);
739
- else console.error("Searched in all available topics");
740
- process.exit(1);
741
- }
742
- for (let i = 0; i < matches.length; i++) {
743
- const match = matches[i];
744
- if (matches.length > 1 && i > 0) console.log("\n---\n");
745
- let matchMarkdown = `# ${match.subjectTitle}\n\n`;
746
- matchMarkdown += `## ${match.section}\n\n`;
747
- if (showLineNumbers && match.startLine && match.endLine) console.log(`Lines ${match.startLine}-${match.endLine} (use with --start/--end)\n`);
748
- matchMarkdown += `\`\`\`typescript\n${match.code}\n\`\`\`\n`;
749
- const rendered = await marked.parse(matchMarkdown);
750
- console.log(rendered);
751
- }
752
- }
753
- /**
754
- * Print only the topic tree
755
- */
756
- function printTopicTree() {
757
- const subjects = getSubjects();
758
- const subjectMap = new Map(subjects.map((s) => [s.id, s]));
759
- function getTitle(subjectId) {
760
- if (isCategory(subjectId)) return getCategoryTitle(subjectId);
761
- const subject = subjectMap.get(subjectId);
762
- return subject ? subject.title : subjectId;
763
- }
764
- function displayNode(subjectId, indent, isLast, isRoot) {
765
- const title = getTitle(subjectId);
766
- if (isRoot) console.log(` ${subjectId.padEnd(30)} ${title}`);
767
- else {
768
- const connector = isLast ? "└─" : "├─";
769
- console.log(`${indent}${connector} ${subjectId.padEnd(26)} ${title}`);
770
- }
771
- const children = getSubjectChildren(subjectId);
772
- if (children.length > 0) {
773
- const childIndent = isRoot ? " " : indent + (isLast ? " " : "│ ");
774
- for (let i = 0; i < children.length; i++) displayNode(children[i], childIndent, i === children.length - 1, false);
775
- }
776
- }
777
- const rootIds = getAllSubjectIdsInOrder().filter((id) => !getSubjectParent(id));
778
- for (const subjectId of rootIds) displayNode(subjectId, "", false, true);
779
- }
780
- /**
781
- * Print information about the corpus command
782
- */
783
- function printCorpusHelp() {
784
- console.log("Fragno Corpus - Code examples and documentation (similar to LLMs.txt");
785
- console.log("");
786
- console.log("Usage: fragno-cli corpus [options] [topic...]");
787
- console.log("");
788
- console.log("Options:");
789
- console.log(" -n, --no-line-numbers Hide line numbers (shown by default)");
790
- console.log(" -s, --start N Starting line number to display from");
791
- console.log(" -e, --end N Ending line number to display to");
792
- console.log(" --headings Show only headings and code block IDs");
793
- console.log(" --id <id> Retrieve a specific code block by ID");
794
- console.log(" --tree Show only the topic tree");
795
- console.log("");
796
- console.log("Examples:");
797
- console.log(" fragno-cli corpus # List all available topics");
798
- console.log(" fragno-cli corpus --tree # Show only the topic tree");
799
- console.log(" fragno-cli corpus defining-routes # Show route definition examples");
800
- console.log(" fragno-cli corpus --headings database-querying");
801
- console.log(" # Show structure overview");
802
- console.log(" fragno-cli corpus --start 10 --end 50 database-querying");
803
- console.log(" # Show specific lines");
804
- console.log(" fragno-cli corpus --id create-user # Get code block by ID");
805
- console.log(" fragno-cli corpus database-adapters kysely-adapter");
806
- console.log(" # Show multiple topics");
807
- console.log("");
808
- console.log("Available topics:");
809
- printTopicTree();
810
- }
811
- const corpusCommand = define({
812
- name: "corpus",
813
- description: "View code examples and documentation for Fragno",
814
- args: {
815
- "no-line-numbers": {
816
- type: "boolean",
817
- short: "n",
818
- description: "Hide line numbers (line numbers are shown by default)"
819
- },
820
- start: {
821
- type: "number",
822
- short: "s",
823
- description: "Starting line number (1-based) to display from"
824
- },
825
- end: {
826
- type: "number",
827
- short: "e",
828
- description: "Ending line number (1-based) to display to"
829
- },
830
- headings: {
831
- type: "boolean",
832
- description: "Show only section headings and code block IDs with line numbers"
833
- },
834
- id: {
835
- type: "string",
836
- description: "Retrieve a specific code block by ID"
837
- },
838
- tree: {
839
- type: "boolean",
840
- description: "Show only the topic tree (without help text)"
841
- }
842
- },
843
- run: async (ctx) => {
844
- const topics = ctx.positionals;
845
- const showLineNumbers = !(ctx.values["no-line-numbers"] ?? false);
846
- const startLine = ctx.values.start;
847
- const endLine = ctx.values.end;
848
- const headingsOnly = ctx.values.headings ?? false;
849
- const codeBlockId = ctx.values.id;
850
- const treeOnly = ctx.values.tree ?? false;
851
- if (codeBlockId) {
852
- await printCodeBlockById(codeBlockId, topics, showLineNumbers);
853
- return;
854
- }
855
- if (treeOnly) {
856
- printTopicTree();
857
- return;
858
- }
859
- if (topics.length === 0) {
860
- printCorpusHelp();
861
- return;
862
- }
863
- if (startLine !== void 0 && endLine !== void 0 && startLine > endLine) {
864
- console.error("Error: --start must be less than or equal to --end");
865
- process.exit(1);
866
- }
867
- try {
868
- await printSubjects(getSubject(...topics), {
869
- showLineNumbers,
870
- startLine,
871
- endLine,
872
- headingsOnly
873
- });
874
- } catch (error) {
875
- if (error instanceof Error && error.message.includes("ENOENT")) {
876
- const missingTopics = topics.filter((topic) => {
877
- try {
878
- getSubject(topic);
879
- return false;
880
- } catch {
881
- return true;
882
- }
883
- });
884
- if (missingTopics.length === 1) console.error(`Error: Subject '${missingTopics[0]}' not found.`);
885
- else if (missingTopics.length > 1) console.error(`Error: Subjects not found: ${missingTopics.map((t) => `'${t}'`).join(", ")}`);
886
- else console.error("Error: One or more subjects not found.");
887
- console.log("\nAvailable topics:");
888
- printTopicTree();
889
- } else {
890
- console.error("Error loading topics:", error instanceof Error ? error.message : error);
891
- console.log("\nRun 'fragno-cli corpus' to see available topics.");
892
- }
893
- process.exit(1);
894
- }
895
- }
896
- });
897
-
898
327
  //#endregion
899
328
  //#region src/cli.ts
900
329
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -918,11 +347,13 @@ async function run() {
918
347
  name: "fragno-cli search",
919
348
  version
920
349
  });
921
- else if (args[0] === "corpus") await cli(args.slice(1), corpusCommand, {
922
- name: "fragno-cli corpus",
923
- version
924
- });
925
- else if (args[0] === "db") {
350
+ else if (args[0] === "serve") {
351
+ const { serveCommand } = await import("./serve-eh3Tpjhc.js");
352
+ await cli(args.slice(1), serveCommand, {
353
+ name: "fragno-cli serve",
354
+ version
355
+ });
356
+ } else if (args[0] === "db") {
926
357
  const subCommandName = args[1];
927
358
  if (!subCommandName || subCommandName === "--help" || subCommandName === "-h") {
928
359
  console.log("Database management commands");
@@ -964,14 +395,14 @@ async function run() {
964
395
  console.log(" fragno-cli <COMMAND>");
965
396
  console.log("");
966
397
  console.log("COMMANDS:");
398
+ console.log(" serve Start a local HTTP server to serve fragments");
967
399
  console.log(" db Database management commands");
968
400
  console.log(" search Search the Fragno documentation");
969
- console.log(" corpus View code examples and documentation for Fragno");
970
401
  console.log("");
971
402
  console.log("For more info, run any command with the `--help` flag:");
403
+ console.log(" fragno-cli serve --help");
972
404
  console.log(" fragno-cli db --help");
973
405
  console.log(" fragno-cli search --help");
974
- console.log(" fragno-cli corpus --help");
975
406
  console.log("");
976
407
  console.log("OPTIONS:");
977
408
  console.log(" -h, --help Display this help message");
@@ -991,5 +422,5 @@ async function run() {
991
422
  if (import.meta.main) await run();
992
423
 
993
424
  //#endregion
994
- export { corpusCommand, dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand, run, searchCommand };
425
+ export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand, run, searchCommand };
995
426
  //# sourceMappingURL=cli.js.map