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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +8 -9
  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-BCnP7cHo.mjs → client-Brv4qlfB.mjs} +28 -30
  7. package/dist/client-Brv4qlfB.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.d.mts.map +1 -1
  21. package/dist/commands/db-verify.mjs +1 -1
  22. package/dist/commands/migrate.d.mts +28 -0
  23. package/dist/commands/migrate.d.mts.map +1 -0
  24. package/dist/commands/{migration-apply.mjs → migrate.mjs} +65 -39
  25. package/dist/commands/migrate.mjs.map +1 -0
  26. package/dist/commands/migration-check.d.mts +18 -0
  27. package/dist/commands/migration-check.d.mts.map +1 -0
  28. package/dist/commands/migration-check.mjs +284 -0
  29. package/dist/commands/migration-check.mjs.map +1 -0
  30. package/dist/commands/migration-graph.d.mts +16 -0
  31. package/dist/commands/migration-graph.d.mts.map +1 -0
  32. package/dist/commands/migration-graph.mjs +141 -0
  33. package/dist/commands/migration-graph.mjs.map +1 -0
  34. package/dist/commands/migration-list.d.mts +20 -0
  35. package/dist/commands/migration-list.d.mts.map +1 -0
  36. package/dist/commands/migration-list.mjs +107 -0
  37. package/dist/commands/migration-list.mjs.map +1 -0
  38. package/dist/commands/migration-log.d.mts +21 -0
  39. package/dist/commands/migration-log.d.mts.map +1 -0
  40. package/dist/commands/migration-log.mjs +146 -0
  41. package/dist/commands/migration-log.mjs.map +1 -0
  42. package/dist/commands/migration-new.d.mts.map +1 -1
  43. package/dist/commands/migration-new.mjs +30 -29
  44. package/dist/commands/migration-new.mjs.map +1 -1
  45. package/dist/commands/migration-plan.d.mts +2 -2
  46. package/dist/commands/migration-plan.d.mts.map +1 -1
  47. package/dist/commands/migration-plan.mjs +1 -1
  48. package/dist/commands/migration-show.d.mts +1 -1
  49. package/dist/commands/migration-show.d.mts.map +1 -1
  50. package/dist/commands/migration-show.mjs +90 -52
  51. package/dist/commands/migration-show.mjs.map +1 -1
  52. package/dist/commands/migration-status.d.mts +5 -17
  53. package/dist/commands/migration-status.d.mts.map +1 -1
  54. package/dist/commands/migration-status.mjs +732 -1
  55. package/dist/commands/migration-status.mjs.map +1 -0
  56. package/dist/commands/ref.d.mts +34 -0
  57. package/dist/commands/ref.d.mts.map +1 -0
  58. package/dist/commands/{migration-ref.mjs → ref.mjs} +28 -57
  59. package/dist/commands/ref.mjs.map +1 -0
  60. package/dist/{contract-emit-9DBda5Ou.mjs → contract-emit-C3STUIBg.mjs} +6 -6
  61. package/dist/{contract-emit-9DBda5Ou.mjs.map → contract-emit-C3STUIBg.mjs.map} +1 -1
  62. package/dist/{contract-emit-B77TsJqf.mjs → contract-emit-iynA3BCA.mjs} +9 -5
  63. package/dist/contract-emit-iynA3BCA.mjs.map +1 -0
  64. package/dist/{contract-infer-ByxhPjpW.mjs → contract-infer-Cnj8G1E2.mjs} +5 -5
  65. package/dist/{contract-infer-ByxhPjpW.mjs.map → contract-infer-Cnj8G1E2.mjs.map} +1 -1
  66. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs → contract-space-aggregate-loader-pAc8CDfY.mjs} +4 -4
  67. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs.map → contract-space-aggregate-loader-pAc8CDfY.mjs.map} +1 -1
  68. package/dist/{db-verify-Czm5T-J4.mjs → db-verify-D7cyH_zz.mjs} +12 -9
  69. package/dist/db-verify-D7cyH_zz.mjs.map +1 -0
  70. package/dist/errors-Cw6kyTyV.mjs +56 -0
  71. package/dist/errors-Cw6kyTyV.mjs.map +1 -0
  72. package/dist/exports/control-api.d.mts +1 -1
  73. package/dist/exports/control-api.d.mts.map +1 -1
  74. package/dist/exports/control-api.mjs +2 -2
  75. package/dist/exports/index.mjs +1 -1
  76. package/dist/exports/index.mjs.map +1 -1
  77. package/dist/exports/init-output.mjs +1 -1
  78. package/dist/{framework-components-ChqVUxR-.mjs → framework-components-xFLFpZUO.mjs} +2 -2
  79. package/dist/{framework-components-ChqVUxR-.mjs.map → framework-components-xFLFpZUO.mjs.map} +1 -1
  80. package/dist/{global-flags-Icqpxk23.d.mts → global-flags-DGmw6Kqg.d.mts} +1 -1
  81. package/dist/{global-flags-Icqpxk23.d.mts.map → global-flags-DGmw6Kqg.d.mts.map} +1 -1
  82. package/dist/{migration-status-By9G5p2H.mjs → graph-render-eJDcLWny.mjs} +3 -692
  83. package/dist/graph-render-eJDcLWny.mjs.map +1 -0
  84. package/dist/{init-B-k3a1Qw.mjs → init-Bqg5JWg7.mjs} +133 -61
  85. package/dist/init-Bqg5JWg7.mjs.map +1 -0
  86. package/dist/{inspect-live-schema-DxdBd4Er.mjs → inspect-live-schema-CWLK_lgs.mjs} +4 -4
  87. package/dist/{inspect-live-schema-DxdBd4Er.mjs.map → inspect-live-schema-CWLK_lgs.mjs.map} +1 -1
  88. package/dist/migration-cli.mjs +1 -1
  89. package/dist/migration-cli.mjs.map +1 -1
  90. package/dist/{migration-command-scaffold-BdV8JYXV.mjs → migration-command-scaffold-CmXXC1UZ.mjs} +4 -4
  91. package/dist/{migration-command-scaffold-BdV8JYXV.mjs.map → migration-command-scaffold-CmXXC1UZ.mjs.map} +1 -1
  92. package/dist/{migration-plan-mRu5K81L.mjs → migration-plan-CHyUlBV0.mjs} +76 -37
  93. package/dist/migration-plan-CHyUlBV0.mjs.map +1 -0
  94. package/dist/migration-types-D2FW63pr.d.mts +15 -0
  95. package/dist/migration-types-D2FW63pr.d.mts.map +1 -0
  96. package/dist/{migrations-CTsyBXCA.mjs → migrations-DyUf5lTt.mjs} +2 -2
  97. package/dist/migrations-DyUf5lTt.mjs.map +1 -0
  98. package/dist/{output-BVj6a971.mjs → output-B60Gw5fu.mjs} +12 -11
  99. package/dist/{output-BVj6a971.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  100. package/dist/{result-handler-rmPVKIP2.mjs → result-handler-Bm_6dDYg.mjs} +2 -2
  101. package/dist/{result-handler-rmPVKIP2.mjs.map → result-handler-Bm_6dDYg.mjs.map} +1 -1
  102. package/dist/{terminal-ui-C_hFNbAn.mjs → terminal-ui-XtOQsqe9.mjs} +2 -54
  103. package/dist/terminal-ui-XtOQsqe9.mjs.map +1 -0
  104. package/dist/{types-LItU7E4l.d.mts → types-0aS865QN.d.mts} +14 -8
  105. package/dist/types-0aS865QN.d.mts.map +1 -0
  106. package/dist/{verify-CiwNWM9N.mjs → verify-D7ypCCe6.mjs} +1 -1
  107. package/dist/{verify-CiwNWM9N.mjs.map → verify-D7ypCCe6.mjs.map} +1 -1
  108. package/package.json +39 -23
  109. package/src/cli.ts +78 -15
  110. package/src/commands/db-sign.ts +102 -32
  111. package/src/commands/db-update.ts +56 -4
  112. package/src/commands/db-verify.ts +19 -3
  113. package/src/commands/init/agent-skill-install.ts +145 -43
  114. package/src/commands/init/errors.ts +2 -2
  115. package/src/commands/init/exit-codes.ts +2 -2
  116. package/src/commands/init/index.ts +1 -1
  117. package/src/commands/init/init.ts +15 -6
  118. package/src/commands/init/inputs.ts +1 -1
  119. package/src/commands/init/output.ts +22 -17
  120. package/src/commands/{migration-apply.ts → migrate.ts} +77 -73
  121. package/src/commands/migration-check/exit-codes.ts +3 -0
  122. package/src/commands/migration-check.ts +369 -0
  123. package/src/commands/migration-graph.ts +184 -0
  124. package/src/commands/migration-list.ts +155 -0
  125. package/src/commands/migration-log.ts +218 -0
  126. package/src/commands/migration-new.ts +30 -22
  127. package/src/commands/migration-plan.ts +104 -35
  128. package/src/commands/migration-show.ts +141 -65
  129. package/src/commands/migration-status.ts +82 -69
  130. package/src/commands/{migration-ref.ts → ref.ts} +32 -86
  131. package/src/control-api/client.ts +30 -21
  132. package/src/control-api/operations/apply-aggregate.ts +4 -4
  133. package/src/control-api/operations/contract-emit.ts +26 -3
  134. package/src/control-api/operations/db-apply-aggregate.ts +4 -3
  135. package/src/control-api/operations/db-verify.ts +2 -2
  136. package/src/control-api/operations/migration-apply.ts +5 -4
  137. package/src/control-api/types.ts +12 -7
  138. package/src/load-ts-contract.ts +9 -1
  139. package/src/migration-cli.ts +1 -1
  140. package/src/utils/cli-errors.ts +37 -0
  141. package/src/utils/command-helpers.ts +28 -3
  142. package/src/utils/contract-space-aggregate-loader.ts +4 -4
  143. package/src/utils/contract-space-seed-phase.ts +2 -2
  144. package/src/utils/formatters/help.ts +12 -2
  145. package/src/utils/formatters/migrations.ts +2 -2
  146. package/dist/cli-errors-D3_sMh2K.mjs.map +0 -1
  147. package/dist/client-BCnP7cHo.mjs.map +0 -1
  148. package/dist/command-helpers-BeZHkxV8.mjs.map +0 -1
  149. package/dist/commands/migration-apply.d.mts +0 -51
  150. package/dist/commands/migration-apply.d.mts.map +0 -1
  151. package/dist/commands/migration-apply.mjs.map +0 -1
  152. package/dist/commands/migration-ref.d.mts +0 -45
  153. package/dist/commands/migration-ref.d.mts.map +0 -1
  154. package/dist/commands/migration-ref.mjs.map +0 -1
  155. package/dist/contract-emit-B77TsJqf.mjs.map +0 -1
  156. package/dist/db-verify-Czm5T-J4.mjs.map +0 -1
  157. package/dist/init-B-k3a1Qw.mjs.map +0 -1
  158. package/dist/migration-plan-mRu5K81L.mjs.map +0 -1
  159. package/dist/migration-status-By9G5p2H.mjs.map +0 -1
  160. package/dist/migrations-CTsyBXCA.mjs.map +0 -1
  161. package/dist/terminal-ui-C_hFNbAn.mjs.map +0 -1
  162. package/dist/types-LItU7E4l.d.mts.map +0 -1
  163. /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import {
5
+ type ControlFamilyInstance,
5
6
  createControlStack,
6
7
  hasOperationPreview,
7
8
  type MigrationPlanOperation,
@@ -18,6 +19,8 @@ import {
18
19
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
19
20
  import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
20
21
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
22
+ import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
23
+ import { readRefs } from '@prisma-next/migration-tools/refs';
21
24
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
22
25
  import { Command } from 'commander';
23
26
  import { join, relative } from 'pathe';
@@ -28,10 +31,10 @@ import {
28
31
  errorContractValidationFailed,
29
32
  errorFileNotFound,
30
33
  errorMigrationPlanningFailed,
31
- errorRuntime,
32
34
  errorTargetMigrationNotSupported,
33
35
  errorUnexpected,
34
36
  mapMigrationToolsError,
37
+ mapRefResolutionError,
35
38
  } from '../utils/cli-errors';
36
39
  import {
37
40
  addGlobalOptions,
@@ -58,6 +61,55 @@ interface MigrationPlanOptions extends CommonCommandOptions {
58
61
  readonly from?: string;
59
62
  }
60
63
 
64
+ /**
65
+ * Load a predecessor migration's destination contract from its sibling
66
+ * `end-contract.json` on disk and route it through the family's
67
+ * `ContractSerializer` (via `deserializeContract`) so the in-memory shape
68
+ * is the hydrated `Contract` every other caller sees. Bypassing this
69
+ * seam was the root cause of TML-2536: a raw `JSON.parse(...) as Contract`
70
+ * here let polymorphic `storage.types` entries reach the planner without
71
+ * the `kind` discriminator the planner dispatches on.
72
+ *
73
+ * Throws `CliStructuredError` with:
74
+ * - `errorFileNotFound` when the sibling file is missing — the user
75
+ * has likely deleted or never authored the snapshot, and the
76
+ * message names the file and points them at re-emitting from the
77
+ * source.
78
+ * - `errorContractValidationFailed` when the JSON parses but the
79
+ * family deserializer rejects it (legacy untagged shape, structural
80
+ * mismatch, etc.) — the message names the predecessor's path so
81
+ * the operator can locate the bad snapshot.
82
+ */
83
+ async function readPredecessorEndContract(
84
+ migrationDir: string,
85
+ familyInstance: ControlFamilyInstance<string, unknown>,
86
+ ): Promise<Contract> {
87
+ const path = join(migrationDir, 'end-contract.json');
88
+ let raw: string;
89
+ try {
90
+ raw = await readFile(path, 'utf-8');
91
+ } catch (error) {
92
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
93
+ throw errorFileNotFound(path, {
94
+ why: `Predecessor migration is missing its destination contract snapshot at ${path}`,
95
+ fix: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
96
+ });
97
+ }
98
+ throw error;
99
+ }
100
+ try {
101
+ return familyInstance.deserializeContract(JSON.parse(raw) as unknown);
102
+ } catch (error) {
103
+ if (CliStructuredError.is(error)) {
104
+ throw error;
105
+ }
106
+ throw errorContractValidationFailed(
107
+ `Predecessor contract at ${path} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
108
+ { where: { path } },
109
+ );
110
+ }
111
+ }
112
+
61
113
  export interface MigrationPlanResult {
62
114
  readonly ok: boolean;
63
115
  readonly noOp: boolean;
@@ -74,7 +126,7 @@ export interface MigrationPlanResult {
74
126
  * Surfacing these in the result (rather than only via `ui.step` log
75
127
  * lines) makes the cross-space side effect explicit to JSON consumers
76
128
  * and the success-summary renderer — the same multi-space side effect
77
- * that `migration apply` will replay.
129
+ * that `migrate` will replay.
78
130
  */
79
131
  readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
80
132
  readonly operations: readonly {
@@ -155,19 +207,26 @@ async function executeMigrationPlanCommand(
155
207
  );
156
208
  }
157
209
 
158
- let toContractJson: Contract;
210
+ // Construct the family instance up-front so on-disk reads (the app
211
+ // contract here + every `readPredecessorEndContract` below) cross the
212
+ // serializer seam at the read site, not after the planner has already
213
+ // started dispatching on raw shapes. See TML-2536.
214
+ const stack = createControlStack(config);
215
+ const familyInstance = config.family.create(stack);
216
+
217
+ let toContract: Contract;
159
218
  try {
160
- toContractJson = JSON.parse(contractJsonContent) as Contract;
219
+ toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
161
220
  } catch (error) {
162
221
  return notOk(
163
222
  errorContractValidationFailed(
164
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
223
+ `Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
165
224
  { where: { path: contractPathAbsolute } },
166
225
  ),
167
226
  );
168
227
  }
169
228
 
170
- const rawStorageHash = toContractJson.storage?.storageHash;
229
+ const rawStorageHash = toContract.storage?.storageHash;
171
230
  if (typeof rawStorageHash !== 'string') {
172
231
  return notOk(
173
232
  errorContractValidationFailed('Contract is missing storageHash', {
@@ -186,24 +245,26 @@ async function executeMigrationPlanCommand(
186
245
  const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
187
246
 
188
247
  if (options.from) {
189
- const resolved = resolveBundleByPrefix(bundles, options.from);
190
- if (!resolved.ok) {
191
- const f = resolved.failure;
248
+ const refs = await readRefs(resolveMigrationPaths(options.config, config).refsDir);
249
+ const refResult = parseContractRef(options.from, { graph, refs });
250
+ if (!refResult.ok) {
251
+ return notOk(mapRefResolutionError(refResult.failure));
252
+ }
253
+ fromHash = refResult.value.hash;
254
+ const matchingBundle = bundles.find((p) => p.metadata.to === fromHash);
255
+ if (!matchingBundle) {
192
256
  return notOk(
193
- f.reason === 'ambiguous'
194
- ? errorRuntime('Multiple matching migrations found', {
195
- why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
196
- fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
197
- })
198
- : errorRuntime('Starting contract not found', {
199
- why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
200
- fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
201
- }),
257
+ errorUnexpected(
258
+ `No migration bundle found for --from "${options.from}" (resolved hash: ${fromHash})`,
259
+ {
260
+ why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${fromHash}.`,
261
+ fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
262
+ },
263
+ ),
202
264
  );
203
265
  }
204
- fromHash = resolved.value.metadata.to;
205
- fromContract = resolved.value.metadata.toContract;
206
- fromContractSourceDir = resolved.value.dirPath;
266
+ fromContractSourceDir = matchingBundle.dirPath;
267
+ fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
207
268
  } else {
208
269
  const latestMigration = findLatestMigration(graph);
209
270
  if (latestMigration) {
@@ -212,8 +273,8 @@ async function executeMigrationPlanCommand(
212
273
  (p) => p.metadata.migrationHash === latestMigration.migrationHash,
213
274
  );
214
275
  if (leafPkg) {
215
- fromContract = leafPkg.metadata.toContract;
216
276
  fromContractSourceDir = leafPkg.dirPath;
277
+ fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
217
278
  }
218
279
  }
219
280
  }
@@ -221,6 +282,12 @@ async function executeMigrationPlanCommand(
221
282
  if (MigrationToolsError.is(error)) {
222
283
  return notOk(mapMigrationToolsError(error));
223
284
  }
285
+ // `readPredecessorEndContract` raises a `CliStructuredError` directly
286
+ // for the missing-snapshot case so the operator gets a precise
287
+ // why/fix; pass it through unchanged rather than re-wrapping.
288
+ if (CliStructuredError.is(error)) {
289
+ return notOk(error);
290
+ }
224
291
  // Wrap unexpected (non-MigrationToolsError) failures from the migration
225
292
  // load phase in a structured CLI envelope. Letting them throw would
226
293
  // bypass `handleResult()` and crash the command — see CLI structured-
@@ -286,15 +353,16 @@ async function executeMigrationPlanCommand(
286
353
  // Phase 2 — load: build the aggregate against the now-consistent disk
287
354
  // state that phase 1 just seeded. The seed phase guarantees every
288
355
  // declared extension has its head ref pinned, so the loader's
289
- // declaredButUnmigrated precheck always passes here.
290
- const stack = createControlStack(config);
291
- const familyInstance = config.family.create(stack);
356
+ // declaredButUnmigrated precheck always passes here. The app contract
357
+ // was already routed through `familyInstance.deserializeContract` at the
358
+ // read site above (see TML-2536), so it's the hydrated `Contract`
359
+ // here — no second validation pass needed.
292
360
  const aggregateResult = await buildContractSpaceAggregate({
293
361
  targetId: config.target.targetId,
294
362
  migrationsDir,
295
- appContract: toContractJson,
363
+ appContract: toContract,
296
364
  extensionPacks: config.extensionPacks ?? [],
297
- validateContract: (json: unknown) => familyInstance.validateContract(json),
365
+ deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
298
366
  });
299
367
  if (!aggregateResult.ok) {
300
368
  return notOk(aggregateResult.failure);
@@ -316,8 +384,6 @@ async function executeMigrationPlanCommand(
316
384
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
317
385
  from: fromHash,
318
386
  to: toStorageHash,
319
- fromContract,
320
- toContract: toContractJson,
321
387
  hints: {
322
388
  used: [],
323
389
  applied: [],
@@ -480,7 +546,10 @@ export function createMigrationPlanCommand(): Command {
480
546
  addGlobalOptions(command)
481
547
  .option('--config <path>', 'Path to prisma-next.config.ts')
482
548
  .option('--name <slug>', 'Name slug for the migration directory', 'migration')
483
- .option('--from <hash>', 'Explicit starting contract hash (overrides latest migration target)')
549
+ .option(
550
+ '--from <contract>',
551
+ 'Starting contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
552
+ )
484
553
  .action(async (options: MigrationPlanOptions) => {
485
554
  const flags = parseGlobalFlags(options);
486
555
  const startTime = Date.now();
@@ -546,7 +615,7 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
546
615
  }
547
616
  lines.push('');
548
617
  lines.push(
549
- `Next: review the extension migrations above, then run ${green_('prisma-next migration apply')}.`,
618
+ `Next: review the extension migrations above, then run ${green_('prisma-next migrate')}.`,
550
619
  );
551
620
  }
552
621
 
@@ -617,11 +686,11 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
617
686
 
618
687
  lines.push('');
619
688
  // The "Next:" hint always points at the canonical apply path
620
- // (`prisma-next migration apply`) regardless of how many spaces
621
- // were materialised — `db update` is a dev-time convenience, not
622
- // the canonical replay step.
689
+ // (`prisma-next migrate`) regardless of how many spaces were
690
+ // materialised — `db update` is a dev-time convenience, not the
691
+ // canonical replay step.
623
692
  lines.push(
624
- `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
693
+ `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migrate')}.`,
625
694
  );
626
695
 
627
696
  if (result.preview && result.preview.statements.length > 0) {
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import {
4
+ APP_SPACE_ID,
4
5
  createControlStack,
5
6
  type MigrationPlanOperation,
6
7
  type OperationPreview,
@@ -12,6 +13,8 @@ import {
12
13
  reconstructGraph,
13
14
  } from '@prisma-next/migration-tools/migration-graph';
14
15
  import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
16
+ import { parseMigrationRef } from '@prisma-next/migration-tools/ref-resolution';
17
+ import { readRefs } from '@prisma-next/migration-tools/refs';
15
18
  import { spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
16
19
  import { ifDefined } from '@prisma-next/utils/defined';
17
20
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
@@ -26,6 +29,7 @@ import {
26
29
  errorRuntime,
27
30
  errorUnexpected,
28
31
  mapMigrationToolsError,
32
+ mapRefResolutionError,
29
33
  } from '../utils/cli-errors';
30
34
  import {
31
35
  addGlobalOptions,
@@ -33,6 +37,7 @@ import {
33
37
  resolveMigrationPaths,
34
38
  setCommandDescriptions,
35
39
  setCommandExamples,
40
+ setCommandSeeAlso,
36
41
  } from '../utils/command-helpers';
37
42
  import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
38
43
  import { formatMigrationShowOutput } from '../utils/formatters/migrations';
@@ -237,7 +242,7 @@ async function executeMigrationShowCommand(
237
242
  ui: TerminalUI,
238
243
  ): Promise<Result<MigrationShowResult, CliStructuredError>> {
239
244
  const config = await loadConfig(options.config);
240
- const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
245
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
241
246
  resolveMigrationPaths(options.config, config);
242
247
 
243
248
  const contractPathAbsolute = resolveContractPath(config);
@@ -261,7 +266,91 @@ async function executeMigrationShowCommand(
261
266
  ui.stderr(header);
262
267
  }
263
268
 
264
- // Load the app contract so the aggregate loader can validate it.
269
+ // `migration show` is an offline command; the control client is constructed
270
+ // purely to dispatch the family-specific `toOperationPreview` capability and
271
+ // is not connected to a database.
272
+ const client = createControlClient({
273
+ family: config.family,
274
+ target: config.target,
275
+ adapter: config.adapter,
276
+ ...ifDefined('driver', config.driver),
277
+ extensionPacks: config.extensionPacks ?? [],
278
+ });
279
+
280
+ // Explicit-target path. Read the app-space migrations directory directly
281
+ // and resolve `target` against the app graph. We deliberately skip
282
+ // `buildContractSpaceAggregate` here for two reasons:
283
+ //
284
+ // 1. Functional: the user asked about ONE specific migration. They don't
285
+ // need extension-space enumeration; resolving + rendering the named
286
+ // package is enough.
287
+ // 2. UX: the aggregate's layout-integrity check (PN-MIG-5001) fires when
288
+ // an extension is declared but its migrations directory hasn't been
289
+ // materialised. Gating an offline read-only inspect command on that
290
+ // check forces users to run `migrate` against a database before they
291
+ // can see what a migration contains — which contradicts what an
292
+ // offline read-only verb should require.
293
+ //
294
+ // Same pattern as `migration list`, `migration graph`, `migration check`:
295
+ // those verbs read `appMigrationsDir` directly without ever consulting
296
+ // the aggregate.
297
+ if (target) {
298
+ try {
299
+ let appPkg: OnDiskMigrationPackage;
300
+ if (looksLikePath(target)) {
301
+ const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
302
+ if (!resolved.ok) return resolved;
303
+ appPkg = await readMigrationPackage(resolved.value);
304
+ } else {
305
+ const allPackages = await readMigrationsDir(appMigrationsDir);
306
+ if (allPackages.length === 0) {
307
+ return notOk(
308
+ errorRuntime('No migrations found', {
309
+ why: `No migration packages found in ${appMigrationsRelative}`,
310
+ fix: 'Run `prisma-next migration plan` to create a migration first.',
311
+ }),
312
+ );
313
+ }
314
+ const graph = reconstructGraph(allPackages);
315
+ const refs = await readRefs(refsDir);
316
+ const migResult = parseMigrationRef(target, { graph, refs });
317
+ if (!migResult.ok) {
318
+ return notOk(mapRefResolutionError(migResult.failure));
319
+ }
320
+ const matchedPkg = allPackages.find(
321
+ (p) => p.metadata.migrationHash === migResult.value.migrationHash,
322
+ );
323
+ if (!matchedPkg) {
324
+ return notOk(
325
+ errorRuntime('Migration package not found', {
326
+ why: `Resolved migration "${migResult.value.dirName}" but the package was not loaded`,
327
+ fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
328
+ }),
329
+ );
330
+ }
331
+ appPkg = matchedPkg;
332
+ }
333
+ return ok({
334
+ ok: true,
335
+ spaces: [pkgToSpaceResult(APP_SPACE_ID, appPkg, client)],
336
+ });
337
+ } catch (error) {
338
+ if (MigrationToolsError.is(error)) {
339
+ return notOk(mapMigrationToolsError(error));
340
+ }
341
+ return notOk(
342
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
343
+ why: `Failed to read app-space migration: ${error instanceof Error ? error.message : String(error)}`,
344
+ }),
345
+ );
346
+ }
347
+ }
348
+
349
+ // No-target path. Enumerate the latest migration per space (app +
350
+ // extensions). The aggregate-loader is needed here because we need to
351
+ // know which extension spaces are declared; its layout-integrity check
352
+ // is appropriate at this entry point because the user is asking the
353
+ // system to report on every loaded space.
265
354
  let contractJsonContent: string;
266
355
  try {
267
356
  contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
@@ -281,93 +370,71 @@ async function executeMigrationShowCommand(
281
370
  );
282
371
  }
283
372
 
373
+ // Construct the family instance up-front so the on-disk app contract
374
+ // read crosses the serializer seam (`familyInstance.deserializeContract`)
375
+ // at the read site. See TML-2536.
376
+ const stack = createControlStack(config);
377
+ const familyInstance = config.family.create(stack);
378
+
284
379
  let appContract: Contract;
285
380
  try {
286
- appContract = JSON.parse(contractJsonContent) as Contract;
381
+ appContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
287
382
  } catch (error) {
288
383
  return notOk(
289
384
  errorContractValidationFailed(
290
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
385
+ `Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
291
386
  { where: { path: contractPathAbsolute } },
292
387
  ),
293
388
  );
294
389
  }
295
390
 
296
- // Build the aggregate against current disk state to enumerate all spaces.
297
- const stack = createControlStack(config);
298
- const familyInstance = config.family.create(stack);
299
391
  const aggregateResult = await buildContractSpaceAggregate({
300
392
  targetId: config.target.targetId,
301
393
  migrationsDir,
302
394
  appContract,
303
395
  extensionPacks: config.extensionPacks ?? [],
304
- validateContract: (json: unknown) => familyInstance.validateContract(json),
396
+ deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
305
397
  });
306
398
  if (!aggregateResult.ok) {
307
399
  return notOk(aggregateResult.failure);
308
400
  }
309
401
  const aggregate = aggregateResult.value;
310
402
 
311
- // `migration show` is an offline command; the control client is constructed
312
- // purely to dispatch the family-specific `toOperationPreview` capability and
313
- // is not connected to a database.
314
- const client = createControlClient({
315
- family: config.family,
316
- target: config.target,
317
- adapter: config.adapter,
318
- ...ifDefined('driver', config.driver),
319
- extensionPacks: config.extensionPacks ?? [],
320
- });
321
-
322
403
  const spaces: MigrationShowSpaceResult[] = [];
323
404
 
324
- // App space: honour the `target` argument (path or hash prefix) when provided.
405
+ // App space: latest leaf.
325
406
  try {
326
- let appPkg: OnDiskMigrationPackage;
327
- if (target && looksLikePath(target)) {
328
- const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
329
- if (!resolved.ok) return resolved;
330
- appPkg = await readMigrationPackage(resolved.value);
331
- } else {
332
- const allPackages = await readMigrationsDir(appMigrationsDir);
333
- if (allPackages.length === 0) {
334
- return notOk(
335
- errorRuntime('No migrations found', {
336
- why: `No migration packages found in ${appMigrationsRelative}`,
337
- fix: 'Run `prisma-next migration plan` to create a migration first.',
338
- }),
339
- );
340
- }
341
- if (target) {
342
- const resolved = resolveByHashPrefix(allPackages, target);
343
- if (!resolved.ok) return resolved;
344
- appPkg = resolved.value;
345
- } else {
346
- const graph = reconstructGraph(allPackages);
347
- const latestMigration = findLatestMigration(graph);
348
- if (!latestMigration) {
349
- return notOk(
350
- errorRuntime('Could not resolve latest migration', {
351
- why: 'No latest migration found in the migration history',
352
- fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
353
- }),
354
- );
355
- }
356
- const leafPkg = allPackages.find(
357
- (p) => p.metadata.migrationHash === latestMigration.migrationHash,
358
- );
359
- if (!leafPkg) {
360
- return notOk(
361
- errorRuntime('Could not resolve latest migration', {
362
- why: `Latest migration ${latestMigration.dirName} does not match any package`,
363
- fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
364
- }),
365
- );
366
- }
367
- appPkg = leafPkg;
368
- }
407
+ const allPackages = await readMigrationsDir(appMigrationsDir);
408
+ if (allPackages.length === 0) {
409
+ return notOk(
410
+ errorRuntime('No migrations found', {
411
+ why: `No migration packages found in ${appMigrationsRelative}`,
412
+ fix: 'Run `prisma-next migration plan` to create a migration first.',
413
+ }),
414
+ );
415
+ }
416
+ const graph = reconstructGraph(allPackages);
417
+ const latestMigration = findLatestMigration(graph);
418
+ if (!latestMigration) {
419
+ return notOk(
420
+ errorRuntime('Could not resolve latest migration', {
421
+ why: 'No latest migration found in the migration history',
422
+ fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
423
+ }),
424
+ );
369
425
  }
370
- spaces.push(pkgToSpaceResult(aggregate.app.spaceId, appPkg, client));
426
+ const leafPkg = allPackages.find(
427
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
428
+ );
429
+ if (!leafPkg) {
430
+ return notOk(
431
+ errorRuntime('Could not resolve latest migration', {
432
+ why: `Latest migration ${latestMigration.dirName} does not match any package`,
433
+ fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
434
+ }),
435
+ );
436
+ }
437
+ spaces.push(pkgToSpaceResult(aggregate.app.spaceId, leafPkg, client));
371
438
  } catch (error) {
372
439
  if (MigrationToolsError.is(error)) {
373
440
  return notOk(mapMigrationToolsError(error));
@@ -415,8 +482,17 @@ export function createMigrationShowCommand(): Command {
415
482
  'prisma-next migration show',
416
483
  'prisma-next migration show sha256:a1b2c3',
417
484
  ]);
485
+ setCommandSeeAlso(command, [
486
+ { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
487
+ { verb: 'migration log', oneLiner: 'Show executed migration history' },
488
+ { verb: 'migration list', oneLiner: 'List on-disk migrations' },
489
+ { verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
490
+ ]);
418
491
  addGlobalOptions(command)
419
- .argument('[target]', 'App-space migration path or migrationHash prefix (defaults to latest)')
492
+ .argument(
493
+ '[target]',
494
+ 'Migration reference: directory name, hash/prefix, or path (defaults to latest)',
495
+ )
420
496
  .option('--config <path>', 'Path to prisma-next.config.ts')
421
497
  .action(async (target: string | undefined, options: MigrationShowOptions) => {
422
498
  const flags = parseGlobalFlags(options);