@fragno-dev/cli 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,93 +1,178 @@
1
- import { isFragnoDatabase, type FragnoDatabase } from "@fragno-dev/db";
2
- import type { AnySchema } from "@fragno-dev/db/schema";
3
1
  import { resolve } from "node:path";
4
- import type { CommandContext } from "gunshi";
2
+ import { define } from "gunshi";
3
+ import { findFragnoDatabases } from "../../utils/find-fragno-databases";
4
+ import type { FragnoDatabase } from "@fragno-dev/db";
5
+ import type { AnySchema } from "@fragno-dev/db/schema";
5
6
 
6
- export async function info(ctx: CommandContext) {
7
- const target = ctx.values["target"];
7
+ export const infoCommand = define({
8
+ name: "info",
9
+ description: "Display database information and migration status",
10
+ args: {},
11
+ run: async (ctx) => {
12
+ const targets = ctx.positionals;
8
13
 
9
- if (!target || typeof target !== "string") {
10
- throw new Error("Target file path is required and must be a string");
11
- }
14
+ if (targets.length === 0) {
15
+ throw new Error("At least one target file path is required");
16
+ }
12
17
 
13
- // Resolve the target file path relative to current working directory
14
- const targetPath = resolve(process.cwd(), target);
18
+ // De-duplicate targets (in case same file was specified multiple times)
19
+ const uniqueTargets = Array.from(new Set(targets));
20
+
21
+ // Load all target files and collect FragnoDatabase instances
22
+ const allFragnoDatabases: FragnoDatabase<AnySchema>[] = [];
23
+
24
+ for (const target of uniqueTargets) {
25
+ const targetPath = resolve(process.cwd(), target);
26
+ console.log(`Loading target file: ${targetPath}`);
27
+
28
+ // Dynamically import the target file
29
+ let targetModule: Record<string, unknown>;
30
+ try {
31
+ targetModule = await import(targetPath);
32
+ } catch (error) {
33
+ throw new Error(
34
+ `Failed to import target file ${target}: ${error instanceof Error ? error.message : String(error)}`,
35
+ );
36
+ }
37
+
38
+ // Find all FragnoDatabase instances or instantiated fragments with databases
39
+ const fragnoDatabases = findFragnoDatabases(targetModule);
40
+
41
+ if (fragnoDatabases.length === 0) {
42
+ console.warn(
43
+ `Warning: No FragnoDatabase instances found in ${target}.\n` +
44
+ `Make sure you export either:\n` +
45
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
46
+ ` - An instantiated fragment with embedded database definition\n`,
47
+ );
48
+ continue;
49
+ }
50
+
51
+ if (fragnoDatabases.length > 1) {
52
+ console.warn(
53
+ `Warning: Multiple FragnoDatabase instances found in ${target} (${fragnoDatabases.length}). Showing info for all of them.`,
54
+ );
55
+ }
56
+
57
+ allFragnoDatabases.push(...fragnoDatabases);
58
+ }
15
59
 
16
- console.log(`Loading target file: ${targetPath}`);
60
+ if (allFragnoDatabases.length === 0) {
61
+ throw new Error(
62
+ `No FragnoDatabase instances found in any of the target files.\n` +
63
+ `Make sure your files export either:\n` +
64
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
65
+ ` - An instantiated fragment with embedded database definition\n`,
66
+ );
67
+ }
17
68
 
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)}`,
69
+ // Collect database information
70
+ const dbInfos = await Promise.all(
71
+ allFragnoDatabases.map(async (fragnoDb) => {
72
+ const info: {
73
+ namespace: string;
74
+ schemaVersion: number;
75
+ migrationSupport: boolean;
76
+ currentVersion?: number;
77
+ pendingVersions?: number;
78
+ status?: string;
79
+ error?: string;
80
+ } = {
81
+ namespace: fragnoDb.namespace,
82
+ schemaVersion: fragnoDb.schema.version,
83
+ migrationSupport: !!fragnoDb.adapter.createMigrationEngine,
84
+ };
85
+
86
+ // Get current database version if migrations are supported
87
+ if (fragnoDb.adapter.createMigrationEngine) {
88
+ try {
89
+ const migrator = fragnoDb.adapter.createMigrationEngine(
90
+ fragnoDb.schema,
91
+ fragnoDb.namespace,
92
+ );
93
+ const currentVersion = await migrator.getVersion();
94
+ info.currentVersion = currentVersion;
95
+ info.pendingVersions = fragnoDb.schema.version - currentVersion;
96
+
97
+ if (info.pendingVersions > 0) {
98
+ info.status = `Pending (${info.pendingVersions} migration(s))`;
99
+ } else if (info.pendingVersions === 0) {
100
+ info.status = "Up to date";
101
+ }
102
+ } catch (error) {
103
+ info.error = error instanceof Error ? error.message : String(error);
104
+ info.status = "Error";
105
+ }
106
+ } else {
107
+ info.status = "Schema only";
108
+ }
109
+
110
+ return info;
111
+ }),
25
112
  );
26
- }
27
113
 
28
- // Find all FragnoDatabase instances in the exported values
29
- const fragnoDatabases: FragnoDatabase<AnySchema>[] = [];
114
+ // Determine if any database supports migrations
115
+ const hasMigrationSupport = dbInfos.some((info) => info.migrationSupport);
30
116
 
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
- }
117
+ // Print compact table
118
+ console.log("");
119
+ console.log(
120
+ `Found ${allFragnoDatabases.length} database(s) across ${uniqueTargets.length} file(s):`,
121
+ );
122
+ console.log("");
37
123
 
38
- if (fragnoDatabases.length === 0) {
39
- throw new Error(`No FragnoDatabase instances found in ${target}.\n`);
40
- }
124
+ // Table header
125
+ const namespaceHeader = "Namespace";
126
+ const versionHeader = "Schema";
127
+ const currentHeader = "Current";
128
+ const statusHeader = "Status";
41
129
 
42
- if (fragnoDatabases.length > 1) {
43
- console.warn(
44
- `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
130
+ const maxNamespaceLen = Math.max(
131
+ namespaceHeader.length,
132
+ ...dbInfos.map((info) => info.namespace.length),
45
133
  );
46
- }
134
+ const namespaceWidth = Math.max(maxNamespaceLen + 2, 20);
135
+ const versionWidth = 8;
136
+ const currentWidth = 9;
137
+ const statusWidth = 25;
47
138
 
48
- // Use the first FragnoDatabase instance
49
- const fragnoDb = fragnoDatabases[0];
139
+ // Print table
140
+ console.log(
141
+ namespaceHeader.padEnd(namespaceWidth) +
142
+ versionHeader.padEnd(versionWidth) +
143
+ (hasMigrationSupport ? currentHeader.padEnd(currentWidth) : "") +
144
+ statusHeader,
145
+ );
146
+ console.log(
147
+ "-".repeat(namespaceWidth) +
148
+ "-".repeat(versionWidth) +
149
+ (hasMigrationSupport ? "-".repeat(currentWidth) : "") +
150
+ "-".repeat(statusWidth),
151
+ );
50
152
 
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}`);
153
+ for (const info of dbInfos) {
154
+ const currentVersionStr =
155
+ info.currentVersion !== undefined ? String(info.currentVersion) : "-";
156
+ console.log(
157
+ info.namespace.padEnd(namespaceWidth) +
158
+ String(info.schemaVersion).padEnd(versionWidth) +
159
+ (hasMigrationSupport ? currentVersionStr.padEnd(currentWidth) : "") +
160
+ (info.status || "-"),
161
+ );
162
+ }
56
163
 
57
- // Check if the adapter supports migrations
58
- if (!fragnoDb.adapter.createMigrationEngine) {
59
- console.log(`Migration Support: No`);
164
+ // Print help text
60
165
  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`);
