@prisma-next/cli 0.5.0-dev.9 → 0.5.0

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 +60 -25
  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
@@ -1,8 +1,42 @@
1
- import { green, yellow } from 'colorette';
1
+ import type { OperationPreview } from '@prisma-next/framework-components/control';
2
+ import { cyan, green, yellow } from 'colorette';
2
3
 
4
+ import type { AggregatePerSpaceExecutionEntry } from '../../control-api/types';
3
5
  import type { GlobalFlags } from '../global-flags';
4
6
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
5
7
 
8
+ /**
9
+ * Render a single statement of an `OperationPreview` for the human-readable
10
+ * preview block. SQL statements get a trailing `;` if missing — matches the
11
+ * legacy `string[]`-based renderer byte-for-byte (per spec OQ-4). Other
12
+ * languages (`'mongodb-shell'`) render verbatim.
13
+ */
14
+ function renderPreviewStatement(text: string, language: string): string | undefined {
15
+ const trimmed = text.trim();
16
+ if (!trimmed) return undefined;
17
+ if (language === 'sql') {
18
+ return trimmed.endsWith(';') ? trimmed : `${trimmed};`;
19
+ }
20
+ return trimmed;
21
+ }
22
+
23
+ /**
24
+ * Choose the header label for a preview block. SQL-only previews keep the
25
+ * legacy `DDL preview` label (preserves CLI byte-identity for SQL targets per
26
+ * spec OQ-4); previews from any other family — or a mix that includes any
27
+ * non-SQL language — use the family-agnostic `Operation preview` label.
28
+ *
29
+ * An empty `statements` array deliberately renders as `Operation preview`
30
+ * rather than `DDL preview`: `Array.prototype.every` is vacuously true for
31
+ * empty arrays, but we have no evidence the preview is SQL-only when no
32
+ * statements are present, so the family-agnostic label is the safer default.
33
+ */
34
+ export function previewBlockHeader(preview: OperationPreview): string {
35
+ const allSql =
36
+ preview.statements.length > 0 && preview.statements.every((s) => s.language === 'sql');
37
+ return allSql ? 'DDL preview' : 'Operation preview';
38
+ }
39
+
6
40
  // ============================================================================
7
41
  // Migration Command Output Formatters (shared by db init and db update)
8
42
  // ============================================================================
@@ -24,7 +58,12 @@ export interface MigrationCommandResult {
24
58
  readonly label: string;
25
59
  readonly operationClass: string;
26
60
  }[];
27
- readonly sql?: readonly string[];
61
+ /**
62
+ * Family-agnostic textual preview of the planned operations. Replaces the
63
+ * previous `sql?: readonly string[]`. Consumers should read
64
+ * `plan.preview?.statements`.
65
+ */
66
+ readonly preview?: OperationPreview;
28
67
  };
29
68
  readonly execution?: {
30
69
  readonly operationsPlanned: number;
@@ -34,12 +73,67 @@ export interface MigrationCommandResult {
34
73
  readonly storageHash: string;
35
74
  readonly profileHash?: string;
36
75
  };
76
+ /**
77
+ * Per-space execution breakdown in canonical schedule order
78
+ * (extensions alphabetically, then app). Surfaces per-space markers
79
+ * + ops grouped by space; closes F1 / F4 / F7 from the M6
80
+ * verification doc. See {@link AggregatePerSpaceExecutionEntry}.
81
+ */
82
+ readonly perSpace?: ReadonlyArray<AggregatePerSpaceExecutionEntry>;
37
83
  readonly summary: string;
38
84
  readonly timings: {
39
85
  readonly total: number;
40
86
  };
41
87
  }
42
88
 
