@prisma-next/cli 0.5.0-dev.9 → 0.6.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 (186) hide show
  1. package/README.md +61 -26
  2. package/dist/cli-errors-B9OBbled.d.mts +3 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  4. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-qVH-rEgd.mjs +1595 -0
  8. package/dist/client-qVH-rEgd.mjs.map +1 -0
  9. package/dist/{result-handler-Ba3zWQsI.mjs → command-helpers-BeZHkxV8.mjs} +70 -47
  10. package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -4
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -4
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +16 -13
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.d.mts.map +1 -1
  19. package/dist/commands/db-schema.mjs +6 -7
  20. package/dist/commands/db-schema.mjs.map +1 -1
  21. package/dist/commands/db-sign.d.mts.map +1 -1
  22. package/dist/commands/db-sign.mjs +9 -9
  23. package/dist/commands/db-sign.mjs.map +1 -1
  24. package/dist/commands/db-update.d.mts.map +1 -1
  25. package/dist/commands/db-update.mjs +15 -13
  26. package/dist/commands/db-update.mjs.map +1 -1
  27. package/dist/commands/db-verify.d.mts.map +1 -1
  28. package/dist/commands/db-verify.mjs +1 -321
  29. package/dist/commands/migration-apply.d.mts +28 -13
  30. package/dist/commands/migration-apply.d.mts.map +1 -1
  31. package/dist/commands/migration-apply.mjs +55 -151
  32. package/dist/commands/migration-apply.mjs.map +1 -1
  33. package/dist/commands/migration-new.d.mts +0 -1
  34. package/dist/commands/migration-new.d.mts.map +1 -1
  35. package/dist/commands/migration-new.mjs +34 -40
  36. package/dist/commands/migration-new.mjs.map +1 -1
  37. package/dist/commands/migration-plan.d.mts +33 -6
  38. package/dist/commands/migration-plan.d.mts.map +1 -1
  39. package/dist/commands/migration-plan.mjs +2 -348
  40. package/dist/commands/migration-ref.d.mts +1 -1
  41. package/dist/commands/migration-ref.d.mts.map +1 -1
  42. package/dist/commands/migration-ref.mjs +8 -12
  43. package/dist/commands/migration-ref.mjs.map +1 -1
  44. package/dist/commands/migration-show.d.mts +13 -7
  45. package/dist/commands/migration-show.d.mts.map +1 -1
  46. package/dist/commands/migration-show.mjs +35 -36
  47. package/dist/commands/migration-show.mjs.map +1 -1
  48. package/dist/commands/migration-status.d.mts +126 -5
  49. package/dist/commands/migration-status.d.mts.map +1 -1
  50. package/dist/commands/migration-status.mjs +2 -4
  51. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  52. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  53. package/dist/config-loader.d.mts +0 -1
  54. package/dist/config-loader.d.mts.map +1 -1
  55. package/dist/config-loader.mjs +2 -3
  56. package/dist/contract-emit-9DBda5Ou.mjs +150 -0
  57. package/dist/contract-emit-9DBda5Ou.mjs.map +1 -0
  58. package/dist/contract-emit-B77TsJqf.mjs +327 -0
  59. package/dist/contract-emit-B77TsJqf.mjs.map +1 -0
  60. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-Dani0mMW.mjs} +4 -6
  61. package/dist/contract-enrichment-Dani0mMW.mjs.map +1 -0
  62. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BK9YFGEG.mjs} +13 -22
  63. package/dist/contract-infer-BK9YFGEG.mjs.map +1 -0
  64. package/dist/db-verify-C0y1PCO2.mjs +404 -0
  65. package/dist/db-verify-C0y1PCO2.mjs.map +1 -0
  66. package/dist/exports/config-types.mjs +1 -2
  67. package/dist/exports/control-api.d.mts +101 -586
  68. package/dist/exports/control-api.d.mts.map +1 -1
  69. package/dist/exports/control-api.mjs +4 -6
  70. package/dist/exports/index.d.mts.map +1 -1
  71. package/dist/exports/index.mjs +28 -30
  72. package/dist/exports/index.mjs.map +1 -1
  73. package/dist/exports/init-output.d.mts +2 -4
  74. package/dist/exports/init-output.d.mts.map +1 -1
  75. package/dist/exports/init-output.mjs +2 -3
  76. package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
  77. package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
  78. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-ChqVUxR-.mjs} +3 -4
  79. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
  80. package/dist/global-flags-Icqpxk23.d.mts +12 -0
  81. package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
  82. package/dist/helpers-eqdN8tH6.mjs +25 -0
  83. package/dist/helpers-eqdN8tH6.mjs.map +1 -0
  84. package/dist/{init-C5220SY9.mjs → init-DETSgw3h.mjs} +40 -49
  85. package/dist/init-DETSgw3h.mjs.map +1 -0
  86. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-CWYxGKlb.mjs} +10 -11
  87. package/dist/inspect-live-schema-CWYxGKlb.mjs.map +1 -0
  88. package/dist/migration-cli.d.mts +41 -12
  89. package/dist/migration-cli.d.mts.map +1 -1
  90. package/dist/migration-cli.mjs +309 -86
  91. package/dist/migration-cli.mjs.map +1 -1
  92. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B5dORFEv.mjs} +8 -9
  93. package/dist/migration-command-scaffold-B5dORFEv.mjs.map +1 -0
  94. package/dist/migration-plan-C6lVaHsO.mjs +554 -0
  95. package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
  96. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CZ-D5k7k.mjs} +272 -65
  97. package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
  98. package/dist/migrations-D_UJnpuW.mjs +216 -0
  99. package/dist/migrations-D_UJnpuW.mjs.map +1 -0
  100. package/dist/{output-BpcQrnnq.mjs → output-B16Kefzx.mjs} +9 -3
  101. package/dist/output-B16Kefzx.mjs.map +1 -0
  102. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DFfvZcYL.mjs} +2 -2
  103. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
  104. package/dist/result-handler-rmPVKIP2.mjs +25 -0
  105. package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
  106. package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
  107. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-C_hFNbAn.mjs} +4 -28
  108. package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
  109. package/dist/types-D7x-IFLO.d.mts +858 -0
  110. package/dist/types-D7x-IFLO.d.mts.map +1 -0
  111. package/dist/{verify-Bkycc-Tf.mjs → verify-CiwNWM9N.mjs} +3 -4
  112. package/dist/verify-CiwNWM9N.mjs.map +1 -0
  113. package/package.json +28 -26
  114. package/src/cli.ts +32 -6
  115. package/src/commands/contract-emit.ts +67 -163
  116. package/src/commands/contract-infer.ts +7 -20
  117. package/src/commands/db-init.ts +15 -3
  118. package/src/commands/db-update.ts +9 -4
  119. package/src/commands/db-verify.ts +47 -15
  120. package/src/commands/init/index.ts +1 -1
  121. package/src/commands/init/init.ts +2 -2
  122. package/src/commands/init/templates/code-templates.ts +26 -18
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +114 -212
  125. package/src/commands/migration-new.ts +42 -45
  126. package/src/commands/migration-plan.ts +212 -72
  127. package/src/commands/migration-ref.ts +8 -7
  128. package/src/commands/migration-show.ts +60 -41
  129. package/src/commands/migration-status.ts +483 -64
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +85 -5
  132. package/src/control-api/contract-enrichment.ts +6 -4
  133. package/src/control-api/operations/apply-aggregate.ts +290 -0
  134. package/src/control-api/operations/contract-emit.ts +198 -115
  135. package/src/control-api/operations/db-apply-aggregate.ts +397 -0
  136. package/src/control-api/operations/db-init.ts +51 -253
  137. package/src/control-api/operations/db-update.ts +66 -183
  138. package/src/control-api/operations/db-verify.ts +342 -0
  139. package/src/control-api/operations/migration-apply.ts +424 -131
  140. package/src/control-api/types.ts +280 -29
  141. package/src/exports/control-api.ts +15 -3
  142. package/src/load-ts-contract.ts +28 -26
  143. package/src/migration-cli.ts +445 -122
  144. package/src/utils/cli-errors.ts +49 -2
  145. package/src/utils/combine-schema-results.ts +84 -0
  146. package/src/utils/command-helpers.ts +69 -25
  147. package/src/utils/contract-space-aggregate-loader.ts +204 -0
  148. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  149. package/src/utils/contract-space-migrate-pass.ts +156 -0
  150. package/src/utils/emit-queue.ts +26 -0
  151. package/src/utils/extension-pack-inputs.ts +170 -0
  152. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  153. package/src/utils/formatters/migrations.ts +197 -61
  154. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  155. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  156. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  157. package/dist/client-CrsnY58k.mjs +0 -997
  158. package/dist/client-CrsnY58k.mjs.map +0 -1
  159. package/dist/commands/db-verify.mjs.map +0 -1
  160. package/dist/commands/migration-plan.mjs.map +0 -1
  161. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  162. package/dist/contract-emit--feXyNd7.mjs +0 -4
  163. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  164. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  165. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  166. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  167. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  168. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  169. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  170. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  171. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  172. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  173. package/dist/init-C5220SY9.mjs.map +0 -1
  174. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  175. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  176. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  177. package/dist/migrations-Bo5WtTla.mjs +0 -153
  178. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  179. package/dist/output-BpcQrnnq.mjs.map +0 -1
  180. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  181. package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
  182. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  183. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  184. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  185. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  186. 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,27 @@ 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 { runContractSpaceExtensionMigrationsPass } from '../utils/contract-space-extension-migrations-pass';