166
+ if (!hasMigrationSupport) {
167
+ console.log("Note: These adapters do not support migrations.");
168
+ console.log("Use '@fragno-dev/cli db generate' to generate schema files.");
169
+ } else {
170
+ const hasPendingMigrations = dbInfos.some(
171
+ (info) => info.pendingVersions && info.pendingVersions > 0,
172
+ );
173
+ if (hasPendingMigrations) {
174
+ console.log("Run '@fragno-dev/cli db migrate <target>' to apply pending migrations.");
175
+ }
84
176
  }
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
- }
177
+ },
178
+ });
@@ -1,131 +1,133 @@
1
1
  import { resolve } from "node:path";
2
- import type { CommandContext } from "gunshi";
2
+ import { define } from "gunshi";
3
3
  import { findFragnoDatabases } from "../../utils/find-fragno-databases";
4
+ import { type FragnoDatabase } from "@fragno-dev/db";
5
+ import { executeMigrations, type ExecuteMigrationResult } from "@fragno-dev/db/generation-engine";
6
+ import type { AnySchema } from "@fragno-dev/db/schema";
7
+
8
+ export const migrateCommand = define({
9
+ name: "migrate",
10
+ description: "Run database migrations for all fragments to their latest versions",
11
+ args: {},
12
+ run: async (ctx) => {
13
+ const targets = ctx.positionals;
14
+
15
+ if (targets.length === 0) {
16
+ throw new Error("At least one target file path is required");
17
+ }
4
18
 
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
- }
19
+ // De-duplicate targets
20
+ const uniqueTargets = Array.from(new Set(targets));
40
21
 
41
- // Find all FragnoDatabase instances or instantiated fragments with databases
42
- const fragnoDatabases = findFragnoDatabases(targetModule);
22
+ // Load all target files and collect FragnoDatabase instances
23
+ const allFragnoDatabases: FragnoDatabase<AnySchema>[] = [];
43
24
 
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
- }
25
+ for (const target of uniqueTargets) {
26
+ const targetPath = resolve(process.cwd(), target);
27
+ console.log(`Loading target file: ${targetPath}`);
52
28
 
53
- if (fragnoDatabases.length > 1) {
54
- console.warn(
55
- `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
56
- );
57
- }
29
+ // Dynamically import the target file
30
+ let targetModule: Record<string, unknown>;
31
+ try {
32
+ targetModule = await import(targetPath);
33
+ } catch (error) {
34
+ throw new Error(
35
+ `Failed to import target file ${target}: ${error instanceof Error ? error.message : String(error)}`,
36
+ );
37
+ }
58
38
 
59
- // Use the first FragnoDatabase instance
60
- const fragnoDb = fragnoDatabases[0];
39
+ // Find all FragnoDatabase instances or instantiated fragments with databases
40
+ const fragnoDatabases = findFragnoDatabases(targetModule);
61
41
 
62
- console.log(`Migrating database for namespace: ${fragnoDb.namespace}`);
42
+ if (fragnoDatabases.length === 0) {
43
+ console.warn(
44
+ `Warning: No FragnoDatabase instances found in ${target}.\n` +
45
+ `Make sure you export either:\n` +
46
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
47
+ ` - An instantiated fragment with embedded database definition\n`,
48
+ );
49
+ continue;
50
+ }
63
51
 
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})`,
52
+ if (fragnoDatabases.length > 1) {
53
+ console.warn(
54
+ `Warning: Multiple FragnoDatabase instances found in ${target} (${fragnoDatabases.length}). Using all of them.`,
98
55
  );
99
56
  }
100
57
 
101
- const preparedMigration = await migrator.prepareMigrationTo(targetVersion, {
102
- updateSettings: true,
103
- });
58
+ allFragnoDatabases.push(...fragnoDatabases);
59
+ }
60
+
61
+ if (allFragnoDatabases.length === 0) {
62
+ throw new Error(
63
+ `No FragnoDatabase instances found in any of the target files.\n` +
64
+ `Make sure your files export either:\n` +
65
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
66
+ ` - An instantiated fragment with embedded database definition\n`,
67
+ );
68
+ }
69
+
70
+ console.log(
71
+ `Found ${allFragnoDatabases.length} FragnoDatabase instance(s) across ${uniqueTargets.length} file(s)`,
72
+ );
73
+
74
+ // Validate all databases use the same adapter object (identity)
75
+ const firstDb = allFragnoDatabases[0];
76
+ const firstAdapter = firstDb.adapter;
77
+ const allSameAdapter = allFragnoDatabases.every((db) => db.adapter === firstAdapter);
78
+
79
+ if (!allSameAdapter) {
80
+ throw new Error(
81
+ "All fragments must use the same database adapter instance. Mixed adapters are not supported.",
82
+ );
83
+ }
84
+
85
+ console.log("\nMigrating all fragments to their latest versions...\n");
104
86
 
105
- if (preparedMigration.operations.length === 0) {
106
- console.log("✓ Database is already at the target version. No migrations needed.");
107
- didMigrate = false;
87
+ let results: ExecuteMigrationResult[];
88
+ try {
89
+ results = await executeMigrations(allFragnoDatabases);
90
+ } catch (error) {
91
+ throw new Error(
92
+ `Migration failed: ${error instanceof Error ? error.message : String(error)}`,
93
+ );
94
+ }
95
+
96
+ // Display progress for each result
97
+ for (const result of results) {
98
+ console.log(`Fragment: ${result.namespace}`);
99
+ console.log(` Current version: ${result.fromVersion}`);
100
+ console.log(` Target version: ${result.toVersion}`);
101
+
102
+ if (result.didMigrate) {
103
+ console.log(` ✓ Migration completed: v${result.fromVersion} → v${result.toVersion}\n`);
108
104
  } else {
109
- await preparedMigration.execute();
110
- didMigrate = true;
105
+ console.log(` ✓ Already at latest version. No migration needed.\n`);
111
106
  }
112
- } else {
113
- console.log(`Migrating to latest version (${fragnoDb.schema.version})...`);
114
- didMigrate = await fragnoDb.runMigrations();
115
107
  }
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}`);
108
+
109
+ // Summary
110
+ console.log("═══════════════════════════════════════");
111
+ console.log("Migration Summary");
112
+ console.log("═══════════════════════════════════════");
113
+
114
+ const migrated = results.filter((r) => r.didMigrate);
115
+ const skipped = results.filter((r) => !r.didMigrate);
116
+
117
+ if (migrated.length > 0) {
118
+ console.log(`\n✓ Migrated ${migrated.length} fragment(s):`);
119
+ for (const r of migrated) {
120
+ console.log(` - ${r.namespace}: v${r.fromVersion} → v${r.toVersion}`);
121
+ }
129
122
  }
130
- }
131
- }
123
+
124
+ if (skipped.length > 0) {
125
+ console.log(`\n○ Skipped ${skipped.length} fragment(s) (already up-to-date):`);
126
+ for (const r of skipped) {
127
+ console.log(` - ${r.namespace}: v${r.toVersion}`);
128
+ }
129
+ }
130
+
131
+ console.log("\n✓ All migrations completed successfully");
132
+ },
133
+ });
@@ -1 +0,0 @@
1
- $ tsc --noEmit