prisma-flow 0.1.0 → 0.2.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.
Files changed (92) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +2133 -708
  3. package/package.json +10 -23
  4. package/public/404/index.html +1 -1
  5. package/public/404.html +1 -1
  6. package/public/__next.__PAGE__.txt +4 -4
  7. package/public/__next._full.txt +12 -12
  8. package/public/__next._head.txt +4 -4
  9. package/public/__next._index.txt +5 -5
  10. package/public/__next._tree.txt +2 -2
  11. package/public/_next/static/chunks/056iufrwvl369.js +1 -0
  12. package/public/_next/static/chunks/{0ov8yxc8yngji.js → 0acm34w22hx9z.js} +1 -1
  13. package/public/_next/static/chunks/{0drl6u3r7627z.js → 0c984jinxm8f6.js} +1 -1
  14. package/public/_next/static/chunks/{031d4hdncmqdj.js → 0e0fl5qcvu-yx.js} +1 -1
  15. package/public/_next/static/chunks/0z1o-405295er.js +1 -0
  16. package/public/_next/static/chunks/{0p4y8c2fq43wu.js → 1gswx446-o00_.js} +1 -1
  17. package/public/_next/static/chunks/1te0x9fwhrf60.js +1 -0
  18. package/public/_next/static/chunks/25hh9nwx-v3c5.css +1 -0
  19. package/public/_next/static/chunks/25o46h8mdjlrg.js +1 -0
  20. package/public/_next/static/chunks/{0m7w4t_yx6o6a.js → 2ltjmp_97014r.js} +1 -1
  21. package/public/_next/static/chunks/{3cdvjzy78ylzj.js → 2nzee6y5-2q17.js} +1 -1
  22. package/public/_next/static/chunks/{3xrarg7xfb7_g.js → 2yb298owf9iw_.js} +1 -1
  23. package/public/_next/static/chunks/{05-c3ty_6dwfk.js → 3au-rj2yh4vzs.js} +1 -1
  24. package/public/_next/static/chunks/{2o1jw19xgqkzz.js → 3diwb2iuom_tm.js} +3 -3
  25. package/public/_next/static/chunks/3vw-_f96ru14r.js +1 -0
  26. package/public/_next/static/chunks/3zh2lc2jn3p3h.js +1 -0
  27. package/public/_next/static/chunks/{turbopack-0r_p3gsi1hu9t.js → turbopack-15dmj_1idpm1l.js} +1 -1
  28. package/public/_not-found/__next._full.txt +10 -10
  29. package/public/_not-found/__next._head.txt +4 -4
  30. package/public/_not-found/__next._index.txt +5 -5
  31. package/public/_not-found/{__next._not-found/__PAGE__.txt → __next._not-found.__PAGE__.txt} +2 -2
  32. package/public/_not-found/__next._not-found.txt +3 -3
  33. package/public/_not-found/__next._tree.txt +2 -2
  34. package/public/_not-found/index.html +1 -1
  35. package/public/_not-found/index.txt +10 -10
  36. package/public/drift/__next._full.txt +12 -12
  37. package/public/drift/__next._head.txt +4 -4
  38. package/public/drift/__next._index.txt +5 -5
  39. package/public/drift/__next._tree.txt +2 -2
  40. package/public/drift/__next.drift.__PAGE__.txt +9 -0
  41. package/public/drift/__next.drift.txt +3 -3
  42. package/public/drift/index.html +1 -1
  43. package/public/drift/index.txt +12 -12
  44. package/public/index.html +1 -1
  45. package/public/index.txt +12 -12
  46. package/public/migrations/__next._full.txt +12 -12
  47. package/public/migrations/__next._head.txt +4 -4
  48. package/public/migrations/__next._index.txt +5 -5
  49. package/public/migrations/__next._tree.txt +2 -2
  50. package/public/migrations/__next.migrations.__PAGE__.txt +9 -0
  51. package/public/migrations/__next.migrations.txt +3 -3
  52. package/public/migrations/index.html +1 -1
  53. package/public/migrations/index.txt +12 -12
  54. package/public/risks/__next._full.txt +12 -12
  55. package/public/risks/__next._head.txt +4 -4
  56. package/public/risks/__next._index.txt +5 -5
  57. package/public/risks/__next._tree.txt +2 -2
  58. package/public/risks/__next.risks.__PAGE__.txt +9 -0
  59. package/public/risks/__next.risks.txt +3 -3
  60. package/public/risks/index.html +1 -1
  61. package/public/risks/index.txt +12 -12
  62. package/public/schema/__next._full.txt +12 -12
  63. package/public/schema/__next._head.txt +4 -4
  64. package/public/schema/__next._index.txt +5 -5
  65. package/public/schema/__next._tree.txt +2 -2
  66. package/public/schema/__next.schema.__PAGE__.txt +9 -0
  67. package/public/schema/__next.schema.txt +3 -3
  68. package/public/schema/index.html +1 -1
  69. package/public/schema/index.txt +12 -12
  70. package/public/simulate/__next._full.txt +12 -12
  71. package/public/simulate/__next._head.txt +4 -4
  72. package/public/simulate/__next._index.txt +5 -5
  73. package/public/simulate/__next._tree.txt +2 -2
  74. package/public/simulate/__next.simulate.__PAGE__.txt +9 -0
  75. package/public/simulate/__next.simulate.txt +3 -3
  76. package/public/simulate/index.html +1 -1
  77. package/public/simulate/index.txt +12 -12
  78. package/public/_next/static/chunks/03q3r_bvmqrr8.js +0 -2
  79. package/public/_next/static/chunks/0s76k7hq9-wfk.css +0 -1
  80. package/public/_next/static/chunks/0zv5uyi-a4e2y.js +0 -1
  81. package/public/_next/static/chunks/1kk4shlfeass1.js +0 -1
  82. package/public/_next/static/chunks/2nykiepra7i1k.js +0 -1
  83. package/public/_next/static/chunks/2ohti557qol0g.js +0 -1
  84. package/public/_next/static/chunks/3w1qpcrpp5s4-.js +0 -2
  85. package/public/drift/__next.drift/__PAGE__.txt +0 -9
  86. package/public/migrations/__next.migrations/__PAGE__.txt +0 -9
  87. package/public/risks/__next.risks/__PAGE__.txt +0 -9
  88. package/public/schema/__next.schema/__PAGE__.txt +0 -9
  89. package/public/simulate/__next.simulate/__PAGE__.txt +0 -9
  90. /package/public/_next/static/{ySSMgkts9h6_mRnScxk9f → 0mXb1HP7_wVjSa11pmE1C}/_buildManifest.js +0 -0
  91. /package/public/_next/static/{ySSMgkts9h6_mRnScxk9f → 0mXb1HP7_wVjSa11pmE1C}/_clientMiddlewareManifest.js +0 -0
  92. /package/public/_next/static/{ySSMgkts9h6_mRnScxk9f → 0mXb1HP7_wVjSa11pmE1C}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command14 } from "commander";
4
+ import { createRequire as createRequire2 } from "module";
5
+ import { Command as Command15 } from "commander";
5
6
 
6
7
  // src/commands/check.ts
7
8
  import chalk from "chalk";
@@ -22,10 +23,10 @@ function makeConsoleLogger(level) {
22
23
  error: 50
23
24
  };
24
25
  const activeLevel = levels[level] ?? 30;
