@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/.turbo/turbo-build.log +11 -7
- package/CHANGELOG.md +77 -0
- package/dist/cli.d.ts +4 -36
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -626
- package/dist/cli.js.map +1 -1
- package/dist/find-fragno-databases-Depht1jV.js +184 -0
- package/dist/find-fragno-databases-Depht1jV.js.map +1 -0
- package/dist/serve-eh3Tpjhc.js +87 -0
- package/dist/serve-eh3Tpjhc.js.map +1 -0
- package/package.json +28 -29
- package/src/cli.ts +14 -12
- package/src/commands/db/generate.ts +3 -1
- package/src/commands/db/info.ts +2 -0
- package/src/commands/db/migrate.ts +3 -1
- package/src/commands/search.ts +1 -0
- package/src/commands/serve.ts +151 -0
- package/src/utils/find-fragno-databases.ts +44 -6
- package/src/utils/load-config.test.ts +42 -36
- package/src/utils/load-config.ts +5 -9
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/src/commands/corpus.test.ts +0 -1129
- package/src/commands/corpus.ts +0 -632
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 {
|
|
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] === "
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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 {
|
|
425
|
+
export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand, run, searchCommand };
|
|
995
426
|
//# sourceMappingURL=cli.js.map
|