@fragno-dev/cli 0.0.0-canary-18943940612-1-20251030142344

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,18 @@
1
+
2
+ > @fragno-dev/cli@0.1.13 build /home/runner/work/fragno/fragno/apps/fragno-cli
3
+ > tsdown
4
+
5
+ ℹ tsdown v0.15.12 powered by rolldown v1.0.0-beta.45
6
+ ℹ Using tsdown config: /home/runner/work/fragno/fragno/apps/fragno-cli/tsdown.config.ts
7
+ ℹ entry: src/cli.ts
8
+ ℹ target: node22.0.0
9
+ ℹ tsconfig: tsconfig.json
10
+ ℹ Build start
11
+ ℹ Granting execute permission to dist/cli.d.ts
12
+ ℹ Granting execute permission to dist/cli.js
13
+ ℹ dist/cli.js 15.22 kB │ gzip: 3.96 kB
14
+ ℹ dist/cli.js.map 30.08 kB │ gzip: 7.78 kB
15
+ ℹ dist/cli.d.ts.map  0.51 kB │ gzip: 0.29 kB
16
+ ℹ dist/cli.d.ts  0.94 kB │ gzip: 0.30 kB
17
+ ℹ 4 files, total: 46.75 kB
18
+ ✔ Build complete in 2653ms
package/CHANGELOG.md ADDED
@@ -0,0 +1,142 @@
1
+ # @fragno-dev/cli
2
+
3
+ ## 0.0.0-canary-18943940612-1-20251030142344
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [2900bfa]
8
+ - Updated dependencies [fdb5aaf]
9
+ - @fragno-dev/db@0.0.0-canary-18943940612-1-20251030142344
10
+
11
+ ## 0.1.13
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [b6dd67a]
16
+ - Updated dependencies [ec1aed0]
17
+ - Updated dependencies [9a58d8c]
18
+ - @fragno-dev/core@0.1.5
19
+ - @fragno-dev/db@0.1.11
20
+
21
+ ## 0.1.12
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [ca57fac]
26
+ - @fragno-dev/core@0.1.4
27
+ - @fragno-dev/db@0.1.10
28
+
29
+ ## 0.1.11
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [ad3e63b]
34
+ - @fragno-dev/db@0.1.10
35
+
36
+ ## 0.1.10
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies [8fcceb6]
41
+ - @fragno-dev/db@0.1.9
42
+
43
+ ## 0.1.9
44
+
45
+ ### Patch Changes
46
+
47
+ - Updated dependencies [f3cdb1d]
48
+ - @fragno-dev/db@0.1.8
49
+
50
+ ## 0.1.8
51
+
52
+ ### Patch Changes
53
+
54
+ - ab6c4bf: fix: make Fragment loading in the CLI more robust
55
+ - Updated dependencies [e36dbcd]
56
+ - Updated dependencies [ab6c4bf]
57
+ - Updated dependencies [d1feecd]
58
+ - @fragno-dev/db@0.1.7
59
+
60
+ ## 0.1.7
61
+
62
+ ### Patch Changes
63
+
64
+ - d330bb9: fix: change `bin` to `fragno-cli`
65
+ - Updated dependencies [70bdcb2]
66
+ - @fragno-dev/db@0.1.6
67
+
68
+ ## 0.1.6
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies [8b2859c]
73
+ - Updated dependencies [bef9f6c]
74
+ - Updated dependencies [711226d]
75
+ - @fragno-dev/db@0.1.5
76
+ - @fragno-dev/core@0.1.3
77
+
78
+ ## 0.1.5
79
+
80
+ ### Patch Changes
81
+
82
+ - Updated dependencies [5d56f48]
83
+ - Updated dependencies [fd3ddd2]
84
+ - @fragno-dev/db@0.1.4
85
+
86
+ ## 0.1.4
87
+
88
+ ### Patch Changes
89
+
90
+ - Updated dependencies [0723f84]
91
+ - @fragno-dev/db@0.1.3
92
+
93
+ ## 0.1.3
94
+
95
+ ### Patch Changes
96
+
97
+ - a8b1f81: `db generate` command now supports targeting multiple Fragment files
98
+ - a8b1f81: `db info` command now supports targeting multiple files
99
+ - be17727: Added support for generating migrations in multi-Fragment applications
100
+ - Updated dependencies [e7122f2]
101
+ - Updated dependencies [921ef11]
102
+ - Updated dependencies [be17727]
103
+ - Updated dependencies [8362d9a]
104
+ - Updated dependencies [8362d9a]
105
+ - Updated dependencies [c70de59]
106
+ - @fragno-dev/db@0.1.2
107
+ - @fragno-dev/core@0.1.2
108
+
109
+ ## 0.1.2
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies [4c1c806]
114
+ - @fragno-dev/db@0.1.1
115
+ - @fragno-dev/core@0.1.1
116
+
117
+ ## 0.1.1
118
+
119
+ ### Patch Changes
120
+
121
+ - Updated dependencies [74a615c]
122
+ - @fragno-dev/core@0.1.0
123
+ - @fragno-dev/db@0.1.0
124
+
125
+ ## 0.1.0
126
+
127
+ ### Minor Changes
128
+
129
+ - 2c583a9: Initial release of @fragno-dev/db
130
+
131
+ ### Patch Changes
132
+
133
+ - Updated dependencies [2c583a9]
134
+ - @fragno-dev/db@0.1.0
135
+
136
+ ## 0.0.2
137
+
138
+ ### Patch Changes
139
+
140
+ - Updated dependencies [1fa71f3]
141
+ - Updated dependencies [c1483c6]
142
+ - @fragno-dev/core@0.0.7
package/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ Copyright 2025 - present "ReJot Nederland B.V.", and individual contributors.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the “Software”), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import * as gunshi5 from "gunshi";
3
+
4
+ //#region src/commands/db/generate.d.ts
5
+ declare const generateCommand: gunshi5.Command<{
6
+ output: {
7
+ type: "string";
8
+ short: string;
9
+ description: string;
10
+ };
11
+ from: {
12
+ type: "number";
13
+ short: string;
14
+ description: string;
15
+ };
16
+ to: {
17
+ type: "number";
18
+ short: string;
19
+ description: string;
20
+ };
21
+ prefix: {
22
+ type: "string";
23
+ short: string;
24
+ description: string;
25
+ };
26
+ }>;
27
+ //#endregion
28
+ //#region src/commands/db/migrate.d.ts
29
+ declare const migrateCommand: gunshi5.Command<{}>;
30
+ //#endregion
31
+ //#region src/commands/db/info.d.ts
32
+ declare const infoCommand: gunshi5.Command<{}>;
33
+ //#endregion
34
+ //#region src/cli.d.ts
35
+ declare const dbCommand: gunshi5.Command<gunshi5.Args>;
36
+ declare const mainCommand: gunshi5.Command<gunshi5.Args>;
37
+ //#endregion
38
+ export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand };
39
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/commands/db/generate.ts","../src/commands/db/migrate.ts","../src/commands/db/info.ts","../src/cli.ts"],"sourcesContent":[],"mappings":";;;;cAOa,iBAyGX,OAAA,CAzG0B;;;;IAAf,WAAA,EAAA,MAyGX;;;;IC3GW,KAAA,EAAA,MAqEX;;;;ICtEW,IAAA,EAAA,QA4HX;;;;ECpGW,MAAA,EAAA;IAWA,IAAA,EAAA,QAaX;;;;;;;cF/CW,gBAqEX,OAAA,CArEyB;;;cCDd,aA4HX,OAAA,CA5HsB;;;AFGX,cGqBA,SHoFX,EGpFoB,OAAA,CAAA,OHrBM,CGyB1B,OAAA,CAJoB,IAAA,CHrBM;cGgCf,aAAW,OAAA,CAAA,QAatB,OAAA,CAbsB,IAAA"}
package/dist/cli.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ import { cli, define, parseArgs, resolveArgs } from "gunshi";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { dirname, relative, resolve } from "node:path";
5
+ import { executeMigrations, generateMigrationsOrSchema } 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/api/fragment-instantiation";
9
+ import { loadConfig } from "c12";
10
+
11
+ //#region src/utils/find-fragno-databases.ts
12
+ async function importFragmentFile(path) {
13
+ const { config } = await loadConfig({ configFile: path });
14
+ const databases = findFragnoDatabases(config);
15
+ const adapterNames = databases.map((db) => `${db.adapter[fragnoDatabaseAdapterNameFakeSymbol]}@${db.adapter[fragnoDatabaseAdapterVersionFakeSymbol]}`);
16
+ if ([...new Set(adapterNames)].length > 1) throw new Error(`All Fragno databases must use the same adapter name and version. Found mismatch: (${adapterNames.join(", ")})`);
17
+ return {
18
+ adapter: databases[0].adapter,
19
+ databases
20
+ };
21
+ }
22
+ /**
23
+ * Imports multiple fragment files and validates they all use the same adapter.
24
+ * Returns the combined databases from all files.
25
+ */
26
+ async function importFragmentFiles(paths) {
27
+ const uniquePaths = Array.from(new Set(paths));
28
+ if (uniquePaths.length === 0) throw new Error("No fragment files provided");
29
+ const allDatabases = [];
30
+ let adapter;
31
+ let firstAdapterFile;
32
+ const cwd = process.cwd();
33
+ for (const path of uniquePaths) {
34
+ const relativePath = relative(cwd, path);
35
+ try {
36
+ const result = await importFragmentFile(path);
37
+ const databases = result["databases"];
38
+ const fileAdapter = result["adapter"];
39
+ if (databases.length === 0) {
40
+ 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`);
41
+ continue;
42
+ }
43
+ if (!adapter) {
44
+ adapter = fileAdapter;
45
+ firstAdapterFile = relativePath;
46
+ }
47
+ const firstAdapterName = adapter[fragnoDatabaseAdapterNameFakeSymbol];
48
+ const firstAdapterVersion = adapter[fragnoDatabaseAdapterVersionFakeSymbol];
49
+ const fileAdapterName = fileAdapter[fragnoDatabaseAdapterNameFakeSymbol];
50
+ const fileAdapterVersion = fileAdapter[fragnoDatabaseAdapterVersionFakeSymbol];
51
+ if (firstAdapterName !== fileAdapterName || firstAdapterVersion !== fileAdapterVersion) {
52
+ const firstAdapterInfo = `${firstAdapterName}@${firstAdapterVersion}`;
53
+ const fileAdapterInfo = `${fileAdapterName}@${fileAdapterVersion}`;
54
+ 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.`);
55
+ }
56
+ allDatabases.push(...databases);
57
+ console.log(` Found ${databases.length} database(s) in ${relativePath}`);
58
+ } catch (error) {
59
+ throw new Error(`Failed to import fragment file ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ }
62
+ 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");
63
+ if (!adapter) throw new Error("No adapter found in any of the fragment files");
64
+ return {
65
+ adapter,
66
+ databases: allDatabases
67
+ };
68
+ }
69
+ function isFragnoInstantiatedFragment(value) {
70
+ return typeof value === "object" && value !== null && instantiatedFragmentFakeSymbol in value && value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol;
71
+ }
72
+ function additionalContextIsDatabaseContext(additionalContext) {
73
+ return typeof additionalContext === "object" && additionalContext !== null && "databaseSchema" in additionalContext && "databaseNamespace" in additionalContext && "databaseAdapter" in additionalContext;
74
+ }
75
+ /**
76
+ * Finds all FragnoDatabase instances in a module, including those embedded
77
+ * in instantiated fragments.
78
+ */
79
+ function findFragnoDatabases(targetModule) {
80
+ const fragnoDatabases = [];
81
+ for (const [_key, value] of Object.entries(targetModule)) if (isFragnoDatabase(value)) fragnoDatabases.push(value);
82
+ else if (isFragnoInstantiatedFragment(value)) {
83
+ const additionalContext = value.additionalContext;
84
+ if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) continue;
85
+ const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;
86
+ fragnoDatabases.push(new FragnoDatabase({
87
+ namespace: databaseNamespace,
88
+ schema: databaseSchema,
89
+ adapter: databaseAdapter
90
+ }));
91
+ }
92
+ return fragnoDatabases;
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/commands/db/generate.ts
97
+ const generateCommand = define({
98
+ name: "generate",
99
+ description: "Generate schema files from FragnoDatabase definitions",
100
+ args: {
101
+ output: {
102
+ type: "string",
103
+ short: "o",
104
+ description: "Output path: for single file, exact file path; for multiple files, output directory (default: current directory)"
105
+ },
106
+ from: {
107
+ type: "number",
108
+ short: "f",
109
+ description: "Source version to generate migration from (default: current database version)"
110
+ },
111
+ to: {
112
+ type: "number",
113
+ short: "t",
114
+ description: "Target version to generate migration to (default: latest schema version)"
115
+ },
116
+ prefix: {
117
+ type: "string",
118
+ short: "p",
119
+ description: "String to prepend to the generated file (e.g., '/* eslint-disable */')"
120
+ }
121
+ },
122
+ run: async (ctx) => {
123
+ const targets = ctx.positionals;
124
+ const output = ctx.values.output;
125
+ const toVersion = ctx.values.to;
126
+ const fromVersion = ctx.values.from;
127
+ const prefix = ctx.values.prefix;
128
+ const { databases: allFragnoDatabases, adapter } = await importFragmentFiles(targets.map((target) => resolve(process.cwd(), target)));
129
+ if (!adapter.createSchemaGenerator && !adapter.createMigrationEngine) throw new Error("The adapter does not support schema generation. Please use an adapter that implements either createSchemaGenerator or createMigrationEngine.");
130
+ console.log("Generating schema...");
131
+ let results;
132
+ try {
133
+ results = await generateMigrationsOrSchema(allFragnoDatabases, {
134
+ path: output,
135
+ toVersion,
136
+ fromVersion
137
+ });
138
+ } catch (error) {
139
+ throw new Error(`Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`);
140
+ }
141
+ for (const result of results) {
142
+ const finalOutputPath = output && results.length === 1 ? resolve(process.cwd(), output) : output ? resolve(process.cwd(), output, result.path) : resolve(process.cwd(), result.path);
143
+ const parentDir = dirname(finalOutputPath);
144
+ try {
145
+ await mkdir(parentDir, { recursive: true });
146
+ } catch (error) {
147
+ throw new Error(`Failed to create directory: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
149
+ try {
150
+ await writeFile(finalOutputPath, prefix ? `${prefix}\n${result.schema}` : result.schema, { encoding: "utf-8" });
151
+ } catch (error) {
152
+ throw new Error(`Failed to write schema file: ${error instanceof Error ? error.message : String(error)}`);
153
+ }
154
+ console.log(`✓ Generated: ${finalOutputPath}`);
155
+ }
156
+ console.log(`\n✓ Schema generated successfully!`);
157
+ console.log(` Files generated: ${results.length}`);
158
+ console.log(` Fragments:`);
159
+ for (const db of allFragnoDatabases) console.log(` - ${db.namespace} (version ${db.schema.version})`);
160
+ }
161
+ });
162
+
163
+ //#endregion
164
+ //#region src/commands/db/migrate.ts
165
+ const migrateCommand = define({
166
+ name: "migrate",
167
+ description: "Run database migrations for all fragments to their latest versions",
168
+ args: {},
169
+ run: async (ctx) => {
170
+ const targets = ctx.positionals;
171
+ if (targets.length === 0) throw new Error("At least one target file path is required");
172
+ const { databases: allFragnoDatabases } = await importFragmentFiles(targets.map((target) => resolve(process.cwd(), target)));
173
+ console.log("\nMigrating all fragments to their latest versions...\n");
174
+ let results;
175
+ try {
176
+ results = await executeMigrations(allFragnoDatabases);
177
+ } catch (error) {
178
+ throw new Error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ for (const result of results) {
181
+ console.log(`Fragment: ${result.namespace}`);
182
+ console.log(` Current version: ${result.fromVersion}`);
183
+ console.log(` Target version: ${result.toVersion}`);
184
+ if (result.didMigrate) console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\n`);
185
+ else console.log(` ✓ Already at latest version. No migration needed.\n`);
186
+ }
187
+ console.log("═══════════════════════════════════════");
188
+ console.log("Migration Summary");
189
+ console.log("═══════════════════════════════════════");
190
+ const migrated = results.filter((r) => r.didMigrate);
191
+ const skipped = results.filter((r) => !r.didMigrate);
192
+ if (migrated.length > 0) {
193
+ console.log(`\n✓ Migrated ${migrated.length} fragment(s):`);
194
+ for (const r of migrated) console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);
195
+ }
196
+ if (skipped.length > 0) {
197
+ console.log(`\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);
198
+ for (const r of skipped) console.log(` - ${r.namespace}: v${r.toVersion}`);
199
+ }
200
+ for (const db of allFragnoDatabases) await db.adapter.close();
201
+ console.log("\n✓ All migrations completed successfully");
202
+ }
203
+ });
204
+
205
+ //#endregion
206
+ //#region src/commands/db/info.ts
207
+ const infoCommand = define({
208
+ name: "info",
209
+ description: "Display database information and migration status",
210
+ args: {},
211
+ run: async (ctx) => {
212
+ const targets = ctx.positionals;
213
+ if (targets.length === 0) throw new Error("At least one target file path is required");
214
+ const { databases: allFragnoDatabases } = await importFragmentFiles(targets.map((target) => resolve(process.cwd(), target)));
215
+ const dbInfos = await Promise.all(allFragnoDatabases.map(async (fragnoDb) => {
216
+ const info = {
217
+ namespace: fragnoDb.namespace,
218
+ schemaVersion: fragnoDb.schema.version,
219
+ migrationSupport: !!fragnoDb.adapter.createMigrationEngine
220
+ };
221
+ if (fragnoDb.adapter.createMigrationEngine) try {
222
+ const currentVersion = await fragnoDb.adapter.createMigrationEngine(fragnoDb.schema, fragnoDb.namespace).getVersion();
223
+ info.currentVersion = currentVersion;
224
+ info.pendingVersions = fragnoDb.schema.version - currentVersion;
225
+ if (info.pendingVersions > 0) info.status = `Pending (${info.pendingVersions} migration(s))`;
226
+ else if (info.pendingVersions === 0) info.status = "Up to date";
227
+ } catch (error) {
228
+ info.error = error instanceof Error ? error.message : String(error);
229
+ info.status = "Error";
230
+ }
231
+ else info.status = "Schema only";
232
+ return info;
233
+ }));
234
+ const hasMigrationSupport = dbInfos.some((info) => info.migrationSupport);
235
+ console.log("");
236
+ console.log(`Database Information:`);
237
+ console.log("");
238
+ const namespaceHeader = "Namespace";
239
+ const versionHeader = "Schema";
240
+ const currentHeader = "Current";
241
+ const statusHeader = "Status";
242
+ const maxNamespaceLen = Math.max(9, ...dbInfos.map((info) => info.namespace.length));
243
+ const namespaceWidth = Math.max(maxNamespaceLen + 2, 20);
244
+ const versionWidth = 8;
245
+ const currentWidth = 9;
246
+ const statusWidth = 25;
247
+ console.log(namespaceHeader.padEnd(namespaceWidth) + versionHeader.padEnd(versionWidth) + (hasMigrationSupport ? currentHeader.padEnd(currentWidth) : "") + statusHeader);
248
+ console.log("-".repeat(namespaceWidth) + "-".repeat(versionWidth) + (hasMigrationSupport ? "-".repeat(currentWidth) : "") + "-".repeat(statusWidth));
249
+ for (const info of dbInfos) {
250
+ const currentVersionStr = info.currentVersion !== void 0 ? String(info.currentVersion) : "-";
251
+ console.log(info.namespace.padEnd(namespaceWidth) + String(info.schemaVersion).padEnd(versionWidth) + (hasMigrationSupport ? currentVersionStr.padEnd(currentWidth) : "") + (info.status || "-"));
252
+ }
253
+ console.log("");
254
+ if (!hasMigrationSupport) {
255
+ console.log("Note: These adapters do not support migrations.");
256
+ console.log("Use 'fragno-cli db generate' to generate schema files.");
257
+ } else if (dbInfos.some((info) => info.pendingVersions && info.pendingVersions > 0)) console.log("Run 'fragno-cli db migrate <target>' to apply pending migrations.");
258
+ }
259
+ });
260
+
261
+ //#endregion
262
+ //#region src/cli.ts
263
+ const dbSubCommands = /* @__PURE__ */ new Map();
264
+ dbSubCommands.set("generate", generateCommand);
265
+ dbSubCommands.set("migrate", migrateCommand);
266
+ dbSubCommands.set("info", infoCommand);
267
+ function printDbHelp() {
268
+ console.log("Database management commands for Fragno");
269
+ console.log("");
270
+ console.log("Usage: fragno-cli db <command> [options]");
271
+ console.log("");
272
+ console.log("Commands:");
273
+ console.log(" generate Generate schema files from FragnoDatabase definitions");
274
+ console.log(" migrate Run database migrations");
275
+ console.log(" info Display database information and migration status");
276
+ console.log("");
277
+ console.log("Run 'fragno-cli db <command> --help' for more information.");
278
+ }
279
+ const dbCommand = define({
280
+ name: "db",
281
+ description: "Database management commands",
282
+ run: printDbHelp
283
+ });
284
+ const rootSubCommands = /* @__PURE__ */ new Map();
285
+ rootSubCommands.set("db", dbCommand);
286
+ const mainCommand = define({
287
+ name: "fragno-cli",
288
+ description: "Fragno CLI - Tools for working with Fragno fragments",
289
+ run: () => {
290
+ console.log("Fragno CLI - Tools for working with Fragno fragments");
291
+ console.log("");
292
+ console.log("Usage: fragno-cli <command> [options]");
293
+ console.log("");
294
+ console.log("Commands:");
295
+ console.log(" db Database management commands");
296
+ console.log("");
297
+ console.log("Run 'fragno-cli <command> --help' for more information.");
298
+ }
299
+ });
300
+ if (import.meta.main) try {
301
+ const args = process.argv.slice(2);
302
+ if (args[0] === "db" && args.length > 1) {
303
+ const subCommandName = args[1];
304
+ if (subCommandName === "--help" || subCommandName === "-h") {
305
+ printDbHelp();
306
+ process.exit(0);
307
+ }
308
+ const subCommand = dbSubCommands.get(subCommandName);
309
+ if (!subCommand) {
310
+ console.error(`Unknown command: ${subCommandName}`);
311
+ console.log("");
312
+ printDbHelp();
313
+ process.exit(1);
314
+ }
315
+ const subArgs = args.slice(2);
316
+ const isSubCommandHelp = subArgs.includes("--help") || subArgs.includes("-h");
317
+ let hasValidationError = false;
318
+ if (!isSubCommandHelp && subCommand.args) {
319
+ const tokens = parseArgs(subArgs);
320
+ hasValidationError = !!resolveArgs(subCommand.args, tokens).error;
321
+ }
322
+ await cli(subArgs, subCommand);
323
+ if (hasValidationError) process.exit(1);
324
+ } else if (args[0] === "db") printDbHelp();
325
+ else await cli(args, mainCommand, { subCommands: rootSubCommands });
326
+ } catch (error) {
327
+ console.error("Error:", error instanceof Error ? error.message : error);
328
+ process.exit(1);
329
+ }
330
+
331
+ //#endregion
332
+ export { dbCommand, generateCommand, infoCommand, mainCommand, migrateCommand };
333
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":["allDatabases: FragnoDatabase<AnySchema>[]","adapter: DatabaseAdapter | undefined","firstAdapterFile: string | undefined","fragnoDatabases: FragnoDatabase<AnySchema>[]","results: { schema: string; path: string; namespace: string }[]","results: ExecuteMigrationResult[]","info: {\n namespace: string;\n schemaVersion: number;\n migrationSupport: boolean;\n currentVersion?: number;\n pendingVersions?: number;\n status?: string;\n error?: string;\n }"],"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 {\n fragnoDatabaseAdapterNameFakeSymbol,\n fragnoDatabaseAdapterVersionFakeSymbol,\n} from \"@fragno-dev/db/adapters\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\nimport {\n instantiatedFragmentFakeSymbol,\n type FragnoInstantiatedFragment,\n} from \"@fragno-dev/core/api/fragment-instantiation\";\nimport { loadConfig } from \"c12\";\nimport { relative } from \"node:path\";\n\nexport async function importFragmentFile(path: string): Promise<Record<string, unknown>> {\n const { config } = await loadConfig({\n configFile: path,\n });\n\n const databases = findFragnoDatabases(config);\n const adapterNames = databases.map(\n (db) =>\n `${db.adapter[fragnoDatabaseAdapterNameFakeSymbol]}@${db.adapter[fragnoDatabaseAdapterVersionFakeSymbol]}`,\n );\n const uniqueAdapterNames = [...new Set(adapterNames)];\n\n if (uniqueAdapterNames.length > 1) {\n throw new Error(\n `All Fragno databases must use the same adapter name and version. ` +\n `Found mismatch: (${adapterNames.join(\", \")})`,\n );\n }\n\n return {\n adapter: databases[0].adapter,\n databases,\n };\n}\n\n/**\n * Imports multiple fragment files and validates they all use the same adapter.\n * Returns the combined databases from all files.\n */\nexport async function importFragmentFiles(paths: string[]): Promise<{\n adapter: DatabaseAdapter;\n databases: FragnoDatabase<AnySchema>[];\n}> {\n // De-duplicate paths (in case same file was specified multiple times)\n const uniquePaths = Array.from(new Set(paths));\n\n if (uniquePaths.length === 0) {\n throw new Error(\"No fragment files provided\");\n }\n\n const allDatabases: FragnoDatabase<AnySchema>[] = [];\n let adapter: DatabaseAdapter | undefined;\n let firstAdapterFile: string | undefined;\n const cwd = process.cwd();\n\n for (const path of uniquePaths) {\n const relativePath = relative(cwd, path);\n\n try {\n const result = await importFragmentFile(path);\n const databases = result[\"databases\"] as FragnoDatabase<AnySchema>[];\n const fileAdapter = result[\"adapter\"] as DatabaseAdapter;\n\n if (databases.length === 0) {\n console.warn(\n `Warning: No FragnoDatabase instances found in ${relativePath}.\\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 continue;\n }\n\n // Set the adapter from the first file with databases\n if (!adapter) {\n adapter = fileAdapter;\n firstAdapterFile = relativePath;\n }\n\n // Validate all files use the same adapter name and version\n const firstAdapterName = adapter[fragnoDatabaseAdapterNameFakeSymbol];\n const firstAdapterVersion = adapter[fragnoDatabaseAdapterVersionFakeSymbol];\n const fileAdapterName = fileAdapter[fragnoDatabaseAdapterNameFakeSymbol];\n const fileAdapterVersion = fileAdapter[fragnoDatabaseAdapterVersionFakeSymbol];\n\n if (firstAdapterName !== fileAdapterName || firstAdapterVersion !== fileAdapterVersion) {\n const firstAdapterInfo = `${firstAdapterName}@${firstAdapterVersion}`;\n const fileAdapterInfo = `${fileAdapterName}@${fileAdapterVersion}`;\n\n throw new Error(\n `All fragments must use the same database adapter. Mixed adapters found:\\n` +\n ` - ${firstAdapterFile}: ${firstAdapterInfo}\\n` +\n ` - ${relativePath}: ${fileAdapterInfo}\\n\\n` +\n `Make sure all fragments use the same adapter name and version.`,\n );\n }\n\n allDatabases.push(...databases);\n console.log(` Found ${databases.length} database(s) in ${relativePath}`);\n } catch (error) {\n throw new Error(\n `Failed to import fragment file ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n if (allDatabases.length === 0) {\n throw new Error(\n `No FragnoDatabase instances found in any of the target files.\\n` +\n `Make sure your files 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 (!adapter) {\n throw new Error(\"No adapter found in any of the fragment files\");\n }\n\n return {\n adapter,\n databases: allDatabases,\n };\n}\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 } else if (isFragnoInstantiatedFragment(value)) {\n const additionalContext = value.additionalContext;\n\n if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {\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 }\n }\n\n return fragnoDatabases;\n}\n","import { writeFile, mkdir } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { define } from \"gunshi\";\nimport { generateMigrationsOrSchema } from \"@fragno-dev/db/generation-engine\";\nimport { importFragmentFiles } from \"../../utils/find-fragno-databases\";\n\n// Define the db generate command with type safety\nexport const generateCommand = define({\n name: \"generate\",\n description: \"Generate schema files from FragnoDatabase definitions\",\n args: {\n output: {\n type: \"string\",\n short: \"o\",\n description:\n \"Output path: for single file, exact file path; for multiple files, output directory (default: current directory)\",\n },\n from: {\n type: \"number\",\n short: \"f\",\n description: \"Source version to generate migration from (default: current database version)\",\n },\n to: {\n type: \"number\",\n short: \"t\",\n description: \"Target version to generate migration to (default: latest schema version)\",\n },\n prefix: {\n type: \"string\",\n short: \"p\",\n description: \"String to prepend to the generated file (e.g., '/* eslint-disable */')\",\n },\n },\n run: async (ctx) => {\n // With `define()` and `multiple: true`, targets is properly typed as string[]\n const targets = ctx.positionals;\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 // Resolve all target paths\n const targetPaths = targets.map((target) => resolve(process.cwd(), target));\n\n // Import all fragment files and validate they use the same adapter\n const { databases: allFragnoDatabases, adapter } = await importFragmentFiles(targetPaths);\n\n // Check if adapter supports any form of schema generation\n if (!adapter.createSchemaGenerator && !adapter.createMigrationEngine) {\n throw new Error(\n `The adapter does not support schema generation. ` +\n `Please use an adapter that implements either createSchemaGenerator or createMigrationEngine.`,\n );\n }\n\n // Generate schema for all fragments\n console.log(\"Generating schema...\");\n\n let results: { schema: string; path: string; namespace: string }[];\n try {\n results = await generateMigrationsOrSchema(allFragnoDatabases, {\n path: output,\n toVersion,\n fromVersion,\n });\n } catch (error) {\n throw new Error(\n `Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Write all generated files\n for (const result of results) {\n // For single file: use output as exact file path\n // For multiple files: use output as base directory\n const finalOutputPath =\n output && results.length === 1\n ? resolve(process.cwd(), output)\n : output\n ? resolve(process.cwd(), output, result.path)\n : resolve(process.cwd(), result.path);\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(`✓ Generated: ${finalOutputPath}`);\n }\n\n console.log(`\\n✓ Schema generated successfully!`);\n console.log(` Files generated: ${results.length}`);\n console.log(` Fragments:`);\n for (const db of allFragnoDatabases) {\n console.log(` - ${db.namespace} (version ${db.schema.version})`);\n }\n },\n});\n","import { resolve } from \"node:path\";\nimport { define } from \"gunshi\";\nimport { importFragmentFiles } from \"../../utils/find-fragno-databases\";\nimport { executeMigrations, type ExecuteMigrationResult } from \"@fragno-dev/db/generation-engine\";\n\nexport const migrateCommand = define({\n name: \"migrate\",\n description: \"Run database migrations for all fragments to their latest versions\",\n args: {},\n run: async (ctx) => {\n const targets = ctx.positionals;\n\n if (targets.length === 0) {\n throw new Error(\"At least one target file path is required\");\n }\n\n // Resolve all target paths\n const targetPaths = targets.map((target) => resolve(process.cwd(), target));\n\n // Import all fragment files and validate they use the same adapter\n const { databases: allFragnoDatabases } = await importFragmentFiles(targetPaths);\n\n console.log(\"\\nMigrating all fragments to their latest versions...\\n\");\n\n let results: ExecuteMigrationResult[];\n try {\n results = await executeMigrations(allFragnoDatabases);\n } catch (error) {\n throw new Error(\n `Migration failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Display progress for each result\n for (const result of results) {\n console.log(`Fragment: ${result.namespace}`);\n console.log(` Current version: ${result.fromVersion}`);\n console.log(` Target version: ${result.toVersion}`);\n\n if (result.didMigrate) {\n console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\\n`);\n } else {\n console.log(` ✓ Already at latest version. No migration needed.\\n`);\n }\n }\n\n // Summary\n console.log(\"═══════════════════════════════════════\");\n console.log(\"Migration Summary\");\n console.log(\"═══════════════════════════════════════\");\n\n const migrated = results.filter((r) => r.didMigrate);\n const skipped = results.filter((r) => !r.didMigrate);\n\n if (migrated.length > 0) {\n console.log(`\\n✓ Migrated ${migrated.length} fragment(s):`);\n for (const r of migrated) {\n console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);\n }\n }\n\n if (skipped.length > 0) {\n console.log(`\\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);\n for (const r of skipped) {\n console.log(` - ${r.namespace}: v${r.toVersion}`);\n }\n }\n\n for (const db of allFragnoDatabases) {\n await db.adapter.close();\n }\n\n console.log(\"\\n✓ All migrations completed successfully\");\n },\n});\n","import { resolve } from \"node:path\";\nimport { define } from \"gunshi\";\nimport { importFragmentFiles } from \"../../utils/find-fragno-databases\";\n\nexport const infoCommand = define({\n name: \"info\",\n description: \"Display database information and migration status\",\n args: {},\n run: async (ctx) => {\n const targets = ctx.positionals;\n\n if (targets.length === 0) {\n throw new Error(\"At least one target file path is required\");\n }\n\n // Resolve all target paths\n const targetPaths = targets.map((target) => resolve(process.cwd(), target));\n\n // Import all fragment files\n const { databases: allFragnoDatabases } = await importFragmentFiles(targetPaths);\n\n // Collect database information\n const dbInfos = await Promise.all(\n allFragnoDatabases.map(async (fragnoDb) => {\n const info: {\n namespace: string;\n schemaVersion: number;\n migrationSupport: boolean;\n currentVersion?: number;\n pendingVersions?: number;\n status?: string;\n error?: string;\n } = {\n namespace: fragnoDb.namespace,\n schemaVersion: fragnoDb.schema.version,\n migrationSupport: !!fragnoDb.adapter.createMigrationEngine,\n };\n\n // Get current database version if migrations are supported\n if (fragnoDb.adapter.createMigrationEngine) {\n try {\n const migrator = fragnoDb.adapter.createMigrationEngine(\n fragnoDb.schema,\n fragnoDb.namespace,\n );\n const currentVersion = await migrator.getVersion();\n info.currentVersion = currentVersion;\n info.pendingVersions = fragnoDb.schema.version - currentVersion;\n\n if (info.pendingVersions > 0) {\n info.status = `Pending (${info.pendingVersions} migration(s))`;\n } else if (info.pendingVersions === 0) {\n info.status = \"Up to date\";\n }\n } catch (error) {\n info.error = error instanceof Error ? error.message : String(error);\n info.status = \"Error\";\n }\n } else {\n info.status = \"Schema only\";\n }\n\n return info;\n }),\n );\n\n // Determine if any database supports migrations\n const hasMigrationSupport = dbInfos.some((info) => info.migrationSupport);\n\n // Print compact table\n console.log(\"\");\n console.log(`Database Information:`);\n console.log(\"\");\n\n // Table header\n const namespaceHeader = \"Namespace\";\n const versionHeader = \"Schema\";\n const currentHeader = \"Current\";\n const statusHeader = \"Status\";\n\n const maxNamespaceLen = Math.max(\n namespaceHeader.length,\n ...dbInfos.map((info) => info.namespace.length),\n );\n const namespaceWidth = Math.max(maxNamespaceLen + 2, 20);\n const versionWidth = 8;\n const currentWidth = 9;\n const statusWidth = 25;\n\n // Print table\n console.log(\n namespaceHeader.padEnd(namespaceWidth) +\n versionHeader.padEnd(versionWidth) +\n (hasMigrationSupport ? currentHeader.padEnd(currentWidth) : \"\") +\n statusHeader,\n );\n console.log(\n \"-\".repeat(namespaceWidth) +\n \"-\".repeat(versionWidth) +\n (hasMigrationSupport ? \"-\".repeat(currentWidth) : \"\") +\n \"-\".repeat(statusWidth),\n );\n\n for (const info of dbInfos) {\n const currentVersionStr =\n info.currentVersion !== undefined ? String(info.currentVersion) : \"-\";\n console.log(\n info.namespace.padEnd(namespaceWidth) +\n String(info.schemaVersion).padEnd(versionWidth) +\n (hasMigrationSupport ? currentVersionStr.padEnd(currentWidth) : \"\") +\n (info.status || \"-\"),\n );\n }\n\n // Print help text\n console.log(\"\");\n if (!hasMigrationSupport) {\n console.log(\"Note: These adapters do not support migrations.\");\n console.log(\"Use 'fragno-cli db generate' to generate schema files.\");\n } else {\n const hasPendingMigrations = dbInfos.some(\n (info) => info.pendingVersions && info.pendingVersions > 0,\n );\n if (hasPendingMigrations) {\n console.log(\"Run 'fragno-cli db migrate <target>' to apply pending migrations.\");\n }\n }\n },\n});\n","#!/usr/bin/env node\n\nimport { cli, define, parseArgs, resolveArgs } from \"gunshi\";\nimport { generateCommand } from \"./commands/db/generate.js\";\nimport { migrateCommand } from \"./commands/db/migrate.js\";\nimport { infoCommand } from \"./commands/db/info.js\";\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-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-cli db <command> --help' for more information.\");\n}\n\n// Define the db command with type safety\nexport const dbCommand = define({\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 with type safety\nexport const mainCommand = define({\n name: \"fragno-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-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-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\nexport { generateCommand, migrateCommand, infoCommand };\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@fragno-dev/cli",
3
+ "version": "0.0.0-canary-18943940612-1-20251030142344",
4
+ "exports": {
5
+ ".": {
6
+ "development": "./src/cli.ts",
7
+ "types": "./dist/cli.d.ts",
8
+ "default": "./dist/cli.js"
9
+ }
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "fragno-cli": "./dist/cli.js"
14
+ },
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22",
20
+ "@fragno-private/typescript-config": "0.0.1",
21
+ "@fragno-private/vitest-config": "0.0.0"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^0.11.0",
25
+ "c12": "^3.3.1",
26
+ "gunshi": "^0.26.3",
27
+ "@fragno-dev/core": "0.1.5",
28
+ "@fragno-dev/db": "0.0.0-canary-18943940612-1-20251030142344"
29
+ },
30
+ "main": "./dist/cli.js",
31
+ "module": "./dist/cli.js",
32
+ "types": "./dist/cli.d.ts",
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "build:watch": "tsdown --watch",
36
+ "types:check": "tsc --noEmit"
37
+ }
38
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cli, define, parseArgs, resolveArgs } from "gunshi";
4
+ import { generateCommand } from "./commands/db/generate.js";
5
+ import { migrateCommand } from "./commands/db/migrate.js";
6
+ import { infoCommand } from "./commands/db/info.js";
7
+
8
+ // Create a Map of db sub-commands
9
+ const dbSubCommands = new Map();
10
+ dbSubCommands.set("generate", generateCommand);
11
+ dbSubCommands.set("migrate", migrateCommand);
12
+ dbSubCommands.set("info", infoCommand);
13
+
14
+ // Helper function to print db command help
15
+ function printDbHelp() {
16
+ console.log("Database management commands for Fragno");
17
+ console.log("");
18
+ console.log("Usage: fragno-cli db <command> [options]");
19
+ console.log("");
20
+ console.log("Commands:");
21
+ console.log(" generate Generate schema files from FragnoDatabase definitions");
22
+ console.log(" migrate Run database migrations");
23
+ console.log(" info Display database information and migration status");
24
+ console.log("");
25
+ console.log("Run 'fragno-cli db <command> --help' for more information.");
26
+ }
27
+
28
+ // Define the db command with type safety
29
+ export const dbCommand = define({
30
+ name: "db",
31
+ description: "Database management commands",
32
+ run: printDbHelp,
33
+ });
34
+
35
+ // Create a Map of root sub-commands
36
+ const rootSubCommands = new Map();
37
+ rootSubCommands.set("db", dbCommand);
38
+
39
+ // Define the main command with type safety
40
+ export const mainCommand = define({
41
+ name: "fragno-cli",
42
+ description: "Fragno CLI - Tools for working with Fragno fragments",
43
+ run: () => {
44
+ console.log("Fragno CLI - Tools for working with Fragno fragments");
45
+ console.log("");
46
+ console.log("Usage: fragno-cli <command> [options]");
47
+ console.log("");
48
+ console.log("Commands:");
49
+ console.log(" db Database management commands");
50
+ console.log("");
51
+ console.log("Run 'fragno-cli <command> --help' for more information.");
52
+ },
53
+ });
54
+
55
+ if (import.meta.main) {
56
+ try {
57
+ // Parse arguments to handle nested subcommands
58
+ const args = process.argv.slice(2);
59
+
60
+ // Check if we're calling a db subcommand directly
61
+ if (args[0] === "db" && args.length > 1) {
62
+ const subCommandName = args[1];
63
+
64
+ // Check if it's a help request
65
+ if (subCommandName === "--help" || subCommandName === "-h") {
66
+ printDbHelp();
67
+ process.exit(0);
68
+ }
69
+
70
+ const subCommand = dbSubCommands.get(subCommandName);
71
+
72
+ if (!subCommand) {
73
+ console.error(`Unknown command: ${subCommandName}`);
74
+ console.log("");
75
+ printDbHelp();
76
+ process.exit(1);
77
+ }
78
+
79
+ // Run the specific subcommand with its args
80
+ const subArgs = args.slice(2);
81
+ const isSubCommandHelp = subArgs.includes("--help") || subArgs.includes("-h");
82
+
83
+ // Check for validation errors before running
84
+ let hasValidationError = false;
85
+ if (!isSubCommandHelp && subCommand.args) {
86
+ const tokens = parseArgs(subArgs);
87
+ const resolved = resolveArgs(subCommand.args, tokens);
88
+ hasValidationError = !!resolved.error;
89
+ }
90
+
91
+ // Run the command (let gunshi handle printing errors/help)
92
+ await cli(subArgs, subCommand);
93
+
94
+ // Exit with error code if there was a validation error
95
+ if (hasValidationError) {
96
+ process.exit(1);
97
+ }
98
+ } else if (args[0] === "db") {
99
+ // "db" command with no subcommand - show db help
100
+ printDbHelp();
101
+ } else {
102
+ // Run the main CLI
103
+ await cli(args, mainCommand, {
104
+ subCommands: rootSubCommands,
105
+ });
106
+ }
107
+ } catch (error) {
108
+ console.error("Error:", error instanceof Error ? error.message : error);
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ export { generateCommand, migrateCommand, infoCommand };
@@ -0,0 +1,113 @@
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import { define } from "gunshi";
4
+ import { generateMigrationsOrSchema } from "@fragno-dev/db/generation-engine";
5
+ import { importFragmentFiles } from "../../utils/find-fragno-databases";
6
+
7
+ // Define the db generate command with type safety
8
+ export const generateCommand = define({
9
+ name: "generate",
10
+ description: "Generate schema files from FragnoDatabase definitions",
11
+ args: {
12
+ output: {
13
+ type: "string",
14
+ short: "o",
15
+ description:
16
+ "Output path: for single file, exact file path; for multiple files, output directory (default: current directory)",
17
+ },
18
+ from: {
19
+ type: "number",
20
+ short: "f",
21
+ description: "Source version to generate migration from (default: current database version)",
22
+ },
23
+ to: {
24
+ type: "number",
25
+ short: "t",
26
+ description: "Target version to generate migration to (default: latest schema version)",
27
+ },
28
+ prefix: {
29
+ type: "string",
30
+ short: "p",
31
+ description: "String to prepend to the generated file (e.g., '/* eslint-disable */')",
32
+ },
33
+ },
34
+ run: async (ctx) => {
35
+ // With `define()` and `multiple: true`, targets is properly typed as string[]
36
+ const targets = ctx.positionals;
37
+ const output = ctx.values.output;
38
+ const toVersion = ctx.values.to;
39
+ const fromVersion = ctx.values.from;
40
+ const prefix = ctx.values.prefix;
41
+
42
+ // Resolve all target paths
43
+ const targetPaths = targets.map((target) => resolve(process.cwd(), target));
44
+
45
+ // Import all fragment files and validate they use the same adapter
46
+ const { databases: allFragnoDatabases, adapter } = await importFragmentFiles(targetPaths);
47
+
48
+ // Check if adapter supports any form of schema generation
49
+ if (!adapter.createSchemaGenerator && !adapter.createMigrationEngine) {
50
+ throw new Error(
51
+ `The adapter does not support schema generation. ` +
52
+ `Please use an adapter that implements either createSchemaGenerator or createMigrationEngine.`,
53
+ );
54
+ }
55
+
56
+ // Generate schema for all fragments
57
+ console.log("Generating schema...");
58
+
59
+ let results: { schema: string; path: string; namespace: string }[];
60
+ try {
61
+ results = await generateMigrationsOrSchema(allFragnoDatabases, {
62
+ path: output,
63
+ toVersion,
64
+ fromVersion,
65
+ });
66
+ } catch (error) {
67
+ throw new Error(
68
+ `Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`,
69
+ );
70
+ }
71
+
72
+ // Write all generated files
73
+ for (const result of results) {
74
+ // For single file: use output as exact file path
75
+ // For multiple files: use output as base directory
76
+ const finalOutputPath =
77
+ output && results.length === 1
78
+ ? resolve(process.cwd(), output)
79
+ : output
80
+ ? resolve(process.cwd(), output, result.path)
81
+ : resolve(process.cwd(), result.path);
82
+
83
+ // Ensure parent directory exists
84
+ const parentDir = dirname(finalOutputPath);
85
+ try {
86
+ await mkdir(parentDir, { recursive: true });
87
+ } catch (error) {
88
+ throw new Error(
89
+ `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`,
90
+ );
91
+ }
92
+
93
+ // Write schema to file
94
+ try {
95
+ const content = prefix ? `${prefix}\n${result.schema}` : result.schema;
96
+ await writeFile(finalOutputPath, content, { encoding: "utf-8" });
97
+ } catch (error) {
98
+ throw new Error(
99
+ `Failed to write schema file: ${error instanceof Error ? error.message : String(error)}`,
100
+ );
101
+ }
102
+
103
+ console.log(`✓ Generated: ${finalOutputPath}`);
104
+ }
105
+
106
+ console.log(`\n✓ Schema generated successfully!`);
107
+ console.log(` Files generated: ${results.length}`);
108
+ console.log(` Fragments:`);
109
+ for (const db of allFragnoDatabases) {
110
+ console.log(` - ${db.namespace} (version ${db.schema.version})`);
111
+ }
112
+ },
113
+ });
@@ -0,0 +1,129 @@
1
+ import { resolve } from "node:path";
2
+ import { define } from "gunshi";
3
+ import { importFragmentFiles } from "../../utils/find-fragno-databases";
4
+
5
+ export const infoCommand = define({
6
+ name: "info",
7
+ description: "Display database information and migration status",
8
+ args: {},
9
+ run: async (ctx) => {
10
+ const targets = ctx.positionals;
11
+
12
+ if (targets.length === 0) {
13
+ throw new Error("At least one target file path is required");
14
+ }
15
+
16
+ // Resolve all target paths
17
+ const targetPaths = targets.map((target) => resolve(process.cwd(), target));
18
+
19
+ // Import all fragment files
20
+ const { databases: allFragnoDatabases } = await importFragmentFiles(targetPaths);
21
+
22
+ // Collect database information
23
+ const dbInfos = await Promise.all(
24
+ allFragnoDatabases.map(async (fragnoDb) => {
25
+ const info: {
26
+ namespace: string;
27
+ schemaVersion: number;
28
+ migrationSupport: boolean;
29
+ currentVersion?: number;
30
+ pendingVersions?: number;
31
+ status?: string;
32
+ error?: string;
33
+ } = {
34
+ namespace: fragnoDb.namespace,
35
+ schemaVersion: fragnoDb.schema.version,
36
+ migrationSupport: !!fragnoDb.adapter.createMigrationEngine,
37
+ };
38
+
39
+ // Get current database version if migrations are supported
40
+ if (fragnoDb.adapter.createMigrationEngine) {
41
+ try {
42
+ const migrator = fragnoDb.adapter.createMigrationEngine(
43
+ fragnoDb.schema,
44
+ fragnoDb.namespace,
45
+ );
46
+ const currentVersion = await migrator.getVersion();
47
+ info.currentVersion = currentVersion;
48
+ info.pendingVersions = fragnoDb.schema.version - currentVersion;
49
+
50
+ if (info.pendingVersions > 0) {
51
+ info.status = `Pending (${info.pendingVersions} migration(s))`;
52
+ } else if (info.pendingVersions === 0) {
53
+ info.status = "Up to date";
54
+ }
55
+ } catch (error) {
56
+ info.error = error instanceof Error ? error.message : String(error);
57
+ info.status = "Error";
58
+ }
59
+ } else {
60
+ info.status = "Schema only";
61
+ }
62
+
63
+ return info;
64
+ }),
65
+ );
66
+
67
+ // Determine if any database supports migrations
68
+ const hasMigrationSupport = dbInfos.some((info) => info.migrationSupport);
69
+
70
+ // Print compact table
71
+ console.log("");
72
+ console.log(`Database Information:`);
73
+ console.log("");
74
+
75
+ // Table header
76
+ const namespaceHeader = "Namespace";
77
+ const versionHeader = "Schema";
78
+ const currentHeader = "Current";
79
+ const statusHeader = "Status";
80
+
81
+ const maxNamespaceLen = Math.max(
82
+ namespaceHeader.length,
83
+ ...dbInfos.map((info) => info.namespace.length),
84
+ );
85
+ const namespaceWidth = Math.max(maxNamespaceLen + 2, 20);
86
+ const versionWidth = 8;
87
+ const currentWidth = 9;
88
+ const statusWidth = 25;
89
+
90
+ // Print table
91
+ console.log(
92
+ namespaceHeader.padEnd(namespaceWidth) +
93
+ versionHeader.padEnd(versionWidth) +
94
+ (hasMigrationSupport ? currentHeader.padEnd(currentWidth) : "") +
95
+ statusHeader,
96
+ );
97
+ console.log(
98
+ "-".repeat(namespaceWidth) +
99
+ "-".repeat(versionWidth) +
100
+ (hasMigrationSupport ? "-".repeat(currentWidth) : "") +
101
+ "-".repeat(statusWidth),
102
+ );
103
+
104
+ for (const info of dbInfos) {
105
+ const currentVersionStr =
106
+ info.currentVersion !== undefined ? String(info.currentVersion) : "-";
107
+ console.log(
108
+ info.namespace.padEnd(namespaceWidth) +
109
+ String(info.schemaVersion).padEnd(versionWidth) +
110
+ (hasMigrationSupport ? currentVersionStr.padEnd(currentWidth) : "") +
111
+ (info.status || "-"),
112
+ );
113
+ }
114
+
115
+ // Print help text
116
+ console.log("");
117
+ if (!hasMigrationSupport) {
118
+ console.log("Note: These adapters do not support migrations.");
119
+ console.log("Use 'fragno-cli db generate' to generate schema files.");
120
+ } else {
121
+ const hasPendingMigrations = dbInfos.some(
122
+ (info) => info.pendingVersions && info.pendingVersions > 0,
123
+ );
124
+ if (hasPendingMigrations) {
125
+ console.log("Run 'fragno-cli db migrate <target>' to apply pending migrations.");
126
+ }
127
+ }
128
+ },
129
+ });
@@ -0,0 +1,75 @@
1
+ import { resolve } from "node:path";
2
+ import { define } from "gunshi";
3
+ import { importFragmentFiles } from "../../utils/find-fragno-databases";
4
+ import { executeMigrations, type ExecuteMigrationResult } from "@fragno-dev/db/generation-engine";
5
+
6
+ export const migrateCommand = define({
7
+ name: "migrate",
8
+ description: "Run database migrations for all fragments to their latest versions",
9
+ args: {},
10
+ run: async (ctx) => {
11
+ const targets = ctx.positionals;
12
+
13
+ if (targets.length === 0) {
14
+ throw new Error("At least one target file path is required");
15
+ }
16
+
17
+ // Resolve all target paths
18
+ const targetPaths = targets.map((target) => resolve(process.cwd(), target));
19
+
20
+ // Import all fragment files and validate they use the same adapter
21
+ const { databases: allFragnoDatabases } = await importFragmentFiles(targetPaths);
22
+
23
+ console.log("\nMigrating all fragments to their latest versions...\n");
24
+
25
+ let results: ExecuteMigrationResult[];
26
+ try {
27
+ results = await executeMigrations(allFragnoDatabases);
28
+ } catch (error) {
29
+ throw new Error(
30
+ `Migration failed: ${error instanceof Error ? error.message : String(error)}`,
31
+ );
32
+ }
33
+
34
+ // Display progress for each result
35
+ for (const result of results) {
36
+ console.log(`Fragment: ${result.namespace}`);
37
+ console.log(` Current version: ${result.fromVersion}`);
38
+ console.log(` Target version: ${result.toVersion}`);
39
+
40
+ if (result.didMigrate) {
41
+ console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\n`);
42
+ } else {
43
+ console.log(` ✓ Already at latest version. No migration needed.\n`);
44
+ }
45
+ }
46
+
47
+ // Summary
48
+ console.log("═══════════════════════════════════════");
49
+ console.log("Migration Summary");
50
+ console.log("═══════════════════════════════════════");
51
+
52
+ const migrated = results.filter((r) => r.didMigrate);
53
+ const skipped = results.filter((r) => !r.didMigrate);
54
+
55
+ if (migrated.length > 0) {
56
+ console.log(`\n✓ Migrated ${migrated.length} fragment(s):`);
57
+ for (const r of migrated) {
58
+ console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);
59
+ }
60
+ }
61
+
62
+ if (skipped.length > 0) {
63
+ console.log(`\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);
64
+ for (const r of skipped) {
65
+ console.log(` - ${r.namespace}: v${r.toVersion}`);
66
+ }
67
+ }
68
+
69
+ for (const db of allFragnoDatabases) {
70
+ await db.adapter.close();
71
+ }
72
+
73
+ console.log("\n✓ All migrations completed successfully");
74
+ },
75
+ });
@@ -0,0 +1,187 @@
1
+ import { isFragnoDatabase, type DatabaseAdapter, FragnoDatabase } from "@fragno-dev/db";
2
+ import {
3
+ fragnoDatabaseAdapterNameFakeSymbol,
4
+ fragnoDatabaseAdapterVersionFakeSymbol,
5
+ } from "@fragno-dev/db/adapters";
6
+ import type { AnySchema } from "@fragno-dev/db/schema";
7
+ import {
8
+ instantiatedFragmentFakeSymbol,
9
+ type FragnoInstantiatedFragment,
10
+ } from "@fragno-dev/core/api/fragment-instantiation";
11
+ import { loadConfig } from "c12";
12
+ import { relative } from "node:path";
13
+
14
+ export async function importFragmentFile(path: string): Promise<Record<string, unknown>> {
15
+ const { config } = await loadConfig({
16
+ configFile: path,
17
+ });
18
+
19
+ const databases = findFragnoDatabases(config);
20
+ const adapterNames = databases.map(
21
+ (db) =>
22
+ `${db.adapter[fragnoDatabaseAdapterNameFakeSymbol]}@${db.adapter[fragnoDatabaseAdapterVersionFakeSymbol]}`,
23
+ );
24
+ const uniqueAdapterNames = [...new Set(adapterNames)];
25
+
26
+ if (uniqueAdapterNames.length > 1) {
27
+ throw new Error(
28
+ `All Fragno databases must use the same adapter name and version. ` +
29
+ `Found mismatch: (${adapterNames.join(", ")})`,
30
+ );
31
+ }
32
+
33
+ return {
34
+ adapter: databases[0].adapter,
35
+ databases,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Imports multiple fragment files and validates they all use the same adapter.
41
+ * Returns the combined databases from all files.
42
+ */
43
+ export async function importFragmentFiles(paths: string[]): Promise<{
44
+ adapter: DatabaseAdapter;
45
+ databases: FragnoDatabase<AnySchema>[];
46
+ }> {
47
+ // De-duplicate paths (in case same file was specified multiple times)
48
+ const uniquePaths = Array.from(new Set(paths));
49
+
50
+ if (uniquePaths.length === 0) {
51
+ throw new Error("No fragment files provided");
52
+ }
53
+
54
+ const allDatabases: FragnoDatabase<AnySchema>[] = [];
55
+ let adapter: DatabaseAdapter | undefined;
56
+ let firstAdapterFile: string | undefined;
57
+ const cwd = process.cwd();
58
+
59
+ for (const path of uniquePaths) {
60
+ const relativePath = relative(cwd, path);
61
+
62
+ try {
63
+ const result = await importFragmentFile(path);
64
+ const databases = result["databases"] as FragnoDatabase<AnySchema>[];
65
+ const fileAdapter = result["adapter"] as DatabaseAdapter;
66
+
67
+ if (databases.length === 0) {
68
+ console.warn(
69
+ `Warning: No FragnoDatabase instances found in ${relativePath}.\n` +
70
+ `Make sure you export either:\n` +
71
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
72
+ ` - An instantiated fragment with embedded database definition\n`,
73
+ );
74
+ continue;
75
+ }
76
+
77
+ // Set the adapter from the first file with databases
78
+ if (!adapter) {
79
+ adapter = fileAdapter;
80
+ firstAdapterFile = relativePath;
81
+ }
82
+
83
+ // Validate all files use the same adapter name and version
84
+ const firstAdapterName = adapter[fragnoDatabaseAdapterNameFakeSymbol];
85
+ const firstAdapterVersion = adapter[fragnoDatabaseAdapterVersionFakeSymbol];
86
+ const fileAdapterName = fileAdapter[fragnoDatabaseAdapterNameFakeSymbol];
87
+ const fileAdapterVersion = fileAdapter[fragnoDatabaseAdapterVersionFakeSymbol];
88
+
89
+ if (firstAdapterName !== fileAdapterName || firstAdapterVersion !== fileAdapterVersion) {
90
+ const firstAdapterInfo = `${firstAdapterName}@${firstAdapterVersion}`;
91
+ const fileAdapterInfo = `${fileAdapterName}@${fileAdapterVersion}`;
92
+
93
+ throw new Error(
94
+ `All fragments must use the same database adapter. Mixed adapters found:\n` +
95
+ ` - ${firstAdapterFile}: ${firstAdapterInfo}\n` +
96
+ ` - ${relativePath}: ${fileAdapterInfo}\n\n` +
97
+ `Make sure all fragments use the same adapter name and version.`,
98
+ );
99
+ }
100
+
101
+ allDatabases.push(...databases);
102
+ console.log(` Found ${databases.length} database(s) in ${relativePath}`);
103
+ } catch (error) {
104
+ throw new Error(
105
+ `Failed to import fragment file ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
106
+ );
107
+ }
108
+ }
109
+
110
+ if (allDatabases.length === 0) {
111
+ throw new Error(
112
+ `No FragnoDatabase instances found in any of the target files.\n` +
113
+ `Make sure your files export either:\n` +
114
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
115
+ ` - An instantiated fragment with embedded database definition\n`,
116
+ );
117
+ }
118
+
119
+ if (!adapter) {
120
+ throw new Error("No adapter found in any of the fragment files");
121
+ }
122
+
123
+ return {
124
+ adapter,
125
+ databases: allDatabases,
126
+ };
127
+ }
128
+
129
+ function isFragnoInstantiatedFragment(
130
+ value: unknown,
131
+ ): value is FragnoInstantiatedFragment<[], {}, {}, {}> {
132
+ return (
133
+ typeof value === "object" &&
134
+ value !== null &&
135
+ instantiatedFragmentFakeSymbol in value &&
136
+ value[instantiatedFragmentFakeSymbol] === instantiatedFragmentFakeSymbol
137
+ );
138
+ }
139
+
140
+ function additionalContextIsDatabaseContext(additionalContext: unknown): additionalContext is {
141
+ databaseSchema: AnySchema;
142
+ databaseNamespace: string;
143
+ databaseAdapter: DatabaseAdapter;
144
+ } {
145
+ return (
146
+ typeof additionalContext === "object" &&
147
+ additionalContext !== null &&
148
+ "databaseSchema" in additionalContext &&
149
+ "databaseNamespace" in additionalContext &&
150
+ "databaseAdapter" in additionalContext
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Finds all FragnoDatabase instances in a module, including those embedded
156
+ * in instantiated fragments.
157
+ */
158
+ export function findFragnoDatabases(
159
+ targetModule: Record<string, unknown>,
160
+ ): FragnoDatabase<AnySchema>[] {
161
+ const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];
162
+
163
+ for (const [_key, value] of Object.entries(targetModule)) {
164
+ if (isFragnoDatabase(value)) {
165
+ fragnoDatabases.push(value);
166
+ } else if (isFragnoInstantiatedFragment(value)) {
167
+ const additionalContext = value.additionalContext;
168
+
169
+ if (!additionalContext || !additionalContextIsDatabaseContext(additionalContext)) {
170
+ continue;
171
+ }
172
+
173
+ // Extract database schema, namespace, and adapter from instantiated fragment's additionalContext
174
+ const { databaseSchema, databaseNamespace, databaseAdapter } = additionalContext;
175
+
176
+ fragnoDatabases.push(
177
+ new FragnoDatabase({
178
+ namespace: databaseNamespace,
179
+ schema: databaseSchema,
180
+ adapter: databaseAdapter,
181
+ }),
182
+ );
183
+ }
184
+ }
185
+
186
+ return fragnoDatabases;
187
+ }
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);