@prisma-next/cli 0.12.0-dev.28 → 0.12.0-dev.29

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 (102) hide show
  1. package/dist/cli.mjs +12 -12
  2. package/dist/{client-nygCs15r.mjs → client-xeWpMlq1.mjs} +4 -4
  3. package/dist/client-xeWpMlq1.mjs.map +1 -0
  4. package/dist/{command-helpers-D7TK5Y9e.mjs → command-helpers-DK_5ItoJ.mjs} +16 -2
  5. package/dist/command-helpers-DK_5ItoJ.mjs.map +1 -0
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.mjs +3 -3
  9. package/dist/commands/db-schema.mjs +3 -3
  10. package/dist/commands/db-sign.mjs +4 -4
  11. package/dist/commands/db-update.mjs +4 -4
  12. package/dist/commands/db-verify.mjs +1 -1
  13. package/dist/commands/migrate.d.mts +1 -1
  14. package/dist/commands/migrate.mjs +4 -4
  15. package/dist/commands/migration-check.mjs +1 -1
  16. package/dist/commands/migration-graph.d.mts +12 -15
  17. package/dist/commands/migration-graph.d.mts.map +1 -1
  18. package/dist/commands/migration-graph.mjs +84 -51
  19. package/dist/commands/migration-graph.mjs.map +1 -1
  20. package/dist/commands/migration-list.d.mts +13 -4
  21. package/dist/commands/migration-list.d.mts.map +1 -1
  22. package/dist/commands/migration-list.mjs +1 -187
  23. package/dist/commands/migration-log.d.mts +2 -2
  24. package/dist/commands/migration-log.mjs +1 -1
  25. package/dist/commands/migration-new.mjs +3 -3
  26. package/dist/commands/migration-plan.d.mts +1 -1
  27. package/dist/commands/migration-plan.mjs +1 -1
  28. package/dist/commands/migration-show.d.mts +1 -1
  29. package/dist/commands/migration-show.mjs +2 -2
  30. package/dist/commands/migration-status.d.mts +20 -2
  31. package/dist/commands/migration-status.d.mts.map +1 -1
  32. package/dist/commands/migration-status.mjs +2 -3
  33. package/dist/commands/ref.d.mts +1 -1
  34. package/dist/commands/ref.mjs +2 -2
  35. package/dist/commands/telemetry/index.mjs +1 -1
  36. package/dist/{contract-at-errors-CK3qoqZf.mjs → contract-at-errors-DG3kjgoz.mjs} +2 -2
  37. package/dist/{contract-at-errors-CK3qoqZf.mjs.map → contract-at-errors-DG3kjgoz.mjs.map} +1 -1
  38. package/dist/{contract-emit-Dzf73HdD.mjs → contract-emit-BO0l6fnT.mjs} +3 -3
  39. package/dist/{contract-emit-Dzf73HdD.mjs.map → contract-emit-BO0l6fnT.mjs.map} +1 -1
  40. package/dist/{contract-emit-DwlIz5Zg.mjs → contract-emit-C0Bs0VRj.mjs} +3 -3
  41. package/dist/{contract-emit-DwlIz5Zg.mjs.map → contract-emit-C0Bs0VRj.mjs.map} +1 -1
  42. package/dist/{contract-infer-Bzh___GO.mjs → contract-infer-2wtPflGH.mjs} +3 -3
  43. package/dist/{contract-infer-Bzh___GO.mjs.map → contract-infer-2wtPflGH.mjs.map} +1 -1
  44. package/dist/{contract-space-aggregate-loader-5zmOENc4.mjs → contract-space-aggregate-loader-Dbr3-jHF.mjs} +2 -2
  45. package/dist/{contract-space-aggregate-loader-5zmOENc4.mjs.map → contract-space-aggregate-loader-Dbr3-jHF.mjs.map} +1 -1
  46. package/dist/{db-verify-CNz036sw.mjs → db-verify-CxHiSiTG.mjs} +4 -4
  47. package/dist/{db-verify-CNz036sw.mjs.map → db-verify-CxHiSiTG.mjs.map} +1 -1
  48. package/dist/exports/control-api.d.mts +1 -1
  49. package/dist/exports/control-api.mjs +2 -2
  50. package/dist/exports/index.mjs +1 -1
  51. package/dist/exports/init-output.mjs +1 -1
  52. package/dist/{framework-components-CyM_xYCY.mjs → framework-components-CxOVKAAh.mjs} +2 -2
  53. package/dist/{framework-components-CyM_xYCY.mjs.map → framework-components-CxOVKAAh.mjs.map} +1 -1
  54. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
  55. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
  56. package/dist/{init-DJsQpr_6.mjs → init-R272pxux.mjs} +4 -4
  57. package/dist/{init-DJsQpr_6.mjs.map → init-R272pxux.mjs.map} +1 -1
  58. package/dist/{inspect-live-schema-DE76Ou4D.mjs → inspect-live-schema-RekOwfi5.mjs} +3 -3
  59. package/dist/{inspect-live-schema-DE76Ou4D.mjs.map → inspect-live-schema-RekOwfi5.mjs.map} +1 -1
  60. package/dist/{migration-check-CL2MzDRX.mjs → migration-check-Dc0cOhKH.mjs} +2 -2
  61. package/dist/{migration-check-CL2MzDRX.mjs.map → migration-check-Dc0cOhKH.mjs.map} +1 -1
  62. package/dist/{migration-command-scaffold-194pA8F5.mjs → migration-command-scaffold-ApB3NxWY.mjs} +3 -3
  63. package/dist/{migration-command-scaffold-194pA8F5.mjs.map → migration-command-scaffold-ApB3NxWY.mjs.map} +1 -1
  64. package/dist/{migration-graph-tree-render-CVmV9sWr.mjs → migration-graph-space-render-dmLLWift.mjs} +389 -210
  65. package/dist/migration-graph-space-render-dmLLWift.mjs.map +1 -0
  66. package/dist/migration-list-C5sXrl0U.mjs +228 -0
  67. package/dist/migration-list-C5sXrl0U.mjs.map +1 -0
  68. package/dist/{migration-log-CP6skD5b.mjs → migration-log-DD_vCbYW.mjs} +4 -4
  69. package/dist/{migration-log-CP6skD5b.mjs.map → migration-log-DD_vCbYW.mjs.map} +1 -1
  70. package/dist/{migration-plan-D61N1hID.mjs → migration-plan-CeTjQOIG.mjs} +5 -5
  71. package/dist/{migration-plan-D61N1hID.mjs.map → migration-plan-CeTjQOIG.mjs.map} +1 -1
  72. package/dist/{migration-status--ejfYqWS.mjs → migration-status-qV8ctwPy.mjs} +61 -45
  73. package/dist/migration-status-qV8ctwPy.mjs.map +1 -0
  74. package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
  75. package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
  76. package/dist/{telemetry-CnfdMrpv.mjs → telemetry-S-NGi9U6.mjs} +2 -2
  77. package/dist/{telemetry-CnfdMrpv.mjs.map → telemetry-S-NGi9U6.mjs.map} +1 -1
  78. package/dist/{types-BYwWOyYJ.d.mts → types-Mh7mdPHM.d.mts} +1 -1
  79. package/dist/{types-BYwWOyYJ.d.mts.map → types-Mh7mdPHM.d.mts.map} +1 -1
  80. package/dist/{verify-By66Zu3y.mjs → verify-BdI-BgYi.mjs} +2 -2
  81. package/dist/{verify-By66Zu3y.mjs.map → verify-BdI-BgYi.mjs.map} +1 -1
  82. package/package.json +18 -18
  83. package/src/commands/migration-graph.ts +125 -58
  84. package/src/commands/migration-list.ts +43 -9
  85. package/src/commands/migration-status.ts +106 -74
  86. package/src/control-api/operations/db-apply.ts +7 -4
  87. package/src/utils/cli-errors.ts +17 -0
  88. package/src/utils/formatters/migration-graph-lane-colors.ts +164 -1
  89. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  90. package/src/utils/formatters/migration-graph-space-render.ts +138 -0
  91. package/src/utils/formatters/migration-graph-tree-render.ts +149 -239
  92. package/src/utils/formatters/migration-list-data-column.ts +6 -0
  93. package/src/utils/formatters/migration-list-render.ts +43 -23
  94. package/src/utils/formatters/migration-list-styler.ts +48 -5
  95. package/src/utils/legend.ts +38 -0
  96. package/dist/client-nygCs15r.mjs.map +0 -1
  97. package/dist/command-helpers-D7TK5Y9e.mjs.map +0 -1
  98. package/dist/commands/migration-list.mjs.map +0 -1
  99. package/dist/migration-graph-tree-render-CVmV9sWr.mjs.map +0 -1
  100. package/dist/migration-status--ejfYqWS.mjs.map +0 -1
  101. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  102. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
