prisma-next 0.12.0-dev.6 → 0.12.0-dev.8

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.
package/dist/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import { createDbUpdateCommand } from "./commands/db-update.mjs";
9
9
  import { t as createDbVerifyCommand } from "./db-verify-v_vUKXTU.mjs";
10
10
  import { createMigrateCommand } from "./commands/migrate.mjs";
11
11
  import { t as createMigrationCheckCommand } from "./migration-check-BiBJoYYW.mjs";
12
- import { t as createMigrationGraphCommand } from "./migration-graph-D7DVUElV.mjs";
12
+ import { t as createMigrationGraphCommand } from "./migration-graph-C9WC-7eO.mjs";
13
13
  import { createMigrationListCommand } from "./commands/migration-list.mjs";
14
14
  import { createMigrationLogCommand } from "./commands/migration-log.mjs";
15
15
  import { createMigrationNewCommand } from "./commands/migration-new.mjs";
@@ -24,7 +24,7 @@ import { fileURLToPath } from "node:url";
24
24
  import { readUserConfig, resolveGating, runTelemetry } from "@prisma-next/cli-telemetry";
25
25
  import { distance } from "closest-match";
26
26
  //#region package.json
27
- var version = "0.12.0-dev.6";
27
+ var version = "0.12.0-dev.8";
28
28
  //#endregion
29
29
  //#region src/utils/telemetry.ts
