@prisma-next/cli 0.5.0-dev.7 → 0.5.0-dev.71

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 (171) hide show
  1. package/README.md +56 -21
  2. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  4. package/dist/cli-errors-QH8kf-C2.d.mts +3 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-0ZX24FXF.mjs +1398 -0
  8. package/dist/client-0ZX24FXF.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +2 -4
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +2 -4
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +14 -13
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +5 -7
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -9
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +13 -13
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -321
  27. package/dist/commands/migration-apply.d.mts +5 -2
  28. package/dist/commands/migration-apply.d.mts.map +1 -1
  29. package/dist/commands/migration-apply.mjs +64 -66
  30. package/dist/commands/migration-apply.mjs.map +1 -1
  31. package/dist/commands/migration-new.d.mts +0 -1
  32. package/dist/commands/migration-new.d.mts.map +1 -1
  33. package/dist/commands/migration-new.mjs +33 -40
  34. package/dist/commands/migration-new.mjs.map +1 -1
  35. package/dist/commands/migration-plan.d.mts +14 -5
  36. package/dist/commands/migration-plan.d.mts.map +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -347
  38. package/dist/commands/migration-ref.d.mts +1 -1
  39. package/dist/commands/migration-ref.d.mts.map +1 -1
  40. package/dist/commands/migration-ref.mjs +7 -12
  41. package/dist/commands/migration-ref.mjs.map +1 -1
  42. package/dist/commands/migration-show.d.mts +13 -7
  43. package/dist/commands/migration-show.d.mts.map +1 -1
  44. package/dist/commands/migration-show.mjs +34 -36
  45. package/dist/commands/migration-show.mjs.map +1 -1
  46. package/dist/commands/migration-status.d.mts +23 -5
  47. package/dist/commands/migration-status.d.mts.map +1 -1
  48. package/dist/commands/migration-status.mjs +2 -4
  49. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  50. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  51. package/dist/config-loader.d.mts +0 -1
  52. package/dist/config-loader.d.mts.map +1 -1
  53. package/dist/config-loader.mjs +2 -3
  54. package/dist/contract-emit-B3ChISB_.mjs +338 -0
  55. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  56. package/dist/contract-emit-DkMqO7f2.mjs +148 -0
  57. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  58. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-CF6ogEJ_.mjs} +4 -6
  59. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  60. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BDKAE0B0.mjs} +12 -22
  61. package/dist/contract-infer-BDKAE0B0.mjs.map +1 -0
  62. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  63. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  64. package/dist/exports/config-types.mjs +1 -2
  65. package/dist/exports/control-api.d.mts +287 -29
  66. package/dist/exports/control-api.d.mts.map +1 -1
  67. package/dist/exports/control-api.mjs +4 -6
  68. package/dist/exports/index.d.mts.map +1 -1
  69. package/dist/exports/index.mjs +28 -30
  70. package/dist/exports/index.mjs.map +1 -1
  71. package/dist/exports/init-output.d.mts +2 -4
  72. package/dist/exports/init-output.d.mts.map +1 -1
  73. package/dist/exports/init-output.mjs +2 -3
  74. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  75. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  76. package/dist/{init-C5220SY9.mjs → init-Deo7U8_U.mjs} +26 -35
  77. package/dist/init-Deo7U8_U.mjs.map +1 -0
  78. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-BAgQMYpD.mjs} +10 -11
  79. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  80. package/dist/migration-cli.d.mts +41 -12
  81. package/dist/migration-cli.d.mts.map +1 -1
  82. package/dist/migration-cli.mjs +309 -86
  83. package/dist/migration-cli.mjs.map +1 -1
  84. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B8J702Uh.mjs} +8 -9
  85. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  86. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  87. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  88. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CjwB2of-.mjs} +117 -64
  89. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  90. package/dist/{migrations-Bo5WtTla.mjs → migrations-CIK94AJf.mjs} +43 -23
  91. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  92. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  93. package/dist/output-DnjfCC_u.mjs.map +1 -0
  94. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  95. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  96. package/dist/{result-handler-Ba3zWQsI.mjs → result-handler-DWb1rFS-.mjs} +52 -27
  97. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  98. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  99. package/dist/{terminal-ui-C3ZLwQxK.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  100. package/dist/{verify-Bkycc-Tf.mjs → verify-BEIa9638.mjs} +3 -4
  101. package/dist/verify-BEIa9638.mjs.map +1 -0
  102. package/package.json +28 -26
  103. package/src/cli.ts +32 -6
  104. package/src/commands/contract-emit.ts +67 -163
  105. package/src/commands/contract-infer.ts +7 -20
  106. package/src/commands/db-init.ts +14 -3
  107. package/src/commands/db-update.ts +8 -4
  108. package/src/commands/db-verify.ts +47 -15
  109. package/src/commands/init/index.ts +1 -1
  110. package/src/commands/init/init.ts +2 -2
  111. package/src/commands/init/templates/code-templates.ts +12 -4
  112. package/src/commands/inspect-live-schema.ts +10 -5
  113. package/src/commands/migration-apply.ts +92 -71
  114. package/src/commands/migration-new.ts +42 -45
  115. package/src/commands/migration-plan.ts +147 -64
  116. package/src/commands/migration-ref.ts +8 -7
  117. package/src/commands/migration-show.ts +60 -41
  118. package/src/commands/migration-status.ts +196 -60
  119. package/src/config-path-validation.ts +0 -1
  120. package/src/control-api/client.ts +69 -1
  121. package/src/control-api/contract-enrichment.ts +6 -4
  122. package/src/control-api/operations/contract-emit.ts +198 -115
  123. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  124. package/src/control-api/operations/db-init.ts +51 -253
  125. package/src/control-api/operations/db-update.ts +66 -183
  126. package/src/control-api/operations/db-verify.ts +342 -0
  127. package/src/control-api/operations/migration-apply.ts +37 -9
  128. package/src/control-api/types.ts +125 -7
  129. package/src/exports/control-api.ts +15 -3
  130. package/src/load-ts-contract.ts +28 -26
  131. package/src/migration-cli.ts +445 -122
  132. package/src/utils/cli-errors.ts +49 -2
  133. package/src/utils/combine-schema-results.ts +84 -0
  134. package/src/utils/command-helpers.ts +69 -25
  135. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  136. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  137. package/src/utils/contract-space-migrate-pass.ts +156 -0
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  143. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  144. package/dist/client-CrsnY58k.mjs +0 -997
  145. package/dist/client-CrsnY58k.mjs.map +0 -1
  146. package/dist/commands/db-verify.mjs.map +0 -1
  147. package/dist/commands/migration-plan.mjs.map +0 -1
  148. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  149. package/dist/contract-emit--feXyNd7.mjs +0 -4
  150. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  151. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  152. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  153. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  154. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  155. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  157. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  159. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  160. package/dist/init-C5220SY9.mjs.map +0 -1
  161. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  162. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  163. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  164. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  165. package/dist/output-BpcQrnnq.mjs.map +0 -1
  166. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  167. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  168. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  169. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  170. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  171. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -2,24 +2,27 @@ 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
+ APP_SPACE_ID,
5
6
  createControlStack,
7
+ hasOperationPreview,
6
8
  type MigrationPlanOperation,
9
+ type OperationPreview,
7
10
  } from '@prisma-next/framework-components/control';