@@ -2,7 +2,17 @@ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
2
  import { bold, createColors, green, yellow } from 'colorette';
3
3
  import stringWidth from 'string-width';
4
4
  import type { GlyphMode } from '../glyph-mode';
5
- import { laneColorForColumn } from './migration-graph-lane-colors';
5
+ import {
6
+ laneColorForColumn,
7
+ NEUTRAL_LANE_COLUMN,
8
+ type RowArcLaneColors,
9
+ resolveConnectorLaneColors,
10
+ resolveRowArcLaneColors,
11
+ stylerForLaneColumn,
12
+ } from './migration-graph-lane-colors';
13
+
14
+ export { resolveConnectorLaneColors } from './migration-graph-lane-colors';
15
+
6
16
  import type {
7
17
  MigrationGraphGridModel,
8
18
  MigrationGraphGridRow,
@@ -13,10 +23,15 @@ import {
13
23
  MIGRATION_LIST_HASH_WIDTH,
14
24
  migrationListEmptySource,
15
25
  migrationListForwardArrow,
26
+ padFromHashColumn,
16
27
  } from './migration-list-data-column';
17
28
  import type { MigrationEdgeKind } from './migration-list-graph-topology';
18
29
  import type { MigrationListStyler } from './migration-list-render';
19
- import { CONTRACT_MARKER_NAME, createAnsiMigrationListStyler } from './migration-list-styler';
30
+ import {
31
+ CONTRACT_MARKER_NAME,
32
+ createAnsiMigrationListStyler,
33
+ formatContractNodeOverlays,
34
+ } from './migration-list-styler';
20
35
 
21
36
  const LABEL_GAP = 2;
22
37
 
@@ -40,8 +55,11 @@ export interface RenderMigrationGraphTreeOptions {
40
55
  readonly contractHash?: string;
41
56
  readonly activeRefName?: string;
42
57
  readonly hashLength?: number;
58
+ readonly globalMaxEdgeTreePrefixWidth?: number;
59
+ readonly globalMaxDirNameWidth?: number;
43
60
  readonly colorize: boolean;
44
61
  readonly glyphMode?: GlyphMode;
62
+ readonly styler?: MigrationListStyler;
45
63
  }
46
64
 
47
65
  interface MigrationGraphTreeGlyphPalette {
@@ -134,13 +152,6 @@ function arrowForEdgeKind(
134
152
  return palette.edgeArrow[kind];
135
153
  }
136
154
 
137
- /**
138
- * The leftmost lane (column 0) renders with the neutral dim lane style rather
139
- * than a palette hue — in the common single-lane case it has nothing to be told
140
- * apart from. Used as the "no owning arc" sentinel during colour resolution.
141
- */
142
- const NEUTRAL_LANE = 0;
143
-
144
155
  /**
145
156
  * Forced bold for branch-coloured names. A branched name pairs its lane hue
146
157
  * (also forced, via {@link laneColorForColumn}) with bold; both must emit even
@@ -149,194 +160,12 @@ const NEUTRAL_LANE = 0;
149
160
  */
150
161
  const { bold: forcedBold } = createColors({ useColor: true });
151
162
 
152
- /**
153
- * The colour-source column for each cell of a row, resolved together because a
154
- * routed back-arc spans columns and must read as **one hue** rather than a
155
- * per-column "rainbow". An arc's horizontal bridges, corners, and node-pair
156
- * connector all take the arc's owning back-lane column (the corner that closes
157
- * the arc), not the column they pass through.
158
- */
159
- interface RowLaneColors {
160
- /** Colour column for a cell's structural glyph (lane / spine / arc body). */
161
- readonly lane: readonly number[];
162
- /** Colour column for a node arc-pair's connector half (`◂` / `─`). */
163
- readonly connector: readonly number[];
164
- /**
165
- * Colour column for the trailing `─` of a landing tee (`┴─`). The junction
166
- * (`lane`) keeps its own column; the dash leads into the next converging arc.
167
- */
168
- readonly dash: readonly number[];
169
- }
170
-
171
- /**
172
- * Resolve per-cell colour columns for a row. Scanning right-to-left lets each
173
- * arc segment inherit the hue of the arc it leads into.
174
- *
175
- * On a converging-landing line (`○◂──────┴─┴─╯`), every horizontal dash segment
176
- * takes the hue of the **nearest landing anchor** — the next `arc-land-tee` or
177
- * `arc-land-corner` — to its right, i.e. the branch it leads into: the bridge
178
- * run leads into the first converging arc, and each tee's trailing `─` leads
179
- * into the next arc out. Tee/corner junction glyphs keep their own column hue.
180
- * This mirrors the forward connector's `┬─` rule (see
181
- * {@link resolveConnectorLaneColors}). A single (non-converging) landing has
182
- * only the corner as an anchor, so its whole horizontal run reads as one hue.
183
- *
184
- * The source side (`○─`, `arc-branch-tee`, `arc-branch-corner`) and pure
185
- * horizontal passes are unaffected: they track the nearest corner to the right
186
- * (`arcCorner`), so a routed back-arc's source fan still reads as one hue. A
187
- * crossing can only be one colour, so it takes the arc owning the horizontal
188
- * run at this row; the crossed vertical lane is occluded at that one cell and
189
- * reappears on the next row.
190
- */
191
- function resolveRowLaneColors(cells: readonly StructuralCell[]): RowLaneColors {
192
- const lane = new Array<number>(cells.length);
193
- const connector = new Array<number>(cells.length);
194
- const dash = new Array<number>(cells.length);
195
- let arcCorner = NEUTRAL_LANE;
196
- let landingAnchor = NEUTRAL_LANE;
197
- for (let column = cells.length - 1; column >= 0; column--) {
198
- const cell = cells[column];
199
- connector[column] = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
200
- switch (cell?.kind) {
201
- case 'arc-branch-corner':
202
- arcCorner = column;
203
- lane[column] = column;
204
- dash[column] = column;
205
- break;
206
- case 'arc-land-corner':
207
- arcCorner = column;
208
- landingAnchor = column;
209
- lane[column] = column;
210
- dash[column] = column;
211
- break;
212
- // An inner co-sourced arc's own back-lane junction: its vertical run
213
- // continues below in this column, so the whole `┬─` keeps its own column.
214
- case 'arc-branch-tee':
215
- lane[column] = column;
216
- dash[column] = column;
217
- break;
218
- // The symmetric co-landing junction: the `┴` keeps its own column (its
219
- // vertical run continues above), but the trailing `─` leads into the next
220
- // converging arc — the nearest landing anchor still to its right.
221
- case 'arc-land-tee':
222
- lane[column] = column;
223
- dash[column] = landingAnchor === NEUTRAL_LANE ? column : landingAnchor;
224
- landingAnchor = column;
225
- break;
226
- case 'arc-crossing':
227
- case 'arc-land-bridge': {
228
- const served = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
229
- lane[column] = served;
230
- dash[column] = served;
231
- break;
232
- }
233
- case 'horizontal-pass':
234
- lane[column] = arcCorner === NEUTRAL_LANE ? column : arcCorner;
235
- dash[column] = lane[column] ?? column;
236
- break;
237
- case 'node':
238
- lane[column] = column;
239
- dash[column] = column;
240
- arcCorner = NEUTRAL_LANE;
241
- landingAnchor = NEUTRAL_LANE;
242
- break;
243
- default:
244
- lane[column] = column;
245
- dash[column] = column;
246
- arcCorner = NEUTRAL_LANE;
247
- landingAnchor = NEUTRAL_LANE;
248
- }
249
- }
250
- return { lane, connector, dash };
251
- }
252
-
253
- /**
254
- * Per-cell colour for a forward branch/merge connector row, split into the
255
- * cell's junction `glyph` and its trailing `dash`. A connector's horizontal run
256
- * is one logical line (a fork into new lanes, or a merge into a surviving lane)
257
- * and reads best as the colour of the lane each segment serves — not dim-gray
258
- * or a per-pass-through-column "rainbow".
259
- */
260
- interface ConnectorLaneColors {
261
- /** Colour column for a cell's junction glyph (`├` / `┬` / `┴` / `╮` / `╯`). */
262
- readonly glyph: readonly number[];
263
- /** Colour column for a tee's trailing `─` — the branch it leads into. */
264
- readonly dash: readonly number[];
265
- }
266
-
267
- /**
268
- * Resolve per-cell connector colours. Scanning right-to-left, a corner or an
269
- * intermediate tee anchors its own lane (its junction glyph takes that column),
270
- * but a tee's **trailing dash leads into the branch on its right** (the next
271
- * branch point), so `┬─` reads as "this lane, then on toward the next" rather
272
- * than tinting the dash with the left lane. The leading tee at `startLane` (the
273
- * fork/merge origin) and pure horizontal segments inherit the nearest branch
274
- * point to their right whole-cell, so the run into a branch — or collapsing
275
- * into a merge corner — stays continuous. An `arc-crossing` keeps its junction
276
- * glyph at its own column but re-anchors `owner` like an intermediate tee so
277
- * dashes on both sides lead into the nearest branch on their right. Pass-through
278
- * verticals outside the run keep their own column (column 0 stays neutral).
279
- */
280
- export function resolveConnectorLaneColors(
281
- cells: readonly StructuralCell[],
282
- startLane: number,
283
- ): ConnectorLaneColors {
284
- const glyph = new Array<number>(cells.length);
285
- const dash = new Array<number>(cells.length);
286
- let owner = NEUTRAL_LANE;
287
- for (let column = cells.length - 1; column >= 0; column--) {
288
- const cell = cells[column];
289
- switch (cell?.kind) {
290
- case 'branch-corner':
291
- case 'merge-corner':
292
- owner = column;
293
- glyph[column] = column;
294
- dash[column] = column;
295
- break;
296
- case 'branch-tee':
297
- case 'merge-tee':
298
- if (column === startLane) {
299
- const served = owner === NEUTRAL_LANE ? column : owner;
300
- glyph[column] = column;
301
- dash[column] = served;
302
- } else {
303
- dash[column] = owner === NEUTRAL_LANE ? column : owner;
304
- glyph[column] = column;
305
- owner = column;
306
- }
307
- break;
308
- case 'arc-crossing':
309
- glyph[column] = column;
310
- dash[column] = owner === NEUTRAL_LANE ? column : owner;
311
- owner = column;
312
- break;
313
- case 'horizontal-pass': {
314
- const served = owner === NEUTRAL_LANE ? column : owner;
315
- glyph[column] = served;
316
- dash[column] = served;
317
- break;
318
- }
319
- default:
320
- glyph[column] = column;
321
- dash[column] = column;
322
- }
323
- }
324
- return { glyph, dash };
325
- }
326
-
327
- /**
328
- * Style a structural glyph by its resolved colour column. Column 0 and the
329
- * neutral sentinel render dim (`style.lane`); columns ≥ 1 take a palette hue.
330
- */
331
163
  function laneStylerForColumn(
332
164
  colorColumn: number,
333
165
  colorize: boolean,
334
166
  style: MigrationListStyler,
335
167
  ): (text: string) => string {
336
- if (!colorize || colorColumn <= NEUTRAL_LANE) {
337
- return (text) => style.lane(text);
338
- }
339
- return laneColorForColumn(colorColumn);
168
+ return stylerForLaneColumn(colorColumn, colorize, style.lane);
340
169
  }
341
170
 
342
171
  /**
@@ -351,12 +180,27 @@ function branchStylerOrDefault(
351
180
  colorize: boolean,
352
181
  fallback: (text: string) => string,
353
182
  ): (text: string) => string {
354
- if (!colorize || column <= NEUTRAL_LANE) {
183
+ if (!colorize || column <= NEUTRAL_LANE_COLUMN) {
355
184
  return fallback;
356
185
  }
357
186
  return laneColorForColumn(column);
358
187
  }
359
188
 
189
+ /**
190
+ * Render a crossing tee (`┼─`): the junction stays dim/neutral so neither arc
191
+ * steals the cell; the trailing dash takes the served lane hue.
192
+ */
193
+ function renderArcCrossing(
194
+ pair: string,
195
+ dashColumn: number,
196
+ colorize: boolean,
197
+ style: MigrationListStyler,
198
+ ): string {
199
+ const junction = colorize ? style.lane : (text: string) => text;
200
+ const dash = laneStylerForColumn(dashColumn, colorize, style);
201
+ return junction(pair.slice(0, 1)) + dash(pair.slice(1));
202
+ }
203
+
360
204
  /**
361
205
  * Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
362
206
  * trailing dash coloured independently: the junction anchors its own lane while
@@ -398,7 +242,7 @@ function renderNodeMarkerPair(
398
242
  function renderCellPair(
399
243
  cell: StructuralCell,
400
244
  column: number,
401
- colors: RowLaneColors,
245
+ colors: RowArcLaneColors,
402
246
  colorize: boolean,
403
247
  style: MigrationListStyler,
404
248
  palette: MigrationGraphTreeGlyphPalette,
@@ -407,7 +251,7 @@ function renderCellPair(
407
251
  const lane = laneStylerForColumn(laneColumn, colorize, style);
408
252
  switch (cell.kind) {
409
253
  case 'node': {
410
- const arcColumn = colors.connector[column] ?? NEUTRAL_LANE;
254
+ const arcColumn = colors.connector[column] ?? NEUTRAL_LANE_COLUMN;
411
255
  if (cell.arcLand === true) {
412
256
  return renderNodeMarkerPair(palette.arcLand, column, arcColumn, colorize, style);
413
257
  }
@@ -512,7 +356,7 @@ function renderConnectorRow(
512
356
  out += lane(palette.horizontalPass);
513
357
  break;
514
358
  case 'arc-crossing':
515
- out += renderConnectorTee(palette.arcCrossing, glyphColumn, dashColumn, colorize, style);
359
+ out += renderArcCrossing(palette.arcCrossing, dashColumn, colorize, style);
516
360
  break;
517
361
  default:
518
362
  out += ' ';
@@ -552,26 +396,41 @@ function abbreviateHash(hash: string, hashLength: number, emptySource: string):
552
396
 
553
397
  const MIN_HASH_DATA_COLUMN = 25;
554
398
 
399
+ interface ContractOverlayNames {
400
+ readonly markers: readonly string[];
401
+ readonly refs: readonly string[];
402
+ }
403
+
555
404
  function overlayNamesForContract(
556
405
  contractHash: string,
557
406
  opts: RenderMigrationGraphTreeOptions,
558
- ): readonly string[] {
559
- const names: string[] = [];
407
+ ): ContractOverlayNames {
408
+ const markers: string[] = [];
409
+ const refs: string[] = [];
560
410
  const userRefs = opts.refsByHash?.get(contractHash);
561
411
  if (userRefs) {
562
- names.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
563
- }
564
- if (opts.dbHash === contractHash) {
565
- names.push(DB_MARKER_NAME);
412
+ refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
566
413
  }
567
414
  if (opts.contractHash === contractHash && contractHash !== EMPTY_CONTRACT_HASH) {
568
- names.push(CONTRACT_MARKER_NAME);
415
+ markers.push(CONTRACT_MARKER_NAME);
416
+ }
417
+ if (opts.dbHash === contractHash) {
418
+ markers.push(DB_MARKER_NAME);
569
419
  }
570
- return names;
420
+ markers.sort((a, b) => {
421
+ if (a === CONTRACT_MARKER_NAME) {
422
+ return -1;
423
+ }
424
+ if (b === CONTRACT_MARKER_NAME) {
425
+ return 1;
426
+ }
427
+ return a.localeCompare(b);
428
+ });
429
+ return { markers, refs };
571
430
  }
572
431
 
573
432
  function createTreeStyler(opts: RenderMigrationGraphTreeOptions): MigrationListStyler {
574
- const base = createAnsiMigrationListStyler({ useColor: opts.colorize });
433
+ const base = opts.styler ?? createAnsiMigrationListStyler({ useColor: opts.colorize });
575
434
  const activeRefName = opts.activeRefName;
576
435
  if (!opts.colorize || activeRefName === undefined) {
577
436
  return base;
@@ -595,6 +454,12 @@ function formatEdgeAnnotationSuffix(
595
454
  return '';
596
455
  }
597
456
  const segments: string[] = [];
457
+ if (annotation.operationCount !== undefined) {
458
+ segments.push(`${annotation.operationCount} ops`);
459
+ }
460
+ if (annotation.invariants !== undefined && annotation.invariants.length > 0) {
461
+ segments.push(style.invariants(annotation.invariants));
462
+ }
598
463
  const status = annotation.status;
599
464
  if (status !== undefined) {
600
465
  const glyphs = overlayStatusGlyphs(opts.glyphMode ?? 'unicode');
@@ -607,17 +472,10 @@ function formatEdgeAnnotationSuffix(
607
472
  segments.push(styler(`${glyph} ${label}`));
608
473
  }
609
474
  }
610
- if (annotation.operationCount !== undefined) {
611
- segments.push(`${annotation.operationCount} ops`);
612
- }
613
- if (annotation.invariants !== undefined && annotation.invariants.length > 0) {
614
- segments.push(style.invariants(annotation.invariants));
615
- }
616
475
  if (segments.length === 0) {
617
476
  return '';
618
477
  }
619
- const prefix = status !== undefined ? ' ' : ' ';
620
- return `${prefix}${segments.join(' ')}`;
478
+ return ` ${segments.join(' ')}`;
621
479
  }
622
480
 
623
481
  function formatEdgeHashColumn(
@@ -628,13 +486,16 @@ function formatEdgeHashColumn(
628
486
  ): string {
629
487
  if (edge.kind === 'self') {
630
488
  const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
631
- return `${style.sourceHash(hash)} ${style.glyph(palette.forwardArrow)} ${style.destHash(hash)}`;
489
+ const source = padFromHashColumn(style.sourceHash(hash), hashLength);
490
+ return `${source} ${style.glyph(palette.forwardArrow)} ${style.destHash(hash)}`;
632
491
  }
633
492
  const source =
634
493
  edge.from === EMPTY_CONTRACT_HASH
635
- ? style.glyph(palette.emptySource) +
636
- ' '.repeat(Math.max(0, hashLength - palette.emptySource.length))
637
- : style.sourceHash(abbreviateHash(edge.from, hashLength, palette.emptySource));
494
+ ? padFromHashColumn(style.glyph(palette.emptySource), hashLength)
495
+ : padFromHashColumn(
496
+ style.sourceHash(abbreviateHash(edge.from, hashLength, palette.emptySource)),
497
+ hashLength,
498
+ );
638
499
  const arrow = style.glyph(palette.forwardArrow);
639
500
  const dest = style.destHash(abbreviateHash(edge.to, hashLength, palette.emptySource));
640
501
  return `${source} ${arrow} ${dest}`;
@@ -691,6 +552,35 @@ function edgeLabelColumn(row: MigrationGraphGridRow, wideLabelColumn: number | u
691
552
  return usesFullRowGutter ? row.cells.length * 2 + LABEL_GAP : (laneIndex + 1) * 2 + LABEL_GAP;
692
553
  }
693
554
 
555
+ function maxEdgeTreePrefixWidth(
556
+ rows: readonly MigrationGraphGridRow[],
557
+ wideLabelColumn: number | undefined,
558
+ ): number {
559
+ let max = 0;
560
+ for (const row of rows) {
561
+ if (row.kind !== 'edge' || row.edge === undefined) continue;
562
+ max = Math.max(max, edgeLabelColumn(row, wideLabelColumn));
563
+ }
564
+ return max;
565
+ }
566
+
567
+ export function computeMaxEdgeTreePrefixWidthForLayout(model: MigrationGraphGridModel): number {
568
+ const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows)
569
+ ? gridWidthForModel(model.rows) * 2 + 4
570
+ : undefined;
571
+ return maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
572
+ }
573
+
574
+ export function computeMaxDirNameLengthForLayout(model: MigrationGraphGridModel): number {
575
+ const allEdges = model.rows
576
+ .filter(
577
+ (row): row is MigrationGraphGridRow & { edge: ClassifiedEdge } =>
578
+ row.kind === 'edge' && row.edge !== undefined,
579
+ )
580
+ .map((row) => row.edge);
581
+ return maxDirNameLength(allEdges);
582
+ }
583
+
694
584
  function nodeHasArcDecoration(row: MigrationGraphGridRow): boolean {
695
585
  return row.cells.some(
696
586
  (cell) => cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true),
@@ -715,6 +605,10 @@ export function renderMigrationGraphTree(
715
605
  )
716
606
  .map((row) => row.edge);
717
607
  const maxDirNameLen = maxDirNameLength(allEdges);
608
+ const effectiveMaxDirNameLen = opts.globalMaxDirNameWidth ?? maxDirNameLen;
609
+ const maxEdgePrefixWidth =
610
+ opts.globalMaxEdgeTreePrefixWidth ?? maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
611
+ const edgeDirNameWidth = rowDirNameWidth(maxEdgePrefixWidth, effectiveMaxDirNameLen, dirNameGap);
718
612
 
719
613
  const lines: string[] = [];
720
614
 
@@ -734,7 +628,7 @@ export function renderMigrationGraphTree(
734
628
  continue;
735
629
  }
736
630
 
737
- const cellColors = resolveRowLaneColors(row.cells);
631
+ const cellColors = resolveRowArcLaneColors(row.cells);
738
632
  let gutter = row.cells
739
633
  .map((cell, column) =>
740
634
  renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
@@ -758,7 +652,7 @@ export function renderMigrationGraphTree(
758
652
  }
759
653
  const labelColumn =
760
654
  row.kind === 'edge'
761
- ? edgeLabelColumn(row, wideLabelColumn)
655
+ ? maxEdgePrefixWidth
762
656
  : wideLabelColumn !== undefined &&
763
657
  (nodeHasArcDecoration(row) || row.contractHash !== undefined)
764
658
  ? wideLabelColumn
@@ -784,8 +678,10 @@ export function renderMigrationGraphTree(
784
678
  } else if (gutter.length < laneSpan * 2) {
785
679
  gutter = gutter.padEnd(laneSpan * 2, ' ');
786
680
  }
787
- const dirNameWidth = rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
788
- const dataColumn = labelColumn + dirNameWidth;
681
+ const dirNameWidth =
682
+ row.kind === 'edge'
683
+ ? edgeDirNameWidth
684
+ : rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
789
685
  const gutterPad = padVisible(gutter, labelColumn);
790
686
 
791
687
  if (row.kind === 'node') {
@@ -798,24 +694,24 @@ export function renderMigrationGraphTree(
798
694
  )
799
695
  .join('');
800
696
  const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
801
- const overlayNames = overlayNamesForContract(contractHash, opts);
802
- if (overlayNames.length === 0) {
697
+ const overlays = overlayNamesForContract(contractHash, opts);
698
+ if (overlays.markers.length === 0 && overlays.refs.length === 0) {
803
699
  lines.push(trimTrailingWhitespace(emptyGutter));
804
700
  continue;
805
701
  }
806
- const overlay = style.refs(overlayNames);
807
- lines.push(trimTrailingWhitespace(`${padVisible(emptyGutter, dataColumn)}${overlay}`));
702
+ const overlay = formatContractNodeOverlays(style, overlays.markers, overlays.refs);
703
+ lines.push(trimTrailingWhitespace(`${emptyGutter}${' '.repeat(LABEL_GAP)}${overlay}`));
808
704
  continue;
809
705
  }
810
706
  const hashText = style.sourceHash(
811
707
  abbreviateHash(contractHash, hashLength, palette.emptySource),
812
708
  );
813
- const overlayNames = overlayNamesForContract(contractHash, opts);
814
- const overlayPad =
815
- overlayNames.length > 0
816
- ? ' '.repeat(Math.max(0, dataColumn - labelColumn - stringWidth(hashText)))
817
- : '';
818
- const overlay = overlayNames.length > 0 ? style.refs(overlayNames) : '';
709
+ const overlays = overlayNamesForContract(contractHash, opts);
710
+ const hasOverlays = overlays.markers.length > 0 || overlays.refs.length > 0;
711
+ const overlayPad = hasOverlays ? ' '.repeat(LABEL_GAP) : '';
712
+ const overlay = hasOverlays
713
+ ? formatContractNodeOverlays(style, overlays.markers, overlays.refs)
714
+ : '';
819
715
  lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
820
716
  continue;
821
717
  }
@@ -828,7 +724,7 @@ export function renderMigrationGraphTree(
828
724
  // A branched name keeps its bold (via `style.dirName`) and adds the lane
829
725
  // hue, so it reads as one with its lane/arrow; column-0 names stay bold-only.
830
726
  const dirNameStyler =
831
- opts.colorize && laneIndex > NEUTRAL_LANE
727
+ opts.colorize && laneIndex > NEUTRAL_LANE_COLUMN
832
728
  ? (text: string) => forcedBold(laneColorForColumn(laneIndex)(text))
833
729
  : style.dirName;
834
730
  const dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
@@ -845,16 +741,26 @@ export interface RenderMigrationGraphLegendOptions {
845
741
  readonly glyphMode?: GlyphMode;
846
742
  }
847
743
 
744
+ function formatLegendExampleMarkers(colorize: boolean): string {
745
+ if (!colorize) {
746
+ return '<contract, db>';
747
+ }
748
+ const open = green('<');
749
+ const close = green('>');
750
+ const separator = green(', ');
751
+ return open + green('contract') + separator + green('db') + close;
752
+ }
753
+
848
754
  /**
849
- * A compact key for the `--tree` visual language: the contract marker, the
850
- * in-lane direction arrows, the empty baseline, the `(refs)` overlay (including
851
- * the reserved `db` live-database and `contract` working-schema markers), and a
755
+ * A compact key for the tree visual language: the contract node glyph, the
756
+ * in-lane direction arrows, the empty baseline, the system-marker `<…>` and
757
+ * user-ref `(…)` bracket conventions (two illustrative example lines), and a
852
758
  * worked sample of the data-column `from → to` migration hash arrow.
853
759
  *
854
760
  * Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
855
761
  * tree renderer, so the key matches whatever the graph itself drew and stays
856
762
  * pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
857
- * line that separates this stderr key from the graph on stdout.
763
+ * line that separates this stderr key from the tree on stdout.
858
764
  */
859
765
  export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptions): string {
860
766
  const palette = paletteFor(opts.glyphMode ?? 'unicode');
@@ -865,13 +771,17 @@ export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptio
865
771
  const appliedPending = opts.colorize
866
772
  ? ` ${green(statusGlyphs.applied)} ${style.summary('applied')} ${yellow(statusGlyphs.pending)} ${style.summary('pending')}`
867
773
  : ` ${statusGlyphs.applied} ${style.summary('applied')} ${statusGlyphs.pending} ${style.summary('pending')}`;
868
- return [
774
+ const exampleMarkers = formatLegendExampleMarkers(opts.colorize);
775
+ const exampleRefs = opts.colorize ? style.refs(['prod', 'staging']) : '(prod, staging)';
776
+ const lines = [
869
777
  'Legend:',
870
778
  ` ${style.kind(node)} ${style.summary('contract')} ${style.kind(palette.edgeArrow.forward)} ${style.summary('forward')} ${style.kind(palette.edgeArrow.rollback)} ${style.summary('rollback')}`,
871
779
  ` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
872
780
  appliedPending,
873
781
  ` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
874
- ` ${style.refs(['refs'])} ${style.summary(`${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`)}`,
782
+ ` ${exampleMarkers} ${style.summary('live markers (contract on disk, database state)')}`,
783
+ ` ${exampleRefs} ${style.summary('user-defined refs')}`,
875
784
  ` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
876
- ].join('\n');
785
+ ];
786
+ return lines.join('\n');
877
787
  }
@@ -1,3 +1,4 @@
1
+ import stringWidth from 'string-width';
1
2
  import type { GlyphMode } from '../glyph-mode';
2
3
 
3
4
  export const MIGRATION_LIST_HASH_WIDTH = 7;
@@ -20,3 +21,8 @@ export function abbreviateContractHash(hash: string): string {
20
21
  const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
21
22
  return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);
22
23
  }
24
+
25
+ export function padFromHashColumn(text: string, width: number): string {
26
+ const padding = Math.max(0, width - stringWidth(text));
27
+ return `${' '.repeat(padding)}${text}`;
28
+ }