@prisma-next/cli 0.12.0-dev.9 → 0.13.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 (214) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
  5. package/dist/client-CJzuo5wX.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
  7. package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +4 -5
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -3
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -6
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -7
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +37 -3
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +298 -12
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +55 -13
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +3 -2
  33. package/dist/commands/migration-graph.d.mts +16 -25
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +185 -2
  36. package/dist/commands/migration-graph.mjs.map +1 -0
  37. package/dist/commands/migration-list.d.mts +26 -27
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -190
  40. package/dist/commands/migration-log.d.mts +9 -19
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +1 -137
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +6 -5
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -1
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +17 -21
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +24 -36
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +42 -144
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +3 -759
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +4 -4
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/commands/telemetry/index.d.mts +7 -0
  61. package/dist/commands/telemetry/index.d.mts.map +1 -0
  62. package/dist/commands/telemetry/index.mjs +2 -0
  63. package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
  64. package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
  65. package/dist/config-loader.mjs +1 -1
  66. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
  67. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
  68. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
  69. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
  70. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
  71. package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
  72. package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
  73. package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
  74. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
  75. package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
  76. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
  77. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
  78. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +5 -3
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +3 -3
  83. package/dist/exports/index.mjs +1 -1
  84. package/dist/exports/index.mjs.map +1 -1
  85. package/dist/exports/init-output.d.mts +1 -3
  86. package/dist/exports/init-output.d.mts.map +1 -1
  87. package/dist/exports/init-output.mjs +1 -1
  88. package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
  89. package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
  90. package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
  91. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
  92. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
  93. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
  94. package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
  95. package/dist/init-0HwB-Vh8.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
  97. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
  98. package/dist/migration-check-soB5uZEQ.mjs +573 -0
  99. package/dist/migration-check-soB5uZEQ.mjs.map +1 -0
  100. package/dist/migration-cli.mjs +1 -1
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
  104. package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
  105. package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
  106. package/dist/migration-list-DlJJ_38Z.mjs +230 -0
  107. package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
  108. package/dist/migration-log-CG0qQAFm.mjs +222 -0
  109. package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
  110. package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
  111. package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
  112. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
  113. package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
  114. package/dist/migration-status-CgWSoI_g.mjs +446 -0
  115. package/dist/migration-status-CgWSoI_g.mjs.map +1 -0
  116. package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
  117. package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
  118. package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
  119. package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
  120. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
  121. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
  122. package/dist/schemas-CeGMYFYX.d.mts +191 -0
  123. package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
  124. package/dist/schemas-KhXMzNA_.mjs +112 -0
  125. package/dist/schemas-KhXMzNA_.mjs.map +1 -0
  126. package/dist/telemetry-BIM4beEO.mjs +122 -0
  127. package/dist/telemetry-BIM4beEO.mjs.map +1 -0
  128. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
  129. package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
  130. package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
  131. package/dist/types-C_tYiJYx.d.mts.map +1 -0
  132. package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
  133. package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
  134. package/package.json +26 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/contract-infer.ts +2 -2
  137. package/src/commands/db-update.ts +7 -1
  138. package/src/commands/init/index.ts +6 -35
  139. package/src/commands/init/init.ts +1 -14
  140. package/src/commands/init/inputs.ts +0 -75
  141. package/src/commands/inspect-live-schema.ts +10 -0
  142. package/src/commands/json/schemas.ts +195 -0
  143. package/src/commands/migrate.ts +527 -8
  144. package/src/commands/migration-check.ts +469 -134
  145. package/src/commands/migration-graph.ts +151 -119
  146. package/src/commands/migration-list.ts +72 -39
  147. package/src/commands/migration-log.ts +52 -102
  148. package/src/commands/migration-new.ts +2 -1
  149. package/src/commands/migration-plan.ts +2 -1
  150. package/src/commands/migration-show.ts +31 -66
  151. package/src/commands/migration-status-overlay.ts +61 -0
  152. package/src/commands/migration-status.ts +458 -1066
  153. package/src/commands/telemetry/index.ts +107 -0
  154. package/src/commands/telemetry/status.ts +67 -0
  155. package/src/control-api/client.ts +70 -9
  156. package/src/control-api/operations/contract-emit.ts +22 -2
  157. package/src/control-api/operations/db-init.ts +6 -3
  158. package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
  159. package/src/control-api/operations/db-update.ts +7 -4
  160. package/src/control-api/operations/db-verify.ts +15 -5
  161. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  162. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  163. package/src/control-api/types.ts +56 -29
  164. package/src/utils/cli-errors.ts +70 -2
  165. package/src/utils/formatters/errors.ts +11 -0
  166. package/src/utils/formatters/migration-graph-command-render.ts +239 -0
  167. package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
  168. package/src/utils/formatters/migration-graph-labels.ts +408 -0
  169. package/src/utils/formatters/migration-graph-model.ts +103 -0
  170. package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
  171. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  172. package/src/utils/formatters/migration-graph-space-render.ts +188 -0
  173. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  174. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  175. package/src/utils/formatters/migration-list-render.ts +135 -71
  176. package/src/utils/formatters/migration-list-styler.ts +46 -5
  177. package/src/utils/formatters/migration-list-types.ts +5 -21
  178. package/src/utils/formatters/migration-log-table.ts +205 -0
  179. package/src/utils/formatters/migrations.ts +33 -11
  180. package/src/utils/global-flags.ts +35 -0
  181. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  182. package/src/utils/legend.ts +38 -0
  183. package/src/utils/migration-path-target.ts +60 -0
  184. package/src/utils/telemetry.ts +68 -32
  185. package/dist/client-KgJorIvG.mjs.map +0 -1
  186. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  187. package/dist/commands/migration-list.mjs.map +0 -1
  188. package/dist/commands/migration-log.mjs.map +0 -1
  189. package/dist/commands/migration-status.mjs.map +0 -1
  190. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  191. package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
  192. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  193. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  194. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  195. package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
  196. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  197. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  198. package/dist/migration-graph-C9WC-7eO.mjs +0 -1478
  199. package/dist/migration-graph-C9WC-7eO.mjs.map +0 -1
  200. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  201. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  202. package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
  203. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  204. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  205. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  206. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  207. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  208. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  209. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  210. package/src/utils/formatters/graph-render.ts +0 -1323
  211. package/src/utils/formatters/graph-types.ts +0 -120
  212. package/src/utils/formatters/migration-graph-lane-colors.ts +0 -31
  213. package/src/utils/formatters/migration-graph-layout.ts +0 -1141
  214. package/src/utils/formatters/migration-graph-tree-render.ts +0 -768
