@prisma-next/cli 0.5.0-dev.9 → 0.5.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 (185) 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-BCnP7cHo.mjs +1485 -0
  8. package/dist/client-BCnP7cHo.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 +64 -10
  45. package/dist/commands/migration-show.d.mts.map +1 -1
  46. package/dist/commands/migration-show.mjs +166 -60
  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-ByxhPjpW.mjs} +13 -22
  63. package/dist/contract-infer-ByxhPjpW.mjs.map +1 -0
  64. package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs +160 -0
  65. package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs.map +1 -0
  66. package/dist/db-verify-Czm5T-J4.mjs +404 -0
  67. package/dist/db-verify-Czm5T-J4.mjs.map +1 -0
  68. package/dist/exports/config-types.mjs +1 -2
  69. package/dist/exports/control-api.d.mts +101 -586
  70. package/dist/exports/control-api.d.mts.map +1 -1
  71. package/dist/exports/control-api.mjs +4 -6
  72. package/dist/exports/index.d.mts.map +1 -1
  73. package/dist/exports/index.mjs +28 -30
  74. package/dist/exports/index.mjs.map +1 -1
  75. package/dist/exports/init-output.d.mts +2 -4
  76. package/dist/exports/init-output.d.mts.map +1 -1
  77. package/dist/exports/init-output.mjs +2 -3
  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-DxdBd4Er.mjs} +10 -11
  87. package/dist/inspect-live-schema-DxdBd4Er.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-BdV8JYXV.mjs} +8 -9
  93. package/dist/migration-command-scaffold-BdV8JYXV.mjs.map +1 -0
  94. package/dist/migration-plan-mRu5K81L.mjs +494 -0
  95. package/dist/migration-plan-mRu5K81L.mjs.map +1 -0
  96. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-By9G5p2H.mjs} +270 -65
  97. package/dist/migration-status-By9G5p2H.mjs.map +1 -0
  98. package/dist/migrations-CTsyBXCA.mjs +229 -0
  99. package/dist/migrations-CTsyBXCA.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-LItU7E4l.d.mts +856 -0
  110. package/dist/types-LItU7E4l.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 +213 -75
  127. package/src/commands/migration-ref.ts +8 -7
  128. package/src/commands/migration-show.ts +274 -70
  129. package/src/commands/migration-status.ts +491 -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 +399 -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 +430 -131
  140. package/src/control-api/types.ts +278 -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 +177 -0
  148. package/src/utils/contract-space-seed-phase.ts +201 -0
  149. package/src/utils/emit-queue.ts +26 -0
  150. package/src/utils/extension-pack-inputs.ts +162 -0
  151. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  152. package/src/utils/formatters/migrations.ts +255 -77
  153. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  154. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  155. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  156. package/dist/client-CrsnY58k.mjs +0 -997
  157. package/dist/client-CrsnY58k.mjs.map +0 -1
  158. package/dist/commands/db-verify.mjs.map +0 -1
  159. package/dist/commands/migration-plan.mjs.map +0 -1
  160. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  161. package/dist/contract-emit--feXyNd7.mjs +0 -4
  162. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  163. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  164. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  165. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  166. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  167. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  168. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  169. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  170. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  171. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  172. package/dist/init-C5220SY9.mjs.map +0 -1
  173. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  174. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  175. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  176. package/dist/migrations-Bo5WtTla.mjs +0 -153
  177. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  178. package/dist/output-BpcQrnnq.mjs.map +0 -1
  179. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  180. package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
  181. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  182. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  183. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  184. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  185. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,35 +1,41 @@
1
- import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
2
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
- import { findPathWithDecision } from '@prisma-next/migration-tools/dag';
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import { errorUnknownInvariant, MigrationToolsError } from '@prisma-next/migration-tools/errors';
4
+ import type { RefEntry } from '@prisma-next/migration-tools/refs';
4
5
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
5
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
6
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
6
+ import { ifDefined } from '@prisma-next/utils/defined';
7
7
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
8
  import { Command } from 'commander';
