@prisma-next/cli 0.11.0 → 0.12.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 (196) hide show
  1. package/README.md +13 -9
  2. package/dist/cli.mjs +259 -12
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-oXO2WCPD.mjs → client-KgJorIvG.mjs} +72 -60
  5. package/dist/client-KgJorIvG.mjs.map +1 -0
  6. package/dist/{command-helpers-BSb0tRC8.mjs → command-helpers-Bbw1GbwL.mjs} +646 -46
  7. package/dist/command-helpers-Bbw1GbwL.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 +32 -7
  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 -4
  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 +12 -10
  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 +41 -11
  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 +6 -2
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +75 -40
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +4 -3
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +1 -280
  33. package/dist/commands/migration-graph.d.mts +13 -2
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +2 -137
  36. package/dist/commands/migration-list.d.mts +64 -4
  37. package/dist/commands/migration-list.d.mts.map +1 -1
  38. package/dist/commands/migration-list.mjs +143 -56
  39. package/dist/commands/migration-list.mjs.map +1 -1
  40. package/dist/commands/migration-log.d.mts +10 -1
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +10 -15
  43. package/dist/commands/migration-log.mjs.map +1 -1
  44. package/dist/commands/migration-new.d.mts.map +1 -1
  45. package/dist/commands/migration-new.mjs +32 -38
  46. package/dist/commands/migration-new.mjs.map +1 -1
  47. package/dist/commands/migration-plan.d.mts +3 -2
  48. package/dist/commands/migration-plan.d.mts.map +1 -1
  49. package/dist/commands/migration-plan.mjs +1 -1
  50. package/dist/commands/migration-show.d.mts +4 -55
  51. package/dist/commands/migration-show.d.mts.map +1 -1
  52. package/dist/commands/migration-show.mjs +61 -153
  53. package/dist/commands/migration-show.mjs.map +1 -1
  54. package/dist/commands/migration-status.d.mts +12 -49
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +85 -81
  57. package/dist/commands/migration-status.mjs.map +1 -1
  58. package/dist/commands/ref.d.mts +1 -1
  59. package/dist/commands/ref.d.mts.map +1 -1
  60. package/dist/commands/ref.mjs +38 -10
  61. package/dist/commands/ref.mjs.map +1 -1
  62. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  63. package/dist/config-loader.d.mts.map +1 -1
  64. package/dist/contract-at-errors-BxP-TOMl.mjs +42 -0
  65. package/dist/contract-at-errors-BxP-TOMl.mjs.map +1 -0
  66. package/dist/{contract-emit-bcrpT-wD.mjs → contract-emit-D-4jrNve.mjs} +25 -10
  67. package/dist/contract-emit-D-4jrNve.mjs.map +1 -0
  68. package/dist/{contract-emit-r4y8Zhf1.mjs → contract-emit-DxcGl4Uq.mjs} +19 -14
  69. package/dist/contract-emit-DxcGl4Uq.mjs.map +1 -0
  70. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-a0V5Y_mL.mjs} +4 -25
  71. package/dist/contract-enrichment-a0V5Y_mL.mjs.map +1 -0
  72. package/dist/{contract-infer-BmySmqVT.mjs → contract-infer-D8uEbJuu.mjs} +4 -5
  73. package/dist/{contract-infer-BmySmqVT.mjs.map → contract-infer-D8uEbJuu.mjs.map} +1 -1
  74. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs +247 -0
  75. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs.map +1 -0
  76. package/dist/{db-verify-BClPs3ph.mjs → db-verify-v_vUKXTU.mjs} +5 -7
  77. package/dist/{db-verify-BClPs3ph.mjs.map → db-verify-v_vUKXTU.mjs.map} +1 -1
  78. package/dist/exports/control-api.d.mts +3 -3
  79. package/dist/exports/control-api.d.mts.map +1 -1
  80. package/dist/exports/control-api.mjs +3 -3
  81. package/dist/exports/index.d.mts.map +1 -1
  82. package/dist/exports/index.mjs +1 -1
  83. package/dist/exports/index.mjs.map +1 -1
  84. package/dist/exports/init-output.d.mts.map +1 -1
  85. package/dist/exports/init-output.mjs +1 -1
  86. package/dist/extension-pack-inputs-IDvjRCi3.mjs +62 -0
  87. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +1 -0
  88. package/dist/{framework-components-65gOHkHB.mjs → framework-components-fYXjz_in.mjs} +2 -2
  89. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-fYXjz_in.mjs.map} +1 -1
  90. package/dist/global-flags-DEHjV8_s.d.mts +34 -0
  91. package/dist/global-flags-DEHjV8_s.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-rFAqZujX.mjs} +2 -2
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-rFAqZujX.mjs.map} +1 -1
  94. package/dist/{init-BCJZPWE1.mjs → init-Cv9UzWL5.mjs} +20 -269
  95. package/dist/init-Cv9UzWL5.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-DSRbFoOL.mjs → inspect-live-schema-C6ohV_oQ.mjs} +4 -5
  97. package/dist/{inspect-live-schema-DSRbFoOL.mjs.map → inspect-live-schema-C6ohV_oQ.mjs.map} +1 -1
  98. package/dist/migration-check-BiBJoYYW.mjs +341 -0
  99. package/dist/migration-check-BiBJoYYW.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts.map +1 -1
  101. package/dist/migration-cli.mjs +4 -4
  102. package/dist/migration-cli.mjs.map +1 -1
  103. package/dist/{migration-command-scaffold-Bzd9La5c.mjs → migration-command-scaffold-CjvwO6at.mjs} +4 -5
  104. package/dist/{migration-command-scaffold-Bzd9La5c.mjs.map → migration-command-scaffold-CjvwO6at.mjs.map} +1 -1
  105. package/dist/migration-graph-D7DVUElV.mjs +1232 -0
  106. package/dist/migration-graph-D7DVUElV.mjs.map +1 -0
  107. package/dist/migration-list-styler-BRwF4-gy.mjs +399 -0
  108. package/dist/migration-list-styler-BRwF4-gy.mjs.map +1 -0
  109. package/dist/{migration-plan-CFwqw3Gk.mjs → migration-plan-9DJ7q7_z.mjs} +372 -133
  110. package/dist/migration-plan-9DJ7q7_z.mjs.map +1 -0
  111. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-D2FW63pr.d.mts} +1 -1
  112. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-D2FW63pr.d.mts.map} +1 -1
  113. package/dist/{migrations-CwZMa1Ck.mjs → migrations-Cv2jxNNK.mjs} +12 -13
  114. package/dist/migrations-Cv2jxNNK.mjs.map +1 -0
  115. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  116. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  117. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
  118. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
  119. package/dist/ref-advancement-DUZqsue6.mjs +50 -0
  120. package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
  121. package/dist/terminal-ui-5Y6mrg93.d.mts +133 -0
  122. package/dist/terminal-ui-5Y6mrg93.d.mts.map +1 -0
  123. package/dist/{types--CqjMdk0.d.mts → types-Dt_SfqFm.d.mts} +28 -28
  124. package/dist/types-Dt_SfqFm.d.mts.map +1 -0
  125. package/dist/{verify-Bom75OYI.mjs → verify-DCA9Sldu.mjs} +2 -2
  126. package/dist/{verify-Bom75OYI.mjs.map → verify-DCA9Sldu.mjs.map} +1 -1
  127. package/package.json +35 -24
  128. package/src/commands/contract-emit.ts +19 -7
  129. package/src/commands/contract-infer.ts +1 -1
  130. package/src/commands/db-init.ts +48 -2
  131. package/src/commands/db-sign.ts +9 -5
  132. package/src/commands/db-update.ts +54 -8
  133. package/src/commands/init/hygiene-gitattributes.ts +2 -2
  134. package/src/commands/init/index.ts +2 -1
  135. package/src/commands/init/templates/code-templates.ts +4 -2
  136. package/src/commands/init/templates/env.ts +13 -14
  137. package/src/commands/migrate.ts +125 -44
  138. package/src/commands/migration-check.ts +43 -83
  139. package/src/commands/migration-graph.ts +75 -60
  140. package/src/commands/migration-list.ts +220 -74
  141. package/src/commands/migration-log.ts +8 -14
  142. package/src/commands/migration-new.ts +44 -48
  143. package/src/commands/migration-plan.ts +412 -197
  144. package/src/commands/migration-show.ts +65 -284
  145. package/src/commands/migration-status.ts +127 -124
  146. package/src/commands/ref.ts +53 -8
  147. package/src/control-api/client.ts +0 -1
  148. package/src/control-api/contract-enrichment.ts +6 -42
  149. package/src/control-api/operations/{apply-aggregate.ts → apply.ts} +44 -75
  150. package/src/control-api/operations/contract-emit.ts +14 -6
  151. package/src/control-api/operations/{db-apply-aggregate.ts → db-apply.ts} +19 -19
  152. package/src/control-api/operations/db-init.ts +4 -4
  153. package/src/control-api/operations/db-update.ts +4 -4
  154. package/src/control-api/operations/db-verify.ts +15 -11
  155. package/src/control-api/operations/migration-apply.ts +56 -47
  156. package/src/control-api/types.ts +26 -27
  157. package/src/migration-cli.ts +4 -4
  158. package/src/utils/cli-errors.ts +234 -0
  159. package/src/utils/command-helpers.ts +9 -24
  160. package/src/utils/contract-at-errors.ts +96 -0
  161. package/src/utils/contract-space-aggregate-loader.ts +336 -117
  162. package/src/utils/formatters/migration-graph-layout.ts +1119 -0
  163. package/src/utils/formatters/migration-graph-rows.ts +336 -0
  164. package/src/utils/formatters/migration-graph-tree-render.ts +459 -0
  165. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  166. package/src/utils/formatters/migration-list-graph-topology.ts +368 -0
  167. package/src/utils/formatters/migration-list-render.ts +191 -0
  168. package/src/utils/formatters/migration-list-styler.ts +63 -0
  169. package/src/utils/formatters/migration-list-types.ts +21 -0
  170. package/src/utils/formatters/migrations.ts +37 -46
  171. package/src/utils/glyph-mode.ts +22 -0
  172. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  173. package/src/utils/plan-resolution.ts +258 -0
  174. package/src/utils/ref-advancement.ts +68 -0
  175. package/src/utils/terminal-ui.ts +42 -1
  176. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  177. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  178. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  179. package/dist/client-oXO2WCPD.mjs.map +0 -1
  180. package/dist/command-helpers-BSb0tRC8.mjs.map +0 -1
  181. package/dist/commands/migration-check.mjs.map +0 -1
  182. package/dist/commands/migration-graph.mjs.map +0 -1
  183. package/dist/contract-emit-bcrpT-wD.mjs.map +0 -1
  184. package/dist/contract-emit-r4y8Zhf1.mjs.map +0 -1
  185. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  186. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  187. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  188. package/dist/global-flags-CdE7M0d9.d.mts +0 -15
  189. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  190. package/dist/init-BCJZPWE1.mjs.map +0 -1
  191. package/dist/migration-plan-CFwqw3Gk.mjs.map +0 -1
  192. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  193. package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
  194. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  195. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  196. package/dist/types--CqjMdk0.d.mts.map +0 -1