30
30
  /**
@@ -12,7 +12,25 @@ interface MigrationGraphOptions extends CommonCommandOptions {
12
12
  readonly dot?: boolean;
13
13
  readonly tree?: boolean;
14
14
  readonly ascii?: boolean;
15
+ readonly legend?: boolean;
15
16
  }
17
+ /**
18
+ * `--legend` describes the `--tree` visual language, so passing it auto-enables
19
+ * the tree path (it has nothing to say about the legacy dagre default).
20
+ */
21
+ declare function migrationGraphUsesTree(options: {
22
+ readonly tree?: boolean;
23
+ readonly legend?: boolean;
24
+ }): boolean;
25
+ /**
26
+ * The legend is decoration printed alongside the command header on stderr, so
27
+ * it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
28
+ * `--quiet`) exactly as the header is.
29
+ */
30
+ declare function migrationGraphShowsLegend(options: {
31
+ readonly legend?: boolean;
32
+ readonly dot?: boolean;
33
+ }, flags: GlobalFlags): boolean;
16
34
  interface MigrationGraphResult {
17
35
  readonly ok: true;
18
36
  readonly graph: MigrationGraph;
@@ -23,5 +41,5 @@ interface MigrationGraphResult {
23
41
  declare function executeMigrationGraphCommand(options: MigrationGraphOptions, flags: GlobalFlags, ui: TerminalUI): Promise<Result<MigrationGraphResult, CliStructuredError>>;
24
42
  declare function createMigrationGraphCommand(): Command;
25
43
  //#endregion
26
- export { MigrationGraphResult, createMigrationGraphCommand, executeMigrationGraphCommand };
44
+ export { MigrationGraphResult, createMigrationGraphCommand, executeMigrationGraphCommand, migrationGraphShowsLegend, migrationGraphUsesTree };
27
45
  //# sourceMappingURL=migration-graph.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/commands/migration-graph.ts"],"mappings":";;;;;;;;;UA0BU,qBAAA,SAA8B,oBAAoB;EAAA,SACjD,MAAA;EAAA,SACA,GAAA;EAAA,SACA,IAAA;EAAA,SACA,KAAA;AAAA;AAAA,UAGM,oBAAA;EAAA,SACN,EAAA;EAAA,SACA,KAAA,EAAO,cAAA;EAAA,SACP,YAAA;EAAA,SACA,IAAA,WAAe,SAAS;EAAA,SACxB,OAAA;AAAA;AAAA,iBAGW,4BAAA,CACpB,OAAA,EAAS,qBAAA,EACT,KAAA,EAAO,WAAA,EACP,EAAA,EAAI,UAAA,GACH,OAAA,CAAQ,MAAA,CAAO,oBAAA,EAAsB,kBAAA;AAAA,iBA0CxB,2BAAA,CAAA,GAA+B,OAAO"}
1
+ {"version":3,"file":"migration-graph.d.mts","names":[],"sources":["../../src/commands/migration-graph.ts"],"mappings":";;;;;;;;;UA6BU,qBAAA,SAA8B,oBAAoB;EAAA,SACjD,MAAA;EAAA,SACA,GAAA;EAAA,SACA,IAAA;EAAA,SACA,KAAA;EAAA,SACA,MAAA;AAAA;;;;;iBAOK,sBAAA,CAAuB,OAAA;EAAA,SAC5B,IAAA;EAAA,SACA,MAAA;AAAA;;;;;;iBAUK,yBAAA,CACd,OAAA;EAAA,SAAoB,MAAA;EAAA,SAA2B,GAAA;AAAA,GAC/C,KAAA,EAAO,WAAW;AAAA,UAOH,oBAAA;EAAA,SACN,EAAA;EAAA,SACA,KAAA,EAAO,cAAA;EAAA,SACP,YAAA;EAAA,SACA,IAAA,WAAe,SAAS;EAAA,SACxB,OAAA;AAAA;AAAA,iBAGW,4BAAA,CACpB,OAAA,EAAS,qBAAA,EACT,KAAA,EAAO,WAAA,EACP,EAAA,EAAI,UAAA,GACH,OAAA,CAAQ,MAAA,CAAO,oBAAA,EAAsB,kBAAA;AAAA,iBAoDxB,2BAAA,CAAA,GAA+B,OAAO"}
@@ -1,2 +1,2 @@
1
- import { n as executeMigrationGraphCommand, t as createMigrationGraphCommand } from "../migration-graph-D7DVUElV.mjs";
2
- export { createMigrationGraphCommand, executeMigrationGraphCommand };
1
+ import { i as migrationGraphUsesTree, n as executeMigrationGraphCommand, r as migrationGraphShowsLegend, t as createMigrationGraphCommand } from "../migration-graph-C9WC-7eO.mjs";
2
+ export { createMigrationGraphCommand, executeMigrationGraphCommand, migrationGraphShowsLegend, migrationGraphUsesTree };
@@ -5,7 +5,7 @@ import { i as migrationGraphToRenderInput, n as graphRenderer } from "./graph-re
5
5
  import { a as migrationListEmptySource, n as createAnsiMigrationListStyler, o as migrationListForwardArrow, s as classifyMigrationGraphTopology, t as CONTRACT_MARKER_NAME } from "./migration-list-styler-BRwF4-gy.mjs";
6
6
  import { Command } from "commander";
7
7
  import { ok } from "@prisma-next/utils/result";
8
- import { bold } from "colorette";
8
+ import { bold, createColors } from "colorette";
9
9
  import stringWidth from "string-width";
10
10
  import { EMPTY_CONTRACT_HASH } from "@prisma-next/migration-tools/constants";
11
11
  //#region src/utils/formatters/migration-graph-layout.ts
@@ -171,7 +171,7 @@ function classifyEdgeAdjacency(edge, position) {
171
171
  function emptyCells(width) {
172
172
  return Array.from({ length: width }, () => ({ kind: "empty" }));
173
173
  }
174
- function buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth) {
174
+ function buildBranchConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth) {
175
175
  const cells = emptyCells(gridWidth);
176
176
  for (let lane = 0; lane < gridWidth; lane++) {
177
177
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
@@ -180,11 +180,13 @@ function buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth) {
180
180
  }
181
181
  if (lane === startLane) cells[lane] = { kind: "branch-tee" };
182
182
  else if (lane === endLane) cells[lane] = { kind: "branch-corner" };
183
- else if (lane > startLane && lane < endLane) cells[lane] = { kind: "branch-tee" };
183
+ else if (lane > startLane && lane < endLane) if (fanTargetLanes.has(lane)) cells[lane] = { kind: "branch-tee" };
184
+ else if (activeLanes.has(lane)) cells[lane] = { kind: "arc-crossing" };
185
+ else cells[lane] = { kind: "branch-tee" };
184
186
  }
185
187
  return cells;
186
188
  }
187
- function buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth) {
189
+ function buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth) {
188
190
  const cells = emptyCells(gridWidth);
189
191
  for (let lane = 0; lane < gridWidth; lane++) {
190
192
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
@@ -193,7 +195,9 @@ function buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth) {
193
195
  }
194
196
  if (lane === startLane) cells[lane] = { kind: "merge-tee" };
195
197
  else if (lane === endLane) cells[lane] = { kind: "merge-corner" };
196
- else if (lane > startLane && lane < endLane) cells[lane] = activeLanes.has(lane) ? { kind: "merge-tee" } : { kind: "horizontal-pass" };
198
+ else if (lane > startLane && lane < endLane) if (fanTargetLanes.has(lane)) cells[lane] = { kind: "merge-tee" };
199
+ else if (activeLanes.has(lane)) cells[lane] = { kind: "arc-crossing" };
200
+ else cells[lane] = { kind: "horizontal-pass" };
197
201
  }
198
202
  return cells;
199
203
  }
@@ -504,18 +508,19 @@ function layoutComponent(componentNodes, allEdges) {
504
508
  const endLane = Math.max(...laneIndices);
505
509
  ensureGridWidth(endLane + 1);
506
510
  const activeLanes = new Set(activeLaneIndices());
511
+ const fanTargetLanes = new Set(laneIndices);
507
512
  rows.push({
508
513
  kind: "merge-connector",
509
514
  contractHash,
510
515
  startLane,
511
516
  endLane,
512
517
  branchCount: laneIndices.length,
513
- cells: buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth)
518
+ cells: buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth)
514
519
  });
515
520
  for (const index of laneIndices) if (index !== startLane) setLane(index, null);
516
521
  return startLane;
517
522
  }