9
9
 
10
10
  import { loadConfig } from '../config-loader';
11
11
  import { createControlClient } from '../control-api/client';
12
- import type { MigrationApplyFailure, MigrationApplyStep } from '../control-api/types';
12
+ import type {
13
+ AggregatePerSpaceExecutionEntry,
14
+ MigrationApplyFailure,
15
+ MigrationApplyPathDecision,
16
+ } from '../control-api/types';
13
17
  import {
14
18
  CliStructuredError,
15
19
  type CliStructuredError as CliStructuredErrorType,
20
+ errorContractValidationFailed,
16
21
  errorDatabaseConnectionRequired,
17
22
  errorDriverRequired,
23
+ errorFileNotFound,
18
24
  errorRuntime,
19
25
  errorTargetMigrationNotSupported,
20
26
  errorUnexpected,
27
+ mapMigrationToolsError,
21
28
  } from '../utils/cli-errors';
22
29
  import {
23
30
  addGlobalOptions,
24
- loadAllBundles,
25
- type MigrationBundleSet,
31
+ collectDeclaredInvariants,
32
+ loadMigrationPackages,
26
33
  maskConnectionUrl,
27
- readContractEnvelope,
34
+ resolveContractPath,
28
35
  resolveMigrationPaths,
29
36
  setCommandDescriptions,
30
37
  setCommandExamples,
31
38
  targetSupportsMigrations,
32
- toPathDecisionResult,
33
39
  } from '../utils/command-helpers';
34
40
  import { formatMigrationApplyCommandOutput } from '../utils/formatters/migrations';
35
41
  import { formatStyledHeader } from '../utils/formatters/styled';
@@ -44,49 +50,50 @@ interface MigrationApplyCommandOptions extends CommonCommandOptions {
44
50
  readonly ref?: string;
45
51
  }
46
52
 
53
+ /**
54
+ * Per-space breakdown of an apply run. The CLI command surfaces these
55
+ * for both the JSON shape (`appliedSpaces[]`) and the human-readable
56
+ * formatter (per-space block — same shape `db init` / `db update`
57
+ * use, M6 sub-spec § Output shape contract).
58
+ */
47
59
  export interface MigrationApplyResult {
48
60
  readonly ok: boolean;
61
+ /** Number of contract spaces that had non-zero pending operations applied. */
49
62
  readonly migrationsApplied: number;
63
+ /** Total contract spaces visible in the aggregate (pending + already-up-to-date). */
50
64
  readonly migrationsTotal: number;
65
+ /**
66
+ * Marker hash for the **app member** post-apply. Surfaced for
67
+ * back-compat with single-space callers; per-space markers live on
68
+ * `perSpace[].marker.storageHash`.
69
+ */
51
70
  readonly markerHash: string;
52
71
  readonly applied: readonly {
72
+ readonly spaceId: string;
53
73
  readonly dirName: string;
74
+ readonly migrationHash: string;
54
75
  readonly from: string;
55
76
  readonly to: string;
56
77
  readonly operationsExecuted: number;
57
78
  }[];
58
79
  readonly summary: string;
59
- readonly pathDecision?: {
60
- readonly fromHash: string;
61
- readonly toHash: string;
62
- readonly alternativeCount: number;
63
- readonly tieBreakReasons: readonly string[];
64
- readonly refName?: string;
65
- readonly selectedPath: readonly {
66
- readonly dirName: string;
67
- readonly migrationId: string;
68
- readonly from: string;
69
- readonly to: string;
70
- }[];
71
- };
80
+ /**
81
+ * Per-space breakdown in canonical schedule order (extensions
82
+ * alphabetically, then app). Always present for the aggregate-walking
83
+ * apply path.
84
+ */
85
+ readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
86
+ /**
87
+ * Path-decision data for the app member. Surfaced for back-compat
88
+ * with single-space callers (cli-journeys invariant tests).
89
+ * Absent for no-op applies where the app had nothing to do.
90
+ */
91
+ readonly pathDecision?: MigrationApplyPathDecision;
72
92
  readonly timings: {
73
93
  readonly total: number;
74
94
  };
75
95
  }