25
- function write(severity, lvl, args) {
26
+ function write(severity, lvl, args2) {
26
27
  if (lvl < activeLevel) return;
27
- const obj = typeof args[0] === "object" && args[0] !== null ? args[0] : {};
28
- const msg = typeof args[0] === "string" ? args[0] : args[1] ?? "";
28
+ const obj = typeof args2[0] === "object" && args2[0] !== null ? args2[0] : {};
29
+ const msg = typeof args2[0] === "string" ? args2[0] : args2[1] ?? "";
29
30
  const line = JSON.stringify({
30
31
  time: (/* @__PURE__ */ new Date()).toISOString(),
31
32
  level: severity,
@@ -115,71 +116,22 @@ async function writeAuditEntry(cwd, action, result, detail, maxMb = DEFAULT_MAX_
115
116
  // src/core/migration-analyzer.ts
116
117
  import fs4 from "fs/promises";
117
118
 
118
- // src/core/prisma-cli.ts
119
- import { execFile } from "child_process";
119
+ // src/core/prisma-detector.ts
120
120
  import fs2 from "fs/promises";
121
121
  import path2 from "path";
122
- import { promisify } from "util";
123
- var execFileAsync = promisify(execFile);
124
- async function fileExists(filePath) {
125
- try {
126
- await fs2.access(filePath);
127
- return true;
128
- } catch {
129
- return false;
130
- }
131
- }
132
- async function resolveLocalPrismaCli(cwd) {
133
- let current = path2.resolve(cwd);
134
- while (true) {
135
- const candidate = path2.join(current, "node_modules", "prisma", "build", "index.js");
136
- if (await fileExists(candidate)) return candidate;
137
- const parent = path2.dirname(current);
138
- if (parent === current) return null;
139
- current = parent;
140
- }
141
- }
142
- async function resolveBundledNpxCli() {
143
- const candidate = path2.join(
144
- path2.dirname(process.execPath),
145
- "node_modules",
146
- "npm",
147
- "bin",
148
- "npx-cli.js"
149
- );
150
- return await fileExists(candidate) ? candidate : null;
122
+ import dotenv from "dotenv";
123
+ function resolveSqliteFilePath(databaseUrl, schemaPath) {
124
+ if (!databaseUrl.startsWith("file:")) return null;
125
+ const rawPath = databaseUrl.slice("file:".length);
126
+ if (!rawPath || rawPath === ":memory:") return rawPath;
127
+ if (path2.isAbsolute(rawPath)) return rawPath;
128
+ const schemaDir = path2.dirname(schemaPath);
129
+ return path2.resolve(schemaDir, rawPath);
151
130
  }
152
- async function execPrisma(cwd, args, options = {}) {
153
- const localCli = await resolveLocalPrismaCli(cwd);
154
- if (localCli) {
155
- return execFileAsync(process.execPath, [localCli, ...args], {
156
- cwd,
157
- env: options.env,
158
- timeout: options.timeout
159
- });
160
- }
161
- if (process.platform === "win32") {
162
- const npxCli = await resolveBundledNpxCli();
163
- if (npxCli) {
164
- return execFileAsync(process.execPath, [npxCli, "prisma", ...args], {
165
- cwd,
166
- env: options.env,
167
- timeout: options.timeout
168
- });
169
- }
170
- }
171
- const command = process.platform === "win32" ? "npx.cmd" : "npx";
172
- return execFileAsync(command, ["prisma", ...args], {
173
- cwd,
174
- env: options.env,
175
- timeout: options.timeout
176
- });
131
+ function normalizeDatabaseUrlForPrismaCommand(databaseUrl, schemaPath) {
132
+ const sqlitePath = resolveSqliteFilePath(databaseUrl, schemaPath);
133
+ return sqlitePath ? `file:${sqlitePath}` : databaseUrl;
177
134
  }
178
-
179
- // src/core/prisma-detector.ts
180
- import fs3 from "fs/promises";
181
- import path3 from "path";
182
- import dotenv from "dotenv";
183
135
  function detectProviderFromSchema(schemaContent) {
184
136
  const match = schemaContent.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"/s);
185
137
  if (!match) return null;
@@ -196,20 +148,20 @@ function detectProviderFromSchema(schemaContent) {
196
148
  }
197
149
  async function tryAccess(p) {
198
150
  try {
199
- await fs3.access(p);
151
+ await fs2.access(p);
200
152
  return true;
201
153
  } catch {
202
154
  return false;
203
155
  }
204
156
  }
205
157
  async function findSchemaPath(cwd) {
206
- const candidates = [path3.join(cwd, "prisma", "schema.prisma"), path3.join(cwd, "schema.prisma")];
158
+ const candidates = [path2.join(cwd, "prisma", "schema.prisma"), path2.join(cwd, "schema.prisma")];
207
159
  for (const p of candidates) {
208
160
  if (await tryAccess(p)) return p;
209
161
  }
210
- const schemaDir = path3.join(cwd, "prisma", "schema");
162
+ const schemaDir = path2.join(cwd, "prisma", "schema");
211
163
  if (await tryAccess(schemaDir)) {
212
- const entries = await fs3.readdir(schemaDir).catch(() => []);
164
+ const entries = await fs2.readdir(schemaDir).catch(() => []);
213
165
  const prismaFiles = entries.filter((e) => e.endsWith(".prisma"));
214
166
  if (prismaFiles.length > 0) {
215
167
  return schemaDir;
@@ -219,15 +171,15 @@ async function findSchemaPath(cwd) {
219
171
  }
220
172
  async function readSchemaContent(schemaPath) {
221
173
  try {
222
- const stat = await fs3.stat(schemaPath);
174
+ const stat = await fs2.stat(schemaPath);
223
175
  if (stat.isDirectory()) {
224
- const files = (await fs3.readdir(schemaPath)).filter((f) => f.endsWith(".prisma")).sort();
176
+ const files = (await fs2.readdir(schemaPath)).filter((f) => f.endsWith(".prisma")).sort();
225
177
  const parts = await Promise.all(
226
- files.map((f) => fs3.readFile(path3.join(schemaPath, f), "utf-8"))
178
+ files.map((f) => fs2.readFile(path2.join(schemaPath, f), "utf-8"))
227
179
  );
228
180
  return parts.join("\n");
229
181
  }
230
- return fs3.readFile(schemaPath, "utf-8");
182
+ return fs2.readFile(schemaPath, "utf-8");
231
183
  } catch {
232
184
  return "";
233
185
  }
@@ -240,13 +192,13 @@ async function detectPackageManager(cwd) {
240
192
  { file: "bun.lockb", manager: "bun" }
241
193
  ];
242
194
  for (const { file, manager } of lockfiles) {
243
- if (await tryAccess(path3.join(cwd, file))) return manager;
195
+ if (await tryAccess(path2.join(cwd, file))) return manager;
244
196
  }
245
197
  return null;
246
198
  }
247
199
  async function detectPrismaVersion(cwd) {
248
200
  try {
249
- const content = await fs3.readFile(path3.join(cwd, "package.json"), "utf-8");
201
+ const content = await fs2.readFile(path2.join(cwd, "package.json"), "utf-8");
250
202
  const pkg = JSON.parse(content);
251
203
  return pkg.dependencies?.prisma ?? pkg.devDependencies?.prisma ?? null;
252
204
  } catch {
@@ -258,9 +210,9 @@ async function detectPrismaProject(cwd) {
258
210
  if (!schemaPath) return null;
259
211
  const schemaContent = await readSchemaContent(schemaPath);
260
212
  let databaseUrl = "";
261
- for (const envFile of [path3.join(cwd, ".env"), path3.join(cwd, "prisma", ".env")]) {
213
+ for (const envFile of [path2.join(cwd, ".env"), path2.join(cwd, "prisma", ".env")]) {
262
214
  try {
263
- const envContent = await fs3.readFile(envFile, "utf-8");
215
+ const envContent = await fs2.readFile(envFile, "utf-8");
264
216
  const parsed = dotenv.parse(envContent);
265
217
  if (parsed.DATABASE_URL) {
266
218
  databaseUrl = parsed.DATABASE_URL;
@@ -269,15 +221,15 @@ async function detectPrismaProject(cwd) {
269
221
  } catch {
270
222
  }
271
223
  }
272
- const schemaDir = await fs3.stat(schemaPath).then((s) => s.isDirectory()).catch(() => false) ? schemaPath : path3.dirname(schemaPath);
273
- const migrationsPath = path3.join(schemaDir, "migrations");
224
+ const schemaDir = await fs2.stat(schemaPath).then((s) => s.isDirectory()).catch(() => false) ? schemaPath : path2.dirname(schemaPath);
225
+ const migrationsPath = path2.join(schemaDir, "migrations");
274
226
  const migrations = [];
275
227
  try {
276
- const entries = await fs3.readdir(migrationsPath, { withFileTypes: true });
228
+ const entries = await fs2.readdir(migrationsPath, { withFileTypes: true });
277
229
  for (const entry of entries) {
278
230
  if (!entry.isDirectory() || entry.name === "migration_lock.toml") continue;
279
- const migrationDir = path3.join(migrationsPath, entry.name);
280
- const sqlPath = path3.join(migrationDir, "migration.sql");
231
+ const migrationDir = path2.join(migrationsPath, entry.name);
232
+ const sqlPath = path2.join(migrationDir, "migration.sql");
281
233
  if (!await tryAccess(sqlPath)) continue;
282
234
  const timestampStr = entry.name.slice(0, 14);
283
235
  const year = Number.parseInt(timestampStr.slice(0, 4), 10);
@@ -321,27 +273,14 @@ function classifyDriftSql(sql) {
321
273
  const upper = sql.toUpperCase().trimStart();
322
274
  if (upper.startsWith("CREATE TABLE")) return "table-missing";
323
275
  if (upper.startsWith("DROP TABLE")) return "table-extra";
324
- if (upper.startsWith("CREATE INDEX") || upper.startsWith("DROP INDEX")) return "index-change";
276
+ if (upper.startsWith("CREATE INDEX") || upper.startsWith("CREATE UNIQUE INDEX") || upper.startsWith("DROP INDEX")) {
277
+ return "index-change";
278
+ }
325
279
  if (upper.startsWith("ALTER INDEX")) return "index-change";
326
280
  if (upper.includes("CONSTRAINT")) return "constraint-change";
327
281
  if (upper.startsWith("ALTER TABLE")) return "column-mismatch";
328
282
  return "unknown";
329
283
  }
330
- function labelDriftType(type) {
331
- const labels = {
332
- "table-missing": "Table missing in database",
333
- "table-extra": "Table exists in database but not in schema",
334
- "column-mismatch": "Column or table structure mismatch",
335
- "index-change": "Index difference detected",
336
- "constraint-change": "Constraint difference detected",
337
- unknown: "Unknown schema change",
338
- missing_migration: "Migration applied to DB but missing from history",
339
- extra_column: "Extra column present in DB not in schema",
340
- extra_table: "Extra table present in DB not in schema",
341
- modified_migration: "Migration SQL was modified after being applied"
342
- };
343
- return labels[type];
344
- }
345
284
  function parseSqlStatements(sql) {
346
285
  const statements = [];
347
286
  let current = "";
@@ -398,42 +337,620 @@ async function detectDrift(cwd) {
398
337
  errorMessage: "No Prisma project found"
399
338
  };
400
339
  }
340
+ const adapter = getPrismaAdapter(project.prismaVersion);
341
+ return adapter.detectDrift(cwd, project.schemaPath, project.databaseUrl, project.provider);
342
+ }
343
+
344
+ // src/core/prisma-cli.ts
345
+ import { execFile } from "child_process";
346
+ import fs3 from "fs/promises";
347
+ import path3 from "path";
348
+ async function fileExists(filePath) {
349
+ try {
350
+ await fs3.access(filePath);
351
+ return true;
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ async function resolveLocalPrismaCli(cwd) {
357
+ let current = path3.resolve(cwd);
358
+ while (true) {
359
+ const candidate = path3.join(current, "node_modules", "prisma", "build", "index.js");
360
+ if (await fileExists(candidate)) return candidate;
361
+ const parent = path3.dirname(current);
362
+ if (parent === current) return null;
363
+ current = parent;
364
+ }
365
+ }
366
+ async function resolveBundledNpxCli() {
367
+ const candidate = path3.join(
368
+ path3.dirname(process.execPath),
369
+ "node_modules",
370
+ "npm",
371
+ "bin",
372
+ "npx-cli.js"
373
+ );
374
+ return await fileExists(candidate) ? candidate : null;
375
+ }
376
+ function killProcessTree(pid) {
377
+ if (process.platform === "win32") {
378
+ const child = execFile("taskkill.exe", ["/pid", String(pid), "/T", "/F"]);
379
+ child.on("error", () => {
380
+ });
381
+ return;
382
+ }
401
383
  try {
402
- const { stdout } = await execPrisma(
384
+ process.kill(pid, "SIGTERM");
385
+ } catch {
386
+ }
387
+ }
388
+ function execFileWithTimeout(command, args2, options) {
389
+ return new Promise((resolve, reject) => {
390
+ const child = execFile(command, args2, {
391
+ cwd: options.cwd,
392
+ env: options.env,
393
+ windowsHide: true,
394
+ maxBuffer: 10 * 1024 * 1024
395
+ });
396
+ let stdout = "";
397
+ let stderr = "";
398
+ let timedOut = false;
399
+ child.stdout?.setEncoding("utf-8");
400
+ child.stderr?.setEncoding("utf-8");
401
+ child.stdout?.on("data", (chunk) => {
402
+ stdout += chunk;
403
+ });
404
+ child.stderr?.on("data", (chunk) => {
405
+ stderr += chunk;
406
+ });
407
+ const timer = options.timeout && options.timeout > 0 ? setTimeout(() => {
408
+ timedOut = true;
409
+ if (child.pid) killProcessTree(child.pid);
410
+ }, options.timeout) : null;
411
+ child.on("error", (error) => {
412
+ if (timer) clearTimeout(timer);
413
+ reject(error);
414
+ });
415
+ child.on("close", (code, signal) => {
416
+ if (timer) clearTimeout(timer);
417
+ if (timedOut) {
418
+ const timeoutError = new Error(
419
+ `Prisma CLI timed out after ${options.timeout}ms: ${command} ${args2.join(" ")}`
420
+ );
421
+ timeoutError.code = "ETIMEDOUT";
422
+ timeoutError.stdout = stdout;
423
+ timeoutError.stderr = stderr;
424
+ timeoutError.signal = signal;
425
+ reject(timeoutError);
426
+ return;
427
+ }
428
+ if (code && code !== 0) {
429
+ const commandError = new Error(
430
+ stderr.trim() || `Prisma CLI exited with code ${code}`
431
+ );
432
+ commandError.code = code;
433
+ commandError.stdout = stdout;
434
+ commandError.stderr = stderr;
435
+ reject(commandError);
436
+ return;
437
+ }
438
+ resolve({ stdout, stderr });
439
+ });
440
+ });
441
+ }
442
+ async function execPrisma(cwd, args2, options = {}) {
443
+ const localCli = await resolveLocalPrismaCli(cwd);
444
+ if (localCli) {
445
+ return execFileWithTimeout(process.execPath, [localCli, ...args2], {
446
+ cwd,
447
+ env: options.env,
448
+ timeout: options.timeout
449
+ });
450
+ }
451
+ if (process.platform === "win32") {
452
+ const npxCli = await resolveBundledNpxCli();
453
+ if (npxCli) {
454
+ return execFileWithTimeout(process.execPath, [npxCli, "prisma", ...args2], {
455
+ cwd,
456
+ env: options.env,
457
+ timeout: options.timeout
458
+ });
459
+ }
460
+ }
461
+ const command = process.platform === "win32" ? "npx.cmd" : "npx";
462
+ return execFileWithTimeout(command, ["prisma", ...args2], {
463
+ cwd,
464
+ env: options.env,
465
+ timeout: options.timeout
466
+ });
467
+ }
468
+ function hasUnsupportedMigrateDiffFlag(error) {
469
+ if (!(error instanceof Error)) return false;
470
+ const commandError = error;
471
+ const message = `${error.message}
472
+ ${commandError.stderr ?? ""}`.toLowerCase();
473
+ return message.includes("was removed") || message.includes("unknown option") || message.includes("unexpected option");
474
+ }
475
+ async function execPrismaMigrateDiff(cwd, { fromSchema, legacyDestinationArgs, env, timeout }) {
476
+ const options = {
477
+ ...env === void 0 ? {} : { env },
478
+ ...timeout === void 0 ? {} : { timeout }
479
+ };
480
+ try {
481
+ return await execPrisma(
482
+ cwd,
483
+ ["migrate", "diff", "--from-schema", fromSchema, "--to-config-datasource", "--script"],
484
+ options
485
+ );
486
+ } catch (error) {
487
+ if (!hasUnsupportedMigrateDiffFlag(error)) throw error;
488
+ return execPrisma(
403
489
  cwd,
404
490
  [
405
491
  "migrate",
406
492
  "diff",
407
493
  "--from-schema-datamodel",
408
- project.schemaPath,
409
- "--to-schema-datasource",
410
- project.schemaPath,
494
+ fromSchema,
495
+ ...legacyDestinationArgs,
411
496
  "--script"
412
497
  ],
413
- { timeout: 3e4 }
498
+ options
414
499
  );
415
- const output = stdout.trim();
416
- if (!output) return { items: [], status: "clean" };
417
- const items = parseSqlStatements(output).filter((s) => !s.toUpperCase().trimStart().startsWith("--")).map((sql) => {
418
- const type = classifyDriftSql(sql);
419
- return { sql, type, description: labelDriftType(type) };
420
- });
421
- return { items, status: items.length > 0 ? "drifted" : "clean" };
500
+ }
501
+ }
502
+
503
+ // src/core/adapters/base-adapter.ts
504
+ function labelDriftType(type) {
505
+ const labels = {
506
+ "table-missing": "Table missing in database",
507
+ "table-extra": "Table exists in database but not in schema",
508
+ "column-mismatch": "Column or table structure mismatch",
509
+ "index-change": "Index difference detected",
510
+ "constraint-change": "Constraint difference detected",
511
+ unknown: "Unknown schema change",
512
+ missing_migration: "Migration applied to DB but missing from history",
513
+ extra_column: "Extra column present in DB not in schema",
514
+ extra_table: "Extra table present in DB not in schema",
515
+ modified_migration: "Migration SQL was modified after being applied"
516
+ };
517
+ return labels[type];
518
+ }
519
+ function parseStatusOutput(stdout, stderr, options) {
520
+ const statusMap = /* @__PURE__ */ new Map();
521
+ const combined = `${stdout}
522
+ ${stderr}`;
523
+ const isExitZero = options?.isExitZero ?? false;
524
+ if (combined.includes("P1001") || combined.includes("Can't reach database server") || combined.includes("Connection refused") || combined.includes("ECONNREFUSED") || combined.includes("ENOTFOUND")) {
525
+ return {
526
+ verification: "error",
527
+ connected: false,
528
+ statusMap,
529
+ errorCode: "DATABASE_UNREACHABLE",
530
+ errorMessage: "Database server is unreachable. Check DATABASE_URL."
531
+ };
532
+ }
533
+ if (combined.includes("P1000") || combined.includes("Authentication failed") || combined.includes("password authentication failed")) {
534
+ return {
535
+ verification: "error",
536
+ connected: false,
537
+ statusMap,
538
+ errorCode: "AUTHENTICATION_FAILED",
539
+ errorMessage: "Database credentials failed authentication."
540
+ };
541
+ }
542
+ if (combined.includes("P1012") || combined.includes("Schema validation error") || combined.includes("Error validating")) {
543
+ return {
544
+ verification: "error",
545
+ connected: false,
546
+ statusMap,
547
+ errorCode: "MALFORMED_SCHEMA",
548
+ errorMessage: "Prisma schema validation failed. Check schema syntax."
549
+ };
550
+ }
551
+ if (combined.includes("P1013") || combined.includes("Invalid configuration") || combined.includes("Invalid database string")) {
552
+ return {
553
+ verification: "error",
554
+ connected: false,
555
+ statusMap,
556
+ errorCode: "INVALID_CONFIG",
557
+ errorMessage: "Invalid Prisma configuration or database URL."
558
+ };
559
+ }
560
+ if (combined.includes("P3005") || combined.includes("not in sync with the migration history") || combined.includes("recorded in the database but missing locally")) {
561
+ return {
562
+ verification: "unknown",
563
+ connected: true,
564
+ statusMap,
565
+ errorCode: "MIGRATION_HISTORY_CONFLICT",
566
+ errorMessage: "Database schema is not in sync with local migration history."
567
+ };
568
+ }
569
+ const trimmedStderr = stderr.trim();
570
+ if (!isExitZero && trimmedStderr.length > 0) {
571
+ if (trimmedStderr.toLowerCase().includes("error") || trimmedStderr.toLowerCase().includes("failure") || trimmedStderr.toLowerCase().includes("fault") || trimmedStderr.toLowerCase().includes("panic") || trimmedStderr.toLowerCase().includes("unknown")) {
572
+ return {
573
+ verification: "unknown",
574
+ connected: false,
575
+ statusMap,
576
+ errorCode: "UNEXPECTED_CLI_FAILURE",
577
+ errorMessage: trimmedStderr
578
+ };
579
+ }
580
+ }
581
+ const lines = stdout.split("\n");
582
+ let mode = "none";
583
+ for (const rawLine of lines) {
584
+ const line = rawLine.trim();
585
+ if (line.includes("have not yet been applied")) {
586
+ mode = "pending";
587
+ continue;
588
+ }
589
+ if (line.match(/failed to apply|rolled back|migration.*failed/i)) {
590
+ mode = "failed";
591
+ continue;
592
+ }
593
+ if (line.includes("Database schema is up to date") || line.includes("No pending migrations")) {
594
+ if (!isExitZero && trimmedStderr.length > 0) {
595
+ return {
596
+ verification: "unknown",
597
+ connected: false,
598
+ statusMap,
599
+ errorCode: "UNEXPECTED_CLI_FAILURE",
600
+ errorMessage: trimmedStderr || "Prisma CLI exited non-zero with clean stdout."
601
+ };
602
+ }
603
+ mode = "none";
604
+ continue;
605
+ }
606
+ if (line === "" || line.startsWith("\u2500") || line.startsWith("The following")) {
607
+ mode = "none";
608
+ continue;
609
+ }
610
+ if (mode === "none") continue;
611
+ const cleaned = line.replace(/^[•\-*]\s*/, "").trim();
612
+ if (cleaned.match(/^\d{14}/)) {
613
+ statusMap.set(cleaned, mode === "pending" ? "pending" : "failed");
614
+ }
615
+ }
616
+ if (statusMap.size > 0) {
617
+ return {
618
+ verification: "verified",
619
+ connected: true,
620
+ statusMap
621
+ };
622
+ }
623
+ if (isExitZero) {
624
+ return {
625
+ verification: "verified",
626
+ connected: true,
627
+ statusMap
628
+ };
629
+ }
630
+ return {
631
+ verification: "unknown",
632
+ connected: false,
633
+ statusMap,
634
+ errorCode: "UNEXPECTED_CLI_FAILURE",
635
+ errorMessage: trimmedStderr || stdout.trim() || "Prisma CLI exited unexpectedly with unknown status."
636
+ };
637
+ }
638
+ async function runPrismaMigrateStatus(cwd, schemaPath) {
639
+ try {
640
+ const { stdout, stderr } = await execPrisma(
641
+ cwd,
642
+ ["migrate", "status", "--schema", schemaPath],
643
+ {
644
+ timeout: 3e4
645
+ }
646
+ );
647
+ return parseStatusOutput(stdout, stderr, { isExitZero: true });
422
648
  } catch (err) {
423
649
  const error = err;
650
+ const stdout = error.stdout ?? "";
424
651
  const stderr = error.stderr ?? "";
425
- if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
426
- logger.debug("Drift detection skipped: database unreachable");
427
- return {
428
- items: [],
429
- status: "error",
430
- errorMessage: "Database unreachable"
431
- };
652
+ logger.debug({ stdout, stderr }, "prisma migrate status returned non-zero");
653
+ return parseStatusOutput(stdout, stderr, { isExitZero: false });
654
+ }
655
+ }
656
+ function parseDriftOutput(stdout) {
657
+ const output = stdout.trim();
658
+ if (!output) return [];
659
+ return parseSqlStatements(output).filter((s) => !s.toUpperCase().trimStart().startsWith("--")).map((sql) => {
660
+ const type = classifyDriftSql(sql);
661
+ return { sql, type, description: labelDriftType(type) };
662
+ });
663
+ }
664
+
665
+ // src/core/adapters/legacy-adapter.ts
666
+ var PrismaLegacyAdapter = class {
667
+ constructor(version) {
668
+ this.version = version;
669
+ }
670
+ generation = "legacy";
671
+ getCapabilities() {
672
+ return {
673
+ supportsClassicMigrationSql: true,
674
+ supportsStructuredMigrationPlan: false,
675
+ supportsContractModel: false,
676
+ supportsDrift: true,
677
+ supportsRollbackPlanning: true,
678
+ supportsExecutedSimulation: true,
679
+ supportsMigrationHistory: true,
680
+ isProductionSupported: true,
681
+ isExperimental: false
682
+ };
683
+ }
684
+ async getMigrationStatus(cwd, schemaPath) {
685
+ return runPrismaMigrateStatus(cwd, schemaPath);
686
+ }
687
+ async detectDrift(cwd, schemaPath, databaseUrl, _provider) {
688
+ try {
689
+ const env = databaseUrl ? { ...process.env, DATABASE_URL: databaseUrl } : process.env;
690
+ const { stdout } = await execPrisma(
691
+ cwd,
692
+ [
693
+ "migrate",
694
+ "diff",
695
+ "--from-schema-datamodel",
696
+ schemaPath,
697
+ "--to-schema-datasource",
698
+ schemaPath,
699
+ "--script"
700
+ ],
701
+ { env, timeout: 3e4 }
702
+ );
703
+ const items = parseDriftOutput(stdout);
704
+ return { items, status: items.length > 0 ? "drifted" : "clean" };
705
+ } catch (err) {
706
+ const error = err;
707
+ const stderr = error.stderr ?? "";
708
+ if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
709
+ logger.debug("Drift detection skipped: database unreachable");
710
+ return { items: [], status: "error", errorMessage: "Database unreachable" };
711
+ }
712
+ const message = error instanceof Error ? error.message : String(error);
713
+ logger.warn({ err }, "Prisma legacy drift detection failed");
714
+ return { items: [], status: "error", errorMessage: message };
715
+ }
716
+ }
717
+ };
718
+
719
+ // src/core/adapters/prisma7-adapter.ts
720
+ var Prisma7Adapter = class {
721
+ constructor(version) {
722
+ this.version = version;
723
+ }
724
+ generation = "prisma7";
725
+ getCapabilities() {
726
+ return {
727
+ supportsClassicMigrationSql: true,
728
+ supportsStructuredMigrationPlan: false,
729
+ supportsContractModel: false,
730
+ supportsDrift: true,
731
+ supportsRollbackPlanning: true,
732
+ supportsExecutedSimulation: true,
733
+ supportsMigrationHistory: true,
734
+ isProductionSupported: true,
735
+ isExperimental: false
736
+ };
737
+ }
738
+ async getMigrationStatus(cwd, schemaPath) {
739
+ return runPrismaMigrateStatus(cwd, schemaPath);
740
+ }
741
+ async detectDrift(cwd, schemaPath, databaseUrl, _provider) {
742
+ try {
743
+ const env = databaseUrl ? { ...process.env, DATABASE_URL: databaseUrl } : process.env;
744
+ const { stdout } = await execPrisma(
745
+ cwd,
746
+ ["migrate", "diff", "--from-schema", schemaPath, "--to-config-datasource", "--script"],
747
+ { env, timeout: 3e4 }
748
+ );
749
+ const items = parseDriftOutput(stdout);
750
+ return { items, status: items.length > 0 ? "drifted" : "clean" };
751
+ } catch (err) {
752
+ const error = err;
753
+ const stderr = error.stderr ?? "";
754
+ if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
755
+ logger.debug("Drift detection skipped: database unreachable");
756
+ return { items: [], status: "error", errorMessage: "Database unreachable" };
757
+ }
758
+ const message = error instanceof Error ? error.message : String(error);
759
+ logger.warn({ err }, "Prisma 7 drift detection failed");
760
+ return { items: [], status: "error", errorMessage: message };
761
+ }
762
+ }
763
+ };
764
+
765
+ // src/core/adapters/prisma8-adapter.ts
766
+ var Prisma8Adapter = class {
767
+ constructor(version) {
768
+ this.version = version;
769
+ }
770
+ generation = "prisma8";
771
+ getCapabilities() {
772
+ return {
773
+ supportsClassicMigrationSql: false,
774
+ supportsStructuredMigrationPlan: false,
775
+ supportsContractModel: false,
776
+ supportsDrift: false,
777
+ supportsRollbackPlanning: false,
778
+ supportsExecutedSimulation: false,
779
+ supportsMigrationHistory: false,
780
+ isProductionSupported: false,
781
+ isExperimental: true
782
+ };
783
+ }
784
+ async getMigrationStatus(_cwd, _schemaPath) {
785
+ return {
786
+ verification: "unknown",
787
+ connected: false,
788
+ statusMap: /* @__PURE__ */ new Map(),
789
+ errorCode: "PRISMA8_EXPERIMENTAL_UNSUPPORTED",
790
+ errorMessage: "Prisma 8 migration verification is experimental and not supported for deployment readiness yet."
791
+ };
792
+ }
793
+ async detectDrift(_cwd, _schemaPath, _databaseUrl, _provider) {
794
+ return {
795
+ items: [],
796
+ status: "not_checked",
797
+ errorMessage: "Prisma 8 drift detection is experimental and not supported for deployment readiness yet."
798
+ };
799
+ }
800
+ };
801
+
802
+ // src/core/adapters/unsupported-adapter.ts
803
+ var UnsupportedPrismaAdapter = class {
804
+ constructor(version) {
805
+ this.version = version;
806
+ }
807
+ generation = "unknown";
808
+ getCapabilities() {
809
+ return {
810
+ supportsClassicMigrationSql: false,
811
+ supportsStructuredMigrationPlan: false,
812
+ supportsContractModel: false,
813
+ supportsDrift: false,
814
+ supportsRollbackPlanning: false,
815
+ supportsExecutedSimulation: false,
816
+ supportsMigrationHistory: false,
817
+ isProductionSupported: false,
818
+ isExperimental: false
819
+ };
820
+ }
821
+ async getMigrationStatus(_cwd, _schemaPath) {
822
+ return {
823
+ verification: "unknown",
824
+ connected: false,
825
+ statusMap: /* @__PURE__ */ new Map(),
826
+ errorCode: "UNSUPPORTED_PRISMA_VERSION",
827
+ errorMessage: this.version ? `Prisma version "${this.version}" is not supported. Supported versions: Prisma 5, 6, and 7.` : "Prisma version could not be detected or is not supported. Supported versions: Prisma 5, 6, and 7."
828
+ };
829
+ }
830
+ async detectDrift(_cwd, _schemaPath, _databaseUrl, _provider) {
831
+ return {
832
+ items: [],
833
+ status: "not_checked",
834
+ errorMessage: this.version ? `Drift detection is not supported for Prisma version "${this.version}".` : "Drift detection requires a supported Prisma version (5, 6, or 7)."
835
+ };
836
+ }
837
+ };
838
+
839
+ // src/core/adapters/adapter-factory.ts
840
+ function getPrismaAdapter(version) {
841
+ if (!version || typeof version !== "string" || !version.trim()) {
842
+ return new UnsupportedPrismaAdapter(version ?? null);
843
+ }
844
+ const clean = version.trim().replace(/^[\^~>=<v\s]+/, "");
845
+ const firstPart = clean.split(".")[0] ?? "";
846
+ const major = Number.parseInt(firstPart, 10);
847
+ if (Number.isNaN(major)) {
848
+ return new UnsupportedPrismaAdapter(version);
849
+ }
850
+ if (major === 5 || major === 6) {
851
+ return new PrismaLegacyAdapter(version);
852
+ }
853
+ if (major === 7) {
854
+ return new Prisma7Adapter(version);
855
+ }
856
+ if (major === 8) {
857
+ return new Prisma8Adapter(version);
858
+ }
859
+ return new UnsupportedPrismaAdapter(version);
860
+ }
861
+
862
+ // src/core/readiness.ts
863
+ function calculateHealthScore(input) {
864
+ if (!input.connected || input.migrationVerification === "error") {
865
+ return 0;
866
+ }
867
+ if (input.migrationVerification === "unknown") {
868
+ return 30;
869
+ }
870
+ let score = 100;
871
+ score -= Math.min(35, input.migrationsFailed * 35);
872
+ score -= Math.min(25, input.driftCount * 10);
873
+ score -= Math.min(15, input.migrationsPending * 5);
874
+ score -= Math.min(30, input.migrationsUnknown * 15);
875
+ if (input.driftStatus === "error") score -= 25;
876
+ if (input.hasCriticalRisk || input.maxRiskScore >= 75) score -= 25;
877
+ else if (input.maxRiskScore >= 50) score -= 15;
878
+ else if (input.maxRiskScore >= 20) score -= 5;
879
+ return Math.max(0, Math.min(100, Math.round(score)));
880
+ }
881
+ function evaluateDeploymentReadiness(input) {
882
+ const healthScore = calculateHealthScore(input);
883
+ const hasCriticalRisk = input.hasCriticalRisk || input.maxRiskScore >= 75;
884
+ const isVerified = input.migrationVerification === "verified" && input.migrationsUnknown === 0;
885
+ const checks = [
886
+ {
887
+ id: "database",
888
+ label: "Database reachable",
889
+ passed: input.connected,
890
+ message: input.connected ? "PrismaFlow can reach the configured datasource." : input.errorMessage || "Check DATABASE_URL and database network access before deploying."
891
+ },
892
+ {
893
+ id: "migration-verification",
894
+ label: "Migration state verified",
895
+ passed: isVerified,
896
+ message: isVerified ? "Migration history successfully verified against the database." : input.errorMessage || "Migration state is unknown or unverified. Database state cannot be proven safe."
897
+ },
898
+ {
899
+ id: "drift",
900
+ label: "No schema drift",
901
+ passed: input.driftStatus === "clean" && input.driftCount === 0,
902
+ message: input.driftStatus === "clean" ? "The live database matches the Prisma schema." : input.driftStatus === "drifted" ? `${input.driftCount} drift item${input.driftCount === 1 ? "" : "s"} must be reviewed.` : input.driftStatus === "error" ? "Drift detection failed. Schema differences cannot be verified." : "Drift check not performed (database disconnected or pending migrations exist)."
903
+ },
904
+ {
905
+ id: "failed-migrations",
906
+ label: "No failed migrations",
907
+ passed: isVerified && input.migrationsFailed === 0,
908
+ message: !isVerified ? "Cannot verify failed migration state (verification unproven)." : input.migrationsFailed === 0 ? "Migration history has no failed entries." : `${input.migrationsFailed} failed migration${input.migrationsFailed === 1 ? "" : "s"} need recovery.`
909
+ },
910
+ {
911
+ id: "pending-migrations",
912
+ label: "No pending migrations",
913
+ passed: isVerified && input.migrationsPending === 0,
914
+ message: !isVerified ? "Cannot verify pending migration state (verification unproven)." : input.migrationsPending === 0 ? "All local migrations are applied." : `${input.migrationsPending} migration${input.migrationsPending === 1 ? "" : "s"} still pending.`
915
+ },
916
+ {
917
+ id: "critical-risks",
918
+ label: "No critical migration risks",
919
+ passed: !hasCriticalRisk,
920
+ message: hasCriticalRisk ? "At least one migration contains a critical data-loss operation." : "No critical data-loss operations were detected."
432
921
  }
433
- const message = error instanceof Error ? error.message : String(error);
434
- logger.warn({ err }, "Drift detection failed");
435
- return { items: [], status: "error", errorMessage: message };
922
+ ];
923
+ const blockers = checks.filter(
924
+ (check) => !check.passed && ["database", "migration-verification", "failed-migrations", "critical-risks"].includes(
925
+ check.id
926
+ )
927
+ );
928
+ const hasDriftBlocker = !checks.find((c) => c.id === "drift")?.passed && (input.driftStatus === "drifted" || input.driftStatus === "error");
929
+ const warnings = checks.filter((check) => !check.passed);
930
+ if (blockers.length > 0 || hasDriftBlocker) {
931
+ const summary = !input.connected ? "Blocked: database is unreachable" : !isVerified ? "Blocked: migration verification is unknown or failed" : input.migrationsFailed > 0 ? "Blocked: failed migrations must be resolved" : hasDriftBlocker ? "Blocked: schema drift detected or drift check failed" : "Blocked: critical migration risks detected";
932
+ return {
933
+ status: "blocked",
934
+ score: healthScore,
935
+ summary,
936
+ checks
937
+ };
436
938
  }
939
+ if (warnings.length > 0) {
940
+ const summary = input.migrationsPending > 0 ? "Attention: pending migrations to review before deploying" : "Attention: review warnings before deploying";
941
+ return {
942
+ status: "attention",
943
+ score: healthScore,
944
+ summary,
945
+ checks
946
+ };
947
+ }
948
+ return {
949
+ status: "ready",
950
+ score: healthScore,
951
+ summary: "Ready for deployment",
952
+ checks
953
+ };
437
954
  }
438
955
 
439
956
  // src/core/migration-analyzer.ts
@@ -447,7 +964,7 @@ var RISK_PATTERNS = [
447
964
  weight: 75
448
965
  },
449
966
  {
450
- pattern: /TRUNCATE/i,
967
+ pattern: /TRUNCATE\s+TABLE|TRUNCATE\s+\w+/i,
451
968
  label: "Truncates table \u2014 full data loss",
452
969
  severity: "critical",
453
970
  description: "Removes all rows from a table. Cannot be rolled back with standard SQL.",
@@ -463,7 +980,7 @@ var RISK_PATTERNS = [
463
980
  weight: 60
464
981
  },
465
982
  {
466
- pattern: /ALTER\s+COLUMN.+NOT\s+NULL/i,
983
+ pattern: /ALTER\s+COLUMN.+NOT\s+NULL|ALTER\s+TABLE.+ADD\s+COLUMN.+NOT\s+NULL/i,
467
984
  label: "Adds NOT NULL constraint",
468
985
  severity: "high",
469
986
  description: "Adding a NOT NULL constraint will fail if any existing rows have NULL values.",
@@ -515,142 +1032,29 @@ function extractTableName(sql) {
515
1032
  const match = sql.match(/(?:DROP|TRUNCATE|ALTER)\s+TABLE\s+(?:"?(\w+)"?\.)?"?(\w+)"?/i);
516
1033
  return match?.[2];
517
1034
  }
518
- function analyzeMigrationRisks(sql) {
519
- return RISK_PATTERNS.filter(({ pattern }) => pattern.test(sql)).map(({ label }) => label);
520
- }
521
- function scoreMigrationRisk(sql) {
522
- const matchedPatterns = RISK_PATTERNS.filter(({ pattern }) => pattern.test(sql));
523
- const factors = matchedPatterns.map((p) => {
524
- const affectedTable = extractTableName(sql);
525
- return {
526
- pattern: p.pattern.source,
527
- severity: p.severity,
528
- description: p.description,
529
- ...affectedTable !== void 0 ? { affectedTable } : {},
530
- recommendation: p.recommendation
531
- };
532
- });
533
- const rawScore = matchedPatterns.reduce((acc, p) => acc + p.weight, 0);
534
- const score = Math.min(100, rawScore);
535
- let level = "low";
536
- if (score >= 75) level = "critical";
537
- else if (score >= 50) level = "high";
538
- else if (score >= 20) level = "medium";
539
- return { score, level, factors };
540
- }
541
- function calculateHealthScore(input) {
542
- let score = 100;
543
- if (!input.connected) score -= 35;
544
- score -= Math.min(30, input.migrationsFailed * 25);
545
- score -= Math.min(20, input.driftCount * 10);
546
- score -= Math.min(15, input.migrationsPending * 5);
547
- if (input.maxRiskScore >= 75) score -= 25;
548
- else if (input.maxRiskScore >= 50) score -= 18;
549
- else if (input.maxRiskScore >= 20) score -= 8;
550
- return Math.max(0, Math.min(100, Math.round(score)));
551
- }
552
- function buildDeploymentReadiness(input) {
553
- const hasCriticalRisk = input.maxRiskScore >= 75;
554
- const checks = [
555
- {
556
- id: "database",
557
- label: "Database reachable",
558
- passed: input.connected,
559
- message: input.connected ? "PrismaFlow can reach the configured datasource." : "Check DATABASE_URL and database network access before deploying."
560
- },
561
- {
562
- id: "drift",
563
- label: "No schema drift",
564
- passed: input.driftCount === 0,
565
- message: input.driftCount === 0 ? "The live database matches the Prisma schema." : `${input.driftCount} drift item${input.driftCount === 1 ? "" : "s"} must be reviewed.`
566
- },
567
- {
568
- id: "failed-migrations",
569
- label: "No failed migrations",
570
- passed: input.migrationsFailed === 0,
571
- message: input.migrationsFailed === 0 ? "Migration history has no failed entries." : `${input.migrationsFailed} failed migration${input.migrationsFailed === 1 ? "" : "s"} need recovery.`
572
- },
573
- {
574
- id: "pending-migrations",
575
- label: "No pending migrations",
576
- passed: input.migrationsPending === 0,
577
- message: input.migrationsPending === 0 ? "All local migrations are applied." : `${input.migrationsPending} migration${input.migrationsPending === 1 ? "" : "s"} still pending.`
578
- },
579
- {
580
- id: "critical-risks",
581
- label: "No critical migration risks",
582
- passed: !hasCriticalRisk,
583
- message: hasCriticalRisk ? "At least one migration contains a critical data-loss operation." : "No critical data-loss operations were detected."
584
- }
585
- ];
586
- const blockers = checks.filter(
587
- (check) => !check.passed && ["database", "drift", "failed-migrations", "critical-risks"].includes(check.id)
588
- );
589
- const warnings = checks.filter((check) => !check.passed);
590
- if (blockers.length > 0) {
591
- return {
592
- status: "blocked",
593
- score: input.healthScore,
594
- summary: "Not ready for deployment",
595
- checks
596
- };
597
- }
598
- if (warnings.length > 0) {
1035
+ function analyzeMigrationRisks(sql) {
1036
+ return RISK_PATTERNS.filter(({ pattern }) => pattern.test(sql)).map(({ label }) => label);
1037
+ }
1038
+ function scoreMigrationRisk(sql) {
1039
+ const matchedPatterns = RISK_PATTERNS.filter(({ pattern }) => pattern.test(sql));
1040
+ const factors = matchedPatterns.map((p) => {
1041
+ const affectedTable = extractTableName(sql);
599
1042
  return {
600
- status: "attention",
601
- score: input.healthScore,
602
- summary: "Review pending work before deployment",
603
- checks
1043
+ pattern: p.pattern.source,
1044
+ severity: p.severity,
1045
+ description: p.description,
1046
+ ...affectedTable !== void 0 ? { affectedTable } : {},
1047
+ recommendation: p.recommendation
604
1048
  };
605
- }
606
- return {
607
- status: "ready",
608
- score: input.healthScore,
609
- summary: "Ready for deployment",
610
- checks
611
- };
612
- }
613
- async function getMigrationStatusMap(cwd, schemaPath) {
614
- const map = /* @__PURE__ */ new Map();
615
- let stdout = "";
616
- try {
617
- const result = await execPrisma(cwd, ["migrate", "status", "--schema", schemaPath], {
618
- timeout: 3e4
619
- });
620
- stdout = result.stdout;
621
- return map;
622
- } catch (err) {
623
- const error = err;
624
- stdout = error.stdout ?? "";
625
- const stderr = error.stderr ?? "";
626
- if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
627
- throw new Error("DATABASE_UNREACHABLE");
628
- }
629
- logger.debug({ stdout, stderr }, "prisma migrate status exited non-zero \u2014 parsing output");
630
- }
631
- const lines = stdout.split("\n");
632
- let mode = "none";
633
- for (const rawLine of lines) {
634
- const line = rawLine.trim();
635
- if (line.includes("have not yet been applied")) {
636
- mode = "pending";
637
- continue;
638
- }
639
- if (line.match(/failed to apply|rolled back|migration.*failed/i)) {
640
- mode = "failed";
641
- continue;
642
- }
643
- if (line === "" || line.startsWith("\u2500") || line.startsWith("The following")) {
644
- mode = "none";
645
- continue;
646
- }
647
- if (mode === "none") continue;
648
- const cleaned = line.replace(/^[•\-*]\s*/, "").trim();
649
- if (cleaned.match(/^\d{14}/)) {
650
- map.set(cleaned, mode === "pending" ? "pending" : "failed");
651
- }
652
- }
653
- return map;
1049
+ });
1050
+ const rawScore = matchedPatterns.reduce((acc, p) => acc + p.weight, 0);
1051
+ const score = Math.min(100, rawScore);
1052
+ const hasCriticalFactor = matchedPatterns.some((pattern) => pattern.severity === "critical");
1053
+ let level = "low";
1054
+ if (hasCriticalFactor || score >= 75) level = "critical";
1055
+ else if (score >= 50) level = "high";
1056
+ else if (score >= 20) level = "medium";
1057
+ return { score, level, factors };
654
1058
  }
655
1059
  async function getMigrationDetails(cwd, name) {
656
1060
  const project = await detectPrismaProject(cwd);
@@ -670,15 +1074,16 @@ async function getMigrationDetails(cwd, name) {
670
1074
  async function getMigrations(cwd) {
671
1075
  const project = await detectPrismaProject(cwd);
672
1076
  if (!project) return [];
673
- let statusMap;
674
- try {
675
- statusMap = await getMigrationStatusMap(cwd, project.schemaPath);
676
- } catch {
677
- statusMap = /* @__PURE__ */ new Map();
678
- }
1077
+ const adapter = getPrismaAdapter(project.prismaVersion);
1078
+ const statusResult = await adapter.getMigrationStatus(cwd, project.schemaPath);
679
1079
  return Promise.all(
680
1080
  project.migrations.map(async (m) => {
681
- const status = statusMap.get(m.name) ?? "applied";
1081
+ let status = "unknown";
1082
+ if (statusResult.verification === "verified") {
1083
+ status = statusResult.statusMap.get(m.name) ?? "applied";
1084
+ } else {
1085
+ status = statusResult.statusMap.get(m.name) ?? "unknown";
1086
+ }
682
1087
  let sql = "";
683
1088
  try {
684
1089
  sql = await fs4.readFile(m.sqlPath, "utf-8");
@@ -693,65 +1098,58 @@ async function getMigrations(cwd) {
693
1098
  async function getProjectStatus(cwd) {
694
1099
  const project = await detectPrismaProject(cwd);
695
1100
  if (!project) throw new Error("No Prisma project found");
696
- let connected = false;
697
- try {
698
- await execPrisma(cwd, ["migrate", "status", "--schema", project.schemaPath], {
699
- timeout: 3e4
700
- });
701
- connected = true;
702
- } catch (err) {
703
- const error = err;
704
- const stderr = error.stderr ?? "";
705
- if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
706
- connected = false;
707
- } else {
708
- connected = true;
709
- }
710
- }
1101
+ const adapter = getPrismaAdapter(project.prismaVersion);
1102
+ const statusResult = await adapter.getMigrationStatus(cwd, project.schemaPath);
711
1103
  const migrations = await getMigrations(cwd);
712
1104
  const migrationsPending = migrations.filter((m) => m.status === "pending").length;
713
1105
  const migrationsFailed = migrations.filter((m) => m.status === "failed").length;
1106
+ const migrationsUnknown = migrations.filter((m) => m.status === "unknown").length;
714
1107
  const migrationsApplied = migrations.filter((m) => m.status === "applied").length;
715
- let driftResult = { items: [], status: "clean" };
716
- if (connected) {
1108
+ let driftResult = { items: [], status: "not_checked" };
1109
+ if (statusResult.connected && statusResult.verification === "verified" && migrationsPending === 0) {
717
1110
  driftResult = await detectDrift(cwd);
718
1111
  }
719
1112
  const hasDrift = driftResult.status === "drifted";
720
1113
  let riskLevel = "low";
721
- if (migrationsFailed > 0) riskLevel = "high";
722
- else if (hasDrift) riskLevel = "medium";
1114
+ if (migrationsFailed > 0 || statusResult.verification === "error") riskLevel = "high";
1115
+ else if (hasDrift || statusResult.verification === "unknown") riskLevel = "medium";
723
1116
  else if (migrationsPending > 0) riskLevel = "low";
724
1117
  const maxRiskScore = Math.max(0, ...migrations.map((m) => m.riskScore.score));
725
- if (maxRiskScore >= 75) riskLevel = "critical";
1118
+ const hasCriticalRisk = migrations.some(
1119
+ (m) => m.riskScore.level === "critical" || m.riskScore.factors.some((factor) => factor.severity === "critical")
1120
+ );
1121
+ if (hasCriticalRisk || maxRiskScore >= 75) riskLevel = "critical";
726
1122
  else if (maxRiskScore >= 50) riskLevel = "high";
727
1123
  else if (maxRiskScore >= 20 && riskLevel === "low") riskLevel = "medium";
728
- const healthScore = calculateHealthScore({
729
- connected,
730
- migrationsPending,
731
- migrationsFailed,
732
- driftCount: driftResult.items.length,
733
- maxRiskScore
734
- });
735
- const deploymentReadiness = buildDeploymentReadiness({
736
- connected,
1124
+ const deploymentReadiness = evaluateDeploymentReadiness({
1125
+ connected: statusResult.connected,
1126
+ migrationVerification: statusResult.verification,
1127
+ migrationsApplied,
737
1128
  migrationsPending,
738
1129
  migrationsFailed,
1130
+ migrationsUnknown,
1131
+ driftStatus: driftResult.status,
739
1132
  driftCount: driftResult.items.length,
740
1133
  maxRiskScore,
741
- healthScore
1134
+ hasCriticalRisk,
1135
+ errorMessage: statusResult.errorMessage ?? driftResult.errorMessage
742
1136
  });
743
1137
  return {
744
- connected,
745
- migrationsApplied: connected ? migrationsApplied : 0,
746
- migrationsPending: connected ? migrationsPending : migrations.length,
1138
+ connected: statusResult.connected,
1139
+ migrationVerification: statusResult.verification,
1140
+ migrationsApplied: statusResult.connected ? migrationsApplied : 0,
1141
+ migrationsPending: statusResult.connected ? migrationsPending : migrations.length,
747
1142
  migrationsFailed,
1143
+ migrationsUnknown,
748
1144
  driftDetected: hasDrift,
749
1145
  driftCount: driftResult.items.length,
1146
+ driftStatus: driftResult.status,
750
1147
  riskLevel,
751
- healthScore,
1148
+ healthScore: deploymentReadiness.score,
752
1149
  deploymentReadiness,
753
1150
  lastSync: (/* @__PURE__ */ new Date()).toISOString(),
754
1151
  ...project.provider ? { provider: project.provider } : {},
1152
+ projectName: "prisma-project",
755
1153
  schemaPath: project.schemaPath,
756
1154
  migrationsPath: project.migrationsPath,
757
1155
  ...project.prismaVersion ? { prismaVersion: project.prismaVersion } : {},
@@ -796,9 +1194,9 @@ async function trackEvent(event, migrationCount) {
796
1194
 
797
1195
  // src/commands/check.ts
798
1196
  function checkCommand() {
799
- return new Command("check").description("Validate database migration state (CI-friendly)").option("--ci", "Exit with a non-zero code when issues are found (for CI pipelines)").option("--json", "Output result as JSON to stdout").option(
1197
+ return new Command("check").description("Validate database migration state and deployment readiness (CI-friendly)").option("--ci", "Exit with a non-zero code when issues or unverified states are found").option("--json", "Output result as JSON to stdout").option(
800
1198
  "--fail-on-risk <level>",
801
- "Exit code 5 when risk meets or exceeds this level (low|medium|high|critical)"
1199
+ "Exit code 5 when estimated risk meets or exceeds this level (low|medium|high|critical)"
802
1200
  ).option("--quiet", "Suppress non-essential output").action(
803
1201
  async (options) => {
804
1202
  const cwd = process.cwd();
@@ -820,14 +1218,18 @@ function checkCommand() {
820
1218
  getProjectStatus(cwd),
821
1219
  getMigrations(cwd)
822
1220
  ]);
823
- const ok = status.migrationsFailed === 0 && !status.driftDetected && status.migrationsPending === 0;
1221
+ const isVerified = status.migrationVerification === "verified" && status.connected;
1222
+ const ok = isVerified && status.migrationsFailed === 0 && !status.driftDetected && status.migrationsPending === 0;
824
1223
  const result = {
825
1224
  ok,
826
1225
  connected: status.connected,
1226
+ migrationVerification: status.migrationVerification,
827
1227
  pendingCount: status.migrationsPending,
828
1228
  failedCount: status.migrationsFailed,
1229
+ unknownCount: status.migrationsUnknown,
829
1230
  driftDetected: status.driftDetected,
830
1231
  driftCount: status.driftCount,
1232
+ driftStatus: status.driftStatus,
831
1233
  riskLevel: status.riskLevel,
832
1234
  healthScore: status.healthScore,
833
1235
  deploymentReadiness: status.deploymentReadiness,
@@ -849,6 +1251,13 @@ function checkCommand() {
849
1251
  } else {
850
1252
  console.log(chalk.red(" \u2716 Database unreachable \u2014 check DATABASE_URL"));
851
1253
  }
1254
+ if (status.migrationVerification === "verified") {
1255
+ console.log(chalk.green(" \u2714 Migration verification: verified"));
1256
+ } else {
1257
+ console.log(
1258
+ chalk.red(` \u2716 Migration verification: ${status.migrationVerification}`) + chalk.dim(" (cannot prove database safety)")
1259
+ );
1260
+ }
852
1261
  console.log(
853
1262
  chalk.green(" \u2714 Applied migrations: ") + chalk.bold(String(status.migrationsApplied))
854
1263
  );
@@ -884,6 +1293,10 @@ function checkCommand() {
884
1293
  )
885
1294
  );
886
1295
  console.log(chalk.dim(" Review the dashboard Drift page before deploying"));
1296
+ } else if (status.driftStatus === "not_checked") {
1297
+ console.log(
1298
+ chalk.yellow(" \u26A0 Schema drift: not checked (pending work or disconnected)")
1299
+ );
887
1300
  } else {
888
1301
  console.log(chalk.green(" \u2714 No schema drift detected"));
889
1302
  }
@@ -894,25 +1307,33 @@ function checkCommand() {
894
1307
  critical: chalk.red
895
1308
  };
896
1309
  const riskColor = riskColors[status.riskLevel] ?? chalk.white;
897
- console.log(` \u{1F512} Risk level: ${riskColor(chalk.bold(status.riskLevel.toUpperCase()))}`);
1310
+ console.log(
1311
+ ` \u{1F512} Estimated risk: ${riskColor(chalk.bold(status.riskLevel.toUpperCase()))}`
1312
+ );
898
1313
  console.log(
899
1314
  ` \u{1F6A6} Readiness: ${riskColor(chalk.bold(`${status.healthScore}/100`))}${chalk.dim(
900
1315
  ` \u2014 ${status.deploymentReadiness.summary}`
901
1316
  )}`
902
1317
  );
903
- const highestRiskMigration = migrations.filter((m) => m.riskScore.score > 0).sort((a, b) => b.riskScore.score - a.riskScore.score)[0];
904
- if (highestRiskMigration?.riskScore.factors.length) {
1318
+ const highestRiskMigration2 = migrations.filter((m) => m.riskScore.score > 0).sort((a, b) => b.riskScore.score - a.riskScore.score)[0];
1319
+ if (highestRiskMigration2?.riskScore.factors.length) {
905
1320
  console.log(chalk.dim(`
906
- Top risk in ${highestRiskMigration.name}:`));
907
- for (const factor of highestRiskMigration.riskScore.factors.slice(0, 2)) {
1321
+ Top heuristic risk in ${highestRiskMigration2.name}:`));
1322
+ for (const factor of highestRiskMigration2.riskScore.factors.slice(0, 2)) {
908
1323
  console.log(chalk.dim(` \u2022 ${factor.description}`));
909
1324
  }
910
1325
  }
911
1326
  console.log(bar);
912
1327
  if (ok) {
913
- console.log(chalk.bold.green(" \u2714 All checks passed"));
1328
+ console.log(chalk.bold.green(" \u2714 All checks passed and verified"));
914
1329
  } else {
915
1330
  console.log(chalk.bold.red(" \u2716 Issues detected \u2014 review above"));
1331
+ if (!status.connected) {
1332
+ console.log(chalk.dim(" \u2192 Fix: Check database connection and DATABASE_URL"));
1333
+ }
1334
+ if (status.migrationVerification !== "verified") {
1335
+ console.log(chalk.dim(" \u2192 Fix: Run prisma-flow doctor to diagnose Prisma CLI"));
1336
+ }
916
1337
  if (status.migrationsFailed > 0) {
917
1338
  console.log(chalk.dim(" \u2192 Fix: Run prisma migrate resolve"));
918
1339
  }
@@ -949,6 +1370,7 @@ function checkCommand() {
949
1370
  if (current >= threshold) process.exit(5);
950
1371
  }
951
1372
  if (options.ci) {
1373
+ if (!status.connected || status.migrationVerification !== "verified") process.exit(4);
952
1374
  if (status.migrationsFailed > 0) process.exit(3);
953
1375
  if (status.driftDetected) process.exit(2);
954
1376
  if (status.migrationsPending > 0) process.exit(1);
@@ -980,7 +1402,486 @@ import { Command as Command2 } from "commander";
980
1402
  import fs5 from "fs";
981
1403
  import path4 from "path";
982
1404
  import { pathToFileURL } from "url";
983
- import { PrismaFlowConfigSchema } from "@prisma-flow/shared";
1405
+
1406
+ // ../shared/dist/index.js
1407
+ import { z } from "zod";
1408
+ var MigrationStatusSchema = z.enum(["applied", "pending", "failed", "unknown"]);
1409
+ var MigrationVerificationStatusSchema = z.enum(["verified", "unknown", "error"]);
1410
+ var RiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
1411
+ var DriftTypeSchema = z.enum([
1412
+ "table-missing",
1413
+ "table-extra",
1414
+ "column-mismatch",
1415
+ "index-change",
1416
+ "constraint-change",
1417
+ "unknown",
1418
+ "missing_migration",
1419
+ "extra_column",
1420
+ "extra_table",
1421
+ "modified_migration"
1422
+ ]);
1423
+ var DriftDetectionStatusSchema = z.enum(["clean", "drifted", "error", "not_checked"]);
1424
+ var LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error"]);
1425
+ var WebhookTypeSchema = z.enum(["slack", "discord", "http"]);
1426
+ var WebhookEventSchema = z.enum([
1427
+ "drift-detected",
1428
+ "migration-failed",
1429
+ "check-complete",
1430
+ "migration-applied",
1431
+ "simulation-complete",
1432
+ "risk-threshold-exceeded"
1433
+ ]);
1434
+ var DatabaseProviderSchema = z.enum([
1435
+ "postgresql",
1436
+ "mysql",
1437
+ "sqlite",
1438
+ "sqlserver",
1439
+ "mongodb"
1440
+ ]);
1441
+ var DriftRepairStrategySchema = z.enum([
1442
+ "reconcile_history",
1443
+ "manual_migration",
1444
+ "manual_sql",
1445
+ "review_only"
1446
+ ]);
1447
+ var SchemaDiffTypeSchema = z.enum([
1448
+ "model_added",
1449
+ "model_removed",
1450
+ "field_added",
1451
+ "field_removed",
1452
+ "field_type_changed",
1453
+ "added",
1454
+ "removed",
1455
+ "modified"
1456
+ ]);
1457
+ var SimulationVerificationSchema = z.enum(["executed", "static-analysis", "not-verified"]);
1458
+ var SimulationOutcomeSchema = z.enum(["success", "failure", "unknown"]);
1459
+ var SimulationModeSchema = z.enum(["static", "shadow", "live"]);
1460
+ var SimulationStatementTypeSchema = z.enum([
1461
+ "CREATE_TABLE",
1462
+ "ALTER_TABLE",
1463
+ "DROP_TABLE",
1464
+ "CREATE_INDEX",
1465
+ "DROP_INDEX",
1466
+ "INSERT",
1467
+ "UPDATE",
1468
+ "DELETE",
1469
+ "TRUNCATE",
1470
+ "OTHER"
1471
+ ]);
1472
+ var DeploymentReadinessStatusSchema = z.enum(["ready", "attention", "blocked"]);
1473
+ var DeploymentReadinessCheckIdSchema = z.enum([
1474
+ "database",
1475
+ "drift",
1476
+ "migration-verification",
1477
+ "failed-migrations",
1478
+ "pending-migrations",
1479
+ "critical-risks"
1480
+ ]);
1481
+ var DeploymentPlanPrioritySchema = z.enum(["blocker", "recommended", "optional"]);
1482
+ var MigrationSchema = z.object({
1483
+ name: z.string().min(1),
1484
+ timestamp: z.string(),
1485
+ status: MigrationStatusSchema,
1486
+ sqlPath: z.string(),
1487
+ createdAt: z.string().optional(),
1488
+ appliedAt: z.string().optional(),
1489
+ durationMs: z.number().int().nonnegative().optional()
1490
+ });
1491
+ var RiskFactorSchema = z.object({
1492
+ pattern: z.string(),
1493
+ severity: RiskLevelSchema,
1494
+ description: z.string(),
1495
+ affectedTable: z.string().optional(),
1496
+ estimatedRows: z.number().int().nonnegative().optional(),
1497
+ recommendation: z.string()
1498
+ });
1499
+ var MigrationRiskScoreSchema = z.object({
1500
+ score: z.number().int().min(0).max(100),
1501
+ level: RiskLevelSchema,
1502
+ factors: z.array(RiskFactorSchema)
1503
+ });
1504
+ var RollbackStepSchema = z.object({
1505
+ index: z.number().int().nonnegative(),
1506
+ forwardSql: z.string(),
1507
+ rollbackSql: z.string(),
1508
+ automated: z.boolean(),
1509
+ warning: z.string().optional()
1510
+ });
1511
+ var RollbackPlanSchema = z.object({
1512
+ migrationName: z.string(),
1513
+ steps: z.array(RollbackStepSchema).default([]),
1514
+ hasManualSteps: z.boolean().default(false),
1515
+ warnings: z.array(z.string()).default([]),
1516
+ generatedAt: z.string(),
1517
+ automated: z.boolean().default(false),
1518
+ sql: z.string().optional()
1519
+ });
1520
+ var MigrationDetailSchema = MigrationSchema.extend({
1521
+ sql: z.string(),
1522
+ risks: z.array(z.string()),
1523
+ riskScore: MigrationRiskScoreSchema.optional(),
1524
+ rollbackPlan: RollbackPlanSchema.optional(),
1525
+ gitBranch: z.string().optional()
1526
+ });
1527
+ var DriftItemSchema = z.object({
1528
+ sql: z.string(),
1529
+ type: DriftTypeSchema,
1530
+ description: z.string(),
1531
+ identifier: z.string().optional(),
1532
+ migrationName: z.string().optional()
1533
+ });
1534
+ var DriftResultSchema = z.object({
1535
+ hasDrift: z.boolean(),
1536
+ driftCount: z.number().int().nonnegative(),
1537
+ differences: z.array(DriftItemSchema),
1538
+ cachedAt: z.string().nullable(),
1539
+ status: DriftDetectionStatusSchema,
1540
+ errorMessage: z.string().optional()
1541
+ });
1542
+ var DeploymentReadinessCheckSchema = z.object({
1543
+ id: DeploymentReadinessCheckIdSchema,
1544
+ label: z.string(),
1545
+ passed: z.boolean(),
1546
+ message: z.string()
1547
+ });
1548
+ var DeploymentReadinessSchema = z.object({
1549
+ status: DeploymentReadinessStatusSchema,
1550
+ score: z.number().int().min(0).max(100),
1551
+ summary: z.string(),
1552
+ checks: z.array(DeploymentReadinessCheckSchema)
1553
+ });
1554
+ var DeploymentPlanActionSchema = z.object({
1555
+ priority: DeploymentPlanPrioritySchema,
1556
+ title: z.string(),
1557
+ detail: z.string(),
1558
+ command: z.string().optional(),
1559
+ href: z.string().optional()
1560
+ });
1561
+ var DeploymentPlanCommandSchema = z.object({
1562
+ label: z.string(),
1563
+ command: z.string(),
1564
+ reason: z.string()
1565
+ });
1566
+ var DeploymentPlanMigrationSummarySchema = z.object({
1567
+ total: z.number().int().nonnegative(),
1568
+ applied: z.number().int().nonnegative(),
1569
+ pending: z.number().int().nonnegative(),
1570
+ failed: z.number().int().nonnegative(),
1571
+ unknown: z.number().int().nonnegative().default(0),
1572
+ verification: MigrationVerificationStatusSchema.default("verified"),
1573
+ pendingNames: z.array(z.string()),
1574
+ failedNames: z.array(z.string()),
1575
+ highestRisk: z.object({
1576
+ name: z.string(),
1577
+ level: RiskLevelSchema,
1578
+ score: z.number().int().min(0).max(100),
1579
+ factors: z.array(RiskFactorSchema)
1580
+ }).optional()
1581
+ });
1582
+ var DeploymentPlanDriftSummarySchema = z.object({
1583
+ status: DriftDetectionStatusSchema,
1584
+ detected: z.boolean(),
1585
+ count: z.number().int().nonnegative(),
1586
+ errorMessage: z.string().optional()
1587
+ });
1588
+ var DeploymentPlanSchema = z.object({
1589
+ schemaVersion: z.literal("prismaflow-plan/v1"),
1590
+ generatedAt: z.string(),
1591
+ decision: DeploymentReadinessStatusSchema,
1592
+ score: z.number().int().min(0).max(100),
1593
+ summary: z.string(),
1594
+ project: z.object({
1595
+ schemaPath: z.string(),
1596
+ migrationsPath: z.string(),
1597
+ provider: DatabaseProviderSchema.optional(),
1598
+ prismaVersion: z.string().optional(),
1599
+ packageManager: z.string().optional(),
1600
+ hasDatabaseUrl: z.boolean()
1601
+ }),
1602
+ checks: z.array(DeploymentReadinessCheckSchema),
1603
+ migrations: DeploymentPlanMigrationSummarySchema,
1604
+ drift: DeploymentPlanDriftSummarySchema,
1605
+ actions: z.array(DeploymentPlanActionSchema),
1606
+ commands: z.array(DeploymentPlanCommandSchema),
1607
+ valueHighlights: z.array(z.string())
1608
+ });
1609
+ var ProjectStatusSchema = z.object({
1610
+ connected: z.boolean(),
1611
+ migrationVerification: MigrationVerificationStatusSchema.default("verified"),
1612
+ migrationsApplied: z.number().int().nonnegative(),
1613
+ migrationsPending: z.number().int().nonnegative(),
1614
+ migrationsFailed: z.number().int().nonnegative(),
1615
+ migrationsUnknown: z.number().int().nonnegative().default(0),
1616
+ driftDetected: z.boolean(),
1617
+ driftCount: z.number().int().nonnegative(),
1618
+ driftStatus: DriftDetectionStatusSchema.default("clean"),
1619
+ riskLevel: RiskLevelSchema,
1620
+ healthScore: z.number().int().min(0).max(100),
1621
+ deploymentReadiness: DeploymentReadinessSchema,
1622
+ lastSync: z.string(),
1623
+ provider: DatabaseProviderSchema.optional(),
1624
+ projectName: z.string().optional(),
1625
+ schemaPath: z.string().optional(),
1626
+ migrationsPath: z.string().optional(),
1627
+ prismaVersion: z.string().optional(),
1628
+ packageManager: z.string().optional(),
1629
+ hasDatabaseUrl: z.boolean().optional()
1630
+ });
1631
+ var SimulationStatementSchema = z.object({
1632
+ index: z.number().int().nonnegative(),
1633
+ sql: z.string(),
1634
+ type: SimulationStatementTypeSchema,
1635
+ isDestructive: z.boolean(),
1636
+ warnings: z.array(z.string()),
1637
+ estimatedRowsAffected: z.number().optional(),
1638
+ success: z.boolean().optional(),
1639
+ error: z.string().optional(),
1640
+ durationMs: z.number().nonnegative().optional()
1641
+ });
1642
+ var SimulationResultSchema = z.object({
1643
+ migrationName: z.string(),
1644
+ verification: SimulationVerificationSchema,
1645
+ outcome: SimulationOutcomeSchema,
1646
+ statements: z.array(SimulationStatementSchema),
1647
+ destructiveStatements: z.number().int().nonnegative(),
1648
+ warnings: z.array(z.string()),
1649
+ simulatedAt: z.string(),
1650
+ error: z.string().optional(),
1651
+ mode: SimulationModeSchema.optional()
1652
+ });
1653
+ var DriftRecoverySuggestionSchema = z.object({
1654
+ driftItem: DriftItemSchema,
1655
+ strategy: DriftRepairStrategySchema,
1656
+ description: z.string(),
1657
+ sql: z.string().optional(),
1658
+ automated: z.literal(false).default(false),
1659
+ risk: RiskLevelSchema,
1660
+ warnings: z.array(z.string()).default([])
1661
+ });
1662
+ var DriftRepairPlanSchema = z.object({
1663
+ generatedAt: z.string(),
1664
+ driftCount: z.number().int().nonnegative(),
1665
+ suggestions: z.array(DriftRecoverySuggestionSchema),
1666
+ isMutatingDisabled: z.literal(true).default(true)
1667
+ });
1668
+ var SchemaDiffSchema = z.object({
1669
+ type: SchemaDiffTypeSchema,
1670
+ modelName: z.string().optional(),
1671
+ fieldName: z.string().optional(),
1672
+ oldType: z.string().optional(),
1673
+ newType: z.string().optional(),
1674
+ description: z.string(),
1675
+ breaking: z.boolean(),
1676
+ entity: z.string().optional(),
1677
+ field: z.string().optional(),
1678
+ before: z.string().optional(),
1679
+ after: z.string().optional()
1680
+ });
1681
+ var MigrationHistoryDiffSchema = z.object({
1682
+ sourceEnv: z.string(),
1683
+ targetEnv: z.string(),
1684
+ sourceApplied: z.number().int().nonnegative(),
1685
+ targetApplied: z.number().int().nonnegative(),
1686
+ onlyInSource: z.array(z.string()),
1687
+ onlyInTarget: z.array(z.string()),
1688
+ divergencePoint: z.string().optional(),
1689
+ inSync: z.boolean(),
1690
+ name: z.string().optional(),
1691
+ presentInSource: z.boolean().optional(),
1692
+ presentInTarget: z.boolean().optional(),
1693
+ statusInSource: MigrationStatusSchema.optional(),
1694
+ statusInTarget: MigrationStatusSchema.optional()
1695
+ });
1696
+ var EnvironmentComparisonEntrySchema = z.object({
1697
+ name: z.string(),
1698
+ reachable: z.boolean(),
1699
+ appliedCount: z.number().int().nonnegative(),
1700
+ pendingCount: z.number().int().nonnegative(),
1701
+ failedCount: z.number().int().nonnegative()
1702
+ });
1703
+ var EnvironmentComparisonSchema = z.object({
1704
+ referenceEnv: z.string(),
1705
+ environments: z.array(EnvironmentComparisonEntrySchema),
1706
+ diffs: z.array(MigrationHistoryDiffSchema),
1707
+ allInSync: z.boolean(),
1708
+ comparedAt: z.string(),
1709
+ source: z.string().optional(),
1710
+ target: z.string().optional(),
1711
+ schemaDiffs: z.array(SchemaDiffSchema).optional(),
1712
+ migrationDiffs: z.array(MigrationHistoryDiffSchema).optional()
1713
+ });
1714
+ var GitMigrationInfoSchema = z.object({
1715
+ migrationName: z.string(),
1716
+ committed: z.boolean(),
1717
+ commitHash: z.string().optional(),
1718
+ commitAuthor: z.string().optional(),
1719
+ commitDate: z.string().optional(),
1720
+ commitMessage: z.string().optional(),
1721
+ branch: z.string().optional(),
1722
+ authorName: z.string().optional(),
1723
+ committedAt: z.string().optional()
1724
+ });
1725
+ var MigrationConflictSchema = z.object({
1726
+ timestamp: z.string(),
1727
+ migrations: z.array(z.string()),
1728
+ type: z.enum(["duplicate_timestamp", "timestamp-overlap", "name-conflict", "history-diverge"]),
1729
+ description: z.string(),
1730
+ migrationA: z.string().optional(),
1731
+ migrationB: z.string().optional(),
1732
+ branches: z.array(z.string()).optional(),
1733
+ conflictType: z.enum(["timestamp-overlap", "name-conflict", "history-diverge"]).optional()
1734
+ });
1735
+ var AuditActionSchema = z.enum([
1736
+ "dashboard.start",
1737
+ "status.check",
1738
+ "drift.detect",
1739
+ "drift.repair",
1740
+ "migration.check",
1741
+ "migration.apply",
1742
+ "migration.simulate",
1743
+ "migration.rollback",
1744
+ "migration.inspect",
1745
+ "migration.history",
1746
+ "migration.create",
1747
+ "deployment.plan",
1748
+ "doctor.run",
1749
+ "env.compare",
1750
+ "schema.diff"
1751
+ ]);
1752
+ var AuditEntrySchema = z.object({
1753
+ timestamp: z.string(),
1754
+ action: AuditActionSchema,
1755
+ cwd: z.string(),
1756
+ result: z.enum(["success", "failure", "warning"]),
1757
+ detail: z.record(z.unknown()).optional()
1758
+ });
1759
+ var PaginationQuerySchema = z.object({
1760
+ page: z.coerce.number().int().positive().default(1),
1761
+ limit: z.coerce.number().int().positive().max(100).default(20)
1762
+ });
1763
+ var PaginationMetaSchema = z.object({
1764
+ page: z.number().int().positive(),
1765
+ limit: z.number().int().positive(),
1766
+ total: z.number().int().nonnegative(),
1767
+ pages: z.number().int().nonnegative()
1768
+ });
1769
+ var WebhookConfigSchema = z.object({
1770
+ type: WebhookTypeSchema,
1771
+ url: z.string().url(),
1772
+ events: z.array(WebhookEventSchema).optional()
1773
+ });
1774
+ var FeatureFlagsSchema = z.object({
1775
+ riskAnalysis: z.boolean().default(true),
1776
+ webhookAlerts: z.boolean().default(false),
1777
+ auditLog: z.boolean().default(false),
1778
+ ciAnnotations: z.boolean().default(true),
1779
+ envComparison: z.boolean().default(false),
1780
+ rollbackGen: z.boolean().default(false),
1781
+ simulation: z.boolean().default(true),
1782
+ gitAwareness: z.boolean().default(false)
1783
+ });
1784
+ var EnvironmentEntrySchema = z.object({
1785
+ name: z.string().min(1),
1786
+ databaseUrl: z.string().url().or(z.string().startsWith("file:"))
1787
+ });
1788
+ var PrismaFlowConfigSchema = z.object({
1789
+ port: z.number().int().positive().max(65535).default(5555),
1790
+ logLevel: LogLevelSchema.default("info"),
1791
+ openBrowser: z.boolean().default(true),
1792
+ features: FeatureFlagsSchema.partial().default({}),
1793
+ webhooks: z.array(WebhookConfigSchema).default([]),
1794
+ environments: z.array(EnvironmentEntrySchema).default([]),
1795
+ auditLogMaxMb: z.number().positive().default(10),
1796
+ riskThreshold: RiskLevelSchema.default("medium")
1797
+ });
1798
+ var SSEEventTypeSchema = z.enum([
1799
+ "status-update",
1800
+ "drift-detected",
1801
+ "drift-resolved",
1802
+ "migration-applied",
1803
+ "migration-failed",
1804
+ "simulation-progress",
1805
+ "simulation-complete",
1806
+ "repair-progress",
1807
+ "repair-complete"
1808
+ ]);
1809
+ var SSEEventSchema = z.object({
1810
+ type: SSEEventTypeSchema,
1811
+ data: z.unknown(),
1812
+ timestamp: z.string()
1813
+ });
1814
+ var SchemaFieldSchema = z.object({
1815
+ name: z.string(),
1816
+ type: z.string(),
1817
+ kind: z.string(),
1818
+ isId: z.boolean(),
1819
+ isRequired: z.boolean(),
1820
+ isList: z.boolean(),
1821
+ isUnique: z.boolean(),
1822
+ hasDefaultValue: z.boolean(),
1823
+ default: z.unknown().optional(),
1824
+ relationName: z.string().optional(),
1825
+ relationFromFields: z.array(z.string()).optional(),
1826
+ relationToFields: z.array(z.string()).optional()
1827
+ });
1828
+ var SchemaModelSchema = z.object({
1829
+ name: z.string(),
1830
+ dbName: z.string().nullable().optional(),
1831
+ fields: z.array(SchemaFieldSchema),
1832
+ primaryKey: z.unknown().nullable().optional(),
1833
+ uniqueFields: z.array(z.array(z.string())).optional(),
1834
+ uniqueIndexes: z.array(z.unknown()).optional()
1835
+ });
1836
+ var SchemaEnumSchema = z.object({
1837
+ name: z.string(),
1838
+ values: z.array(
1839
+ z.union([z.string(), z.object({ name: z.string(), dbName: z.string().nullable().optional() })])
1840
+ )
1841
+ });
1842
+ var SchemaDatamodelSchema = z.object({
1843
+ models: z.array(SchemaModelSchema),
1844
+ enums: z.array(SchemaEnumSchema),
1845
+ types: z.array(z.unknown()).optional()
1846
+ });
1847
+ var PrismaFlowError = class extends Error {
1848
+ constructor(message, code, cause) {
1849
+ super(message);
1850
+ this.code = code;
1851
+ this.cause = cause;
1852
+ this.name = "PrismaFlowError";
1853
+ }
1854
+ };
1855
+ var SimulationError = class extends PrismaFlowError {
1856
+ constructor(migrationName, cause) {
1857
+ super(`Migration simulation failed for "${migrationName}".`, "SIMULATION_FAILED", cause);
1858
+ this.name = "SimulationError";
1859
+ }
1860
+ };
1861
+ var RollbackError = class extends PrismaFlowError {
1862
+ constructor(migrationName, detail) {
1863
+ super(`Rollback failed for "${migrationName}": ${detail}`, "ROLLBACK_FAILED");
1864
+ this.name = "RollbackError";
1865
+ }
1866
+ };
1867
+ var EnvironmentComparisonError = class extends PrismaFlowError {
1868
+ constructor(source, target, cause) {
1869
+ super(
1870
+ `Environment comparison failed between "${source}" and "${target}".`,
1871
+ "ENV_COMPARISON_FAILED",
1872
+ cause
1873
+ );
1874
+ this.name = "EnvironmentComparisonError";
1875
+ }
1876
+ };
1877
+ var GitAwarenessError = class extends PrismaFlowError {
1878
+ constructor(detail, cause) {
1879
+ super(`Git awareness error: ${detail}`, "GIT_AWARENESS_ERROR", cause);
1880
+ this.name = "GitAwarenessError";
1881
+ }
1882
+ };
1883
+
1884
+ // src/core/config-loader.ts
984
1885
  var _cached = null;
985
1886
  var _cachedCwd = null;
986
1887
  function applyEnvOverrides(config) {
@@ -1080,8 +1981,7 @@ function getConfigSync() {
1080
1981
  }
1081
1982
 
1082
1983
  // src/core/env-comparator.ts
1083
- import { EnvironmentComparisonError } from "@prisma-flow/shared";
1084
- function parseStatusOutput(output) {
1984
+ function parseStatusOutput2(output) {
1085
1985
  const applied = [];
1086
1986
  const pending = [];
1087
1987
  const failed = [];
@@ -1115,13 +2015,13 @@ async function queryMigrationStatus(envName, databaseUrl, schemaPath, cwd) {
1115
2015
  env,
1116
2016
  timeout: 3e4
1117
2017
  });
1118
- return { ...parseStatusOutput(stdout), reachable: true };
2018
+ return { ...parseStatusOutput2(stdout), reachable: true };
1119
2019
  } catch (err) {
1120
2020
  const error = err instanceof Error ? err : new Error(String(err));
1121
2021
  if ("stdout" in err) {
1122
2022
  const output = err.stdout ?? "";
1123
2023
  if (output.length > 0) {
1124
- return { ...parseStatusOutput(output), reachable: true };
2024
+ return { ...parseStatusOutput2(output), reachable: true };
1125
2025
  }
1126
2026
  }
1127
2027
  if (error.message.includes("P1001") || error.message.includes("Connection refused") || error.message.includes("Can't reach database server")) {
@@ -1323,12 +2223,12 @@ import open from "open";
1323
2223
 
1324
2224
  // src/server/index.ts
1325
2225
  import { randomBytes } from "crypto";
1326
- import fs12 from "fs";
1327
- import path11 from "path";
2226
+ import fs11 from "fs";
2227
+ import path10 from "path";
1328
2228
  import { fileURLToPath } from "url";
1329
2229
  import { serve } from "@hono/node-server";
1330
2230
  import { serveStatic } from "@hono/node-server/serve-static";
1331
- import { Hono as Hono15 } from "hono";
2231
+ import { Hono as Hono16 } from "hono";
1332
2232
  import { cors } from "hono/cors";
1333
2233
  import { secureHeaders } from "hono/secure-headers";
1334
2234
 
@@ -1422,21 +2322,15 @@ import { Hono as Hono4 } from "hono";
1422
2322
  // src/core/schema-differ.ts
1423
2323
  import fs7 from "fs/promises";
1424
2324
  async function diffSchemaVsDatabase(schemaPath, databaseUrl, cwd) {
1425
- const env = { ...process.env, DATABASE_URL: databaseUrl };
2325
+ const normalizedDatabaseUrl = normalizeDatabaseUrlForPrismaCommand(databaseUrl, schemaPath);
2326
+ const env = { ...process.env, DATABASE_URL: normalizedDatabaseUrl };
1426
2327
  try {
1427
- const { stdout } = await execPrisma(
1428
- cwd,
1429
- [
1430
- "migrate",
1431
- "diff",
1432
- "--from-schema-datamodel",
1433
- schemaPath,
1434
- "--to-database-url",
1435
- databaseUrl,
1436
- "--script"
1437
- ],
1438
- { env, timeout: 3e4 }
1439
- );
2328
+ const { stdout } = await execPrismaMigrateDiff(cwd, {
2329
+ fromSchema: schemaPath,
2330
+ legacyDestinationArgs: ["--to-database-url", normalizedDatabaseUrl],
2331
+ env,
2332
+ timeout: 3e4
2333
+ });
1440
2334
  const diffs = parseSqlDiff(stdout);
1441
2335
  return { sql: stdout, diffs };
1442
2336
  } catch (err) {
@@ -1625,12 +2519,11 @@ import { Hono as Hono7 } from "hono";
1625
2519
  import { execFile as execFile2 } from "child_process";
1626
2520
  import fs8 from "fs/promises";
1627
2521
  import path6 from "path";
1628
- import { promisify as promisify2 } from "util";
1629
- import { GitAwarenessError } from "@prisma-flow/shared";
1630
- var execAsync = promisify2(execFile2);
1631
- async function git(args, cwd) {
2522
+ import { promisify } from "util";
2523
+ var execAsync = promisify(execFile2);
2524
+ async function git(args2, cwd) {
1632
2525
  try {
1633
- const { stdout } = await execAsync("git", args, { cwd, timeout: 1e4 });
2526
+ const { stdout } = await execAsync("git", args2, { cwd, timeout: 1e4 });
1634
2527
  return stdout.trim();
1635
2528
  } catch (err) {
1636
2529
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1809,112 +2702,365 @@ app8.get("/:name", async (c) => {
1809
2702
  });
1810
2703
  var migrations_default = app8;
1811
2704
 
1812
- // src/server/routes/repair.ts
2705
+ // src/server/routes/plan.ts
1813
2706
  import { Hono as Hono9 } from "hono";
1814
2707
 
1815
- // src/core/drift-recovery.ts
1816
- import fs9 from "fs/promises";
1817
- import path7 from "path";
1818
- import { DriftRepairError } from "@prisma-flow/shared";
1819
- function generateRepairSuggestions(driftItems, migrationsDir) {
1820
- void migrationsDir;
1821
- const suggestions = [];
1822
- for (const item of driftItems) {
1823
- const strategy = pickStrategy(item);
1824
- const repairSql = generateRepairSql(item, strategy);
1825
- suggestions.push({
1826
- driftItem: item,
1827
- strategy,
1828
- description: describeStrategy(item, strategy),
1829
- ...repairSql !== void 0 ? { sql: repairSql } : {},
1830
- automated: strategy !== "MANUAL_SQL",
1831
- risk: assessRisk(item)
2708
+ // src/core/deployment-plan.ts
2709
+ function uniqueCommands(commands) {
2710
+ const seen = /* @__PURE__ */ new Set();
2711
+ return commands.filter((item) => {
2712
+ if (seen.has(item.command)) return false;
2713
+ seen.add(item.command);
2714
+ return true;
2715
+ });
2716
+ }
2717
+ function highestRiskMigration(migrations) {
2718
+ return [...migrations].filter((migration) => migration.riskScore.score > 0).sort((a, b) => b.riskScore.score - a.riskScore.score)[0];
2719
+ }
2720
+ function actionForCheck(check, state, highestRisk) {
2721
+ const firstPending = state.migrations.find((migration) => migration.status === "pending");
2722
+ switch (check.id) {
2723
+ case "database":
2724
+ return {
2725
+ priority: "blocker",
2726
+ title: state.status.hasDatabaseUrl ? "Restore database connectivity" : "Configure DATABASE_URL",
2727
+ detail: state.status.hasDatabaseUrl ? "PrismaFlow found a database URL, but Prisma cannot reach the database. Check credentials, network access, and the target database process." : "Add a local DATABASE_URL before relying on migration status, drift, or simulation output.",
2728
+ command: "prisma-flow doctor"
2729
+ };
2730
+ case "migration-verification":
2731
+ return {
2732
+ priority: "blocker",
2733
+ title: "Verify migration state",
2734
+ detail: "Prisma CLI returned an unverified or error status. Run doctor diagnostics or check schema/database access.",
2735
+ command: "prisma-flow doctor"
2736
+ };
2737
+ case "drift":
2738
+ return {
2739
+ priority: "blocker",
2740
+ title: "Resolve schema drift before deploy",
2741
+ detail: "The live database does not match the Prisma schema or migration history. Review the drift evidence before applying new migrations.",
2742
+ command: "prisma-flow repair --json",
2743
+ href: "/drift"
2744
+ };
2745
+ case "failed-migrations":
2746
+ return {
2747
+ priority: "blocker",
2748
+ title: "Recover failed migration history",
2749
+ detail: "Failed migrations must be investigated and resolved intentionally before more migrations are applied.",
2750
+ command: "prisma migrate resolve",
2751
+ href: "/migrations"
2752
+ };
2753
+ case "pending-migrations":
2754
+ return {
2755
+ priority: "recommended",
2756
+ title: "Simulate pending migration SQL",
2757
+ detail: firstPending ? `Run a simulation for ${firstPending.name}, then deploy only after the output matches your expectations.` : "Run simulation before applying pending migration SQL to a shared database.",
2758
+ command: firstPending ? `prisma-flow simulate ${firstPending.name} --fail-on-destructive` : "prisma-flow simulate <migration> --fail-on-destructive",
2759
+ href: "/simulate"
2760
+ };
2761
+ case "critical-risks":
2762
+ return {
2763
+ priority: "blocker",
2764
+ title: "Review destructive SQL and rollback limits",
2765
+ detail: highestRisk ? `${highestRisk.name} contains critical-risk SQL. Inspect the statements and rollback plan before approval.` : "At least one migration contains destructive SQL. Inspect the SQL and rollback plan before approval.",
2766
+ command: highestRisk ? `prisma-flow inspect ${highestRisk.name} --rollback --sql` : "prisma-flow report --format markdown",
2767
+ href: "/risks"
2768
+ };
2769
+ }
2770
+ }
2771
+ function buildActions(state, highestRisk) {
2772
+ const failedChecks = state.status.deploymentReadiness.checks.filter((check) => !check.passed);
2773
+ const actions = failedChecks.map((check) => actionForCheck(check, state, highestRisk));
2774
+ const firstPending = state.migrations.find((migration) => migration.status === "pending");
2775
+ if (firstPending && !actions.some((action) => action.command === "prisma migrate deploy")) {
2776
+ actions.push({
2777
+ priority: "recommended",
2778
+ title: "Deploy after review",
2779
+ detail: "Once drift, failures, and destructive SQL are handled, apply pending migrations with Prisma.",
2780
+ command: "prisma migrate deploy",
2781
+ href: "/migrations"
1832
2782
  });
1833
2783
  }
1834
- return suggestions;
2784
+ if (actions.length === 0) {
2785
+ actions.push({
2786
+ priority: "recommended",
2787
+ title: "Keep a review artifact",
2788
+ detail: "The project is ready. Save a PrismaFlow report for your pull request or release checklist.",
2789
+ command: "prisma-flow report --format markdown --output prismaflow-report.md"
2790
+ });
2791
+ }
2792
+ return actions;
2793
+ }
2794
+ function buildCommands(state, highestRisk) {
2795
+ const firstPending = state.migrations.find((migration) => migration.status === "pending");
2796
+ const commands = [
2797
+ {
2798
+ label: "CI gate",
2799
+ command: "prisma-flow check --ci --json",
2800
+ reason: "Fail builds when migrations, drift, or configured risk thresholds are unsafe."
2801
+ },
2802
+ {
2803
+ label: "Review artifact",
2804
+ command: "prisma-flow report --format markdown --output prismaflow-report.md",
2805
+ reason: "Create a human-readable migration review summary for PRs and releases."
2806
+ },
2807
+ {
2808
+ label: "Dashboard",
2809
+ command: "prisma-flow dashboard",
2810
+ reason: "Open the local visual dashboard for drift, timeline, rollback, and simulation review."
2811
+ }
2812
+ ];
2813
+ if (firstPending) {
2814
+ commands.unshift(
2815
+ {
2816
+ label: "Inspect pending migration",
2817
+ command: `prisma-flow inspect ${firstPending.name} --rollback --sql`,
2818
+ reason: "Read generated SQL, risk factors, warnings, and rollback coverage before deploy."
2819
+ },
2820
+ {
2821
+ label: "Simulate pending migration",
2822
+ command: `prisma-flow simulate ${firstPending.name} --fail-on-destructive`,
2823
+ reason: "Catch destructive or failing statements before touching a shared database."
2824
+ }
2825
+ );
2826
+ commands.push({
2827
+ label: "Apply migrations",
2828
+ command: "prisma migrate deploy",
2829
+ reason: "Use Prisma to apply reviewed migration files in production or CI."
2830
+ });
2831
+ }
2832
+ if (state.status.driftDetected) {
2833
+ commands.unshift({
2834
+ label: "Repair drift",
2835
+ command: "prisma-flow repair --json",
2836
+ reason: "Generate recovery suggestions for differences between schema and database."
2837
+ });
2838
+ }
2839
+ if (state.status.migrationsFailed > 0) {
2840
+ commands.unshift({
2841
+ label: "Resolve failed migration",
2842
+ command: "prisma migrate resolve",
2843
+ reason: "Mark failed migration state only after the database has been manually verified."
2844
+ });
2845
+ }
2846
+ if (highestRisk && highestRisk.riskScore.level === "critical") {
2847
+ commands.unshift({
2848
+ label: "Critical-risk inspection",
2849
+ command: `prisma-flow inspect ${highestRisk.name} --rollback --sql`,
2850
+ reason: "Review destructive SQL and rollback limitations before approval."
2851
+ });
2852
+ }
2853
+ return uniqueCommands(commands);
2854
+ }
2855
+ function summarizeDecision(status) {
2856
+ const failedChecks = status.deploymentReadiness.checks.filter((check) => !check.passed);
2857
+ if (failedChecks.length === 0) {
2858
+ return "Ready: no blocking drift, failed migrations, pending work, or critical migration risks were detected.";
2859
+ }
2860
+ const issueLabels = {
2861
+ database: "database connectivity",
2862
+ "migration-verification": "migration verification",
2863
+ drift: "schema drift",
2864
+ "failed-migrations": "failed migrations",
2865
+ "pending-migrations": "pending migrations",
2866
+ "critical-risks": "critical migration risks"
2867
+ };
2868
+ const failedLabels = failedChecks.map((check) => issueLabels[check.id]).join(", ");
2869
+ if (status.deploymentReadiness.status === "blocked") {
2870
+ return `Blocked: fix ${failedLabels} before deploying.`;
2871
+ }
2872
+ return `Attention needed: review ${failedLabels} before deploying.`;
2873
+ }
2874
+ function createDeploymentPlanFromState(state) {
2875
+ const pending = state.migrations.filter((migration) => migration.status === "pending");
2876
+ const failed = state.migrations.filter((migration) => migration.status === "failed");
2877
+ const unknown = state.migrations.filter((migration) => migration.status === "unknown");
2878
+ const highestRisk = highestRiskMigration(pending.length > 0 ? pending : state.migrations);
2879
+ const drift = state.drift ?? {
2880
+ status: state.status.driftStatus,
2881
+ detected: state.status.driftDetected,
2882
+ count: state.status.driftCount
2883
+ };
2884
+ return {
2885
+ schemaVersion: "prismaflow-plan/v1",
2886
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2887
+ decision: state.status.deploymentReadiness.status,
2888
+ score: state.status.deploymentReadiness.score,
2889
+ summary: summarizeDecision(state.status),
2890
+ project: {
2891
+ schemaPath: state.project.schemaPath,
2892
+ migrationsPath: state.project.migrationsPath,
2893
+ ...state.project.provider ? { provider: state.project.provider } : {},
2894
+ ...state.project.prismaVersion ? { prismaVersion: state.project.prismaVersion } : {},
2895
+ ...state.project.packageManager ? { packageManager: state.project.packageManager } : {},
2896
+ hasDatabaseUrl: state.project.databaseUrl.length > 0
2897
+ },
2898
+ checks: state.status.deploymentReadiness.checks,
2899
+ migrations: {
2900
+ total: state.migrations.length,
2901
+ applied: state.migrations.filter((migration) => migration.status === "applied").length,
2902
+ pending: pending.length,
2903
+ failed: failed.length,
2904
+ unknown: unknown.length,
2905
+ verification: state.status.migrationVerification,
2906
+ pendingNames: pending.map((migration) => migration.name),
2907
+ failedNames: failed.map((migration) => migration.name),
2908
+ ...highestRisk ? {
2909
+ highestRisk: {
2910
+ name: highestRisk.name,
2911
+ level: highestRisk.riskScore.level,
2912
+ score: highestRisk.riskScore.score,
2913
+ factors: highestRisk.riskScore.factors
2914
+ }
2915
+ } : {}
2916
+ },
2917
+ drift,
2918
+ actions: buildActions(state, highestRisk),
2919
+ commands: buildCommands(state, highestRisk),
2920
+ valueHighlights: [
2921
+ "Go/no-go deployment decision from the same migration state your CI and dashboard use.",
2922
+ "Risk-ranked SQL review, rollback guidance, and simulation commands before Prisma applies anything.",
2923
+ "Drift, failed migrations, pending work, and destructive statements are turned into concrete next actions."
2924
+ ]
2925
+ };
2926
+ }
2927
+ async function buildDeploymentPlan(cwd) {
2928
+ const project = await detectPrismaProject(cwd);
2929
+ if (!project) throw new Error("No Prisma project found");
2930
+ const [status, migrations] = await Promise.all([getProjectStatus(cwd), getMigrations(cwd)]);
2931
+ return createDeploymentPlanFromState({
2932
+ project,
2933
+ status,
2934
+ migrations
2935
+ });
1835
2936
  }
2937
+
2938
+ // src/server/routes/plan.ts
2939
+ var app9 = new Hono9();
2940
+ app9.get("/", async (c) => {
2941
+ try {
2942
+ const projectPath = c.get("projectPath");
2943
+ const plan = await buildDeploymentPlan(projectPath);
2944
+ return c.json({ success: true, data: plan });
2945
+ } catch (error) {
2946
+ const message = error instanceof Error ? error.message : "Unknown error";
2947
+ return c.json({ success: false, error: message }, 500);
2948
+ }
2949
+ });
2950
+ var plan_default = app9;
2951
+
2952
+ // src/server/routes/repair.ts
2953
+ import { Hono as Hono10 } from "hono";
2954
+
2955
+ // src/core/drift-recovery.ts
1836
2956
  function pickStrategy(item) {
1837
2957
  switch (item.type) {
1838
2958
  case "missing_migration":
1839
- return "APPLY_MIGRATION";
1840
- case "extra_column":
1841
- case "extra_table":
1842
- return "SQUASH";
1843
2959
  case "modified_migration":
1844
- return "MANUAL_SQL";
2960
+ return "reconcile_history";
2961
+ case "extra_table":
2962
+ case "extra_column":
2963
+ case "table-missing":
2964
+ case "table-extra":
2965
+ case "column-mismatch":
2966
+ return "manual_migration";
2967
+ case "index-change":
2968
+ case "constraint-change":
2969
+ return "manual_sql";
1845
2970
  default:
1846
- return "MANUAL_SQL";
2971
+ return "review_only";
1847
2972
  }
1848
2973
  }
1849
2974
  function describeStrategy(item, strategy) {
2975
+ const target = item.migrationName ?? item.identifier ?? "detected change";
1850
2976
  switch (strategy) {
1851
- case "APPLY_MIGRATION":
1852
- return `Apply missing migration "${item.migrationName ?? item.identifier}" to the database`;
1853
- case "SQUASH":
1854
- return `Create a new migration that incorporates the "${item.identifier}" drift (extra ${item.type.replace("extra_", "")} in DB)`;
1855
- case "MANUAL_SQL":
1856
- return `Manually review and apply SQL to reconcile "${item.identifier}" \u2014 automated repair not safe`;
1857
- case "IGNORE":
1858
- return `"${item.identifier}" is safe to ignore (documentation-only drift)`;
2977
+ case "reconcile_history":
2978
+ return `Reconcile migration history record for "${target}". Note: this updates the _prisma_migrations table only and does NOT execute migration SQL.`;
2979
+ case "manual_migration":
2980
+ return `Author a new Prisma migration or update schema.prisma to reflect the "${target}" change.`;
2981
+ case "manual_sql":
2982
+ return `Review and apply manual SQL adjustments for "${target}" after verifying database constraints and indexes.`;
1859
2983
  default:
1860
- return `Review drift for "${item.identifier}"`;
2984
+ return `Manually inspect "${target}" \u2014 automated analysis cannot determine safe resolution.`;
1861
2985
  }
1862
2986
  }
1863
- function generateRepairSql(item, strategy) {
1864
- if (strategy === "APPLY_MIGRATION" && item.migrationName) {
1865
- return `-- Run: npx prisma migrate resolve --applied "${item.migrationName}"`;
2987
+ function generateGuidanceSql(item, strategy) {
2988
+ if (strategy === "reconcile_history" && item.migrationName) {
2989
+ return `-- Manual history reconciliation only (does NOT run SQL):
2990
+ -- npx prisma migrate resolve --applied "${item.migrationName}"`;
1866
2991
  }
1867
- if (strategy === "SQUASH" && item.type === "extra_table") {
1868
- return `-- Extra table detected. To squash into schema:
1869
- -- 1. Add the model to prisma/schema.prisma
1870
- -- 2. Run: npx prisma migrate dev --name squash_${item.identifier}`;
2992
+ if (strategy === "manual_migration" && (item.type === "extra_table" || item.type === "table-extra")) {
2993
+ return `-- Extra table detected. To incorporate into Prisma schema:
2994
+ -- 1. Add model to prisma/schema.prisma
2995
+ -- 2. Run: npx prisma migrate dev --name sync_${item.identifier ?? "table"}`;
1871
2996
  }
1872
- if (strategy === "SQUASH" && item.type === "extra_column") {
1873
- const identifier = item.identifier ?? "unknown.unknown";
1874
- const [table, column] = identifier.split(".");
1875
- return `-- Extra column ${column} on ${table}. To squash:
1876
- -- 1. Add field to model in prisma/schema.prisma
1877
- -- 2. Run: npx prisma migrate dev --name squash_${column}_on_${table}`;
2997
+ if (strategy === "manual_migration" && (item.type === "extra_column" || item.type === "column-mismatch")) {
2998
+ return `-- Schema difference detected on ${item.identifier ?? "column"}.
2999
+ -- Update prisma/schema.prisma and generate a migration with: npx prisma migrate dev`;
1878
3000
  }
1879
3001
  if (item.sql) {
1880
- return `-- Suggested SQL (review carefully before running):
3002
+ return `-- Suggested SQL for operator review (do NOT apply without verification):
1881
3003
  ${item.sql}`;
1882
3004
  }
1883
3005
  return void 0;
1884
3006
  }
1885
3007
  function assessRisk(item) {
1886
- if (item.type === "modified_migration") return "high";
1887
- if (item.type === "extra_table") return "medium";
1888
- if (item.type === "extra_column") return "low";
1889
- if (item.type === "missing_migration") return "medium";
3008
+ if (item.type === "modified_migration" || item.type === "table-missing") return "high";
3009
+ if (item.type === "extra_table" || item.type === "column-mismatch") return "medium";
3010
+ if (item.type === "extra_column" || item.type === "missing_migration") return "medium";
3011
+ if (item.type === "index-change") return "low";
1890
3012
  return "medium";
1891
3013
  }
1892
- async function applyRepairs(suggestions, schemaPath, cwd) {
1893
- const results = [];
1894
- for (const suggestion of suggestions) {
1895
- if (suggestion.strategy !== "APPLY_MIGRATION") continue;
1896
- if (!suggestion.driftItem.migrationName) continue;
1897
- const name = suggestion.driftItem.migrationName;
1898
- try {
1899
- await execPrisma(cwd, ["migrate", "resolve", "--applied", name, "--schema", schemaPath], {
1900
- timeout: 3e4
1901
- });
1902
- results.push({ migrationName: name, success: true });
1903
- } catch (err) {
1904
- const error = err instanceof Error ? err : new Error(String(err));
1905
- results.push({
1906
- migrationName: name,
1907
- success: false,
1908
- error: error.message
1909
- });
1910
- }
3014
+ function generateWarnings(item, strategy) {
3015
+ const warnings = [];
3016
+ if (strategy === "reconcile_history") {
3017
+ warnings.push(
3018
+ "prisma migrate resolve only updates the migration record; it does NOT execute migration SQL statements on the database."
3019
+ );
3020
+ }
3021
+ if (item.type === "table-missing") {
3022
+ warnings.push(
3023
+ "The database is missing a table expected by Prisma schema. Queries may fail immediately."
3024
+ );
3025
+ }
3026
+ if (item.type === "modified_migration") {
3027
+ warnings.push(
3028
+ "Migration file contents differ from what was applied. Manual schema verification required."
3029
+ );
3030
+ }
3031
+ return warnings;
3032
+ }
3033
+ function generateRepairSuggestions(driftItems, _migrationsDir) {
3034
+ const suggestions = [];
3035
+ for (const item of driftItems) {
3036
+ const strategy = pickStrategy(item);
3037
+ const sql = generateGuidanceSql(item, strategy);
3038
+ const warnings = generateWarnings(item, strategy);
3039
+ suggestions.push({
3040
+ driftItem: item,
3041
+ strategy,
3042
+ description: describeStrategy(item, strategy),
3043
+ ...sql !== void 0 ? { sql } : {},
3044
+ automated: false,
3045
+ risk: assessRisk(item),
3046
+ warnings
3047
+ });
1911
3048
  }
1912
- return results;
3049
+ return suggestions;
3050
+ }
3051
+ function buildDriftRepairPlan(driftItems, migrationsDir) {
3052
+ const suggestions = generateRepairSuggestions(driftItems, migrationsDir);
3053
+ return {
3054
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3055
+ driftCount: driftItems.length,
3056
+ suggestions,
3057
+ isMutatingDisabled: true
3058
+ };
1913
3059
  }
1914
3060
 
1915
3061
  // src/server/routes/repair.ts
1916
- var app9 = new Hono9();
1917
- app9.get("/", async (c) => {
3062
+ var app10 = new Hono10();
3063
+ app10.get("/", async (c) => {
1918
3064
  const projectPath = c.get("projectPath");
1919
3065
  try {
1920
3066
  const project = await detectPrismaProject(projectPath);
@@ -1929,45 +3075,32 @@ app9.get("/", async (c) => {
1929
3075
  502
1930
3076
  );
1931
3077
  }
1932
- const suggestions = generateRepairSuggestions(driftResult.items, project.migrationsPath);
3078
+ const plan = buildDriftRepairPlan(driftResult.items, project.migrationsPath);
1933
3079
  return c.json({
1934
3080
  success: true,
1935
- data: { drifted: driftResult.status === "drifted", suggestions }
3081
+ data: { drifted: driftResult.status === "drifted", plan }
1936
3082
  });
1937
3083
  } catch (err) {
1938
3084
  const message = err instanceof Error ? err.message : String(err);
1939
3085
  return c.json({ success: false, error: message }, 500);
1940
3086
  }
1941
3087
  });
1942
- app9.post("/apply", async (c) => {
1943
- const projectPath = c.get("projectPath");
1944
- try {
1945
- const project = await detectPrismaProject(projectPath);
1946
- if (!project) return c.json({ success: false, error: "No Prisma project found" }, 404);
1947
- const driftResult = await detectDrift(projectPath);
1948
- if (driftResult.status === "error") {
1949
- return c.json(
1950
- {
1951
- success: false,
1952
- error: driftResult.errorMessage ?? "Drift detection failed"
1953
- },
1954
- 502
1955
- );
1956
- }
1957
- const suggestions = generateRepairSuggestions(driftResult.items, project.migrationsPath);
1958
- const results = await applyRepairs(suggestions, project.schemaPath, projectPath);
1959
- return c.json({ success: true, data: results });
1960
- } catch (err) {
1961
- const message = err instanceof Error ? err.message : String(err);
1962
- return c.json({ success: false, error: message }, 500);
1963
- }
3088
+ app10.post("/apply", async (c) => {
3089
+ return c.json(
3090
+ {
3091
+ success: false,
3092
+ error: "Automatic mutating repair is disabled in PrismaFlow V1. Use `prisma-flow repair` for plan-only recovery guidance.",
3093
+ mutatingDisabled: true
3094
+ },
3095
+ 400
3096
+ );
1964
3097
  });
1965
- var repair_default = app9;
3098
+ var repair_default = app10;
1966
3099
 
1967
3100
  // src/server/routes/risks.ts
1968
- import { Hono as Hono10 } from "hono";
1969
- var app10 = new Hono10();
1970
- app10.get("/", async (c) => {
3101
+ import { Hono as Hono11 } from "hono";
3102
+ var app11 = new Hono11();
3103
+ app11.get("/", async (c) => {
1971
3104
  const projectPath = c.get("projectPath");
1972
3105
  try {
1973
3106
  const project = await detectPrismaProject(projectPath);
@@ -1986,7 +3119,7 @@ app10.get("/", async (c) => {
1986
3119
  return c.json({ success: false, error: message }, 500);
1987
3120
  }
1988
3121
  });
1989
- app10.get("/:migration", async (c) => {
3122
+ app11.get("/:migration", async (c) => {
1990
3123
  const projectPath = c.get("projectPath");
1991
3124
  const migrationQuery = c.req.param("migration");
1992
3125
  try {
@@ -2003,31 +3136,29 @@ app10.get("/:migration", async (c) => {
2003
3136
  return c.json({ success: false, error: message }, 500);
2004
3137
  }
2005
3138
  });
2006
- var risks_default = app10;
3139
+ var risks_default = app11;
2007
3140
 
2008
3141
  // src/server/routes/rollback.ts
2009
- import path9 from "path";
2010
- import { Hono as Hono11 } from "hono";
3142
+ import path8 from "path";
3143
+ import { Hono as Hono12 } from "hono";
2011
3144
 
2012
3145
  // src/core/rollback-generator.ts
2013
- import fs11 from "fs/promises";
2014
- import { RollbackError } from "@prisma-flow/shared";
3146
+ import fs10 from "fs/promises";
2015
3147
 
2016
3148
  // src/core/simulator.ts
2017
3149
  import { execFile as execFile3 } from "child_process";
2018
- import fs10 from "fs/promises";
3150
+ import fs9 from "fs/promises";
2019
3151
  import os from "os";
2020
- import path8 from "path";
2021
- import { promisify as promisify3 } from "util";
2022
- import { SimulationError } from "@prisma-flow/shared";
2023
- var execAsync2 = promisify3(execFile3);
3152
+ import path7 from "path";
3153
+ import { promisify as promisify2 } from "util";
3154
+ var execAsync2 = promisify2(execFile3);
2024
3155
  var DESTRUCTIVE_PATTERNS = [
2025
3156
  {
2026
3157
  pattern: /drop\s+table/i,
2027
3158
  warning: "Drops a table \u2014 all data will be lost"
2028
3159
  },
2029
3160
  {
2030
- pattern: /truncate\s+table/i,
3161
+ pattern: /truncate\s+table|truncate\s+\w+/i,
2031
3162
  warning: "Truncates a table \u2014 all rows will be deleted"
2032
3163
  },
2033
3164
  {
@@ -2035,8 +3166,8 @@ var DESTRUCTIVE_PATTERNS = [
2035
3166
  warning: "Drops a column \u2014 data in that column will be lost"
2036
3167
  },
2037
3168
  {
2038
- pattern: /alter\s+column.*not\s+null/i,
2039
- warning: "Adds NOT NULL \u2014 will fail if existing rows have NULLs"
3169
+ pattern: /alter\s+column.*not\s+null|alter\s+table.*add\s+column.*not\s+null/i,
3170
+ warning: "Adds NOT NULL constraint \u2014 will fail if existing rows have NULLs"
2040
3171
  },
2041
3172
  { pattern: /delete\s+from/i, warning: "Deletes rows \u2014 data will be lost" },
2042
3173
  {
@@ -2072,7 +3203,8 @@ function estimateRowsAffected(sql) {
2072
3203
  return void 0;
2073
3204
  }
2074
3205
  function splitStatements(sql) {
2075
- return sql.split(";").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("--"));
3206
+ const withoutComments = sql.split("\n").filter((line) => !line.trimStart().startsWith("--")).join("\n");
3207
+ return withoutComments.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
2076
3208
  }
2077
3209
  function analyseStatically(migrationName, statements) {
2078
3210
  const parsed = statements.map((sql, index) => {
@@ -2094,9 +3226,9 @@ function analyseStatically(migrationName, statements) {
2094
3226
  const allWarnings = destructive.flatMap((s) => s.warnings);
2095
3227
  return {
2096
3228
  migrationName,
3229
+ verification: "static-analysis",
3230
+ outcome: "unknown",
2097
3231
  statements: parsed,
2098
- wouldSucceed: true,
2099
- // static analysis optimistic
2100
3232
  destructiveStatements: destructive.length,
2101
3233
  warnings: allWarnings,
2102
3234
  simulatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2104,46 +3236,89 @@ function analyseStatically(migrationName, statements) {
2104
3236
  };
2105
3237
  }
2106
3238
  async function simulateSqlite(migrationName, sqlFilePath, dbFilePath) {
2107
- const tmpDir = await fs10.mkdtemp(path8.join(os.tmpdir(), "prismaflow-sim-"));
2108
- const shadowDb = path8.join(tmpDir, "shadow.db");
3239
+ const tmpDir = await fs9.mkdtemp(path7.join(os.tmpdir(), "prismaflow-sim-"));
3240
+ const shadowDb = path7.join(tmpDir, "shadow.db");
2109
3241
  try {
2110
- await fs10.copyFile(dbFilePath, shadowDb);
2111
- const sql = await fs10.readFile(sqlFilePath, "utf-8");
3242
+ await fs9.copyFile(dbFilePath, shadowDb);
3243
+ const sql = await fs9.readFile(sqlFilePath, "utf-8");
2112
3244
  const statements = splitStatements(sql);
2113
3245
  try {
3246
+ const startTime = Date.now();
2114
3247
  await execAsync2("sqlite3", [shadowDb, sql], { timeout: 3e4 });
2115
- const staticResult = analyseStatically(migrationName, statements);
2116
- return { ...staticResult, wouldSucceed: true, mode: "shadow" };
3248
+ const durationMs = Date.now() - startTime;
3249
+ const staticAnalysis = analyseStatically(migrationName, statements);
3250
+ return {
3251
+ ...staticAnalysis,
3252
+ verification: "executed",
3253
+ outcome: "success",
3254
+ mode: "shadow",
3255
+ statements: staticAnalysis.statements.map((stmt) => ({
3256
+ ...stmt,
3257
+ success: true,
3258
+ durationMs: Math.round(durationMs / Math.max(1, statements.length))
3259
+ }))
3260
+ };
2117
3261
  } catch (err) {
2118
3262
  const error = err instanceof Error ? err : new Error(String(err));
2119
- const staticResult = analyseStatically(migrationName, statements);
3263
+ const staticAnalysis = analyseStatically(migrationName, statements);
3264
+ const errorCode = err.code;
3265
+ if (errorCode === "ENOENT" || error.message.includes("ENOENT") || error.message.includes("not recognized")) {
3266
+ return {
3267
+ ...staticAnalysis,
3268
+ verification: "static-analysis",
3269
+ outcome: "unknown",
3270
+ warnings: [
3271
+ ...staticAnalysis.warnings,
3272
+ "sqlite3 CLI is not installed; static analysis only \u2014 execution not verified."
3273
+ ],
3274
+ mode: "static"
3275
+ };
3276
+ }
2120
3277
  return {
2121
- ...staticResult,
2122
- wouldSucceed: false,
3278
+ ...staticAnalysis,
3279
+ verification: "executed",
3280
+ outcome: "failure",
2123
3281
  error: error.message,
2124
3282
  mode: "shadow"
2125
3283
  };
2126
3284
  }
2127
3285
  } catch {
2128
- const sql = await fs10.readFile(sqlFilePath, "utf-8").catch(() => "");
2129
- return analyseStatically(migrationName, splitStatements(sql));
3286
+ const sql = await fs9.readFile(sqlFilePath, "utf-8").catch(() => "");
3287
+ const staticAnalysis = analyseStatically(migrationName, splitStatements(sql));
3288
+ return {
3289
+ ...staticAnalysis,
3290
+ warnings: [
3291
+ ...staticAnalysis.warnings,
3292
+ "Shadow database setup failed; static analysis only \u2014 execution not verified."
3293
+ ]
3294
+ };
2130
3295
  } finally {
2131
- await fs10.rm(tmpDir, { recursive: true, force: true }).catch(() => {
3296
+ await fs9.rm(tmpDir, { recursive: true, force: true }).catch(() => {
2132
3297
  });
2133
3298
  }
2134
3299
  }
2135
- async function simulate(migrationName, sqlFilePath, dbFilePath) {
3300
+ async function simulate(migrationName, sqlFilePath, dbFilePath, provider) {
2136
3301
  try {
2137
- const sql = await fs10.readFile(sqlFilePath, "utf-8");
3302
+ const sql = await fs9.readFile(sqlFilePath, "utf-8");
2138
3303
  const statements = splitStatements(sql);
2139
- if (dbFilePath && dbFilePath !== ":memory:") {
3304
+ if (provider === "sqlite" && dbFilePath && dbFilePath !== ":memory:") {
2140
3305
  try {
2141
- await fs10.access(dbFilePath);
3306
+ await fs9.access(dbFilePath);
2142
3307
  return await simulateSqlite(migrationName, sqlFilePath, dbFilePath);
2143
3308
  } catch {
2144
3309
  }
2145
3310
  }
2146
- return analyseStatically(migrationName, statements);
3311
+ const staticResult = analyseStatically(migrationName, statements);
3312
+ if (provider && provider !== "sqlite") {
3313
+ return {
3314
+ ...staticResult,
3315
+ warnings: [
3316
+ ...staticResult.warnings,
3317
+ `Shadow execution is not configured for provider "${provider}"; static analysis only \u2014 execution not verified.`
3318
+ ]
3319
+ };
3320
+ }
3321
+ return staticResult;
2147
3322
  } catch (err) {
2148
3323
  const error = err instanceof Error ? err : new Error(String(err));
2149
3324
  throw new SimulationError(migrationName, error);
@@ -2242,7 +3417,7 @@ function invertStatement(sql) {
2242
3417
  async function generateRollbackPlan(migrationName, sqlFilePath) {
2243
3418
  let sql;
2244
3419
  try {
2245
- sql = await fs11.readFile(sqlFilePath, "utf-8");
3420
+ sql = await fs10.readFile(sqlFilePath, "utf-8");
2246
3421
  } catch (err) {
2247
3422
  const error = err instanceof Error ? err : new Error(String(err));
2248
3423
  throw new RollbackError(migrationName, `Cannot read SQL file: ${error.message}`);
@@ -2288,8 +3463,8 @@ ${plan.warnings.map((w) => `-- ! ${w}`).join("\n")}` : null,
2288
3463
  }
2289
3464
 
2290
3465
  // src/server/routes/rollback.ts
2291
- var app11 = new Hono11();
2292
- app11.get("/:migration", async (c) => {
3466
+ var app12 = new Hono12();
3467
+ app12.get("/:migration", async (c) => {
2293
3468
  const projectPath = c.get("projectPath");
2294
3469
  const migrationQuery = c.req.param("migration");
2295
3470
  try {
@@ -2299,7 +3474,7 @@ app11.get("/:migration", async (c) => {
2299
3474
  (m) => m.name === migrationQuery || m.name.startsWith(migrationQuery)
2300
3475
  );
2301
3476
  if (!match) return c.json({ success: false, error: "Migration not found" }, 404);
2302
- const sqlFile = path9.join(project.migrationsPath, match.name, "migration.sql");
3477
+ const sqlFile = path8.join(project.migrationsPath, match.name, "migration.sql");
2303
3478
  const plan = await generateRollbackPlan(match.name, sqlFile);
2304
3479
  const format = c.req.query("format");
2305
3480
  if (format === "sql") {
@@ -2314,10 +3489,10 @@ app11.get("/:migration", async (c) => {
2314
3489
  return c.json({ success: false, error: message }, 500);
2315
3490
  }
2316
3491
  });
2317
- var rollback_default = app11;
3492
+ var rollback_default = app12;
2318
3493
 
2319
3494
  // src/server/routes/schema.ts
2320
- import { Hono as Hono12 } from "hono";
3495
+ import { Hono as Hono13 } from "hono";
2321
3496
 
2322
3497
  // src/core/schema-parser.ts
2323
3498
  async function loadGetDMMF() {
@@ -2342,8 +3517,8 @@ async function parseSchema(cwd) {
2342
3517
  }
2343
3518
 
2344
3519
  // src/server/routes/schema.ts
2345
- var app12 = new Hono12();
2346
- app12.get("/", async (c) => {
3520
+ var app13 = new Hono13();
3521
+ app13.get("/", async (c) => {
2347
3522
  try {
2348
3523
  const projectPath = c.get("projectPath");
2349
3524
  const schema = await parseSchema(projectPath);
@@ -2353,13 +3528,13 @@ app12.get("/", async (c) => {
2353
3528
  return c.json({ success: false, error: message }, 500);
2354
3529
  }
2355
3530
  });
2356
- var schema_default = app12;
3531
+ var schema_default = app13;
2357
3532
 
2358
3533
  // src/server/routes/simulate.ts
2359
- import path10 from "path";
2360
- import { Hono as Hono13 } from "hono";
2361
- var app13 = new Hono13();
2362
- app13.get("/:migration", async (c) => {
3534
+ import path9 from "path";
3535
+ import { Hono as Hono14 } from "hono";
3536
+ var app14 = new Hono14();
3537
+ app14.get("/:migration", async (c) => {
2363
3538
  const projectPath = c.get("projectPath");
2364
3539
  const migrationQuery = c.req.param("migration");
2365
3540
  try {
@@ -2369,21 +3544,21 @@ app13.get("/:migration", async (c) => {
2369
3544
  (m) => m.name === migrationQuery || m.name.startsWith(migrationQuery)
2370
3545
  );
2371
3546
  if (!match) return c.json({ success: false, error: "Migration not found" }, 404);
2372
- const sqlFile = path10.join(project.migrationsPath, match.name, "migration.sql");
2373
- const dbPath = project.provider === "sqlite" && project.databaseUrl ? project.databaseUrl.replace(/^file:/, "") : void 0;
2374
- const result = await simulate(match.name, sqlFile, dbPath);
3547
+ const sqlFile = path9.join(project.migrationsPath, match.name, "migration.sql");
3548
+ const dbPath = project.provider === "sqlite" && project.databaseUrl ? resolveSqliteFilePath(project.databaseUrl, project.schemaPath) ?? void 0 : void 0;
3549
+ const result = await simulate(match.name, sqlFile, dbPath, project.provider);
2375
3550
  return c.json({ success: true, data: result });
2376
3551
  } catch (err) {
2377
3552
  const message = err instanceof Error ? err.message : String(err);
2378
3553
  return c.json({ success: false, error: message }, 500);
2379
3554
  }
2380
3555
  });
2381
- var simulate_default = app13;
3556
+ var simulate_default = app14;
2382
3557
 
2383
3558
  // src/server/routes/status.ts
2384
- import { Hono as Hono14 } from "hono";
2385
- var app14 = new Hono14();
2386
- app14.get("/", async (c) => {
3559
+ import { Hono as Hono15 } from "hono";
3560
+ var app15 = new Hono15();
3561
+ app15.get("/", async (c) => {
2387
3562
  try {
2388
3563
  const projectPath = c.get("projectPath");
2389
3564
  const status = await getProjectStatus(projectPath);
@@ -2393,23 +3568,25 @@ app14.get("/", async (c) => {
2393
3568
  return c.json({ success: false, error: message }, 500);
2394
3569
  }
2395
3570
  });
2396
- var status_default = app14;
3571
+ var status_default = app15;
2397
3572
 
2398
3573
  // src/server/index.ts
2399
3574
  var __filename = fileURLToPath(import.meta.url);
2400
- var __dirname = path11.dirname(__filename);
3575
+ var __dirname = path10.dirname(__filename);
2401
3576
  function createServer(projectPath) {
2402
- const app15 = new Hono15();
3577
+ const app16 = new Hono16();
2403
3578
  const token = randomBytes(24).toString("hex");
2404
- app15.use("*", secureHeaders());
2405
- app15.use(
3579
+ app16.use("*", secureHeaders());
3580
+ app16.use(
2406
3581
  "*",
2407
3582
  cors({
2408
3583
  origin: (origin) => {
2409
3584
  if (!origin) return null;
2410
3585
  try {
2411
3586
  const url = new URL(origin);
2412
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") return origin;
3587
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]") {
3588
+ return origin;
3589
+ }
2413
3590
  } catch {
2414
3591
  }
2415
3592
  return null;
@@ -2418,18 +3595,19 @@ function createServer(projectPath) {
2418
3595
  allowHeaders: ["Content-Type", "Authorization"]
2419
3596
  })
2420
3597
  );
2421
- app15.use("*", async (c, next) => {
3598
+ app16.use("*", async (c, next) => {
2422
3599
  c.set("requestId", randomBytes(8).toString("hex"));
2423
3600
  c.res.headers.set("X-Request-Id", c.get("requestId"));
2424
3601
  await next();
2425
3602
  });
2426
- app15.use("*", async (c, next) => {
3603
+ app16.use("*", async (c, next) => {
2427
3604
  const started = Date.now();
2428
3605
  await next();
3606
+ const parsedUrl = new URL(c.req.url);
2429
3607
  logger.debug(
2430
3608
  {
2431
3609
  method: c.req.method,
2432
- path: new URL(c.req.url).pathname,
3610
+ path: parsedUrl.pathname,
2433
3611
  status: c.res.status,
2434
3612
  durationMs: Date.now() - started,
2435
3613
  requestId: c.get("requestId")
@@ -2437,78 +3615,78 @@ function createServer(projectPath) {
2437
3615
  "HTTP request"
2438
3616
  );
2439
3617
  });
2440
- app15.use("/api/*", async (c, next) => {
3618
+ app16.use("/api/*", async (c, next) => {
2441
3619
  const authHeader = c.req.header("Authorization") ?? "";
2442
3620
  const queryToken = c.req.query("token") ?? "";
2443
3621
  const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : queryToken;
2444
3622
  if (provided !== token) {
2445
- return c.json({ success: false, error: "Unauthorized" }, 401);
3623
+ return c.json({ success: false, error: "Unauthorized \u2014 valid auth token required" }, 401);
2446
3624
  }
2447
3625
  return next();
2448
3626
  });
2449
- app15.use("*", async (c, next) => {
3627
+ app16.use("*", async (c, next) => {
2450
3628
  c.set("projectPath", projectPath);
2451
3629
  await next();
2452
3630
  });
2453
- app15.route("/api/migrations", migrations_default);
2454
- app15.route("/api/status", status_default);
2455
- app15.route("/api/schema", schema_default);
2456
- app15.route("/api/drift", drift_default);
2457
- app15.route("/api/risks", risks_default);
2458
- app15.route("/api/rollback", rollback_default);
2459
- app15.route("/api/simulate", simulate_default);
2460
- app15.route("/api/repair", repair_default);
2461
- app15.route("/api/diff", diff_default);
2462
- app15.route("/api/compare", compare_default);
2463
- app15.route("/api/git", git_default);
2464
- app15.route("/api/audit", audit_default);
2465
- app15.route("/api/config", config_default);
2466
- app15.route("/api/events", events_default);
2467
- app15.get("/health", (c) => {
3631
+ app16.route("/api/migrations", migrations_default);
3632
+ app16.route("/api/plan", plan_default);
3633
+ app16.route("/api/status", status_default);
3634
+ app16.route("/api/schema", schema_default);
3635
+ app16.route("/api/drift", drift_default);
3636
+ app16.route("/api/risks", risks_default);
3637
+ app16.route("/api/rollback", rollback_default);
3638
+ app16.route("/api/simulate", simulate_default);
3639
+ app16.route("/api/repair", repair_default);
3640
+ app16.route("/api/diff", diff_default);
3641
+ app16.route("/api/compare", compare_default);
3642
+ app16.route("/api/git", git_default);
3643
+ app16.route("/api/audit", audit_default);
3644
+ app16.route("/api/config", config_default);
3645
+ app16.route("/api/events", events_default);
3646
+ app16.get("/health", (c) => {
2468
3647
  return c.json({
2469
3648
  status: "ok",
2470
3649
  service: "prisma-flow",
2471
- version: process.env.npm_package_version ?? "0.0.0",
2472
3650
  uptime: process.uptime()
2473
3651
  });
2474
3652
  });
2475
3653
  const candidates = [
2476
- path11.resolve(__dirname, "../public"),
3654
+ path10.resolve(__dirname, "../public"),
2477
3655
  // dist/public
2478
- path11.resolve(__dirname, "../../public"),
3656
+ path10.resolve(__dirname, "../../public"),
2479
3657
  // packages/cli/public (source)
2480
- path11.resolve(__dirname, "../../..", "packages", "cli", "public"),
3658
+ path10.resolve(__dirname, "../../..", "packages", "cli", "public"),
2481
3659
  // monorepo relative
2482
- path11.resolve(process.cwd(), "public"),
3660
+ path10.resolve(process.cwd(), "public"),
2483
3661
  // cwd public
2484
- path11.resolve(process.cwd(), "packages", "cli", "public"),
2485
- path11.resolve(process.cwd(), "..", "packages", "cli", "public")
3662
+ path10.resolve(process.cwd(), "packages", "cli", "public"),
3663
+ path10.resolve(process.cwd(), "..", "packages", "cli", "public")
2486
3664
  ];
2487
3665
  let publicDir = null;
2488
3666
  for (const cand of candidates) {
2489
- if (fs12.existsSync(cand)) {
3667
+ if (fs11.existsSync(cand)) {
2490
3668
  publicDir = cand;
2491
3669
  break;
2492
3670
  }
2493
3671
  }
2494
3672
  if (!publicDir) {
2495
- publicDir = path11.resolve(__dirname, "../public");
3673
+ publicDir = path10.resolve(__dirname, "../public");
2496
3674
  }
2497
- if (!fs12.existsSync(publicDir)) {
3675
+ if (!fs11.existsSync(publicDir)) {
2498
3676
  logger.warn({ publicDir }, "serveStatic: public directory not found");
2499
3677
  }
2500
- app15.use(
3678
+ app16.use(
2501
3679
  "/*",
2502
3680
  serveStatic({
2503
3681
  root: publicDir,
2504
3682
  rewriteRequestPath: (p) => p === "/" ? "/index.html" : p
2505
3683
  })
2506
3684
  );
2507
- app15.get("*", async (c) => {
3685
+ app16.get("*", async (c) => {
2508
3686
  try {
2509
- const indexPath = path11.join(publicDir, "index.html");
2510
- if (fs12.existsSync(indexPath)) {
2511
- const content = await fs12.promises.readFile(indexPath, "utf-8");
3687
+ const indexPath = path10.join(publicDir, "index.html");
3688
+ if (fs11.existsSync(indexPath)) {
3689
+ const content = await fs11.promises.readFile(indexPath, "utf-8");
2512
3690
  return c.html(content);
2513
3691
  }
2514
3692
  return c.text("Dashboard not built \u2014 run `npm run build` first", 404);
@@ -2516,11 +3694,20 @@ function createServer(projectPath) {
2516
3694
  return c.text("Error serving dashboard", 500);
2517
3695
  }
2518
3696
  });
2519
- return { app: app15, token };
3697
+ return { app: app16, token };
3698
+ }
3699
+ function isLoopbackAddress(host) {
3700
+ const normalized = host.trim().toLowerCase();
3701
+ return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
2520
3702
  }
2521
- function startServer(app15, port = 5555) {
2522
- const server = serve({ fetch: app15.fetch, port }, (info) => {
2523
- logger.info({ port: info.port }, "PrismaFlow API running");
3703
+ function startServer(app16, port = 5555, hostname = process.env.PRISMAFLOW_HOST || "127.0.0.1") {
3704
+ if (!isLoopbackAddress(hostname)) {
3705
+ const errorMsg = `SecurityError: PrismaFlow V1 strictly rejects non-loopback bindings (${hostname}) for database safety. Only loopback addresses (127.0.0.1, localhost, ::1) are allowed.`;
3706
+ logger.error({ hostname }, errorMsg);
3707
+ throw new Error(errorMsg);
3708
+ }
3709
+ const server = serve({ fetch: app16.fetch, port, hostname }, (info) => {
3710
+ logger.info({ port: info.port, hostname }, "PrismaFlow API running on loopback");
2524
3711
  });
2525
3712
  const shutdown = () => {
2526
3713
  logger.info("Shutting down PrismaFlow server...");
@@ -2549,9 +3736,9 @@ function dashboardCommand() {
2549
3736
  }
2550
3737
  const port = options.port ? Number.parseInt(options.port, 10) : config.port ?? 5555;
2551
3738
  const shouldOpen = options.open !== false && config.openBrowser;
2552
- const { app: app15, token } = createServer(cwd);
3739
+ const { app: app16, token } = createServer(cwd);
2553
3740
  const url = `http://localhost:${port}?token=${token}`;
2554
- startServer(app15, port);
3741
+ startServer(app16, port);
2555
3742
  console.log();
2556
3743
  console.log(chalk3.bold.cyan(" \u{1F680} PrismaFlow Dashboard"));
2557
3744
  console.log(chalk3.dim("\u2501".repeat(40)));
@@ -2656,12 +3843,12 @@ function diffCommand() {
2656
3843
 
2657
3844
  // src/commands/doctor.ts
2658
3845
  import { execFile as execFile4 } from "child_process";
2659
- import fs13 from "fs/promises";
2660
- import path12 from "path";
2661
- import { promisify as promisify4 } from "util";
3846
+ import fs12 from "fs/promises";
3847
+ import path11 from "path";
3848
+ import { promisify as promisify3 } from "util";
2662
3849
  import chalk5 from "chalk";
2663
3850
  import { Command as Command5 } from "commander";
2664
- var execFileAsync2 = promisify4(execFile4);
3851
+ var execFileAsync = promisify3(execFile4);
2665
3852
  async function runChecks(checks) {
2666
3853
  let passed = 0;
2667
3854
  let failed = 0;
@@ -2730,7 +3917,7 @@ function doctorCommand() {
2730
3917
  label: "prisma schema found",
2731
3918
  run: async () => ({
2732
3919
  ok: project !== null,
2733
- detail: project ? path12.relative(cwd, project.schemaPath) : "not found"
3920
+ detail: project ? path11.relative(cwd, project.schemaPath) : "not found"
2734
3921
  })
2735
3922
  },
2736
3923
  {
@@ -2745,7 +3932,7 @@ function doctorCommand() {
2745
3932
  run: async () => {
2746
3933
  if (!project) return { ok: false, detail: "no project" };
2747
3934
  try {
2748
- await fs13.access(project.migrationsPath);
3935
+ await fs12.access(project.migrationsPath);
2749
3936
  return {
2750
3937
  ok: true,
2751
3938
  detail: `${project.migrations.length} migration(s)`
@@ -2769,14 +3956,14 @@ function doctorCommand() {
2769
3956
  label: "prismaflow.config.ts present",
2770
3957
  run: async () => {
2771
3958
  const configCandidates = [
2772
- path12.join(cwd, "prismaflow.config.ts"),
2773
- path12.join(cwd, "prismaflow.config.js"),
2774
- path12.join(cwd, "prismaflow.config.mjs")
3959
+ path11.join(cwd, "prismaflow.config.ts"),
3960
+ path11.join(cwd, "prismaflow.config.js"),
3961
+ path11.join(cwd, "prismaflow.config.mjs")
2775
3962
  ];
2776
3963
  for (const c of configCandidates) {
2777
3964
  try {
2778
- await fs13.access(c);
2779
- return { ok: true, detail: path12.relative(cwd, c) };
3965
+ await fs12.access(c);
3966
+ return { ok: true, detail: path11.relative(cwd, c) };
2780
3967
  } catch {
2781
3968
  }
2782
3969
  }
@@ -2787,11 +3974,11 @@ function doctorCommand() {
2787
3974
  label: "git repository detected",
2788
3975
  run: async () => {
2789
3976
  try {
2790
- await execFileAsync2("git", ["rev-parse", "--git-dir"], {
3977
+ await execFileAsync("git", ["rev-parse", "--git-dir"], {
2791
3978
  cwd,
2792
3979
  timeout: 5e3
2793
3980
  });
2794
- const { stdout } = await execFileAsync2(
3981
+ const { stdout } = await execFileAsync(
2795
3982
  "git",
2796
3983
  ["rev-parse", "--abbrev-ref", "HEAD"],
2797
3984
  {
@@ -2814,29 +4001,23 @@ function doctorCommand() {
2814
4001
  merge(
2815
4002
  await runChecks([
2816
4003
  {
2817
- label: "database reachable",
4004
+ label: "database reachable and verified",
2818
4005
  run: async () => {
2819
- try {
2820
- await execPrisma(cwd, ["migrate", "status", "--schema", project.schemaPath], {
2821
- timeout: 2e4
2822
- });
2823
- return { ok: true, detail: "all migrations applied" };
2824
- } catch (err) {
2825
- const error = err;
2826
- const stderr = error.stderr ?? "";
2827
- const stdout = error.stdout ?? "";
2828
- if (stderr.includes("P1001") || stderr.includes("Can't reach database server") || stderr.includes("Connection refused")) {
2829
- return {
2830
- ok: false,
2831
- detail: "database unreachable (P1001)"
2832
- };
2833
- }
2834
- const pending = stdout.match(/have not yet been applied/i);
4006
+ const adapter = getPrismaAdapter(project.prismaVersion);
4007
+ const res = await adapter.getMigrationStatus(cwd, project.schemaPath);
4008
+ if (res.connected && res.verification === "verified") {
4009
+ const pending = Array.from(res.statusMap.values()).filter(
4010
+ (s) => s === "pending"
4011
+ ).length;
2835
4012
  return {
2836
4013
  ok: true,
2837
- detail: pending ? "connected \u2014 pending migrations exist" : "connected"
4014
+ detail: pending > 0 ? `connected \u2014 ${pending} pending migration(s)` : "connected and verified"
2838
4015
  };
2839
4016
  }
4017
+ return {
4018
+ ok: false,
4019
+ detail: res.errorMessage || `unverified (${res.errorCode ?? "unknown"})`
4020
+ };
2840
4021
  }
2841
4022
  }
2842
4023
  ])
@@ -2954,8 +4135,8 @@ function historyCommand() {
2954
4135
  }
2955
4136
 
2956
4137
  // src/commands/init.ts
2957
- import fs14 from "fs/promises";
2958
- import path13 from "path";
4138
+ import fs13 from "fs/promises";
4139
+ import path12 from "path";
2959
4140
  import chalk7 from "chalk";
2960
4141
  import { Command as Command7 } from "commander";
2961
4142
  var CONFIG_TEMPLATE = `import type { PrismaFlowConfig } from 'prisma-flow'
@@ -3008,19 +4189,19 @@ export default config
3008
4189
  function initCommand() {
3009
4190
  return new Command7("init").description("Create a prismaflow.config.ts in the current project").option("-f, --force", "Overwrite existing config file").action(async (options) => {
3010
4191
  const cwd = process.cwd();
3011
- const dest = path13.join(cwd, "prismaflow.config.ts");
4192
+ const dest = path12.join(cwd, "prismaflow.config.ts");
3012
4193
  try {
3013
- await fs14.access(dest);
4194
+ await fs13.access(dest);
3014
4195
  if (!options.force) {
3015
4196
  console.log(chalk7.yellow("\u26A0 prismaflow.config.ts already exists."));
3016
4197
  console.log(chalk7.dim(" Use --force to overwrite: prisma-flow init --force"));
3017
4198
  process.exit(0);
3018
4199
  }
3019
- await fs14.writeFile(dest, CONFIG_TEMPLATE, "utf-8");
4200
+ await fs13.writeFile(dest, CONFIG_TEMPLATE, "utf-8");
3020
4201
  console.log(chalk7.green("\u2714 prismaflow.config.ts overwritten."));
3021
4202
  } catch {
3022
4203
  try {
3023
- await fs14.writeFile(dest, CONFIG_TEMPLATE, "utf-8");
4204
+ await fs13.writeFile(dest, CONFIG_TEMPLATE, "utf-8");
3024
4205
  console.log(chalk7.green("\u2714 Created prismaflow.config.ts"));
3025
4206
  console.log(chalk7.dim(" Edit it to customise PrismaFlow for your project."));
3026
4207
  console.log();
@@ -3040,8 +4221,8 @@ function initCommand() {
3040
4221
  }
3041
4222
 
3042
4223
  // src/commands/inspect.ts
3043
- import fs15 from "fs/promises";
3044
- import path14 from "path";
4224
+ import fs14 from "fs/promises";
4225
+ import path13 from "path";
3045
4226
  import chalk8 from "chalk";
3046
4227
  import { Command as Command8 } from "commander";
3047
4228
  function inspectCommand() {
@@ -3064,10 +4245,10 @@ function inspectCommand() {
3064
4245
  );
3065
4246
  process.exit(1);
3066
4247
  }
3067
- const sqlFile = path14.join(project.migrationsPath, match.name, "migration.sql");
4248
+ const sqlFile = path13.join(project.migrationsPath, match.name, "migration.sql");
3068
4249
  let sql = "";
3069
4250
  try {
3070
- sql = await fs15.readFile(sqlFile, "utf-8");
4251
+ sql = await fs14.readFile(sqlFile, "utf-8");
3071
4252
  } catch {
3072
4253
  sql = "-- SQL file not found";
3073
4254
  }
@@ -3148,20 +4329,232 @@ function inspectCommand() {
3148
4329
  );
3149
4330
  }
3150
4331
 
3151
- // src/commands/repair.ts
4332
+ // src/commands/plan.ts
4333
+ import fs15 from "fs/promises";
4334
+ import path14 from "path";
3152
4335
  import chalk9 from "chalk";
3153
4336
  import { Command as Command9 } from "commander";
4337
+ function normalizeFormat(format, json) {
4338
+ if (json) return "json";
4339
+ if (format === "json" || format === "markdown" || format === "md") {
4340
+ return format === "md" ? "markdown" : format;
4341
+ }
4342
+ return "human";
4343
+ }
4344
+ function renderMarkdown(plan) {
4345
+ const lines = [
4346
+ "# PrismaFlow Deployment Plan",
4347
+ "",
4348
+ `Generated: ${plan.generatedAt}`,
4349
+ `Decision: ${plan.decision}`,
4350
+ `Score: ${plan.score}/100`,
4351
+ "",
4352
+ plan.summary,
4353
+ "",
4354
+ "## Project",
4355
+ "",
4356
+ `- Schema: ${plan.project.schemaPath}`,
4357
+ `- Migrations: ${plan.project.migrationsPath}`,
4358
+ `- Provider: ${plan.project.provider ?? "unknown"}`,
4359
+ `- Prisma version: ${plan.project.prismaVersion ?? "unknown"}`,
4360
+ `- Package manager: ${plan.project.packageManager ?? "unknown"}`,
4361
+ `- DATABASE_URL detected: ${plan.project.hasDatabaseUrl ? "yes" : "no"}`,
4362
+ "",
4363
+ "## Readiness Checks",
4364
+ "",
4365
+ "| Check | Status | Message |",
4366
+ "| --- | --- | --- |",
4367
+ ...plan.checks.map(
4368
+ (check) => `| ${check.label} | ${check.passed ? "pass" : "fail"} | ${check.message} |`
4369
+ ),
4370
+ "",
4371
+ "## Migration Summary",
4372
+ "",
4373
+ `- Total: ${plan.migrations.total}`,
4374
+ `- Applied: ${plan.migrations.applied}`,
4375
+ `- Pending: ${plan.migrations.pending}`,
4376
+ `- Failed: ${plan.migrations.failed}`,
4377
+ `- Drift: ${plan.drift.detected ? `${plan.drift.count} item(s)` : "none"}`,
4378
+ "",
4379
+ "## Next Actions",
4380
+ "",
4381
+ ...plan.actions.map(
4382
+ (action) => `- **${action.priority}: ${action.title}** \u2014 ${action.detail}${action.command ? `
4383
+ - Command: \`${action.command}\`` : ""}`
4384
+ ),
4385
+ "",
4386
+ "## Useful Commands",
4387
+ "",
4388
+ ...plan.commands.map((command) => `- \`${command.command}\` \u2014 ${command.reason}`),
4389
+ ""
4390
+ ];
4391
+ if (plan.migrations.highestRisk) {
4392
+ lines.splice(
4393
+ lines.indexOf("## Next Actions") - 1,
4394
+ 0,
4395
+ "",
4396
+ "## Highest Risk",
4397
+ "",
4398
+ `- Migration: ${plan.migrations.highestRisk.name}`,
4399
+ `- Level: ${plan.migrations.highestRisk.level}`,
4400
+ `- Score: ${plan.migrations.highestRisk.score}/100`
4401
+ );
4402
+ }
4403
+ return `${lines.join("\n")}
4404
+ `;
4405
+ }
4406
+ async function writeOutput(content, outputPath) {
4407
+ if (!outputPath) {
4408
+ process.stdout.write(content);
4409
+ return null;
4410
+ }
4411
+ const resolved = path14.resolve(process.cwd(), outputPath);
4412
+ await fs15.mkdir(path14.dirname(resolved), { recursive: true });
4413
+ await fs15.writeFile(resolved, content, "utf-8");
4414
+ return resolved;
4415
+ }
4416
+ function actionIcon(priority) {
4417
+ if (priority === "blocker") return chalk9.red("!");
4418
+ if (priority === "recommended") return chalk9.yellow(">");
4419
+ return chalk9.dim("-");
4420
+ }
4421
+ function decisionColor(decision) {
4422
+ if (decision === "ready") return chalk9.green;
4423
+ if (decision === "attention") return chalk9.yellow;
4424
+ return chalk9.red;
4425
+ }
4426
+ function renderHuman(plan) {
4427
+ const color = decisionColor(plan.decision);
4428
+ const lines = [
4429
+ "",
4430
+ chalk9.bold.cyan(" PrismaFlow Deployment Plan"),
4431
+ chalk9.dim("\u2501".repeat(60)),
4432
+ `${chalk9.bold("Decision:")} ${color(plan.decision.toUpperCase())}`,
4433
+ `${chalk9.bold("Score:")} ${color(`${plan.score}/100`)}`,
4434
+ `${chalk9.bold("Summary:")} ${plan.summary}`,
4435
+ "",
4436
+ chalk9.bold("Project"),
4437
+ ` Schema: ${chalk9.dim(plan.project.schemaPath)}`,
4438
+ ` Migrations: ${chalk9.dim(plan.project.migrationsPath)}`,
4439
+ ` Provider: ${chalk9.dim(plan.project.provider ?? "unknown")}`,
4440
+ "",
4441
+ chalk9.bold("Readiness")
4442
+ ];
4443
+ for (const check of plan.checks) {
4444
+ lines.push(
4445
+ ` ${check.passed ? chalk9.green("\u2713") : chalk9.red("\u2717")} ${check.label}: ${chalk9.dim(
4446
+ check.message
4447
+ )}`
4448
+ );
4449
+ }
4450
+ lines.push(
4451
+ "",
4452
+ chalk9.bold("Migrations"),
4453
+ ` ${plan.migrations.applied} applied / ${plan.migrations.pending} pending / ${plan.migrations.failed} failed`
4454
+ );
4455
+ if (plan.migrations.highestRisk) {
4456
+ lines.push(
4457
+ ` Highest risk: ${plan.migrations.highestRisk.name} ${chalk9.dim(
4458
+ `(${plan.migrations.highestRisk.level}, ${plan.migrations.highestRisk.score}/100)`
4459
+ )}`
4460
+ );
4461
+ }
4462
+ lines.push("", chalk9.bold("Next Actions"));
4463
+ for (const action of plan.actions) {
4464
+ lines.push(` ${actionIcon(action.priority)} ${chalk9.bold(action.title)}`);
4465
+ lines.push(` ${chalk9.dim(action.detail)}`);
4466
+ if (action.command) lines.push(` ${chalk9.cyan(action.command)}`);
4467
+ }
4468
+ lines.push("", chalk9.bold("Useful Commands"));
4469
+ for (const command of plan.commands.slice(0, 5)) {
4470
+ lines.push(` ${chalk9.cyan(command.command)}`);
4471
+ lines.push(` ${chalk9.dim(command.reason)}`);
4472
+ }
4473
+ lines.push("", chalk9.dim("\u2501".repeat(60)), "");
4474
+ return `${lines.join("\n")}`;
4475
+ }
4476
+ function exitCodeForPlan(plan, ci) {
4477
+ if (!ci) return 0;
4478
+ if (plan.decision === "blocked") return 2;
4479
+ if (plan.decision === "attention") return 1;
4480
+ return 0;
4481
+ }
4482
+ function planCommand() {
4483
+ return new Command9("plan").description("Generate an actionable migration deployment plan").option("--format <format>", "Output format: human, json, or markdown", "human").option("--json", "Shortcut for --format json").option("--ci", "Exit non-zero when the plan is attention or blocked").option("-o, --output <path>", "Write JSON or Markdown output to a file").action(
4484
+ async (options) => {
4485
+ const cwd = process.cwd();
4486
+ const format = normalizeFormat(options.format, options.json);
4487
+ try {
4488
+ const plan = await buildDeploymentPlan(cwd);
4489
+ const content = format === "json" ? `${JSON.stringify(plan, null, 2)}
4490
+ ` : format === "markdown" ? renderMarkdown(plan) : renderHuman(plan);
4491
+ const writtenPath = await writeOutput(content, options.output);
4492
+ if (writtenPath) {
4493
+ process.stdout.write(`${chalk9.green("\u2713 Plan written:")} ${writtenPath}
4494
+ `);
4495
+ }
4496
+ await Promise.all([
4497
+ writeAuditEntry(
4498
+ cwd,
4499
+ "deployment.plan",
4500
+ plan.decision === "ready" ? "success" : "warning",
4501
+ {
4502
+ decision: plan.decision,
4503
+ score: plan.score,
4504
+ pending: plan.migrations.pending,
4505
+ failed: plan.migrations.failed,
4506
+ drift: plan.drift.count
4507
+ }
4508
+ ),
4509
+ trackEvent("plan", plan.migrations.total)
4510
+ ]).catch(() => {
4511
+ });
4512
+ process.exit(exitCodeForPlan(plan, options.ci));
4513
+ } catch (error) {
4514
+ const message = error instanceof Error ? error.message : String(error);
4515
+ if (format === "json") {
4516
+ process.stdout.write(`${JSON.stringify({ error: message }, null, 2)}
4517
+ `);
4518
+ } else {
4519
+ process.stderr.write(`${chalk9.red(`\u2716 Plan failed: ${message}`)}
4520
+ `);
4521
+ }
4522
+ await writeAuditEntry(cwd, "deployment.plan", "failure", { error: message }).catch(
4523
+ () => {
4524
+ }
4525
+ );
4526
+ process.exit(4);
4527
+ }
4528
+ }
4529
+ );
4530
+ }
4531
+
4532
+ // src/commands/repair.ts
4533
+ import chalk10 from "chalk";
4534
+ import { Command as Command10 } from "commander";
3154
4535
  function repairCommand() {
3155
- return new Command9("repair").description("Detect drift and generate (or apply) repair suggestions").option("--apply", "Auto-apply safe repair steps (APPLY_MIGRATION only)").option("--json", "Output as JSON").action(async (options) => {
4536
+ return new Command10("repair").description("Detect schema drift and generate plan-only recovery guidance").option("--apply", "Disabled in V1: mutating repair is plan-only").option("--json", "Output as JSON").action(async (options) => {
3156
4537
  const cwd = process.cwd();
4538
+ if (options.apply) {
4539
+ const errorMsg = "Automatic mutating repair is disabled in PrismaFlow V1 for database safety. `prisma-flow repair` is strictly plan-only. Please review the recovery plan and execute any verified actions manually.";
4540
+ if (options.json) {
4541
+ process.stdout.write(
4542
+ `${JSON.stringify({ ok: false, error: errorMsg, mutatingDisabled: true })}
4543
+ `
4544
+ );
4545
+ } else {
4546
+ console.error(chalk10.red(`\u2716 ${errorMsg}`));
4547
+ }
4548
+ process.exit(1);
4549
+ }
3157
4550
  try {
3158
4551
  const project = await detectPrismaProject(cwd);
3159
4552
  if (!project) {
3160
- console.error(chalk9.red("\u2716 No Prisma project found."));
4553
+ console.error(chalk10.red("\u2716 No Prisma project found."));
3161
4554
  process.exit(1);
3162
4555
  }
3163
4556
  if (!options.json) {
3164
- process.stdout.write(chalk9.dim(" Detecting drift...\n"));
4557
+ process.stdout.write(chalk10.dim(" Detecting schema drift...\n"));
3165
4558
  }
3166
4559
  const driftResult = await detectDrift(cwd);
3167
4560
  if (driftResult.status === "error") {
@@ -3171,80 +4564,71 @@ function repairCommand() {
3171
4564
  `
3172
4565
  );
3173
4566
  } else {
3174
- console.error(chalk9.red(`\u2716 Drift detection failed: ${driftResult.errorMessage}`));
4567
+ console.error(chalk10.red(`\u2716 Drift detection failed: ${driftResult.errorMessage}`));
3175
4568
  }
3176
4569
  process.exit(1);
3177
4570
  }
3178
4571
  if (driftResult.status === "clean" || driftResult.items.length === 0) {
3179
4572
  if (options.json) {
3180
4573
  process.stdout.write(
3181
- `${JSON.stringify({ ok: true, drifted: false, suggestions: [] })}
4574
+ `${JSON.stringify({ ok: true, drifted: false, plan: buildDriftRepairPlan([]) })}
3182
4575
  `
3183
4576
  );
3184
4577
  } else {
3185
- console.log(chalk9.green(" \u2714 No drift detected \u2014 database is in sync."));
4578
+ console.log(chalk10.green(" \u2714 No drift detected \u2014 database schema is in sync."));
3186
4579
  }
3187
4580
  process.exit(0);
3188
4581
  }
3189
- const suggestions = generateRepairSuggestions(driftResult.items, project.migrationsPath);
4582
+ const plan = buildDriftRepairPlan(driftResult.items, project.migrationsPath);
3190
4583
  if (options.json) {
3191
- process.stdout.write(
3192
- `${JSON.stringify({ ok: true, drifted: true, suggestions }, null, 2)}
3193
- `
3194
- );
3195
- if (options.apply) {
3196
- const results = await applyRepairs(suggestions, project.schemaPath, cwd);
3197
- const out = JSON.stringify({ applied: results }, null, 2);
3198
- process.stdout.write(`${out}
4584
+ process.stdout.write(`${JSON.stringify({ ok: true, drifted: true, plan }, null, 2)}
3199
4585
  `);
3200
- }
3201
4586
  process.exit(0);
3202
4587
  }
3203
4588
  console.log();
3204
- console.log(chalk9.bold.cyan(" \u{1F527} Drift Repair"));
3205
- console.log(chalk9.dim("\u2501".repeat(50)));
4589
+ console.log(chalk10.bold.cyan(" \u{1F527} PrismaFlow Drift Recovery Plan (Plan-Only)"));
4590
+ console.log(chalk10.dim("\u2501".repeat(60)));
4591
+ console.log(
4592
+ ` ${chalk10.bold("Drift items found:")} ${chalk10.yellow(driftResult.items.length.toString())}`
4593
+ );
3206
4594
  console.log(
3207
- ` ${chalk9.bold("Drift items found:")} ${chalk9.yellow(driftResult.items.length.toString())}`
4595
+ chalk10.dim(" All suggestions are advisory for manual review before operator execution.")
3208
4596
  );
3209
4597
  console.log();
3210
- for (const suggestion of suggestions) {
3211
- const riskColor = suggestion.risk === "high" ? chalk9.red : suggestion.risk === "medium" ? chalk9.yellow : chalk9.green;
4598
+ for (const suggestion of plan.suggestions) {
4599
+ const riskColor = suggestion.risk === "critical" || suggestion.risk === "high" ? chalk10.red : suggestion.risk === "medium" ? chalk10.yellow : chalk10.green;
3212
4600
  console.log(
3213
4601
  ` ${riskColor("\u25CF")} [${riskColor(suggestion.risk.toUpperCase())}] ${suggestion.description}`
3214
4602
  );
3215
4603
  console.log(
3216
- ` Strategy: ${chalk9.cyan(suggestion.strategy)} (${suggestion.automated ? chalk9.green("automated") : chalk9.yellow("manual")})`
4604
+ ` Strategy: ${chalk10.cyan(suggestion.strategy)} ${chalk10.dim("(manual operator review)")}`
3217
4605
  );
3218
- if (suggestion.sql) {
3219
- console.log(chalk9.dim(` ${suggestion.sql.split("\n")[0]}`));
4606
+ for (const w of suggestion.warnings) {
4607
+ console.log(` ${chalk10.yellow("\u26A0")} ${chalk10.dim(w)}`);
3220
4608
  }
3221
- }
3222
- if (options.apply) {
3223
- console.log();
3224
- console.log(chalk9.bold(" Applying auto-repair steps..."));
3225
- const results = await applyRepairs(suggestions, project.schemaPath, cwd);
3226
- for (const r of results) {
3227
- console.log(
3228
- ` ${r.success ? chalk9.green("\u2713") : chalk9.red("\u2716")} ${r.migrationName}${r.error ? chalk9.dim(` \u2014 ${r.error}`) : ""}`
3229
- );
4609
+ if (suggestion.sql) {
4610
+ console.log(chalk10.dim(` ${suggestion.sql.split("\n")[0]}`));
3230
4611
  }
3231
- } else {
3232
- console.log();
3233
- console.log(chalk9.dim(" To auto-apply safe repairs: prisma-flow repair --apply"));
3234
4612
  }
3235
4613
  console.log();
4614
+ console.log(
4615
+ chalk10.dim(
4616
+ " Note: PrismaFlow V1 is strictly plan-only. To resolve history records, use `prisma migrate resolve` manually."
4617
+ )
4618
+ );
4619
+ console.log();
3236
4620
  await Promise.all([
3237
4621
  writeAuditEntry(cwd, "drift.repair", "success", {
3238
4622
  items: driftResult.items.length,
3239
- applied: options.apply
4623
+ planOnly: true
3240
4624
  }),
3241
4625
  trackEvent("repair", driftResult.items.length)
3242
4626
  ]).catch(() => {
3243
4627
  });
3244
- process.exit(driftResult.items.length > 0 && !options.apply ? 2 : 0);
4628
+ process.exit(driftResult.items.length > 0 ? 2 : 0);
3245
4629
  } catch (err) {
3246
4630
  const message = err instanceof Error ? err.message : String(err);
3247
- console.error(chalk9.red(`\u2716 ${message}`));
4631
+ console.error(chalk10.red(`\u2716 ${message}`));
3248
4632
  process.exit(1);
3249
4633
  }
3250
4634
  });
@@ -3253,8 +4637,8 @@ function repairCommand() {
3253
4637
  // src/commands/report.ts
3254
4638
  import fs16 from "fs/promises";
3255
4639
  import path15 from "path";
3256
- import chalk10 from "chalk";
3257
- import { Command as Command10 } from "commander";
4640
+ import chalk11 from "chalk";
4641
+ import { Command as Command11 } from "commander";
3258
4642
  function formatDate(value) {
3259
4643
  if (!value) return "Not recorded";
3260
4644
  const date = new Date(value);
@@ -3275,11 +4659,11 @@ function buildRecommendations(status, migrations, driftCount) {
3275
4659
  const recommendations = [
3276
4660
  ...readinessActions,
3277
4661
  ...uniqueRecommendations(topRiskFactors),
3278
- ...driftCount > 0 ? ["Review drift details and reconcile the Prisma schema before deployment."] : []
4662
+ ...driftCount > 0 ? ["Review schema drift details and reconcile the Prisma schema before deployment."] : []
3279
4663
  ];
3280
4664
  return recommendations.length > 0 ? recommendations : ["No blocking issues detected. Keep reviewing generated SQL before production deploys."];
3281
4665
  }
3282
- function renderMarkdown(report) {
4666
+ function renderMarkdown2(report) {
3283
4667
  const lines = [
3284
4668
  "# PrismaFlow Report",
3285
4669
  "",
@@ -3297,11 +4681,13 @@ function renderMarkdown(report) {
3297
4681
  "## Summary",
3298
4682
  "",
3299
4683
  `- Database connected: ${report.summary.connected ? "yes" : "no"}`,
4684
+ `- Migration verification: ${report.summary.migrationVerification}`,
3300
4685
  `- Applied migrations: ${report.summary.migrationsApplied}`,
3301
4686
  `- Pending migrations: ${report.summary.migrationsPending}`,
3302
4687
  `- Failed migrations: ${report.summary.migrationsFailed}`,
3303
- `- Drift: ${report.summary.driftDetected ? `${report.summary.driftCount} item(s)` : "none"}`,
3304
- `- Risk level: ${report.summary.riskLevel}`,
4688
+ `- Unknown migrations: ${report.summary.migrationsUnknown}`,
4689
+ `- Drift: ${report.summary.driftDetected ? `${report.summary.driftCount} item(s)` : report.summary.driftStatus}`,
4690
+ `- Estimated risk level: ${report.summary.riskLevel} (heuristic)`,
3305
4691
  `- Health score: ${report.summary.healthScore}/100`,
3306
4692
  `- Deployment readiness: ${report.readiness.summary} (${report.readiness.score}/100)`,
3307
4693
  "",
@@ -3315,7 +4701,7 @@ function renderMarkdown(report) {
3315
4701
  "",
3316
4702
  "## Migration Timeline",
3317
4703
  "",
3318
- "| Migration | Status | Created | Applied | Duration | Risk |",
4704
+ "| Migration | Status | Created | Applied | Duration | Estimated Risk |",
3319
4705
  "| --- | --- | --- | --- | --- | --- |",
3320
4706
  ...report.migrations.map(
3321
4707
  (migration) => `| ${migration.name} | ${migration.status} | ${formatDate(migration.createdAt)} | ${formatDate(
@@ -3327,7 +4713,9 @@ function renderMarkdown(report) {
3327
4713
  ""
3328
4714
  ];
3329
4715
  if (report.drift.items.length === 0) {
3330
- lines.push("No drift detected.");
4716
+ lines.push(
4717
+ report.drift.status === "clean" ? "No drift detected." : `Drift status: ${report.drift.status}`
4718
+ );
3331
4719
  } else {
3332
4720
  for (const item of report.drift.items) {
3333
4721
  lines.push(`- ${item.type}: ${item.description}`);
@@ -3350,19 +4738,19 @@ async function writeReport(content, outputPath) {
3350
4738
  await fs16.writeFile(resolved, content, "utf-8");
3351
4739
  return resolved;
3352
4740
  }
3353
- function normalizeFormat(format, json) {
4741
+ function normalizeFormat2(format, json) {
3354
4742
  if (json) return "json";
3355
4743
  if (format === "md" || format === "markdown") return "markdown";
3356
4744
  return "json";
3357
4745
  }
3358
4746
  function reportCommand() {
3359
- return new Command10("report").description("Generate a local PrismaFlow report for review or CI artifacts").option("--format <format>", "Output format: json or markdown", "json").option("--json", "Shortcut for --format json").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options) => {
4747
+ return new Command11("report").description("Generate a local PrismaFlow report for review or CI artifacts").option("--format <format>", "Output format: json or markdown", "json").option("--json", "Shortcut for --format json").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options) => {
3360
4748
  const cwd = process.cwd();
3361
- const format = normalizeFormat(options.format, options.json);
4749
+ const format = normalizeFormat2(options.format, options.json);
3362
4750
  try {
3363
4751
  const project = await detectPrismaProject(cwd);
3364
4752
  if (!project) {
3365
- process.stderr.write(`${chalk10.red("\u2716 No Prisma project found.")}
4753
+ process.stderr.write(`${chalk11.red("\u2716 No Prisma project found.")}
3366
4754
  `);
3367
4755
  process.exit(4);
3368
4756
  }
@@ -3392,11 +4780,14 @@ function reportCommand() {
3392
4780
  },
3393
4781
  summary: {
3394
4782
  connected: status.connected,
4783
+ migrationVerification: status.migrationVerification,
3395
4784
  migrationsApplied: status.migrationsApplied,
3396
4785
  migrationsPending: status.migrationsPending,
3397
4786
  migrationsFailed: status.migrationsFailed,
4787
+ migrationsUnknown: status.migrationsUnknown,
3398
4788
  driftDetected: status.driftDetected,
3399
4789
  driftCount: status.driftCount,
4790
+ driftStatus: status.driftStatus,
3400
4791
  riskLevel: status.riskLevel,
3401
4792
  healthScore: status.healthScore
3402
4793
  },
@@ -3416,16 +4807,16 @@ function reportCommand() {
3416
4807
  },
3417
4808
  recommendations: buildRecommendations(status, reportMigrations, drift.items.length)
3418
4809
  };
3419
- const content = format === "markdown" ? renderMarkdown(report) : `${JSON.stringify(report, null, 2)}
4810
+ const content = format === "markdown" ? renderMarkdown2(report) : `${JSON.stringify(report, null, 2)}
3420
4811
  `;
3421
4812
  const writtenPath = await writeReport(content, options.output);
3422
4813
  if (writtenPath) {
3423
- process.stdout.write(`${chalk10.green("\u2714 Report written:")} ${writtenPath}
4814
+ process.stdout.write(`${chalk11.green("\u2714 Report written:")} ${writtenPath}
3424
4815
  `);
3425
4816
  }
3426
4817
  } catch (error) {
3427
4818
  const message = error instanceof Error ? error.message : String(error);
3428
- process.stderr.write(`${chalk10.red(`\u2716 Report failed: ${message}`)}
4819
+ process.stderr.write(`${chalk11.red(`\u2716 Report failed: ${message}`)}
3429
4820
  `);
3430
4821
  process.exit(4);
3431
4822
  }
@@ -3434,23 +4825,23 @@ function reportCommand() {
3434
4825
 
3435
4826
  // src/commands/rollback.ts
3436
4827
  import path16 from "path";
3437
- import chalk11 from "chalk";
3438
- import { Command as Command11 } from "commander";
4828
+ import chalk12 from "chalk";
4829
+ import { Command as Command12 } from "commander";
3439
4830
  function rollbackCommand() {
3440
- return new Command11("rollback").description("Generate a rollback plan for a migration").argument("<migration>", "Migration name or timestamp prefix").option("--json", "Output as JSON").option("--print-sql", "Print the rollback SQL to stdout").option("--include-manual", "Include manual-only steps in SQL output", true).action(
4831
+ return new Command12("rollback").description("Generate a rollback plan for a migration").argument("<migration>", "Migration name or timestamp prefix").option("--json", "Output as JSON").option("--print-sql", "Print the rollback SQL to stdout").option("--include-manual", "Include manual-only steps in SQL output", true).action(
3441
4832
  async (migrationQuery, options) => {
3442
4833
  const cwd = process.cwd();
3443
4834
  try {
3444
4835
  const project = await detectPrismaProject(cwd);
3445
4836
  if (!project) {
3446
- console.error(chalk11.red("\u2716 No Prisma project found."));
4837
+ console.error(chalk12.red("\u2716 No Prisma project found."));
3447
4838
  process.exit(1);
3448
4839
  }
3449
4840
  const match = project.migrations.find(
3450
4841
  (m) => m.name === migrationQuery || m.name.startsWith(migrationQuery) || m.timestamp.startsWith(migrationQuery)
3451
4842
  );
3452
4843
  if (!match) {
3453
- console.error(chalk11.red(`\u2716 Migration "${migrationQuery}" not found.`));
4844
+ console.error(chalk12.red(`\u2716 Migration "${migrationQuery}" not found.`));
3454
4845
  process.exit(1);
3455
4846
  }
3456
4847
  const sqlFile = path16.join(project.migrationsPath, match.name, "migration.sql");
@@ -3468,36 +4859,35 @@ function rollbackCommand() {
3468
4859
  const autoCount = plan.steps.filter((s) => s.automated).length;
3469
4860
  const manualCount = plan.steps.filter((s) => !s.automated).length;
3470
4861
  console.log();
3471
- console.log(chalk11.bold.cyan(` \u21A9 Rollback Plan: ${match.name}`));
3472
- console.log(chalk11.dim("\u2501".repeat(60)));
4862
+ console.log(chalk12.bold.cyan(` \u21A9 Rollback Plan: ${match.name}`));
4863
+ console.log(chalk12.dim("\u2501".repeat(60)));
3473
4864
  console.log(
3474
- ` ${chalk11.bold("Steps:")} ${autoCount} automated, ${manualCount > 0 ? chalk11.yellow(`${manualCount} manual`) : chalk11.green("0 manual")}`
4865
+ ` ${chalk12.bold("Steps:")} ${autoCount} automated, ${manualCount > 0 ? chalk12.yellow(`${manualCount} manual`) : chalk12.green("0 manual")}`
3475
4866
  );
3476
4867
  console.log(
3477
- ` ${chalk11.bold("Fully automated:")} ${plan.automated ? chalk11.green("Yes") : chalk11.yellow("No \u2014 manual review required")}`
4868
+ ` ${chalk12.bold("Fully automated:")} ${plan.automated ? chalk12.green("Yes") : chalk12.yellow("No \u2014 manual review required")}`
3478
4869
  );
3479
4870
  console.log();
3480
- for (let i = 0; i < plan.steps.length; i++) {
3481
- const step = plan.steps[i];
3482
- const icon = step.automated ? chalk11.green("\u2713") : chalk11.yellow("\u26A0");
3483
- const label = step.automated ? chalk11.dim("[auto]") : chalk11.yellow("[manual]");
4871
+ for (const [i, step] of plan.steps.entries()) {
4872
+ const icon = step.automated ? chalk12.green("\u2713") : chalk12.yellow("\u26A0");
4873
+ const label = step.automated ? chalk12.dim("[auto]") : chalk12.yellow("[manual]");
3484
4874
  console.log(` ${icon} Step ${i + 1} ${label}`);
3485
- console.log(` ${chalk11.dim("Forward:")} ${step.forwardSql.slice(0, 60)}`);
3486
- console.log(` ${chalk11.dim("Rollback:")} ${step.rollbackSql.slice(0, 60)}`);
4875
+ console.log(` ${chalk12.dim("Forward:")} ${step.forwardSql.slice(0, 60)}`);
4876
+ console.log(` ${chalk12.dim("Rollback:")} ${step.rollbackSql.slice(0, 60)}`);
3487
4877
  if (step.warning) {
3488
- console.log(` ${chalk11.yellow("\u26A0")} ${step.warning}`);
4878
+ console.log(` ${chalk12.yellow("\u26A0")} ${step.warning}`);
3489
4879
  }
3490
4880
  }
3491
4881
  if (plan.warnings.length > 0) {
3492
4882
  console.log();
3493
- console.log(chalk11.bold(" Warnings:"));
4883
+ console.log(chalk12.bold(" Warnings:"));
3494
4884
  for (const w of plan.warnings) {
3495
- console.log(` ${chalk11.yellow("\u26A0")} ${w}`);
4885
+ console.log(` ${chalk12.yellow("\u26A0")} ${w}`);
3496
4886
  }
3497
4887
  }
3498
4888
  console.log();
3499
- console.log(chalk11.dim(" To generate rollback SQL:"));
3500
- console.log(chalk11.dim(` prisma-flow rollback ${match.name} --print-sql`));
4889
+ console.log(chalk12.dim(" To generate rollback SQL:"));
4890
+ console.log(chalk12.dim(` prisma-flow rollback ${match.name} --print-sql`));
3501
4891
  console.log();
3502
4892
  await Promise.all([
3503
4893
  writeAuditEntry(cwd, "migration.rollback", "success", {
@@ -3509,7 +4899,7 @@ function rollbackCommand() {
3509
4899
  process.exit(plan.hasManualSteps ? 1 : 0);
3510
4900
  } catch (err) {
3511
4901
  const message = err instanceof Error ? err.message : String(err);
3512
- console.error(chalk11.red(`\u2716 ${message}`));
4902
+ console.error(chalk12.red(`\u2716 ${message}`));
3513
4903
  process.exit(1);
3514
4904
  }
3515
4905
  }
@@ -3518,86 +4908,92 @@ function rollbackCommand() {
3518
4908
 
3519
4909
  // src/commands/simulate.ts
3520
4910
  import path17 from "path";
3521
- import chalk12 from "chalk";
3522
- import { Command as Command12 } from "commander";
4911
+ import chalk13 from "chalk";
4912
+ import { Command as Command13 } from "commander";
3523
4913
  function simulateCommand() {
3524
- return new Command12("simulate").description("Dry-run a migration to preview its effect without applying it").argument("<migration>", "Migration name or timestamp prefix").option("--json", "Output as JSON").option("--fail-on-destructive", "Exit with code 2 if destructive statements are found").action(
4914
+ return new Command13("simulate").description("Dry-run a migration or perform static SQL analysis to preview changes").argument("<migration>", "Migration name or timestamp prefix").option("--json", "Output as JSON").option("--fail-on-destructive", "Exit with code 2 if destructive statements are found").action(
3525
4915
  async (migrationQuery, options) => {
3526
4916
  const cwd = process.cwd();
3527
4917
  try {
3528
4918
  const project = await detectPrismaProject(cwd);
3529
4919
  if (!project) {
3530
- console.error(chalk12.red("\u2716 No Prisma project found."));
4920
+ console.error(chalk13.red("\u2716 No Prisma project found."));
3531
4921
  process.exit(1);
3532
4922
  }
3533
4923
  const match = project.migrations.find(
3534
4924
  (m) => m.name === migrationQuery || m.name.startsWith(migrationQuery) || m.timestamp.startsWith(migrationQuery)
3535
4925
  );
3536
4926
  if (!match) {
3537
- console.error(chalk12.red(`\u2716 Migration "${migrationQuery}" not found.`));
4927
+ console.error(chalk13.red(`\u2716 Migration "${migrationQuery}" not found.`));
3538
4928
  process.exit(1);
3539
4929
  }
3540
4930
  const sqlFile = path17.join(project.migrationsPath, match.name, "migration.sql");
3541
- const dbPath = project.provider === "sqlite" && project.databaseUrl ? project.databaseUrl.replace(/^file:/, "") : void 0;
3542
- const result = await simulate(match.name, sqlFile, dbPath);
4931
+ const dbPath = project.provider === "sqlite" && project.databaseUrl ? resolveSqliteFilePath(project.databaseUrl, project.schemaPath) ?? void 0 : void 0;
4932
+ const result = await simulate(match.name, sqlFile, dbPath, project.provider);
3543
4933
  if (options.json) {
3544
4934
  process.stdout.write(`${JSON.stringify(result, null, 2)}
3545
4935
  `);
3546
4936
  process.exit(0);
3547
4937
  }
3548
4938
  console.log();
3549
- console.log(chalk12.bold.cyan(` \u{1F52C} Simulation: ${match.name}`));
3550
- console.log(chalk12.dim("\u2501".repeat(60)));
3551
- console.log(` ${chalk12.bold("Mode:")} ${result.mode}`);
3552
- console.log(
3553
- ` ${chalk12.bold("Result:")} ${result.wouldSucceed ? chalk12.green("Would succeed") : chalk12.red("Would fail")}`
3554
- );
4939
+ console.log(chalk13.bold.cyan(` \u{1F52C} Migration Simulation: ${match.name}`));
4940
+ console.log(chalk13.dim("\u2501".repeat(60)));
4941
+ let verificationLabel = chalk13.yellow("Static analysis only \u2014 execution not verified");
4942
+ if (result.verification === "executed") {
4943
+ verificationLabel = result.outcome === "success" ? chalk13.green("Executed successfully in shadow database") : chalk13.red("Executed with failure in shadow database");
4944
+ } else if (result.verification === "not-verified") {
4945
+ verificationLabel = chalk13.dim("Not verified for this provider");
4946
+ }
4947
+ console.log(` ${chalk13.bold("Verification:")} ${verificationLabel}`);
4948
+ console.log(` ${chalk13.bold("Outcome:")} ${result.outcome.toUpperCase()}`);
3555
4949
  console.log(
3556
- ` ${chalk12.bold("Statements:")} ${result.statements.length} (${result.destructiveStatements} destructive)`
4950
+ ` ${chalk13.bold("Statements:")} ${result.statements.length} (${result.destructiveStatements} destructive)`
3557
4951
  );
3558
4952
  console.log();
3559
4953
  for (const stmt of result.statements) {
3560
- const icon = stmt.isDestructive ? chalk12.red("\u26A0") : chalk12.green("\u2713");
3561
- const typeLabel = chalk12.dim(`[${stmt.type}]`);
4954
+ const icon = stmt.isDestructive ? chalk13.red("\u26A0") : chalk13.green("\u2713");
4955
+ const typeLabel = chalk13.dim(`[${stmt.type}]`);
3562
4956
  const preview = stmt.sql.slice(0, 70).replace(/\n/g, " ");
3563
4957
  console.log(
3564
- ` ${icon} ${typeLabel} ${preview}${stmt.sql.length > 70 ? chalk12.dim("\u2026") : ""}`
4958
+ ` ${icon} ${typeLabel} ${preview}${stmt.sql.length > 70 ? chalk13.dim("\u2026") : ""}`
3565
4959
  );
3566
4960
  for (const w of stmt.warnings) {
3567
- console.log(` ${chalk12.yellow("\u2192")} ${chalk12.yellow(w)}`);
4961
+ console.log(` ${chalk13.yellow("\u2192")} ${chalk13.yellow(w)}`);
3568
4962
  }
3569
4963
  }
3570
4964
  if (result.warnings.length > 0) {
3571
4965
  console.log();
3572
- console.log(chalk12.bold(" Warnings:"));
4966
+ console.log(chalk13.bold(" Warnings:"));
3573
4967
  for (const w of result.warnings) {
3574
- console.log(` ${chalk12.yellow("\u26A0")} ${w}`);
4968
+ console.log(` ${chalk13.yellow("\u26A0")} ${w}`);
3575
4969
  }
3576
4970
  }
3577
- if (!result.wouldSucceed && result.error) {
4971
+ if (result.outcome === "failure" && result.error) {
3578
4972
  console.log();
3579
- console.log(chalk12.red(` \u2716 Error: ${result.error}`));
4973
+ console.log(chalk13.red(` \u2716 Execution Error: ${result.error}`));
3580
4974
  }
3581
4975
  console.log();
3582
4976
  await Promise.all([
3583
4977
  writeAuditEntry(
3584
4978
  cwd,
3585
4979
  "migration.simulate",
3586
- result.wouldSucceed ? "success" : "warning",
4980
+ result.outcome === "failure" ? "failure" : "success",
3587
4981
  {
3588
4982
  migration: match.name,
4983
+ verification: result.verification,
4984
+ outcome: result.outcome,
3589
4985
  destructive: result.destructiveStatements
3590
4986
  }
3591
4987
  ),
3592
4988
  trackEvent("simulate", result.statements.length)
3593
4989
  ]).catch(() => {
3594
4990
  });
3595
- if (!result.wouldSucceed) process.exit(1);
4991
+ if (result.outcome === "failure") process.exit(1);
3596
4992
  if (options.failOnDestructive && result.destructiveStatements > 0) process.exit(2);
3597
4993
  process.exit(0);
3598
4994
  } catch (err) {
3599
4995
  const message = err instanceof Error ? err.message : String(err);
3600
- console.error(chalk12.red(`\u2716 ${message}`));
4996
+ console.error(chalk13.red(`\u2716 ${message}`));
3601
4997
  process.exit(1);
3602
4998
  }
3603
4999
  }
@@ -3605,10 +5001,10 @@ function simulateCommand() {
3605
5001
  }
3606
5002
 
3607
5003
  // src/commands/status.ts
3608
- import chalk13 from "chalk";
3609
- import { Command as Command13 } from "commander";
5004
+ import chalk14 from "chalk";
5005
+ import { Command as Command14 } from "commander";
3610
5006
  function statusCommand() {
3611
- return new Command13("status").description("Show current migration and database status").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").action(async (options) => {
5007
+ return new Command14("status").description("Show current migration, drift, and database verification status").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").action(async (options) => {
3612
5008
  const cwd = process.cwd();
3613
5009
  try {
3614
5010
  const project = await detectPrismaProject(cwd);
@@ -3617,8 +5013,8 @@ function statusCommand() {
3617
5013
  process.stdout.write(`${JSON.stringify({ error: "No Prisma project found" })}
3618
5014
  `);
3619
5015
  } else {
3620
- console.error(chalk13.red("\u2716 No Prisma schema found in the current directory."));
3621
- console.error(chalk13.dim(" Run prisma init to create one, then try again."));
5016
+ console.error(chalk14.red("\u2716 No Prisma schema found in the current directory."));
5017
+ console.error(chalk14.dim(" Run prisma init to create one, then try again."));
3622
5018
  }
3623
5019
  process.exit(1);
3624
5020
  }
@@ -3627,35 +5023,39 @@ function statusCommand() {
3627
5023
  process.stdout.write(`${JSON.stringify(status, null, 2)}
3628
5024
  `);
3629
5025
  } else {
3630
- const bar = chalk13.dim("\u2501".repeat(50));
3631
- const riskColor = status.riskLevel === "critical" ? chalk13.red : status.riskLevel === "high" ? chalk13.red : status.riskLevel === "medium" ? chalk13.yellow : chalk13.green;
5026
+ const bar = chalk14.dim("\u2501".repeat(50));
5027
+ const riskColor = status.riskLevel === "critical" ? chalk14.red : status.riskLevel === "high" ? chalk14.red : status.riskLevel === "medium" ? chalk14.yellow : chalk14.green;
3632
5028
  if (!options.quiet) console.log();
3633
- console.log(chalk13.bold.cyan(" \u{1F4CA} PrismaFlow Status"));
5029
+ console.log(chalk14.bold.cyan(" \u{1F4CA} PrismaFlow Status"));
3634
5030
  console.log(bar);
3635
5031
  console.log(
3636
- chalk13.bold(" Schema: ") + chalk13.dim(status.schemaPath ?? project.schemaPath)
5032
+ chalk14.bold(" Schema: ") + chalk14.dim(status.schemaPath ?? project.schemaPath)
3637
5033
  );
3638
5034
  if (status.provider) {
3639
- console.log(chalk13.bold(" Provider: ") + chalk13.cyan(status.provider));
5035
+ console.log(chalk14.bold(" Provider: ") + chalk14.cyan(status.provider));
3640
5036
  }
3641
5037
  console.log(
3642
- chalk13.bold(" Database: "),
3643
- status.connected ? chalk13.green("\u2714 Connected") : chalk13.red("\u2716 Disconnected")
5038
+ chalk14.bold(" Database: "),
5039
+ status.connected ? chalk14.green("\u2714 Connected") : chalk14.red("\u2716 Disconnected")
3644
5040
  );
3645
5041
  console.log(
3646
- chalk13.bold(" Migrations: ") + chalk13.green(`${status.migrationsApplied} applied`) + chalk13.dim(" / ") + (status.migrationsPending > 0 ? chalk13.yellow(`${status.migrationsPending} pending`) : chalk13.dim("0 pending")) + chalk13.dim(" / ") + (status.migrationsFailed > 0 ? chalk13.red(`${status.migrationsFailed} failed`) : chalk13.dim("0 failed"))
5042
+ chalk14.bold(" Verification: "),
5043
+ status.migrationVerification === "verified" ? chalk14.green("\u2714 Verified") : chalk14.red(`\u2716 ${status.migrationVerification}`)
3647
5044
  );
3648
5045
  console.log(
3649
- chalk13.bold(" Drift: "),
3650
- status.driftDetected ? chalk13.red(
5046
+ chalk14.bold(" Migrations: ") + chalk14.green(`${status.migrationsApplied} applied`) + chalk14.dim(" / ") + (status.migrationsPending > 0 ? chalk14.yellow(`${status.migrationsPending} pending`) : chalk14.dim("0 pending")) + chalk14.dim(" / ") + (status.migrationsFailed > 0 ? chalk14.red(`${status.migrationsFailed} failed`) : chalk14.dim("0 failed")) + (status.migrationsUnknown > 0 ? chalk14.dim(" / ") + chalk14.red(`${status.migrationsUnknown} unknown`) : "")
5047
+ );
5048
+ console.log(
5049
+ chalk14.bold(" Drift: "),
5050
+ status.driftDetected ? chalk14.red(
3651
5051
  `\u2716 ${status.driftCount} issue${status.driftCount !== 1 ? "s" : ""} detected`
3652
- ) : chalk13.green("\u2714 None")
5052
+ ) : status.driftStatus === "not_checked" ? chalk14.yellow("\u26A0 Not checked") : chalk14.green("\u2714 None")
3653
5053
  );
3654
5054
  console.log(
3655
- chalk13.bold(" Risk Level: ") + riskColor(chalk13.bold(status.riskLevel.toUpperCase()))
5055
+ chalk14.bold(" Risk Level: ") + riskColor(chalk14.bold(status.riskLevel.toUpperCase())) + chalk14.dim(" (heuristic)")
3656
5056
  );
3657
5057
  console.log(
3658
- chalk13.bold(" Health Score: ") + riskColor(chalk13.bold(`${status.healthScore}/100`)) + chalk13.dim(` (${status.deploymentReadiness.summary})`)
5058
+ chalk14.bold(" Health Score: ") + riskColor(chalk14.bold(`${status.healthScore}/100`)) + chalk14.dim(` (${status.deploymentReadiness.summary})`)
3659
5059
  );
3660
5060
  const lastMigration = migrations.filter((m) => m.status === "applied").pop();
3661
5061
  if (lastMigration) {
@@ -3665,27 +5065,37 @@ function statusCommand() {
3665
5065
  day: "numeric"
3666
5066
  });
3667
5067
  console.log(
3668
- chalk13.bold(" Last Applied: ") + chalk13.dim(`${lastMigration.name} (${date})`)
5068
+ chalk14.bold(" Last Applied: ") + chalk14.dim(`${lastMigration.name} (${date})`)
3669
5069
  );
3670
5070
  }
3671
5071
  console.log(
3672
- chalk13.bold(" Last Sync: ") + chalk13.dim(new Date(status.lastSync).toLocaleString())
5072
+ chalk14.bold(" Last Sync: ") + chalk14.dim(new Date(status.lastSync).toLocaleString())
3673
5073
  );
3674
5074
  console.log(bar);
5075
+ if (!status.connected) {
5076
+ console.log(chalk14.red(" \u2716 Database unreachable \u2014 check DATABASE_URL in .env"));
5077
+ }
5078
+ if (status.migrationVerification !== "verified") {
5079
+ console.log(
5080
+ chalk14.red(
5081
+ ` \u2716 Migration verification: ${status.migrationVerification} \u2014 run prisma-flow doctor`
5082
+ )
5083
+ );
5084
+ }
3675
5085
  if (status.migrationsPending > 0) {
3676
- console.log(chalk13.yellow(" \u26A0 Pending migrations \u2014 run: prisma migrate deploy"));
5086
+ console.log(chalk14.yellow(" \u26A0 Pending migrations \u2014 run: prisma migrate deploy"));
3677
5087
  }
3678
5088
  if (status.migrationsFailed > 0) {
3679
- console.log(chalk13.red(" \u2716 Failed migrations \u2014 run: prisma migrate resolve"));
5089
+ console.log(chalk14.red(" \u2716 Failed migrations \u2014 run: prisma migrate resolve"));
3680
5090
  }
3681
5091
  if (status.driftDetected) {
3682
- console.log(chalk13.red(" \u2716 Schema drift \u2014 review the dashboard Drift page"));
5092
+ console.log(chalk14.red(" \u2716 Schema drift \u2014 review the dashboard Drift page"));
3683
5093
  }
3684
5094
  if (status.deploymentReadiness.status === "ready") {
3685
- console.log(chalk13.green(" \u2714 Deployment readiness checks passed"));
5095
+ console.log(chalk14.green(" \u2714 Deployment readiness checks passed"));
3686
5096
  } else {
3687
5097
  for (const check of status.deploymentReadiness.checks.filter((item) => !item.passed)) {
3688
- console.log(chalk13.yellow(` \u26A0 ${check.label}: ${check.message}`));
5098
+ console.log(chalk14.yellow(` \u26A0 ${check.label}: ${check.message}`));
3689
5099
  }
3690
5100
  }
3691
5101
  if (!options.quiet) console.log();
@@ -3707,7 +5117,7 @@ function statusCommand() {
3707
5117
  process.stdout.write(`${JSON.stringify({ error: message })}
3708
5118
  `);
3709
5119
  } else {
3710
- console.error(chalk13.red(`\u2716 Error: ${message}`));
5120
+ console.error(chalk14.red(`\u2716 Error: ${message}`));
3711
5121
  }
3712
5122
  await writeAuditEntry(cwd, "status.check", "failure", {
3713
5123
  error: message
@@ -3719,12 +5129,21 @@ function statusCommand() {
3719
5129
  }
3720
5130
 
3721
5131
  // src/index.ts
3722
- var program = new Command14();
3723
- program.name("prisma-flow").description("Visual Prisma migration management \u2014 safe, observable, production-ready").version(process.env.npm_package_version ?? "0.1.0");
3724
- program.addCommand(dashboardCommand(), { isDefault: true });
5132
+ var require2 = createRequire2(import.meta.url);
5133
+ var cliVersion = "0.2.0";
5134
+ try {
5135
+ const pkg = require2("../package.json");
5136
+ if (pkg.version) cliVersion = pkg.version;
5137
+ } catch {
5138
+ cliVersion = process.env.npm_package_version ?? "0.2.0";
5139
+ }
5140
+ var program = new Command15();
5141
+ program.name("prisma-flow").description("Visual Prisma migration management \u2014 safe, observable, production-ready").version(cliVersion);
5142
+ program.addCommand(dashboardCommand());
3725
5143
  program.addCommand(statusCommand());
3726
5144
  program.addCommand(checkCommand());
3727
5145
  program.addCommand(reportCommand());
5146
+ program.addCommand(planCommand());
3728
5147
  program.addCommand(initCommand());
3729
5148
  program.addCommand(doctorCommand());
3730
5149
  program.addCommand(inspectCommand());
@@ -3734,4 +5153,10 @@ program.addCommand(rollbackCommand());
3734
5153
  program.addCommand(repairCommand());
3735
5154
  program.addCommand(compareCommand());
3736
5155
  program.addCommand(historyCommand());
5156
+ var args = process.argv.slice(2);
5157
+ var firstArg = args[0];
5158
+ var startsWithDashboardOption = firstArg === "-p" || firstArg === "--port" || firstArg?.startsWith("--port=") || firstArg === "--no-open";
5159
+ if (args.length === 0 || startsWithDashboardOption) {
5160
+ process.argv.splice(2, 0, "dashboard");
5161
+ }
3737
5162
  program.parse(process.argv);