47
+ import {
48
+ formatContractSpaceDriftWarning,
49
+ runContractSpaceMigratePass,
50
+ } from '../utils/contract-space-migrate-pass';
51
+ import {
52
+ toExtensionInputs,
53
+ toExtensionMigrationsInputs,
54
+ toMigratePassInputs,
55
+ } from '../utils/extension-pack-inputs';
42
56
  import { formatStyledHeader } from '../utils/formatters/styled';
43
57
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
44
58
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -55,15 +69,33 @@ interface MigrationPlanOptions extends CommonCommandOptions {
55
69
  export interface MigrationPlanResult {
56
70
  readonly ok: boolean;
57
71
  readonly noOp: boolean;
58
- readonly from: string;
72
+ readonly from: string | null;
59
73
  readonly to: string;
60
74
  readonly dir?: string;
75
+ /**
76
+ * Extension-space migration packages materialised onto disk during this
77
+ * `plan` run. Each entry names a `migrations/<spaceId>/<dirName>/`
78
+ * tree the framework wrote alongside the app-space migration directory.
79
+ * Empty when the project has no extension packs declaring a contract
80
+ * space, or when every extension-space package is already on disk.
81
+ *
82
+ * Surfacing these in the result (rather than only via `ui.step` log
83
+ * lines) makes the cross-space side effect explicit to JSON consumers
84
+ * and the success-summary renderer — the same multi-space side effect
85
+ * that `migration apply` will replay.
86
+ */
87
+ readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
61
88
  readonly operations: readonly {
62
89
  readonly id: string;
63
90
  readonly label: string;
64
91
  readonly operationClass: string;
65
92
  }[];
66
- readonly sql?: readonly string[];
93
+ /**
94
+ * Family-agnostic textual preview of the migration plan operations.
95
+ * Replaces the previous `sql?: readonly string[]` field; consumers should
96
+ * read `result.preview?.statements`.
97
+ */
98
+ readonly preview?: OperationPreview;
67
99
  readonly summary: string;
68
100
  /**
69
101
  * When true, `migration.ts` was written but contains unfilled
@@ -76,22 +108,6 @@ export interface MigrationPlanResult {
76
108
  };
77
109
  }
78
110
 
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
111
  async function executeMigrationPlanCommand(
96
112
  options: MigrationPlanOptions,
97
113
  flags: GlobalFlags,
@@ -99,10 +115,8 @@ async function executeMigrationPlanCommand(
99
115
  startTime: number,
100
116
  ): Promise<Result<MigrationPlanResult, CliStructuredError>> {
101
117
  const config = await loadConfig(options.config);
102
- const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
103
- options.config,
104
- config,
105
- );
118
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
119
+ resolveMigrationPaths(options.config, config);
106
120
 
107
121
  const contractPathAbsolute = resolveContractPath(config);
108
122
  const contractPath = relative(process.cwd(), contractPathAbsolute);
@@ -111,7 +125,7 @@ async function executeMigrationPlanCommand(
111
125
  const details: Array<{ label: string; value: string }> = [
112
126
  { label: 'config', value: configPath },
113
127
  { label: 'contract', value: contractPath },
114
- { label: 'migrations', value: migrationsRelative },
128
+ { label: 'migrations', value: appMigrationsRelative },
115
129
  ];
116
130
  if (options.from) {
117
131
  details.push({ label: 'from', value: options.from });
@@ -173,11 +187,11 @@ async function executeMigrationPlanCommand(
173
187
 
174
188
  // Read existing migrations and determine "from" contract
175
189
  let fromContract: Contract | null = null;
176
- let fromHash: string = EMPTY_CONTRACT_HASH;
190
+ let fromHash: string | null = null;
177
191
  let fromContractSourceDir: string | null = null;
178
192
 
179
193
  try {
180
- const { bundles, graph } = await loadAllBundles(migrationsDir);
194
+ const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
181
195
 
182
196
  if (options.from) {
183
197
  const resolved = resolveBundleByPrefix(bundles, options.from);
@@ -186,25 +200,27 @@ async function executeMigrationPlanCommand(
186
200
  return notOk(
187
201
  f.reason === 'ambiguous'
188
202
  ? errorRuntime('Multiple matching migrations found', {
189
- why: `Prefix "${options.from}" matches ${f.count} migrations in ${migrationsRelative}`,
203
+ why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
190
204
  fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
191
205
  })
192
206
  : errorRuntime('Starting contract not found', {
193
- why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
207
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
194
208
  fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
195
209
  }),
196
210
  );
197
211
  }
198
- fromHash = resolved.value.manifest.to;
199
- fromContract = resolved.value.manifest.toContract;
212
+ fromHash = resolved.value.metadata.to;
213
+ fromContract = resolved.value.metadata.toContract;
200
214
  fromContractSourceDir = resolved.value.dirPath;
201
215
  } else {
202
216
  const latestMigration = findLatestMigration(graph);
203
217
  if (latestMigration) {
204
218
  fromHash = latestMigration.to;
205
- const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
219
+ const leafPkg = bundles.find(
220
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
221
+ );
206
222
  if (leafPkg) {
207
- fromContract = leafPkg.manifest.toContract;
223
+ fromContract = leafPkg.metadata.toContract;
208
224
  fromContractSourceDir = leafPkg.dirPath;
209
225
  }
210
226
  }
@@ -213,7 +229,52 @@ async function executeMigrationPlanCommand(
213
229
  if (MigrationToolsError.is(error)) {
214
230
  return notOk(mapMigrationToolsError(error));
215
231
  }
216
- throw error;
232
+ // Wrap unexpected (non-MigrationToolsError) failures from the migration
233
+ // load phase in a structured CLI envelope. Letting them throw would
234
+ // bypass `handleResult()` and crash the command — see CLI structured-
235
+ // errors guideline (CliStructuredError + Result pattern).
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ return notOk(
238
+ errorUnexpected(message, {
239
+ why: `Unexpected error while loading migrations: ${message}`,
240
+ }),
241
+ );
242
+ }
243
+
244
+ // Per-space migrate pass: drift detection + on-disk artefact emission for
245
+ // every loaded extension that exposes a `contractSpace`. Runs *before*
246
+ // the app-space no-op check so that an extension bump alone (with no
247
+ // structural app-space change) still re-pins extension artefacts on
248
+ // disk. Drift warnings are non-fatal — the on-disk artefacts are refreshed
249
+ // and the user is notified that the bump is being captured.
250
+ // Single descriptor-import boundary: every consumer of `extensionPacks`
251
+ // goes through `toExtensionInputs` + a per-consumer adapter. AC11.
252
+ const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
253
+ const migratePass = await runContractSpaceMigratePass({
254
+ migrationsDir,
255
+ extensionPacks: toMigratePassInputs(canonicalExtensionInputs),
256
+ });
257
+ if (!flags.json && !flags.quiet) {
258
+ for (const drift of migratePass.drifts) {
259
+ if (drift.kind === 'drift') {
260
+ ui.stderr(formatContractSpaceDriftWarning(drift));
261
+ }
262
+ }
263
+ }
264
+
265
+ // Materialise descriptor-shipped migration packages onto disk under
266
+ // `migrations/<spaceId>/<dirName>/` for any package not yet present.
267
+ // Idempotent (existing dirs are left untouched).
268
+ // Uses `planAllSpaces` for deterministic ordering + duplicate-spaceId
269
+ // detection.
270
+ const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
271
+ migrationsDir,
272
+ extensionPacks: toExtensionMigrationsInputs(canonicalExtensionInputs),
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)
@@ -224,6 +285,7 @@ async function executeMigrationPlanCommand(
224
285
  from: fromHash,
225
286
  to: toStorageHash,
226
287
  operations: [],
288
+ emittedExtensionDirs: extensionMigrationsResult.emitted,
227
289
  summary: 'No changes detected between contracts',
228
290
  timings: { total: Date.now() - startTime },
229
291
  };
@@ -249,12 +311,11 @@ async function executeMigrationPlanCommand(
249
311
  const timestamp = new Date();
250
312
  const slug = options.name ?? 'migration';
251
313
  const dirName = formatMigrationDirName(timestamp, slug);
252
- const packageDir = join(migrationsDir, dirName);
314
+ const packageDir = join(appMigrationsDir, dirName);
253
315
 
254
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
316
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
255
317
  from: fromHash,
256
318
  to: toStorageHash,
257
- kind: 'regular',
258
319
  fromContract,
259
320
  toContract: toContractJson,
260
321
  hints: {
@@ -275,9 +336,9 @@ async function executeMigrationPlanCommand(
275
336
  contract: toContractJson,
276
337
  schema: fromSchema,
277
338
  policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
278
- fromHash,
279
339
  fromContract,
280
340
  frameworkComponents,
341
+ spaceId: APP_SPACE_ID,
281
342
  });
282
343
  if (plannerResult.kind === 'failure') {
283
344
  return notOk(
@@ -320,18 +381,22 @@ async function executeMigrationPlanCommand(
320
381
 
321
382
  const migrationTsContent = plannerResult.plan.renderTypeScript();
322
383
 
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;
384
+ // Always-attest: compute migrationHash over (metadata, ops). When
385
+ // placeholders blocked lowering, ops is `[]` and the hash is computed
386
+ // over the empty list — re-emitting after the user fills the placeholder
387
+ // produces a different hash (over the real ops). This is intentional;
327
388
  // there is no on-disk "draft" state.
328
389
  const opsForWrite = hasPlaceholders ? [] : plannedOps;
329
- const manifest: MigrationManifest = {
330
- ...baseManifest,
331
- migrationId: computeMigrationId(baseManifest, opsForWrite),
390
+ const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
391
+ ...baseMetadata,
392
+ providedInvariants: deriveProvidedInvariants(opsForWrite),
393
+ };
394
+ const metadata: MigrationMetadata = {
395
+ ...metadataWithInvariants,
396
+ migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
332
397
  };
333
398
 
334
- await writeMigrationPackage(packageDir, manifest, opsForWrite);
399
+ await writeMigrationPackage(packageDir, metadata, opsForWrite);
335
400
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
336
401
  await copyFilesWithRename(packageDir, [
337
402
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -356,6 +421,7 @@ async function executeMigrationPlanCommand(
356
421
  to: toStorageHash,
357
422
  dir: relative(process.cwd(), packageDir),
358
423
  operations: [],
424
+ emittedExtensionDirs: extensionMigrationsResult.emitted,
359
425
  pendingPlaceholders: true,
360
426
  summary:
361
427
  'Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
@@ -364,7 +430,9 @@ async function executeMigrationPlanCommand(
364
430
  return ok(result);
365
431
  }
366
432
 
367
- const sql = extractSqlDdl(plannedOps);
433
+ const preview = hasOperationPreview(familyInstance)
434
+ ? familyInstance.toOperationPreview(plannedOps)
435
+ : undefined;
368
436
  const result: MigrationPlanResult = {
369
437
  ok: true,
370
438
  noOp: false,
@@ -376,13 +444,25 @@ async function executeMigrationPlanCommand(
376
444
  label: op.label,
377
445
  operationClass: op.operationClass,
378
446
  })),
379
- sql,
380
- summary: `Planned ${plannedOps.length} operation(s)`,
447
+ emittedExtensionDirs: extensionMigrationsResult.emitted,
448
+ ...(preview !== undefined ? { preview } : {}),
449
+ summary: buildPlanSummary(plannedOps.length, extensionMigrationsResult.emitted.length),
381
450
  timings: { total: Date.now() - startTime },
382
451
  };
383
452
  return ok(result);
384
453
  } catch (error) {
385
- return notOk(mapMigrationToolsError(error));
454
+ if (CliStructuredError.is(error)) {
455
+ return notOk(error);
456
+ }
457
+ if (MigrationToolsError.is(error)) {
458
+ return notOk(mapMigrationToolsError(error));
459
+ }
460
+ const message = error instanceof Error ? error.message : String(error);
461
+ return notOk(
462
+ errorUnexpected(message, {
463
+ why: `Unexpected error during migration plan: ${message}`,
464
+ }),
465
+ );
386
466
  }
387
467
  }
388
468
 
@@ -424,7 +504,29 @@ export function createMigrationPlanCommand(): Command {
424
504
  return command;
425
505
  }
426
506
 
427
- function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
507
+ /**
508
+ * Compose the success-line summary so the cross-space side effect
509
+ * (extension-space migration packages materialised on disk during
510
+ * this `plan` run) is visible in the top line — not just in the
511
+ * step log above it.
512
+ *
513
+ * Example outputs:
514
+ * - `Planned 3 operation(s)` (app-space-only project)
515
+ * - `Planned 3 operation(s); materialised 1 extension-space migration` (one extension)
516
+ * - `Planned 3 operation(s); materialised 2 extension-space migrations` (two extensions)
517
+ *
518
+ * Locks AC3 at the summary-line level: a reader of the success line
519
+ * can tell that something happened beyond the app space.
520
+ */
521
+ function buildPlanSummary(plannedOpsCount: number, emittedExtensionDirsCount: number): string {
522
+ const base = `Planned ${plannedOpsCount} operation(s)`;
523
+ if (emittedExtensionDirsCount === 0) return base;
524
+ const noun =
525
+ emittedExtensionDirsCount === 1 ? 'extension-space migration' : 'extension-space migrations';
526
+ return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
527
+ }
528
+
529
+ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
428
530
  const lines: string[] = [];
429
531
  const useColor = flags.color !== false;
430
532
 
@@ -432,10 +534,29 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
432
534
  const yellow_ = useColor ? (s: string) => `\x1b[33m${s}\x1b[0m` : (s: string) => s;
433
535
  const dim_ = useColor ? (s: string) => `\x1b[2m${s}\x1b[0m` : (s: string) => s;
434
536
 
537
+ // Renders the extension-space materialisation block + canonical apply-step
538
+ // hint shared by the no-op, placeholder, and full-plan branches. The app
539
+ // space short-circuits do not skip it: an extension-only bump emits new
540
+ // `migrations/<spaceId>/<dirName>/` directories on disk that the user
541
+ // still has to apply, so the success line must surface them.
542
+ function appendEmittedExtensions(): void {
543
+ if (result.emittedExtensionDirs.length === 0) return;
544
+ lines.push('');
545
+ lines.push(dim_('Emitted extension migrations:'));
546
+ for (const entry of result.emittedExtensionDirs) {
547
+ lines.push(dim_(` ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`));
548
+ }
549
+ lines.push('');
550
+ lines.push(
551
+ `Next: review the extension migrations above, then run ${green_('prisma-next migration apply')}.`,
552
+ );
553
+ }
554
+
435
555
  if (result.noOp) {
436
556
  lines.push(`${green_('✔')} No changes detected`);
437
557
  lines.push(dim_(` from: ${result.from}`));
438
558
  lines.push(dim_(` to: ${result.to}`));
559
+ appendEmittedExtensions();
439
560
  return lines.join('\n');
440
561
  }
441
562
 
@@ -452,6 +573,7 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
452
573
  'Open migration.ts and replace each `placeholder(...)` call with your actual query.',
453
574
  );
454
575
  lines.push(`Then run: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)}`);
576
+ appendEmittedExtensions();
455
577
  return lines.join('\n');
456
578
  }
