@prisma-next/cli 0.3.0-dev.6 → 0.3.0-dev.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +314 -80
  3. package/dist/cli-errors-JlPTsazx.mjs +3 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.js +1 -2376
  6. package/dist/cli.mjs +203 -0
  7. package/dist/cli.mjs.map +1 -0
  8. package/dist/client-PimzSD1f.mjs +981 -0
  9. package/dist/client-PimzSD1f.mjs.map +1 -0
  10. package/dist/commands/contract-emit.d.mts +7 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -0
  12. package/dist/commands/contract-emit.mjs +151 -0
  13. package/dist/commands/contract-emit.mjs.map +1 -0
  14. package/dist/commands/db-init.d.mts +7 -0
  15. package/dist/commands/db-init.d.mts.map +1 -0
  16. package/dist/commands/db-init.mjs +134 -0
  17. package/dist/commands/db-init.mjs.map +1 -0
  18. package/dist/commands/db-introspect.d.mts +7 -0
  19. package/dist/commands/db-introspect.d.mts.map +1 -0
  20. package/dist/commands/db-introspect.mjs +118 -0
  21. package/dist/commands/db-introspect.mjs.map +1 -0
  22. package/dist/commands/db-schema-verify.d.mts +7 -0
  23. package/dist/commands/db-schema-verify.d.mts.map +1 -0
  24. package/dist/commands/db-schema-verify.mjs +120 -0
  25. package/dist/commands/db-schema-verify.mjs.map +1 -0
  26. package/dist/commands/db-sign.d.mts +7 -0
  27. package/dist/commands/db-sign.d.mts.map +1 -0
  28. package/dist/commands/db-sign.mjs +142 -0
  29. package/dist/commands/db-sign.mjs.map +1 -0
  30. package/dist/commands/db-update.d.mts +7 -0
  31. package/dist/commands/db-update.d.mts.map +1 -0
  32. package/dist/commands/db-update.mjs +123 -0
  33. package/dist/commands/db-update.mjs.map +1 -0
  34. package/dist/commands/db-verify.d.mts +7 -0
  35. package/dist/commands/db-verify.d.mts.map +1 -0
  36. package/dist/commands/db-verify.mjs +133 -0
  37. package/dist/commands/db-verify.mjs.map +1 -0
  38. package/dist/commands/migration-apply.d.mts +23 -0
  39. package/dist/commands/migration-apply.d.mts.map +1 -0
  40. package/dist/commands/migration-apply.mjs +250 -0
  41. package/dist/commands/migration-apply.mjs.map +1 -0
  42. package/dist/commands/migration-plan.d.mts +25 -0
  43. package/dist/commands/migration-plan.d.mts.map +1 -0
  44. package/dist/commands/migration-plan.mjs +266 -0
  45. package/dist/commands/migration-plan.mjs.map +1 -0
  46. package/dist/commands/migration-show.d.mts +28 -0
  47. package/dist/commands/migration-show.d.mts.map +1 -0
  48. package/dist/commands/migration-show.mjs +138 -0
  49. package/dist/commands/migration-show.mjs.map +1 -0
  50. package/dist/commands/migration-status.d.mts +35 -0
  51. package/dist/commands/migration-status.d.mts.map +1 -0
  52. package/dist/commands/migration-status.mjs +260 -0
  53. package/dist/commands/migration-status.mjs.map +1 -0
  54. package/dist/commands/migration-verify.d.mts +16 -0
  55. package/dist/commands/migration-verify.d.mts.map +1 -0
  56. package/dist/commands/migration-verify.mjs +86 -0
  57. package/dist/commands/migration-verify.mjs.map +1 -0
  58. package/dist/config-loader-PPf4CtDj.mjs +43 -0
  59. package/dist/config-loader-PPf4CtDj.mjs.map +1 -0
  60. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  61. package/dist/config-loader.d.mts.map +1 -0
  62. package/dist/config-loader.mjs +3 -0
  63. package/dist/exports/config-types.d.mts +2 -0
  64. package/dist/exports/config-types.mjs +3 -0
  65. package/dist/exports/control-api.d.mts +621 -0
  66. package/dist/exports/control-api.d.mts.map +1 -0
  67. package/dist/exports/control-api.mjs +98 -0
  68. package/dist/exports/control-api.mjs.map +1 -0
  69. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  70. package/dist/exports/index.d.mts.map +1 -0
  71. package/dist/exports/index.mjs +135 -0
  72. package/dist/exports/index.mjs.map +1 -0
  73. package/dist/extract-sql-ddl-BmlKvk4o.mjs +26 -0
  74. package/dist/extract-sql-ddl-BmlKvk4o.mjs.map +1 -0
  75. package/dist/framework-components-CjV_jD8f.mjs +59 -0
  76. package/dist/framework-components-CjV_jD8f.mjs.map +1 -0
  77. package/dist/migration-command-scaffold-DfY_F3ev.mjs +97 -0
  78. package/dist/migration-command-scaffold-DfY_F3ev.mjs.map +1 -0
  79. package/dist/progress-adapter-DENrzF6I.mjs +49 -0
  80. package/dist/progress-adapter-DENrzF6I.mjs.map +1 -0
  81. package/dist/result-handler-iA9JtUC7.mjs +1186 -0
  82. package/dist/result-handler-iA9JtUC7.mjs.map +1 -0
  83. package/package.json +75 -38
  84. package/src/cli.ts +43 -0
  85. package/src/commands/contract-emit.ts +221 -111
  86. package/src/commands/db-init.ts +217 -426
  87. package/src/commands/db-introspect.ts +148 -185
  88. package/src/commands/db-schema-verify.ts +162 -149
  89. package/src/commands/db-sign.ts +215 -202
  90. package/src/commands/db-update.ts +220 -0
  91. package/src/commands/db-verify.ts +193 -156
  92. package/src/commands/migration-apply.ts +431 -0
  93. package/src/commands/migration-plan.ts +446 -0
  94. package/src/commands/migration-show.ts +255 -0
  95. package/src/commands/migration-status.ts +436 -0
  96. package/src/commands/migration-verify.ts +151 -0
  97. package/src/config-loader.ts +13 -3
  98. package/src/control-api/client.ts +605 -0
  99. package/src/control-api/errors.ts +9 -0
  100. package/src/control-api/operations/contract-emit.ts +161 -0
  101. package/src/control-api/operations/db-init.ts +286 -0
  102. package/src/control-api/operations/db-update.ts +221 -0
  103. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  104. package/src/control-api/operations/migration-apply.ts +195 -0
  105. package/src/control-api/operations/migration-helpers.ts +49 -0
  106. package/src/control-api/types.ts +687 -0
  107. package/src/exports/config-types.ts +3 -3
  108. package/src/exports/control-api.ts +53 -0
  109. package/src/load-ts-contract.ts +16 -11
  110. package/src/utils/cli-errors.ts +3 -1
  111. package/src/utils/command-helpers.ts +92 -3
  112. package/src/utils/framework-components.ts +11 -30
  113. package/src/utils/migration-command-scaffold.ts +190 -0
  114. package/src/utils/output.ts +363 -25
  115. package/src/utils/progress-adapter.ts +86 -0
  116. package/dist/chunk-464LNZCE.js +0 -134
  117. package/dist/chunk-464LNZCE.js.map +0 -1
  118. package/dist/chunk-BZMBKEEQ.js +0 -997
  119. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  120. package/dist/chunk-HWYQOCAJ.js +0 -47
  121. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  122. package/dist/chunk-ZKYEJROM.js +0 -94
  123. package/dist/chunk-ZKYEJROM.js.map +0 -1
  124. package/dist/cli.d.ts +0 -2
  125. package/dist/cli.d.ts.map +0 -1
  126. package/dist/cli.js.map +0 -1
  127. package/dist/commands/contract-emit.d.ts +0 -3
  128. package/dist/commands/contract-emit.d.ts.map +0 -1
  129. package/dist/commands/contract-emit.js +0 -9
  130. package/dist/commands/contract-emit.js.map +0 -1
  131. package/dist/commands/db-init.d.ts +0 -3
  132. package/dist/commands/db-init.d.ts.map +0 -1
  133. package/dist/commands/db-init.js +0 -341
  134. package/dist/commands/db-init.js.map +0 -1
  135. package/dist/commands/db-introspect.d.ts +0 -3
  136. package/dist/commands/db-introspect.d.ts.map +0 -1
  137. package/dist/commands/db-introspect.js +0 -190
  138. package/dist/commands/db-introspect.js.map +0 -1
  139. package/dist/commands/db-schema-verify.d.ts +0 -3
  140. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  141. package/dist/commands/db-schema-verify.js +0 -164
  142. package/dist/commands/db-schema-verify.js.map +0 -1
  143. package/dist/commands/db-sign.d.ts +0 -3
  144. package/dist/commands/db-sign.d.ts.map +0 -1
  145. package/dist/commands/db-sign.js +0 -199
  146. package/dist/commands/db-sign.js.map +0 -1
  147. package/dist/commands/db-verify.d.ts +0 -3
  148. package/dist/commands/db-verify.d.ts.map +0 -1
  149. package/dist/commands/db-verify.js +0 -173
  150. package/dist/commands/db-verify.js.map +0 -1
  151. package/dist/config-loader.d.ts.map +0 -1
  152. package/dist/config-loader.js +0 -7
  153. package/dist/config-loader.js.map +0 -1
  154. package/dist/exports/config-types.d.ts +0 -3
  155. package/dist/exports/config-types.d.ts.map +0 -1
  156. package/dist/exports/config-types.js +0 -6
  157. package/dist/exports/config-types.js.map +0 -1
  158. package/dist/exports/index.d.ts +0 -4
  159. package/dist/exports/index.d.ts.map +0 -1
  160. package/dist/exports/index.js +0 -175
  161. package/dist/exports/index.js.map +0 -1
  162. package/dist/load-ts-contract.d.ts.map +0 -1
  163. package/dist/utils/action.d.ts +0 -16
  164. package/dist/utils/action.d.ts.map +0 -1
  165. package/dist/utils/cli-errors.d.ts +0 -7
  166. package/dist/utils/cli-errors.d.ts.map +0 -1
  167. package/dist/utils/command-helpers.d.ts +0 -12
  168. package/dist/utils/command-helpers.d.ts.map +0 -1
  169. package/dist/utils/framework-components.d.ts +0 -81
  170. package/dist/utils/framework-components.d.ts.map +0 -1
  171. package/dist/utils/global-flags.d.ts +0 -25
  172. package/dist/utils/global-flags.d.ts.map +0 -1
  173. package/dist/utils/output.d.ts +0 -142
  174. package/dist/utils/output.d.ts.map +0 -1
  175. package/dist/utils/result-handler.d.ts +0 -15
  176. package/dist/utils/result-handler.d.ts.map +0 -1
  177. package/dist/utils/spinner.d.ts +0 -29
  178. package/dist/utils/spinner.d.ts.map +0 -1
  179. package/src/utils/action.ts +0 -43
  180. package/src/utils/spinner.ts +0 -67
