@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.
- package/.turbo/turbo-build.log +15 -0
- package/.turbo/turbo-types$colon$check.log +1 -0
- package/CHANGELOG.md +20 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +358 -0
- package/dist/cli.js.map +1 -0
- package/package.json +38 -0
- package/src/cli.ts +181 -0
- package/src/commands/db/generate.ts +168 -0
- package/src/commands/db/info.ts +93 -0
- package/src/commands/db/migrate.ts +131 -0
- package/src/utils/find-fragno-databases.ts +69 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +6 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
$ tsdown
|
|
2
|
+
[34mℹ[39m tsdown [2mv0.15.9[22m powered by rolldown [2mv1.0.0-beta.44[22m
|
|
3
|
+
[34mℹ[39m Using tsdown config: [4m/home/runner/work/fragno/fragno/apps/fragno-cli/tsdown.config.ts[24m
|
|
4
|
+
[34mℹ[39m entry: [34msrc/cli.ts[39m
|
|
5
|
+
[34mℹ[39m target: [34mnode20.0.0[39m
|
|
6
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
7
|
+
[34mℹ[39m Build start
|
|
8
|
+
[34mℹ[39m Granting execute permission to [4mdist/cli.d.ts[24m
|
|
9
|
+
[34mℹ[39m Granting execute permission to [4mdist/cli.js[24m
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mcli.js[22m [2m16.07 kB[22m [2m│ gzip: 3.50 kB[22m
|
|
11
|
+
[34mℹ[39m [2mdist/[22mcli.js.map [2m30.92 kB[22m [2m│ gzip: 6.79 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22mcli.d.ts.map [2m 0.23 kB[22m [2m│ gzip: 0.19 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mcli.d.ts[22m[39m [2m 0.39 kB[22m [2m│ gzip: 0.19 kB[22m
|
|
14
|
+
[34mℹ[39m 4 files, total: 47.61 kB
|
|
15
|
+
[32m✔[39m Build complete in [32m8325ms[39m
|
|
@@ -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
|
package/dist/cli.js.map
ADDED
|
@@ -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
package/tsdown.config.ts
ADDED
package/vitest.config.ts
ADDED