89
+ /**
90
+ * Render the shared per-space execution block consumed by the `db init`
91
+ * / `db update` / `migration apply` summaries (M6 sub-spec § Output
92
+ * shape contract). Always shows: space label (`Extension space: <id>`
93
+ * or `App space`) → per-op lines under each space → per-space marker
94
+ * hash (when known).
95
+ *
96
+ * `mode` controls the marker label phrasing — `'apply'` shows
97
+ * `marker → <hash>` (post-apply), `'plan'` omits the marker line
98
+ * entirely (no marker has been written yet).
99
+ */
100
+ export function formatPerSpaceBlock(
101
+ perSpace: ReadonlyArray<AggregatePerSpaceExecutionEntry>,
102
+ mode: 'plan' | 'apply',
103
+ useColor: boolean,
104
+ ): readonly string[] {
105
+ const formatYellow = createColorFormatter(useColor, yellow);
106
+ const formatCyan = createColorFormatter(useColor, cyan);
107
+ const formatDimText = (text: string) => formatDim(useColor, text);
108
+
109
+ const lines: string[] = [];
110
+ for (let s = 0; s < perSpace.length; s++) {
111
+ const space = perSpace[s]!;
112
+ if (s > 0) lines.push('');
113
+ const header =
114
+ space.kind === 'app'
115
+ ? formatCyan('App space')
116
+ : formatCyan(`Extension space: ${space.spaceId}`);
117
+ lines.push(header);
118
+ if (space.operations.length === 0) {
119
+ lines.push(` ${formatDimText('(no operations)')}`);
120
+ } else {
121
+ for (let i = 0; i < space.operations.length; i++) {
122
+ const op = space.operations[i]!;
123
+ const isLast = i === space.operations.length - 1;
124
+ const treeChar = isLast ? '└' : '├';
125
+ const destructiveMarker =
126
+ op.operationClass === 'destructive' ? ` ${formatYellow('(destructive)')}` : '';
127
+ lines.push(` ${formatDimText(treeChar)}─ ${op.label}${destructiveMarker}`);
128
+ }
129
+ }
130
+ if (mode === 'apply' && space.marker) {
131
+ lines.push(` ${formatDimText(`marker: ${space.marker.storageHash}`)}`);
132
+ }
133
+ }
134
+ return lines;
135
+ }
136
+
43
137
  /**
44
138
  * Formats human-readable output for migration commands (db init, db update) in plan mode.
45
139
  */
