zenstack-kit 0.1.1
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/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/cli/app.d.ts +12 -0
- package/dist/cli/app.d.ts.map +1 -0
- package/dist/cli/app.js +253 -0
- package/dist/cli/commands.d.ts +70 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +308 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/prompt-provider.d.ts +10 -0
- package/dist/cli/prompt-provider.d.ts.map +1 -0
- package/dist/cli/prompt-provider.js +41 -0
- package/dist/cli/prompts.d.ts +27 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +133 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +240 -0
- package/dist/config/index.d.ts +96 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +48 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +44 -0
- package/dist/config-loader.d.ts +6 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +36 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/init-prompts.d.ts +13 -0
- package/dist/init-prompts.d.ts.map +1 -0
- package/dist/init-prompts.js +64 -0
- package/dist/introspect.d.ts +54 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +75 -0
- package/dist/kysely-adapter.d.ts +49 -0
- package/dist/kysely-adapter.d.ts.map +1 -0
- package/dist/kysely-adapter.js +74 -0
- package/dist/migrate-apply.d.ts +18 -0
- package/dist/migrate-apply.d.ts.map +1 -0
- package/dist/migrate-apply.js +61 -0
- package/dist/migrate.d.ts +108 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +127 -0
- package/dist/migrations/apply.d.ts +18 -0
- package/dist/migrations/apply.d.ts.map +1 -0
- package/dist/migrations/apply.js +61 -0
- package/dist/migrations/diff.d.ts +161 -0
- package/dist/migrations/diff.d.ts.map +1 -0
- package/dist/migrations/diff.js +620 -0
- package/dist/migrations/prisma.d.ts +193 -0
- package/dist/migrations/prisma.d.ts.map +1 -0
- package/dist/migrations/prisma.js +929 -0
- package/dist/migrations.d.ts +161 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +620 -0
- package/dist/prisma-migrations.d.ts +160 -0
- package/dist/prisma-migrations.d.ts.map +1 -0
- package/dist/prisma-migrations.js +789 -0
- package/dist/prompts.d.ts +10 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +41 -0
- package/dist/pull.d.ts +23 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/pull.js +424 -0
- package/dist/schema/introspect.d.ts +54 -0
- package/dist/schema/introspect.d.ts.map +1 -0
- package/dist/schema/introspect.js +75 -0
- package/dist/schema/pull.d.ts +23 -0
- package/dist/schema/pull.d.ts.map +1 -0
- package/dist/schema/pull.js +424 -0
- package/dist/schema/snapshot.d.ts +46 -0
- package/dist/schema/snapshot.d.ts.map +1 -0
- package/dist/schema/snapshot.js +278 -0
- package/dist/schema-snapshot.d.ts +45 -0
- package/dist/schema-snapshot.d.ts.map +1 -0
- package/dist/schema-snapshot.js +265 -0
- package/dist/sql/compiler.d.ts +74 -0
- package/dist/sql/compiler.d.ts.map +1 -0
- package/dist/sql/compiler.js +270 -0
- package/dist/sql/kysely-adapter.d.ts +49 -0
- package/dist/sql/kysely-adapter.d.ts.map +1 -0
- package/dist/sql/kysely-adapter.js +74 -0
- package/dist/sql-compiler.d.ts +74 -0
- package/dist/sql-compiler.d.ts.map +1 -0
- package/dist/sql-compiler.js +243 -0
- package/package.json +81 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command implementations for zenstack-kit CLI
|
|
3
|
+
*
|
|
4
|
+
* These functions contain the core logic and can be tested independently
|
|
5
|
+
* from the CLI/UI layer.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { loadConfig } from "../config/loader.js";
|
|
10
|
+
import { pullSchema } from "../schema/pull.js";
|
|
11
|
+
import { createPrismaMigration, applyPrismaMigrations, previewPrismaMigrations, hasPrismaSchemaChanges, hasSnapshot, scanMigrationFolders, writeMigrationLog, initializeSnapshot, createInitialMigration, detectPotentialRenames, } from "../migrations/prisma.js";
|
|
12
|
+
export class CommandError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "CommandError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load and validate config, returning resolved paths
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveConfig(ctx) {
|
|
22
|
+
const loaded = await loadConfig(ctx.cwd);
|
|
23
|
+
if (!loaded) {
|
|
24
|
+
throw new CommandError("No zenstack-kit config file found.");
|
|
25
|
+
}
|
|
26
|
+
const { config, configDir } = loaded;
|
|
27
|
+
const relativeSchemaPath = ctx.options.schema || config.schema || "./schema.zmodel";
|
|
28
|
+
const relativeOutputPath = ctx.options.migrations || config.migrations?.migrationsFolder || "./prisma/migrations";
|
|
29
|
+
const dialect = (ctx.options.dialect || config.dialect || "sqlite");
|
|
30
|
+
// Resolve paths relative to config file location
|
|
31
|
+
const schemaPath = path.resolve(configDir, relativeSchemaPath);
|
|
32
|
+
const outputPath = path.resolve(configDir, relativeOutputPath);
|
|
33
|
+
return { config, schemaPath, outputPath, dialect };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate that the schema file exists (schemaPath should be absolute)
|
|
37
|
+
*/
|
|
38
|
+
export function validateSchemaExists(schemaPath) {
|
|
39
|
+
if (!fs.existsSync(schemaPath)) {
|
|
40
|
+
throw new CommandError(`ZenStack schema file not found: ${schemaPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get connection URL from config based on dialect
|
|
45
|
+
*/
|
|
46
|
+
export function getConnectionUrl(config, dialect) {
|
|
47
|
+
const dbCredentials = config.dbCredentials;
|
|
48
|
+
return dialect === "sqlite"
|
|
49
|
+
? dbCredentials?.file
|
|
50
|
+
: dbCredentials?.url;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* migrate:generate command
|
|
54
|
+
*/
|
|
55
|
+
export async function runMigrateGenerate(ctx) {
|
|
56
|
+
const { config, schemaPath, outputPath, dialect } = await resolveConfig(ctx);
|
|
57
|
+
validateSchemaExists(schemaPath);
|
|
58
|
+
const snapshotExists = await hasSnapshot(outputPath);
|
|
59
|
+
if (!snapshotExists) {
|
|
60
|
+
throw new CommandError("No snapshot found. Run 'zenstack-kit init' first.");
|
|
61
|
+
}
|
|
62
|
+
ctx.log("info", "Generating migration...");
|
|
63
|
+
const hasChanges = await hasPrismaSchemaChanges({
|
|
64
|
+
schemaPath,
|
|
65
|
+
outputPath,
|
|
66
|
+
});
|
|
67
|
+
if (!hasChanges) {
|
|
68
|
+
ctx.log("warning", "No schema changes detected");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Detect potential renames and prompt user to disambiguate
|
|
72
|
+
const potentialRenames = await detectPotentialRenames({ schemaPath, outputPath });
|
|
73
|
+
const renameTables = [];
|
|
74
|
+
const renameColumns = [];
|
|
75
|
+
// Prompt for table renames
|
|
76
|
+
for (const rename of potentialRenames.tables) {
|
|
77
|
+
if (ctx.promptTableRename) {
|
|
78
|
+
const choice = await ctx.promptTableRename(rename.from, rename.to);
|
|
79
|
+
if (choice === "rename") {
|
|
80
|
+
renameTables.push(rename);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Prompt for column renames
|
|
85
|
+
for (const rename of potentialRenames.columns) {
|
|
86
|
+
if (ctx.promptColumnRename) {
|
|
87
|
+
const choice = await ctx.promptColumnRename(rename.table, rename.from, rename.to);
|
|
88
|
+
if (choice === "rename") {
|
|
89
|
+
renameColumns.push(rename);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const name = ctx.options.name || "migration";
|
|
94
|
+
const migration = await createPrismaMigration({
|
|
95
|
+
name,
|
|
96
|
+
schemaPath,
|
|
97
|
+
outputPath,
|
|
98
|
+
dialect,
|
|
99
|
+
renameTables: renameTables.length > 0 ? renameTables : undefined,
|
|
100
|
+
renameColumns: renameColumns.length > 0 ? renameColumns : undefined,
|
|
101
|
+
});
|
|
102
|
+
if (!migration) {
|
|
103
|
+
ctx.log("warning", "No schema changes detected");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
ctx.log("success", `Migration created: ${migration.folderName}/migration.sql`);
|
|
107
|
+
ctx.log("info", `Path: ${migration.folderPath}`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* migrate:apply command
|
|
111
|
+
*/
|
|
112
|
+
export async function runMigrateApply(ctx) {
|
|
113
|
+
const { config, outputPath, dialect } = await resolveConfig(ctx);
|
|
114
|
+
const connectionUrl = getConnectionUrl(config, dialect);
|
|
115
|
+
const migrationsTable = ctx.options.table || config.migrations?.migrationsTable || "_prisma_migrations";
|
|
116
|
+
const migrationsSchema = ctx.options.dbSchema ||
|
|
117
|
+
config.migrations?.migrationsSchema ||
|
|
118
|
+
"public";
|
|
119
|
+
const snapshotExists = await hasSnapshot(outputPath);
|
|
120
|
+
if (!snapshotExists) {
|
|
121
|
+
throw new CommandError("No snapshot found. Run 'zenstack-kit init' first.");
|
|
122
|
+
}
|
|
123
|
+
const migrations = await scanMigrationFolders(outputPath);
|
|
124
|
+
if (migrations.length === 0) {
|
|
125
|
+
throw new CommandError("No migrations found.");
|
|
126
|
+
}
|
|
127
|
+
if (dialect !== "sqlite" && !connectionUrl) {
|
|
128
|
+
throw new CommandError("Database connection URL is required for non-sqlite dialects.");
|
|
129
|
+
}
|
|
130
|
+
const databasePath = dialect === "sqlite" ? connectionUrl : undefined;
|
|
131
|
+
// Preview mode - show pending migrations without applying
|
|
132
|
+
if (ctx.options.preview) {
|
|
133
|
+
ctx.log("info", "Preview mode - showing pending migrations:");
|
|
134
|
+
const preview = await previewPrismaMigrations({
|
|
135
|
+
migrationsFolder: outputPath,
|
|
136
|
+
dialect,
|
|
137
|
+
connectionUrl,
|
|
138
|
+
databasePath,
|
|
139
|
+
migrationsTable,
|
|
140
|
+
migrationsSchema,
|
|
141
|
+
});
|
|
142
|
+
if (preview.pending.length === 0) {
|
|
143
|
+
ctx.log("warning", "No pending migrations");
|
|
144
|
+
if (preview.alreadyApplied.length > 0) {
|
|
145
|
+
ctx.log("info", `${preview.alreadyApplied.length} migration(s) already applied`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const migration of preview.pending) {
|
|
150
|
+
ctx.log("info", `Pending: ${migration.name}`);
|
|
151
|
+
ctx.log("info", `SQL:\n${migration.sql}`);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
ctx.log("info", "Applying migrations...");
|
|
156
|
+
const result = await applyPrismaMigrations({
|
|
157
|
+
migrationsFolder: outputPath,
|
|
158
|
+
dialect,
|
|
159
|
+
connectionUrl,
|
|
160
|
+
databasePath,
|
|
161
|
+
migrationsTable,
|
|
162
|
+
migrationsSchema,
|
|
163
|
+
});
|
|
164
|
+
if (result.applied.length === 0 && !result.failed) {
|
|
165
|
+
ctx.log("warning", "No pending migrations");
|
|
166
|
+
if (result.alreadyApplied.length > 0) {
|
|
167
|
+
ctx.log("info", `${result.alreadyApplied.length} migration(s) already applied`);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const item of result.applied) {
|
|
172
|
+
ctx.log("success", `Applied: ${item.migrationName} (${item.duration}ms)`);
|
|
173
|
+
}
|
|
174
|
+
if (result.failed) {
|
|
175
|
+
throw new CommandError(`Migration failed: ${result.failed.migrationName} - ${result.failed.error}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* init command
|
|
180
|
+
*/
|
|
181
|
+
export async function runInit(ctx) {
|
|
182
|
+
const { config, schemaPath, outputPath, dialect } = await resolveConfig(ctx);
|
|
183
|
+
validateSchemaExists(schemaPath);
|
|
184
|
+
const snapshotExists = await hasSnapshot(outputPath);
|
|
185
|
+
const existingMigrations = await scanMigrationFolders(outputPath);
|
|
186
|
+
// CASE A: Snapshot already exists
|
|
187
|
+
if (snapshotExists) {
|
|
188
|
+
if (!ctx.promptSnapshotExists) {
|
|
189
|
+
throw new CommandError("Snapshot already exists and no prompt handler provided.");
|
|
190
|
+
}
|
|
191
|
+
const choice = await ctx.promptSnapshotExists();
|
|
192
|
+
if (choice === "skip") {
|
|
193
|
+
ctx.log("warning", "Skipped - no changes made");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
ctx.log("info", "Reinitializing...");
|
|
197
|
+
const result = await initializeSnapshot({ schemaPath, outputPath });
|
|
198
|
+
const migrations = await scanMigrationFolders(outputPath);
|
|
199
|
+
await writeMigrationLog(outputPath, migrations);
|
|
200
|
+
ctx.log("success", `Snapshot recreated: ${result.snapshotPath}`);
|
|
201
|
+
ctx.log("success", `Migration log rebuilt with ${migrations.length} migration(s)`);
|
|
202
|
+
ctx.log("info", `${result.tableCount} table(s) captured`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// CASE B: No snapshot but migrations exist (takeover mode)
|
|
206
|
+
if (existingMigrations.length > 0) {
|
|
207
|
+
ctx.log("info", "Initializing snapshot...");
|
|
208
|
+
ctx.log("warning", `Found ${existingMigrations.length} existing migration(s) without snapshot.`);
|
|
209
|
+
ctx.log("info", "Taking over from existing migrations...");
|
|
210
|
+
for (const migration of existingMigrations) {
|
|
211
|
+
ctx.log("info", `${migration.name} (${migration.checksum.slice(0, 8)}...)`);
|
|
212
|
+
}
|
|
213
|
+
const result = await initializeSnapshot({ schemaPath, outputPath });
|
|
214
|
+
await writeMigrationLog(outputPath, existingMigrations);
|
|
215
|
+
ctx.log("success", `Snapshot created: ${result.snapshotPath}`);
|
|
216
|
+
ctx.log("success", `Migration log created with ${existingMigrations.length} migration(s)`);
|
|
217
|
+
ctx.log("info", `${result.tableCount} table(s) captured`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// CASE C: Fresh init (no snapshot, no migrations)
|
|
221
|
+
let choice;
|
|
222
|
+
if (ctx.options.baseline) {
|
|
223
|
+
choice = "baseline";
|
|
224
|
+
}
|
|
225
|
+
else if (ctx.options.createInitial) {
|
|
226
|
+
choice = "create_initial";
|
|
227
|
+
}
|
|
228
|
+
else if (ctx.promptFreshInit) {
|
|
229
|
+
choice = await ctx.promptFreshInit();
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw new CommandError("Fresh init requires --baseline or --create-initial flag, or a prompt handler.");
|
|
233
|
+
}
|
|
234
|
+
if (choice === "baseline") {
|
|
235
|
+
ctx.log("info", "Creating baseline snapshot...");
|
|
236
|
+
const result = await initializeSnapshot({ schemaPath, outputPath });
|
|
237
|
+
await writeMigrationLog(outputPath, []);
|
|
238
|
+
ctx.log("success", `Snapshot created: ${result.snapshotPath}`);
|
|
239
|
+
ctx.log("success", "Empty migration log created");
|
|
240
|
+
ctx.log("info", `${result.tableCount} table(s) captured`);
|
|
241
|
+
ctx.log("info", "Baselined. Future changes will generate migrations.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Create initial migration
|
|
245
|
+
ctx.log("info", "Creating initial migration...");
|
|
246
|
+
const migration = await createInitialMigration({
|
|
247
|
+
name: "init",
|
|
248
|
+
schemaPath,
|
|
249
|
+
outputPath,
|
|
250
|
+
dialect,
|
|
251
|
+
});
|
|
252
|
+
ctx.log("success", `Initial migration created: ${migration.folderName}/migration.sql`);
|
|
253
|
+
ctx.log("info", `Path: ${migration.folderPath}`);
|
|
254
|
+
ctx.log("info", "Run migrate:apply to set up the database.");
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* pull command
|
|
258
|
+
*/
|
|
259
|
+
export async function runPull(ctx) {
|
|
260
|
+
const { config, dialect, outputPath: migrationsPath } = await resolveConfig(ctx);
|
|
261
|
+
const connectionUrl = getConnectionUrl(config, dialect);
|
|
262
|
+
if (dialect !== "sqlite" && !connectionUrl) {
|
|
263
|
+
throw new CommandError("Database connection URL is required for non-sqlite dialects.");
|
|
264
|
+
}
|
|
265
|
+
const databasePath = dialect === "sqlite" ? connectionUrl : undefined;
|
|
266
|
+
const schemaOutputPath = ctx.options.output || config.schema || "./schema.zmodel";
|
|
267
|
+
// Check for existing files that would be affected
|
|
268
|
+
const existingFiles = [];
|
|
269
|
+
if (fs.existsSync(schemaOutputPath)) {
|
|
270
|
+
existingFiles.push(`Schema: ${schemaOutputPath}`);
|
|
271
|
+
}
|
|
272
|
+
const snapshotExists = await hasSnapshot(migrationsPath);
|
|
273
|
+
if (snapshotExists) {
|
|
274
|
+
existingFiles.push(`Snapshot: ${migrationsPath}/meta/_snapshot.json`);
|
|
275
|
+
}
|
|
276
|
+
const migrations = await scanMigrationFolders(migrationsPath);
|
|
277
|
+
if (migrations.length > 0) {
|
|
278
|
+
existingFiles.push(`Migrations: ${migrations.length} migration(s) in ${migrationsPath}`);
|
|
279
|
+
}
|
|
280
|
+
// If there are existing files and not using --force, ask for confirmation
|
|
281
|
+
if (existingFiles.length > 0 && !ctx.options.force) {
|
|
282
|
+
ctx.log("warning", "The following files/directories already exist:");
|
|
283
|
+
for (const file of existingFiles) {
|
|
284
|
+
ctx.log("info", ` ${file}`);
|
|
285
|
+
}
|
|
286
|
+
if (!ctx.promptPullConfirm) {
|
|
287
|
+
throw new CommandError("Existing schema/migrations found. Use --force to overwrite, or provide a prompt handler.");
|
|
288
|
+
}
|
|
289
|
+
const confirmed = await ctx.promptPullConfirm(existingFiles);
|
|
290
|
+
if (!confirmed) {
|
|
291
|
+
ctx.log("warning", "Aborted - no changes made");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
ctx.log("info", "Pulling schema from database...");
|
|
296
|
+
const result = await pullSchema({
|
|
297
|
+
dialect,
|
|
298
|
+
connectionUrl,
|
|
299
|
+
databasePath,
|
|
300
|
+
outputPath: schemaOutputPath,
|
|
301
|
+
});
|
|
302
|
+
ctx.log("success", `Schema generated: ${result.outputPath}`);
|
|
303
|
+
ctx.log("info", `${result.tableCount} table(s) introspected`);
|
|
304
|
+
// If we have existing migrations, warn about resetting
|
|
305
|
+
if (snapshotExists || migrations.length > 0) {
|
|
306
|
+
ctx.log("warning", "You should run 'zenstack-kit init' to reset the snapshot after reviewing the schema.");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zenstack-kit CLI - Database tooling for ZenStack schemas
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* migrate:generate Generate a new SQL migration
|
|
7
|
+
* migrate:apply Apply pending migrations
|
|
8
|
+
* init Initialize snapshot from existing schema
|
|
9
|
+
* pull Introspect database and generate schema
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zenstack-kit CLI - Database tooling for ZenStack schemas
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* migrate:generate Generate a new SQL migration
|
|
7
|
+
* migrate:apply Apply pending migrations
|
|
8
|
+
* init Initialize snapshot from existing schema
|
|
9
|
+
* pull Introspect database and generate schema
|
|
10
|
+
*/
|
|
11
|
+
import { runCli } from "./app.js";
|
|
12
|
+
runCli();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt utilities with injectable provider for tests.
|
|
3
|
+
*/
|
|
4
|
+
export interface PromptProvider {
|
|
5
|
+
question(message: string): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createDefaultPromptProvider(): PromptProvider;
|
|
8
|
+
export declare function setPromptProvider(provider: PromptProvider | null): void;
|
|
9
|
+
export declare function getPromptProvider(): PromptProvider;
|
|
10
|
+
//# sourceMappingURL=prompt-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-provider.d.ts","sourceRoot":"","sources":["../../src/cli/prompt-provider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5C;AAED,wBAAgB,2BAA2B,IAAI,cAAc,CAS5D;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAEvE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAsBlD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt utilities with injectable provider for tests.
|
|
3
|
+
*/
|
|
4
|
+
import { createInterface } from "readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "process";
|
|
6
|
+
export function createDefaultPromptProvider() {
|
|
7
|
+
return {
|
|
8
|
+
async question(message) {
|
|
9
|
+
const rl = createInterface({ input, output });
|
|
10
|
+
const answer = await rl.question(message);
|
|
11
|
+
rl.close();
|
|
12
|
+
return answer;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
let currentProvider = null;
|
|
17
|
+
export function setPromptProvider(provider) {
|
|
18
|
+
currentProvider = provider;
|
|
19
|
+
}
|
|
20
|
+
export function getPromptProvider() {
|
|
21
|
+
if (currentProvider) {
|
|
22
|
+
return currentProvider;
|
|
23
|
+
}
|
|
24
|
+
const envAnswers = process.env.ZENSTACK_KIT_PROMPT_ANSWERS;
|
|
25
|
+
if (envAnswers) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(envAnswers);
|
|
28
|
+
const queue = Array.isArray(parsed) ? [...parsed] : [];
|
|
29
|
+
currentProvider = {
|
|
30
|
+
async question() {
|
|
31
|
+
return queue.shift() ?? "";
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return currentProvider;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return createDefaultPromptProvider();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return createDefaultPromptProvider();
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for the init command using ink
|
|
3
|
+
*/
|
|
4
|
+
export type InitChoice = "skip" | "reinitialize" | "baseline" | "create_initial";
|
|
5
|
+
export type ConfirmChoice = "yes" | "no";
|
|
6
|
+
/**
|
|
7
|
+
* Prompt user when snapshot already exists (Case A)
|
|
8
|
+
*/
|
|
9
|
+
export declare function promptSnapshotExists(): Promise<InitChoice>;
|
|
10
|
+
/**
|
|
11
|
+
* Prompt user for fresh init when no migrations exist (Case C)
|
|
12
|
+
*/
|
|
13
|
+
export declare function promptFreshInit(): Promise<InitChoice>;
|
|
14
|
+
/**
|
|
15
|
+
* Prompt user to confirm overwriting existing files during pull
|
|
16
|
+
*/
|
|
17
|
+
export declare function promptPullConfirm(existingFiles: string[]): Promise<boolean>;
|
|
18
|
+
export type RenameChoice = "rename" | "delete_create";
|
|
19
|
+
/**
|
|
20
|
+
* Prompt user to disambiguate a potential table rename
|
|
21
|
+
*/
|
|
22
|
+
export declare function promptTableRename(from: string, to: string): Promise<RenameChoice>;
|
|
23
|
+
/**
|
|
24
|
+
* Prompt user to disambiguate a potential column rename
|
|
25
|
+
*/
|
|
26
|
+
export declare function promptColumnRename(table: string, from: string, to: string): Promise<RenameChoice>;
|
|
27
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/cli/prompts.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,cAAc,GAAG,UAAU,GAAG,gBAAgB,CAAC;AACjF,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,IAAI,CAAC;AAsCzC;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,UAAU,CAAC,CAyBhE;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAyB3D;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BjF;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,eAAe,CAAC;AAEtD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAyBvF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,CAAC,CAyBvB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Interactive prompts for the init command using ink
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { render, Box, Text } from "ink";
|
|
7
|
+
import SelectInput from "ink-select-input";
|
|
8
|
+
function SelectPrompt({ message, items, onSelect }) {
|
|
9
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
10
|
+
const handleSelect = (item) => {
|
|
11
|
+
onSelect(item.value);
|
|
12
|
+
};
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "? " }), _jsx(Text, { children: message })] }), _jsx(SelectInput, { items: items, onSelect: handleSelect, onHighlight: (item) => {
|
|
14
|
+
const idx = items.findIndex((i) => i.value === item.value);
|
|
15
|
+
if (idx !== -1)
|
|
16
|
+
setSelectedIndex(idx);
|
|
17
|
+
} }), items[selectedIndex]?.description && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [" ", items[selectedIndex].description] }) }))] }));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Prompt user when snapshot already exists (Case A)
|
|
21
|
+
*/
|
|
22
|
+
export async function promptSnapshotExists() {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const { unmount, waitUntilExit } = render(_jsx(SelectPrompt, { message: "Snapshot already exists. What would you like to do?", items: [
|
|
25
|
+
{
|
|
26
|
+
label: "Skip",
|
|
27
|
+
value: "skip",
|
|
28
|
+
description: "Do nothing and exit",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Reinitialize",
|
|
32
|
+
value: "reinitialize",
|
|
33
|
+
description: "Overwrite snapshot and rebuild migration log from existing migrations",
|
|
34
|
+
},
|
|
35
|
+
], onSelect: (value) => {
|
|
36
|
+
unmount();
|
|
37
|
+
resolve(value);
|
|
38
|
+
} }));
|
|
39
|
+
waitUntilExit();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Prompt user for fresh init when no migrations exist (Case C)
|
|
44
|
+
*/
|
|
45
|
+
export async function promptFreshInit() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const { unmount, waitUntilExit } = render(_jsx(SelectPrompt, { message: "No migrations found. What would you like to do?", items: [
|
|
48
|
+
{
|
|
49
|
+
label: "Baseline only",
|
|
50
|
+
value: "baseline",
|
|
51
|
+
description: "Create snapshot only - use when database already matches schema",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Create initial migration",
|
|
55
|
+
value: "create_initial",
|
|
56
|
+
description: "Create snapshot + initial migration - use when database is empty",
|
|
57
|
+
},
|
|
58
|
+
], onSelect: (value) => {
|
|
59
|
+
unmount();
|
|
60
|
+
resolve(value);
|
|
61
|
+
} }));
|
|
62
|
+
waitUntilExit();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Prompt user to confirm overwriting existing files during pull
|
|
67
|
+
*/
|
|
68
|
+
export async function promptPullConfirm(existingFiles) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const { unmount, waitUntilExit } = render(_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 " }), _jsx(Text, { children: "Existing files will be affected. Continue?" })] }), _jsx(SelectPrompt, { message: "", items: [
|
|
71
|
+
{
|
|
72
|
+
label: "No, abort",
|
|
73
|
+
value: "no",
|
|
74
|
+
description: "Cancel and keep existing files",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: "Yes, continue",
|
|
78
|
+
value: "yes",
|
|
79
|
+
description: "Overwrite schema file (migrations will not be deleted)",
|
|
80
|
+
},
|
|
81
|
+
], onSelect: (value) => {
|
|
82
|
+
unmount();
|
|
83
|
+
resolve(value === "yes");
|
|
84
|
+
} })] }));
|
|
85
|
+
waitUntilExit();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Prompt user to disambiguate a potential table rename
|
|
90
|
+
*/
|
|
91
|
+
export async function promptTableRename(from, to) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const { unmount, waitUntilExit } = render(_jsx(SelectPrompt, { message: `Table "${from}" was removed and "${to}" was added. Is this a rename?`, items: [
|
|
94
|
+
{
|
|
95
|
+
label: `Rename "${from}" to "${to}"`,
|
|
96
|
+
value: "rename",
|
|
97
|
+
description: "Preserve data by renaming the table",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: `Delete "${from}" and create "${to}"`,
|
|
101
|
+
value: "delete_create",
|
|
102
|
+
description: "Drop the old table and create a new one (data will be lost)",
|
|
103
|
+
},
|
|
104
|
+
], onSelect: (value) => {
|
|
105
|
+
unmount();
|
|
106
|
+
resolve(value);
|
|
107
|
+
} }));
|
|
108
|
+
waitUntilExit();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Prompt user to disambiguate a potential column rename
|
|
113
|
+
*/
|
|
114
|
+
export async function promptColumnRename(table, from, to) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const { unmount, waitUntilExit } = render(_jsx(SelectPrompt, { message: `Column "${from}" was removed and "${to}" was added in table "${table}". Is this a rename?`, items: [
|
|
117
|
+
{
|
|
118
|
+
label: `Rename "${from}" to "${to}"`,
|
|
119
|
+
value: "rename",
|
|
120
|
+
description: "Preserve data by renaming the column",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: `Delete "${from}" and create "${to}"`,
|
|
124
|
+
value: "delete_create",
|
|
125
|
+
description: "Drop the old column and create a new one (data will be lost)",
|
|
126
|
+
},
|
|
127
|
+
], onSelect: (value) => {
|
|
128
|
+
unmount();
|
|
129
|
+
resolve(value);
|
|
130
|
+
} }));
|
|
131
|
+
waitUntilExit();
|
|
132
|
+
});
|
|
133
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zenstack-kit CLI - Database tooling for ZenStack schemas
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* migrate:generate Generate a new SQL migration
|
|
7
|
+
* migrate:apply Apply pending migrations
|
|
8
|
+
* init Initialize snapshot from existing schema
|
|
9
|
+
* pull Introspect database and generate schema
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"}
|