8
- import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
9
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
10
- import { findLatestMigration } from '@prisma-next/migration-tools/dag';
11
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
12
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
13
+ import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
11
14
  import {
12
15
  copyFilesWithRename,
13
16
  formatMigrationDirName,
14
17
  writeMigrationPackage,
15
18
  } from '@prisma-next/migration-tools/io';
19
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
20
+ import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
16
21
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
17
- import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
18
22
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
19
23
  import { Command } from 'commander';
20
24
  import { join, relative } from 'pathe';
21
25
  import { loadConfig } from '../config-loader';
22
- import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
23
26
  import {
24
27
  type CliErrorConflict,
25
28
  CliStructuredError,
@@ -29,16 +32,26 @@ import {
29
32
  errorRuntime,
30
33
  errorTargetMigrationNotSupported,
31
34
  errorUnexpected,
35
+ mapMigrationToolsError,
32
36
  } from '../utils/cli-errors';
33
37
  import {
34
38
  addGlobalOptions,
35
39
  getTargetMigrations,
36
- loadAllBundles,
40
+ loadMigrationPackages,
37
41
  resolveContractPath,
38
42
  resolveMigrationPaths,
39
43
  setCommandDescriptions,
40
44
  setCommandExamples,
41
45
  } from '../utils/command-helpers';
46
+ import {
47
+ type ExtensionMigrationsExtensionInput,
48
+ runContractSpaceExtensionMigrationsPass,
49
+ } from '../utils/contract-space-extension-migrations-pass';
50
+ import {
51
+ formatContractSpaceDriftWarning,
52
+ type MigrateExtensionInput,
53
+ runContractSpaceMigratePass,
54
+ } from '../utils/contract-space-migrate-pass';
42
55
  import { formatStyledHeader } from '../utils/formatters/styled';
43
56
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
44
57
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -55,7 +68,7 @@ interface MigrationPlanOptions extends CommonCommandOptions {
55
68
  export interface MigrationPlanResult {
56
69
  readonly ok: boolean;
57
70
  readonly noOp: boolean;
58
- readonly from: string;
71
+ readonly from: string | null;
59
72
  readonly to: string;
60
73
  readonly dir?: string;
61
74
  readonly operations: readonly {
@@ -63,7 +76,12 @@ export interface MigrationPlanResult {
63
76
  readonly label: string;
64
77
  readonly operationClass: string;
65
78
  }[];
66
- readonly sql?: readonly string[];
79
+ /**
80
+ * Family-agnostic textual preview of the migration plan operations.
81
+ * Replaces the previous `sql?: readonly string[]` field; consumers should
82
+ * read `result.preview?.statements`.
83
+ */
84
+ readonly preview?: OperationPreview;
67
85
  readonly summary: string;
68
86
  /**
69
87
  * When true, `migration.ts` was written but contains unfilled
@@ -76,22 +94,6 @@ export interface MigrationPlanResult {
76
94
  };
77
95
  }
78
96
 
79
- function mapMigrationToolsError(error: unknown): CliStructuredError {
80
- if (CliStructuredError.is(error)) {
81
- return error;
82
- }
83
- if (MigrationToolsError.is(error)) {
84
- return errorRuntime(error.message, {
85
- why: error.why,
86
- fix: error.fix,
87
- meta: { code: error.code, ...(error.details ?? {}) },
88
- });
89
- }
90
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
91
- why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
92
- });
93
- }
94
-
95
97
  async function executeMigrationPlanCommand(
96
98
  options: MigrationPlanOptions,
97
99
  flags: GlobalFlags,
@@ -99,10 +101,8 @@ async function executeMigrationPlanCommand(
99
101
  startTime: number,
100
102
  ): Promise<Result<MigrationPlanResult, CliStructuredError>> {
101
103
  const config = await loadConfig(options.config);
102
- const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
103
- options.config,
104
- config,
105
- );
104
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
105
+ resolveMigrationPaths(options.config, config);
106
106
 
107
107
  const contractPathAbsolute = resolveContractPath(config);
108
108
  const contractPath = relative(process.cwd(), contractPathAbsolute);
@@ -111,7 +111,7 @@ async function executeMigrationPlanCommand(
111
111
  const details: Array<{ label: string; value: string }> = [
112
112
  { label: 'config', value: configPath },
113
113
  { label: 'contract', value: contractPath },
114
- { label: 'migrations', value: migrationsRelative },
114
+ { label: 'migrations', value: appMigrationsRelative },
115
115
  ];
116
116
  if (options.from) {
117
117
  details.push({ label: 'from', value: options.from });
@@ -173,11 +173,11 @@ async function executeMigrationPlanCommand(
173
173
 
174
174
  // Read existing migrations and determine "from" contract
175
175
  let fromContract: Contract | null = null;
176
- let fromHash: string = EMPTY_CONTRACT_HASH;
176
+ let fromHash: string | null = null;
177
177
  let fromContractSourceDir: string | null = null;
178
178
 
179
179
  try {
180
- const { bundles, graph } = await loadAllBundles(migrationsDir);
180
+ const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
181
181
 
182
182
  if (options.from) {
183
183
  const resolved = resolveBundleByPrefix(bundles, options.from);
@@ -186,25 +186,27 @@ async function executeMigrationPlanCommand(
186
186
  return notOk(
187
187
  f.reason === 'ambiguous'
188
188
  ? errorRuntime('Multiple matching migrations found', {
189
- why: `Prefix "${options.from}" matches ${f.count} migrations in ${migrationsRelative}`,
189
+ why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
190
190
  fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
191
191
  })
192
192
  : errorRuntime('Starting contract not found', {
193
- why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
193
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
194
194
  fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
195
195
  }),
196
196
  );
197
197
  }
198
- fromHash = resolved.value.manifest.to;
199
- fromContract = resolved.value.manifest.toContract;
198
+ fromHash = resolved.value.metadata.to;
199
+ fromContract = resolved.value.metadata.toContract;
200
200
  fromContractSourceDir = resolved.value.dirPath;
201
201
  } else {
202
202
  const latestMigration = findLatestMigration(graph);
203
203
  if (latestMigration) {
204
204
  fromHash = latestMigration.to;
205
- const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
205
+ const leafPkg = bundles.find(
206
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
207
+ );
206
208
  if (leafPkg) {
207
- fromContract = leafPkg.manifest.toContract;
209
+ fromContract = leafPkg.metadata.toContract;
208
210
  fromContractSourceDir = leafPkg.dirPath;
209
211
  }
210
212
  }
@@ -213,7 +215,66 @@ async function executeMigrationPlanCommand(
213
215
  if (MigrationToolsError.is(error)) {
214
216
  return notOk(mapMigrationToolsError(error));
215
217
  }
216
- throw error;
218
+ // Wrap unexpected (non-MigrationToolsError) failures from the migration
219
+ // load phase in a structured CLI envelope. Letting them throw would
220
+ // bypass `handleResult()` and crash the command — see CLI structured-
221
+ // errors guideline (CliStructuredError + Result pattern).
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ return notOk(
224
+ errorUnexpected(message, {
225
+ why: `Unexpected error while loading migrations: ${message}`,
226
+ }),
227
+ );
228
+ }
229
+
230
+ // Per-space migrate pass: drift detection + on-disk artefact emission for
231
+ // every loaded extension that exposes a `contractSpace`. Runs *before*
232
+ // the app-space no-op check so that an extension bump alone (with no
233
+ // structural app-space change) still re-pins extension artefacts on
234
+ // disk. Drift warnings are non-fatal — the on-disk artefacts are refreshed
235
+ // and the user is notified that the bump is being captured.
236
+ const extensionInputs: readonly MigrateExtensionInput[] = (config.extensionPacks ?? []).map(
237
+ (pack) => {
238
+ const cs = (pack as { readonly contractSpace?: MigrateExtensionInput['contractSpace'] })
239
+ .contractSpace;
240
+ return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
241
+ },
242
+ );
243
+ const migratePass = await runContractSpaceMigratePass({
244
+ migrationsDir,
245
+ extensionPacks: extensionInputs,
246
+ });
247
+ if (!flags.json && !flags.quiet) {
248
+ for (const drift of migratePass.drifts) {
249
+ if (drift.kind === 'drift') {
250
+ ui.stderr(formatContractSpaceDriftWarning(drift));
251
+ }
252
+ }
253
+ }
254
+
255
+ // Materialise descriptor-shipped migration packages onto disk under
256
+ // `migrations/<spaceId>/<dirName>/` for any package not yet present.
257
+ // Idempotent (existing dirs are left untouched).
258
+ // Uses `planAllSpaces` for deterministic ordering + duplicate-spaceId
259
+ // detection.
260
+ const extensionMigrationsInputs: readonly ExtensionMigrationsExtensionInput[] = (
261
+ config.extensionPacks ?? []
262
+ ).map((pack) => {
263
+ const cs = (
264
+ pack as {
265
+ readonly contractSpace?: ExtensionMigrationsExtensionInput['contractSpace'];
266
+ }
267
+ ).contractSpace;
268
+ return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
269
+ });
270
+ const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
271
+ migrationsDir,
272
+ extensionPacks: extensionMigrationsInputs,
273
+ });
274
+ if (!flags.json && !flags.quiet) {
275
+ for (const entry of extensionMigrationsResult.emitted) {
276
+ ui.step(`Emitted ${entry.spaceId}/${entry.dirName}`);
277
+ }
217
278
  }
218
279
 
219
280
  // Check for no-op (same hash means no changes)
@@ -249,12 +310,11 @@ async function executeMigrationPlanCommand(
249
310
  const timestamp = new Date();
250
311
  const slug = options.name ?? 'migration';
251
312
  const dirName = formatMigrationDirName(timestamp, slug);
252
- const packageDir = join(migrationsDir, dirName);
313
+ const packageDir = join(appMigrationsDir, dirName);
253
314
 
254
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
315
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
255
316
  from: fromHash,
256
317
  to: toStorageHash,
257
- kind: 'regular',
258
318
  fromContract,
259
319
  toContract: toContractJson,
260
320
  hints: {
@@ -275,9 +335,9 @@ async function executeMigrationPlanCommand(
275
335
  contract: toContractJson,
276
336
  schema: fromSchema,
277
337
  policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
278
- fromHash,
279
338
  fromContract,
280
339
  frameworkComponents,
340
+ spaceId: APP_SPACE_ID,
281
341
  });
282
342
  if (plannerResult.kind === 'failure') {
283
343
  return notOk(
@@ -320,18 +380,22 @@ async function executeMigrationPlanCommand(
320
380
 
321
381
  const migrationTsContent = plannerResult.plan.renderTypeScript();
322
382
 
323
- // Always-attest: compute migrationId over (manifest, ops). When
324
- // placeholders blocked lowering, ops is `[]` and the id hashes over
325
- // the empty list — re-emitting after the user fills the placeholder
326
- // produces a different id (over the real ops). This is intentional;
383
+ // Always-attest: compute migrationHash over (metadata, ops). When
384
+ // placeholders blocked lowering, ops is `[]` and the hash is computed
385
+ // over the empty list — re-emitting after the user fills the placeholder
386
+ // produces a different hash (over the real ops). This is intentional;
327
387
  // there is no on-disk "draft" state.
328
388
  const opsForWrite = hasPlaceholders ? [] : plannedOps;
329
- const manifest: MigrationManifest = {
330
- ...baseManifest,
331
- migrationId: computeMigrationId(baseManifest, opsForWrite),
389
+ const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
390
+ ...baseMetadata,
391
+ providedInvariants: deriveProvidedInvariants(opsForWrite),
392
+ };
393
+ const metadata: MigrationMetadata = {
394
+ ...metadataWithInvariants,
395
+ migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
332
396
  };
333
397
 
334
- await writeMigrationPackage(packageDir, manifest, opsForWrite);
398
+ await writeMigrationPackage(packageDir, metadata, opsForWrite);
335
399
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
336
400
  await copyFilesWithRename(packageDir, [
337
401
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -364,7 +428,9 @@ async function executeMigrationPlanCommand(
364
428
  return ok(result);
365
429
  }
366
430
 
367
- const sql = extractSqlDdl(plannedOps);
431
+ const preview = hasOperationPreview(familyInstance)
432
+ ? familyInstance.toOperationPreview(plannedOps)
433
+ : undefined;
368
434
  const result: MigrationPlanResult = {
369
435
  ok: true,
370
436
  noOp: false,
@@ -376,13 +442,24 @@ async function executeMigrationPlanCommand(
376
442
  label: op.label,
377
443
  operationClass: op.operationClass,
378
444
  })),
379
- sql,
445
+ ...(preview !== undefined ? { preview } : {}),
380
446
  summary: `Planned ${plannedOps.length} operation(s)`,
381
447
  timings: { total: Date.now() - startTime },
382
448
  };
383
449
  return ok(result);
384
450
  } catch (error) {
385
- return notOk(mapMigrationToolsError(error));
451
+ if (CliStructuredError.is(error)) {
452
+ return notOk(error);
453
+ }
454
+ if (MigrationToolsError.is(error)) {
455
+ return notOk(mapMigrationToolsError(error));
456
+ }
457
+ const message = error instanceof Error ? error.message : String(error);
458
+ return notOk(
459
+ errorUnexpected(message, {
460
+ why: `Unexpected error during migration plan: ${message}`,
461
+ }),
462
+ );
386
463
  }
387
464
  }
388
465
 
@@ -489,17 +566,20 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
489
566
 
490
567
  lines.push('');
491
568
  lines.push(
492
- `Next: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)} to emit ops.json and attest migrationId before running ${green_('prisma-next migration apply')}.`,
569
+ `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
493
570
  );
494
571
 
495
- if (result.sql && result.sql.length > 0) {
572
+ if (result.preview && result.preview.statements.length > 0) {
573
+ // The non-empty length is already guaranteed by the surrounding check, so
574
+ // a plain `every` here is equivalent to the helper in formatters/migrations.ts.
575
+ const allSql = result.preview.statements.every((s) => s.language === 'sql');
496
576
  lines.push('');
497
- lines.push(dim_('DDL preview'));
577
+ lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
498
578
  lines.push('');
499
- for (const statement of result.sql) {
500
- const trimmed = statement.trim();
579
+ for (const statement of result.preview.statements) {
580
+ const trimmed = statement.text.trim();
501
581
  if (!trimmed) continue;
502
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
582
+ const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
503
583
  lines.push(line);
504
584
  }
505
585
  }
@@ -517,24 +597,27 @@ export type PrefixResolutionFailure =
517
597
  | { reason: 'not-found' };
518
598
 
519
599
  /**
520
- * Resolve a migration bundle by exact hash or prefix match.
600
+ * Resolve a migration package by **target contract hash** (`metadata.to`)
601
+ * using exact match or prefix match.
521
602
  *
603
+ * Note: matches `metadata.to` (the contract hash this migration produces),
604
+ * not `metadata.migrationHash` (the package's content-addressed identity).
522
605
  * Tries exact match first, then prefix match (auto-prepending `sha256:` when
523
- * the needle omits the scheme). Returns the matched bundle on success, or a
606
+ * the needle omits the scheme). Returns the matched package on success, or a
524
607
  * discriminated failure indicating whether the prefix was ambiguous or simply
525
608
  * not found.
526
609
  *
527
610
  * @internal Exported for testing only.
528
611
  */
529
- export function resolveBundleByPrefix<T extends { manifest: { to: string } }>(
612
+ export function resolveBundleByPrefix<T extends { metadata: { to: string } }>(
530
613
  bundles: readonly T[],
531
614
  needle: string,
532
615
  ): Result<T, PrefixResolutionFailure> {
533
- const exact = bundles.find((p) => p.manifest.to === needle);
616
+ const exact = bundles.find((p) => p.metadata.to === needle);
534
617
  if (exact) return ok(exact);
535
618
 
536
619
  const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
537
- const candidates = bundles.filter((p) => p.manifest.to.startsWith(prefixWithScheme));
620
+ const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
538
621
 
539
622
  if (candidates.length === 1) return ok(candidates[0]!);
540
623
  if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });
@@ -1,3 +1,4 @@
1
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
1
2
  import type { RefEntry } from '@prisma-next/migration-tools/refs';
2
3
  import {
3
4
  deleteRef,
@@ -7,11 +8,15 @@ import {
7
8
  validateRefValue,
8
9
  writeRef,
9
10
  } from '@prisma-next/migration-tools/refs';
10
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
11
11
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
12
  import { Command } from 'commander';
13
13
  import { loadConfig } from '../config-loader';
14
- import { CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
14
+ import {
15
+ CliStructuredError,
16
+ errorRuntime,
17
+ errorUnexpected,
18
+ mapMigrationToolsError,
19
+ } from '../utils/cli-errors';
15
20
  import {
16
21
  addGlobalOptions,
17
22
  resolveMigrationPaths,
@@ -49,11 +54,7 @@ interface RefListResult {
49
54
 
50
55
  function mapError(error: unknown): CliStructuredError {
51
56
  if (MigrationToolsError.is(error)) {
52
- return errorRuntime(error.message, {
53
- why: error.why,
54
- fix: error.fix,
55
- meta: { code: error.code },
56
- });
57
+ return mapMigrationToolsError(error);
57
58
  }
58
59
  return errorUnexpected(error instanceof Error ? error.message : String(error));
59
60
  }
@@ -1,14 +1,26 @@
1
- import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
- import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
1
+ import type {
2
+ MigrationPlanOperation,
3
+ OperationPreview,
4
+ } from '@prisma-next/framework-components/control';
5
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
6
  import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
4
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
5
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
7
+ import {
8
+ findLatestMigration,
9
+ reconstructGraph,
10
+ } from '@prisma-next/migration-tools/migration-graph';
11
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
12
+ import { APP_SPACE_ID, spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
6
13
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
7
14
  import { Command } from 'commander';
8
15
  import { relative, resolve } from 'pathe';
9
16
  import { loadConfig } from '../config-loader';
10
- import { extractOperationStatements } from '../control-api/operations/extract-operation-statements';
11
- import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
17
+ import { createControlClient } from '../control-api/client';
18
+ import {
19
+ type CliStructuredError,
20
+ errorRuntime,
21
+ errorUnexpected,
22
+ mapMigrationToolsError,
23
+ } from '../utils/cli-errors';
12
24
  import {
13
25
  addGlobalOptions,
14
26
  setCommandDescriptions,
@@ -29,17 +41,22 @@ export interface MigrationShowResult {
29
41
  readonly ok: true;
30
42
  readonly dirName: string;
31
43
  readonly dirPath: string;
32
- readonly from: string;
44
+ readonly from: string | null;
33
45
  readonly to: string;
34
- readonly migrationId: string;
35
- readonly kind: string;
46
+ readonly migrationHash: string;
36
47
  readonly createdAt: string;
37
48
  readonly operations: readonly {
38
49
  readonly id: string;
39
50
  readonly label: string;
40
51
  readonly operationClass: string;
41
52
  }[];
42
- readonly sql: readonly string[];
53
+ /**
54
+ * Family-agnostic textual preview of the migration's operations. Replaces
55
+ * the previous string-array DDL field. Always defined; statements is empty
56
+ * for a no-op migration or a family that does not implement the
57
+ * `OperationPreviewCapable` capability.
58
+ */
59
+ readonly preview: OperationPreview;
43
60
  readonly summary: string;
44
61
  }
45
62
 
@@ -48,11 +65,11 @@ function looksLikePath(target: string): boolean {
48
65
  }
49
66
 
50
67
  export function resolveByHashPrefix(
51
- packages: readonly MigrationBundle[],
68
+ packages: readonly OnDiskMigrationPackage[],
52
69
  prefix: string,
53
- ): Result<MigrationBundle, CliStructuredError> {
70
+ ): Result<OnDiskMigrationPackage, CliStructuredError> {
54
71
  const normalizedPrefix = prefix.startsWith('sha256:') ? prefix : `sha256:${prefix}`;
55
- const matches = packages.filter((p) => p.manifest.migrationId.startsWith(normalizedPrefix));
72
+ const matches = packages.filter((p) => p.metadata.migrationHash.startsWith(normalizedPrefix));
56
73
 
57
74
  if (matches.length === 1) {
58
75
  return ok(matches[0]!);
@@ -61,13 +78,13 @@ export function resolveByHashPrefix(
61
78
  if (matches.length === 0) {
62
79
  return notOk(
63
80
  errorRuntime('No migration found matching prefix', {
64
- why: `No migration has a migrationId starting with "${normalizedPrefix}"`,
81
+ why: `No migration has a migrationHash starting with "${normalizedPrefix}"`,
65
82
  fix: 'Run `prisma-next migration show` (no argument) to see the latest migration, or check the migrations directory for available packages.',
66
83
  }),
67
84
  );
68
85
  }
69
86
 
70
- const candidates = matches.map((p) => ` ${p.dirName} ${p.manifest.migrationId}`).join('\n');
87
+ const candidates = matches.map((p) => ` ${p.dirName} ${p.metadata.migrationHash}`).join('\n');
71
88
  return notOk(
72
89
  errorRuntime('Ambiguous hash prefix', {
73
90
  why: `Multiple migrations match prefix "${normalizedPrefix}":\n${candidates}`,
@@ -87,16 +104,17 @@ async function executeMigrationShowCommand(
87
104
  ? relative(process.cwd(), resolve(options.config))
88
105
  : 'prisma-next.config.ts';
89
106
 
90
- const migrationsDir = resolve(
107
+ const migrationsDirRoot = resolve(
91
108
  options.config ? resolve(options.config, '..') : process.cwd(),
92
109
  config.migrations?.dir ?? 'migrations',
93
110
  );
94
- const migrationsRelative = relative(process.cwd(), migrationsDir);
111
+ const appMigrationsDir = spaceMigrationDirectory(migrationsDirRoot, APP_SPACE_ID);
112
+ const appMigrationsRelative = relative(process.cwd(), appMigrationsDir);
95
113
 
96
114
  if (!flags.json && !flags.quiet) {
97
115
  const details: Array<{ label: string; value: string }> = [
98
116
  { label: 'config', value: configPath },
99
- { label: 'migrations', value: migrationsRelative },
117
+ { label: 'migrations', value: appMigrationsRelative },
100
118
  ];
101
119
  if (target) {
102
120
  details.push({ label: 'target', value: target });
@@ -110,17 +128,17 @@ async function executeMigrationShowCommand(
110
128
  ui.stderr(header);
111
129
  }
112
130
 
113
- let pkg: MigrationBundle;
131
+ let pkg: OnDiskMigrationPackage;
114
132
 
115
133
  try {
116
134
  if (target && looksLikePath(target)) {
117
135
  pkg = await readMigrationPackage(resolve(target));
118
136
  } else {
119
- const allPackages = await readMigrationsDir(migrationsDir);
137
+ const allPackages = await readMigrationsDir(appMigrationsDir);
120
138
  if (allPackages.length === 0) {
121
139
  return notOk(
122
140
  errorRuntime('No migrations found', {
123
- why: `No migration packages found in ${migrationsRelative}`,
141
+ why: `No migration packages found in ${appMigrationsRelative}`,
124
142
  fix: 'Run `prisma-next migration plan` to create a migration first.',
125
143
  }),
126
144
  );
@@ -142,7 +160,7 @@ async function executeMigrationShowCommand(
142
160
  );
143
161
  }
144
162
  const leafPkg = allPackages.find(
145
- (p) => p.manifest.migrationId === latestMigration.migrationId,
163
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
146
164
  );
147
165
  if (!leafPkg) {
148
166
  return notOk(
@@ -157,13 +175,7 @@ async function executeMigrationShowCommand(
157
175
  }
158
176
  } catch (error) {
159
177
  if (MigrationToolsError.is(error)) {
160
- return notOk(
161
- errorRuntime(error.message, {
162
- why: error.why,
163
- fix: error.fix,
164
- meta: { code: error.code, ...(error.details ?? {}) },
165
- }),
166
- );
178
+ return notOk(mapMigrationToolsError(error));
167
179
  }
168
180
  return notOk(
169
181
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -173,23 +185,33 @@ async function executeMigrationShowCommand(
173
185
  }
174
186
 
175
187
  const ops = pkg.ops as readonly MigrationPlanOperation[];
176
- const sql = extractOperationStatements(config.family.familyId, ops) ?? [];
188
+
189
+ // `migration show` is an offline command; the control client is constructed
190
+ // purely to dispatch the family-specific `toOperationPreview` capability and
191
+ // is not connected to a database.
192
+ const client = createControlClient({
193
+ family: config.family,
194
+ target: config.target,
195
+ adapter: config.adapter,
196
+ ...(config.driver ? { driver: config.driver } : {}),
197
+ extensionPacks: config.extensionPacks ?? [],
198
+ });
199
+ const preview: OperationPreview = client.toOperationPreview(ops) ?? { statements: [] };
177
200
 
178
201
  const result: MigrationShowResult = {
179
202
  ok: true,
180
203
  dirName: pkg.dirName,
181
204
  dirPath: relative(process.cwd(), pkg.dirPath),
182
- from: pkg.manifest.from,
183
- to: pkg.manifest.to,
184
- migrationId: pkg.manifest.migrationId,
185
- kind: pkg.manifest.kind,
186
- createdAt: pkg.manifest.createdAt,
205
+ from: pkg.metadata.from,
206
+ to: pkg.metadata.to,
207
+ migrationHash: pkg.metadata.migrationHash,
208
+ createdAt: pkg.metadata.createdAt,
187
209
  operations: ops.map((op) => ({
188
210
  id: op.id,
189
211
  label: op.label,
190
212
  operationClass: op.operationClass,
191
213
  })),
192
- sql,
214
+ preview,
193
215
  summary: `${ops.length} operation(s)`,
194
216
  };
195
217
  return ok(result);
@@ -200,7 +222,7 @@ export function createMigrationShowCommand(): Command {
200
222
  setCommandDescriptions(
201
223
  command,
202
224
  'Display migration package contents',
203
- 'Shows the operations, DDL preview, and metadata for a migration package.\n' +
225
+ 'Shows the operations, statement preview, and metadata for a migration package.\n' +
204
226
  'Accepts a directory path, a hash prefix (git-style), or defaults to the\n' +
205
227
  'latest migration.',
206
228
  );
@@ -209,10 +231,7 @@ export function createMigrationShowCommand(): Command {
209
231
  'prisma-next migration show sha256:a1b2c3',
210
232
  ]);
211
233
  addGlobalOptions(command)
212
- .argument(
213
- '[target]',
214
- 'Migration directory path or migrationId hash prefix (defaults to latest)',
215
- )
234
+ .argument('[target]', 'Migration directory path or migrationHash prefix (defaults to latest)')
216
235
  .option('--config <path>', 'Path to prisma-next.config.ts')
217
236
  .action(async (target: string | undefined, options: MigrationShowOptions) => {
218
237
  const flags = parseGlobalFlags(options);