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.
- package/README.md +1 -0
- package/dist/index.js +2133 -708
- package/package.json +10 -23
- package/public/404/index.html +1 -1
- package/public/404.html +1 -1
- package/public/__next.__PAGE__.txt +4 -4
- package/public/__next._full.txt +12 -12
- package/public/__next._head.txt +4 -4
- package/public/__next._index.txt +5 -5
- package/public/__next._tree.txt +2 -2
- package/public/_next/static/chunks/056iufrwvl369.js +1 -0
- package/public/_next/static/chunks/{0ov8yxc8yngji.js → 0acm34w22hx9z.js} +1 -1
- package/public/_next/static/chunks/{0drl6u3r7627z.js → 0c984jinxm8f6.js} +1 -1
- package/public/_next/static/chunks/{031d4hdncmqdj.js → 0e0fl5qcvu-yx.js} +1 -1
- package/public/_next/static/chunks/0z1o-405295er.js +1 -0
- package/public/_next/static/chunks/{0p4y8c2fq43wu.js → 1gswx446-o00_.js} +1 -1
- package/public/_next/static/chunks/1te0x9fwhrf60.js +1 -0
- package/public/_next/static/chunks/25hh9nwx-v3c5.css +1 -0
- package/public/_next/static/chunks/25o46h8mdjlrg.js +1 -0
- package/public/_next/static/chunks/{0m7w4t_yx6o6a.js → 2ltjmp_97014r.js} +1 -1
- package/public/_next/static/chunks/{3cdvjzy78ylzj.js → 2nzee6y5-2q17.js} +1 -1
- package/public/_next/static/chunks/{3xrarg7xfb7_g.js → 2yb298owf9iw_.js} +1 -1
- package/public/_next/static/chunks/{05-c3ty_6dwfk.js → 3au-rj2yh4vzs.js} +1 -1
- package/public/_next/static/chunks/{2o1jw19xgqkzz.js → 3diwb2iuom_tm.js} +3 -3
- package/public/_next/static/chunks/3vw-_f96ru14r.js +1 -0
- package/public/_next/static/chunks/3zh2lc2jn3p3h.js +1 -0
- package/public/_next/static/chunks/{turbopack-0r_p3gsi1hu9t.js → turbopack-15dmj_1idpm1l.js} +1 -1
- package/public/_not-found/__next._full.txt +10 -10
- package/public/_not-found/__next._head.txt +4 -4
- package/public/_not-found/__next._index.txt +5 -5
- package/public/_not-found/{__next._not-found/__PAGE__.txt → __next._not-found.__PAGE__.txt} +2 -2
- package/public/_not-found/__next._not-found.txt +3 -3
- package/public/_not-found/__next._tree.txt +2 -2
- package/public/_not-found/index.html +1 -1
- package/public/_not-found/index.txt +10 -10
- package/public/drift/__next._full.txt +12 -12
- package/public/drift/__next._head.txt +4 -4
- package/public/drift/__next._index.txt +5 -5
- package/public/drift/__next._tree.txt +2 -2
- package/public/drift/__next.drift.__PAGE__.txt +9 -0
- package/public/drift/__next.drift.txt +3 -3
- package/public/drift/index.html +1 -1
- package/public/drift/index.txt +12 -12
- package/public/index.html +1 -1
- package/public/index.txt +12 -12
- package/public/migrations/__next._full.txt +12 -12
- package/public/migrations/__next._head.txt +4 -4
- package/public/migrations/__next._index.txt +5 -5
- package/public/migrations/__next._tree.txt +2 -2
- package/public/migrations/__next.migrations.__PAGE__.txt +9 -0
- package/public/migrations/__next.migrations.txt +3 -3
- package/public/migrations/index.html +1 -1
- package/public/migrations/index.txt +12 -12
- package/public/risks/__next._full.txt +12 -12
- package/public/risks/__next._head.txt +4 -4
- package/public/risks/__next._index.txt +5 -5
- package/public/risks/__next._tree.txt +2 -2
- package/public/risks/__next.risks.__PAGE__.txt +9 -0
- package/public/risks/__next.risks.txt +3 -3
- package/public/risks/index.html +1 -1
- package/public/risks/index.txt +12 -12
- package/public/schema/__next._full.txt +12 -12
- package/public/schema/__next._head.txt +4 -4
- package/public/schema/__next._index.txt +5 -5
- package/public/schema/__next._tree.txt +2 -2
- package/public/schema/__next.schema.__PAGE__.txt +9 -0
- package/public/schema/__next.schema.txt +3 -3
- package/public/schema/index.html +1 -1
- package/public/schema/index.txt +12 -12
- package/public/simulate/__next._full.txt +12 -12
- package/public/simulate/__next._head.txt +4 -4
- package/public/simulate/__next._index.txt +5 -5
- package/public/simulate/__next._tree.txt +2 -2
- package/public/simulate/__next.simulate.__PAGE__.txt +9 -0
- package/public/simulate/__next.simulate.txt +3 -3
- package/public/simulate/index.html +1 -1
- package/public/simulate/index.txt +12 -12
- package/public/_next/static/chunks/03q3r_bvmqrr8.js +0 -2
- package/public/_next/static/chunks/0s76k7hq9-wfk.css +0 -1
- package/public/_next/static/chunks/0zv5uyi-a4e2y.js +0 -1
- package/public/_next/static/chunks/1kk4shlfeass1.js +0 -1
- package/public/_next/static/chunks/2nykiepra7i1k.js +0 -1
- package/public/_next/static/chunks/2ohti557qol0g.js +0 -1
- package/public/_next/static/chunks/3w1qpcrpp5s4-.js +0 -2
- package/public/drift/__next.drift/__PAGE__.txt +0 -9
- package/public/migrations/__next.migrations/__PAGE__.txt +0 -9
- package/public/risks/__next.risks/__PAGE__.txt +0 -9
- package/public/schema/__next.schema/__PAGE__.txt +0 -9
- package/public/simulate/__next.simulate/__PAGE__.txt +0 -9
- /package/public/_next/static/{ySSMgkts9h6_mRnScxk9f → 0mXb1HP7_wVjSa11pmE1C}/_buildManifest.js +0 -0
- /package/public/_next/static/{ySSMgkts9h6_mRnScxk9f → 0mXb1HP7_wVjSa11pmE1C}/_clientMiddlewareManifest.js +0 -0
- /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 {
|
|
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,
|
|
26
|
+
function write(severity, lvl, args2) {
|
|
26
27
|
if (lvl < activeLevel) return;
|
|
27
|
-
const obj = typeof
|
|
28
|
-
const msg = typeof
|
|
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-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
|
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 = [
|
|
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 =
|
|
162
|
+
const schemaDir = path2.join(cwd, "prisma", "schema");
|
|
211
163
|
if (await tryAccess(schemaDir)) {
|
|
212
|
-
const entries = await
|
|
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
|
|
174
|
+
const stat = await fs2.stat(schemaPath);
|
|
223
175
|
if (stat.isDirectory()) {
|
|
224
|
-
const files = (await
|
|
176
|
+
const files = (await fs2.readdir(schemaPath)).filter((f) => f.endsWith(".prisma")).sort();
|
|
225
177
|
const parts = await Promise.all(
|
|
226
|
-
files.map((f) =>
|
|
178
|
+
files.map((f) => fs2.readFile(path2.join(schemaPath, f), "utf-8"))
|
|
227
179
|
);
|
|
228
180
|
return parts.join("\n");
|
|
229
181
|
}
|
|
230
|
-
return
|
|
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(
|
|
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
|
|
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 [
|
|
213
|
+
for (const envFile of [path2.join(cwd, ".env"), path2.join(cwd, "prisma", ".env")]) {
|
|
262
214
|
try {
|
|
263
|
-
const envContent = await
|
|
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
|
|
273
|
-
const migrationsPath =
|
|
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
|
|
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 =
|
|
280
|
-
const sqlPath =
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
project.schemaPath,
|
|
494
|
+
fromSchema,
|
|
495
|
+
...legacyDestinationArgs,
|
|
411
496
|
"--script"
|
|
412
497
|
],
|
|
413
|
-
|
|
498
|
+
options
|
|
414
499
|
);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
729
|
-
connected,
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
1134
|
+
hasCriticalRisk,
|
|
1135
|
+
errorMessage: statusResult.errorMessage ?? driftResult.errorMessage
|
|
742
1136
|
});
|
|
743
1137
|
return {
|
|
744
|
-
connected,
|
|
745
|
-
|
|
746
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
904
|
-
if (
|
|
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 ${
|
|
907
|
-
for (const factor of
|
|
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
|
-
|
|
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
|
-
|
|
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 { ...
|
|
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 { ...
|
|
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
|
|
1327
|
-
import
|
|
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
|
|
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
|
|
2325
|
+
const normalizedDatabaseUrl = normalizeDatabaseUrlForPrismaCommand(databaseUrl, schemaPath);
|
|
2326
|
+
const env = { ...process.env, DATABASE_URL: normalizedDatabaseUrl };
|
|
1426
2327
|
try {
|
|
1427
|
-
const { stdout } = await
|
|
1428
|
-
|
|
1429
|
-
[
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
1629
|
-
|
|
1630
|
-
|
|
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",
|
|
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/
|
|
2705
|
+
// src/server/routes/plan.ts
|
|
1813
2706
|
import { Hono as Hono9 } from "hono";
|
|
1814
2707
|
|
|
1815
|
-
// src/core/
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
1852
|
-
return `
|
|
1853
|
-
case "
|
|
1854
|
-
return `
|
|
1855
|
-
case "
|
|
1856
|
-
return `
|
|
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 `
|
|
2984
|
+
return `Manually inspect "${target}" \u2014 automated analysis cannot determine safe resolution.`;
|
|
1861
2985
|
}
|
|
1862
2986
|
}
|
|
1863
|
-
function
|
|
1864
|
-
if (strategy === "
|
|
1865
|
-
return `--
|
|
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 === "
|
|
1868
|
-
return `-- Extra table detected. To
|
|
1869
|
-
-- 1. Add
|
|
1870
|
-
-- 2. Run: npx prisma migrate dev --name
|
|
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 === "
|
|
1873
|
-
|
|
1874
|
-
|
|
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
|
|
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 "
|
|
1889
|
-
if (item.type === "
|
|
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
|
-
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
|
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
|
|
1917
|
-
|
|
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
|
|
3078
|
+
const plan = buildDriftRepairPlan(driftResult.items, project.migrationsPath);
|
|
1933
3079
|
return c.json({
|
|
1934
3080
|
success: true,
|
|
1935
|
-
data: { drifted: driftResult.status === "drifted",
|
|
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
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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 =
|
|
3098
|
+
var repair_default = app10;
|
|
1966
3099
|
|
|
1967
3100
|
// src/server/routes/risks.ts
|
|
1968
|
-
import { Hono as
|
|
1969
|
-
var
|
|
1970
|
-
|
|
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
|
-
|
|
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 =
|
|
3139
|
+
var risks_default = app11;
|
|
2007
3140
|
|
|
2008
3141
|
// src/server/routes/rollback.ts
|
|
2009
|
-
import
|
|
2010
|
-
import { Hono as
|
|
3142
|
+
import path8 from "path";
|
|
3143
|
+
import { Hono as Hono12 } from "hono";
|
|
2011
3144
|
|
|
2012
3145
|
// src/core/rollback-generator.ts
|
|
2013
|
-
import
|
|
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
|
|
3150
|
+
import fs9 from "fs/promises";
|
|
2019
3151
|
import os from "os";
|
|
2020
|
-
import
|
|
2021
|
-
import { promisify as
|
|
2022
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2108
|
-
const shadowDb =
|
|
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
|
|
2111
|
-
const sql = await
|
|
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
|
|
2116
|
-
|
|
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
|
|
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
|
-
...
|
|
2122
|
-
|
|
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
|
|
2129
|
-
|
|
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
|
|
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
|
|
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
|
|
3306
|
+
await fs9.access(dbFilePath);
|
|
2142
3307
|
return await simulateSqlite(migrationName, sqlFilePath, dbFilePath);
|
|
2143
3308
|
} catch {
|
|
2144
3309
|
}
|
|
2145
3310
|
}
|
|
2146
|
-
|
|
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
|
|
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
|
|
2292
|
-
|
|
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 =
|
|
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 =
|
|
3492
|
+
var rollback_default = app12;
|
|
2318
3493
|
|
|
2319
3494
|
// src/server/routes/schema.ts
|
|
2320
|
-
import { Hono as
|
|
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
|
|
2346
|
-
|
|
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 =
|
|
3531
|
+
var schema_default = app13;
|
|
2357
3532
|
|
|
2358
3533
|
// src/server/routes/simulate.ts
|
|
2359
|
-
import
|
|
2360
|
-
import { Hono as
|
|
2361
|
-
var
|
|
2362
|
-
|
|
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 =
|
|
2373
|
-
const dbPath = project.provider === "sqlite" && project.databaseUrl ? project.databaseUrl.
|
|
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 =
|
|
3556
|
+
var simulate_default = app14;
|
|
2382
3557
|
|
|
2383
3558
|
// src/server/routes/status.ts
|
|
2384
|
-
import { Hono as
|
|
2385
|
-
var
|
|
2386
|
-
|
|
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 =
|
|
3571
|
+
var status_default = app15;
|
|
2397
3572
|
|
|
2398
3573
|
// src/server/index.ts
|
|
2399
3574
|
var __filename = fileURLToPath(import.meta.url);
|
|
2400
|
-
var __dirname =
|
|
3575
|
+
var __dirname = path10.dirname(__filename);
|
|
2401
3576
|
function createServer(projectPath) {
|
|
2402
|
-
const
|
|
3577
|
+
const app16 = new Hono16();
|
|
2403
3578
|
const token = randomBytes(24).toString("hex");
|
|
2404
|
-
|
|
2405
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3627
|
+
app16.use("*", async (c, next) => {
|
|
2450
3628
|
c.set("projectPath", projectPath);
|
|
2451
3629
|
await next();
|
|
2452
3630
|
});
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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
|
-
|
|
3654
|
+
path10.resolve(__dirname, "../public"),
|
|
2477
3655
|
// dist/public
|
|
2478
|
-
|
|
3656
|
+
path10.resolve(__dirname, "../../public"),
|
|
2479
3657
|
// packages/cli/public (source)
|
|
2480
|
-
|
|
3658
|
+
path10.resolve(__dirname, "../../..", "packages", "cli", "public"),
|
|
2481
3659
|
// monorepo relative
|
|
2482
|
-
|
|
3660
|
+
path10.resolve(process.cwd(), "public"),
|
|
2483
3661
|
// cwd public
|
|
2484
|
-
|
|
2485
|
-
|
|
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 (
|
|
3667
|
+
if (fs11.existsSync(cand)) {
|
|
2490
3668
|
publicDir = cand;
|
|
2491
3669
|
break;
|
|
2492
3670
|
}
|
|
2493
3671
|
}
|
|
2494
3672
|
if (!publicDir) {
|
|
2495
|
-
publicDir =
|
|
3673
|
+
publicDir = path10.resolve(__dirname, "../public");
|
|
2496
3674
|
}
|
|
2497
|
-
if (!
|
|
3675
|
+
if (!fs11.existsSync(publicDir)) {
|
|
2498
3676
|
logger.warn({ publicDir }, "serveStatic: public directory not found");
|
|
2499
3677
|
}
|
|
2500
|
-
|
|
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
|
-
|
|
3685
|
+
app16.get("*", async (c) => {
|
|
2508
3686
|
try {
|
|
2509
|
-
const indexPath =
|
|
2510
|
-
if (
|
|
2511
|
-
const content = await
|
|
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:
|
|
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(
|
|
2522
|
-
|
|
2523
|
-
|
|
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:
|
|
3739
|
+
const { app: app16, token } = createServer(cwd);
|
|
2553
3740
|
const url = `http://localhost:${port}?token=${token}`;
|
|
2554
|
-
startServer(
|
|
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
|
|
2660
|
-
import
|
|
2661
|
-
import { promisify as
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
|
2779
|
-
return { ok: true, detail:
|
|
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
|
|
3977
|
+
await execFileAsync("git", ["rev-parse", "--git-dir"], {
|
|
2791
3978
|
cwd,
|
|
2792
3979
|
timeout: 5e3
|
|
2793
3980
|
});
|
|
2794
|
-
const { stdout } = await
|
|
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
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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 ?
|
|
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
|
|
2958
|
-
import
|
|
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 =
|
|
4192
|
+
const dest = path12.join(cwd, "prismaflow.config.ts");
|
|
3012
4193
|
try {
|
|
3013
|
-
await
|
|
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
|
|
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
|
|
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
|
|
3044
|
-
import
|
|
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 =
|
|
4248
|
+
const sqlFile = path13.join(project.migrationsPath, match.name, "migration.sql");
|
|
3068
4249
|
let sql = "";
|
|
3069
4250
|
try {
|
|
3070
|
-
sql = await
|
|
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/
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
4574
|
+
`${JSON.stringify({ ok: true, drifted: false, plan: buildDriftRepairPlan([]) })}
|
|
3182
4575
|
`
|
|
3183
4576
|
);
|
|
3184
4577
|
} else {
|
|
3185
|
-
console.log(
|
|
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
|
|
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(
|
|
3205
|
-
console.log(
|
|
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
|
-
|
|
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" ?
|
|
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: ${
|
|
4604
|
+
` Strategy: ${chalk10.cyan(suggestion.strategy)} ${chalk10.dim("(manual operator review)")}`
|
|
3217
4605
|
);
|
|
3218
|
-
|
|
3219
|
-
console.log(
|
|
4606
|
+
for (const w of suggestion.warnings) {
|
|
4607
|
+
console.log(` ${chalk10.yellow("\u26A0")} ${chalk10.dim(w)}`);
|
|
3220
4608
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
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
|
-
|
|
4623
|
+
planOnly: true
|
|
3240
4624
|
}),
|
|
3241
4625
|
trackEvent("repair", driftResult.items.length)
|
|
3242
4626
|
]).catch(() => {
|
|
3243
4627
|
});
|
|
3244
|
-
process.exit(driftResult.items.length > 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(
|
|
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
|
|
3257
|
-
import { Command as
|
|
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
|
|
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
|
-
`-
|
|
3304
|
-
`-
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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(`${
|
|
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" ?
|
|
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(`${
|
|
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(`${
|
|
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
|
|
3438
|
-
import { Command as
|
|
4828
|
+
import chalk12 from "chalk";
|
|
4829
|
+
import { Command as Command12 } from "commander";
|
|
3439
4830
|
function rollbackCommand() {
|
|
3440
|
-
return new
|
|
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(
|
|
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(
|
|
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(
|
|
3472
|
-
console.log(
|
|
4862
|
+
console.log(chalk12.bold.cyan(` \u21A9 Rollback Plan: ${match.name}`));
|
|
4863
|
+
console.log(chalk12.dim("\u2501".repeat(60)));
|
|
3473
4864
|
console.log(
|
|
3474
|
-
` ${
|
|
4865
|
+
` ${chalk12.bold("Steps:")} ${autoCount} automated, ${manualCount > 0 ? chalk12.yellow(`${manualCount} manual`) : chalk12.green("0 manual")}`
|
|
3475
4866
|
);
|
|
3476
4867
|
console.log(
|
|
3477
|
-
` ${
|
|
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 (
|
|
3481
|
-
const
|
|
3482
|
-
const
|
|
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(` ${
|
|
3486
|
-
console.log(` ${
|
|
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(` ${
|
|
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(
|
|
4883
|
+
console.log(chalk12.bold(" Warnings:"));
|
|
3494
4884
|
for (const w of plan.warnings) {
|
|
3495
|
-
console.log(` ${
|
|
4885
|
+
console.log(` ${chalk12.yellow("\u26A0")} ${w}`);
|
|
3496
4886
|
}
|
|
3497
4887
|
}
|
|
3498
4888
|
console.log();
|
|
3499
|
-
console.log(
|
|
3500
|
-
console.log(
|
|
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(
|
|
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
|
|
3522
|
-
import { Command as
|
|
4911
|
+
import chalk13 from "chalk";
|
|
4912
|
+
import { Command as Command13 } from "commander";
|
|
3523
4913
|
function simulateCommand() {
|
|
3524
|
-
return new
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
3550
|
-
console.log(
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
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
|
-
` ${
|
|
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 ?
|
|
3561
|
-
const typeLabel =
|
|
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 ?
|
|
4958
|
+
` ${icon} ${typeLabel} ${preview}${stmt.sql.length > 70 ? chalk13.dim("\u2026") : ""}`
|
|
3565
4959
|
);
|
|
3566
4960
|
for (const w of stmt.warnings) {
|
|
3567
|
-
console.log(` ${
|
|
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(
|
|
4966
|
+
console.log(chalk13.bold(" Warnings:"));
|
|
3573
4967
|
for (const w of result.warnings) {
|
|
3574
|
-
console.log(` ${
|
|
4968
|
+
console.log(` ${chalk13.yellow("\u26A0")} ${w}`);
|
|
3575
4969
|
}
|
|
3576
4970
|
}
|
|
3577
|
-
if (
|
|
4971
|
+
if (result.outcome === "failure" && result.error) {
|
|
3578
4972
|
console.log();
|
|
3579
|
-
console.log(
|
|
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.
|
|
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 (
|
|
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(
|
|
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
|
|
3609
|
-
import { Command as
|
|
5004
|
+
import chalk14 from "chalk";
|
|
5005
|
+
import { Command as Command14 } from "commander";
|
|
3610
5006
|
function statusCommand() {
|
|
3611
|
-
return new
|
|
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(
|
|
3621
|
-
console.error(
|
|
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 =
|
|
3631
|
-
const riskColor = status.riskLevel === "critical" ?
|
|
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(
|
|
5029
|
+
console.log(chalk14.bold.cyan(" \u{1F4CA} PrismaFlow Status"));
|
|
3634
5030
|
console.log(bar);
|
|
3635
5031
|
console.log(
|
|
3636
|
-
|
|
5032
|
+
chalk14.bold(" Schema: ") + chalk14.dim(status.schemaPath ?? project.schemaPath)
|
|
3637
5033
|
);
|
|
3638
5034
|
if (status.provider) {
|
|
3639
|
-
console.log(
|
|
5035
|
+
console.log(chalk14.bold(" Provider: ") + chalk14.cyan(status.provider));
|
|
3640
5036
|
}
|
|
3641
5037
|
console.log(
|
|
3642
|
-
|
|
3643
|
-
status.connected ?
|
|
5038
|
+
chalk14.bold(" Database: "),
|
|
5039
|
+
status.connected ? chalk14.green("\u2714 Connected") : chalk14.red("\u2716 Disconnected")
|
|
3644
5040
|
);
|
|
3645
5041
|
console.log(
|
|
3646
|
-
|
|
5042
|
+
chalk14.bold(" Verification: "),
|
|
5043
|
+
status.migrationVerification === "verified" ? chalk14.green("\u2714 Verified") : chalk14.red(`\u2716 ${status.migrationVerification}`)
|
|
3647
5044
|
);
|
|
3648
5045
|
console.log(
|
|
3649
|
-
|
|
3650
|
-
|
|
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
|
-
) :
|
|
5052
|
+
) : status.driftStatus === "not_checked" ? chalk14.yellow("\u26A0 Not checked") : chalk14.green("\u2714 None")
|
|
3653
5053
|
);
|
|
3654
5054
|
console.log(
|
|
3655
|
-
|
|
5055
|
+
chalk14.bold(" Risk Level: ") + riskColor(chalk14.bold(status.riskLevel.toUpperCase())) + chalk14.dim(" (heuristic)")
|
|
3656
5056
|
);
|
|
3657
5057
|
console.log(
|
|
3658
|
-
|
|
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
|
-
|
|
5068
|
+
chalk14.bold(" Last Applied: ") + chalk14.dim(`${lastMigration.name} (${date})`)
|
|
3669
5069
|
);
|
|
3670
5070
|
}
|
|
3671
5071
|
console.log(
|
|
3672
|
-
|
|
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(
|
|
5086
|
+
console.log(chalk14.yellow(" \u26A0 Pending migrations \u2014 run: prisma migrate deploy"));
|
|
3677
5087
|
}
|
|
3678
5088
|
if (status.migrationsFailed > 0) {
|
|
3679
|
-
console.log(
|
|
5089
|
+
console.log(chalk14.red(" \u2716 Failed migrations \u2014 run: prisma migrate resolve"));
|
|
3680
5090
|
}
|
|
3681
5091
|
if (status.driftDetected) {
|
|
3682
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
3723
|
-
|
|
3724
|
-
|
|
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);
|