457
579
 
@@ -464,11 +586,11 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
464
586
  const op = result.operations[i]!;
465
587
  const isLast = i === result.operations.length - 1;
466
588
  const treeChar = isLast ? '└' : '├';
467
- const opClassLabel =
468
- op.operationClass === 'destructive'
469
- ? yellow_(`[${op.operationClass}]`)
470
- : dim_(`[${op.operationClass}]`);
471
- lines.push(`${dim_(treeChar)}─ ${op.label} ${opClassLabel}`);
589
+ // operationClass tag is intentionally NOT inlined per spec:
590
+ // a destructive footer warning still surfaces below this list.
591
+ const destructiveMarker =
592
+ op.operationClass === 'destructive' ? ` ${yellow_('(destructive)')}` : '';
593
+ lines.push(`${dim_(treeChar)}─ ${op.label}${destructiveMarker}`);
472
594
  }
473
595
 
474
596
  const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
@@ -484,22 +606,37 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
484
606
  lines.push(dim_(`from: ${result.from}`));
485
607
  lines.push(dim_(`to: ${result.to}`));
486
608
  if (result.dir) {
487
- lines.push(dim_(`dir: ${result.dir}`));
609
+ lines.push(dim_(`App space → ${result.dir}`));
610
+ }
611
+ // Per-space block: surface the extension-space directories materialised
612
+ // alongside the app-space migration. Without this block the cross-space
613
+ // side effect is invisible in the success summary (e2e finding F1).
614
+ for (const entry of result.emittedExtensionDirs) {
615
+ lines.push(
616
+ dim_(`Extension space ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`),
617
+ );
488
618
  }
