@fragno-dev/cli 0.1.0

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.
@@ -0,0 +1,15 @@
1
+ $ tsdown
2
+ ℹ tsdown v0.15.9 powered by rolldown v1.0.0-beta.44
3
+ ℹ Using tsdown config: /home/runner/work/fragno/fragno/apps/fragno-cli/tsdown.config.ts
4
+ ℹ entry: src/cli.ts
5
+ ℹ target: node20.0.0
6
+ ℹ tsconfig: tsconfig.json
7
+ ℹ Build start
8
+ ℹ Granting execute permission to dist/cli.d.ts
9
+ ℹ Granting execute permission to dist/cli.js
10
+ ℹ dist/cli.js 16.07 kB │ gzip: 3.50 kB
11
+ ℹ dist/cli.js.map 30.92 kB │ gzip: 6.79 kB
12
+ ℹ dist/cli.d.ts.map  0.23 kB │ gzip: 0.19 kB
13
+ ℹ dist/cli.d.ts  0.39 kB │ gzip: 0.19 kB
14
+ ℹ 4 files, total: 47.61 kB
15
+ ✔ Build complete in 8325ms
@@ -0,0 +1 @@
1
+ $ tsc --noEmit
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @fragno-dev/cli
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2c583a9: Initial release of @fragno-dev/db
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [2c583a9]
12
+ - @fragno-dev/db@0.1.0
13
+
14
+ ## 0.0.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [1fa71f3]
19
+ - Updated dependencies [c1483c6]
20
+ - @fragno-dev/core@0.0.7
package/dist/cli.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "gunshi";
3
+
4
+ //#region src/cli.d.ts
5
+ declare const generateCommand: Command;
6
+ declare const migrateCommand: Command;
7
+ declare const infoCommand: Command;
8
+ declare const dbCommand: Command;
9
+ declare const mainCommand: Command;
10
+ //#endregion
11
+ export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand };
12
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"sourcesContent":[],"mappings":";;;;cAQa,iBAAiB;cAkCjB,gBAAgB;AAlChB,cAyDA,WA1BZ,EA0ByB,OA1BzB;AAGY,cAwDA,SApCZ,EAoCuB,OAxDK;AAuBhB,cA4CA,WA5Ca,EA4CA,OAlCzB"}
package/dist/cli.js ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ import { cli, parseArgs, resolveArgs } from "gunshi";
3
+ import { mkdir, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { FragnoDatabase, isFragnoDatabase } from "@fragno-dev/db";
6
+ import { instantiatedFragmentFakeSymbol } from "@fragno-dev/core/api/fragment-instantiation";
7
+
8
+ //#region src/utils/find-fragno-databases.ts
9
+ function isFragnoInstantiatedFragment(value) {
10
+ return typeof value === "object" && value !== null && instantiatedFragmentFakeSymbol in value && value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol;
11
+ }
12
+ function additionalContextIsDatabaseContext(additionalContext) {
13
+ return typeof additionalContext === "object" && additionalContext !== null && "databaseSchema" in additionalContext && "databaseNamespace" in additionalContext && "databaseAdapter" in additionalContext;
14
+ }
15
+ /**
16
+ * Finds all FragnoDatabase instances in a module, including those embedded
17
+ * in instantiated fragments.
18
+ */
19
+ function findFragnoDatabases(targetModule) {
20
+ const fragnoDatabases = [];
21
+ for (const [key, value] of Object.entries(targetModule)) if (isFragnoDatabase(value)) {
22
+ fragnoDatabases.push(value);
23
+ console.log(`Found FragnoDatabase instance: ${key}`);
24
+ } else if (isFragnoInstantiatedFragment(value)) {
25
+ const additionalContext = value.additionalContext;
26
+ if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {
27
+ console.warn(`Instantiated fragment ${key} has no database context`);
28
+ continue;
29
+ }
30
+ const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;
31
+ fragnoDatabases.push(new FragnoDatabase({
32
+ namespace: databaseNamespace,
33
+ schema: databaseSchema,
34
+ adapter: databaseAdapter
35
+ }));
36
+ console.log(`Found database context in instantiated fragment: ${key}`);
37
+ }
38
+ return fragnoDatabases;
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/commands/db/generate.ts
43
+ async function generate(ctx) {
44
+ const target = ctx.values["target"];
45
+ const output = ctx.values["output"];
46
+ const toVersion = ctx.values["to"];
47
+ const fromVersion = ctx.values["from"];
48
+ const prefix = ctx.values["prefix"];
49
+ if (!target || typeof target !== "string") throw new Error("Target file path is required and must be a string");
50
+ if (typeof output === "number" || typeof output === "boolean") throw new Error("Output file path is required and must be a string");
51
+ if (toVersion !== void 0 && typeof toVersion !== "string" && typeof toVersion !== "number") throw new Error("Version must be a number or string");
52
+ if (fromVersion !== void 0 && typeof fromVersion !== "string" && typeof fromVersion !== "number") throw new Error("Version must be a number or string");
53
+ if (prefix !== void 0 && typeof prefix !== "string") throw new Error("Prefix must be a string");
54
+ const targetPath = resolve(process.cwd(), target);
55
+ console.log(`Loading target file: ${targetPath}`);
56
+ let targetModule;
57
+ try {
58
+ targetModule = await import(targetPath);
59
+ } catch (error) {
60
+ throw new Error(`Failed to import target file: ${error instanceof Error ? error.message : String(error)}`);
61
+ }
62
+ const fragnoDatabases = findFragnoDatabases(targetModule);
63
+ if (fragnoDatabases.length === 0) throw new Error(`No FragnoDatabase instances found in ${target}.\nMake sure you export either:\n - A FragnoDatabase instance created with .create(adapter)\n - An instantiated fragment with embedded database definition\n`);
64
+ if (fragnoDatabases.length > 1) console.warn(`Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`);
65
+ const fragnoDb = fragnoDatabases[0];
66
+ const targetVersion = toVersion !== void 0 ? parseInt(String(toVersion), 10) : void 0;
67
+ const sourceVersion = fromVersion !== void 0 ? parseInt(String(fromVersion), 10) : void 0;
68
+ if (targetVersion !== void 0 && isNaN(targetVersion)) throw new Error(`Invalid version number: ${toVersion}`);
69
+ if (sourceVersion !== void 0 && isNaN(sourceVersion)) throw new Error(`Invalid version number: ${fromVersion}`);
70
+ if (sourceVersion !== void 0 && targetVersion !== void 0) console.log(`Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion} to version ${targetVersion})`);
71
+ else if (targetVersion !== void 0) console.log(`Generating schema for namespace: ${fragnoDb.namespace} (to version ${targetVersion})`);
72
+ else if (sourceVersion !== void 0) console.log(`Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion})`);
73
+ else console.log(`Generating schema for namespace: ${fragnoDb.namespace}`);
74
+ let isDirectory = false;
75
+ if (output) {
76
+ const resolvedOutput = resolve(process.cwd(), output);
77
+ try {
78
+ isDirectory = (await stat(resolvedOutput)).isDirectory();
79
+ } catch {
80
+ isDirectory = output.endsWith("/");
81
+ }
82
+ }
83
+ let result;
84
+ try {
85
+ result = await fragnoDb.generateSchema({
86
+ path: isDirectory ? void 0 : output,
87
+ toVersion: targetVersion,
88
+ fromVersion: sourceVersion
89
+ });
90
+ } catch (error) {
91
+ throw new Error(`Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`);
92
+ }
93
+ let finalOutputPath;
94
+ if (isDirectory && output) finalOutputPath = join(resolve(process.cwd(), output), basename(result.path));
95
+ else if (output) finalOutputPath = resolve(process.cwd(), output);
96
+ else finalOutputPath = resolve(process.cwd(), result.path);
97
+ const parentDir = dirname(finalOutputPath);
98
+ try {
99
+ await mkdir(parentDir, { recursive: true });
100
+ } catch (error) {
101
+ throw new Error(`Failed to create directory: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ try {
104
+ const content = prefix ? `${prefix}\n${result.schema}` : result.schema;
105
+ await writeFile(finalOutputPath, content, { encoding: "utf-8" });
106
+ } catch (error) {
107
+ throw new Error(`Failed to write schema file: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ console.log(`✓ Schema generated successfully: ${finalOutputPath}`);
110
+ console.log(` Namespace: ${fragnoDb.namespace}`);
111
+ console.log(` Schema version: ${fragnoDb.schema.version}`);
112
+ }
113
+
114
+ //#endregion
115
+ //#region src/commands/db/migrate.ts
116
+ async function migrate(ctx) {
117
+ const target = ctx.values["target"];
118
+ const version = ctx.values["to"];
119
+ const fromVersion = ctx.values["from"];
120
+ if (!target || typeof target !== "string") throw new Error("Target file path is required and must be a string");
121
+ if (version !== void 0 && typeof version !== "string" && typeof version !== "number") throw new Error("Version must be a number or string");
122
+ if (fromVersion !== void 0 && typeof fromVersion !== "string" && typeof fromVersion !== "number") throw new Error("Version must be a number or string");
123
+ const targetPath = resolve(process.cwd(), target);
124
+ console.log(`Loading target file: ${targetPath}`);
125
+ let targetModule;
126
+ try {
127
+ targetModule = await import(targetPath);
128
+ } catch (error) {
129
+ throw new Error(`Failed to import target file: ${error instanceof Error ? error.message : String(error)}`);
130
+ }
131
+ const fragnoDatabases = findFragnoDatabases(targetModule);
132
+ if (fragnoDatabases.length === 0) throw new Error(`No FragnoDatabase instances found in ${target}.\nMake sure you export either:\n - A FragnoDatabase instance created with .create(adapter)\n - An instantiated fragment with embedded database definition\n`);
133
+ if (fragnoDatabases.length > 1) console.warn(`Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`);
134
+ const fragnoDb = fragnoDatabases[0];
135
+ console.log(`Migrating database for namespace: ${fragnoDb.namespace}`);
136
+ if (!fragnoDb.adapter.createMigrationEngine) throw new Error("Adapter does not support running migrations. The adapter only supports schema generation.\nTry using 'fragno db generate' instead to generate schema files.");
137
+ const targetVersion = version !== void 0 ? parseInt(String(version), 10) : void 0;
138
+ const expectedFromVersion = fromVersion !== void 0 ? parseInt(String(fromVersion), 10) : void 0;
139
+ if (targetVersion !== void 0 && isNaN(targetVersion)) throw new Error(`Invalid version number: ${version}`);
140
+ if (expectedFromVersion !== void 0 && isNaN(expectedFromVersion)) throw new Error(`Invalid version number: ${fromVersion}`);
141
+ let didMigrate;
142
+ try {
143
+ if (targetVersion !== void 0) {
144
+ console.log(`Migrating to version ${targetVersion}...`);
145
+ const migrator = fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace);
146
+ const currentVersion = await migrator.getVersion();
147
+ console.log(`Current version: ${currentVersion}`);
148
+ if (expectedFromVersion !== void 0 && currentVersion !== expectedFromVersion) throw new Error(`Current database version (${currentVersion}) does not match expected --from version (${expectedFromVersion})`);
149
+ const preparedMigration = await migrator.prepareMigrationTo(targetVersion, { updateSettings: true });
150
+ if (preparedMigration.operations.length === 0) {
151
+ console.log("✓ Database is already at the target version. No migrations needed.");
152
+ didMigrate = false;
153
+ } else {
154
+ await preparedMigration.execute();
155
+ didMigrate = true;
156
+ }
157
+ } else {
158
+ console.log(`Migrating to latest version (${fragnoDb.schema.version})...`);
159
+ didMigrate = await fragnoDb.runMigrations();
160
+ }
161
+ } catch (error) {
162
+ throw new Error(`Failed to run migrations: ${error instanceof Error ? error.message : String(error)}`);
163
+ }
164
+ if (didMigrate) {
165
+ console.log(`✓ Migration completed successfully`);
166
+ console.log(` Namespace: ${fragnoDb.namespace}`);
167
+ if (targetVersion !== void 0) console.log(` New version: ${targetVersion}`);
168
+ else console.log(` New version: ${fragnoDb.schema.version}`);
169
+ }
170
+ }
171
+
172
+ //#endregion
173
+ //#region src/commands/db/info.ts
174
+ async function info(ctx) {
175
+ const target = ctx.values["target"];
176
+ if (!target || typeof target !== "string") throw new Error("Target file path is required and must be a string");
177
+ const targetPath = resolve(process.cwd(), target);
178
+ console.log(`Loading target file: ${targetPath}`);
179
+ let targetModule;
180
+ try {
181
+ targetModule = await import(targetPath);
182
+ } catch (error) {
183
+ throw new Error(`Failed to import target file: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ const fragnoDatabases = [];
186
+ for (const [key, value] of Object.entries(targetModule)) if (isFragnoDatabase(value)) {
187
+ fragnoDatabases.push(value);
188
+ console.log(`Found FragnoDatabase instance: ${key}`);
189
+ }
190
+ if (fragnoDatabases.length === 0) throw new Error(`No FragnoDatabase instances found in ${target}.\n`);
191
+ if (fragnoDatabases.length > 1) console.warn(`Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`);
192
+ const fragnoDb = fragnoDatabases[0];
193
+ console.log("");
194
+ console.log("Database Information");
195
+ console.log("=".repeat(50));
196
+ console.log(`Namespace: ${fragnoDb.namespace}`);
197
+ console.log(`Latest Schema Version: ${fragnoDb.schema.version}`);
198
+ if (!fragnoDb.adapter.createMigrationEngine) {
199
+ console.log(`Migration Support: No`);
200
+ console.log("");
201
+ console.log("Note: This adapter does not support running migrations.");
202
+ console.log("Use 'fragno db generate' to generate schema files.");
203
+ return;
204
+ }
205
+ console.log(`Migration Support: Yes`);
206
+ try {
207
+ const currentVersion = await fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace).getVersion();
208
+ console.log(`Current Database Version: ${currentVersion}`);
209
+ const pendingVersions = fragnoDb.schema.version - currentVersion;
210
+ if (pendingVersions > 0) {
211
+ console.log("");
212
+ console.log(`⚠ Pending Migrations: ${pendingVersions} version(s) behind`);
213
+ console.log(` Run '@fragno-dev/cli db migrate --target ${target}' to update`);
214
+ } else if (pendingVersions === 0) {
215
+ console.log("");
216
+ console.log(`✓ Database is up to date`);
217
+ }
218
+ } catch (error) {
219
+ console.log("");
220
+ console.log(`Warning: Could not retrieve current version: ${error instanceof Error ? error.message : String(error)}`);
221
+ }
222
+ console.log("=".repeat(50));
223
+ }
224
+
225
+ //#endregion
226
+ //#region src/cli.ts
227
+ const generateCommand = {
228
+ name: "generate",
229
+ description: "Generate schema files from FragnoDatabase definitions",
230
+ args: {
231
+ target: {
232
+ type: "positional",
233
+ description: "Path to the file that exports a FragnoDatabase instance"
234
+ },
235
+ output: {
236
+ type: "string",
237
+ short: "o",
238
+ description: "Output path for the generated schema file (default: schema.sql for Kysely, schema.ts for Drizzle)"
239
+ },
240
+ from: {
241
+ type: "string",
242
+ short: "f",
243
+ description: "Source version to generate migration from (default: current database version)"
244
+ },
245
+ to: {
246
+ type: "string",
247
+ short: "t",
248
+ description: "Target version to generate migration to (default: latest schema version)"
249
+ },
250
+ prefix: {
251
+ type: "string",
252
+ short: "p",
253
+ description: "String to prepend to the generated file (e.g., '/* eslint-disable */')"
254
+ }
255
+ },
256
+ run: generate
257
+ };
258
+ const migrateCommand = {
259
+ name: "migrate",
260
+ description: "Run database migrations",
261
+ args: {
262
+ target: {
263
+ type: "positional",
264
+ description: "Path to the file that exports a FragnoDatabase instance"
265
+ },
266
+ from: {
267
+ type: "string",
268
+ short: "f",
269
+ description: "Expected current database version (validates before migrating)"
270
+ },
271
+ to: {
272
+ type: "string",
273
+ short: "t",
274
+ description: "Target version to migrate to (default: latest schema version)"
275
+ }
276
+ },
277
+ run: migrate
278
+ };
279
+ const infoCommand = {
280
+ name: "info",
281
+ description: "Display database information and migration status",
282
+ args: { target: {
283
+ type: "positional",
284
+ description: "Path to the file that exports a FragnoDatabase instance"
285
+ } },
286
+ run: info
287
+ };
288
+ const dbSubCommands = /* @__PURE__ */ new Map();
289
+ dbSubCommands.set("generate", generateCommand);
290
+ dbSubCommands.set("migrate", migrateCommand);
291
+ dbSubCommands.set("info", infoCommand);
292
+ function printDbHelp() {
293
+ console.log("Database management commands for Fragno");
294
+ console.log("");
295
+ console.log("Usage: @fragno-dev/cli db <command> [options]");
296
+ console.log("");
297
+ console.log("Commands:");
298
+ console.log(" generate Generate schema files from FragnoDatabase definitions");
299
+ console.log(" migrate Run database migrations");
300
+ console.log(" info Display database information and migration status");
301
+ console.log("");
302
+ console.log("Run '@fragno-dev/cli db <command> --help' for more information.");
303
+ }
304
+ const dbCommand = {
305
+ name: "db",
306
+ description: "Database management commands",
307
+ run: printDbHelp
308
+ };
309
+ const rootSubCommands = /* @__PURE__ */ new Map();
310
+ rootSubCommands.set("db", dbCommand);
311
+ const mainCommand = {
312
+ name: "@fragno-dev/cli",
313
+ description: "Fragno CLI - Tools for working with Fragno fragments",
314
+ run: () => {
315
+ console.log("Fragno CLI - Tools for working with Fragno fragments");
316
+ console.log("");
317
+ console.log("Usage: @fragno-dev/cli <command> [options]");
318
+ console.log("");
319
+ console.log("Commands:");
320
+ console.log(" db Database management commands");
321
+ console.log("");
322
+ console.log("Run '@fragno-dev/cli <command> --help' for more information.");
323
+ }
324
+ };
325
+ if (import.meta.main) try {
326
+ const args = process.argv.slice(2);
327
+ if (args[0] === "db" && args.length > 1) {
328
+ const subCommandName = args[1];
329
+ if (subCommandName === "--help" || subCommandName === "-h") {
330
+ printDbHelp();
331
+ process.exit(0);
332
+ }
333
+ const subCommand = dbSubCommands.get(subCommandName);
334
+ if (!subCommand) {
335
+ console.error(`Unknown command: ${subCommandName}`);
336
+ console.log("");
337
+ printDbHelp();
338
+ process.exit(1);
339
+ }
340
+ const subArgs = args.slice(2);
341
+ const isSubCommandHelp = subArgs.includes("--help") || subArgs.includes("-h");
342
+ let hasValidationError = false;
343
+ if (!isSubCommandHelp && subCommand.args) {
344
+ const tokens = parseArgs(subArgs);
345
+ hasValidationError = !!resolveArgs(subCommand.args, tokens).error;
346
+ }
347
+ await cli(subArgs, subCommand);
348
+ if (hasValidationError) process.exit(1);
349
+ } else if (args[0] === "db") printDbHelp();
350
+ else await cli(args, mainCommand, { subCommands: rootSubCommands });
351
+ } catch (error) {
352
+ console.error("Error:", error instanceof Error ? error.message : error);
353
+ process.exit(1);
354
+ }
355
+
356
+ //#endregion
357
+ export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand };
358
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":["fragnoDatabases: FragnoDatabase<AnySchema>[]","targetModule: Record<string, unknown>","result: { schema: string; path: string }","finalOutputPath: string","targetModule: Record<string, unknown>","didMigrate: boolean","targetModule: Record<string, unknown>","fragnoDatabases: FragnoDatabase<AnySchema>[]","generateCommand: Command","migrateCommand: Command","infoCommand: Command","dbCommand: Command","mainCommand: Command"],"sources":["../src/utils/find-fragno-databases.ts","../src/commands/db/generate.ts","../src/commands/db/migrate.ts","../src/commands/db/info.ts","../src/cli.ts"],"sourcesContent":["import { isFragnoDatabase, type DatabaseAdapter, FragnoDatabase } from \"@fragno-dev/db\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\nimport {\n instantiatedFragmentFakeSymbol,\n type FragnoInstantiatedFragment,\n} from \"@fragno-dev/core/api/fragment-instantiation\";\n\nfunction isFragnoInstantiatedFragment(\n value: unknown,\n): value is FragnoInstantiatedFragment<[], {}, {}, {}> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n instantiatedFragmentFakeSymbol in value &&\n value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol\n );\n}\n\nfunction additionalContextIsDatabaseContext(additionalContext: unknown): additionalContext is {\n databaseSchema: AnySchema;\n databaseNamespace: string;\n databaseAdapter: DatabaseAdapter;\n} {\n return (\n typeof additionalContext === \"object\" &&\n additionalContext !== null &&\n \"databaseSchema\" in additionalContext &&\n \"databaseNamespace\" in additionalContext &&\n \"databaseAdapter\" in additionalContext\n );\n}\n\n/**\n * Finds all FragnoDatabase instances in a module, including those embedded\n * in instantiated fragments.\n */\nexport function findFragnoDatabases(\n targetModule: Record<string, unknown>,\n): FragnoDatabase<AnySchema>[] {\n const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];\n\n for (const [key, value] of Object.entries(targetModule)) {\n if (isFragnoDatabase(value)) {\n fragnoDatabases.push(value);\n console.log(`Found FragnoDatabase instance: ${key}`);\n } else if (isFragnoInstantiatedFragment(value)) {\n const additionalContext = value.additionalContext;\n\n if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {\n console.warn(`Instantiated fragment ${key} has no database context`);\n continue;\n }\n\n // Extract database schema, namespace, and adapter from instantiated fragment's additionalContext\n const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;\n\n fragnoDatabases.push(\n new FragnoDatabase({\n namespace: databaseNamespace,\n schema: databaseSchema,\n adapter: databaseAdapter,\n }),\n );\n console.log(`Found database context in instantiated fragment: ${key}`);\n }\n }\n\n return fragnoDatabases;\n}\n","import { writeFile, stat, mkdir } from \"node:fs/promises\";\nimport { resolve, join, dirname, basename } from \"node:path\";\nimport type { CommandContext } from \"gunshi\";\nimport { findFragnoDatabases } from \"../../utils/find-fragno-databases.js\";\n\nexport async function generate(ctx: CommandContext) {\n const target = ctx.values[\"target\"];\n const output = ctx.values[\"output\"];\n const toVersion = ctx.values[\"to\"];\n const fromVersion = ctx.values[\"from\"];\n const prefix = ctx.values[\"prefix\"];\n\n if (!target || typeof target !== \"string\") {\n throw new Error(\"Target file path is required and must be a string\");\n }\n\n if (typeof output === \"number\" || typeof output === \"boolean\") {\n throw new Error(\"Output file path is required and must be a string\");\n }\n\n if (toVersion !== undefined && typeof toVersion !== \"string\" && typeof toVersion !== \"number\") {\n throw new Error(\"Version must be a number or string\");\n }\n\n if (\n fromVersion !== undefined &&\n typeof fromVersion !== \"string\" &&\n typeof fromVersion !== \"number\"\n ) {\n throw new Error(\"Version must be a number or string\");\n }\n\n if (prefix !== undefined && typeof prefix !== \"string\") {\n throw new Error(\"Prefix must be a string\");\n }\n\n // Resolve the target file path relative to current working directory\n const targetPath = resolve(process.cwd(), target);\n\n console.log(`Loading target file: ${targetPath}`);\n\n // Dynamically import the target file\n let targetModule: Record<string, unknown>;\n try {\n targetModule = await import(targetPath);\n } catch (error) {\n throw new Error(\n `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Find all FragnoDatabase instances or instantiated fragments with databases\n const fragnoDatabases = findFragnoDatabases(targetModule);\n\n if (fragnoDatabases.length === 0) {\n throw new Error(\n `No FragnoDatabase instances found in ${target}.\\n` +\n `Make sure you export either:\\n` +\n ` - A FragnoDatabase instance created with .create(adapter)\\n` +\n ` - An instantiated fragment with embedded database definition\\n`,\n );\n }\n\n if (fragnoDatabases.length > 1) {\n console.warn(\n `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,\n );\n }\n\n // Use the first FragnoDatabase instance\n const fragnoDb = fragnoDatabases[0];\n\n // Parse versions if provided\n const targetVersion = toVersion !== undefined ? parseInt(String(toVersion), 10) : undefined;\n const sourceVersion = fromVersion !== undefined ? parseInt(String(fromVersion), 10) : undefined;\n\n if (targetVersion !== undefined && isNaN(targetVersion)) {\n throw new Error(`Invalid version number: ${toVersion}`);\n }\n\n if (sourceVersion !== undefined && isNaN(sourceVersion)) {\n throw new Error(`Invalid version number: ${fromVersion}`);\n }\n\n if (sourceVersion !== undefined && targetVersion !== undefined) {\n console.log(\n `Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion} to version ${targetVersion})`,\n );\n } else if (targetVersion !== undefined) {\n console.log(\n `Generating schema for namespace: ${fragnoDb.namespace} (to version ${targetVersion})`,\n );\n } else if (sourceVersion !== undefined) {\n console.log(\n `Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion})`,\n );\n } else {\n console.log(`Generating schema for namespace: ${fragnoDb.namespace}`);\n }\n\n // Determine if output is a directory or file\n let isDirectory = false;\n\n if (output) {\n const resolvedOutput = resolve(process.cwd(), output);\n try {\n const stats = await stat(resolvedOutput);\n isDirectory = stats.isDirectory();\n } catch {\n // Path doesn't exist - check if it looks like a directory (ends with /)\n isDirectory = output.endsWith(\"/\");\n }\n }\n\n // Generate schema\n let result: { schema: string; path: string };\n try {\n // If output is a directory, pass undefined to get the default file name\n // Otherwise pass the output path as-is\n result = await fragnoDb.generateSchema({\n path: isDirectory ? undefined : output,\n toVersion: targetVersion,\n fromVersion: sourceVersion,\n });\n } catch (error) {\n throw new Error(\n `Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Resolve final output path\n let finalOutputPath: string;\n if (isDirectory && output) {\n // Combine directory with generated file name\n const resolvedDir = resolve(process.cwd(), output);\n finalOutputPath = join(resolvedDir, basename(result.path));\n } else if (output) {\n // Use the provided path as-is\n finalOutputPath = resolve(process.cwd(), output);\n } else {\n // Use the generated file name in current directory\n finalOutputPath = resolve(process.cwd(), result.path);\n }\n\n // Ensure parent directory exists\n const parentDir = dirname(finalOutputPath);\n try {\n await mkdir(parentDir, { recursive: true });\n } catch (error) {\n throw new Error(\n `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Write schema to file\n try {\n const content = prefix ? `${prefix}\\n${result.schema}` : result.schema;\n await writeFile(finalOutputPath, content, { encoding: \"utf-8\" });\n } catch (error) {\n throw new Error(\n `Failed to write schema file: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n console.log(`✓ Schema generated successfully: ${finalOutputPath}`);\n console.log(` Namespace: ${fragnoDb.namespace}`);\n console.log(` Schema version: ${fragnoDb.schema.version}`);\n}\n","import { resolve } from \"node:path\";\nimport type { CommandContext } from \"gunshi\";\nimport { findFragnoDatabases } from \"../../utils/find-fragno-databases\";\n\nexport async function migrate(ctx: CommandContext) {\n const target = ctx.values[\"target\"];\n const version = ctx.values[\"to\"];\n const fromVersion = ctx.values[\"from\"];\n\n if (!target || typeof target !== \"string\") {\n throw new Error(\"Target file path is required and must be a string\");\n }\n\n if (version !== undefined && typeof version !== \"string\" && typeof version !== \"number\") {\n throw new Error(\"Version must be a number or string\");\n }\n\n if (\n fromVersion !== undefined &&\n typeof fromVersion !== \"string\" &&\n typeof fromVersion !== \"number\"\n ) {\n throw new Error(\"Version must be a number or string\");\n }\n\n // Resolve the target file path relative to current working directory\n const targetPath = resolve(process.cwd(), target);\n\n console.log(`Loading target file: ${targetPath}`);\n\n // Dynamically import the target file\n let targetModule: Record<string, unknown>;\n try {\n targetModule = await import(targetPath);\n } catch (error) {\n throw new Error(\n `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Find all FragnoDatabase instances or instantiated fragments with databases\n const fragnoDatabases = findFragnoDatabases(targetModule);\n\n if (fragnoDatabases.length === 0) {\n throw new Error(\n `No FragnoDatabase instances found in ${target}.\\n` +\n `Make sure you export either:\\n` +\n ` - A FragnoDatabase instance created with .create(adapter)\\n` +\n ` - An instantiated fragment with embedded database definition\\n`,\n );\n }\n\n if (fragnoDatabases.length > 1) {\n console.warn(\n `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,\n );\n }\n\n // Use the first FragnoDatabase instance\n const fragnoDb = fragnoDatabases[0];\n\n console.log(`Migrating database for namespace: ${fragnoDb.namespace}`);\n\n // Check if the adapter supports migrations\n if (!fragnoDb.adapter.createMigrationEngine) {\n throw new Error(\n `Adapter does not support running migrations. The adapter only supports schema generation.\\n` +\n `Try using 'fragno db generate' instead to generate schema files.`,\n );\n }\n\n // Parse versions if provided\n const targetVersion = version !== undefined ? parseInt(String(version), 10) : undefined;\n const expectedFromVersion =\n fromVersion !== undefined ? parseInt(String(fromVersion), 10) : undefined;\n\n if (targetVersion !== undefined && isNaN(targetVersion)) {\n throw new Error(`Invalid version number: ${version}`);\n }\n\n if (expectedFromVersion !== undefined && isNaN(expectedFromVersion)) {\n throw new Error(`Invalid version number: ${fromVersion}`);\n }\n\n // Run migrations\n let didMigrate: boolean;\n try {\n if (targetVersion !== undefined) {\n console.log(`Migrating to version ${targetVersion}...`);\n const migrator = fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace);\n const currentVersion = await migrator.getVersion();\n console.log(`Current version: ${currentVersion}`);\n\n // Validate from version if provided\n if (expectedFromVersion !== undefined && currentVersion !== expectedFromVersion) {\n throw new Error(\n `Current database version (${currentVersion}) does not match expected --from version (${expectedFromVersion})`,\n );\n }\n\n const preparedMigration = await migrator.prepareMigrationTo(targetVersion, {\n updateSettings: true,\n });\n\n if (preparedMigration.operations.length === 0) {\n console.log(\"✓ Database is already at the target version. No migrations needed.\");\n didMigrate = false;\n } else {\n await preparedMigration.execute();\n didMigrate = true;\n }\n } else {\n console.log(`Migrating to latest version (${fragnoDb.schema.version})...`);\n didMigrate = await fragnoDb.runMigrations();\n }\n } catch (error) {\n throw new Error(\n `Failed to run migrations: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n if (didMigrate) {\n console.log(`✓ Migration completed successfully`);\n console.log(` Namespace: ${fragnoDb.namespace}`);\n if (targetVersion !== undefined) {\n console.log(` New version: ${targetVersion}`);\n } else {\n console.log(` New version: ${fragnoDb.schema.version}`);\n }\n }\n}\n","import { isFragnoDatabase, type FragnoDatabase } from \"@fragno-dev/db\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\nimport { resolve } from \"node:path\";\nimport type { CommandContext } from \"gunshi\";\n\nexport async function info(ctx: CommandContext) {\n const target = ctx.values[\"target\"];\n\n if (!target || typeof target !== \"string\") {\n throw new Error(\"Target file path is required and must be a string\");\n }\n\n // Resolve the target file path relative to current working directory\n const targetPath = resolve(process.cwd(), target);\n\n console.log(`Loading target file: ${targetPath}`);\n\n // Dynamically import the target file\n let targetModule: Record<string, unknown>;\n try {\n targetModule = await import(targetPath);\n } catch (error) {\n throw new Error(\n `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Find all FragnoDatabase instances in the exported values\n const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];\n\n for (const [key, value] of Object.entries(targetModule)) {\n if (isFragnoDatabase(value)) {\n fragnoDatabases.push(value);\n console.log(`Found FragnoDatabase instance: ${key}`);\n }\n }\n\n if (fragnoDatabases.length === 0) {\n throw new Error(`No FragnoDatabase instances found in ${target}.\\n`);\n }\n\n if (fragnoDatabases.length > 1) {\n console.warn(\n `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,\n );\n }\n\n // Use the first FragnoDatabase instance\n const fragnoDb = fragnoDatabases[0];\n\n console.log(\"\");\n console.log(\"Database Information\");\n console.log(\"=\".repeat(50));\n console.log(`Namespace: ${fragnoDb.namespace}`);\n console.log(`Latest Schema Version: ${fragnoDb.schema.version}`);\n\n // Check if the adapter supports migrations\n if (!fragnoDb.adapter.createMigrationEngine) {\n console.log(`Migration Support: No`);\n console.log(\"\");\n console.log(\"Note: This adapter does not support running migrations.\");\n console.log(\"Use 'fragno db generate' to generate schema files.\");\n return;\n }\n\n console.log(`Migration Support: Yes`);\n\n // Get current database version\n try {\n const migrator = fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace);\n const currentVersion = await migrator.getVersion();\n\n console.log(`Current Database Version: ${currentVersion}`);\n\n // Check if migrations are pending\n const pendingVersions = fragnoDb.schema.version - currentVersion;\n if (pendingVersions > 0) {\n console.log(\"\");\n console.log(`⚠ Pending Migrations: ${pendingVersions} version(s) behind`);\n console.log(` Run '@fragno-dev/cli db migrate --target ${target}' to update`);\n } else if (pendingVersions === 0) {\n console.log(\"\");\n console.log(`✓ Database is up to date`);\n }\n } catch (error) {\n console.log(\"\");\n console.log(\n `Warning: Could not retrieve current version: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n console.log(\"=\".repeat(50));\n}\n","#!/usr/bin/env node\n\nimport { cli, type Command, parseArgs, resolveArgs } from \"gunshi\";\nimport { generate } from \"./commands/db/generate.js\";\nimport { migrate } from \"./commands/db/migrate.js\";\nimport { info } from \"./commands/db/info.js\";\n\n// Define the db generate command\nexport const generateCommand: Command = {\n name: \"generate\",\n description: \"Generate schema files from FragnoDatabase definitions\",\n args: {\n target: {\n type: \"positional\" as const,\n description: \"Path to the file that exports a FragnoDatabase instance\",\n },\n output: {\n type: \"string\" as const,\n short: \"o\",\n description:\n \"Output path for the generated schema file (default: schema.sql for Kysely, schema.ts for Drizzle)\",\n },\n from: {\n type: \"string\" as const,\n short: \"f\",\n description: \"Source version to generate migration from (default: current database version)\",\n },\n to: {\n type: \"string\" as const,\n short: \"t\",\n description: \"Target version to generate migration to (default: latest schema version)\",\n },\n prefix: {\n type: \"string\" as const,\n short: \"p\",\n description: \"String to prepend to the generated file (e.g., '/* eslint-disable */')\",\n },\n },\n run: generate,\n};\n\n// Define the db migrate command\nexport const migrateCommand: Command = {\n name: \"migrate\",\n description: \"Run database migrations\",\n args: {\n target: {\n type: \"positional\" as const,\n description: \"Path to the file that exports a FragnoDatabase instance\",\n },\n from: {\n type: \"string\" as const,\n short: \"f\",\n description: \"Expected current database version (validates before migrating)\",\n },\n to: {\n type: \"string\" as const,\n short: \"t\",\n description: \"Target version to migrate to (default: latest schema version)\",\n },\n },\n run: migrate,\n};\n\n// Define the db info command\nexport const infoCommand: Command = {\n name: \"info\",\n description: \"Display database information and migration status\",\n args: {\n target: {\n type: \"positional\" as const,\n description: \"Path to the file that exports a FragnoDatabase instance\",\n },\n },\n run: info,\n};\n\n// Create a Map of db sub-commands\nconst dbSubCommands = new Map();\ndbSubCommands.set(\"generate\", generateCommand);\ndbSubCommands.set(\"migrate\", migrateCommand);\ndbSubCommands.set(\"info\", infoCommand);\n\n// Helper function to print db command help\nfunction printDbHelp() {\n console.log(\"Database management commands for Fragno\");\n console.log(\"\");\n console.log(\"Usage: @fragno-dev/cli db <command> [options]\");\n console.log(\"\");\n console.log(\"Commands:\");\n console.log(\" generate Generate schema files from FragnoDatabase definitions\");\n console.log(\" migrate Run database migrations\");\n console.log(\" info Display database information and migration status\");\n console.log(\"\");\n console.log(\"Run '@fragno-dev/cli db <command> --help' for more information.\");\n}\n\n// Define the db command\nexport const dbCommand: Command = {\n name: \"db\",\n description: \"Database management commands\",\n run: printDbHelp,\n};\n\n// Create a Map of root sub-commands\nconst rootSubCommands = new Map();\nrootSubCommands.set(\"db\", dbCommand);\n\n// Define the main command\nexport const mainCommand: Command = {\n name: \"@fragno-dev/cli\",\n description: \"Fragno CLI - Tools for working with Fragno fragments\",\n run: () => {\n console.log(\"Fragno CLI - Tools for working with Fragno fragments\");\n console.log(\"\");\n console.log(\"Usage: @fragno-dev/cli <command> [options]\");\n console.log(\"\");\n console.log(\"Commands:\");\n console.log(\" db Database management commands\");\n console.log(\"\");\n console.log(\"Run '@fragno-dev/cli <command> --help' for more information.\");\n },\n};\n\nif (import.meta.main) {\n try {\n // Parse arguments to handle nested subcommands\n const args = process.argv.slice(2);\n\n // Check if we're calling a db subcommand directly\n if (args[0] === \"db\" && args.length > 1) {\n const subCommandName = args[1];\n\n // Check if it's a help request\n if (subCommandName === \"--help\" || subCommandName === \"-h\") {\n printDbHelp();\n process.exit(0);\n }\n\n const subCommand = dbSubCommands.get(subCommandName);\n\n if (!subCommand) {\n console.error(`Unknown command: ${subCommandName}`);\n console.log(\"\");\n printDbHelp();\n process.exit(1);\n }\n\n // Run the specific subcommand with its args\n const subArgs = args.slice(2);\n const isSubCommandHelp = subArgs.includes(\"--help\") || subArgs.includes(\"-h\");\n\n // Check for validation errors before running\n let hasValidationError = false;\n if (!isSubCommandHelp && subCommand.args) {\n const tokens = parseArgs(subArgs);\n const resolved = resolveArgs(subCommand.args, tokens);\n hasValidationError = !!resolved.error;\n }\n\n // Run the command (let gunshi handle printing errors/help)\n await cli(subArgs, subCommand);\n\n // Exit with error code if there was a validation error\n if (hasValidationError) {\n process.exit(1);\n }\n } else if (args[0] === \"db\") {\n // \"db\" command with no subcommand - show db help\n printDbHelp();\n } else {\n // Run the main CLI\n await cli(args, mainCommand, {\n subCommands: rootSubCommands,\n });\n }\n } catch (error) {\n console.error(\"Error:\", error instanceof Error ? error.message : error);\n process.exit(1);\n }\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@fragno-dev/cli",
3
+ "version": "0.1.0",
4
+ "exports": {
5
+ ".": {
6
+ "bun": "./src/cli.ts",
7
+ "development": "./src/cli.ts",
8
+ "types": "./dist/cli.d.ts",
9
+ "default": "./dist/cli.js"
10
+ }
11
+ },
12
+ "type": "module",
13
+ "bin": {
14
+ "@fragno-dev/cli": "./dist/cli.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsdown",
18
+ "build:watch": "tsdown --watch",
19
+ "types:check": "tsc --noEmit"
20
+ },
21
+ "engines": {
22
+ "node": "^20.0.0 || >=22.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22",
26
+ "@fragno-private/typescript-config": "0.0.1",
27
+ "@fragno-private/vitest-config": "0.0.0"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^0.11.0",
31
+ "gunshi": "^0.26.3",
32
+ "@fragno-dev/core": "0.0.7",
33
+ "@fragno-dev/db": "0.1.0"
34
+ },
35
+ "main": "./dist/cli.js",
36
+ "module": "./dist/cli.js",
37
+ "types": "./dist/cli.d.ts"
38
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cli, type Command, parseArgs, resolveArgs } from "gunshi";
4
+ import { generate } from "./commands/db/generate.js";
5
+ import { migrate } from "./commands/db/migrate.js";
6
+ import { info } from "./commands/db/info.js";
7
+
8
+ // Define the db generate command
9
+ export const generateCommand: Command = {
10
+ name: "generate",
11
+ description: "Generate schema files from FragnoDatabase definitions",
12
+ args: {
13
+ target: {
14
+ type: "positional" as const,
15
+ description: "Path to the file that exports a FragnoDatabase instance",
16
+ },
17
+ output: {
18
+ type: "string" as const,
19
+ short: "o",
20
+ description:
21
+ "Output path for the generated schema file (default: schema.sql for Kysely, schema.ts for Drizzle)",
22
+ },
23
+ from: {
24
+ type: "string" as const,
25
+ short: "f",
26
+ description: "Source version to generate migration from (default: current database version)",
27
+ },
28
+ to: {
29
+ type: "string" as const,
30
+ short: "t",
31
+ description: "Target version to generate migration to (default: latest schema version)",
32
+ },
33
+ prefix: {
34
+ type: "string" as const,
35
+ short: "p",
36
+ description: "String to prepend to the generated file (e.g., '/* eslint-disable */')",
37
+ },
38
+ },
39
+ run: generate,
40
+ };
41
+
42
+ // Define the db migrate command
43
+ export const migrateCommand: Command = {
44
+ name: "migrate",
45
+ description: "Run database migrations",
46
+ args: {
47
+ target: {
48
+ type: "positional" as const,
49
+ description: "Path to the file that exports a FragnoDatabase instance",
50
+ },
51
+ from: {
52
+ type: "string" as const,
53
+ short: "f",
54
+ description: "Expected current database version (validates before migrating)",
55
+ },
56
+ to: {
57
+ type: "string" as const,
58
+ short: "t",
59
+ description: "Target version to migrate to (default: latest schema version)",
60
+ },
61
+ },
62
+ run: migrate,
63
+ };
64
+
65
+ // Define the db info command
66
+ export const infoCommand: Command = {
67
+ name: "info",
68
+ description: "Display database information and migration status",
69
+ args: {
70
+ target: {
71
+ type: "positional" as const,
72
+ description: "Path to the file that exports a FragnoDatabase instance",
73
+ },
74
+ },
75
+ run: info,
76
+ };
77
+
78
+ // Create a Map of db sub-commands
79
+ const dbSubCommands = new Map();
80
+ dbSubCommands.set("generate", generateCommand);
81
+ dbSubCommands.set("migrate", migrateCommand);
82
+ dbSubCommands.set("info", infoCommand);
83
+
84
+ // Helper function to print db command help
85
+ function printDbHelp() {
86
+ console.log("Database management commands for Fragno");
87
+ console.log("");
88
+ console.log("Usage: @fragno-dev/cli db <command> [options]");
89
+ console.log("");
90
+ console.log("Commands:");
91
+ console.log(" generate Generate schema files from FragnoDatabase definitions");
92
+ console.log(" migrate Run database migrations");
93
+ console.log(" info Display database information and migration status");
94
+ console.log("");
95
+ console.log("Run '@fragno-dev/cli db <command> --help' for more information.");
96
+ }
97
+
98
+ // Define the db command
99
+ export const dbCommand: Command = {
100
+ name: "db",
101
+ description: "Database management commands",
102
+ run: printDbHelp,
103
+ };
104
+
105
+ // Create a Map of root sub-commands
106
+ const rootSubCommands = new Map();
107
+ rootSubCommands.set("db", dbCommand);
108
+
109
+ // Define the main command
110
+ export const mainCommand: Command = {
111
+ name: "@fragno-dev/cli",
112
+ description: "Fragno CLI - Tools for working with Fragno fragments",
113
+ run: () => {
114
+ console.log("Fragno CLI - Tools for working with Fragno fragments");
115
+ console.log("");
116
+ console.log("Usage: @fragno-dev/cli <command> [options]");
117
+ console.log("");
118
+ console.log("Commands:");
119
+ console.log(" db Database management commands");
120
+ console.log("");
121
+ console.log("Run '@fragno-dev/cli <command> --help' for more information.");
122
+ },
123
+ };
124
+
125
+ if (import.meta.main) {
126
+ try {
127
+ // Parse arguments to handle nested subcommands
128
+ const args = process.argv.slice(2);
129
+
130
+ // Check if we're calling a db subcommand directly
131
+ if (args[0] === "db" && args.length > 1) {
132
+ const subCommandName = args[1];
133
+
134
+ // Check if it's a help request
135
+ if (subCommandName === "--help" || subCommandName === "-h") {
136
+ printDbHelp();
137
+ process.exit(0);
138
+ }
139
+
140
+ const subCommand = dbSubCommands.get(subCommandName);
141
+
142
+ if (!subCommand) {
143
+ console.error(`Unknown command: ${subCommandName}`);
144
+ console.log("");
145
+ printDbHelp();
146
+ process.exit(1);
147
+ }
148
+
149
+ // Run the specific subcommand with its args
150
+ const subArgs = args.slice(2);
151
+ const isSubCommandHelp = subArgs.includes("--help") || subArgs.includes("-h");
152
+
153
+ // Check for validation errors before running
154
+ let hasValidationError = false;
155
+ if (!isSubCommandHelp && subCommand.args) {
156
+ const tokens = parseArgs(subArgs);
157
+ const resolved = resolveArgs(subCommand.args, tokens);
158
+ hasValidationError = !!resolved.error;
159
+ }
160
+
161
+ // Run the command (let gunshi handle printing errors/help)
162
+ await cli(subArgs, subCommand);
163
+
164
+ // Exit with error code if there was a validation error
165
+ if (hasValidationError) {
166
+ process.exit(1);
167
+ }
168
+ } else if (args[0] === "db") {
169
+ // "db" command with no subcommand - show db help
170
+ printDbHelp();
171
+ } else {
172
+ // Run the main CLI
173
+ await cli(args, mainCommand, {
174
+ subCommands: rootSubCommands,
175
+ });
176
+ }
177
+ } catch (error) {
178
+ console.error("Error:", error instanceof Error ? error.message : error);
179
+ process.exit(1);
180
+ }
181
+ }
@@ -0,0 +1,168 @@
1
+ import { writeFile, stat, mkdir } from "node:fs/promises";
2
+ import { resolve, join, dirname, basename } from "node:path";
3
+ import type { CommandContext } from "gunshi";
4
+ import { findFragnoDatabases } from "../../utils/find-fragno-databases.js";
5
+
6
+ export async function generate(ctx: CommandContext) {
7
+ const target = ctx.values["target"];
8
+ const output = ctx.values["output"];
9
+ const toVersion = ctx.values["to"];
10
+ const fromVersion = ctx.values["from"];
11
+ const prefix = ctx.values["prefix"];
12
+
13
+ if (!target || typeof target !== "string") {
14
+ throw new Error("Target file path is required and must be a string");
15
+ }
16
+
17
+ if (typeof output === "number" || typeof output === "boolean") {
18
+ throw new Error("Output file path is required and must be a string");
19
+ }
20
+
21
+ if (toVersion !== undefined && typeof toVersion !== "string" && typeof toVersion !== "number") {
22
+ throw new Error("Version must be a number or string");
23
+ }
24
+
25
+ if (
26
+ fromVersion !== undefined &&
27
+ typeof fromVersion !== "string" &&
28
+ typeof fromVersion !== "number"
29
+ ) {
30
+ throw new Error("Version must be a number or string");
31
+ }
32
+
33
+ if (prefix !== undefined && typeof prefix !== "string") {
34
+ throw new Error("Prefix must be a string");
35
+ }
36
+
37
+ // Resolve the target file path relative to current working directory
38
+ const targetPath = resolve(process.cwd(), target);
39
+
40
+ console.log(`Loading target file: ${targetPath}`);
41
+
42
+ // Dynamically import the target file
43
+ let targetModule: Record<string, unknown>;
44
+ try {
45
+ targetModule = await import(targetPath);
46
+ } catch (error) {
47
+ throw new Error(
48
+ `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,
49
+ );
50
+ }
51
+
52
+ // Find all FragnoDatabase instances or instantiated fragments with databases
53
+ const fragnoDatabases = findFragnoDatabases(targetModule);
54
+
55
+ if (fragnoDatabases.length === 0) {
56
+ throw new Error(
57
+ `No FragnoDatabase instances found in ${target}.\n` +
58
+ `Make sure you export either:\n` +
59
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
60
+ ` - An instantiated fragment with embedded database definition\n`,
61
+ );
62
+ }
63
+
64
+ if (fragnoDatabases.length > 1) {
65
+ console.warn(
66
+ `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
67
+ );
68
+ }
69
+
70
+ // Use the first FragnoDatabase instance
71
+ const fragnoDb = fragnoDatabases[0];
72
+
73
+ // Parse versions if provided
74
+ const targetVersion = toVersion !== undefined ? parseInt(String(toVersion), 10) : undefined;
75
+ const sourceVersion = fromVersion !== undefined ? parseInt(String(fromVersion), 10) : undefined;
76
+
77
+ if (targetVersion !== undefined && isNaN(targetVersion)) {
78
+ throw new Error(`Invalid version number: ${toVersion}`);
79
+ }
80
+
81
+ if (sourceVersion !== undefined && isNaN(sourceVersion)) {
82
+ throw new Error(`Invalid version number: ${fromVersion}`);
83
+ }
84
+
85
+ if (sourceVersion !== undefined && targetVersion !== undefined) {
86
+ console.log(
87
+ `Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion} to version ${targetVersion})`,
88
+ );
89
+ } else if (targetVersion !== undefined) {
90
+ console.log(
91
+ `Generating schema for namespace: ${fragnoDb.namespace} (to version ${targetVersion})`,
92
+ );
93
+ } else if (sourceVersion !== undefined) {
94
+ console.log(
95
+ `Generating schema for namespace: ${fragnoDb.namespace} (from version ${sourceVersion})`,
96
+ );
97
+ } else {
98
+ console.log(`Generating schema for namespace: ${fragnoDb.namespace}`);
99
+ }
100
+
101
+ // Determine if output is a directory or file
102
+ let isDirectory = false;
103
+
104
+ if (output) {
105
+ const resolvedOutput = resolve(process.cwd(), output);
106
+ try {
107
+ const stats = await stat(resolvedOutput);
108
+ isDirectory = stats.isDirectory();
109
+ } catch {
110
+ // Path doesn't exist - check if it looks like a directory (ends with /)
111
+ isDirectory = output.endsWith("/");
112
+ }
113
+ }
114
+
115
+ // Generate schema
116
+ let result: { schema: string; path: string };
117
+ try {
118
+ // If output is a directory, pass undefined to get the default file name
119
+ // Otherwise pass the output path as-is
120
+ result = await fragnoDb.generateSchema({
121
+ path: isDirectory ? undefined : output,
122
+ toVersion: targetVersion,
123
+ fromVersion: sourceVersion,
124
+ });
125
+ } catch (error) {
126
+ throw new Error(
127
+ `Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`,
128
+ );
129
+ }
130
+
131
+ // Resolve final output path
132
+ let finalOutputPath: string;
133
+ if (isDirectory && output) {
134
+ // Combine directory with generated file name
135
+ const resolvedDir = resolve(process.cwd(), output);
136
+ finalOutputPath = join(resolvedDir, basename(result.path));
137
+ } else if (output) {
138
+ // Use the provided path as-is
139
+ finalOutputPath = resolve(process.cwd(), output);
140
+ } else {
141
+ // Use the generated file name in current directory
142
+ finalOutputPath = resolve(process.cwd(), result.path);
143
+ }
144
+
145
+ // Ensure parent directory exists
146
+ const parentDir = dirname(finalOutputPath);
147
+ try {
148
+ await mkdir(parentDir, { recursive: true });
149
+ } catch (error) {
150
+ throw new Error(
151
+ `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`,
152
+ );
153
+ }
154
+
155
+ // Write schema to file
156
+ try {
157
+ const content = prefix ? `${prefix}\n${result.schema}` : result.schema;
158
+ await writeFile(finalOutputPath, content, { encoding: "utf-8" });
159
+ } catch (error) {
160
+ throw new Error(
161
+ `Failed to write schema file: ${error instanceof Error ? error.message : String(error)}`,
162
+ );
163
+ }
164
+
165
+ console.log(`✓ Schema generated successfully: ${finalOutputPath}`);
166
+ console.log(` Namespace: ${fragnoDb.namespace}`);
167
+ console.log(` Schema version: ${fragnoDb.schema.version}`);
168
+ }
@@ -0,0 +1,93 @@
1
+ import { isFragnoDatabase, type FragnoDatabase } from "@fragno-dev/db";
2
+ import type { AnySchema } from "@fragno-dev/db/schema";
3
+ import { resolve } from "node:path";
4
+ import type { CommandContext } from "gunshi";
5
+
6
+ export async function info(ctx: CommandContext) {
7
+ const target = ctx.values["target"];
8
+
9
+ if (!target || typeof target !== "string") {
10
+ throw new Error("Target file path is required and must be a string");
11
+ }
12
+
13
+ // Resolve the target file path relative to current working directory
14
+ const targetPath = resolve(process.cwd(), target);
15
+
16
+ console.log(`Loading target file: ${targetPath}`);
17
+
18
+ // Dynamically import the target file
19
+ let targetModule: Record<string, unknown>;
20
+ try {
21
+ targetModule = await import(targetPath);
22
+ } catch (error) {
23
+ throw new Error(
24
+ `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,
25
+ );
26
+ }
27
+
28
+ // Find all FragnoDatabase instances in the exported values
29
+ const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];
30
+
31
+ for (const [key, value] of Object.entries(targetModule)) {
32
+ if (isFragnoDatabase(value)) {
33
+ fragnoDatabases.push(value);
34
+ console.log(`Found FragnoDatabase instance: ${key}`);
35
+ }
36
+ }
37
+
38
+ if (fragnoDatabases.length === 0) {
39
+ throw new Error(`No FragnoDatabase instances found in ${target}.\n`);
40
+ }
41
+
42
+ if (fragnoDatabases.length > 1) {
43
+ console.warn(
44
+ `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
45
+ );
46
+ }
47
+
48
+ // Use the first FragnoDatabase instance
49
+ const fragnoDb = fragnoDatabases[0];
50
+
51
+ console.log("");
52
+ console.log("Database Information");
53
+ console.log("=".repeat(50));
54
+ console.log(`Namespace: ${fragnoDb.namespace}`);
55
+ console.log(`Latest Schema Version: ${fragnoDb.schema.version}`);
56
+
57
+ // Check if the adapter supports migrations
58
+ if (!fragnoDb.adapter.createMigrationEngine) {
59
+ console.log(`Migration Support: No`);
60
+ console.log("");
61
+ console.log("Note: This adapter does not support running migrations.");
62
+ console.log("Use 'fragno db generate' to generate schema files.");
63
+ return;
64
+ }
65
+
66
+ console.log(`Migration Support: Yes`);
67
+
68
+ // Get current database version
69
+ try {
70
+ const migrator = fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace);
71
+ const currentVersion = await migrator.getVersion();
72
+
73
+ console.log(`Current Database Version: ${currentVersion}`);
74
+
75
+ // Check if migrations are pending
76
+ const pendingVersions = fragnoDb.schema.version - currentVersion;
77
+ if (pendingVersions > 0) {
78
+ console.log("");
79
+ console.log(`⚠ Pending Migrations: ${pendingVersions} version(s) behind`);
80
+ console.log(` Run '@fragno-dev/cli db migrate --target ${target}' to update`);
81
+ } else if (pendingVersions === 0) {
82
+ console.log("");
83
+ console.log(`✓ Database is up to date`);
84
+ }
85
+ } catch (error) {
86
+ console.log("");
87
+ console.log(
88
+ `Warning: Could not retrieve current version: ${error instanceof Error ? error.message : String(error)}`,
89
+ );
90
+ }
91
+
92
+ console.log("=".repeat(50));
93
+ }
@@ -0,0 +1,131 @@
1
+ import { resolve } from "node:path";
2
+ import type { CommandContext } from "gunshi";
3
+ import { findFragnoDatabases } from "../../utils/find-fragno-databases";
4
+
5
+ export async function migrate(ctx: CommandContext) {
6
+ const target = ctx.values["target"];
7
+ const version = ctx.values["to"];
8
+ const fromVersion = ctx.values["from"];
9
+
10
+ if (!target || typeof target !== "string") {
11
+ throw new Error("Target file path is required and must be a string");
12
+ }
13
+
14
+ if (version !== undefined && typeof version !== "string" && typeof version !== "number") {
15
+ throw new Error("Version must be a number or string");
16
+ }
17
+
18
+ if (
19
+ fromVersion !== undefined &&
20
+ typeof fromVersion !== "string" &&
21
+ typeof fromVersion !== "number"
22
+ ) {
23
+ throw new Error("Version must be a number or string");
24
+ }
25
+
26
+ // Resolve the target file path relative to current working directory
27
+ const targetPath = resolve(process.cwd(), target);
28
+
29
+ console.log(`Loading target file: ${targetPath}`);
30
+
31
+ // Dynamically import the target file
32
+ let targetModule: Record<string, unknown>;
33
+ try {
34
+ targetModule = await import(targetPath);
35
+ } catch (error) {
36
+ throw new Error(
37
+ `Failed to import target file: ${error instanceof Error ? error.message : String(error)}`,
38
+ );
39
+ }
40
+
41
+ // Find all FragnoDatabase instances or instantiated fragments with databases
42
+ const fragnoDatabases = findFragnoDatabases(targetModule);
43
+
44
+ if (fragnoDatabases.length === 0) {
45
+ throw new Error(
46
+ `No FragnoDatabase instances found in ${target}.\n` +
47
+ `Make sure you export either:\n` +
48
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
49
+ ` - An instantiated fragment with embedded database definition\n`,
50
+ );
51
+ }
52
+
53
+ if (fragnoDatabases.length > 1) {
54
+ console.warn(
55
+ `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
56
+ );
57
+ }
58
+
59
+ // Use the first FragnoDatabase instance
60
+ const fragnoDb = fragnoDatabases[0];
61
+
62
+ console.log(`Migrating database for namespace: ${fragnoDb.namespace}`);
63
+
64
+ // Check if the adapter supports migrations
65
+ if (!fragnoDb.adapter.createMigrationEngine) {
66
+ throw new Error(
67
+ `Adapter does not support running migrations. The adapter only supports schema generation.\n` +
68
+ `Try using 'fragno db generate' instead to generate schema files.`,
69
+ );
70
+ }
71
+
72
+ // Parse versions if provided
73
+ const targetVersion = version !== undefined ? parseInt(String(version), 10) : undefined;
74
+ const expectedFromVersion =
75
+ fromVersion !== undefined ? parseInt(String(fromVersion), 10) : undefined;
76
+
77
+ if (targetVersion !== undefined && isNaN(targetVersion)) {
78
+ throw new Error(`Invalid version number: ${version}`);
79
+ }
80
+
81
+ if (expectedFromVersion !== undefined && isNaN(expectedFromVersion)) {
82
+ throw new Error(`Invalid version number: ${fromVersion}`);
83
+ }
84
+
85
+ // Run migrations
86
+ let didMigrate: boolean;
87
+ try {
88
+ if (targetVersion !== undefined) {
89
+ console.log(`Migrating to version ${targetVersion}...`);
90
+ const migrator = fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace);
91
+ const currentVersion = await migrator.getVersion();
92
+ console.log(`Current version: ${currentVersion}`);
93
+
94
+ // Validate from version if provided
95
+ if (expectedFromVersion !== undefined && currentVersion !== expectedFromVersion) {
96
+ throw new Error(
97
+ `Current database version (${currentVersion}) does not match expected --from version (${expectedFromVersion})`,
98
+ );
99
+ }
100
+
101
+ const preparedMigration = await migrator.prepareMigrationTo(targetVersion, {
102
+ updateSettings: true,
103
+ });
104
+
105
+ if (preparedMigration.operations.length === 0) {
106
+ console.log("✓ Database is already at the target version. No migrations needed.");
107
+ didMigrate = false;
108
+ } else {
109
+ await preparedMigration.execute();
110
+ didMigrate = true;
111
+ }
112
+ } else {
113
+ console.log(`Migrating to latest version (${fragnoDb.schema.version})...`);
114
+ didMigrate = await fragnoDb.runMigrations();
115
+ }
116
+ } catch (error) {
117
+ throw new Error(
118
+ `Failed to run migrations: ${error instanceof Error ? error.message : String(error)}`,
119
+ );
120
+ }
121
+
122
+ if (didMigrate) {
123
+ console.log(`✓ Migration completed successfully`);
124
+ console.log(` Namespace: ${fragnoDb.namespace}`);
125
+ if (targetVersion !== undefined) {
126
+ console.log(` New version: ${targetVersion}`);
127
+ } else {
128
+ console.log(` New version: ${fragnoDb.schema.version}`);
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,69 @@
1
+ import { isFragnoDatabase, type DatabaseAdapter, FragnoDatabase } from "@fragno-dev/db";
2
+ import type { AnySchema } from "@fragno-dev/db/schema";
3
+ import {
4
+ instantiatedFragmentFakeSymbol,
5
+ type FragnoInstantiatedFragment,
6
+ } from "@fragno-dev/core/api/fragment-instantiation";
7
+
8
+ function isFragnoInstantiatedFragment(
9
+ value: unknown,
10
+ ): value is FragnoInstantiatedFragment<[], {}, {}, {}> {
11
+ return (
12
+ typeof value === "object" &&
13
+ value !== null &&
14
+ instantiatedFragmentFakeSymbol in value &&
15
+ value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol
16
+ );
17
+ }
18
+
19
+ function additionalContextIsDatabaseContext(additionalContext: unknown): additionalContext is {
20
+ databaseSchema: AnySchema;
21
+ databaseNamespace: string;
22
+ databaseAdapter: DatabaseAdapter;
23
+ } {
24
+ return (
25
+ typeof additionalContext === "object" &&
26
+ additionalContext !== null &&
27
+ "databaseSchema" in additionalContext &&
28
+ "databaseNamespace" in additionalContext &&
29
+ "databaseAdapter" in additionalContext
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Finds all FragnoDatabase instances in a module, including those embedded
35
+ * in instantiated fragments.
36
+ */
37
+ export function findFragnoDatabases(
38
+ targetModule: Record<string, unknown>,
39
+ ): FragnoDatabase<AnySchema>[] {
40
+ const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];
41
+
42
+ for (const [key, value] of Object.entries(targetModule)) {
43
+ if (isFragnoDatabase(value)) {
44
+ fragnoDatabases.push(value);
45
+ console.log(`Found FragnoDatabase instance: ${key}`);
46
+ } else if (isFragnoInstantiatedFragment(value)) {
47
+ const additionalContext = value.additionalContext;
48
+
49
+ if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {
50
+ console.warn(`Instantiated fragment ${key} has no database context`);
51
+ continue;
52
+ }
53
+
54
+ // Extract database schema, namespace, and adapter from instantiated fragment's additionalContext
55
+ const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;
56
+
57
+ fragnoDatabases.push(
58
+ new FragnoDatabase({
59
+ namespace: databaseNamespace,
60
+ schema: databaseSchema,
61
+ adapter: databaseAdapter,
62
+ }),
63
+ );
64
+ console.log(`Found database context in instantiated fragment: ${key}`);
65
+ }
66
+ }
67
+
68
+ return fragnoDatabases;
69
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@fragno-private/typescript-config/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": ".",
6
+ "composite": true
7
+ },
8
+ "include": ["src"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "./src/cli.ts",
5
+ dts: true,
6
+ });
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { baseConfig } from "@fragno-private/vitest-config";
3
+
4
+ export default defineConfig(baseConfig);