@@ -1,18 +1,22 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { createControlStack } from '@prisma-next/framework-components/control';
4
+ import { type ContractSpaceMember, requireHeadRef } from '@prisma-next/migration-tools/aggregate';
5
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
4
6
  import { errorUnknownInvariant, MigrationToolsError } from '@prisma-next/migration-tools/errors';
5
7
  import { findLatestMigration, isGraphNode } from '@prisma-next/migration-tools/migration-graph';
6
8
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
7
- import type { RefEntry } from '@prisma-next/migration-tools/refs';
9
+ import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
10
+ import { readRefs } from '@prisma-next/migration-tools/refs';
8
11
  import { ifDefined } from '@prisma-next/utils/defined';
9
12
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
10
13
  import { Command } from 'commander';
11
14
  import { loadConfig } from '../config-loader';
12
15
  import { createControlClient } from '../control-api/client';
16
+ import { planMemberPath } from '../control-api/operations/migrate';
13
17
  import type {
14
- MigrationApplyFailure,
15
- MigrationApplyPathDecision,
18
+ MigrateFailure,
19
+ MigratePathDecision,
16
20
  PerSpaceExecutionEntry,
17
21
  } from '../control-api/types';
18
22
  import {
@@ -29,6 +33,7 @@ import {
29
33
  errorUnexpected,
30
34
  mapMigrationToolsError,
31
35
  mapRefResolutionError,
36
+ requireLiveDatabase,
32
37
  } from '../utils/cli-errors';
33
38
  import {
34
39
  addGlobalOptions,
@@ -42,10 +47,26 @@ import {
42
47
  } from '../utils/command-helpers';
43
48
  import { mapContractAtError } from '../utils/contract-at-errors';
44
49
  import {
50
+ buildReadAggregate,
45
51
  loadContractSpaceAggregateForCli,
46
52
  refuseContractSpaceIntegrity,
47
53
  } from '../utils/contract-space-aggregate-loader';
48
54
  import { toDeclaredExtensionsFromRaw } from '../utils/extension-pack-inputs';
55
+ import {
56
+ computeLabelColumn,
57
+ computeMaxDirNameWidth,
58
+ renderMigrationGraphCommand,
59
+ } from '../utils/formatters/migration-graph-command-render';
60
+ import { buildGrid } from '../utils/formatters/migration-graph-grid-layout';
61
+ import {
62
+ formatOnPathMigrationRow,
63
+ type MigrationEdgeAnnotation,
64
+ } from '../utils/formatters/migration-graph-labels';
65
+ import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
66
+ import {
67
+ highlightFromEdgeAnnotations,
68
+ indentMigrationGraphTreeBlock,
69
+ } from '../utils/formatters/migration-graph-space-render';
49
70
  import { formatMigrationApplyCommandOutput } from '../utils/formatters/migrations';
50
71
  import { formatStyledHeader } from '../utils/formatters/styled';
51
72
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -53,12 +74,51 @@ import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags'
53
74
  import { executeRefAdvancement, readContractIR } from '../utils/ref-advancement';
54
75
  import { handleResult } from '../utils/result-handler';
55
76
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
77
+ import { listRefsByContractHash } from './migration-list';
56
78
 
57
79
  interface MigrateCommandOptions extends CommonCommandOptions {
58
80
  readonly db?: string;
59
81
  readonly config?: string;
60
82
  readonly to?: string;
61
83
  readonly advanceRef?: string;
84
+ readonly show?: boolean;
85
+ readonly from?: string;
86
+ }
87
+
88
+ /**
89
+ * One migration that will run in a `migrate --show` preview, in execution order.
90
+ */
91
+ export interface MigrateShowMigration {
92
+ readonly spaceId: string;
93
+ readonly dirName: string;
94
+ readonly migrationHash: string;
95
+ readonly from: string;
96
+ readonly to: string;
97
+ }
98
+
99
+ /** Result returned by `migrate --show`. Read-only; no writes performed. */
100
+ export interface MigrateShowResult {
101
+ readonly ok: true;
102
+ readonly migrations: readonly MigrateShowMigration[];
103
+ readonly summary: string;
104
+ /**
105
+ * Pre-rendered Tier-3 graph tree for human output. Off-path migrations render
106
+ * dim; on-path migrations render in ordinary colours. Only present in human
107
+ * (non-JSON) mode.
108
+ */
109
+ readonly graphOutput?: string;
110
+ /**
111
+ * Name column width for the "Will run, in order:" list — globally aligned with
112
+ * every graph-tree section. Only present in human (non-JSON) mode.
113
+ */
114
+ readonly runListDirNameWidth?: number;
115
+ /**
116
+ * Left-pad offset (number of blank spaces) matching the graph's data-column
117
+ * offset (`globalMaxEdgeTreePrefixWidth`). Used to align list rows with graph
118
+ * rows so every `→` in the output (graph + list) lands at the same column.
119
+ * Only present in human (non-JSON) mode when multiple spaces are rendered.
120
+ */
121
+ readonly runListLeftPad?: number;
62
122
  }
63
123
 
64
124
  export interface MigrateResult {
@@ -76,14 +136,447 @@ export interface MigrateResult {
76
136
  }[];
77
137
  readonly summary: string;
78
138
  readonly perSpace: readonly PerSpaceExecutionEntry[];
79
- readonly pathDecision?: MigrationApplyPathDecision;
139
+ readonly pathDecision?: MigratePathDecision;
80
140
  readonly timings: {
81
141
  readonly total: number;
82
142
  };
83
143
  readonly advancedRef?: { readonly name: string; readonly hash: string } | null;
84
144
  }
85
145
 
86
- function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
146
+ /**
147
+ * Read-only preview of the migration path `migrate` will take.
148
+ *
149
+ * Computes the path through the SAME seam as `executeMigrate`:
150
+ * - `readAllMarkers()` for the from-state (when no `--from` is given), preserving
151
+ * the full marker including `invariants` (not just `storageHash`).
152
+ * - `planMemberPath()` (shared with `executeMigrate`) for per-member path selection,
153
+ * which feeds `graphWalkStrategy()` with the same target hash, target invariants,
154
+ * and current marker as the real apply path uses.
155
+ *
156
+ * Returns BEFORE any write boundary (`runMigration` / marker / DDL). No
157
+ * DB state is mutated.
158
+ */
159
+ async function executeMigrateShowCommand(
160
+ options: MigrateCommandOptions,
161
+ flags: GlobalFlags,
162
+ ui: TerminalUI,
163
+ ): Promise<Result<MigrateShowResult, CliStructuredErrorType>> {
164
+ const config = await loadConfig(options.config);
165
+ const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
166
+ options.config,
167
+ config,
168
+ );
169
+
170
+ const dbConnection = options.db ?? config.db?.connection;
171
+ const hasDriver = !!config.driver;
172
+ const hasExplicitFrom = options.from !== undefined;
173
+
174
+ // When --from is omitted we read the live DB marker (same as migrate's default).
175
+ // When --from is given, we're in offline hypothetical mode — no connection needed.
176
+ if (!hasExplicitFrom) {
177
+ const missingDb = requireLiveDatabase({
178
+ dbConnection,
179
+ hasDriver,
180
+ why: 'migrate --show needs a database connection to read the live marker (or pass --from <contract> for an offline preview)',
181
+ retryCommand: 'prisma-next migrate --show --from <contract>',
182
+ });
183
+ if (missingDb) {
184
+ return notOk(missingDb);
185
+ }
186
+ }
187
+
188
+ let allRefs: Refs = {};
189
+ try {
190
+ allRefs = await readRefs(refsDir);
191
+ } catch (error) {
192
+ if (MigrationToolsError.is(error)) {
193
+ return notOk(mapMigrationToolsError(error));
194
+ }
195
+ throw error;
196
+ }
197
+
198
+ const loaded = await buildReadAggregate(config, { migrationsDir });
199
+ if (!loaded.ok) {
200
+ return notOk(loaded.failure);
201
+ }
202
+ const { aggregate, contractHash } = loaded.value;
203
+ const appGraph = aggregate.app.graph();
204
+
205
+ // Resolve the --to target (defaults to the on-disk contract, same as migrate).
206
+ // Also capture the ref's invariants so planMemberPath feeds graphWalkStrategy the
207
+ // same target invariants that real migrate would use (refInvariants ?? headRef.invariants).
208
+ let targetHash: string = contractHash;
209
+ let refInvariants: readonly string[] | undefined;
210
+ if (options.to) {
211
+ const toResult = parseContractRef(options.to, {
212
+ graph: appGraph,
213
+ refs: allRefs,
214
+ contractHash,
215
+ });
216
+ if (!toResult.ok) {
217
+ return notOk(mapRefResolutionError(toResult.failure));
218
+ }
219
+ if (toResult.value.provenance.kind === 'reserved-db') {
220
+ return notOk(
221
+ errorDatabaseConnectionRequired({
222
+ why: '@db is not valid as a --to target; it names the live database state, not a target contract.',
223
+ commandName: 'migrate --show',
224
+ }),
225
+ );
226
+ }
227
+ targetHash = toResult.value.hash;
228
+ if (toResult.value.provenance.kind === 'ref') {
229
+ const refEntry = allRefs[toResult.value.provenance.refName];
230
+ if (refEntry) refInvariants = refEntry.invariants;
231
+ }
232
+ }
233
+
234
+ if (!flags.json && !flags.quiet) {
235
+ const details: Array<{ label: string; value: string }> = [
236
+ { label: 'config', value: configPath },
237
+ { label: 'migrations', value: migrationsRelative },
238
+ ];
239
+ if (dbConnection && !hasExplicitFrom) {
240
+ details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
241
+ }
242
+ if (options.from) {
243
+ details.push({ label: 'from', value: options.from });
244
+ }
245
+ if (options.to) {
246
+ details.push({ label: 'to', value: options.to });
247
+ }
248
+ const header = formatStyledHeader({
249
+ command: 'migrate --show',
250
+ description: 'Preview the migration path migrate will take (read-only)',
251
+ details,
252
+ flags,
253
+ });
254
+ ui.stderr(header);
255
+ }
256
+
257
+ // Resolve the from-state.
258
+ // - Explicit --from: parse it offline (no connection).
259
+ // - Omitted: read the live DB marker via readAllMarkers() — the same source migrate uses.
260
+ //
261
+ // Full marker records (storageHash + invariants) are preserved so planMemberPath
262
+ // can feed graphWalkStrategy the complete currentMarker — exactly as executeMigrate
263
+ // does via familyInstance.readAllMarkers(). A stripped { storageHash, invariants: [] }
264
+ // marker would produce a different `required` set and a different (incorrect) path.
265
+ type LiveMarker = { readonly storageHash: string; readonly invariants: readonly string[] };
266
+ const markerBySpace = new Map<string, LiveMarker | null>();
267
+ const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
268
+
269
+ if (hasExplicitFrom) {
270
+ // @db with explicit --from requires a connection
271
+ if (options.from === '@db') {
272
+ const missingDb = requireLiveDatabase({
273
+ dbConnection,
274
+ hasDriver,
275
+ why: '@db resolves to the live database marker and requires a --db connection',
276
+ retryCommand: 'prisma-next migrate --show --from @db --db $DATABASE_URL',
277
+ });
278
+ if (missingDb) {
279
+ return notOk(missingDb);
280
+ }
281
+ // Fall through to the connection path below
282
+ } else {
283
+ const fromResult = parseContractRef(options.from, {
284
+ graph: appGraph,
285
+ refs: allRefs,
286
+ contractHash,
287
+ });
288
+ if (!fromResult.ok) {
289
+ return notOk(mapRefResolutionError(fromResult.failure));
290
+ }
291
+ if (fromResult.value.provenance.kind === 'reserved-db') {
292
+ // Unreachable given the @db branch above, but guard for safety
293
+ const missingDb = requireLiveDatabase({
294
+ dbConnection,
295
+ hasDriver,
296
+ why: '@db resolves to the live database marker and requires a --db connection',
297
+ });
298
+ if (missingDb) {
299
+ return notOk(missingDb);
300
+ }
301
+ } else {
302
+ // Offline hypothetical: the --from ref only carries a hash (no live invariants).
303
+ // Apply the from-hash marker to the APP space only. Extension spaces are left
304
+ // absent from markerBySpace (treated as null / greenfield by planMemberPath),
305
+ // so they plan from their own marker → own head — exactly as executeMigrate does.
306
+ const fromHash = fromResult.value.hash;
307
+ const offlineMarker: LiveMarker | null =
308
+ fromHash === EMPTY_CONTRACT_HASH ? null : { storageHash: fromHash, invariants: [] };
309
+ markerBySpace.set(aggregate.app.spaceId, offlineMarker);
310
+ }
311
+ }
312
+ }
313
+
314
+ // If we need the live DB marker (no --from, or --from @db), connect and read.
315
+ const needsLiveMarker = !hasExplicitFrom || options.from === '@db';
316
+ if (needsLiveMarker) {
317
+ if (!dbConnection || !hasDriver) {
318
+ return notOk(
319
+ errorDatabaseConnectionRequired({
320
+ why: 'A database connection is required to read the live marker for migrate --show',
321
+ commandName: 'migrate --show',
322
+ }),
323
+ );
324
+ }
325
+ const client = createControlClient({
326
+ family: config.family,
327
+ target: config.target,
328
+ adapter: config.adapter,
329
+ driver: config.driver!,
330
+ extensionPacks: config.extensionPacks ?? [],
331
+ });
332
+ try {
333
+ await client.connect(dbConnection);
334
+ const allMarkers = await client.readAllMarkers();
335
+ // Store the full marker record (storageHash + invariants) per space.
336
+ // This is the same data executeMigrate uses via familyInstance.readAllMarkers().
337
+ for (const member of allMembers) {
338
+ const marker = allMarkers.get(member.spaceId);
339
+ markerBySpace.set(member.spaceId, marker ?? null);
340
+ }
341
+ } catch (error) {
342
+ if (CliStructuredError.is(error)) {
343
+ return notOk(error);
344
+ }
345
+ if (MigrationToolsError.is(error)) {
346
+ return notOk(mapMigrationToolsError(error));
347
+ }
348
+ return notOk(
349
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
350
+ why: `Failed to read live DB marker: ${error instanceof Error ? error.message : String(error)}`,
351
+ }),
352
+ );
353
+ } finally {
354
+ await client.close();
355
+ }
356
+ }
357
+
358
+ // Walk the path via planMemberPath — the same helper executeMigrate uses.
359
+ // planMemberPath feeds graphWalkStrategy identical inputs (targetHash, targetInvariants,
360
+ // currentMarker with full invariants), so the preview path is always the path migrate runs.
361
+ //
362
+ // Canonical schedule order: extensions alphabetically first, then app — mirroring the
363
+ // runner's `applyOrder` in operations/migrate.ts so the "Will run, in order:" list
364
+ // reflects the actual execution sequence (extensions install first, app last).
365
+ const canonicalOrderMembers: ReadonlyArray<ContractSpaceMember> = [
366
+ ...aggregate.extensions,
367
+ aggregate.app,
368
+ ];
369
+ const orderedMigrations: MigrateShowMigration[] = [];
370
+ for (const member of canonicalOrderMembers) {
371
+ const isAppMember = member.spaceId === aggregate.app.spaceId;
372
+ const headRef = requireHeadRef(member);
373
+ const memberTargetHash = isAppMember ? targetHash : headRef.hash;
374
+ const memberRefInvariants = isAppMember ? refInvariants : undefined;
375
+ const liveMarker = markerBySpace.get(member.spaceId) ?? null;
376
+
377
+ const outcome = planMemberPath({
378
+ member,
379
+ aggregate,
380
+ targetHash: memberTargetHash,
381
+ refInvariants: memberRefInvariants,
382
+ liveMarker,
383
+ });
384
+
385
+ if (outcome.kind === 'at-head') {
386
+ // Empty-graph member already at target — nothing to run for this space.
387
+ continue;
388
+ }
389
+ if (outcome.kind === 'never-planned') {
390
+ return notOk(
391
+ errorPathUnreachable({
392
+ code: 'MIGRATION_PATH_NOT_FOUND',
393
+ summary: `No on-disk migrations for contract space "${outcome.spaceId}"`,
394
+ why: `migrate is replay-only: space "${outcome.spaceId}" has no on-disk migrations but its head ref targets "${outcome.targetHash}".`,
395
+ meta: { spaceId: outcome.spaceId, target: outcome.targetHash, kind: 'neverPlanned' },
396
+ }),
397
+ );
398
+ }
399
+ if (outcome.kind === 'unreachable') {
400
+ const fromHash = outcome.liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
401
+ return notOk(
402
+ errorPathUnreachable({
403
+ code: 'MIGRATION_PATH_NOT_FOUND',
404
+ summary: `No migration path from ${fromHash.slice(0, 14)} to ${outcome.targetHash.slice(0, 14)} in space "${outcome.spaceId}".`,
405
+ why: `The migration graph has no path from the from-state to the target in space "${outcome.spaceId}".`,
406
+ meta: { spaceId: outcome.spaceId, from: fromHash, to: outcome.targetHash },
407
+ }),
408
+ );
409
+ }
410
+ if (outcome.kind === 'unsatisfiable') {
411
+ return notOk(
412
+ errorRuntime(`Missing required invariants for space "${outcome.spaceId}"`, {
413
+ why: `The path requires invariants not available on disk: ${outcome.missing.join(', ')}`,
414
+ }),
415
+ );
416
+ }
417
+
418
+ for (const edge of outcome.plan.migrationEdges) {
419
+ orderedMigrations.push({
420
+ spaceId: member.spaceId,
421
+ dirName: edge.dirName,
422
+ migrationHash: edge.migrationHash,
423
+ from: edge.from,
424
+ to: edge.to,
425
+ });
426
+ }
427
+ }
428
+
429
+ const count = orderedMigrations.length;
430
+ const summary =
431
+ count === 0
432
+ ? 'Already up to date — nothing to run'
433
+ : `${count} migration${count === 1 ? '' : 's'} will run`;
434
+
435
+ // Build the Tier-3 graph visualization (human mode only; skipped for --json).
436
+ // Reuses the existing annotation hook — no parallel renderer.
437
+ let graphOutput: string | undefined;
438
+ let runListDirNameWidth: number | undefined;
439
+ let runListLeftPad: number | undefined;
440
+ if (!flags.json) {
441
+ const onPathHashes = new Set(orderedMigrations.map((m) => m.migrationHash));
442
+ const colorize = flags.color !== false;
443
+
444
+ // Build layouts for all spaces first so we can compute global column widths
445
+ // before rendering. This ensures the name column, hash column, and ops column
446
+ // start at the same horizontal offset across every space section AND the
447
+ // "Will run, in order:" list below.
448
+ const memberLayouts = allMembers.map((member) => {
449
+ const isApp = member.spaceId === aggregate.app.spaceId;
450
+ const memberGraph = member.graph();
451
+ const rowModel = buildMigrationGraphRows(memberGraph, isApp ? { contractHash } : {});
452
+ const edgeAnnotations = new Map<string, MigrationEdgeAnnotation>();
453
+ for (const edge of memberGraph.migrationByHash.values()) {
454
+ edgeAnnotations.set(edge.migrationHash, {
455
+ pathHighlight: onPathHashes.has(edge.migrationHash) ? 'on-path' : 'off-path',
456
+ });
457
+ }
458
+ // The on-path migration set lifts to focus mode so the chosen route draws
459
+ // green/continuous; off-path lanes dim. Rows, gutter, and labels all come
460
+ // from this one grid.
461
+ const grid = buildGrid(rowModel, {}, highlightFromEdgeAnnotations(edgeAnnotations));
462
+ return { member, isApp, memberGraph, rowModel, grid, edgeAnnotations };
463
+ });
464
+
465
+ // Global max across all space grids so every section's labels share columns.
466
+ const globalLabelColumn =
467
+ memberLayouts.length > 1
468
+ ? Math.max(...memberLayouts.map(({ grid }) => computeLabelColumn(grid, 'unicode')))
469
+ : undefined;
470
+ const globalMaxDirNameWidthFromLayouts =
471
+ memberLayouts.length > 1
472
+ ? Math.max(...memberLayouts.map(({ rowModel }) => computeMaxDirNameWidth(rowModel)))
473
+ : undefined;
474
+ // The run-list name column width must be at least as wide as the global tree dirName
475
+ // width so that tree sections and the list align at the hash column.
476
+ const runListMaxFromMigrations =
477
+ orderedMigrations.length > 0
478
+ ? Math.max(...orderedMigrations.map((m) => m.dirName.length))
479
+ : 0;
480
+ const globalMaxDirNameWidth =
481
+ globalMaxDirNameWidthFromLayouts !== undefined
482
+ ? Math.max(globalMaxDirNameWidthFromLayouts, runListMaxFromMigrations)
483
+ : undefined;
484
+ runListDirNameWidth = globalMaxDirNameWidth ?? runListMaxFromMigrations;
485
+ runListLeftPad = globalLabelColumn;
486
+
487
+ // Render each space section with globally computed widths.
488
+ const showSpaceHeadings = allMembers.length > 1;
489
+ const sections: string[] = [];
490
+ for (const { member, isApp, rowModel, grid, edgeAnnotations } of memberLayouts) {
491
+ const liveMarker = markerBySpace.get(member.spaceId) ?? null;
492
+ const liveMarkerHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
493
+ const tree = renderMigrationGraphCommand({
494
+ grid,
495
+ rowModel,
496
+ contractHash,
497
+ isAppSpace: isApp,
498
+ ...(needsLiveMarker ? { dbHash: liveMarkerHash } : {}),
499
+ refsByHash: listRefsByContractHash(member),
500
+ edgeAnnotationsByHash: edgeAnnotations,
501
+ colorize,
502
+ glyphMode: 'unicode',
503
+ ...(globalLabelColumn !== undefined ? { globalLabelColumn } : {}),
504
+ ...(globalMaxDirNameWidth !== undefined ? { globalMaxDirNameWidth } : {}),
505
+ });
506
+ if (tree.length === 0) continue;
507
+ if (showSpaceHeadings) {
508
+ sections.push(`${member.spaceId}:\n${indentMigrationGraphTreeBlock(tree, ' ')}`);
509
+ } else {
510
+ sections.push(tree);
511
+ }
512
+ }
513
+ graphOutput = sections.join('\n\n');
514
+ }
515
+
516
+ return ok({
517
+ ok: true,
518
+ migrations: orderedMigrations,
519
+ summary,
520
+ ...(graphOutput !== undefined ? { graphOutput } : {}),
521
+ ...(runListDirNameWidth !== undefined ? { runListDirNameWidth } : {}),
522
+ ...(runListLeftPad !== undefined ? { runListLeftPad } : {}),
523
+ });
524
+ }
525
+
526
+ function formatMigrateShowOutput(result: MigrateShowResult, flags: GlobalFlags): string {
527
+ if (flags.quiet) return '';
528
+ const colorize = flags.color !== false;
529
+ const lines: string[] = [];
530
+ // Graph tree first (shows the full topology with on-path highlighted).
531
+ if (result.graphOutput !== undefined && result.graphOutput.length > 0) {
532
+ lines.push(result.graphOutput);
533
+ lines.push('');
534
+ }
535
+ const n = result.migrations.length;
536
+ if (n > 0) {
537
+ // Consolidated header: one line replaces the old separate summary + blank +
538
+ // "Will run, in order:" header.
539
+ lines.push(`The following ${n} migration${n === 1 ? '' : 's'} will run:`);
540
+ // Ordered list rendered through the SAME on-path row renderer as the tree.
541
+ // `formatOnPathMigrationRow` uses PATH_HIGHLIGHT_STYLES.onPath so the list and
542
+ // graph-tree rows are styled identically — changing the on-path colour in future
543
+ // is a one-line edit in PATH_HIGHLIGHT_STYLES.
544
+ //
545
+ // Alignment anchor: the `→` arrow (source-hash onward) must land at the SAME
546
+ // absolute column as in graph edge rows, across every graph section and this list.
547
+ //
548
+ // Multi-space output layout (space headings + 2-space indented tree sections):
549
+ // Graph edge row: [2 heading][G gutter][D dirName][7 source] [→] [dest]
550
+ // List row: [2 spaces][L dirName][ ][7 source] [→] [dest]
551
+ // Alignment: 2 + G + D + 9 = 2 + L + 2 + 9 => L = G + D - 2
552
+ //
553
+ // Single-space output layout (flat tree, no heading indent):
554
+ // Graph edge row: [G gutter][D dirName][7 source] [→] [dest]
555
+ // List row: [2 spaces][L dirName][ ][7 source] [→] [dest]
556
+ // Alignment: G + D + 9 = 2 + L + 2 + 9 => L = G + D - 4
557
+ //
558
+ // D (edgeDirNameWidth) = max(rawDirNameWidth + LABEL_GAP, MIN_HASH_DATA_COLUMN - G)
559
+ // where LABEL_GAP = 2 and MIN_HASH_DATA_COLUMN = 25 (same constants as the renderer).
560
+ //
561
+ // runListLeftPad is set only for multi-space; undefined means single-space.
562
+ const isMultiSpace = result.runListLeftPad !== undefined;
563
+ const gutter = result.runListLeftPad ?? 0;
564
+ const rawDirNameWidth =
565
+ result.runListDirNameWidth ?? Math.max(...result.migrations.map((m) => m.dirName.length));
566
+ const edgeDirNameWidth = Math.max(rawDirNameWidth + 2, 25 - gutter);
567
+ const listDirNameWidth = gutter + edgeDirNameWidth - (isMultiSpace ? 2 : 4);
568
+ for (const m of result.migrations) {
569
+ lines.push(
570
+ ` ${formatOnPathMigrationRow(m.dirName, m.from, m.to, listDirNameWidth, colorize, 'unicode')}`,
571
+ );
572
+ }
573
+ } else {
574
+ lines.push(result.summary);
575
+ }
576
+ return lines.join('\n');
577
+ }
578
+
579
+ function mapApplyFailure(failure: MigrateFailure): CliStructuredErrorType {
87
580
  if (failure.code === 'MIGRATION_PATH_NOT_FOUND') {
88
581
  return errorPathUnreachable(failure);
89
582
  }
@@ -136,7 +629,7 @@ async function executeMigrateCommand(
136
629
 
137
630
  // Construct the family instance up-front so the on-disk contract read
138
631
  // crosses the serializer seam (`familyInstance.deserializeContract`) at
139
- // the read site. The downstream `client.migrationApply({ contract })`
632
+ // the read site. The downstream `client.migrate({ contract })`
140
633
  // re-validates internally (no harm — validation is idempotent), but
141
634
  // closing the gap at the read site is what makes the cast-pattern
142
635
  // lint enforceable and matches the other CLI commands. See TML-2536.
@@ -312,7 +805,7 @@ async function executeMigrateCommand(
312
805
  }
313
806
  }
314
807
 
315
- const applyResult = await client.migrationApply({
808
+ const applyResult = await client.migrate({
316
809
  contract: applyContract,
317
810
  migrationsDir,
318
811
  ...ifDefined('refHash', refEntry?.hash),
@@ -384,12 +877,15 @@ export function createMigrateCommand(): Command {
384
877
  'Walks every contract space (app + extensions) and applies pending\n' +
385
878
  'on-disk migrations in canonical order (extensions alphabetically,\n' +
386
879
  'then app). Graph-walks the on-disk migration graph for every space.\n' +
387
- 'Use --to to target a specific contract (hash, ref name, migration dir).',
880
+ 'Use --to to target a specific contract (hash, ref name, migration dir).\n' +
881
+ 'Use --show for a read-only preview of the path that would run.',
388
882
  );
389
883
  setCommandExamples(command, [
390
884
  'prisma-next migrate --db $DATABASE_URL',
391
885
  'prisma-next migrate --to production --db $DATABASE_URL',
392
886
  'prisma-next migrate --to sha256:abc123 --db $DATABASE_URL',
887
+ 'prisma-next migrate --show --db $DATABASE_URL',
888
+ 'prisma-next migrate --show --from @contract --to production',
393
889
  ]);
394
890
  addGlobalOptions(command)
395
891
  .option('--db <url>', 'Database connection string')
@@ -399,12 +895,35 @@ export function createMigrateCommand(): Command {
399
895
  'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
400
896
  )
401
897
  .option('--advance-ref <name>', 'Advance the named ref to the post-apply marker after success')
898
+ .option('--show', 'Preview the migration path without applying (read-only)')
899
+ .option(
900
+ '--from <contract>',
901
+ 'From-state for --show preview (@contract, @db, hash, ref name, or migration dir)',
902
+ )
402
903
  .action(async (options: MigrateCommandOptions) => {
403
904
  const flags = parseGlobalFlagsOrExit(options);
404
905
  const startTime = Date.now();
405
906
 
406
907
  const ui = createTerminalUI(flags);
407
908
 
909
+ if (options.show) {
910
+ // Read-only path: compute the migration plan and print the ordered list.
911
+ // NEVER reaches runMigration() or any write boundary.
912
+ const result = await executeMigrateShowCommand(options, flags, ui);
913
+
914
+ const exitCode = handleResult(result, flags, ui, (showResult) => {
915
+ if (flags.json) {
916
+ ui.output(JSON.stringify(showResult, null, 2));
917
+ } else {
918
+ // Print directly to stdout — not via ui.log() which injects Clack's │ gutter.
919
+ ui.output(formatMigrateShowOutput(showResult, flags));
920
+ }
921
+ });
922
+
923
+ process.exit(exitCode);
924
+ return;
925
+ }
926
+
408
927
  const result = await executeMigrateCommand(options, flags, ui, startTime);
409
928
 
410
929
  const exitCode = handleResult(result, flags, ui, (migrateResult) => {