@@ -59,22 +153,43 @@ export function formatMigrationPlanOutput(
59
153
 
60
154
  // Plan summary
61
155
  const operationCount = result.plan?.operations.length ?? 0;
62
- lines.push(`${formatGreen('✔')} Planned ${operationCount} operation(s)`);
156
+ const spaceCount = result.perSpace?.length ?? 0;
157
+ if (spaceCount > 0) {
158
+ lines.push(
159
+ `${formatGreen('✔')} Planned ${operationCount} operation(s) across ${spaceCount} contract space${spaceCount === 1 ? '' : 's'}`,
160
+ );
161
+ } else {
162
+ lines.push(`${formatGreen('✔')} Planned ${operationCount} operation(s)`);
163
+ }
63
164
 
64
- // Show operations tree
65
- if (result.plan?.operations && result.plan.operations.length > 0) {
66
- const formatYellow = createColorFormatter(useColor, yellow);
165
+ const formatYellow = createColorFormatter(useColor, yellow);
166
+
167
+ // Per-space breakdown takes precedence over the flat ops tree when
168
+ // the aggregate flow surfaced one (M6 sub-spec § Output shape contract).
169
+ if (result.perSpace && result.perSpace.length > 0) {
170
+ lines.push('');
171
+ lines.push(...formatPerSpaceBlock(result.perSpace, 'plan', useColor));
172
+ const hasDestructive = result.perSpace.some((s) =>
173
+ s.operations.some((op) => op.operationClass === 'destructive'),
174
+ );
175
+ if (hasDestructive) {
176
+ lines.push('');
177
+ lines.push(
178
+ `${formatYellow('⚠')} This migration contains destructive operations that may cause data loss.`,
179
+ );
180
+ }
181
+ } else if (result.plan?.operations && result.plan.operations.length > 0) {
182
+ // Single-space fallback (no aggregate breakdown). Same flat tree
183
+ // we've always rendered.
67
184
  lines.push(`${formatDimText('│')}`);
68
185
  for (let i = 0; i < result.plan.operations.length; i++) {
69
186
  const op = result.plan.operations[i];
70
187
  if (!op) continue;
71
188
  const isLast = i === result.plan.operations.length - 1;
72
189
  const treeChar = isLast ? '└' : '├';
73
- const opClassLabel =
74
- op.operationClass === 'destructive'
75
- ? formatYellow(`[${op.operationClass}]`)
76
- : formatDimText(`[${op.operationClass}]`);
77
- lines.push(`${formatDimText(treeChar)}─ ${op.label} ${opClassLabel}`);
190
+ const destructiveMarker =
191
+ op.operationClass === 'destructive' ? ` ${formatYellow('(destructive)')}` : '';
192
+ lines.push(`${formatDimText(treeChar)}─ ${op.label}${destructiveMarker}`);
78
193
  }
79
194
 
80
195
  const hasDestructive = result.plan.operations.some((op) => op.operationClass === 'destructive');
@@ -92,20 +207,20 @@ export function formatMigrationPlanOutput(
92
207
  lines.push(`${formatDimText(`Destination hash: ${result.plan.destination.storageHash}`)}`);
93
208
  }
94
209
 
95
- // SQL DDL preview (SQL family only)
96
- const planSql = result.plan?.sql;
97
- if (planSql) {
210
+ // Statement preview (any family that implements OperationPreviewCapable)
211
+ const preview = result.plan?.preview;
212
+ if (preview) {
98
213
  lines.push('');
99
- lines.push(`${formatDimText('DDL preview')}`);
100
- if (planSql.length === 0) {
101
- lines.push(`${formatDimText('No DDL operations.')}`);
214
+ lines.push(`${formatDimText(previewBlockHeader(preview))}`);
215
+ if (preview.statements.length === 0) {
216
+ lines.push(`${formatDimText('No operations.')}`);
102
217
  } else {
103
218
  lines.push('');
104
- for (const statement of planSql) {
105
- const trimmed = statement.trim();
106
- if (!trimmed) continue;
107
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
108
- lines.push(`${line}`);
219
+ for (const statement of preview.statements) {
220
+ const rendered = renderPreviewStatement(statement.text, statement.language);
221
+ if (rendered) {
222
+ lines.push(rendered);
223
+ }
109
224
  }
110
225
  }
111
226
  }
@@ -127,10 +242,20 @@ export interface MigrationApplyCommandOutputResult {
127
242
  readonly migrationsApplied: number;
128
243
  readonly markerHash: string;
129
244
  readonly applied: readonly {
130
- readonly dirName: string;
245
+ readonly spaceId: string;
246
+ readonly dirName?: string;
247
+ readonly migrationHash?: string;
248
+ readonly from?: string;
249
+ readonly to?: string;
131
250
  readonly operationsExecuted: number;
132
251
  }[];
133
252
  readonly summary: string;
253
+ /**
254
+ * Per-space breakdown in canonical schedule order (extensions
255
+ * alphabetically, then app). Always present for the aggregate-walking
256
+ * `migration apply` command.
257
+ */
258
+ readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
134
259
  readonly timings?: {
135
260
  readonly total: number;
136
261
  };
@@ -149,26 +274,17 @@ export function formatMigrationApplyCommandOutput(
149
274
  const formatGreen = createColorFormatter(useColor, green);
150
275
  const formatDimText = (text: string) => formatDim(useColor, text);
151
276
 
152
- if (result.migrationsApplied === 0) {
153
- lines.push(`${formatGreen('✔')} ${result.summary}`);
154
- lines.push(formatDimText(` marker: ${result.markerHash}`));
155
- return lines.join('\n');
156
- }
157
-
158
277
  lines.push(`${formatGreen('✔')} ${result.summary}`);
159
- lines.push('');
160
278
 
161
- for (let i = 0; i < result.applied.length; i++) {
162
- const migration = result.applied[i]!;
163
- const isLast = i === result.applied.length - 1;
164
- const treeChar = isLast ? '└' : '├';
165
- lines.push(
166
- `${formatDimText(treeChar)}─ ${migration.dirName} ${formatDimText(`[${migration.operationsExecuted} op(s)]`)}`,
167
- );
279
+ if (result.perSpace.length > 0) {
280
+ lines.push('');
281
+ for (const line of formatPerSpaceBlock(result.perSpace, 'apply', useColor)) {
282
+ lines.push(line);
283
+ }
168
284
  }
169
285
 
170
286
  lines.push('');
171
- lines.push(formatDimText(`marker: ${result.markerHash}`));
287
+ lines.push(formatDimText('Next: prisma-next migration status'));
172
288
 
173
289
  if (isVerbose(flags, 1) && result.timings) {
174
290
  lines.push('');
@@ -181,17 +297,16 @@ export function formatMigrationApplyCommandOutput(
181
297
  interface MigrationShowResult {
182
298
  readonly dirName: string;
183
299
  readonly dirPath: string;
184
- readonly from: string;
300
+ readonly from: string | null;
185
301
  readonly to: string;
186
- readonly migrationId: string;
187
- readonly kind: string;
302
+ readonly migrationHash: string;
188
303
  readonly createdAt: string;
189
304
  readonly operations: readonly {
190
305
  readonly id: string;
191
306
  readonly label: string;
192
307
  readonly operationClass: string;
193
308
  }[];
194
- readonly sql: readonly string[];
309
+ readonly preview: OperationPreview;
195
310
  readonly summary: string;
196
311
  }
197
312
 
@@ -208,10 +323,9 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
208
323
  const formatDimText = (text: string) => formatDim(useColor, text);
209
324
 
210
325
  lines.push(`${formatGreen('✔')} ${result.dirName}`);
211
- lines.push(`${formatDimText(` kind: ${result.kind}`)}`);
212
- lines.push(`${formatDimText(` from: ${result.from}`)}`);
326
+ lines.push(`${formatDimText(` from: ${result.from ?? '(baseline)'}`)}`);
213
327
  lines.push(`${formatDimText(` to: ${result.to}`)}`);
214
- lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
328
+ lines.push(`${formatDimText(` migrationHash: ${result.migrationHash}`)}`);
215
329
  lines.push(`${formatDimText(` created: ${result.createdAt}`)}`);
216
330
 
217
331
  lines.push('');
@@ -223,11 +337,9 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
223
337
  const op = result.operations[i]!;
224
338
  const isLast = i === result.operations.length - 1;
225
339
  const treeChar = isLast ? '└' : '├';
226
- const opClassLabel =
227
- op.operationClass === 'destructive'
228
- ? formatYellow(`[${op.operationClass}]`)
229
- : formatDimText(`[${op.operationClass}]`);
230
- lines.push(`${formatDimText(treeChar)}─ ${op.label} ${opClassLabel}`);
340
+ const destructiveMarker =
341
+ op.operationClass === 'destructive' ? ` ${formatYellow('(destructive)')}` : '';
342
+ lines.push(`${formatDimText(treeChar)}─ ${op.label}${destructiveMarker}`);
231
343
  }
232
344
 
233
345
  const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
@@ -239,15 +351,15 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
239
351
  }
240
352
  }
241
353
 
242
- if (result.sql.length > 0) {
354
+ if (result.preview.statements.length > 0) {
243
355
  lines.push('');
244
- lines.push(`${formatDimText('DDL preview')}`);
356
+ lines.push(`${formatDimText(previewBlockHeader(result.preview))}`);
245
357
  lines.push('');
246
- for (const statement of result.sql) {
247
- const trimmed = statement.trim();
248
- if (!trimmed) continue;
249
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
250
- lines.push(`${line}`);
358
+ for (const statement of result.preview.statements) {
359
+ const rendered = renderPreviewStatement(statement.text, statement.language);
360
+ if (rendered) {
361
+ lines.push(rendered);
362
+ }
251
363
  }
252
364
  }
253
365
 
@@ -274,15 +386,39 @@ export function formatMigrationApplyOutput(
274
386
  if (result.ok) {
275
387
  // Success summary
276
388
  const executed = result.execution?.operationsExecuted ?? 0;
389
+ const spaceCount = result.perSpace?.length ?? 0;
390
+
277
391
  if (executed === 0) {
278
- lines.push(`${formatGreen('✔')} Database already matches contract`);
392
+ const acrossClause =
393
+ spaceCount > 0 ? ` across ${spaceCount} contract space${spaceCount === 1 ? '' : 's'}` : '';
394
+ lines.push(`${formatGreen('✔')} Database already matches contract${acrossClause}`);
395
+ } else if (spaceCount > 0) {
396
+ lines.push(
397
+ `${formatGreen('✔')} Applied ${executed} operation(s) across ${spaceCount} contract space${spaceCount === 1 ? '' : 's'}`,
398
+ );
279
399
  } else {
280
400
  lines.push(`${formatGreen('✔')} Applied ${executed} operation(s)`);
281
401
  }
282
402
 
283
- // Marker info
284
- if (result.marker) {
285
- lines.push(`${formatDimText(` Signature: ${result.marker.storageHash}`)}`);
403
+ // Per-space breakdown — replaces the single ambiguous `Signature:`
404
+ // line per M6 sub-spec § Output shape contract / AC4 / AC5.
405
+ if (result.perSpace && result.perSpace.length > 0) {
406
+ lines.push('');
407
+ lines.push(...formatPerSpaceBlock(result.perSpace, 'apply', useColor));
408
+ lines.push('');
409
+ lines.push(
410
+ formatDimText(
411
+ `Run 'prisma-next migration status' to confirm ${
412
+ spaceCount === 1 ? 'the space is' : 'all spaces are'
413
+ } up to date.`,
414
+ ),
415
+ );
416
+ } else if (result.marker) {
417
+ // Single-space fallback (no aggregate breakdown surfaced — e.g.
418
+ // older callers / non-aggregate code paths). Renamed from
419
+ // `Signature` to `App-space marker` per AC4 — when only one
420
+ // marker is observable, name what it covers explicitly.
421
+ lines.push(`${formatDimText(` App-space marker: ${result.marker.storageHash}`)}`);
286
422
  if (result.marker.profileHash) {
287
423
  lines.push(`${formatDimText(` Profile hash: ${result.marker.profileHash}`)}`);
288
424
  }
@@ -0,0 +1,134 @@
1
+ import { readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'pathe';
3
+
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+
8
+ function createTempArtifactPath(path: string, publicationToken: string, phase: string): string {
9
+ return join(dirname(path), `.${basename(path)}.${process.pid}.${publicationToken}.${phase}.tmp`);
10
+ }
11
+
12
+ type PreviousArtifact = { readonly content: string } | 'remove';
13
+
14
+ async function readExistingArtifact(path: string): Promise<PreviousArtifact> {
15
+ try {
16
+ return { content: await readFile(path, 'utf-8') };
17
+ } catch (error) {
18
+ if (isRecord(error) && error['code'] === 'ENOENT') {
19
+ return 'remove';
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ async function restoreArtifact(
26
+ path: string,
27
+ previous: PreviousArtifact,
28
+ publicationToken: string,
29
+ ): Promise<void> {
30
+ if (previous === 'remove') {
31
+ await rm(path, { force: true });
32
+ return;
33
+ }
34
+
35
+ const restorePath = createTempArtifactPath(path, publicationToken, 'rollback');
36
+ await writeFile(restorePath, previous.content, 'utf-8');
37
+ try {
38
+ await rename(restorePath, path);
39
+ } finally {
40
+ await rm(restorePath, { force: true });
41
+ }
42
+ }
43
+
44
+ interface PublishEntry {
45
+ readonly tempPath: string;
46
+ readonly outputPath: string;
47
+ readonly previous: PreviousArtifact;
48
+ }
49
+
50
+ function withRollbackFailureCause(error: unknown, rollbackFailures: readonly unknown[]): Error {
51
+ const rollbackCause = new AggregateError(
52
+ rollbackFailures,
53
+ 'Failed to restore published artifacts',
54
+ );
55
+
56
+ if (error instanceof Error) {
57
+ Object.defineProperty(error, 'cause', {
58
+ value: rollbackCause,
59
+ configurable: true,
60
+ writable: true,
61
+ });
62
+ return error;
63
+ }
64
+
65
+ return new Error(String(error), { cause: rollbackCause });
66
+ }
67
+
68
+ async function publishPairWithRollback(
69
+ entries: readonly PublishEntry[],
70
+ publicationToken: string,
71
+ ): Promise<void> {
72
+ const replaced: PublishEntry[] = [];
73
+ try {
74
+ for (const entry of entries) {
75
+ await rename(entry.tempPath, entry.outputPath);
76
+ replaced.push(entry);
77
+ }
78
+ } catch (error) {
79
+ const rollbackResults = await Promise.allSettled(
80
+ replaced.map((entry) => restoreArtifact(entry.outputPath, entry.previous, publicationToken)),
81
+ );
82
+ const rollbackFailures = rollbackResults.flatMap((result) =>
83
+ result.status === 'rejected' ? [result.reason] : [],
84
+ );
85
+
86
+ if (rollbackFailures.length > 0) {
87
+ throw withRollbackFailureCause(error, rollbackFailures);
88
+ }
89
+
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ export async function publishContractArtifactPair({
95
+ outputJsonPath,
96
+ outputDtsPath,
97
+ contractJson,
98
+ contractDts,
99
+ publicationToken,
100
+ beforePublish,
101
+ }: {
102
+ readonly outputJsonPath: string;
103
+ readonly outputDtsPath: string;
104
+ readonly contractJson: string;
105
+ readonly contractDts: string;
106
+ readonly publicationToken: string;
107
+ readonly beforePublish?: () => Promise<boolean> | boolean;
108
+ }): Promise<boolean> {
109
+ const tempJsonPath = createTempArtifactPath(outputJsonPath, publicationToken, 'next');
110
+ const tempDtsPath = createTempArtifactPath(outputDtsPath, publicationToken, 'next');
111
+
112
+ try {
113
+ await writeFile(tempJsonPath, contractJson, 'utf-8');
114
+ await writeFile(tempDtsPath, contractDts, 'utf-8');
115
+
116
+ if ((await beforePublish?.()) === false) {
117
+ return false;
118
+ }
119
+
120
+ const previousJson = await readExistingArtifact(outputJsonPath);
121
+ const previousDts = await readExistingArtifact(outputDtsPath);
122
+
123
+ await publishPairWithRollback(
124
+ [
125
+ { tempPath: tempDtsPath, outputPath: outputDtsPath, previous: previousDts },
126
+ { tempPath: tempJsonPath, outputPath: outputJsonPath, previous: previousJson },
127
+ ],
128
+ publicationToken,
129
+ );
130
+ return true;
131
+ } finally {
132
+ await Promise.allSettled([rm(tempJsonPath, { force: true }), rm(tempDtsPath, { force: true })]);
133
+ }
134
+ }
@@ -1,4 +0,0 @@
1
- import { CliStructuredError as CliStructuredError$1 } from "@prisma-next/errors/control";
2
- import "@prisma-next/errors/execution";
3
- import "@prisma-next/errors/migration";
4
- export { CliStructuredError$1 as t };
@@ -1,5 +0,0 @@
1
- import { CliStructuredError as CliStructuredError$1, errorConfigValidation as errorConfigValidation$1, errorContractConfigMissing as errorContractConfigMissing$1, errorContractValidationFailed, errorDatabaseConnectionRequired, errorDriverRequired, errorFileNotFound, errorMigrationPlanningFailed, errorTargetMigrationNotSupported, errorUnexpected as errorUnexpected$1 } from "@prisma-next/errors/control";
2
- import { ERROR_CODE_DESTRUCTIVE_CHANGES, errorDestructiveChanges, errorHashMismatch, errorMarkerMissing, errorRunnerFailed, errorRuntime as errorRuntime$1, errorTargetMismatch } from "@prisma-next/errors/execution";
3
- import "@prisma-next/errors/migration";
4
-
5
- export { errorUnexpected$1 as _, errorContractValidationFailed as a, errorDriverRequired as c, errorMarkerMissing as d, errorMigrationPlanningFailed as f, errorTargetMismatch as g, errorTargetMigrationNotSupported as h, errorContractConfigMissing$1 as i, errorFileNotFound as l, errorRuntime$1 as m, ERROR_CODE_DESTRUCTIVE_CHANGES as n, errorDatabaseConnectionRequired as o, errorRunnerFailed as p, errorConfigValidation$1 as r, errorDestructiveChanges as s, CliStructuredError$1 as t, errorHashMismatch as u };