@@ -0,0 +1,459 @@
1
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
+ import { bold } from 'colorette';
3
+ import stringWidth from 'string-width';
4
+ import type { GlyphMode } from '../glyph-mode';
5
+ import type {
6
+ MigrationGraphGridModel,
7
+ MigrationGraphGridRow,
8
+ StructuralCell,
9
+ } from './migration-graph-layout';
10
+ import type { ClassifiedEdge } from './migration-graph-rows';
11
+ import {
12
+ MIGRATION_LIST_HASH_WIDTH,
13
+ migrationListEmptySource,
14
+ migrationListForwardArrow,
15
+ } from './migration-list-data-column';
16
+ import type { MigrationEdgeKind } from './migration-list-graph-topology';
17
+ import type { MigrationListStyler } from './migration-list-render';
18
+ import { CONTRACT_MARKER_NAME, createAnsiMigrationListStyler } from './migration-list-styler';
19
+
20
+ const LABEL_GAP = 2;
21
+
22
+ /**
23
+ * The live-database overlay marker. Just another ref as far as styling goes —
24
+ * the only emphasized markers are the active ref and the `contract`
25
+ * desired-state marker (see {@link CONTRACT_MARKER_NAME}).
26
+ */
27
+ const DB_MARKER_NAME = 'db';
28
+
29
+ export interface RenderMigrationGraphTreeOptions {
30
+ readonly refsByHash?: ReadonlyMap<string, readonly string[]>;
31
+ readonly dbHash?: string;
32
+ readonly contractHash?: string;
33
+ readonly activeRefName?: string;
34
+ readonly hashLength?: number;
35
+ readonly colorize: boolean;
36
+ readonly glyphMode?: GlyphMode;
37
+ }
38
+
39
+ interface MigrationGraphTreeGlyphPalette {
40
+ readonly node: string;
41
+ readonly arcLand: string;
42
+ readonly arcTee: string;
43
+ readonly verticalPass: string;
44
+ readonly branchTee: string;
45
+ readonly mergeTee: string;
46
+ readonly branchCorner: string;
47
+ readonly mergeCorner: string;
48
+ readonly arcBranchCorner: string;
49
+ readonly arcBranchTee: string;
50
+ readonly arcLandCorner: string;
51
+ readonly arcCrossing: string;
52
+ readonly arcLandBridge: string;
53
+ readonly horizontalPass: string;
54
+ readonly connectorBranchTee: string;
55
+ readonly connectorBranchTeeCo: string;
56
+ readonly connectorMergeTeeCo: string;
57
+ readonly edgeArrow: Readonly<Record<MigrationEdgeKind, string>>;
58
+ readonly forwardArrow: string;
59
+ readonly emptySource: string;
60
+ }
61
+
62
+ const UNICODE_PALETTE: MigrationGraphTreeGlyphPalette = {
63
+ node: '○ ',
64
+ arcLand: '○◂',
65
+ arcTee: '○─',
66
+ verticalPass: '│ ',
67
+ branchTee: '├─',
68
+ mergeTee: '├─',
69
+ branchCorner: '╮ ',
70
+ mergeCorner: '╯ ',
71
+ arcBranchCorner: '╮ ',
72
+ arcBranchTee: '┬─',
73
+ arcLandCorner: '╯ ',
74
+ arcCrossing: '┼─',
75
+ arcLandBridge: '──',
76
+ horizontalPass: '──',
77
+ connectorBranchTee: '├─',
78
+ connectorBranchTeeCo: '┬─',
79
+ connectorMergeTeeCo: '┴─',
80
+ edgeArrow: { forward: '↑', rollback: '↓', self: '⟲' },
81
+ forwardArrow: migrationListForwardArrow('unicode'),
82
+ emptySource: migrationListEmptySource('unicode'),
83
+ };
84
+
85
+ const ASCII_PALETTE: MigrationGraphTreeGlyphPalette = {
86
+ node: '* ',
87
+ arcLand: '*<',
88
+ arcTee: '*-',
89
+ verticalPass: '| ',
90
+ branchTee: '+-',
91
+ mergeTee: '+-',
92
+ branchCorner: '\\ ',
93
+ mergeCorner: '/ ',
94
+ arcBranchCorner: '\\ ',
95
+ arcBranchTee: '+-',
96
+ arcLandCorner: '/ ',
97
+ arcCrossing: '+-',
98
+ arcLandBridge: '--',
99
+ horizontalPass: '--',
100
+ connectorBranchTee: '+-',
101
+ connectorBranchTeeCo: '+-',
102
+ connectorMergeTeeCo: '+-',
103
+ edgeArrow: { forward: '^', rollback: 'v', self: '@' },
104
+ forwardArrow: migrationListForwardArrow('ascii'),
105
+ emptySource: migrationListEmptySource('ascii'),
106
+ };
107
+
108
+ function paletteFor(mode: GlyphMode): MigrationGraphTreeGlyphPalette {
109
+ return mode === 'ascii' ? ASCII_PALETTE : UNICODE_PALETTE;
110
+ }
111
+
112
+ function arrowForEdgeKind(
113
+ kind: MigrationEdgeKind,
114
+ palette: MigrationGraphTreeGlyphPalette,
115
+ ): string {
116
+ return palette.edgeArrow[kind];
117
+ }
118
+
119
+ /**
120
+ * A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
121
+ * marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`).
122
+ * The marker is the signal and stays bright (`style.kind`); the connector is
123
+ * gutter and stays dim (`style.lane`) — consistent with the plain node marker,
124
+ * which is never dimmed.
125
+ */
126
+ function renderNodeMarkerPair(pair: string, style: MigrationListStyler): string {
127
+ return style.kind(pair.slice(0, 1)) + style.lane(pair.slice(1));
128
+ }
129
+
130
+ function renderCellPair(
131
+ cell: StructuralCell,
132
+ style: MigrationListStyler,
133
+ palette: MigrationGraphTreeGlyphPalette,
134
+ ): string {
135
+ switch (cell.kind) {
136
+ case 'node':
137
+ if (cell.arcLand === true) return renderNodeMarkerPair(palette.arcLand, style);
138
+ if (cell.arcTee === true) return renderNodeMarkerPair(palette.arcTee, style);
139
+ return style.kind(palette.node);
140
+ case 'vertical-pass':
141
+ return style.lane(palette.verticalPass);
142
+ case 'edge-lane':
143
+ // The lane stays dim; the direction arrow (↑ / ↓ / ⟲) is the signal and
144
+ // stays bright, like the contract-node marker.
145
+ return cell.ownsLabel
146
+ ? style.lane(palette.verticalPass.trimEnd()) +
147
+ style.kind(arrowForEdgeKind(cell.edgeKind, palette))
148
+ : style.lane(palette.verticalPass);
149
+ case 'branch-tee':
150
+ return style.lane(palette.branchTee);
151
+ case 'merge-tee':
152
+ return style.lane(palette.mergeTee);
153
+ case 'branch-corner':
154
+ return style.lane(palette.branchCorner);
155
+ case 'merge-corner':
156
+ return style.lane(palette.mergeCorner);
157
+ case 'arc-branch-corner':
158
+ return style.lane(palette.arcBranchCorner);
159
+ case 'arc-branch-tee':
160
+ return style.lane(palette.arcBranchTee);
161
+ case 'arc-land-corner':
162
+ return style.lane(palette.arcLandCorner);
163
+ case 'arc-crossing':
164
+ return style.lane(palette.arcCrossing);
165
+ case 'arc-land-bridge':
166
+ return style.lane(palette.arcLandBridge);
167
+ case 'horizontal-pass':
168
+ return style.lane(palette.horizontalPass);
169
+ case 'empty':
170
+ return ' ';
171
+ }
172
+ }
173
+
174
+ function renderConnectorRow(
175
+ row: MigrationGraphGridRow,
176
+ gridWidth: number,
177
+ style: MigrationListStyler,
178
+ palette: MigrationGraphTreeGlyphPalette,
179
+ ): string {
180
+ const isMerge = row.kind === 'merge-connector';
181
+ if (row.cells.length > 0) {
182
+ let seenTee = false;
183
+ let out = '';
184
+ for (const cell of row.cells) {
185
+ switch (cell.kind) {
186
+ case 'branch-tee':
187
+ out += style.lane(seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee);
188
+ seenTee = true;
189
+ break;
190
+ case 'merge-tee':
191
+ out += style.lane(seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee);
192
+ seenTee = true;
193
+ break;
194
+ case 'branch-corner':
195
+ out += style.lane(palette.branchCorner);
196
+ break;
197
+ case 'merge-corner':
198
+ out += style.lane(palette.mergeCorner);
199
+ break;
200
+ case 'vertical-pass':
201
+ out += style.lane(palette.verticalPass);
202
+ break;
203
+ case 'horizontal-pass':
204
+ out += style.lane(palette.horizontalPass);
205
+ break;
206
+ default:
207
+ out += ' ';
208
+ }
209
+ }
210
+ // The cells array is sized to the grid width at emit time; a back-arc lane
211
+ // allocated by a later row can push the grid wider afterwards, so pad any
212
+ // trailing columns rather than dropping the lanes that pass through here.
213
+ for (let column = row.cells.length; column < gridWidth; column++) {
214
+ out += ' ';
215
+ }
216
+ return out;
217
+ }
218
+
219
+ const start = row.startLane ?? 0;
220
+ const end = row.endLane ?? start;
221
+ let out = '';
222
+ for (let column = 0; column < gridWidth; column++) {
223
+ if (column < start || column > end) out += ' ';
224
+ else if (column === start) out += style.lane(palette.connectorBranchTee);
225
+ else if (column === end)
226
+ out += style.lane(isMerge ? palette.mergeCorner : palette.branchCorner);
227
+ else out += style.lane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
228
+ }
229
+ return out;
230
+ }
231
+
232
+ function abbreviateHash(hash: string, hashLength: number, emptySource: string): string {
233
+ if (hash === EMPTY_CONTRACT_HASH) {
234
+ return emptySource;
235
+ }
236
+ const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
237
+ return stripped.slice(0, hashLength);
238
+ }
239
+
240
+ const MIN_HASH_DATA_COLUMN = 25;
241
+
242
+ function overlayNamesForContract(
243
+ contractHash: string,
244
+ opts: RenderMigrationGraphTreeOptions,
245
+ ): readonly string[] {
246
+ const names: string[] = [];
247
+ const userRefs = opts.refsByHash?.get(contractHash);
248
+ if (userRefs) {
249
+ names.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
250
+ }
251
+ if (opts.dbHash === contractHash) {
252
+ names.push(DB_MARKER_NAME);
253
+ }
254
+ if (opts.contractHash === contractHash && contractHash !== EMPTY_CONTRACT_HASH) {
255
+ names.push(CONTRACT_MARKER_NAME);
256
+ }
257
+ return names;
258
+ }
259
+
260
+ function createTreeStyler(opts: RenderMigrationGraphTreeOptions): MigrationListStyler {
261
+ const base = createAnsiMigrationListStyler({ useColor: opts.colorize });
262
+ const activeRefName = opts.activeRefName;
263
+ if (!opts.colorize || activeRefName === undefined) {
264
+ return base;
265
+ }
266
+ return {
267
+ ...base,
268
+ refs: (names) => {
269
+ const styledNames = names.map((name) => (name === activeRefName ? bold(name) : name));
270
+ return base.refs(styledNames);
271
+ },
272
+ };
273
+ }
274
+
275
+ function formatEdgeHashColumn(
276
+ edge: ClassifiedEdge,
277
+ style: MigrationListStyler,
278
+ hashLength: number,
279
+ palette: MigrationGraphTreeGlyphPalette,
280
+ ): string {
281
+ if (edge.kind === 'self') {
282
+ const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
283
+ return `${style.sourceHash(hash)} ${style.glyph(palette.forwardArrow)} ${style.destHash(hash)}`;
284
+ }
285
+ const source =
286
+ edge.from === EMPTY_CONTRACT_HASH
287
+ ? style.glyph(palette.emptySource) +
288
+ ' '.repeat(Math.max(0, hashLength - palette.emptySource.length))
289
+ : style.sourceHash(abbreviateHash(edge.from, hashLength, palette.emptySource));
290
+ const arrow = style.glyph(palette.forwardArrow);
291
+ const dest = style.destHash(abbreviateHash(edge.to, hashLength, palette.emptySource));
292
+ return `${source} ${arrow} ${dest}`;
293
+ }
294
+
295
+ function padVisible(text: string, targetWidth: number): string {
296
+ const padding = Math.max(0, targetWidth - stringWidth(text));
297
+ return text + ' '.repeat(padding);
298
+ }
299
+
300
+ function gridWidthForModel(rows: readonly MigrationGraphGridRow[]): number {
301
+ return rows.reduce(
302
+ (max, row) =>
303
+ row.kind === 'node' || row.kind === 'edge' ? Math.max(max, row.cells.length) : max,
304
+ 1,
305
+ );
306
+ }
307
+
308
+ function maxDirNameLength(edges: readonly ClassifiedEdge[]): number {
309
+ if (edges.length === 0) return 0;
310
+ return Math.max(...edges.map((edge) => edge.dirName.length));
311
+ }
312
+
313
+ function rowDirNameWidth(labelColumn: number, maxDirNameLen: number, dirNameGap: number): number {
314
+ return Math.max(maxDirNameLen + dirNameGap, MIN_HASH_DATA_COLUMN - labelColumn);
315
+ }
316
+
317
+ function gridUsesSkipRollbackArcs(rows: readonly MigrationGraphGridRow[]): boolean {
318
+ return rows.some((row) =>
319
+ row.cells.some(
320
+ (cell) => cell.kind === 'edge-lane' && cell.adjacency === 'node-skipping-rollback',
321
+ ),
322
+ );
323
+ }
324
+
325
+ function edgeLabelColumn(row: MigrationGraphGridRow, wideLabelColumn: number | undefined): number {
326
+ if (wideLabelColumn !== undefined) {
327
+ return wideLabelColumn;
328
+ }
329
+ const laneIndex = row.laneIndex ?? 0;
330
+ if (row.edge?.from === EMPTY_CONTRACT_HASH && laneIndex === 0) {
331
+ return (laneIndex + 1) * 2 + LABEL_GAP;
332
+ }
333
+ const usesFullRowGutter = row.cells.some(
334
+ (cell, index) => index > laneIndex && cell.kind === 'vertical-pass',
335
+ );
336
+ return usesFullRowGutter ? row.cells.length * 2 + LABEL_GAP : (laneIndex + 1) * 2 + LABEL_GAP;
337
+ }
338
+
339
+ function nodeHasArcDecoration(row: MigrationGraphGridRow): boolean {
340
+ return row.cells.some(
341
+ (cell) => cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true),
342
+ );
343
+ }
344
+
345
+ export function renderMigrationGraphTree(
346
+ model: MigrationGraphGridModel,
347
+ opts: RenderMigrationGraphTreeOptions,
348
+ ): string {
349
+ const glyphMode = opts.glyphMode ?? 'unicode';
350
+ const palette = paletteFor(glyphMode);
351
+ const style = createTreeStyler(opts);
352
+ const hashLength = opts.hashLength ?? MIGRATION_LIST_HASH_WIDTH;
353
+ const gridWidth = gridWidthForModel(model.rows);
354
+ const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows) ? gridWidth * 2 + 4 : undefined;
355
+ const dirNameGap = wideLabelColumn !== undefined ? 3 : LABEL_GAP;
356
+ const allEdges = model.rows
357
+ .filter(
358
+ (row): row is MigrationGraphGridRow & { edge: ClassifiedEdge } =>
359
+ row.kind === 'edge' && row.edge !== undefined,
360
+ )
361
+ .map((row) => row.edge);
362
+ const maxDirNameLen = maxDirNameLength(allEdges);
363
+
364
+ const lines: string[] = [];
365
+
366
+ for (let rowIndex = 0; rowIndex < model.rows.length; rowIndex++) {
367
+ const row = model.rows[rowIndex];
368
+ if (row === undefined) continue;
369
+
370
+ if (row.kind === 'component-separator') {
371
+ lines.push('');
372
+ continue;
373
+ }
374
+
375
+ if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
376
+ lines.push(renderConnectorRow(row, gridWidth, style, palette).replace(/\s+$/, ''));
377
+ continue;
378
+ }
379
+
380
+ let gutter = row.cells.map((cell) => renderCellPair(cell, style, palette)).join('');
381
+ const prevRow = model.rows[rowIndex - 1];
382
+ let laneSpan = row.cells.length;
383
+ if (row.kind === 'node') {
384
+ const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
385
+ if (prevRow?.kind === 'merge-connector' || contractHash === EMPTY_CONTRACT_HASH) {
386
+ laneSpan = 1;
387
+ } else {
388
+ laneSpan = row.cells.length;
389
+ }
390
+ }
391
+ const labelColumn =
392
+ row.kind === 'edge'
393
+ ? edgeLabelColumn(row, wideLabelColumn)
394
+ : wideLabelColumn !== undefined &&
395
+ (nodeHasArcDecoration(row) || row.contractHash !== undefined)
396
+ ? wideLabelColumn
397
+ : laneSpan * 2 + LABEL_GAP;
398
+ if (
399
+ row.kind === 'edge' &&
400
+ row.edge?.from === EMPTY_CONTRACT_HASH &&
401
+ (row.laneIndex ?? 0) === 0
402
+ ) {
403
+ gutter = row.cells
404
+ .slice(0, 1)
405
+ .map((cell) => renderCellPair(cell, style, palette))
406
+ .join('');
407
+ } else if (row.kind === 'node' && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) {
408
+ gutter = row.cells
409
+ .slice(0, laneSpan)
410
+ .map((cell) => renderCellPair(cell, style, palette))
411
+ .join('');
412
+ } else if (gutter.length < laneSpan * 2) {
413
+ gutter = gutter.padEnd(laneSpan * 2, ' ');
414
+ }
415
+ const dirNameWidth = rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
416
+ const dataColumn = labelColumn + dirNameWidth;
417
+ const gutterPad = padVisible(gutter, labelColumn);
418
+
419
+ if (row.kind === 'node') {
420
+ const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
421
+ if (contractHash === EMPTY_CONTRACT_HASH) {
422
+ const trailingLanes = row.cells
423
+ .slice(1)
424
+ .map((cell) => renderCellPair(cell, style, palette))
425
+ .join('');
426
+ const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
427
+ const overlayNames = overlayNamesForContract(contractHash, opts);
428
+ if (overlayNames.length === 0) {
429
+ lines.push(emptyGutter.replace(/\s+$/, ''));
430
+ continue;
431
+ }
432
+ const overlay = style.refs(overlayNames);
433
+ lines.push(`${padVisible(emptyGutter, dataColumn)}${overlay}`.replace(/\s+$/, ''));
434
+ continue;
435
+ }
436
+ const hashText = style.sourceHash(
437
+ abbreviateHash(contractHash, hashLength, palette.emptySource),
438
+ );
439
+ const overlayNames = overlayNamesForContract(contractHash, opts);
440
+ const overlayPad =
441
+ overlayNames.length > 0
442
+ ? ' '.repeat(Math.max(0, dataColumn - labelColumn - stringWidth(hashText)))
443
+ : '';
444
+ const overlay = overlayNames.length > 0 ? style.refs(overlayNames) : '';
445
+ lines.push(`${gutterPad}${hashText}${overlayPad}${overlay}`.replace(/\s+$/, ''));
446
+ continue;
447
+ }
448
+
449
+ const edge = row.edge;
450
+ if (edge === undefined) continue;
451
+
452
+ const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
453
+ const dirName = `${style.dirName(edge.dirName)}${dirNamePadding}`;
454
+ const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette);
455
+ lines.push(`${gutterPad}${dirName}${hashColumn}`.replace(/\s+$/, ''));
456
+ }
457
+
458
+ return lines.join('\n');
459
+ }
@@ -0,0 +1,115 @@
1
+ import type { GlyphMode } from '../glyph-mode';
2
+ import type { MigrationEdgeKind } from './migration-list-graph-topology';
3
+ import type { MigrationListStyler } from './migration-list-render';
4
+ import type { MigrationListEntry } from './migration-list-types';
5
+
6
+ export const MIGRATION_LIST_HASH_WIDTH = 7;
7
+ export const MIGRATION_LIST_EMPTY_SOURCE = '∅';
8
+ export const MIGRATION_LIST_ASCII_EMPTY_SOURCE = '-';
9
+ export const MIGRATION_LIST_FORWARD_EDGE_GLYPH = '→';
10
+ export const MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH = '->';
11
+ export const MIGRATION_LIST_DECORATION_PREFIX = ' ';
12
+
13
+ export const MIGRATION_LIST_UNICODE_KIND_GLYPH: Record<MigrationEdgeKind, string> = {
14
+ forward: '*',
15
+ rollback: '↩',
16
+ self: '⟲',
17
+ };
18
+
19
+ export const MIGRATION_LIST_ASCII_KIND_GLYPH: Record<MigrationEdgeKind, string> = {
20
+ forward: '*',
21
+ rollback: '<',
22
+ self: '~',
23
+ };
24
+
25
+ export function migrationListKindGlyph(glyphMode: GlyphMode, edgeKind: MigrationEdgeKind): string {
26
+ return glyphMode === 'ascii'
27
+ ? MIGRATION_LIST_ASCII_KIND_GLYPH[edgeKind]
28
+ : MIGRATION_LIST_UNICODE_KIND_GLYPH[edgeKind];
29
+ }
30
+
31
+ export function migrationListForwardArrow(glyphMode: GlyphMode): string {
32
+ return glyphMode === 'ascii'
33
+ ? MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH
34
+ : MIGRATION_LIST_FORWARD_EDGE_GLYPH;
35
+ }
36
+
37
+ export function migrationListEmptySource(glyphMode: GlyphMode): string {
38
+ return glyphMode === 'ascii' ? MIGRATION_LIST_ASCII_EMPTY_SOURCE : MIGRATION_LIST_EMPTY_SOURCE;
39
+ }
40
+
41
+ export function abbreviateContractHash(hash: string): string {
42
+ const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
43
+ return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);
44
+ }
45
+
46
+ export function computeMigrationDirNameWidth(migrations: readonly MigrationListEntry[]): number {
47
+ if (migrations.length === 0) return 0;
48
+ return Math.max(...migrations.map((entry) => entry.dirName.length)) + 2;
49
+ }
50
+
51
+ function formatSourceColumn(
52
+ from: string | null,
53
+ style: MigrationListStyler,
54
+ emptySource: string,
55
+ ): string {
56
+ if (from === null) {
57
+ return style.glyph(emptySource) + ' '.repeat(MIGRATION_LIST_HASH_WIDTH - emptySource.length);
58
+ }
59
+ return style.sourceHash(abbreviateContractHash(from));
60
+ }
61
+
62
+ export function formatDecorations(
63
+ providedInvariants: readonly string[],
64
+ refs: readonly string[],
65
+ style: MigrationListStyler,
66
+ ): string {
67
+ const blocks: string[] = [];
68
+ if (providedInvariants.length > 0) {
69
+ blocks.push(style.invariants(providedInvariants));
70
+ }
71
+ if (refs.length > 0) {
72
+ blocks.push(style.refs(refs));
73
+ }
74
+ if (blocks.length === 0) return '';
75
+ return `${MIGRATION_LIST_DECORATION_PREFIX}${blocks.join(' ')}`;
76
+ }
77
+
78
+ export interface MigrationDataColumnOptions {
79
+ readonly dirNameWidth: number;
80
+ readonly edgeKind: MigrationEdgeKind;
81
+ readonly style: MigrationListStyler;
82
+ readonly forwardArrow?: string;
83
+ readonly emptySource?: string;
84
+ }
85
+
86
+ export function formatMigrationDataColumn(
87
+ migration: MigrationListEntry,
88
+ options: MigrationDataColumnOptions,
89
+ ): string {
90
+ const {
91
+ dirNameWidth,
92
+ edgeKind,
93
+ style,
94
+ forwardArrow = MIGRATION_LIST_FORWARD_EDGE_GLYPH,
95
+ emptySource = MIGRATION_LIST_EMPTY_SOURCE,
96
+ } = options;
97
+ const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - migration.dirName.length));
98
+ const dirName = `${style.dirName(migration.dirName)}${dirNamePadding}`;
99
+ const decorations = formatDecorations(migration.providedInvariants, migration.refs, style);
100
+
101
+ if (edgeKind === 'self') {
102
+ const contractHash = migration.from ?? migration.to;
103
+ const hash = style.sourceHash(abbreviateContractHash(contractHash));
104
+ return `${dirName}${hash}${decorations}`;
105
+ }
106
+
107
+ const source = formatSourceColumn(migration.from, style, emptySource);
108
+ const arrow = style.glyph(forwardArrow);
109
+ const dest = style.destHash(abbreviateContractHash(migration.to));
110
+ return `${dirName}${source} ${arrow} ${dest}${decorations}`;
111
+ }
112
+
113
+ export function formatNodeLineDataColumn(contractHash: string, style: MigrationListStyler): string {
114
+ return style.sourceHash(abbreviateContractHash(contractHash));
115
+ }