76
96
 
77
- function mapMigrationToolsError(error: unknown): CliStructuredErrorType {
78
- if (MigrationToolsError.is(error)) {
79
- return errorRuntime(error.message, {
80
- why: error.why,
81
- fix: error.fix,
82
- meta: { code: error.code, ...(error.details ?? {}) },
83
- });
84
- }
85
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
86
- why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
87
- });
88
- }
89
-
90
97
  function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
91
98
  return errorRuntime(failure.summary, {
92
99
  why: failure.why ?? 'Migration runner failed',
@@ -95,16 +102,6 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
95
102
  });
96
103
  }
97
104
 
98
- function packageToStep(pkg: MigrationBundle): MigrationApplyStep {
99
- return {
100
- dirName: pkg.dirName,
101
- from: pkg.manifest.from,
102
- to: pkg.manifest.to,
103
- toContract: pkg.manifest.toContract,
104
- operations: pkg.ops,
105
- };
106
- }
107
-
108
105
  async function executeMigrationApplyCommand(
109
106
  options: MigrationApplyCommandOptions,
110
107
  flags: GlobalFlags,
@@ -112,10 +109,8 @@ async function executeMigrationApplyCommand(
112
109
  startTime: number,
113
110
  ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
114
111
  const config = await loadConfig(options.config);
115
- const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
116
- options.config,
117
- config,
118
- );
112
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
113
+ resolveMigrationPaths(options.config, config);
119
114
 
120
115
  const dbConnection = options.db ?? config.db?.connection;
121
116
  if (!dbConnection) {
@@ -143,38 +138,49 @@ async function executeMigrationApplyCommand(
143
138
  );
144
139
  }
145
140
 
146
- let destinationHash: string;
147
- let refName: string | undefined;
141
+ let refEntry: RefEntry | undefined;
142
+ const refName = options.ref;
148
143
 
149
- if (options.ref) {
150
- refName = options.ref;
144
+ if (refName) {
151
145
  try {
152
146
  const refs = await readRefs(refsDir);
153
- destinationHash = resolveRef(refs, refName).hash;
147
+ refEntry = resolveRef(refs, refName);
154
148
  } catch (error) {
155
149
  if (MigrationToolsError.is(error)) {
156
150
  return notOk(mapMigrationToolsError(error));
157
151
  }
158
152
  throw error;
159
153
  }
160
- } else {
161
- try {
162
- const envelope = await readContractEnvelope(config);
163
- destinationHash = envelope.storageHash;
164
- } catch (error) {
154
+ }
155
+
156
+ // Resolve and parse the contract envelope. The aggregate-walking
157
+ // operation needs the validated app contract to load the aggregate.
158
+ const contractPathAbsolute = resolveContractPath(config);
159
+ let contractRaw: Contract;
160
+ try {
161
+ const contractContent = await readFile(contractPathAbsolute, 'utf-8');
162
+ contractRaw = JSON.parse(contractContent) as Contract;
163
+ } catch (error) {
164
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
165
165
  return notOk(
166
- errorRuntime('Current contract is unavailable', {
167
- why: `Failed to read contract: ${error instanceof Error ? error.message : String(error)}`,
166
+ errorFileNotFound(contractPathAbsolute, {
167
+ why: `Contract file not found at ${contractPathAbsolute}`,
168
168
  fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry apply.',
169
169
  }),
170
170
  );
171
171
  }
172
+ return notOk(
173
+ errorContractValidationFailed(
174
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
175
+ { where: { path: contractPathAbsolute } },
176
+ ),
177
+ );
172
178
  }
173
179
 
174
180
  if (!flags.json && !flags.quiet) {
175
181
  const details: Array<{ label: string; value: string }> = [
176
182
  { label: 'config', value: configPath },
177
- { label: 'migrations', value: migrationsRelative },
183
+ { label: 'migrations', value: appMigrationsRelative },
178
184
  ];
179
185
  if (typeof dbConnection === 'string') {
180
186
  details.push({
@@ -195,10 +201,11 @@ async function executeMigrationApplyCommand(
195
201
  ui.stderr(header);
196
202
  }
197
203
 
198
- // Read migrations and build migration chain model (offline no DB needed)
199
- let migrations: MigrationBundleSet;
204
+ // Load app-space migration packagesthe aggregate operation
205
+ // needs them to hydrate the app member's graph for graph-walk.
206
+ let appPackages: Awaited<ReturnType<typeof loadMigrationPackages>>;
200
207
  try {
201
- migrations = await loadAllBundles(migrationsDir);
208
+ appPackages = await loadMigrationPackages(appMigrationsDir);
202
209
  } catch (error) {
203
210
  if (MigrationToolsError.is(error)) {
204
211
  return notOk(mapMigrationToolsError(error));
@@ -206,27 +213,6 @@ async function executeMigrationApplyCommand(
206
213
  throw error;
207
214
  }
208
215
 
209
- // Defense in depth: re-hash every bundle and confirm the recorded
210
- // `migrationId` matches the on-disk `(manifest, ops)`. Catches FS
211
- // corruption, partial writes, and post-emit hand edits before we
212
- // start touching the database.
213
- for (const bundle of migrations.bundles) {
214
- const verified = verifyMigrationBundle(bundle);
215
- if (!verified.ok) {
216
- return notOk(
217
- errorRuntime(`Migration package is corrupt: ${bundle.dirName}`, {
218
- why: `Stored migrationId "${verified.storedMigrationId}" does not match the recomputed hash "${verified.computedMigrationId}" for ${migrationsRelative}/${bundle.dirName}. The migration.json or ops.json has been edited or partially written since emit.`,
219
- fix: `Re-emit the package by running \`node "${migrationsRelative}/${bundle.dirName}/migration.ts"\`, or restore the directory from version control.`,
220
- meta: {
221
- dirName: bundle.dirName,
222
- storedMigrationId: verified.storedMigrationId,
223
- computedMigrationId: verified.computedMigrationId,
224
- },
225
- }),
226
- );
227
- }
228
- }
229
-
230
216
  const client = createControlClient({
231
217
  family: config.family,
232
218
  target: config.target,
@@ -237,134 +223,45 @@ async function executeMigrationApplyCommand(
237
223
 
238
224
  try {
239
225
  await client.connect(dbConnection);
240
- const marker = await client.readMarker();
241
226
 
242
- // --- No migrations on disk ---
243
- if (migrations.bundles.length === 0) {
244
- if (marker?.storageHash) {
245
- return notOk(
246
- errorRuntime('Database has state but no migrations exist', {
247
- why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${migrationsRelative}`,
248
- fix: 'Ensure the migrations directory is correct. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
249
- meta: { markerHash: marker.storageHash, migrationsDir: migrationsRelative },
250
- }),
251
- );
252
- }
253
- // Non-empty contract + no migrations = user needs to plan first.
254
- if (destinationHash !== EMPTY_CONTRACT_HASH) {
227
+ // Pre-check unknown invariants against `(declared by app graph) ∪
228
+ // (already on the app marker)`. The marker side of the union
229
+ // catches the case where the ref carries an invariant whose
230
+ // declaring migration was retired (history rewritten) but whose
231
+ // id is recorded on the marker — surfacing UNKNOWN_INVARIANT
232
+ // there would be misleading because the database has already
233
+ // satisfied the requirement.
234
+ if (refEntry && refEntry.invariants.length > 0) {
235
+ const allMarkers = await client.readAllMarkers();
236
+ const appMarker = allMarkers.get('app') ?? null;
237
+ const declared = collectDeclaredInvariants(appPackages.graph);
238
+ const known = new Set<string>(declared);
239
+ for (const id of appMarker?.invariants ?? []) known.add(id);
240
+ const unknown = refEntry.invariants.filter((id) => !known.has(id));
241
+ if (unknown.length > 0) {
255
242
  return notOk(
256
- errorRuntime('Current contract has no planned migrations', {
257
- why: `No migrations were found in ${migrationsRelative}, but current contract hash is "${destinationHash}"`,
258
- fix: 'Run `prisma-next migration plan` to create a migration for the current contract.',
259
- meta: { destinationHash, migrationsDir: migrationsRelative },
260
- }),
243
+ mapMigrationToolsError(
244
+ errorUnknownInvariant({
245
+ ...ifDefined('refName', refName),
246
+ unknown,
247
+ declared: [...declared].sort(),
248
+ }),
249
+ ),
261
250
  );
262
251
  }
263
- // Empty contract + no migrations = nothing to do.
264
- return ok({
265
- ok: true,
266
- migrationsApplied: 0,
267
- migrationsTotal: 0,
268
- markerHash: EMPTY_CONTRACT_HASH,
269
- applied: [],
270
- summary: 'No migrations found',
271
- timings: { total: Date.now() - startTime },
272
- });
273
- }
274
-
275
- // --- Validate marker state ---
276
-
277
- // The empty sentinel should never appear in a real marker row — if it does,
278
- // the marker was corrupted and replaying all migrations would be dangerous.
279
- if (marker?.storageHash === EMPTY_CONTRACT_HASH) {
280
- return notOk(
281
- errorRuntime('Database marker contains the empty sentinel hash', {
282
- why: `The marker row exists but contains the empty sentinel value "${EMPTY_CONTRACT_HASH}". This should never happen — the marker should contain the hash of the last applied contract.`,
283
- fix: 'The marker is corrupted. Run `prisma-next db sign` to overwrite it with the correct contract hash, or drop and recreate the database.',
284
- meta: { markerHash: EMPTY_CONTRACT_HASH },
285
- }),
286
- );
287
- }
288
-
289
- const markerHash = marker?.storageHash;
290
-
291
- if (markerHash !== undefined && !migrations.graph.nodes.has(markerHash)) {
292
- return notOk(
293
- errorRuntime('Database marker does not match any known migration', {
294
- why: `The database marker hash "${markerHash}" is not found in the migration history at ${migrationsRelative}`,
295
- fix: 'Ensure the migrations directory matches this database. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
296
- meta: { markerHash, knownNodes: [...migrations.graph.nodes] },
297
- }),
298
- );
299
- }
300
-
301
- if (!migrations.graph.nodes.has(destinationHash)) {
302
- return notOk(
303
- errorRuntime('Current contract has no planned migration path', {
304
- why: `Current contract hash "${destinationHash}" is not present in the migration history at ${migrationsRelative}`,
305
- fix: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
306
- meta: { destinationHash, knownNodes: [...migrations.graph.nodes] },
307
- }),
308
- );
309
- }
310
-
311
- // --- Resolve path and apply ---
312
-
313
- // "No marker" means the database is fresh — start from the empty contract hash.
314
- const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
315
-
316
- const decision = findPathWithDecision(migrations.graph, originHash, destinationHash, refName);
317
- if (!decision) {
318
- return notOk(
319
- errorRuntime('No migration path from current state to target', {
320
- why: `Cannot find a path from "${originHash}" to target "${destinationHash}"`,
321
- fix: 'Check the migration history for gaps or inconsistencies.',
322
- meta: { markerHash: originHash, destinationHash },
323
- }),
324
- );
325
- }
326
-
327
- const pendingPath = decision.selectedPath;
328
- const pathDecision = toPathDecisionResult(decision);
329
-
330
- if (pendingPath.length === 0) {
331
- return ok({
332
- ok: true,
333
- migrationsApplied: 0,
334
- migrationsTotal: 0,
335
- markerHash: originHash,
336
- applied: [],
337
- summary: 'Already up to date',
338
- pathDecision,
339
- timings: { total: Date.now() - startTime },
340
- });
341
- }
342
-
343
- const bundleByDir = new Map(migrations.bundles.map((b) => [b.dirName, b]));
344
- const pendingMigrations: MigrationApplyStep[] = [];
345
- for (const migration of pendingPath) {
346
- const pkg = bundleByDir.get(migration.dirName);
347
- if (!pkg) {
348
- return notOk(
349
- errorRuntime(`Migration package not found: ${migration.dirName}`, {
350
- why: `The migration directory for path segment ${migration.from} → ${migration.to} was not found`,
351
- fix: 'Ensure all migration directories are present and intact.',
352
- }),
353
- );
354
- }
355
- pendingMigrations.push(packageToStep(pkg));
356
252
  }
357
253
 
358
254
  if (!flags.quiet && !flags.json) {
359
- for (const migration of pendingMigrations) {
360
- ui.step(`Pending ${migration.dirName}`);
361
- }
255
+ ui.step('Loading contract spaces…');
362
256
  }
363
257
 
364
258
  const applyResult = await client.migrationApply({
365
- originHash,
366
- destinationHash,
367
- pendingMigrations,
259
+ contract: contractRaw,
260
+ migrationsDir,
261
+ appMigrationPackages: appPackages.bundles,
262
+ ...ifDefined('refHash', refEntry?.hash),
263
+ ...(refEntry?.invariants ? { refInvariants: refEntry.invariants } : {}),
264
+ ...(refEntry !== undefined ? ifDefined('refName', refName) : {}),
368
265
  });
369
266
 
370
267
  if (!applyResult.ok) {
@@ -376,17 +273,21 @@ async function executeMigrationApplyCommand(
376
273
  return ok({
377
274
  ok: true,
378
275
  migrationsApplied: value.migrationsApplied,
379
- migrationsTotal: pendingPath.length,
276
+ migrationsTotal: value.perSpace.length,
380
277
  markerHash: value.markerHash,
381
278
  applied: value.applied,
382
279
  summary: value.summary,
383
- pathDecision,
280
+ perSpace: value.perSpace,
281
+ ...ifDefined('pathDecision', value.pathDecision),
384
282
  timings: { total: Date.now() - startTime },
385
283
  });
386
284
  } catch (error) {
387
285
  if (CliStructuredError.is(error)) {
388
286
  return notOk(error);
389
287
  }
288
+ if (MigrationToolsError.is(error)) {
289
+ return notOk(mapMigrationToolsError(error));
290
+ }
390
291
  return notOk(
391
292
  errorUnexpected(error instanceof Error ? error.message : String(error), {
392
293
  why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
@@ -402,16 +303,17 @@ export function createMigrationApplyCommand(): Command {
402
303
  setCommandDescriptions(
403
304
  command,
404
305
  'Apply planned migrations to the database',
405
- 'Applies previously planned migrations (created by `migration plan`) to a live database.\n' +
406
- 'Compares the database marker against the migration history to determine which\n' +
407
- 'migrations are pending, then executes them sequentially. Each migration runs\n' +
408
- 'in its own transaction. Does not plan new migrations run `migration plan` first.',
306
+ 'Walks every contract space (app + extensions) and applies pending\n' +
307
+ 'on-disk migrations in canonical order (extensions alphabetically,\n' +
308
+ 'then app). Graph-walks the on-disk migration graph for every space —\n' +
309
+ "no introspection, no synth. Each space's marker advances inside its\n" +
310
+ "transaction; per-space failure rolls back every space's writes.",
409
311
  );
410
312
  setCommandExamples(command, ['prisma-next migration apply --db $DATABASE_URL']);
411
313
  addGlobalOptions(command)
412
314
  .option('--db <url>', 'Database connection string')
413
315
  .option('--config <path>', 'Path to prisma-next.config.ts')
414
- .option('--ref <name>', 'Target ref name from migrations/refs/')
316
+ .option('--ref <name>', 'App-space target ref name from migrations/app/refs/')
415
317
  .action(async (options: MigrationApplyCommandOptions) => {
416
318
  const flags = parseGlobalFlags(options);
417
319
  const startTime = Date.now();