@fragno-dev/cli 0.1.2 → 0.1.4

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,168 +1,175 @@
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);
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import { define } from "gunshi";
4
+ import { findFragnoDatabases } from "../../utils/find-fragno-databases";
5
+ import type { FragnoDatabase } from "@fragno-dev/db";
6
+ import type { AnySchema } from "@fragno-dev/db/schema";
7
+ import { generateMigrationsOrSchema } from "@fragno-dev/db/generation-engine";
8
+
9
+ // Define the db generate command with type safety
10
+ export const generateCommand = define({
11
+ name: "generate",
12
+ description: "Generate schema files from FragnoDatabase definitions",
13
+ args: {
14
+ output: {
15
+ type: "string",
16
+ short: "o",
17
+ description:
18
+ "Output path: for single file, exact file path; for multiple files, output directory (default: current directory)",
19
+ },
20
+ from: {
21
+ type: "number",
22
+ short: "f",
23
+ description: "Source version to generate migration from (default: current database version)",
24
+ },
25
+ to: {
26
+ type: "number",
27
+ short: "t",
28
+ description: "Target version to generate migration to (default: latest schema version)",
29
+ },
30
+ prefix: {
31
+ type: "string",
32
+ short: "p",
33
+ description: "String to prepend to the generated file (e.g., '/* eslint-disable */')",
34
+ },
35
+ },
36
+ run: async (ctx) => {
37
+ // With `define()` and `multiple: true`, targets is properly typed as string[]
38
+ const targets = ctx.positionals;
39
+ const output = ctx.values.output;
40
+ const toVersion = ctx.values.to;
41
+ const fromVersion = ctx.values.from;
42
+ const prefix = ctx.values.prefix;
43
+
44
+ // De-duplicate targets (in case same file was specified multiple times)
45
+ const uniqueTargets = Array.from(new Set(targets));
46
+
47
+ // Load all target files and collect FragnoDatabase instances
48
+ const allFragnoDatabases: FragnoDatabase<AnySchema>[] = [];
49
+
50
+ for (const target of uniqueTargets) {
51
+ const targetPath = resolve(process.cwd(), target);
52
+ console.log(`Loading target file: ${targetPath}`);
53
+
54
+ // Dynamically import the target file
55
+ let targetModule: Record<string, unknown>;
56
+ try {
57
+ targetModule = await import(targetPath);
58
+ } catch (error) {
59
+ throw new Error(
60
+ `Failed to import target file ${target}: ${error instanceof Error ? error.message : String(error)}`,
61
+ );
62
+ }
63
+
64
+ // Find all FragnoDatabase instances or instantiated fragments with databases
65
+ const fragnoDatabases = findFragnoDatabases(targetModule);
66
+
67
+ if (fragnoDatabases.length === 0) {
68
+ console.warn(
69
+ `Warning: No FragnoDatabase instances found in ${target}.\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
+ if (fragnoDatabases.length > 1) {
78
+ console.warn(
79
+ `Warning: Multiple FragnoDatabase instances found in ${target} (${fragnoDatabases.length}). Using all of them.`,
80
+ );
81
+ }
82
+
83
+ allFragnoDatabases.push(...fragnoDatabases);
84
+ }
54
85
 
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
- }
86
+ if (allFragnoDatabases.length === 0) {
87
+ throw new Error(
88
+ `No FragnoDatabase instances found in any of the target files.\n` +
89
+ `Make sure your files export either:\n` +
90
+ ` - A FragnoDatabase instance created with .create(adapter)\n` +
91
+ ` - An instantiated fragment with embedded database definition\n`,
92
+ );
93
+ }
63
94
 
64
- if (fragnoDatabases.length > 1) {
65
- console.warn(
66
- `Warning: Multiple FragnoDatabase instances found (${fragnoDatabases.length}). Using the first one.`,
95
+ console.log(
96
+ `Found ${allFragnoDatabases.length} FragnoDatabase instance(s) across ${uniqueTargets.length} file(s)`,
67
97
  );
68
- }
69
98
 
70
- // Use the first FragnoDatabase instance
71
- const fragnoDb = fragnoDatabases[0];
99
+ // Validate all databases use the same adapter object (identity)
100
+ const firstDb = allFragnoDatabases[0];
101
+ const firstAdapter = firstDb.adapter;
102
+ const allSameAdapter = allFragnoDatabases.every((db) => db.adapter === firstAdapter);
72
103
 
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
- }
104
+ if (!allSameAdapter) {
105
+ throw new Error(
106
+ "All fragments must use the same database adapter instance. Mixed adapters are not supported.",
107
+ );
108
+ }
84
109
 
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
- }
110
+ // Check if adapter supports any form of schema generation
111
+ if (!firstDb.adapter.createSchemaGenerator && !firstDb.adapter.createMigrationEngine) {
112
+ throw new Error(
113
+ `The adapter does not support schema generation. ` +
114
+ `Please use an adapter that implements either createSchemaGenerator or createMigrationEngine.`,
115
+ );
116
+ }
100
117
 
101
- // Determine if output is a directory or file
102
- let isDirectory = false;
118
+ // Generate schema for all fragments
119
+ console.log("Generating schema...");
103
120
 
104
- if (output) {
105
- const resolvedOutput = resolve(process.cwd(), output);
121
+ let results: { schema: string; path: string; namespace: string }[];
106
122
  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("/");
123
+ results = await generateMigrationsOrSchema(allFragnoDatabases, {
124
+ path: output,
125
+ toVersion,
126
+ fromVersion,
127
+ });
128
+ } catch (error) {
129
+ throw new Error(
130
+ `Failed to generate schema: ${error instanceof Error ? error.message : String(error)}`,
131
+ );
112
132
  }
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
133
 
165
- console.log(`✓ Schema generated successfully: ${finalOutputPath}`);
166
- console.log(` Namespace: ${fragnoDb.namespace}`);
167
- console.log(` Schema version: ${fragnoDb.schema.version}`);
168
- }
134
+ // Write all generated files
135
+ for (const result of results) {
136
+ // For single file: use output as exact file path
137
+ // For multiple files: use output as base directory
138
+ const finalOutputPath =
139
+ output && results.length === 1
140
+ ? resolve(process.cwd(), output)
141
+ : output
142
+ ? resolve(process.cwd(), output, result.path)
143
+ : resolve(process.cwd(), result.path);
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(`✓ Generated: ${finalOutputPath}`);
166
+ }
167
+
168
+ console.log(`\n✓ Schema generated successfully!`);
169
+ console.log(` Files generated: ${results.length}`);
170
+ console.log(` Fragments:`);
171
+ for (const db of allFragnoDatabases) {
172
+ console.log(` - ${db.namespace} (version ${db.schema.version})`);
173
+ }
174
+ },
175
+ });
@@ -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
+ });