@prisma-next/cli 0.8.0-dev.1 → 0.8.0-dev.11

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 (152) hide show
  1. package/README.md +7 -8
  2. package/dist/{cli-errors-D3_sMh2K.mjs → cli-errors-CF60g2cG.mjs} +40 -2
  3. package/dist/cli-errors-CF60g2cG.mjs.map +1 -0
  4. package/dist/cli.mjs +67 -19
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-4d26awB-.mjs → client-XkUw4xD0.mjs} +10 -9
  7. package/dist/client-XkUw4xD0.mjs.map +1 -0
  8. package/dist/{command-helpers-BeZHkxV8.mjs → command-helpers-D3vL5yi8.mjs} +29 -6
  9. package/dist/command-helpers-D3vL5yi8.mjs.map +1 -0
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.mjs +7 -7
  13. package/dist/commands/db-schema.mjs +5 -5
  14. package/dist/commands/db-sign.d.mts.map +1 -1
  15. package/dist/commands/db-sign.mjs +67 -25
  16. package/dist/commands/db-sign.mjs.map +1 -1
  17. package/dist/commands/db-update.d.mts.map +1 -1
  18. package/dist/commands/db-update.mjs +37 -9
  19. package/dist/commands/db-update.mjs.map +1 -1
  20. package/dist/commands/db-verify.mjs +1 -1
  21. package/dist/commands/migrate.d.mts +28 -0
  22. package/dist/commands/migrate.d.mts.map +1 -0
  23. package/dist/commands/{migration-apply.mjs → migrate.mjs} +54 -36
  24. package/dist/commands/migrate.mjs.map +1 -0
  25. package/dist/commands/migration-check.d.mts +18 -0
  26. package/dist/commands/migration-check.d.mts.map +1 -0
  27. package/dist/commands/migration-check.mjs +284 -0
  28. package/dist/commands/migration-check.mjs.map +1 -0
  29. package/dist/commands/migration-graph.d.mts +16 -0
  30. package/dist/commands/migration-graph.d.mts.map +1 -0
  31. package/dist/commands/migration-graph.mjs +141 -0
  32. package/dist/commands/migration-graph.mjs.map +1 -0
  33. package/dist/commands/migration-list.d.mts +20 -0
  34. package/dist/commands/migration-list.d.mts.map +1 -0
  35. package/dist/commands/migration-list.mjs +107 -0
  36. package/dist/commands/migration-list.mjs.map +1 -0
  37. package/dist/commands/migration-log.d.mts +21 -0
  38. package/dist/commands/migration-log.d.mts.map +1 -0
  39. package/dist/commands/migration-log.mjs +146 -0
  40. package/dist/commands/migration-log.mjs.map +1 -0
  41. package/dist/commands/migration-new.d.mts.map +1 -1
  42. package/dist/commands/migration-new.mjs +21 -20
  43. package/dist/commands/migration-new.mjs.map +1 -1
  44. package/dist/commands/migration-plan.d.mts +2 -2
  45. package/dist/commands/migration-plan.d.mts.map +1 -1
  46. package/dist/commands/migration-plan.mjs +1 -1
  47. package/dist/commands/migration-show.d.mts +1 -1
  48. package/dist/commands/migration-show.d.mts.map +1 -1
  49. package/dist/commands/migration-show.mjs +85 -47
  50. package/dist/commands/migration-show.mjs.map +1 -1
  51. package/dist/commands/migration-status.d.mts +3 -15
  52. package/dist/commands/migration-status.d.mts.map +1 -1
  53. package/dist/commands/migration-status.mjs +732 -1
  54. package/dist/commands/migration-status.mjs.map +1 -0
  55. package/dist/commands/ref.d.mts +34 -0
  56. package/dist/commands/ref.d.mts.map +1 -0
  57. package/dist/commands/{migration-ref.mjs → ref.mjs} +28 -57
  58. package/dist/commands/ref.mjs.map +1 -0
  59. package/dist/{contract-emit-DLc5GYbr.mjs → contract-emit-CgoFk9AU.mjs} +3 -3
  60. package/dist/{contract-emit-DLc5GYbr.mjs.map → contract-emit-CgoFk9AU.mjs.map} +1 -1
  61. package/dist/{contract-emit-BhKR-D9Y.mjs → contract-emit-GpxW5RLe.mjs} +6 -6
  62. package/dist/{contract-emit-BhKR-D9Y.mjs.map → contract-emit-GpxW5RLe.mjs.map} +1 -1
  63. package/dist/{contract-infer-Bnla2kuK.mjs → contract-infer-D8edZOCi.mjs} +5 -5
  64. package/dist/{contract-infer-Bnla2kuK.mjs.map → contract-infer-D8edZOCi.mjs.map} +1 -1
  65. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs → contract-space-aggregate-loader-D68YpuPR.mjs} +3 -3
  66. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs.map → contract-space-aggregate-loader-D68YpuPR.mjs.map} +1 -1
  67. package/dist/{db-verify-DitNxDiE.mjs → db-verify-DtRB9iHJ.mjs} +7 -7
  68. package/dist/{db-verify-DitNxDiE.mjs.map → db-verify-DtRB9iHJ.mjs.map} +1 -1
  69. package/dist/errors-Cw6kyTyV.mjs +56 -0
  70. package/dist/errors-Cw6kyTyV.mjs.map +1 -0
  71. package/dist/exports/control-api.d.mts +1 -1
  72. package/dist/exports/control-api.mjs +2 -2
  73. package/dist/exports/index.mjs +1 -1
  74. package/dist/exports/init-output.mjs +1 -1
  75. package/dist/{framework-components-ChqVUxR-.mjs → framework-components-xFLFpZUO.mjs} +2 -2
  76. package/dist/{framework-components-ChqVUxR-.mjs.map → framework-components-xFLFpZUO.mjs.map} +1 -1
  77. package/dist/{global-flags-Icqpxk23.d.mts → global-flags-DGmw6Kqg.d.mts} +1 -1
  78. package/dist/{global-flags-Icqpxk23.d.mts.map → global-flags-DGmw6Kqg.d.mts.map} +1 -1
  79. package/dist/{migration-status-Do4Ei0i_.mjs → graph-render-eJDcLWny.mjs} +3 -692
  80. package/dist/graph-render-eJDcLWny.mjs.map +1 -0
  81. package/dist/{init-DW94FRsD.mjs → init-CCcFZoE2.mjs} +133 -61
  82. package/dist/init-CCcFZoE2.mjs.map +1 -0
  83. package/dist/{inspect-live-schema-CyzAzPzF.mjs → inspect-live-schema-CPPqCips.mjs} +4 -4
  84. package/dist/{inspect-live-schema-CyzAzPzF.mjs.map → inspect-live-schema-CPPqCips.mjs.map} +1 -1
  85. package/dist/migration-cli.mjs +1 -1
  86. package/dist/migration-cli.mjs.map +1 -1
  87. package/dist/{migration-command-scaffold-Jp1rosw8.mjs → migration-command-scaffold-B_ezTTwX.mjs} +4 -4
  88. package/dist/{migration-command-scaffold-Jp1rosw8.mjs.map → migration-command-scaffold-B_ezTTwX.mjs.map} +1 -1
  89. package/dist/{migration-plan-q1pPoOCf.mjs → migration-plan-DWB-NTxH.mjs} +54 -28
  90. package/dist/migration-plan-DWB-NTxH.mjs.map +1 -0
  91. package/dist/migration-types-D2FW63pr.d.mts +15 -0
  92. package/dist/migration-types-D2FW63pr.d.mts.map +1 -0
  93. package/dist/{migrations-CTsyBXCA.mjs → migrations-DyUf5lTt.mjs} +2 -2
  94. package/dist/migrations-DyUf5lTt.mjs.map +1 -0
  95. package/dist/{output-BVj6a971.mjs → output-B60Gw5fu.mjs} +12 -11
  96. package/dist/{output-BVj6a971.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  97. package/dist/{result-handler-rmPVKIP2.mjs → result-handler-Bm_6dDYg.mjs} +2 -2
  98. package/dist/{result-handler-rmPVKIP2.mjs.map → result-handler-Bm_6dDYg.mjs.map} +1 -1
  99. package/dist/{terminal-ui-C_hFNbAn.mjs → terminal-ui-XtOQsqe9.mjs} +2 -54
  100. package/dist/terminal-ui-XtOQsqe9.mjs.map +1 -0
  101. package/dist/{types-LItU7E4l.d.mts → types-BS_wpjAY.d.mts} +2 -2
  102. package/dist/{types-LItU7E4l.d.mts.map → types-BS_wpjAY.d.mts.map} +1 -1
  103. package/dist/{verify-CiwNWM9N.mjs → verify-D7ypCCe6.mjs} +1 -1
  104. package/dist/{verify-CiwNWM9N.mjs.map → verify-D7ypCCe6.mjs.map} +1 -1
  105. package/package.json +39 -23
  106. package/src/cli.ts +78 -15
  107. package/src/commands/db-sign.ts +102 -32
  108. package/src/commands/db-update.ts +56 -4
  109. package/src/commands/init/agent-skill-install.ts +145 -43
  110. package/src/commands/init/errors.ts +2 -2
  111. package/src/commands/init/exit-codes.ts +2 -2
  112. package/src/commands/init/index.ts +1 -1
  113. package/src/commands/init/init.ts +15 -6
  114. package/src/commands/init/inputs.ts +1 -1
  115. package/src/commands/init/output.ts +22 -17
  116. package/src/commands/{migration-apply.ts → migrate.ts} +54 -70
  117. package/src/commands/migration-check/exit-codes.ts +3 -0
  118. package/src/commands/migration-check.ts +369 -0
  119. package/src/commands/migration-graph.ts +184 -0
  120. package/src/commands/migration-list.ts +155 -0
  121. package/src/commands/migration-log.ts +218 -0
  122. package/src/commands/migration-new.ts +17 -9
  123. package/src/commands/migration-plan.ts +65 -26
  124. package/src/commands/migration-show.ts +132 -60
  125. package/src/commands/migration-status.ts +77 -64
  126. package/src/commands/{migration-ref.ts → ref.ts} +32 -86
  127. package/src/control-api/operations/apply-aggregate.ts +4 -4
  128. package/src/control-api/operations/db-apply-aggregate.ts +3 -2
  129. package/src/control-api/operations/migration-apply.ts +4 -3
  130. package/src/control-api/types.ts +1 -2
  131. package/src/migration-cli.ts +1 -1
  132. package/src/utils/cli-errors.ts +37 -0
  133. package/src/utils/command-helpers.ts +28 -3
  134. package/src/utils/contract-space-aggregate-loader.ts +2 -2
  135. package/src/utils/contract-space-seed-phase.ts +1 -1
  136. package/src/utils/formatters/help.ts +12 -2
  137. package/src/utils/formatters/migrations.ts +2 -2
  138. package/dist/cli-errors-D3_sMh2K.mjs.map +0 -1
  139. package/dist/client-4d26awB-.mjs.map +0 -1
  140. package/dist/command-helpers-BeZHkxV8.mjs.map +0 -1
  141. package/dist/commands/migration-apply.d.mts +0 -51
  142. package/dist/commands/migration-apply.d.mts.map +0 -1
  143. package/dist/commands/migration-apply.mjs.map +0 -1
  144. package/dist/commands/migration-ref.d.mts +0 -45
  145. package/dist/commands/migration-ref.d.mts.map +0 -1
  146. package/dist/commands/migration-ref.mjs.map +0 -1
  147. package/dist/init-DW94FRsD.mjs.map +0 -1
  148. package/dist/migration-plan-q1pPoOCf.mjs.map +0 -1
  149. package/dist/migration-status-Do4Ei0i_.mjs.map +0 -1
  150. package/dist/migrations-CTsyBXCA.mjs.map +0 -1
  151. package/dist/terminal-ui-C_hFNbAn.mjs.map +0 -1
  152. /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
@@ -1,8 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { errorUnknownInvariant, MigrationToolsError } from '@prisma-next/migration-tools/errors';
4
+ import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
4
5
  import type { RefEntry } from '@prisma-next/migration-tools/refs';
5
- import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
6
+ import { readRefs } from '@prisma-next/migration-tools/refs';
6
7
  import { ifDefined } from '@prisma-next/utils/defined';
7
8
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
9
  import { Command } from 'commander';
@@ -25,6 +26,7 @@ import {
25
26
  errorTargetMigrationNotSupported,
26
27
  errorUnexpected,
27
28
  mapMigrationToolsError,
29
+ mapRefResolutionError,
28
30
  } from '../utils/cli-errors';
29
31
  import {
30
32
  addGlobalOptions,
@@ -44,29 +46,16 @@ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
44
46
  import { handleResult } from '../utils/result-handler';
45
47
  import { TerminalUI } from '../utils/terminal-ui';
46
48
 
47
- interface MigrationApplyCommandOptions extends CommonCommandOptions {
49
+ interface MigrateCommandOptions extends CommonCommandOptions {
48
50
  readonly db?: string;
49
51
  readonly config?: string;
50
- readonly ref?: string;
52
+ readonly to?: string;
51
53
  }
52
54
 
53
- /**
54
- * Per-space breakdown of an apply run. The CLI command surfaces these
55
- * for both the JSON shape (`appliedSpaces[]`) and the human-readable
56
- * formatter (per-space block — same shape `db init` / `db update`
57
- * use, M6 sub-spec § Output shape contract).
58
- */
59
- export interface MigrationApplyResult {
55
+ export interface MigrateResult {
60
56
  readonly ok: boolean;
61
- /** Number of contract spaces that had non-zero pending operations applied. */
62
57
  readonly migrationsApplied: number;
63
- /** Total contract spaces visible in the aggregate (pending + already-up-to-date). */
64
58
  readonly migrationsTotal: number;
65
- /**
66
- * Marker hash for the **app member** post-apply. Surfaced for
67
- * back-compat with single-space callers; per-space markers live on
68
- * `perSpace[].marker.storageHash`.
69
- */
70
59
  readonly markerHash: string;
71
60
  readonly applied: readonly {
72
61
  readonly spaceId: string;
@@ -77,17 +66,7 @@ export interface MigrationApplyResult {
77
66
  readonly operationsExecuted: number;
78
67
  }[];
79
68
  readonly summary: string;
80
- /**
81
- * Per-space breakdown in canonical schedule order (extensions
82
- * alphabetically, then app). Always present for the aggregate-walking
83
- * apply path.
84
- */
85
69
  readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
86
- /**
87
- * Path-decision data for the app member. Surfaced for back-compat
88
- * with single-space callers (cli-journeys invariant tests).
89
- * Absent for no-op applies where the app had nothing to do.
90
- */
91
70
  readonly pathDecision?: MigrationApplyPathDecision;
92
71
  readonly timings: {
93
72
  readonly total: number;
@@ -97,17 +76,17 @@ export interface MigrationApplyResult {
97
76
  function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
98
77
  return errorRuntime(failure.summary, {
99
78
  why: failure.why ?? 'Migration runner failed',
100
- fix: 'Fix the issue and re-run `prisma-next migration apply` — previously applied migrations are preserved.',
79
+ fix: 'Fix the issue and re-run `prisma-next migrate --to <contract>` — previously applied migrations are preserved.',
101
80
  meta: failure.meta ?? {},
102
81
  });
103
82
  }
104
83
 
105
- async function executeMigrationApplyCommand(
106
- options: MigrationApplyCommandOptions,
84
+ async function executeMigrateCommand(
85
+ options: MigrateCommandOptions,
107
86
  flags: GlobalFlags,
108
87
  ui: TerminalUI,
109
88
  startTime: number,
110
- ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
89
+ ): Promise<Result<MigrateResult, CliStructuredErrorType>> {
111
90
  const config = await loadConfig(options.config);
112
91
  const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
113
92
  resolveMigrationPaths(options.config, config);
@@ -116,8 +95,8 @@ async function executeMigrationApplyCommand(
116
95
  if (!dbConnection) {
117
96
  return notOk(
118
97
  errorDatabaseConnectionRequired({
119
- why: `Database connection is required for migration apply (set db.connection in ${configPath}, or pass --db <url>)`,
120
- commandName: 'migration apply',
98
+ why: `Database connection is required for migrate (set db.connection in ${configPath}, or pass --db <url>)`,
99
+ commandName: 'migrate',
121
100
  }),
122
101
  );
123
102
  }
@@ -125,7 +104,7 @@ async function executeMigrationApplyCommand(
125
104
  if (!config.driver) {
126
105
  return notOk(
127
106
  errorDriverRequired({
128
- why: 'Config.driver is required for migration apply',
107
+ why: 'Config.driver is required for migrate',
129
108
  }),
130
109
  );
131
110
  }
@@ -139,12 +118,22 @@ async function executeMigrationApplyCommand(
139
118
  }
140
119
 
141
120
  let refEntry: RefEntry | undefined;
142
- const refName = options.ref;
121
+ const toArg = options.to;
143
122
 
144
- if (refName) {
123
+ if (toArg) {
145
124
  try {
146
125
  const refs = await readRefs(refsDir);
147
- refEntry = resolveRef(refs, refName);
126
+ const { graph } = await loadMigrationPackages(appMigrationsDir);
127
+ const refResult = parseContractRef(toArg, { graph, refs });
128
+ if (!refResult.ok) {
129
+ return notOk(mapRefResolutionError(refResult.failure));
130
+ }
131
+ if (refResult.value.provenance.kind === 'ref') {
132
+ const resolved = refs[refResult.value.provenance.refName];
133
+ if (resolved) refEntry = resolved;
134
+ } else {
135
+ refEntry = { hash: refResult.value.hash, invariants: [] };
136
+ }
148
137
  } catch (error) {
149
138
  if (MigrationToolsError.is(error)) {
150
139
  return notOk(mapMigrationToolsError(error));
@@ -153,8 +142,6 @@ async function executeMigrationApplyCommand(
153
142
  }
154
143
  }
155
144
 
156
- // Resolve and parse the contract envelope. The aggregate-walking
157
- // operation needs the validated app contract to load the aggregate.
158
145
  const contractPathAbsolute = resolveContractPath(config);
159
146
  let contractRaw: Contract;
160
147
  try {
@@ -165,7 +152,7 @@ async function executeMigrationApplyCommand(
165
152
  return notOk(
166
153
  errorFileNotFound(contractPathAbsolute, {
167
154
  why: `Contract file not found at ${contractPathAbsolute}`,
168
- fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry apply.',
155
+ fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry.',
169
156
  }),
170
157
  );
171
158
  }
@@ -188,21 +175,19 @@ async function executeMigrationApplyCommand(
188
175
  value: maskConnectionUrl(dbConnection),
189
176
  });
190
177
  }
191
- if (refName) {
192
- details.push({ label: 'ref', value: refName });
178
+ if (toArg) {
179
+ details.push({ label: 'to', value: toArg });
193
180
  }
194
181
  const header = formatStyledHeader({
195
- command: 'migration apply',
196
- description: 'Apply planned migrations to the database',
197
- url: 'https://pris.ly/migration-apply',
182
+ command: 'migrate',
183
+ description: 'Apply planned migrations to advance the database',
184
+ url: 'https://pris.ly/migrate',
198
185
  details,
199
186
  flags,
200
187
  });
201
188
  ui.stderr(header);
202
189
  }
203
190
 
204
- // Load app-space migration packages — the aggregate operation
205
- // needs them to hydrate the app member's graph for graph-walk.
206
191
  let appPackages: Awaited<ReturnType<typeof loadMigrationPackages>>;
207
192
  try {
208
193
  appPackages = await loadMigrationPackages(appMigrationsDir);
@@ -224,13 +209,6 @@ async function executeMigrationApplyCommand(
224
209
  try {
225
210
  await client.connect(dbConnection);
226
211
 
227
- // Pre-check unknown invariants against `(declared by app graph) ∪
228
- // (already on the app marker)`. The marker side of the union
229
- // catches the case where the ref carries an invariant whose
230
- // declaring migration was retired (history rewritten) but whose
231
- // id is recorded on the marker — surfacing UNKNOWN_INVARIANT
232
- // there would be misleading because the database has already
233
- // satisfied the requirement.
234
212
  if (refEntry && refEntry.invariants.length > 0) {
235
213
  const allMarkers = await client.readAllMarkers();
236
214
  const appMarker = allMarkers.get('app') ?? null;
@@ -242,7 +220,7 @@ async function executeMigrationApplyCommand(
242
220
  return notOk(
243
221
  mapMigrationToolsError(
244
222
  errorUnknownInvariant({
245
- ...ifDefined('refName', refName),
223
+ ...ifDefined('refName', toArg),
246
224
  unknown,
247
225
  declared: [...declared].sort(),
248
226
  }),
@@ -261,7 +239,7 @@ async function executeMigrationApplyCommand(
261
239
  appMigrationPackages: appPackages.bundles,
262
240
  ...ifDefined('refHash', refEntry?.hash),
263
241
  ...(refEntry?.invariants ? { refInvariants: refEntry.invariants } : {}),
264
- ...(refEntry !== undefined ? ifDefined('refName', refName) : {}),
242
+ ...(refEntry !== undefined ? ifDefined('refName', toArg) : {}),
265
243
  });
266
244
 
267
245
  if (!applyResult.ok) {
@@ -290,7 +268,7 @@ async function executeMigrationApplyCommand(
290
268
  }
291
269
  return notOk(
292
270
  errorUnexpected(error instanceof Error ? error.message : String(error), {
293
- why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
271
+ why: `Unexpected error during migrate: ${error instanceof Error ? error.message : String(error)}`,
294
272
  }),
295
273
  );
296
274
  } finally {
@@ -298,23 +276,29 @@ async function executeMigrationApplyCommand(
298
276
  }
299
277
  }
300
278
 
301
- export function createMigrationApplyCommand(): Command {
302
- const command = new Command('apply');
279
+ export function createMigrateCommand(): Command {
280
+ const command = new Command('migrate');
303
281
  setCommandDescriptions(
304
282
  command,
305
- 'Apply planned migrations to the database',
283
+ 'Apply planned migrations to advance the database',
306
284
  'Walks every contract space (app + extensions) and applies pending\n' +
307
285
  'on-disk migrations in canonical order (extensions alphabetically,\n' +
308
- 'then app). Graph-walks the on-disk migration graph for every space —\n' +
309
- "no introspection, no synth. Each space's marker advances inside its\n" +
310
- "transaction; per-space failure rolls back every space's writes.",
286
+ 'then app). Graph-walks the on-disk migration graph for every space.\n' +
287
+ 'Use --to to target a specific contract (hash, ref name, migration dir).',
311
288
  );
312
- setCommandExamples(command, ['prisma-next migration apply --db $DATABASE_URL']);
289
+ setCommandExamples(command, [
290
+ 'prisma-next migrate --db $DATABASE_URL',
291
+ 'prisma-next migrate --to production --db $DATABASE_URL',
292
+ 'prisma-next migrate --to sha256:abc123 --db $DATABASE_URL',
293
+ ]);
313
294
  addGlobalOptions(command)
314
295
  .option('--db <url>', 'Database connection string')
315
296
  .option('--config <path>', 'Path to prisma-next.config.ts')
316
- .option('--ref <name>', 'App-space target ref name from migrations/app/refs/')
317
- .action(async (options: MigrationApplyCommandOptions) => {
297
+ .option(
298
+ '--to <contract>',
299
+ 'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
300
+ )
301
+ .action(async (options: MigrateCommandOptions) => {
318
302
  const flags = parseGlobalFlags(options);
319
303
  const startTime = Date.now();
320
304
 
@@ -323,13 +307,13 @@ export function createMigrationApplyCommand(): Command {
323
307
  interactive: flags.interactive,
324
308
  });
325
309
 
326
- const result = await executeMigrationApplyCommand(options, flags, ui, startTime);
310
+ const result = await executeMigrateCommand(options, flags, ui, startTime);
327
311
 
328
- const exitCode = handleResult(result, flags, ui, (applyResult) => {
312
+ const exitCode = handleResult(result, flags, ui, (migrateResult) => {
329
313
  if (flags.json) {
330
- ui.output(JSON.stringify(applyResult, null, 2));
314
+ ui.output(JSON.stringify(migrateResult, null, 2));
331
315
  } else if (!flags.quiet) {
332
- ui.log(formatMigrationApplyCommandOutput(applyResult, flags));
316
+ ui.log(formatMigrationApplyCommandOutput(migrateResult, flags));
333
317
  }
334
318
  });
335
319
 
@@ -0,0 +1,3 @@
1
+ export const OK = 0;
2
+ export const PRECONDITION = 2;
3
+ export const INTEGRITY_FAILED = 4;
@@ -0,0 +1,369 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
+ import { verifyMigrationHash } from '@prisma-next/migration-tools/hash';
4
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
5
+ import { parseMigrationRef } from '@prisma-next/migration-tools/ref-resolution';
6
+ import { readRefs } from '@prisma-next/migration-tools/refs';
7
+ import { Command } from 'commander';
8
+ import { join, relative } from 'pathe';
9
+ import { loadConfig } from '../config-loader';
10
+ import {
11
+ addGlobalOptions,
12
+ loadMigrationPackages,
13
+ resolveMigrationPaths,
14
+ setCommandDescriptions,
15
+ setCommandExamples,
16
+ setCommandSeeAlso,
17
+ } from '../utils/command-helpers';
18
+ import { formatStyledHeader } from '../utils/formatters/styled';
19
+ import type { CommonCommandOptions } from '../utils/global-flags';
20
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
21
+ import { TerminalUI } from '../utils/terminal-ui';
22
+ import { INTEGRITY_FAILED, OK, PRECONDITION } from './migration-check/exit-codes';
23
+
24
+ interface MigrationCheckOptions extends CommonCommandOptions {
25
+ readonly config?: string;
26
+ }
27
+
28
+ export interface CheckFailure {
29
+ readonly pnCode: string;
30
+ readonly where: string;
31
+ readonly why: string;
32
+ readonly fix: string;
33
+ }
34
+
35
+ export interface MigrationCheckResult {
36
+ readonly ok: boolean;
37
+ readonly failures: readonly CheckFailure[];
38
+ readonly summary: string;
39
+ }
40
+
41
+ /**
42
+ * Canonical user-facing locator for a check failure: the cwd-relative path
43
+ * to the migration package directory. Surfacing the same shape across every
44
+ * PN code means `--json` consumers can branch uniformly on `where`.
45
+ */
46
+ function migrationPathRelative(dirPath: string): string {
47
+ return relative(process.cwd(), dirPath);
48
+ }
49
+
50
+ function migrationFileRelative(dirPath: string, fileName: string): string {
51
+ return join(migrationPathRelative(dirPath), fileName);
52
+ }
53
+
54
+ function checkFileExists(dirPath: string, dirName: string, fileName: string): CheckFailure | null {
55
+ if (!existsSync(join(dirPath, fileName))) {
56
+ return {
57
+ pnCode: 'PN-MIG-CHECK-002',
58
+ where: migrationFileRelative(dirPath, fileName),
59
+ why: `${fileName} is missing from ${dirName}`,
60
+ fix: 'Re-emit the migration package or restore from version control.',
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Within-migration snapshot-consistency check (PN-MIG-CHECK-005).
68
+ *
69
+ * Compares the migration's stored `metadata.to` against the `storageHash`
70
+ * recorded in its on-disk `end-contract.json` snapshot. The two values are
71
+ * independent on-disk records of the same fact (the migration's destination
72
+ * contract); drift between them indicates the package is internally
73
+ * corrupt. Cross-migration consistency (one migration's end-contract.json
74
+ * agreeing with the next migration's start-contract.json) is a separate
75
+ * check that requires shadow execution and is deferred to
76
+ * `migration preflight`.
77
+ *
78
+ * Shared between the graph-wide and per-migration code paths so both report
79
+ * the same failure for the same on-disk state.
80
+ */
81
+ function checkSnapshotConsistency(pkg: OnDiskMigrationPackage): CheckFailure | null {
82
+ const endContractPath = join(pkg.dirPath, 'end-contract.json');
83
+ if (!existsSync(endContractPath)) return null;
84
+ try {
85
+ const raw = JSON.parse(readFileSync(endContractPath, 'utf-8')) as Record<string, unknown>;
86
+ const storage = raw['storage'] as Record<string, unknown> | undefined;
87
+ const snapshotHash = storage?.['storageHash'];
88
+ if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
89
+ return {
90
+ pnCode: 'PN-MIG-CHECK-005',
91
+ where: migrationPathRelative(pkg.dirPath),
92
+ why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but end-contract.json has storageHash=${snapshotHash}`,
93
+ fix: 'Re-emit the migration package so migration.json and end-contract.json agree.',
94
+ };
95
+ }
96
+ } catch {
97
+ return {
98
+ pnCode: 'PN-MIG-CHECK-006',
99
+ where: migrationPathRelative(pkg.dirPath),
100
+ why: `Migration "${pkg.dirName}" has an unparseable end-contract.json.`,
101
+ fix: 'Re-emit the migration package to repair the snapshot file.',
102
+ };
103
+ }
104
+ return null;
105
+ }
106
+
107
+ async function executeMigrationCheckCommand(
108
+ target: string | undefined,
109
+ options: MigrationCheckOptions,
110
+ flags: GlobalFlags,
111
+ ui: TerminalUI,
112
+ ): Promise<{ result: MigrationCheckResult; exitCode: number }> {
113
+ const config = await loadConfig(options.config);
114
+ const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } = resolveMigrationPaths(
115
+ options.config,
116
+ config,
117
+ );
118
+
119
+ if (!flags.json && !flags.quiet) {
120
+ const details: Array<{ label: string; value: string }> = [
121
+ { label: 'config', value: configPath },
122
+ { label: 'migrations', value: appMigrationsRelative },
123
+ ];
124
+ if (target) {
125
+ details.push({ label: 'target', value: target });
126
+ }
127
+ const header = formatStyledHeader({
128
+ command: 'migration check',
129
+ description: 'Verify artifact and graph integrity',
130
+ details,
131
+ flags,
132
+ });
133
+ ui.stderr(header);
134
+ }
135
+
136
+ const failures: CheckFailure[] = [];
137
+
138
+ let bundles: Awaited<ReturnType<typeof loadMigrationPackages>>['bundles'];
139
+ let graph: Awaited<ReturnType<typeof loadMigrationPackages>>['graph'];
140
+ try {
141
+ const loaded = await loadMigrationPackages(appMigrationsDir);
142
+ bundles = loaded.bundles;
143
+ graph = loaded.graph;
144
+ } catch (error) {
145
+ if (MigrationToolsError.is(error)) {
146
+ const pnCode =
147
+ error.code === 'MIGRATION.HASH_MISMATCH' ? 'PN-MIG-CHECK-001' : 'PN-MIG-CHECK-002';
148
+ // Normalise to a cwd-relative path. `error.details.dir` is absolute
149
+ // (the migration-tools layer doesn't know the caller's cwd); the
150
+ // `filePath` fallback is also absolute. Surfacing the relative form
151
+ // matches the rest of the command's `where` shape and keeps `--json`
152
+ // consumers from having to special-case the bootstrap-failure path.
153
+ const rawWhere =
154
+ (error.details?.['dir'] as string) ?? (error.details?.['filePath'] as string) ?? null;
155
+ const where = rawWhere ? relative(process.cwd(), rawWhere) : 'unknown';
156
+ failures.push({
157
+ pnCode,
158
+ where,
159
+ why: error.why,
160
+ fix: error.fix,
161
+ });
162
+ return {
163
+ result: { ok: false, failures, summary: `${failures.length} integrity failure(s)` },
164
+ exitCode: INTEGRITY_FAILED,
165
+ };
166
+ }
167
+ throw error;
168
+ }
169
+
170
+ if (existsSync(appMigrationsDir)) {
171
+ const loadedDirNames = new Set(bundles.map((p) => p.dirName));
172
+ try {
173
+ const entries = readdirSync(appMigrationsDir);
174
+ for (const entry of entries) {
175
+ if (entry.startsWith('.') || entry.startsWith('_') || entry === 'refs') continue;
176
+ const entryPath = join(appMigrationsDir, entry);
177
+ try {
178
+ if (!statSync(entryPath).isDirectory()) continue;
179
+ } catch {
180
+ continue;
181
+ }
182
+ if (!loadedDirNames.has(entry)) {
183
+ for (const f of ['migration.json', 'ops.json']) {
184
+ const fail = checkFileExists(entryPath, entry, f);
185
+ if (fail) failures.push(fail);
186
+ }
187
+ }
188
+ }
189
+ } catch {
190
+ // migrations dir unreadable — skip
191
+ }
192
+ }
193
+
194
+ if (target) {
195
+ const refs = await readRefs(refsDir);
196
+ const migResult = parseMigrationRef(target, { graph, refs });
197
+ if (!migResult.ok) {
198
+ const msg =
199
+ migResult.failure.kind === 'not-found'
200
+ ? `Migration "${target}" does not exist`
201
+ : migResult.failure.kind === 'wrong-grammar'
202
+ ? migResult.failure.message
203
+ : `Invalid migration reference: "${target}"`;
204
+ return {
205
+ result: { ok: false, failures: [], summary: msg },
206
+ exitCode: PRECONDITION,
207
+ };
208
+ }
209
+
210
+ const matchedPkg = bundles.find(
211
+ (p) => p.metadata.migrationHash === migResult.value.migrationHash,
212
+ );
213
+ if (!matchedPkg) {
214
+ return {
215
+ result: {
216
+ ok: false,
217
+ failures: [],
218
+ summary: `Migration package for "${target}" not found on disk`,
219
+ },
220
+ exitCode: PRECONDITION,
221
+ };
222
+ }
223
+
224
+ for (const f of ['migration.json', 'ops.json']) {
225
+ const fail = checkFileExists(matchedPkg.dirPath, matchedPkg.dirName, f);
226
+ if (fail) failures.push(fail);
227
+ }
228
+
229
+ const verification = verifyMigrationHash(matchedPkg);
230
+ if (!verification.ok) {
231
+ failures.push({
232
+ pnCode: 'PN-MIG-CHECK-001',
233
+ where: migrationFileRelative(matchedPkg.dirPath, 'migration.json'),
234
+ why: `Stored hash ${verification.storedHash} does not match recomputed hash ${verification.computedHash}`,
235
+ fix: 'Re-emit the migration package or restore from version control.',
236
+ });
237
+ }
238
+
239
+ // PN-MIG-CHECK-005 must fire per-migration as well as graph-wide; both
240
+ // call sites delegate to the shared helper so the same on-disk drift
241
+ // produces the same failure regardless of how the user invoked check.
242
+ const snapshotFailure = checkSnapshotConsistency(matchedPkg);
243
+ if (snapshotFailure) failures.push(snapshotFailure);
244
+ } else {
245
+ for (const pkg of bundles) {
246
+ for (const f of ['migration.json', 'ops.json']) {
247
+ const fail = checkFileExists(pkg.dirPath, pkg.dirName, f);
248
+ if (fail) failures.push(fail);
249
+ }
250
+
251
+ const verification = verifyMigrationHash(pkg);
252
+ if (!verification.ok) {
253
+ failures.push({
254
+ pnCode: 'PN-MIG-CHECK-001',
255
+ where: migrationFileRelative(pkg.dirPath, 'migration.json'),
256
+ why: `Stored hash ${verification.storedHash} does not match recomputed hash ${verification.computedHash}`,
257
+ fix: 'Re-emit the migration package or restore from version control.',
258
+ });
259
+ }
260
+ }
261
+
262
+ for (const pkg of bundles) {
263
+ const snapshotFailure = checkSnapshotConsistency(pkg);
264
+ if (snapshotFailure) failures.push(snapshotFailure);
265
+ }
266
+
267
+ const allToHashes = new Set(bundles.map((p) => p.metadata.to));
268
+ for (const pkg of bundles) {
269
+ const isReachable =
270
+ pkg.metadata.from === null ||
271
+ allToHashes.has(pkg.metadata.from) ||
272
+ pkg.metadata.from === 'sha256:empty';
273
+ if (!isReachable) {
274
+ failures.push({
275
+ pnCode: 'PN-MIG-CHECK-003',
276
+ where: migrationPathRelative(pkg.dirPath),
277
+ why: `Migration "${pkg.dirName}" starts from ${pkg.metadata.from} which no other migration produces`,
278
+ fix: 'This migration is unreachable in the graph. Delete it or re-emit a connecting migration.',
279
+ });
280
+ }
281
+ }
282
+
283
+ try {
284
+ const refs = await readRefs(refsDir);
285
+ for (const [name, entry] of Object.entries(refs)) {
286
+ if (!graph.nodes.has(entry.hash)) {
287
+ failures.push({
288
+ pnCode: 'PN-MIG-CHECK-004',
289
+ where: relative(process.cwd(), join(refsDir, `${name}.json`)),
290
+ why: `Ref "${name}" points at ${entry.hash} which does not exist in the migration graph`,
291
+ fix: `Update the ref with \`prisma-next ref set ${name} <valid-hash>\` or delete it.`,
292
+ });
293
+ }
294
+ }
295
+ } catch {
296
+ // Refs unreadable — skip ref checks
297
+ }
298
+ }
299
+
300
+ if (failures.length === 0) {
301
+ return {
302
+ result: { ok: true, failures: [], summary: 'All checks passed' },
303
+ exitCode: OK,
304
+ };
305
+ }
306
+
307
+ return {
308
+ result: { ok: false, failures, summary: `${failures.length} integrity failure(s)` },
309
+ exitCode: INTEGRITY_FAILED,
310
+ };
311
+ }
312
+
313
+ export function createMigrationCheckCommand(): Command {
314
+ const command = new Command('check');
315
+ setCommandDescriptions(
316
+ command,
317
+ 'Verify artifact and graph integrity',
318
+ 'Validates that on-disk migration packages are internally consistent\n' +
319
+ '(hashes match, manifests are complete) and that the graph is well-formed\n' +
320
+ '(edges connect, refs point at valid nodes). Offline — does not consult\n' +
321
+ 'the database.',
322
+ );
323
+ setCommandExamples(command, [
324
+ 'prisma-next migration check',
325
+ 'prisma-next migration check 20260101-add-users',
326
+ 'prisma-next migration check --json',
327
+ ]);
328
+ setCommandSeeAlso(command, [
329
+ { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
330
+ { verb: 'migration list', oneLiner: 'List on-disk migrations' },
331
+ { verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
332
+ ]);
333
+ command.exitOverride();
334
+ addGlobalOptions(command)
335
+ .argument('[migration]', 'Migration reference (directory name or hash) to check')
336
+ .option('--config <path>', 'Path to prisma-next.config.ts')
337
+ .action(async (target: string | undefined, options: MigrationCheckOptions) => {
338
+ const flags = parseGlobalFlags(options);
339
+ const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
340
+
341
+ let result: MigrationCheckResult;
342
+ let exitCode: number;
343
+ try {
344
+ ({ result, exitCode } = await executeMigrationCheckCommand(target, options, flags, ui));
345
+ } catch (error) {
346
+ const msg = error instanceof Error ? error.message : String(error);
347
+ result = { ok: false, failures: [], summary: msg };
348
+ exitCode = PRECONDITION;
349
+ }
350
+
351
+ if (flags.json) {
352
+ ui.output(JSON.stringify(result, null, 2));
353
+ } else if (!flags.quiet) {
354
+ if (result.ok) {
355
+ ui.log(`✔ ${result.summary}`);
356
+ } else {
357
+ for (const f of result.failures) {
358
+ ui.log(`✗ [${f.pnCode}] ${f.where}: ${f.why}`);
359
+ ui.log(` fix: ${f.fix}`);
360
+ }
361
+ ui.log(`\n${result.summary}`);
362
+ }
363
+ }
364
+
365
+ process.exit(exitCode);
366
+ });
367
+
368
+ return command;
369
+ }