518
- function emitBranchConnector(contractHash, startLane, endLane, branchCount) {
523
+ function emitBranchConnector(contractHash, startLane, endLane, branchCount, fanTargetLanes) {
519
524
  ensureGridWidth(endLane + 1);
520
525
  const activeLanes = new Set(activeLaneIndices());
521
526
  rows.push({
@@ -524,7 +529,7 @@ function layoutComponent(componentNodes, allEdges) {
524
529
  startLane,
525
530
  endLane,
526
531
  branchCount,
527
- cells: buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth)
532
+ cells: buildBranchConnectorCells(startLane, endLane, new Set(fanTargetLanes), activeLanes, gridWidth)
528
533
  });
529
534
  }
530
535
  function emitEdgeRow(edge, lane, convergenceProducer) {
@@ -597,7 +602,7 @@ function layoutComponent(componentNodes, allEdges) {
597
602
  }
598
603
  if (groups.length >= 2) {
599
604
  const endLane = Math.max(...laneForGroup);
600
- emitBranchConnector(node, column, endLane, groups.length);
605
+ emitBranchConnector(node, column, endLane, groups.length, laneForGroup);
601
606
  }
602
607
  for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
603
608
  const group = groups[groupIndex];
@@ -835,6 +840,30 @@ function buildMigrationGraphRows(graph, options = {}) {
835
840
  };
836
841
  }
837
842
  //#endregion
843
+ //#region src/utils/formatters/migration-graph-lane-colors.ts
844
+ const { magenta: magenta$1, cyan: cyan$1, green: green$1, yellow: yellow$1, blueBright, red: red$1 } = createColors({ useColor: true });
845
+ const LANE_COLOR_CYCLE = [
846
+ magenta$1,
847
+ cyan$1,
848
+ green$1,
849
+ yellow$1,
850
+ blueBright,
851
+ red$1
852
+ ];
853
+ /**
854
+ * The hue for a gutter column. The leftmost lane (column 0) is **neutral** — it
855
+ * has nothing to be told apart from in the common single-lane linear case, so
856
+ * the renderer dims it rather than tinting it; the rotating palette is reserved
857
+ * for columns ≥ 1 (where a second lane exists to distinguish). Callers must dim
858
+ * column 0 themselves; this returns identity for it as a guard. A lane freed and
859
+ * reused by a later branch keeps its column's hue — coloring is by position, not
860
+ * branch identity, exactly like `git log --graph`.
861
+ */
862
+ function laneColorForColumn(column) {
863
+ if (column <= 0) return (text) => text;
864
+ return LANE_COLOR_CYCLE[(column - 1) % LANE_COLOR_CYCLE.length] ?? ((text) => text);
865
+ }
866
+ //#endregion
838
867
  //#region src/utils/formatters/migration-graph-tree-render.ts
839
868
  const LABEL_GAP = 2;
840
869
  /**
@@ -902,74 +931,233 @@ function arrowForEdgeKind(kind, palette) {
902
931
  return palette.edgeArrow[kind];
903
932
  }
904
933
  /**
934
+ * The leftmost lane (column 0) renders with the neutral dim lane style rather
935
+ * than a palette hue — in the common single-lane case it has nothing to be told
936
+ * apart from. Used as the "no owning arc" sentinel during colour resolution.
937
+ */
938
+ const NEUTRAL_LANE = 0;
939
+ /**
940
+ * Forced bold for branch-coloured names. A branched name pairs its lane hue
941
+ * (also forced, via {@link laneColorForColumn}) with bold; both must emit even
942
+ * when colorette's ambient TTY detection is off, so the colorized branch name
943
+ * is deterministically bold + hue rather than hue-only.
944
+ */
945
+ const { bold: forcedBold } = createColors({ useColor: true });
946
+ /**
947
+ * Resolve per-cell colour columns for a row. Scanning right-to-left lets each
948
+ * arc bridge inherit the corner column that closes it (the arc's back-lane), so
949
+ * the whole arc — vertical run (already its own column), horizontal bridges,
950
+ * corners, crossings, and the `◂`/`─` connector — reads as a single continuous
951
+ * hue. A crossing can only be one colour, so rather than leave it dim (wrong for
952
+ * both crossing lines) it takes the arc owning the horizontal run at this row
953
+ * (the nearest corner to its right); the crossed vertical lane is simply
954
+ * occluded at that one cell and reappears on the next row.
955
+ */
956
+ function resolveRowLaneColors(cells) {
957
+ const lane = new Array(cells.length);
958
+ const connector = new Array(cells.length);
959
+ let arcCorner = NEUTRAL_LANE;
960
+ for (let column = cells.length - 1; column >= 0; column--) {
961
+ const cell = cells[column];
962
+ connector[column] = arcCorner;
963
+ switch (cell?.kind) {
964
+ case "arc-branch-corner":
965
+ case "arc-land-corner":
966
+ arcCorner = column;
967
+ lane[column] = column;
968
+ break;
969
+ case "arc-branch-tee":
970
+ lane[column] = column;
971
+ break;
972
+ case "arc-crossing":
973
+ case "arc-land-bridge":
974
+ lane[column] = arcCorner;
975
+ break;
976
+ case "horizontal-pass":
977
+ lane[column] = arcCorner === NEUTRAL_LANE ? column : arcCorner;
978
+ break;
979
+ case "node":
980
+ lane[column] = column;
981
+ arcCorner = NEUTRAL_LANE;
982
+ break;
983
+ default:
984
+ lane[column] = column;
985
+ arcCorner = NEUTRAL_LANE;
986
+ }
987
+ }
988
+ return {
989
+ lane,
990
+ connector
991
+ };
992
+ }
993
+ /**
994
+ * Resolve per-cell connector colours. Scanning right-to-left, a corner or an
995
+ * intermediate tee anchors its own lane (its junction glyph takes that column),
996
+ * but a tee's **trailing dash leads into the branch on its right** (the next
997
+ * branch point), so `┬─` reads as "this lane, then on toward the next" rather
998
+ * than tinting the dash with the left lane. The leading tee at `startLane` (the
999
+ * fork/merge origin) and pure horizontal segments inherit the nearest branch
1000
+ * point to their right whole-cell, so the run into a branch — or collapsing
1001
+ * into a merge corner — stays continuous. Pass-through verticals outside the
1002
+ * run keep their own column (column 0 stays neutral).
1003
+ */
1004
+ function resolveConnectorLaneColors(cells, startLane) {
1005
+ const glyph = new Array(cells.length);
1006
+ const dash = new Array(cells.length);
1007
+ let owner = NEUTRAL_LANE;
1008
+ for (let column = cells.length - 1; column >= 0; column--) switch (cells[column]?.kind) {
1009
+ case "branch-corner":
1010
+ case "merge-corner":
1011
+ owner = column;
1012
+ glyph[column] = column;
1013
+ dash[column] = column;
1014
+ break;
1015
+ case "branch-tee":
1016
+ case "merge-tee":
1017
+ if (column === startLane) {
1018
+ const served = owner === NEUTRAL_LANE ? column : owner;
1019
+ glyph[column] = column;
1020
+ dash[column] = served;
1021
+ } else {
1022
+ dash[column] = owner === NEUTRAL_LANE ? column : owner;
1023
+ glyph[column] = column;
1024
+ owner = column;
1025
+ }
1026
+ break;
1027
+ case "arc-crossing":
1028
+ glyph[column] = column;
1029
+ dash[column] = column;
1030
+ break;
1031
+ case "horizontal-pass": {
1032
+ const served = owner === NEUTRAL_LANE ? column : owner;
1033
+ glyph[column] = served;
1034
+ dash[column] = served;
1035
+ break;
1036
+ }
1037
+ default:
1038
+ glyph[column] = column;
1039
+ dash[column] = column;
1040
+ }
1041
+ return {
1042
+ glyph,
1043
+ dash
1044
+ };
1045
+ }
1046
+ /**
1047
+ * Style a structural glyph by its resolved colour column. Column 0 and the
1048
+ * neutral sentinel render dim (`style.lane`); columns ≥ 1 take a palette hue.
1049
+ */
1050
+ function laneStylerForColumn(colorColumn, colorize, style) {
1051
+ if (!colorize || colorColumn <= NEUTRAL_LANE) return (text) => style.lane(text);
1052
+ return laneColorForColumn(colorColumn);
1053
+ }
1054
+ /**
1055
+ * Tint a branch-owned token (direction arrow, migration name) by its edge's
1056
+ * lane so the whole branch row reads in one colour. Column 0 has nothing to be
1057
+ * told apart from in the common linear chain, so it keeps the token's existing
1058
+ * default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
1059
+ * colour. With colour off, the fallback (also colourless) is used unchanged.
1060
+ */
1061
+ function branchStylerOrDefault(column, colorize, fallback) {
1062
+ if (!colorize || column <= NEUTRAL_LANE) return fallback;
1063
+ return laneColorForColumn(column);
1064
+ }
1065
+ /**
1066
+ * Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
1067
+ * trailing dash coloured independently: the junction anchors its own lane while
1068
+ * the dash leads into the branch on its right.
1069
+ */
1070
+ function renderConnectorTee(pair, glyphColumn, dashColumn, colorize, style) {
1071
+ const glyph = laneStylerForColumn(glyphColumn, colorize, style);
1072
+ if (glyphColumn === dashColumn) return glyph(pair);
1073
+ return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
1074
+ }
1075
+ /**
905
1076
  * A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
906
- * marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`).
907
- * The marker is the signal and stays bright (`style.kind`); the connector is
908
- * gutter and stays dim (`style.lane`) consistent with the plain node marker,
909
- * which is never dimmed.
1077
+ * marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
1078
+ * marker takes its own lane's hue (so each node visibly belongs to its branch);
1079
+ * the connector follows the arc it belongs to (its owning back-lane hue).
1080
+ * Direction arrows are handled elsewhere — they take their edge's lane hue too.
910
1081
  */
911
- function renderNodeMarkerPair(pair, style) {
912
- return style.kind(pair.slice(0, 1)) + style.lane(pair.slice(1));
1082
+ function renderNodeMarkerPair(pair, nodeColumn, arcColumn, colorize, style) {
1083
+ const marker = laneStylerForColumn(nodeColumn, colorize, style);
1084
+ const connector = laneStylerForColumn(arcColumn, colorize, style);
1085
+ return marker(pair.slice(0, 1)) + connector(pair.slice(1));
913
1086
  }
914
- function renderCellPair(cell, style, palette) {
1087
+ function renderCellPair(cell, column, colors, colorize, style, palette) {
1088
+ const lane = laneStylerForColumn(colors.lane[column] ?? column, colorize, style);
915
1089
  switch (cell.kind) {
916
- case "node":
917
- if (cell.arcLand === true) return renderNodeMarkerPair(palette.arcLand, style);
918
- if (cell.arcTee === true) return renderNodeMarkerPair(palette.arcTee, style);
919
- return style.kind(palette.node);
920
- case "vertical-pass": return style.lane(palette.verticalPass);
921
- case "edge-lane": return cell.ownsLabel ? style.lane(palette.verticalPass.trimEnd()) + style.kind(arrowForEdgeKind(cell.edgeKind, palette)) : style.lane(palette.verticalPass);
922
- case "branch-tee": return style.lane(palette.branchTee);
923
- case "merge-tee": return style.lane(palette.mergeTee);
924
- case "branch-corner": return style.lane(palette.branchCorner);
925
- case "merge-corner": return style.lane(palette.mergeCorner);
926
- case "arc-branch-corner": return style.lane(palette.arcBranchCorner);
927
- case "arc-branch-tee": return style.lane(palette.arcBranchTee);
928
- case "arc-land-corner": return style.lane(palette.arcLandCorner);
929
- case "arc-crossing": return style.lane(palette.arcCrossing);
930
- case "arc-land-bridge": return style.lane(palette.arcLandBridge);
931
- case "horizontal-pass": return style.lane(palette.horizontalPass);
1090
+ case "node": {
1091
+ const arcColumn = colors.connector[column] ?? NEUTRAL_LANE;
1092
+ if (cell.arcLand === true) return renderNodeMarkerPair(palette.arcLand, column, arcColumn, colorize, style);
1093
+ if (cell.arcTee === true) return renderNodeMarkerPair(palette.arcTee, column, arcColumn, colorize, style);
1094
+ return lane(palette.node);
1095
+ }
1096
+ case "vertical-pass": return lane(palette.verticalPass);
1097
+ case "edge-lane": return cell.ownsLabel ? lane(palette.verticalPass.trimEnd()) + branchStylerOrDefault(column, colorize, style.kind)(arrowForEdgeKind(cell.edgeKind, palette)) : lane(palette.verticalPass);
1098
+ case "branch-tee": return lane(palette.branchTee);
1099
+ case "merge-tee": return lane(palette.mergeTee);
1100
+ case "branch-corner": return lane(palette.branchCorner);
1101
+ case "merge-corner": return lane(palette.mergeCorner);
1102
+ case "arc-branch-corner": return lane(palette.arcBranchCorner);
1103
+ case "arc-branch-tee": return lane(palette.arcBranchTee);
1104
+ case "arc-land-corner": return lane(palette.arcLandCorner);
1105
+ case "arc-crossing": return lane(palette.arcLandBridge);
1106
+ case "arc-land-bridge": return lane(palette.arcLandBridge);
1107
+ case "horizontal-pass": return lane(palette.horizontalPass);
932
1108
  case "empty": return " ";
933
1109
  }
934
1110
  }
935
- function renderConnectorRow(row, gridWidth, style, palette) {
1111
+ function renderConnectorRow(row, gridWidth, colorize, style, palette) {
936
1112
  const isMerge = row.kind === "merge-connector";
937
1113
  if (row.cells.length > 0) {
1114
+ const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
938
1115
  let seenTee = false;
939
1116
  let out = "";
940
- for (const cell of row.cells) switch (cell.kind) {
941
- case "branch-tee":
942
- out += style.lane(seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee);
943
- seenTee = true;
944
- break;
945
- case "merge-tee":
946
- out += style.lane(seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee);
947
- seenTee = true;
948
- break;
949
- case "branch-corner":
950
- out += style.lane(palette.branchCorner);
951
- break;
952
- case "merge-corner":
953
- out += style.lane(palette.mergeCorner);
954
- break;
955
- case "vertical-pass":
956
- out += style.lane(palette.verticalPass);
957
- break;
958
- case "horizontal-pass":
959
- out += style.lane(palette.horizontalPass);
960
- break;
961
- default: out += " ";
1117
+ for (let column = 0; column < row.cells.length; column++) {
1118
+ const cell = row.cells[column];
1119
+ if (cell === void 0) continue;
1120
+ const glyphColumn = colors.glyph[column] ?? column;
1121
+ const dashColumn = colors.dash[column] ?? glyphColumn;
1122
+ const lane = laneStylerForColumn(glyphColumn, colorize, style);
1123
+ switch (cell.kind) {
1124
+ case "branch-tee":
1125
+ out += renderConnectorTee(seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee, glyphColumn, dashColumn, colorize, style);
1126
+ seenTee = true;
1127
+ break;
1128
+ case "merge-tee":
1129
+ out += renderConnectorTee(seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee, glyphColumn, dashColumn, colorize, style);
1130
+ seenTee = true;
1131
+ break;
1132
+ case "branch-corner":
1133
+ out += lane(palette.branchCorner);
1134
+ break;
1135
+ case "merge-corner":
1136
+ out += lane(palette.mergeCorner);
1137
+ break;
1138
+ case "vertical-pass":
1139
+ out += lane(palette.verticalPass);
1140
+ break;
1141
+ case "horizontal-pass":
1142
+ out += lane(palette.horizontalPass);
1143
+ break;
1144
+ case "arc-crossing":
1145
+ out += renderConnectorTee(palette.arcCrossing, glyphColumn, dashColumn, colorize, style);
1146
+ break;
1147
+ default: out += " ";
1148
+ }
962
1149
  }
963
1150
  for (let column = row.cells.length; column < gridWidth; column++) out += " ";
964
1151
  return out;
965
1152
  }
966
1153
  const start = row.startLane ?? 0;
967
1154
  const end = row.endLane ?? start;
1155
+ const runLane = laneStylerForColumn(end, colorize, style);
968
1156
  let out = "";
969
1157
  for (let column = 0; column < gridWidth; column++) if (column < start || column > end) out += " ";
970
- else if (column === start) out += style.lane(palette.connectorBranchTee);
971
- else if (column === end) out += style.lane(isMerge ? palette.mergeCorner : palette.branchCorner);
972
- else out += style.lane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
1158
+ else if (column === start) out += runLane(palette.connectorBranchTee);
1159
+ else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
1160
+ else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
973
1161
  return out;
974
1162
  }
975
1163
  function abbreviateHash(hash, hashLength, emptySource) {
@@ -1008,6 +1196,11 @@ function padVisible(text, targetWidth) {
1008
1196
  const padding = Math.max(0, targetWidth - stringWidth(text));
1009
1197
  return text + " ".repeat(padding);
1010
1198
  }
1199
+ const ANSI_ESCAPE = "\x1B";
1200
+ function trimTrailingWhitespace(line) {
1201
+ const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
1202
+ return line.replace(trailingSpaceBeforeReset, "$1").replace(/\s+$/, "");
1203
+ }
1011
1204
  function gridWidthForModel(rows) {
1012
1205
  return rows.reduce((max, row) => row.kind === "node" || row.kind === "edge" ? Math.max(max, row.cells.length) : max, 1);
1013
1206
  }
@@ -1047,20 +1240,24 @@ function renderMigrationGraphTree(model, opts) {
1047
1240
  continue;
1048
1241
  }
1049
1242
  if (row.kind === "branch-connector" || row.kind === "merge-connector") {
1050
- lines.push(renderConnectorRow(row, gridWidth, style, palette).replace(/\s+$/, ""));
1243
+ lines.push(trimTrailingWhitespace(renderConnectorRow(row, gridWidth, opts.colorize, style, palette)));
1051
1244
  continue;
1052
1245
  }
1053
- let gutter = row.cells.map((cell) => renderCellPair(cell, style, palette)).join("");
1054
- const prevRow = model.rows[rowIndex - 1];
1246
+ const cellColors = resolveRowLaneColors(row.cells);
1247
+ let gutter = row.cells.map((cell, column) => renderCellPair(cell, column, cellColors, opts.colorize, style, palette)).join("");
1055
1248
  let laneSpan = row.cells.length;
1056
- if (row.kind === "node") {
1057
- const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
1058
- if (prevRow?.kind === "merge-connector" || contractHash === EMPTY_CONTRACT_HASH) laneSpan = 1;
1059
- else laneSpan = row.cells.length;
1249
+ if (row.kind === "node") if ((row.contractHash ?? EMPTY_CONTRACT_HASH) === EMPTY_CONTRACT_HASH) laneSpan = 1;
1250
+ else {
1251
+ let lastActiveColumn = -1;
1252
+ for (let column = row.cells.length - 1; column >= 0; column--) if (row.cells[column]?.kind !== "empty") {
1253
+ lastActiveColumn = column;
1254
+ break;
1255
+ }
1256
+ laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
1060
1257
  }
1061
1258
  const labelColumn = row.kind === "edge" ? edgeLabelColumn(row, wideLabelColumn) : wideLabelColumn !== void 0 && (nodeHasArcDecoration(row) || row.contractHash !== void 0) ? wideLabelColumn : laneSpan * 2 + LABEL_GAP;
1062
- if (row.kind === "edge" && row.edge?.from === EMPTY_CONTRACT_HASH && (row.laneIndex ?? 0) === 0) gutter = row.cells.slice(0, 1).map((cell) => renderCellPair(cell, style, palette)).join("");
1063
- else if (row.kind === "node" && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) gutter = row.cells.slice(0, laneSpan).map((cell) => renderCellPair(cell, style, palette)).join("");
1259
+ if (row.kind === "edge" && row.edge?.from === EMPTY_CONTRACT_HASH && (row.laneIndex ?? 0) === 0) gutter = row.cells.slice(0, 1).map((cell, column) => renderCellPair(cell, column, cellColors, opts.colorize, style, palette)).join("");
1260
+ else if (row.kind === "node" && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) gutter = row.cells.slice(0, laneSpan).map((cell, column) => renderCellPair(cell, column, cellColors, opts.colorize, style, palette)).join("");
1064
1261
  else if (gutter.length < laneSpan * 2) gutter = gutter.padEnd(laneSpan * 2, " ");
1065
1262
  const dirNameWidth = rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
1066
1263
  const dataColumn = labelColumn + dirNameWidth;
@@ -1068,35 +1265,76 @@ function renderMigrationGraphTree(model, opts) {
1068
1265
  if (row.kind === "node") {
1069
1266
  const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
1070
1267
  if (contractHash === EMPTY_CONTRACT_HASH) {
1071
- const trailingLanes = row.cells.slice(1).map((cell) => renderCellPair(cell, style, palette)).join("");
1268
+ const trailingLanes = row.cells.slice(1).map((cell, offset) => renderCellPair(cell, offset + 1, cellColors, opts.colorize, style, palette)).join("");
1072
1269
  const emptyGutter = palette.emptySource.padEnd(2, " ") + trailingLanes;
1073
1270
  const overlayNames = overlayNamesForContract(contractHash, opts);
1074
1271
  if (overlayNames.length === 0) {
1075
- lines.push(emptyGutter.replace(/\s+$/, ""));
1272
+ lines.push(trimTrailingWhitespace(emptyGutter));
1076
1273
  continue;
1077
1274
  }
1078
1275
  const overlay = style.refs(overlayNames);
1079
- lines.push(`${padVisible(emptyGutter, dataColumn)}${overlay}`.replace(/\s+$/, ""));
1276
+ lines.push(trimTrailingWhitespace(`${padVisible(emptyGutter, dataColumn)}${overlay}`));
1080
1277
  continue;
1081
1278
  }
1082
1279
  const hashText = style.sourceHash(abbreviateHash(contractHash, hashLength, palette.emptySource));
1083
1280
  const overlayNames = overlayNamesForContract(contractHash, opts);
1084
1281
  const overlayPad = overlayNames.length > 0 ? " ".repeat(Math.max(0, dataColumn - labelColumn - stringWidth(hashText))) : "";
1085
1282
  const overlay = overlayNames.length > 0 ? style.refs(overlayNames) : "";
1086
- lines.push(`${gutterPad}${hashText}${overlayPad}${overlay}`.replace(/\s+$/, ""));
1283
+ lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
1087
1284
  continue;
1088
1285
  }
1089
1286
  const edge = row.edge;
1090
1287
  if (edge === void 0) continue;
1091
1288
  const dirNamePadding = " ".repeat(Math.max(0, dirNameWidth - edge.dirName.length));
1092
- const dirName = `${style.dirName(edge.dirName)}${dirNamePadding}`;
1289
+ const laneIndex = row.laneIndex ?? 0;
1290
+ const dirName = `${(opts.colorize && laneIndex > NEUTRAL_LANE ? (text) => forcedBold(laneColorForColumn(laneIndex)(text)) : style.dirName)(edge.dirName)}${dirNamePadding}`;
1093
1291
  const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette);
1094
- lines.push(`${gutterPad}${dirName}${hashColumn}`.replace(/\s+$/, ""));
1292
+ lines.push(trimTrailingWhitespace(`${gutterPad}${dirName}${hashColumn}`));
1095
1293
  }
1096
1294
  return lines.join("\n");
1097
1295
  }
1296
+ /**
1297
+ * A compact key for the `--tree` visual language: the contract marker, the
1298
+ * in-lane direction arrows, the empty baseline, the `(refs)` overlay (including
1299
+ * the reserved `db` live-database and `contract` working-schema markers), and a
1300
+ * worked sample of the data-column `from → to` migration hash arrow.
1301
+ *
1302
+ * Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
1303
+ * tree renderer, so the key matches whatever the graph itself drew and stays
1304
+ * pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
1305
+ * line that separates this stderr key from the graph on stdout.
1306
+ */
1307
+ function renderMigrationGraphLegend(opts) {
1308
+ const palette = paletteFor(opts.glyphMode ?? "unicode");
1309
+ const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
1310
+ const node = palette.node.trimEnd();
1311
+ const sampleArrow = `${style.sourceHash("aaaaaa")} ${style.glyph(palette.forwardArrow)} ${style.destHash("bbbbbb")}`;
1312
+ return [
1313
+ "Legend:",
1314
+ ` ${style.kind(node)} contract ${style.kind(palette.edgeArrow.forward)} forward ${style.kind(palette.edgeArrow.rollback)} rollback`,
1315
+ ` ${style.kind(palette.edgeArrow.self)} migration without schema change`,
1316
+ ` ${style.glyph(palette.emptySource)} empty database (baseline)`,
1317
+ ` ${style.refs(["refs"])} ${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`,
1318
+ ` ${sampleArrow} migration from contract aaaaaa to bbbbbb`
1319
+ ].join("\n");
1320
+ }
1098
1321
  //#endregion
1099
1322
  //#region src/commands/migration-graph.ts
1323
+ /**
1324
+ * `--legend` describes the `--tree` visual language, so passing it auto-enables
1325
+ * the tree path (it has nothing to say about the legacy dagre default).
1326
+ */
1327
+ function migrationGraphUsesTree(options) {
1328
+ return options.tree === true || options.legend === true;
1329
+ }
1330
+ /**
1331
+ * The legend is decoration printed alongside the command header on stderr, so
1332
+ * it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
1333
+ * `--quiet`) exactly as the header is.
1334
+ */
1335
+ function migrationGraphShowsLegend(options, flags) {
1336
+ return options.legend === true && options.dot !== true && flags.json !== true && flags.quiet !== true;
1337
+ }
1100
1338
  async function executeMigrationGraphCommand(options, flags, ui) {
1101
1339
  const config = await loadConfig(options.config);
1102
1340
  const { configPath, appMigrationsRelative, migrationsDir } = resolveMigrationPaths(options.config, config);
@@ -1114,6 +1352,13 @@ async function executeMigrationGraphCommand(options, flags, ui) {
1114
1352
  flags
1115
1353
  });
1116
1354
  ui.stderr(header);
1355
+ if (migrationGraphShowsLegend(options, flags)) {
1356
+ ui.stderr(renderMigrationGraphLegend({
1357
+ colorize: flags.color !== false,
1358
+ glyphMode: ui.resolveGlyphMode(options.ascii === true)
1359
+ }));
1360
+ ui.stderr("");
1361
+ }
1117
1362
  }
1118
1363
  const loaded = await buildReadAggregate(config, { migrationsDir });
1119
1364
  if (!loaded.ok) return loaded;
@@ -1139,7 +1384,8 @@ function createMigrationGraphCommand() {
1139
1384
  "prisma-next migration graph --json",
1140
1385
  "prisma-next migration graph --dot",
1141
1386
  "prisma-next migration graph --tree",
1142
- "prisma-next migration graph --tree --ascii"
1387
+ "prisma-next migration graph --tree --ascii",
1388
+ "prisma-next migration graph --legend"
1143
1389
  ]);
1144
1390
  setCommandSeeAlso(command, [
1145
1391
  {
@@ -1159,7 +1405,7 @@ function createMigrationGraphCommand() {
1159
1405
  oneLiner: "Display migration package contents"
1160
1406
  }
1161
1407
  ]);
1162
- addGlobalOptions(command).option("--config <path>", "Path to prisma-next.config.ts").option("--dot", "Output in Graphviz DOT format").option("--tree", "Experimental condensed annotated tree renderer").option("--ascii", "Use ASCII glyphs for --tree (pipe-friendly)").action(async (options) => {
1408
+ addGlobalOptions(command).option("--config <path>", "Path to prisma-next.config.ts").option("--dot", "Output in Graphviz DOT format").option("--tree", "Experimental condensed annotated tree renderer").option("--ascii", "Use ASCII glyphs for --tree (pipe-friendly)").option("--legend", "Print a key for the --tree glyphs and lane colors (implies --tree)").action(async (options) => {
1163
1409
  const flags = parseGlobalFlagsOrExit(options);
1164
1410
  const ui = createTerminalUI(flags);
1165
1411
  const exitCode = handleResult(await executeMigrationGraphCommand(options, flags, ui), flags, ui, (graphResult) => {
@@ -1186,7 +1432,7 @@ function createMigrationGraphCommand() {
1186
1432
  edges,
1187
1433
  summary: graphResult.summary
1188
1434
  }, null, 2));
1189
- } else if (!flags.quiet) if (options.tree) {
1435
+ } else if (!flags.quiet) if (migrationGraphUsesTree(options)) {
1190
1436
  const refsByHash = /* @__PURE__ */ new Map();
1191
1437
  for (const ref of graphResult.refs) {
1192
1438
  const existing = refsByHash.get(ref.hash);
@@ -1227,6 +1473,6 @@ function createMigrationGraphCommand() {
1227
1473
  return command;
1228
1474
  }
1229
1475
  //#endregion
1230
- export { executeMigrationGraphCommand as n, createMigrationGraphCommand as t };
1476
+ export { migrationGraphUsesTree as i, executeMigrationGraphCommand as n, migrationGraphShowsLegend as r, createMigrationGraphCommand as t };
1231
1477
 
1232
- //# sourceMappingURL=migration-graph-D7DVUElV.mjs.map
1478
+ //# sourceMappingURL=migration-graph-C9WC-7eO.mjs.map