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