@@ -0,0 +1,436 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
3
+ import type { MigrationPlanOperation } from '@prisma-next/core-control-plane/types';
4
+ import { findLeaf, findPath, reconstructGraph } from '@prisma-next/migration-tools/dag';
5
+ import { readMigrationsDir } from '@prisma-next/migration-tools/io';
6
+ import type {
7
+ MigrationChainEntry,
8
+ MigrationGraph,
9
+ MigrationPackage,
10
+ } from '@prisma-next/migration-tools/types';
11
+ import { MigrationToolsError } from '@prisma-next/migration-tools/types';
12
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
13
+ import { Command } from 'commander';
14
+ import { relative, resolve } from 'pathe';
15
+ import { loadConfig } from '../config-loader';
16
+ import { createControlClient } from '../control-api/client';
17
+ import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
18
+ import {
19
+ maskConnectionUrl,
20
+ resolveContractPath,
21
+ setCommandDescriptions,
22
+ } from '../utils/command-helpers';
23
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
24
+ import {
25
+ formatCommandHelp,
26
+ formatMigrationStatusOutput,
27
+ formatStyledHeader,
28
+ } from '../utils/output';
29
+ import { handleResult } from '../utils/result-handler';
30
+
31
+ interface MigrationStatusOptions {
32
+ readonly db?: string;
33
+ readonly config?: string;
34
+ readonly json?: string | boolean;
35
+ readonly quiet?: boolean;
36
+ readonly q?: boolean;
37
+ readonly verbose?: boolean;
38
+ readonly v?: boolean;
39
+ readonly vv?: boolean;
40
+ readonly trace?: boolean;
41
+ readonly timestamps?: boolean;
42
+ readonly color?: boolean;
43
+ readonly 'no-color'?: boolean;
44
+ }
45
+
46
+ export interface MigrationStatusEntry {
47
+ readonly dirName: string;
48
+ readonly from: string;
49
+ readonly to: string;
50
+ readonly migrationId: string | null;
51
+ readonly operationCount: number;
52
+ readonly operationSummary: string;
53
+ readonly hasDestructive: boolean;
54
+ readonly status: 'applied' | 'pending' | 'unknown';
55
+ }
56
+
57
+ export interface StatusDiagnostic {
58
+ readonly code: string;
59
+ readonly severity: 'warn' | 'info';
60
+ readonly message: string;
61
+ readonly hints: readonly string[];
62
+ }
63
+
64
+ export interface MigrationStatusResult {
65
+ readonly ok: true;
66
+ readonly mode: 'online' | 'offline';
67
+ readonly migrations: readonly MigrationStatusEntry[];
68
+ readonly markerHash?: string;
69
+ readonly leafHash: string;
70
+ readonly contractHash: string;
71
+ readonly summary: string;
72
+ readonly diagnostics: readonly StatusDiagnostic[];
73
+ }
74
+
75
+ function summarizeOps(ops: readonly MigrationPlanOperation[]): {
76
+ summary: string;
77
+ hasDestructive: boolean;
78
+ } {
79
+ if (ops.length === 0) return { summary: '0 ops', hasDestructive: false };
80
+
81
+ const classes = new Map<string, number>();
82
+ for (const op of ops) {
83
+ classes.set(op.operationClass, (classes.get(op.operationClass) ?? 0) + 1);
84
+ }
85
+
86
+ const hasDestructive = classes.has('destructive');
87
+ const count = ops.length;
88
+ const noun = count === 1 ? 'op' : 'ops';
89
+
90
+ if (classes.size === 1) {
91
+ const cls = [...classes.keys()][0]!;
92
+ return { summary: `${count} ${noun} (all ${cls})`, hasDestructive };
93
+ }
94
+
95
+ const destructiveCount = classes.get('destructive');
96
+ if (destructiveCount) {
97
+ return { summary: `${count} ${noun} (${destructiveCount} destructive)`, hasDestructive };
98
+ }
99
+
100
+ const parts = [...classes.entries()].map(([cls, n]) => `${n} ${cls}`);
101
+ return { summary: `${count} ${noun} (${parts.join(', ')})`, hasDestructive };
102
+ }
103
+
104
+ export function buildMigrationEntries(
105
+ chain: readonly MigrationChainEntry[],
106
+ packages: readonly MigrationPackage[],
107
+ markerHash: string | undefined,
108
+ ): MigrationStatusEntry[] {
109
+ const pkgByDirName = new Map(packages.map((p) => [p.dirName, p]));
110
+
111
+ const markerInChain =
112
+ markerHash === undefined ||
113
+ markerHash === EMPTY_CONTRACT_HASH ||
114
+ chain.some((e) => e.to === markerHash);
115
+
116
+ const entries: MigrationStatusEntry[] = [];
117
+ let reachedMarker = markerHash === undefined || markerHash === EMPTY_CONTRACT_HASH;
118
+
119
+ for (const migration of chain) {
120
+ const pkg = pkgByDirName.get(migration.dirName);
121
+ const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
122
+ const { summary, hasDestructive } = summarizeOps(ops);
123
+
124
+ let status: 'applied' | 'pending' | 'unknown';
125
+ if (markerHash === undefined || !markerInChain) {
126
+ status = 'unknown';
127
+ } else if (reachedMarker) {
128
+ status = 'pending';
129
+ } else {
130
+ status = 'applied';
131
+ }
132
+
133
+ entries.push({
134
+ dirName: migration.dirName,
135
+ from: migration.from,
136
+ to: migration.to,
137
+ migrationId: migration.migrationId,
138
+ operationCount: ops.length,
139
+ operationSummary: summary,
140
+ hasDestructive,
141
+ status,
142
+ });
143
+
144
+ if (!reachedMarker && migration.to === markerHash) {
145
+ reachedMarker = true;
146
+ }
147
+ }
148
+
149
+ return entries;
150
+ }
151
+
152
+ async function executeMigrationStatusCommand(
153
+ options: MigrationStatusOptions,
154
+ flags: GlobalFlags,
155
+ ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
156
+ const config = await loadConfig(options.config);
157
+ const configPath = options.config
158
+ ? relative(process.cwd(), resolve(options.config))
159
+ : 'prisma-next.config.ts';
160
+
161
+ const migrationsDir = resolve(
162
+ options.config ? resolve(options.config, '..') : process.cwd(),
163
+ config.migrations?.dir ?? 'migrations',
164
+ );
165
+ const migrationsRelative = relative(process.cwd(), migrationsDir);
166
+
167
+ const dbConnection = options.db ?? config.db?.connection;
168
+ const hasDriver = !!config.driver;
169
+
170
+ if (flags.json !== 'object' && !flags.quiet) {
171
+ const details: Array<{ label: string; value: string }> = [
172
+ { label: 'config', value: configPath },
173
+ { label: 'migrations', value: migrationsRelative },
174
+ ];
175
+ if (dbConnection && hasDriver) {
176
+ details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
177
+ }
178
+ const header = formatStyledHeader({
179
+ command: 'migration status',
180
+ description: 'Show migration chain and applied status',
181
+ details,
182
+ flags,
183
+ });
184
+ console.log(header);
185
+ }
186
+
187
+ const diagnostics: StatusDiagnostic[] = [];
188
+ let contractHash: string = EMPTY_CONTRACT_HASH;
189
+ try {
190
+ const contractPathAbsolute = resolveContractPath(config);
191
+ const contractContent = await readFile(contractPathAbsolute, 'utf-8');
192
+ try {
193
+ const contractRaw = JSON.parse(contractContent) as Record<string, unknown>;
194
+ const hash = contractRaw['storageHash'];
195
+ if (typeof hash === 'string') {
196
+ contractHash = hash;
197
+ } else {
198
+ diagnostics.push({
199
+ code: 'CONTRACT.MISSING_HASH',
200
+ severity: 'warn',
201
+ message: 'Contract file exists but has no storageHash field',
202
+ hints: ["Run 'prisma-next contract emit' to regenerate the contract"],
203
+ });
204
+ }
205
+ } catch {
206
+ diagnostics.push({
207
+ code: 'CONTRACT.INVALID_JSON',
208
+ severity: 'warn',
209
+ message: 'Contract file contains invalid JSON',
210
+ hints: ["Run 'prisma-next contract emit' to regenerate the contract"],
211
+ });
212
+ }
213
+ } catch {
214
+ diagnostics.push({
215
+ code: 'CONTRACT.UNREADABLE',
216
+ severity: 'warn',
217
+ message: 'Could not read contract file — contract state unknown',
218
+ hints: ["Run 'prisma-next contract emit' to generate a contract"],
219
+ });
220
+ }
221
+
222
+ let allPackages: readonly MigrationPackage[];
223
+ try {
224
+ allPackages = await readMigrationsDir(migrationsDir);
225
+ } catch (error) {
226
+ if (MigrationToolsError.is(error)) {
227
+ return notOk(
228
+ errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
229
+ );
230
+ }
231
+ return notOk(
232
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
233
+ why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
234
+ }),
235
+ );
236
+ }
237
+
238
+ const attested = allPackages.filter((p) => typeof p.manifest.migrationId === 'string');
239
+
240
+ if (attested.length === 0) {
241
+ if (contractHash !== EMPTY_CONTRACT_HASH) {
242
+ diagnostics.push({
243
+ code: 'CONTRACT.AHEAD',
244
+ severity: 'warn',
245
+ message: 'Contract has changed since the last migration was planned',
246
+ hints: [
247
+ "Run 'prisma-next migration plan' to generate a migration for the current contract",
248
+ ],
249
+ });
250
+ }
251
+ return ok({
252
+ ok: true,
253
+ mode: dbConnection && hasDriver ? 'online' : 'offline',
254
+ migrations: [],
255
+ leafHash: EMPTY_CONTRACT_HASH,
256
+ contractHash,
257
+ summary: 'No migrations found',
258
+ diagnostics,
259
+ });
260
+ }
261
+
262
+ let graph: MigrationGraph;
263
+ let leafHash: string;
264
+ try {
265
+ graph = reconstructGraph(attested);
266
+ leafHash = findLeaf(graph);
267
+ } catch (error) {
268
+ if (MigrationToolsError.is(error)) {
269
+ return notOk(
270
+ errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
271
+ );
272
+ }
273
+ throw error;
274
+ }
275
+
276
+ const chain = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);
277
+ if (!chain) {
278
+ return notOk(
279
+ errorRuntime('Cannot reconstruct migration chain', {
280
+ why: `No path from ${EMPTY_CONTRACT_HASH} to leaf ${leafHash}`,
281
+ fix: 'The migration history may have gaps. Check the migrations directory for missing or corrupted packages.',
282
+ }),
283
+ );
284
+ }
285
+
286
+ let markerHash: string | undefined;
287
+ let mode: 'online' | 'offline' = 'offline';
288
+
289
+ if (dbConnection && hasDriver) {
290
+ try {
291
+ const client = createControlClient({
292
+ family: config.family,
293
+ target: config.target,
294
+ adapter: config.adapter,
295
+ driver: config.driver,
296
+ extensionPacks: config.extensionPacks ?? [],
297
+ });
298
+ try {
299
+ await client.connect(dbConnection);
300
+ const marker = await client.readMarker();
301
+ markerHash = marker?.storageHash ?? EMPTY_CONTRACT_HASH;
302
+ mode = 'online';
303
+ } finally {
304
+ await client.close();
305
+ }
306
+ } catch {
307
+ if (flags.json !== 'object' && !flags.quiet) {
308
+ console.log(' ⚠ Could not connect to database — showing offline status\n');
309
+ }
310
+ }
311
+ }
312
+
313
+ const entries = buildMigrationEntries(
314
+ chain,
315
+ attested,
316
+ mode === 'online' ? markerHash : undefined,
317
+ );
318
+
319
+ const markerInChain =
320
+ markerHash === undefined ||
321
+ markerHash === EMPTY_CONTRACT_HASH ||
322
+ chain.some((e) => e.to === markerHash);
323
+
324
+ let summary: string;
325
+ if (mode === 'online') {
326
+ if (!markerInChain) {
327
+ summary = `Database marker does not match any migration — was the database managed with 'db update'?`;
328
+ } else {
329
+ const pendingCount = entries.filter((e) => e.status === 'pending').length;
330
+ const appliedCount = entries.filter((e) => e.status === 'applied').length;
331
+ if (pendingCount === 0) {
332
+ summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
333
+ } else if (markerHash === EMPTY_CONTRACT_HASH) {
334
+ summary = `${pendingCount} pending migration(s) — database has no marker`;
335
+ } else {
336
+ summary = `${pendingCount} pending migration(s) — run 'prisma-next migration apply' to apply`;
337
+ }
338
+ }
339
+ } else {
340
+ summary = `${entries.length} migration(s) on disk`;
341
+ }
342
+
343
+ if (contractHash !== EMPTY_CONTRACT_HASH && contractHash !== leafHash) {
344
+ diagnostics.push({
345
+ code: 'CONTRACT.AHEAD',
346
+ severity: 'warn',
347
+ message: 'Contract has changed since the last migration was planned',
348
+ hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"],
349
+ });
350
+ }
351
+
352
+ if (mode === 'online') {
353
+ const pendingCount = entries.filter((e) => e.status === 'pending').length;
354
+ if (!markerInChain) {
355
+ diagnostics.push({
356
+ code: 'MIGRATION.MARKER_DIVERGED',
357
+ severity: 'warn',
358
+ message: 'Database marker does not match any migration in the chain',
359
+ hints: [
360
+ "The database may have been managed with 'db update' instead of migrations",
361
+ "Run 'prisma-next db verify' to inspect the database state",
362
+ ],
363
+ });
364
+ } else if (pendingCount > 0) {
365
+ diagnostics.push({
366
+ code: 'MIGRATION.DATABASE_BEHIND',
367
+ severity: 'info',
368
+ message: `${pendingCount} migration(s) pending`,
369
+ hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
370
+ });
371
+ } else {
372
+ diagnostics.push({
373
+ code: 'MIGRATION.UP_TO_DATE',
374
+ severity: 'info',
375
+ message: 'Database is up to date',
376
+ hints: [],
377
+ });
378
+ }
379
+ }
380
+
381
+ const result: MigrationStatusResult = {
382
+ ok: true,
383
+ mode,
384
+ migrations: entries,
385
+ leafHash,
386
+ contractHash,
387
+ summary,
388
+ diagnostics,
389
+ ...(markerHash !== undefined ? { markerHash } : {}),
390
+ };
391
+ return ok(result);
392
+ }
393
+
394
+ export function createMigrationStatusCommand(): Command {
395
+ const command = new Command('status');
396
+ setCommandDescriptions(
397
+ command,
398
+ 'Show migration chain and applied status',
399
+ 'Displays the migration chain in order. When a database connection\n' +
400
+ 'is available, shows which migrations are applied and which are pending.\n' +
401
+ 'Without a database connection, shows the chain from disk only.',
402
+ );
403
+ command
404
+ .configureHelp({
405
+ formatHelp: (cmd) => {
406
+ const defaultFlags = parseGlobalFlags({});
407
+ return formatCommandHelp({ command: cmd, flags: defaultFlags });
408
+ },
409
+ })
410
+ .option('--db <url>', 'Database connection string')
411
+ .option('--config <path>', 'Path to prisma-next.config.ts')
412
+ .option('--json [format]', 'Output as JSON (object)', false)
413
+ .option('-q, --quiet', 'Quiet mode: errors only')
414
+ .option('-v, --verbose', 'Verbose output')
415
+ .option('-vv, --trace', 'Trace output')
416
+ .option('--timestamps', 'Add timestamps to output')
417
+ .option('--color', 'Force color output')
418
+ .option('--no-color', 'Disable color output')
419
+ .action(async (options: MigrationStatusOptions) => {
420
+ const flags = parseGlobalFlags(options);
421
+
422
+ const result = await executeMigrationStatusCommand(options, flags);
423
+
424
+ const exitCode = handleResult(result, flags, (statusResult) => {
425
+ if (flags.json === 'object') {
426
+ console.log(JSON.stringify(statusResult, null, 2));
427
+ } else if (!flags.quiet) {
428
+ console.log(formatMigrationStatusOutput(statusResult, flags));
429
+ }
430
+ });
431
+
432
+ process.exit(exitCode);
433
+ });
434
+
435
+ return command;
436
+ }
@@ -0,0 +1,151 @@
1
+ import { attestMigration, verifyMigration } from '@prisma-next/migration-tools/attestation';
2
+ import { MigrationToolsError } from '@prisma-next/migration-tools/types';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
4
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
+ import { Command } from 'commander';
6
+ import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
7
+ import { setCommandDescriptions } from '../utils/command-helpers';
8
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
9
+ import {
10
+ formatCommandHelp,
11
+ formatMigrationVerifyCommandOutput,
12
+ formatStyledHeader,
13
+ } from '../utils/output';
14
+ import { handleResult } from '../utils/result-handler';
15
+
16
+ interface MigrationVerifyOptions {
17
+ readonly dir: string;
18
+ readonly json?: string | boolean;
19
+ readonly quiet?: boolean;
20
+ readonly q?: boolean;
21
+ readonly verbose?: boolean;
22
+ readonly v?: boolean;
23
+ readonly vv?: boolean;
24
+ readonly trace?: boolean;
25
+ readonly timestamps?: boolean;
26
+ readonly color?: boolean;
27
+ readonly 'no-color'?: boolean;
28
+ }
29
+
30
+ export interface MigrationVerifyResult {
31
+ readonly ok: boolean;
32
+ readonly status: 'verified' | 'attested';
33
+ readonly dir: string;
34
+ readonly migrationId?: string;
35
+ readonly storedMigrationId?: string;
36
+ readonly computedMigrationId?: string;
37
+ readonly summary: string;
38
+ }
39
+
40
+ async function executeMigrationVerifyCommand(
41
+ options: MigrationVerifyOptions,
42
+ flags: GlobalFlags,
43
+ ): Promise<Result<MigrationVerifyResult, CliStructuredError>> {
44
+ const dir = options.dir;
45
+
46
+ if (flags.json !== 'object' && !flags.quiet) {
47
+ const header = formatStyledHeader({
48
+ command: 'migration verify',
49
+ description: 'Verify migration package integrity',
50
+ details: [{ label: 'dir', value: dir }],
51
+ flags,
52
+ });
53
+ console.log(header);
54
+ }
55
+
56
+ try {
57
+ const result = await verifyMigration(dir);
58
+
59
+ if (result.ok) {
60
+ return ok({
61
+ ok: true,
62
+ status: 'verified',
63
+ dir,
64
+ ...ifDefined('migrationId', result.storedMigrationId),
65
+ ...ifDefined('storedMigrationId', result.storedMigrationId),
66
+ ...ifDefined('computedMigrationId', result.computedMigrationId),
67
+ summary: 'Migration package verified — migrationId matches',
68
+ });
69
+ }
70
+
71
+ if (result.reason === 'draft') {
72
+ const migrationId = await attestMigration(dir);
73
+ return ok({
74
+ ok: true,
75
+ status: 'attested',
76
+ dir,
77
+ migrationId,
78
+ summary: `Draft migration attested with migrationId: ${migrationId}`,
79
+ });
80
+ }
81
+
82
+ return notOk(
83
+ errorRuntime('migrationId mismatch — migration has been modified', {
84
+ why: `stored=${result.storedMigrationId}, computed=${result.computedMigrationId}`,
85
+ fix: 'If the change was intentional, set "migrationId" to null in migration.json and rerun `migration verify` to re-attest. Otherwise, restore the original migration.',
86
+ meta: {
87
+ storedMigrationId: result.storedMigrationId,
88
+ computedMigrationId: result.computedMigrationId,
89
+ },
90
+ }),
91
+ );
92
+ } catch (error) {
93
+ if (MigrationToolsError.is(error)) {
94
+ return notOk(
95
+ errorRuntime(error.message, {
96
+ why: error.why,
97
+ fix: error.fix,
98
+ meta: { code: error.code, ...(error.details ?? {}) },
99
+ }),
100
+ );
101
+ }
102
+ return notOk(
103
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
104
+ why: `Failed to verify migration: ${error instanceof Error ? error.message : String(error)}`,
105
+ }),
106
+ );
107
+ }
108
+ }
109
+
110
+ export function createMigrationVerifyCommand(): Command {
111
+ const command = new Command('verify');
112
+ setCommandDescriptions(
113
+ command,
114
+ 'Verify a migration package migrationId',
115
+ 'Recomputes the content-addressed migrationId for a migration package and compares\n' +
116
+ 'it against the stored value. Draft migrations (migrationId: null) are automatically\n' +
117
+ 'attested.',
118
+ );
119
+ command
120
+ .configureHelp({
121
+ formatHelp: (cmd) => {
122
+ const defaultFlags = parseGlobalFlags({});
123
+ return formatCommandHelp({ command: cmd, flags: defaultFlags });
124
+ },
125
+ })
126
+ .requiredOption('--dir <path>', 'Path to the migration package directory')
127
+ .option('--json [format]', 'Output as JSON (object)', false)
128
+ .option('-q, --quiet', 'Quiet mode: errors only')
129
+ .option('-v, --verbose', 'Verbose output')
130
+ .option('-vv, --trace', 'Trace output')
131
+ .option('--timestamps', 'Add timestamps to output')
132
+ .option('--color', 'Force color output')
133
+ .option('--no-color', 'Disable color output')
134
+ .action(async (options: MigrationVerifyOptions) => {
135
+ const flags = parseGlobalFlags(options);
136
+
137
+ const result = await executeMigrationVerifyCommand(options, flags);
138
+
139
+ const exitCode = handleResult(result, flags, (verifyResult) => {
140
+ if (flags.json === 'object') {
141
+ console.log(JSON.stringify(verifyResult, null, 2));
142
+ } else if (!flags.quiet) {
143
+ console.log(formatMigrationVerifyCommandOutput(verifyResult, flags));
144
+ }
145
+ });
146
+
147
+ process.exit(exitCode);
148
+ });
149
+
150
+ return command;
151
+ }
@@ -1,7 +1,11 @@
1
1
  import { dirname, resolve } from 'node:path';
2
- import type { PrismaNextConfig } from '@prisma-next/core-control-plane/config-types';
3
- import { validateConfig } from '@prisma-next/core-control-plane/config-validation';
4
- import { errorConfigFileNotFound, errorUnexpected } from '@prisma-next/core-control-plane/errors';
2
+ import type { PrismaNextConfig } from '@prisma-next/config/config-types';
3
+ import { ConfigValidationError, validateConfig } from '@prisma-next/config/config-validation';
4
+ import {
5
+ errorConfigFileNotFound,
6
+ errorConfigValidation,
7
+ errorUnexpected,
8
+ } from '@prisma-next/core-control-plane/errors';
5
9
  import { loadConfig as loadConfigC12 } from 'c12';
6
10
 
7
11
  /**
@@ -44,6 +48,12 @@ export async function loadConfig(configPath?: string): Promise<PrismaNextConfig>
44
48
 
45
49
  return result.config;
46
50
  } catch (error) {
51
+ if (error instanceof ConfigValidationError) {
52
+ throw errorConfigValidation(error.field, {
53
+ why: error.why,
54
+ });
55
+ }
56
+
47
57
  // Re-throw structured errors as-is
48
58
  if (
49
59
  error instanceof Error &&