489
619
 
490
620
  lines.push('');
621
+ // The "Next:" hint always points at the canonical apply path
622
+ // (`prisma-next migration apply`) regardless of how many spaces
623
+ // were materialised — `db update` is a dev-time convenience, not
624
+ // the canonical replay step.
491
625
  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')}.`,
626
+ `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
493
627
  );
494
628
 
495
- if (result.sql && result.sql.length > 0) {
629
+ if (result.preview && result.preview.statements.length > 0) {
630
+ // The non-empty length is already guaranteed by the surrounding check, so
631
+ // a plain `every` here is equivalent to the helper in formatters/migrations.ts.
632
+ const allSql = result.preview.statements.every((s) => s.language === 'sql');
496
633
  lines.push('');
497
- lines.push(dim_('DDL preview'));
634
+ lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
498
635
  lines.push('');
499
- for (const statement of result.sql) {
500
- const trimmed = statement.trim();
636
+ for (const statement of result.preview.statements) {
637
+ const trimmed = statement.text.trim();
501
638
  if (!trimmed) continue;
502
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
639
+ const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
503
640
  lines.push(line);
504
641
  }
505
642
  }
@@ -517,24 +654,27 @@ export type PrefixResolutionFailure =
517
654
  | { reason: 'not-found' };
518
655
 
519
656
  /**
520
- * Resolve a migration bundle by exact hash or prefix match.
657
+ * Resolve a migration package by **target contract hash** (`metadata.to`)
658
+ * using exact match or prefix match.
521
659
  *
660
+ * Note: matches `metadata.to` (the contract hash this migration produces),
661
+ * not `metadata.migrationHash` (the package's content-addressed identity).
522
662
  * 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
663
+ * the needle omits the scheme). Returns the matched package on success, or a
524
664
  * discriminated failure indicating whether the prefix was ambiguous or simply
525
665
  * not found.
526
666
  *
527
667
  * @internal Exported for testing only.
528
668
  */
529
- export function resolveBundleByPrefix<T extends { manifest: { to: string } }>(
669
+ export function resolveBundleByPrefix<T extends { metadata: { to: string } }>(
530
670
  bundles: readonly T[],
531
671
  needle: string,
532
672
  ): Result<T, PrefixResolutionFailure> {
533
- const exact = bundles.find((p) => p.manifest.to === needle);
673
+ const exact = bundles.find((p) => p.metadata.to === needle);
534
674
  if (exact) return ok(exact);
535
675
 
536
676
  const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
537
- const candidates = bundles.filter((p) => p.manifest.to.startsWith(prefixWithScheme));
677
+ const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
538
678
 
539
679
  if (candidates.length === 1) return ok(candidates[0]!);
540
680
  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
  }