@prisma-next/cli 0.12.0-dev.6 → 0.12.0-dev.61
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/README.md +2 -2
- package/dist/cli.mjs +180 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-ROablRVC.mjs} +198 -105
- package/dist/client-ROablRVC.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
- package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +4 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +6 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +10 -7
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +37 -3
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +292 -12
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +55 -13
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +3 -2
- package/dist/commands/migration-graph.d.mts +17 -8
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +185 -2
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +26 -27
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +2 -190
- package/dist/commands/migration-log.d.mts +9 -19
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +4 -4
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +17 -21
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +24 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +42 -144
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +4 -4
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
- package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
- package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
- package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
- package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-Bsp46T8u.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-Bsp46T8u.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-CMKyBJZH.mjs} +6 -6
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-CMKyBJZH.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +2 -2
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +1 -3
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
- package/dist/init-0HwB-Vh8.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-CDXkYGh0.mjs} +5 -5
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-CDXkYGh0.mjs.map} +1 -1
- package/dist/migration-check-VwM8xCZV.mjs +574 -0
- package/dist/migration-check-VwM8xCZV.mjs.map +1 -0
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-BC3X6KBg.mjs} +5 -5
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-BC3X6KBg.mjs.map} +1 -1
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs +2370 -0
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs.map +1 -0
- package/dist/migration-list-CyLslAtv.mjs +230 -0
- package/dist/migration-list-CyLslAtv.mjs.map +1 -0
- package/dist/migration-log-DvC-Iq_k.mjs +222 -0
- package/dist/migration-log-DvC-Iq_k.mjs.map +1 -0
- package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
- package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-DUBRTJEl.mjs} +7 -7
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-DUBRTJEl.mjs.map} +1 -1
- package/dist/migration-status-DnEW9YQn.mjs +447 -0
- package/dist/migration-status-DnEW9YQn.mjs.map +1 -0
- package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
- package/dist/schemas-CeGMYFYX.d.mts +191 -0
- package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
- package/dist/schemas-KhXMzNA_.mjs +112 -0
- package/dist/schemas-KhXMzNA_.mjs.map +1 -0
- package/dist/telemetry-BIM4beEO.mjs +122 -0
- package/dist/telemetry-BIM4beEO.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
- package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
- package/dist/{types-Dt_SfqFm.d.mts → types-BepB6ydp.d.mts} +44 -31
- package/dist/types-BepB6ydp.d.mts.map +1 -0
- package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
- package/package.json +25 -22
- package/src/cli.ts +5 -0
- package/src/commands/db-update.ts +7 -1
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/json/schemas.ts +195 -0
- package/src/commands/migrate.ts +518 -8
- package/src/commands/migration-check.ts +469 -134
- package/src/commands/migration-graph.ts +164 -91
- package/src/commands/migration-list.ts +72 -39
- package/src/commands/migration-log.ts +52 -102
- package/src/commands/migration-show.ts +31 -66
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +457 -1067
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +40 -9
- package/src/control-api/operations/contract-emit.ts +22 -2
- package/src/control-api/operations/db-init.ts +3 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +51 -13
- package/src/control-api/operations/db-update.ts +4 -4
- package/src/control-api/operations/db-verify.ts +15 -5
- package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
- package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
- package/src/control-api/types.ts +46 -29
- package/src/utils/cli-errors.ts +70 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
- package/src/utils/formatters/migration-graph-layout.ts +227 -38
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +148 -0
- package/src/utils/formatters/migration-graph-tree-render.ts +959 -81
- package/src/utils/formatters/migration-list-data-column.ts +4 -91
- package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
- package/src/utils/formatters/migration-list-render.ts +135 -71
- package/src/utils/formatters/migration-list-styler.ts +46 -5
- package/src/utils/formatters/migration-list-types.ts +5 -21
- package/src/utils/formatters/migration-log-table.ts +205 -0
- package/src/utils/formatters/migrations.ts +33 -11
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/integrity-violation-to-check-failure.ts +28 -19
- package/src/utils/legend.ts +38 -0
- package/src/utils/migration-path-target.ts +60 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
- package/dist/commands/migration-list.mjs.map +0 -1
- package/dist/commands/migration-log.mjs.map +0 -1
- package/dist/commands/migration-status.mjs.map +0 -1
- package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
- package/dist/graph-render-rFAqZujX.mjs +0 -1081
- package/dist/graph-render-rFAqZujX.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/migration-check-BiBJoYYW.mjs +0 -341
- package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs +0 -1232
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
- package/dist/migration-types-D2FW63pr.d.mts +0 -15
- package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
- package/dist/migrations-Cv2jxNNK.mjs +0 -228
- package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
- package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
- package/dist/types-Dt_SfqFm.d.mts.map +0 -1
- package/src/utils/formatters/graph-migration-mapper.ts +0 -235
- package/src/utils/formatters/graph-render.ts +0 -1323
- package/src/utils/formatters/graph-types.ts +0 -120
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
2
|
-
import { bold } from 'colorette';
|
|
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 {
|
|
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
|
+
|
|
5
16
|
import type {
|
|
6
17
|
MigrationGraphGridModel,
|
|
7
18
|
MigrationGraphGridRow,
|
|
@@ -12,10 +23,15 @@ import {
|
|
|
12
23
|
MIGRATION_LIST_HASH_WIDTH,
|
|
13
24
|
migrationListEmptySource,
|
|
14
25
|
migrationListForwardArrow,
|
|
26
|
+
padFromHashColumn,
|
|
15
27
|
} from './migration-list-data-column';
|
|
16
28
|
import type { MigrationEdgeKind } from './migration-list-graph-topology';
|
|
17
29
|
import type { MigrationListStyler } from './migration-list-render';
|
|
18
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
CONTRACT_MARKER_NAME,
|
|
32
|
+
createAnsiMigrationListStyler,
|
|
33
|
+
formatContractNodeOverlays,
|
|
34
|
+
} from './migration-list-styler';
|
|
19
35
|
|
|
20
36
|
const LABEL_GAP = 2;
|
|
21
37
|
|
|
@@ -26,14 +42,37 @@ const LABEL_GAP = 2;
|
|
|
26
42
|
*/
|
|
27
43
|
const DB_MARKER_NAME = 'db';
|
|
28
44
|
|
|
45
|
+
export interface MigrationEdgeAnnotation {
|
|
46
|
+
readonly status?: 'applied' | 'pending';
|
|
47
|
+
readonly operationCount?: number;
|
|
48
|
+
readonly invariants?: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Path-highlight annotation for `migrate --show` preview.
|
|
51
|
+
* - `'on-path'`: migration is on the chosen path; rendered in bright green (nodes, hashes, names, lane lines).
|
|
52
|
+
* - `'off-path'`: migration is off the chosen path; fully drawn but in uniform dim grey.
|
|
53
|
+
*/
|
|
54
|
+
readonly pathHighlight?: 'on-path' | 'off-path';
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
export interface RenderMigrationGraphTreeOptions {
|
|
30
58
|
readonly refsByHash?: ReadonlyMap<string, readonly string[]>;
|
|
59
|
+
readonly edgeAnnotationsByHash?: ReadonlyMap<string, MigrationEdgeAnnotation>;
|
|
31
60
|
readonly dbHash?: string;
|
|
32
61
|
readonly contractHash?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether this render is for the app space. When false, the `@contract`
|
|
64
|
+
* marker is suppressed — `@contract` is an app-space concept and must not
|
|
65
|
+
* appear in extension spaces (e.g. `pgvector:`). Defaults to `true` so
|
|
66
|
+
* single-space callers that do not pass this option are unaffected.
|
|
67
|
+
*/
|
|
68
|
+
readonly isAppSpace?: boolean;
|
|
33
69
|
readonly activeRefName?: string;
|
|
34
70
|
readonly hashLength?: number;
|
|
71
|
+
readonly globalMaxEdgeTreePrefixWidth?: number;
|
|
72
|
+
readonly globalMaxDirNameWidth?: number;
|
|
35
73
|
readonly colorize: boolean;
|
|
36
74
|
readonly glyphMode?: GlyphMode;
|
|
75
|
+
readonly styler?: MigrationListStyler;
|
|
37
76
|
}
|
|
38
77
|
|
|
39
78
|
interface MigrationGraphTreeGlyphPalette {
|
|
@@ -48,6 +87,7 @@ interface MigrationGraphTreeGlyphPalette {
|
|
|
48
87
|
readonly arcBranchCorner: string;
|
|
49
88
|
readonly arcBranchTee: string;
|
|
50
89
|
readonly arcLandCorner: string;
|
|
90
|
+
readonly arcLandTee: string;
|
|
51
91
|
readonly arcCrossing: string;
|
|
52
92
|
readonly arcLandBridge: string;
|
|
53
93
|
readonly horizontalPass: string;
|
|
@@ -71,6 +111,7 @@ const UNICODE_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
|
71
111
|
arcBranchCorner: '╮ ',
|
|
72
112
|
arcBranchTee: '┬─',
|
|
73
113
|
arcLandCorner: '╯ ',
|
|
114
|
+
arcLandTee: '┴─',
|
|
74
115
|
arcCrossing: '┼─',
|
|
75
116
|
arcLandBridge: '──',
|
|
76
117
|
horizontalPass: '──',
|
|
@@ -94,6 +135,7 @@ const ASCII_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
|
94
135
|
arcBranchCorner: '\\ ',
|
|
95
136
|
arcBranchTee: '+-',
|
|
96
137
|
arcLandCorner: '/ ',
|
|
138
|
+
arcLandTee: '+-',
|
|
97
139
|
arcCrossing: '+-',
|
|
98
140
|
arcLandBridge: '--',
|
|
99
141
|
horizontalPass: '--',
|
|
@@ -109,6 +151,13 @@ function paletteFor(mode: GlyphMode): MigrationGraphTreeGlyphPalette {
|
|
|
109
151
|
return mode === 'ascii' ? ASCII_PALETTE : UNICODE_PALETTE;
|
|
110
152
|
}
|
|
111
153
|
|
|
154
|
+
function overlayStatusGlyphs(mode: GlyphMode): {
|
|
155
|
+
readonly applied: string;
|
|
156
|
+
readonly pending: string;
|
|
157
|
+
} {
|
|
158
|
+
return mode === 'ascii' ? { applied: '+', pending: '>' } : { applied: '✓', pending: '⧗' };
|
|
159
|
+
}
|
|
160
|
+
|
|
112
161
|
function arrowForEdgeKind(
|
|
113
162
|
kind: MigrationEdgeKind,
|
|
114
163
|
palette: MigrationGraphTreeGlyphPalette,
|
|
@@ -116,92 +165,385 @@ function arrowForEdgeKind(
|
|
|
116
165
|
return palette.edgeArrow[kind];
|
|
117
166
|
}
|
|
118
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Forced-color functions that always emit ANSI regardless of the ambient TTY
|
|
170
|
+
* environment (NO_COLOR, piped output). Used for:
|
|
171
|
+
*
|
|
172
|
+
* - `forcedBold`: branch-coloured migration names pair their lane hue with bold;
|
|
173
|
+
* both must emit so the name is deterministically bold + hue.
|
|
174
|
+
* - `forcedDim`: off-path path-highlight override (migrate --show).
|
|
175
|
+
* The renderer gates this behind `opts.colorize`; the forced variant ensures
|
|
176
|
+
* ANSI is emitted in controlled environments (e.g. tests with `NO_COLOR=1`)
|
|
177
|
+
* when the caller explicitly requests colour. Without forcing, `dim()` from
|
|
178
|
+
* the ambient module-level import no-ops under NO_COLOR, making the
|
|
179
|
+
* path-highlight unreachable in tests.
|
|
180
|
+
*/
|
|
181
|
+
const {
|
|
182
|
+
bold: forcedBold,
|
|
183
|
+
dim: forcedDim,
|
|
184
|
+
greenBright: forcedGreen,
|
|
185
|
+
} = createColors({ useColor: true });
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The two styles used in `migrate --show` path-highlight mode.
|
|
189
|
+
*
|
|
190
|
+
* In path-highlight mode the normal by-branch rotating-colour logic
|
|
191
|
+
* (`LANE_COLOR_CYCLE` / `laneStylerForColumn`) is suppressed entirely.
|
|
192
|
+
* Every glyph, name, and hash is styled by its on-path / off-path role,
|
|
193
|
+
* never by lane column index.
|
|
194
|
+
*
|
|
195
|
+
* - `onPath`: neutral single-path style — exactly how a linear (no-branch)
|
|
196
|
+
* section renders today. Lane glyphs are dim, names are bold, hashes use
|
|
197
|
+
* the default `sourceHash`/`destHash` colours. No rotation hue is applied.
|
|
198
|
+
* This is identical to how the pgvector single-path section renders.
|
|
199
|
+
* - `offPath`: uniform dim grey on every cell (name, hashes, lane glyphs,
|
|
200
|
+
* direction arrows).
|
|
201
|
+
*
|
|
202
|
+
* To change the on-path or off-path colour in future, edit this object only.
|
|
203
|
+
*/
|
|
204
|
+
export const PATH_HIGHLIGHT_STYLES = {
|
|
205
|
+
/**
|
|
206
|
+
* Lane/glyph/arrow stylers for on-path cells.
|
|
207
|
+
*
|
|
208
|
+
* - lane: `forcedGreen` when colour is on — bright green so the on-path
|
|
209
|
+
* branch glyphs (`│ ├ ╯ ↑`) and node markers (`○`/`∅`) are visually
|
|
210
|
+
* distinct from off-path (dim grey). Uses forced ANSI so it survives
|
|
211
|
+
* NO_COLOR in tests. Identity when `colorize` is false.
|
|
212
|
+
* - arrow: identity (plain, no colouring)
|
|
213
|
+
* - dirName: `bold` (ambient bold — name stays white/bold, not green)
|
|
214
|
+
* - hashOverride: undefined — `style.sourceHash`/`style.destHash` apply
|
|
215
|
+
* normally (cyan) so hashes keep their existing neutral colour.
|
|
216
|
+
*
|
|
217
|
+
* `style` is the same `MigrationListStyler` the tree renderer uses.
|
|
218
|
+
* Rotation (`LANE_COLOR_CYCLE`) is never applied to on-path cells.
|
|
219
|
+
*/
|
|
220
|
+
onPath: (_style: MigrationListStyler, colorize: boolean) => ({
|
|
221
|
+
lane: colorize ? forcedGreen : (text: string) => text,
|
|
222
|
+
arrow: (text: string) => text,
|
|
223
|
+
dirName: (text: string) => bold(text),
|
|
224
|
+
hashOverride: undefined,
|
|
225
|
+
}),
|
|
226
|
+
/**
|
|
227
|
+
* Lane/glyph/arrow/hash stylers for off-path cells.
|
|
228
|
+
* Uniform dim grey on everything — uses `forcedDim` so ANSI is emitted even
|
|
229
|
+
* under NO_COLOR (test environments use `colorize:true` + NO_COLOR=1 to verify dim).
|
|
230
|
+
* Returns identity functions when colour is off (`colorize: false`).
|
|
231
|
+
*/
|
|
232
|
+
offPath: (colorize: boolean) => ({
|
|
233
|
+
lane: colorize ? forcedDim : (text: string) => text,
|
|
234
|
+
arrow: colorize ? forcedDim : (text: string) => text,
|
|
235
|
+
dirName: colorize ? forcedDim : (text: string) => text,
|
|
236
|
+
hashOverride: colorize ? forcedDim : undefined,
|
|
237
|
+
}),
|
|
238
|
+
} as const;
|
|
239
|
+
|
|
240
|
+
function laneStylerForColumn(
|
|
241
|
+
colorColumn: number,
|
|
242
|
+
colorize: boolean,
|
|
243
|
+
style: MigrationListStyler,
|
|
244
|
+
): (text: string) => string {
|
|
245
|
+
return stylerForLaneColumn(colorColumn, colorize, style.lane);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Tint a branch-owned token (direction arrow, migration name) by its edge's
|
|
250
|
+
* lane so the whole branch row reads in one colour. Column 0 has nothing to be
|
|
251
|
+
* told apart from in the common linear chain, so it keeps the token's existing
|
|
252
|
+
* default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
|
|
253
|
+
* colour. With colour off, the fallback (also colourless) is used unchanged.
|
|
254
|
+
*/
|
|
255
|
+
function branchStylerOrDefault(
|
|
256
|
+
column: number,
|
|
257
|
+
colorize: boolean,
|
|
258
|
+
fallback: (text: string) => string,
|
|
259
|
+
): (text: string) => string {
|
|
260
|
+
if (!colorize || column <= NEUTRAL_LANE_COLUMN) {
|
|
261
|
+
return fallback;
|
|
262
|
+
}
|
|
263
|
+
return laneColorForColumn(column);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Render a crossing tee (`┼─`): the junction stays dim/neutral so neither arc
|
|
268
|
+
* steals the cell; the trailing dash takes the served lane hue.
|
|
269
|
+
*/
|
|
270
|
+
function renderArcCrossing(
|
|
271
|
+
pair: string,
|
|
272
|
+
dashColumn: number,
|
|
273
|
+
colorize: boolean,
|
|
274
|
+
style: MigrationListStyler,
|
|
275
|
+
): string {
|
|
276
|
+
const junction = colorize ? style.lane : (text: string) => text;
|
|
277
|
+
const dash = laneStylerForColumn(dashColumn, colorize, style);
|
|
278
|
+
return junction(pair.slice(0, 1)) + dash(pair.slice(1));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
|
|
283
|
+
* trailing dash coloured independently: the junction anchors its own lane while
|
|
284
|
+
* the dash leads into the branch on its right.
|
|
285
|
+
*/
|
|
286
|
+
function renderConnectorTee(
|
|
287
|
+
pair: string,
|
|
288
|
+
glyphColumn: number,
|
|
289
|
+
dashColumn: number,
|
|
290
|
+
colorize: boolean,
|
|
291
|
+
style: MigrationListStyler,
|
|
292
|
+
): string {
|
|
293
|
+
const glyph = laneStylerForColumn(glyphColumn, colorize, style);
|
|
294
|
+
if (glyphColumn === dashColumn) {
|
|
295
|
+
return glyph(pair);
|
|
296
|
+
}
|
|
297
|
+
return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
|
|
298
|
+
}
|
|
299
|
+
|
|
119
300
|
/**
|
|
120
301
|
* A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
|
|
121
|
-
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`).
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
302
|
+
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
|
|
303
|
+
* marker takes its own lane's hue (so each node visibly belongs to its branch);
|
|
304
|
+
* the connector follows the arc it belongs to (its owning back-lane hue).
|
|
305
|
+
* Direction arrows are handled elsewhere — they take their edge's lane hue too.
|
|
306
|
+
*
|
|
307
|
+
* When `laneOverride` is provided (for path-highlight rows), it replaces the
|
|
308
|
+
* marker styler. `arcLaneOverride` (if provided) replaces the connector styler
|
|
309
|
+
* independently — this matters when the node is on-path but the arc belongs to
|
|
310
|
+
* an off-path rollback edge, which must render dim rather than green.
|
|
125
311
|
*/
|
|
126
|
-
function renderNodeMarkerPair(
|
|
127
|
-
|
|
312
|
+
function renderNodeMarkerPair(
|
|
313
|
+
pair: string,
|
|
314
|
+
nodeColumn: number,
|
|
315
|
+
arcColumn: number,
|
|
316
|
+
colorize: boolean,
|
|
317
|
+
style: MigrationListStyler,
|
|
318
|
+
laneOverride?: (text: string) => string,
|
|
319
|
+
arcLaneOverride?: (text: string) => string,
|
|
320
|
+
): string {
|
|
321
|
+
const marker = laneOverride ?? laneStylerForColumn(nodeColumn, colorize, style);
|
|
322
|
+
const connector =
|
|
323
|
+
arcLaneOverride ?? laneOverride ?? laneStylerForColumn(arcColumn, colorize, style);
|
|
324
|
+
return marker(pair.slice(0, 1)) + connector(pair.slice(1));
|
|
128
325
|
}
|
|
129
326
|
|
|
130
327
|
function renderCellPair(
|
|
131
328
|
cell: StructuralCell,
|
|
329
|
+
column: number,
|
|
330
|
+
colors: RowArcLaneColors,
|
|
331
|
+
colorize: boolean,
|
|
132
332
|
style: MigrationListStyler,
|
|
133
333
|
palette: MigrationGraphTreeGlyphPalette,
|
|
334
|
+
laneOverride?: (text: string) => string,
|
|
335
|
+
arrowOverride?: (text: string) => string,
|
|
336
|
+
arcLaneOverride?: (text: string) => string,
|
|
134
337
|
): string {
|
|
338
|
+
const laneColumn = colors.lane[column] ?? column;
|
|
339
|
+
// In path-highlight mode (`laneOverride` present), the rotating lane colour is
|
|
340
|
+
// bypassed entirely — the override applies to every structural glyph. Without an
|
|
341
|
+
// override (normal graph/status/list mode), the existing rotation logic applies.
|
|
342
|
+
const lane = laneOverride ?? laneStylerForColumn(laneColumn, colorize, style);
|
|
343
|
+
// `arrowOverride` is used only for the direction arrow on edge-lane cells.
|
|
344
|
+
// When absent, the normal `branchStylerOrDefault` logic applies (rotation for lanes ≥ 1).
|
|
345
|
+
// In path-highlight mode it is always set alongside `laneOverride`.
|
|
346
|
+
const arrow =
|
|
347
|
+
arrowOverride ?? ((text: string) => branchStylerOrDefault(column, colorize, style.kind)(text));
|
|
135
348
|
switch (cell.kind) {
|
|
136
|
-
case 'node':
|
|
137
|
-
|
|
138
|
-
if (cell.
|
|
139
|
-
|
|
349
|
+
case 'node': {
|
|
350
|
+
const arcColumn = colors.connector[column] ?? NEUTRAL_LANE_COLUMN;
|
|
351
|
+
if (cell.arcLand === true) {
|
|
352
|
+
return renderNodeMarkerPair(
|
|
353
|
+
palette.arcLand,
|
|
354
|
+
column,
|
|
355
|
+
arcColumn,
|
|
356
|
+
colorize,
|
|
357
|
+
style,
|
|
358
|
+
laneOverride,
|
|
359
|
+
arcLaneOverride,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (cell.arcTee === true) {
|
|
363
|
+
return renderNodeMarkerPair(
|
|
364
|
+
palette.arcTee,
|
|
365
|
+
column,
|
|
366
|
+
arcColumn,
|
|
367
|
+
colorize,
|
|
368
|
+
style,
|
|
369
|
+
laneOverride,
|
|
370
|
+
arcLaneOverride,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return lane(palette.node);
|
|
374
|
+
}
|
|
140
375
|
case 'vertical-pass':
|
|
141
|
-
return
|
|
376
|
+
return lane(palette.verticalPass);
|
|
142
377
|
case 'edge-lane':
|
|
143
|
-
// The lane stays dim; the direction arrow (↑ / ↓ / ⟲) is the signal and
|
|
144
|
-
// stays bright, like the contract-node marker.
|
|
145
378
|
return cell.ownsLabel
|
|
146
|
-
?
|
|
147
|
-
|
|
148
|
-
: style.lane(palette.verticalPass);
|
|
379
|
+
? lane(palette.verticalPass.trimEnd()) + arrow(arrowForEdgeKind(cell.edgeKind, palette))
|
|
380
|
+
: lane(palette.verticalPass);
|
|
149
381
|
case 'branch-tee':
|
|
150
|
-
return
|
|
382
|
+
return lane(palette.branchTee);
|
|
151
383
|
case 'merge-tee':
|
|
152
|
-
return
|
|
384
|
+
return lane(palette.mergeTee);
|
|
153
385
|
case 'branch-corner':
|
|
154
|
-
return
|
|
386
|
+
return lane(palette.branchCorner);
|
|
155
387
|
case 'merge-corner':
|
|
156
|
-
return
|
|
388
|
+
return lane(palette.mergeCorner);
|
|
157
389
|
case 'arc-branch-corner':
|
|
158
|
-
return
|
|
390
|
+
return lane(palette.arcBranchCorner);
|
|
159
391
|
case 'arc-branch-tee':
|
|
160
|
-
return
|
|
392
|
+
return lane(palette.arcBranchTee);
|
|
161
393
|
case 'arc-land-corner':
|
|
162
|
-
return
|
|
394
|
+
return lane(palette.arcLandCorner);
|
|
395
|
+
case 'arc-land-tee':
|
|
396
|
+
// When a lane override is active, apply it uniformly to both glyph and dash parts
|
|
397
|
+
// so neither part emits a rotation hue.
|
|
398
|
+
return laneOverride !== undefined
|
|
399
|
+
? laneOverride(palette.arcLandTee)
|
|
400
|
+
: renderConnectorTee(
|
|
401
|
+
palette.arcLandTee,
|
|
402
|
+
laneColumn,
|
|
403
|
+
colors.dash[column] ?? laneColumn,
|
|
404
|
+
colorize,
|
|
405
|
+
style,
|
|
406
|
+
);
|
|
163
407
|
case 'arc-crossing':
|
|
164
|
-
return
|
|
408
|
+
return lane(palette.arcLandBridge);
|
|
165
409
|
case 'arc-land-bridge':
|
|
166
|
-
return
|
|
410
|
+
return lane(palette.arcLandBridge);
|
|
167
411
|
case 'horizontal-pass':
|
|
168
|
-
return
|
|
412
|
+
return lane(palette.horizontalPass);
|
|
169
413
|
case 'empty':
|
|
170
414
|
return ' ';
|
|
171
415
|
}
|
|
172
416
|
}
|
|
173
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Render a branch-connector or merge-connector row.
|
|
420
|
+
*
|
|
421
|
+
* `columnLaneOverride` is an optional per-column map populated when path-highlight
|
|
422
|
+
* annotations are active (`migrate --show`). For each column in the connector's
|
|
423
|
+
* lane range, the map supplies the override styler (dim for off-path) that should
|
|
424
|
+
* replace the normal rotating-lane colour for that column. Columns absent from the
|
|
425
|
+
* map (on-path or unannotated) use the standard `laneStylerForColumn` logic unchanged.
|
|
426
|
+
* This ensures off-path branch connectors appear dim rather than in their rotation
|
|
427
|
+
* colour (e.g. magenta).
|
|
428
|
+
*/
|
|
174
429
|
function renderConnectorRow(
|
|
175
430
|
row: MigrationGraphGridRow,
|
|
176
431
|
gridWidth: number,
|
|
432
|
+
colorize: boolean,
|
|
177
433
|
style: MigrationListStyler,
|
|
178
434
|
palette: MigrationGraphTreeGlyphPalette,
|
|
435
|
+
columnLaneOverride?: ReadonlyMap<number, (text: string) => string>,
|
|
179
436
|
): string {
|
|
437
|
+
const resolvedLane = (column: number): ((text: string) => string) =>
|
|
438
|
+
columnLaneOverride?.get(column) ?? laneStylerForColumn(column, colorize, style);
|
|
439
|
+
|
|
180
440
|
const isMerge = row.kind === 'merge-connector';
|
|
181
441
|
if (row.cells.length > 0) {
|
|
442
|
+
const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
|
|
182
443
|
let seenTee = false;
|
|
183
444
|
let out = '';
|
|
184
|
-
for (
|
|
445
|
+
for (let column = 0; column < row.cells.length; column++) {
|
|
446
|
+
const cell = row.cells[column];
|
|
447
|
+
if (cell === undefined) continue;
|
|
448
|
+
const glyphColumn = colors.glyph[column] ?? column;
|
|
449
|
+
const dashColumn = colors.dash[column] ?? glyphColumn;
|
|
450
|
+
const override = columnLaneOverride?.get(glyphColumn);
|
|
451
|
+
// In path-highlight mode, the dash column's override is used for the trailing dash
|
|
452
|
+
// even when the glyph column has no override. This handles branch-tee cells whose
|
|
453
|
+
// migrationHash is undefined (no previous edge occupied that lane) — the tee's dash
|
|
454
|
+
// belongs to the connector run and should follow the corner's annotation.
|
|
455
|
+
const dashOverrideForPathHighlight = columnLaneOverride?.get(dashColumn) ?? override;
|
|
456
|
+
if (
|
|
457
|
+
override !== undefined ||
|
|
458
|
+
(columnLaneOverride !== undefined && dashOverrideForPathHighlight !== undefined)
|
|
459
|
+
) {
|
|
460
|
+
// When an override is active for this column (or when a dash override is available
|
|
461
|
+
// via the connected corner), apply the glyph column's override to the junction glyph
|
|
462
|
+
// (├/┬/┴), and the dash column's override to the trailing dash.
|
|
463
|
+
// This matters for merge/branch connectors: the on-path trunk's tee (├) is green
|
|
464
|
+
// while the dash (─) and corner (╯) bridging to an OFF-path column are dim.
|
|
465
|
+
// For non-tee cells (corner, pass, crossing), the single-column override is fine.
|
|
466
|
+
const effectiveOverride = override ?? dashOverrideForPathHighlight;
|
|
467
|
+
if (effectiveOverride === undefined) {
|
|
468
|
+
out += ' ';
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
switch (cell.kind) {
|
|
472
|
+
case 'branch-tee':
|
|
473
|
+
case 'merge-tee': {
|
|
474
|
+
const pair = seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee;
|
|
475
|
+
// Both the junction glyph and its trailing dash belong to this tee cell's
|
|
476
|
+
// own edge — use effectiveOverride for both so an off-path tee's dash is dim
|
|
477
|
+
// even when the next column (dashColumn) belongs to an on-path edge.
|
|
478
|
+
out += effectiveOverride(pair.slice(0, 1)) + effectiveOverride(pair.slice(1));
|
|
479
|
+
seenTee = true;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'branch-corner':
|
|
483
|
+
out += effectiveOverride(palette.branchCorner);
|
|
484
|
+
break;
|
|
485
|
+
case 'merge-corner':
|
|
486
|
+
out += effectiveOverride(palette.mergeCorner);
|
|
487
|
+
break;
|
|
488
|
+
case 'vertical-pass':
|
|
489
|
+
out += effectiveOverride(palette.verticalPass);
|
|
490
|
+
break;
|
|
491
|
+
case 'horizontal-pass':
|
|
492
|
+
out += effectiveOverride(palette.horizontalPass);
|
|
493
|
+
break;
|
|
494
|
+
case 'arc-crossing': {
|
|
495
|
+
// The junction glyph (┼) belongs to the vertical lane (effectiveOverride).
|
|
496
|
+
// The trailing dash (─) runs horizontally into the next column — it belongs
|
|
497
|
+
// to that column's owner (dashColumn). Use the dash column's override so an
|
|
498
|
+
// off-path horizontal continuation is dim even when the crossing is on-path.
|
|
499
|
+
const arcCrossingDashOverride =
|
|
500
|
+
columnLaneOverride?.get(dashColumn) ?? effectiveOverride;
|
|
501
|
+
out +=
|
|
502
|
+
effectiveOverride(palette.arcCrossing.slice(0, 1)) +
|
|
503
|
+
arcCrossingDashOverride(palette.arcCrossing.slice(1));
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
default:
|
|
507
|
+
out += ' ';
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const lane = laneStylerForColumn(glyphColumn, colorize, style);
|
|
185
512
|
switch (cell.kind) {
|
|
186
513
|
case 'branch-tee':
|
|
187
|
-
out +=
|
|
514
|
+
out += renderConnectorTee(
|
|
515
|
+
seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee,
|
|
516
|
+
glyphColumn,
|
|
517
|
+
dashColumn,
|
|
518
|
+
colorize,
|
|
519
|
+
style,
|
|
520
|
+
);
|
|
188
521
|
seenTee = true;
|
|
189
522
|
break;
|
|
190
523
|
case 'merge-tee':
|
|
191
|
-
out +=
|
|
524
|
+
out += renderConnectorTee(
|
|
525
|
+
seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee,
|
|
526
|
+
glyphColumn,
|
|
527
|
+
dashColumn,
|
|
528
|
+
colorize,
|
|
529
|
+
style,
|
|
530
|
+
);
|
|
192
531
|
seenTee = true;
|
|
193
532
|
break;
|
|
194
533
|
case 'branch-corner':
|
|
195
|
-
out +=
|
|
534
|
+
out += lane(palette.branchCorner);
|
|
196
535
|
break;
|
|
197
536
|
case 'merge-corner':
|
|
198
|
-
out +=
|
|
537
|
+
out += lane(palette.mergeCorner);
|
|
199
538
|
break;
|
|
200
539
|
case 'vertical-pass':
|
|
201
|
-
out +=
|
|
540
|
+
out += lane(palette.verticalPass);
|
|
202
541
|
break;
|
|
203
542
|
case 'horizontal-pass':
|
|
204
|
-
out +=
|
|
543
|
+
out += lane(palette.horizontalPass);
|
|
544
|
+
break;
|
|
545
|
+
case 'arc-crossing':
|
|
546
|
+
out += renderArcCrossing(palette.arcCrossing, dashColumn, colorize, style);
|
|
205
547
|
break;
|
|
206
548
|
default:
|
|
207
549
|
out += ' ';
|
|
@@ -218,13 +560,15 @@ function renderConnectorRow(
|
|
|
218
560
|
|
|
219
561
|
const start = row.startLane ?? 0;
|
|
220
562
|
const end = row.endLane ?? start;
|
|
563
|
+
// The whole fork/merge run reads as one line in the served lane's hue (the
|
|
564
|
+
// corner it reaches); pass-through columns outside the run keep their own.
|
|
565
|
+
const runLane = resolvedLane(end);
|
|
221
566
|
let out = '';
|
|
222
567
|
for (let column = 0; column < gridWidth; column++) {
|
|
223
568
|
if (column < start || column > end) out += ' ';
|
|
224
|
-
else if (column === start) out +=
|
|
225
|
-
else if (column === end)
|
|
226
|
-
|
|
227
|
-
else out += style.lane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
569
|
+
else if (column === start) out += runLane(palette.connectorBranchTee);
|
|
570
|
+
else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
|
|
571
|
+
else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
228
572
|
}
|
|
229
573
|
return out;
|
|
230
574
|
}
|
|
@@ -239,26 +583,45 @@ function abbreviateHash(hash: string, hashLength: number, emptySource: string):
|
|
|
239
583
|
|
|
240
584
|
const MIN_HASH_DATA_COLUMN = 25;
|
|
241
585
|
|
|
586
|
+
interface ContractOverlayNames {
|
|
587
|
+
readonly markers: readonly string[];
|
|
588
|
+
readonly refs: readonly string[];
|
|
589
|
+
}
|
|
590
|
+
|
|
242
591
|
function overlayNamesForContract(
|
|
243
592
|
contractHash: string,
|
|
244
593
|
opts: RenderMigrationGraphTreeOptions,
|
|
245
|
-
):
|
|
246
|
-
const
|
|
594
|
+
): ContractOverlayNames {
|
|
595
|
+
const markers: string[] = [];
|
|
596
|
+
const refs: string[] = [];
|
|
247
597
|
const userRefs = opts.refsByHash?.get(contractHash);
|
|
248
598
|
if (userRefs) {
|
|
249
|
-
|
|
599
|
+
refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
|
|
250
600
|
}
|
|
251
|
-
if (
|
|
252
|
-
|
|
601
|
+
if (
|
|
602
|
+
opts.isAppSpace !== false &&
|
|
603
|
+
opts.contractHash === contractHash &&
|
|
604
|
+
contractHash !== EMPTY_CONTRACT_HASH
|
|
605
|
+
) {
|
|
606
|
+
markers.push(CONTRACT_MARKER_NAME);
|
|
253
607
|
}
|
|
254
|
-
if (opts.
|
|
255
|
-
|
|
608
|
+
if (opts.dbHash === contractHash) {
|
|
609
|
+
markers.push(DB_MARKER_NAME);
|
|
256
610
|
}
|
|
257
|
-
|
|
611
|
+
markers.sort((a, b) => {
|
|
612
|
+
if (a === CONTRACT_MARKER_NAME) {
|
|
613
|
+
return -1;
|
|
614
|
+
}
|
|
615
|
+
if (b === CONTRACT_MARKER_NAME) {
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
return a.localeCompare(b);
|
|
619
|
+
});
|
|
620
|
+
return { markers, refs };
|
|
258
621
|
}
|
|
259
622
|
|
|
260
623
|
function createTreeStyler(opts: RenderMigrationGraphTreeOptions): MigrationListStyler {
|
|
261
|
-
const base = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
624
|
+
const base = opts.styler ?? createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
262
625
|
const activeRefName = opts.activeRefName;
|
|
263
626
|
if (!opts.colorize || activeRefName === undefined) {
|
|
264
627
|
return base;
|
|
@@ -272,23 +635,78 @@ function createTreeStyler(opts: RenderMigrationGraphTreeOptions): MigrationListS
|
|
|
272
635
|
};
|
|
273
636
|
}
|
|
274
637
|
|
|
638
|
+
function formatEdgeAnnotationSuffix(
|
|
639
|
+
migrationHash: string,
|
|
640
|
+
opts: RenderMigrationGraphTreeOptions,
|
|
641
|
+
style: MigrationListStyler,
|
|
642
|
+
): string {
|
|
643
|
+
const annotation = opts.edgeAnnotationsByHash?.get(migrationHash);
|
|
644
|
+
if (annotation === undefined) {
|
|
645
|
+
return '';
|
|
646
|
+
}
|
|
647
|
+
const isOffPath = annotation.pathHighlight === 'off-path';
|
|
648
|
+
const segments: string[] = [];
|
|
649
|
+
if (annotation.operationCount !== undefined) {
|
|
650
|
+
segments.push(`${annotation.operationCount} ops`);
|
|
651
|
+
}
|
|
652
|
+
if (annotation.invariants !== undefined && annotation.invariants.length > 0) {
|
|
653
|
+
segments.push(style.invariants(annotation.invariants));
|
|
654
|
+
}
|
|
655
|
+
const status = annotation.status;
|
|
656
|
+
if (status !== undefined) {
|
|
657
|
+
const glyphs = overlayStatusGlyphs(opts.glyphMode ?? 'unicode');
|
|
658
|
+
const glyph = status === 'applied' ? glyphs.applied : glyphs.pending;
|
|
659
|
+
const label = status === 'applied' ? 'applied' : 'pending';
|
|
660
|
+
if (!opts.colorize) {
|
|
661
|
+
segments.push(`${glyph} ${label}`);
|
|
662
|
+
} else {
|
|
663
|
+
const styler = status === 'applied' ? green : yellow;
|
|
664
|
+
segments.push(styler(`${glyph} ${label}`));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (annotation.pathHighlight === 'on-path') {
|
|
668
|
+
const glyph = opts.glyphMode === 'ascii' ? '>' : '↑';
|
|
669
|
+
segments.push(`${glyph} will run`);
|
|
670
|
+
}
|
|
671
|
+
if (segments.length === 0) {
|
|
672
|
+
return '';
|
|
673
|
+
}
|
|
674
|
+
const suffix = ` ${segments.join(' ')}`;
|
|
675
|
+
return opts.colorize && isOffPath ? forcedDim(suffix) : suffix;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Format the `from → to` hash data column for an edge row.
|
|
680
|
+
*
|
|
681
|
+
* When `hashOverride` is provided (off-path → `dim`), it replaces ALL sub-stylers
|
|
682
|
+
* (`sourceHash`, `destHash`, arrow `glyph`) so dim reaches every character without
|
|
683
|
+
* inner ANSI codes (e.g. the dim+cyan of `sourceHash`) overriding it. On-path edges
|
|
684
|
+
* carry no override. Without an override, the normal `style` sub-stylers apply.
|
|
685
|
+
*/
|
|
275
686
|
function formatEdgeHashColumn(
|
|
276
687
|
edge: ClassifiedEdge,
|
|
277
688
|
style: MigrationListStyler,
|
|
278
689
|
hashLength: number,
|
|
279
690
|
palette: MigrationGraphTreeGlyphPalette,
|
|
691
|
+
hashOverride?: (text: string) => string,
|
|
280
692
|
): string {
|
|
693
|
+
const src = hashOverride ?? style.sourceHash;
|
|
694
|
+
const dst = hashOverride ?? style.destHash;
|
|
695
|
+
const glyph = hashOverride ?? style.glyph;
|
|
281
696
|
if (edge.kind === 'self') {
|
|
282
697
|
const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
|
|
283
|
-
|
|
698
|
+
const source = padFromHashColumn(src(hash), hashLength);
|
|
699
|
+
return `${source} ${glyph(palette.forwardArrow)} ${dst(hash)}`;
|
|
284
700
|
}
|
|
285
701
|
const source =
|
|
286
702
|
edge.from === EMPTY_CONTRACT_HASH
|
|
287
|
-
?
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
703
|
+
? padFromHashColumn(glyph(palette.emptySource), hashLength)
|
|
704
|
+
: padFromHashColumn(
|
|
705
|
+
src(abbreviateHash(edge.from, hashLength, palette.emptySource)),
|
|
706
|
+
hashLength,
|
|
707
|
+
);
|
|
708
|
+
const arrow = glyph(palette.forwardArrow);
|
|
709
|
+
const dest = dst(abbreviateHash(edge.to, hashLength, palette.emptySource));
|
|
292
710
|
return `${source} ${arrow} ${dest}`;
|
|
293
711
|
}
|
|
294
712
|
|
|
@@ -297,6 +715,13 @@ function padVisible(text: string, targetWidth: number): string {
|
|
|
297
715
|
return text + ' '.repeat(padding);
|
|
298
716
|
}
|
|
299
717
|
|
|
718
|
+
const ANSI_ESCAPE = '\x1b';
|
|
719
|
+
|
|
720
|
+
function trimTrailingWhitespace(line: string): string {
|
|
721
|
+
const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
|
|
722
|
+
return line.replace(trailingSpaceBeforeReset, '$1').replace(/\s+$/, '');
|
|
723
|
+
}
|
|
724
|
+
|
|
300
725
|
function gridWidthForModel(rows: readonly MigrationGraphGridRow[]): number {
|
|
301
726
|
return rows.reduce(
|
|
302
727
|
(max, row) =>
|
|
@@ -336,6 +761,35 @@ function edgeLabelColumn(row: MigrationGraphGridRow, wideLabelColumn: number | u
|
|
|
336
761
|
return usesFullRowGutter ? row.cells.length * 2 + LABEL_GAP : (laneIndex + 1) * 2 + LABEL_GAP;
|
|
337
762
|
}
|
|
338
763
|
|
|
764
|
+
function maxEdgeTreePrefixWidth(
|
|
765
|
+
rows: readonly MigrationGraphGridRow[],
|
|
766
|
+
wideLabelColumn: number | undefined,
|
|
767
|
+
): number {
|
|
768
|
+
let max = 0;
|
|
769
|
+
for (const row of rows) {
|
|
770
|
+
if (row.kind !== 'edge' || row.edge === undefined) continue;
|
|
771
|
+
max = Math.max(max, edgeLabelColumn(row, wideLabelColumn));
|
|
772
|
+
}
|
|
773
|
+
return max;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function computeMaxEdgeTreePrefixWidthForLayout(model: MigrationGraphGridModel): number {
|
|
777
|
+
const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows)
|
|
778
|
+
? gridWidthForModel(model.rows) * 2 + 4
|
|
779
|
+
: undefined;
|
|
780
|
+
return maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export function computeMaxDirNameLengthForLayout(model: MigrationGraphGridModel): number {
|
|
784
|
+
const allEdges = model.rows
|
|
785
|
+
.filter(
|
|
786
|
+
(row): row is MigrationGraphGridRow & { edge: ClassifiedEdge } =>
|
|
787
|
+
row.kind === 'edge' && row.edge !== undefined,
|
|
788
|
+
)
|
|
789
|
+
.map((row) => row.edge);
|
|
790
|
+
return maxDirNameLength(allEdges);
|
|
791
|
+
}
|
|
792
|
+
|
|
339
793
|
function nodeHasArcDecoration(row: MigrationGraphGridRow): boolean {
|
|
340
794
|
return row.cells.some(
|
|
341
795
|
(cell) => cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true),
|
|
@@ -360,6 +814,89 @@ export function renderMigrationGraphTree(
|
|
|
360
814
|
)
|
|
361
815
|
.map((row) => row.edge);
|
|
362
816
|
const maxDirNameLen = maxDirNameLength(allEdges);
|
|
817
|
+
const effectiveMaxDirNameLen = opts.globalMaxDirNameWidth ?? maxDirNameLen;
|
|
818
|
+
const maxEdgePrefixWidth =
|
|
819
|
+
opts.globalMaxEdgeTreePrefixWidth ?? maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
820
|
+
const edgeDirNameWidth = rowDirNameWidth(maxEdgePrefixWidth, effectiveMaxDirNameLen, dirNameGap);
|
|
821
|
+
|
|
822
|
+
// Build a contract-hash → path-highlight map so node rows can be coloured correctly.
|
|
823
|
+
// On-path wins: if a contract is both `from` of an on-path edge and `to` of an off-path
|
|
824
|
+
// edge (or vice-versa), it is treated as on-path.
|
|
825
|
+
// This map is only populated when edgeAnnotationsByHash is provided (migrate --show);
|
|
826
|
+
// for every other command (graph/status/list) it is empty and the code below is a no-op.
|
|
827
|
+
// NOTE: this is ONLY used for node-marker (○/∅) classification. Connector rows and
|
|
828
|
+
// structural cells (tees, corners, arcs) use their per-cell migrationHash directly —
|
|
829
|
+
// not this map and not any column-level aggregate.
|
|
830
|
+
const contractHighlights = new Map<string, 'on-path' | 'off-path'>();
|
|
831
|
+
if (opts.edgeAnnotationsByHash) {
|
|
832
|
+
for (const row of model.rows) {
|
|
833
|
+
if (row.kind !== 'edge' || row.edge === undefined) continue;
|
|
834
|
+
const annotation = opts.edgeAnnotationsByHash.get(row.edge.migrationHash);
|
|
835
|
+
if (annotation?.pathHighlight === undefined) continue;
|
|
836
|
+
const highlight = annotation.pathHighlight;
|
|
837
|
+
for (const hash of [row.edge.from, row.edge.to]) {
|
|
838
|
+
if (hash === EMPTY_CONTRACT_HASH) continue;
|
|
839
|
+
const existing = contractHighlights.get(hash);
|
|
840
|
+
// On-path wins over off-path when a contract hash appears in both.
|
|
841
|
+
if (existing !== 'on-path') {
|
|
842
|
+
contractHighlights.set(hash, highlight);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// In path-highlight mode (`opts.edgeAnnotationsByHash` present), the by-branch rotating
|
|
849
|
+
// colour logic is suppressed entirely. Every glyph is styled by on-path / off-path role
|
|
850
|
+
// via PATH_HIGHLIGHT_STYLES — never by lane column index. In normal mode (no annotations)
|
|
851
|
+
// `pathHighlightActive` is false and the code below is a complete no-op; rotation applies.
|
|
852
|
+
const pathHighlightActive = opts.edgeAnnotationsByHash !== undefined;
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Resolve the lane and arrow overrides for a row in path-highlight mode.
|
|
856
|
+
* - on-path → neutral single-path style (style.lane for glyphs, plain arrow, bold name).
|
|
857
|
+
* Rotation colour is suppressed; `style.sourceHash`/`style.destHash` apply for hashes.
|
|
858
|
+
* - off-path → uniform dim grey (forcedDim) on every glyph, arrow, name, and hash.
|
|
859
|
+
* - undefined → `undefined` (no override). Unannotated rows use normal rotation. This covers
|
|
860
|
+
* both non-path-highlight commands (graph/status/list) and any annotation without pathHighlight.
|
|
861
|
+
* - When pathHighlightActive is false: always returns undefined, preserving normal rotation.
|
|
862
|
+
*/
|
|
863
|
+
function pathStyleForHighlight(highlight: 'on-path' | 'off-path' | undefined):
|
|
864
|
+
| {
|
|
865
|
+
lane: ((text: string) => string) | undefined;
|
|
866
|
+
arrow: ((text: string) => string) | undefined;
|
|
867
|
+
dirName: ((text: string) => string) | undefined;
|
|
868
|
+
hashOverride: ((text: string) => string) | undefined;
|
|
869
|
+
}
|
|
870
|
+
| undefined {
|
|
871
|
+
if (!pathHighlightActive || highlight === undefined) return undefined;
|
|
872
|
+
if (highlight === 'off-path') {
|
|
873
|
+
const s = PATH_HIGHLIGHT_STYLES.offPath(opts.colorize);
|
|
874
|
+
return { lane: s.lane, arrow: s.arrow, dirName: s.dirName, hashOverride: s.hashOverride };
|
|
875
|
+
}
|
|
876
|
+
// on-path → green lane glyphs, bold name, neutral hashes
|
|
877
|
+
const s = PATH_HIGHLIGHT_STYLES.onPath(style, opts.colorize);
|
|
878
|
+
return { lane: s.lane, arrow: s.arrow, dirName: s.dirName, hashOverride: s.hashOverride };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Lane override for a given highlight in path-highlight mode.
|
|
883
|
+
* Returns the `lane` part only — used for per-cell overrides.
|
|
884
|
+
*/
|
|
885
|
+
function pathLaneFor(
|
|
886
|
+
highlight: 'on-path' | 'off-path' | undefined,
|
|
887
|
+
): ((text: string) => string) | undefined {
|
|
888
|
+
return pathStyleForHighlight(highlight)?.lane;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Arrow override for a given highlight in path-highlight mode.
|
|
893
|
+
* Returns the `arrow` part only — used for edge-lane cell arrow rendering.
|
|
894
|
+
*/
|
|
895
|
+
function pathArrowFor(
|
|
896
|
+
highlight: 'on-path' | 'off-path' | undefined,
|
|
897
|
+
): ((text: string) => string) | undefined {
|
|
898
|
+
return pathStyleForHighlight(highlight)?.arrow;
|
|
899
|
+
}
|
|
363
900
|
|
|
364
901
|
const lines: string[] = [];
|
|
365
902
|
|
|
@@ -373,24 +910,165 @@ export function renderMigrationGraphTree(
|
|
|
373
910
|
}
|
|
374
911
|
|
|
375
912
|
if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
|
|
376
|
-
|
|
913
|
+
// In path-highlight mode, build a per-column lane override from each cell's own
|
|
914
|
+
// migrationHash. Each structural cell (branch-tee, branch-corner, merge-tee,
|
|
915
|
+
// merge-corner, vertical-pass, arc-crossing) carries the migrationHash of the
|
|
916
|
+
// edge it visually belongs to (set by Stage 2). We look up that edge's annotation
|
|
917
|
+
// directly — no column-level aggregate, no "on-path wins" across columns.
|
|
918
|
+
let connectorColumnOverride: Map<number, (text: string) => string> | undefined;
|
|
919
|
+
if (pathHighlightActive && opts.colorize) {
|
|
920
|
+
connectorColumnOverride = new Map();
|
|
921
|
+
for (let col = 0; col < row.cells.length; col++) {
|
|
922
|
+
const cell = row.cells[col];
|
|
923
|
+
if (cell === undefined || cell.kind === 'empty') continue;
|
|
924
|
+
// arc-crossing: colour by the vertical lane's owner (migrationHash), not the arc.
|
|
925
|
+
const hashForCell =
|
|
926
|
+
'migrationHash' in cell && cell.migrationHash !== undefined
|
|
927
|
+
? cell.migrationHash
|
|
928
|
+
: undefined;
|
|
929
|
+
if (hashForCell === undefined) continue;
|
|
930
|
+
const highlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
931
|
+
const override = pathLaneFor(highlight);
|
|
932
|
+
if (override !== undefined) {
|
|
933
|
+
connectorColumnOverride.set(col, override);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (connectorColumnOverride.size === 0) {
|
|
937
|
+
connectorColumnOverride = undefined;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
lines.push(
|
|
941
|
+
trimTrailingWhitespace(
|
|
942
|
+
renderConnectorRow(
|
|
943
|
+
row,
|
|
944
|
+
gridWidth,
|
|
945
|
+
opts.colorize,
|
|
946
|
+
style,
|
|
947
|
+
palette,
|
|
948
|
+
connectorColumnOverride,
|
|
949
|
+
),
|
|
950
|
+
),
|
|
951
|
+
);
|
|
377
952
|
continue;
|
|
378
953
|
}
|
|
379
954
|
|
|
380
|
-
|
|
381
|
-
|
|
955
|
+
// Determine the per-row path-highlight style for path-highlight rendering.
|
|
956
|
+
// For edge rows: derived from the edge's annotation.
|
|
957
|
+
// For node rows: derived from the contract hash's membership in on/off-path edges.
|
|
958
|
+
// When pathHighlightActive is false, pathStyleForHighlight returns undefined and
|
|
959
|
+
// the normal rotating-colour lane styler applies everywhere (no-op for non-show commands).
|
|
960
|
+
let rowPathHighlight: 'on-path' | 'off-path' | undefined;
|
|
961
|
+
if (row.kind === 'edge' && row.edge !== undefined) {
|
|
962
|
+
rowPathHighlight = opts.edgeAnnotationsByHash?.get(row.edge.migrationHash)?.pathHighlight;
|
|
963
|
+
} else if (row.kind === 'node' && row.contractHash !== undefined) {
|
|
964
|
+
rowPathHighlight = contractHighlights.get(row.contractHash);
|
|
965
|
+
}
|
|
966
|
+
const rowStyle = pathStyleForHighlight(rowPathHighlight);
|
|
967
|
+
const rowLaneOverride = rowStyle?.lane;
|
|
968
|
+
const rowArrowOverride = rowStyle?.arrow;
|
|
969
|
+
|
|
970
|
+
// Classify every cell by its own edge's annotation (migrationHash → edgeAnnotationsByHash).
|
|
971
|
+
// Each structural cell (vertical-pass, branch-tee, arc-land-corner, etc.) carries the
|
|
972
|
+
// migrationHash of the edge it visually belongs to (set by the layout builder, Stage 2).
|
|
973
|
+
// We read that hash directly — no column-level aggregate, no "on-path wins" across columns.
|
|
974
|
+
//
|
|
975
|
+
// - vertical-pass: classifies by cell.migrationHash (the edge passing through), NOT by column.
|
|
976
|
+
// - edge-lane: classifies by cell.migrationHash (the edge's own row).
|
|
977
|
+
// - branch-tee/corner, merge-tee/corner, arc-*: classifies by cell.migrationHash.
|
|
978
|
+
// - arc-crossing: classifies by cell.migrationHash (the vertical lane's owner), so the
|
|
979
|
+
// crossing reads as the lane passing THROUGH, not the arc skipping over.
|
|
980
|
+
// - node (○/∅): classifies by rowPathHighlight derived from contractHighlights (the
|
|
981
|
+
// node's incident edges); falls through to rowLaneOverride.
|
|
982
|
+
//
|
|
983
|
+
// When pathHighlightActive is false (normal graph/status/list mode), all overrides are
|
|
984
|
+
// undefined and the normal rotating-colour lane styler applies unchanged.
|
|
985
|
+
const cellColors = resolveRowArcLaneColors(row.cells);
|
|
986
|
+
let gutter = row.cells
|
|
987
|
+
.map((cell, column) => {
|
|
988
|
+
let laneOverride = rowLaneOverride;
|
|
989
|
+
let arrowOverride = rowArrowOverride;
|
|
990
|
+
let arcLaneOverride: ((text: string) => string) | undefined;
|
|
991
|
+
if (pathHighlightActive) {
|
|
992
|
+
if (cell.kind === 'edge-lane') {
|
|
993
|
+
// Own cell: colour comes from this cell's own edge annotation.
|
|
994
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(
|
|
995
|
+
cell.migrationHash,
|
|
996
|
+
)?.pathHighlight;
|
|
997
|
+
laneOverride = pathLaneFor(cellHighlight);
|
|
998
|
+
arrowOverride = pathArrowFor(cellHighlight);
|
|
999
|
+
} else if (cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true)) {
|
|
1000
|
+
// Node with arc decoration: the node marker takes the node's own row highlight
|
|
1001
|
+
// (rowLaneOverride), but the arc connector belongs to the back-arc edge which may
|
|
1002
|
+
// have a different annotation. Look up the arc cell's migrationHash to derive the
|
|
1003
|
+
// arc connector's colour independently.
|
|
1004
|
+
const arcColumn = cellColors.connector[column] ?? NEUTRAL_LANE_COLUMN;
|
|
1005
|
+
const arcCell = row.cells[arcColumn];
|
|
1006
|
+
const arcHash =
|
|
1007
|
+
arcCell !== undefined && 'migrationHash' in arcCell
|
|
1008
|
+
? arcCell.migrationHash
|
|
1009
|
+
: undefined;
|
|
1010
|
+
if (arcHash !== undefined) {
|
|
1011
|
+
const arcHighlight = opts.edgeAnnotationsByHash?.get(arcHash)?.pathHighlight;
|
|
1012
|
+
arcLaneOverride = pathLaneFor(arcHighlight);
|
|
1013
|
+
}
|
|
1014
|
+
// laneOverride stays as rowLaneOverride (the node marker colour)
|
|
1015
|
+
} else if (cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1016
|
+
// Routing cells (vertical-pass, branch-tee, merge-corner, arc-*, horizontal-pass):
|
|
1017
|
+
// each carries a migrationHash for the edge it belongs to. Classify by that hash.
|
|
1018
|
+
//
|
|
1019
|
+
// arc-crossing in node/edge rows renders as '──' (the arc bridge over the crossing),
|
|
1020
|
+
// not '┼─'. Colour by the arc edge (arcMigrationHash) so an off-path arc bridge is
|
|
1021
|
+
// dim even when the crossed vertical lane (migrationHash) is on-path.
|
|
1022
|
+
// In connector rows, arc-crossing renders '┼─' where the junction belongs to the
|
|
1023
|
+
// vertical lane — handled separately in renderConnectorRow.
|
|
1024
|
+
const hashForCell =
|
|
1025
|
+
cell.kind === 'arc-crossing' &&
|
|
1026
|
+
'arcMigrationHash' in cell &&
|
|
1027
|
+
cell.arcMigrationHash !== undefined
|
|
1028
|
+
? cell.arcMigrationHash
|
|
1029
|
+
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1030
|
+
? cell.migrationHash
|
|
1031
|
+
: undefined;
|
|
1032
|
+
if (hashForCell !== undefined) {
|
|
1033
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1034
|
+
laneOverride = pathLaneFor(cellHighlight);
|
|
1035
|
+
arrowOverride = pathArrowFor(cellHighlight);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// plain node cells (no arcTee/arcLand) fall through to rowLaneOverride
|
|
1039
|
+
}
|
|
1040
|
+
return renderCellPair(
|
|
1041
|
+
cell,
|
|
1042
|
+
column,
|
|
1043
|
+
cellColors,
|
|
1044
|
+
opts.colorize,
|
|
1045
|
+
style,
|
|
1046
|
+
palette,
|
|
1047
|
+
laneOverride,
|
|
1048
|
+
arrowOverride,
|
|
1049
|
+
arcLaneOverride,
|
|
1050
|
+
);
|
|
1051
|
+
})
|
|
1052
|
+
.join('');
|
|
382
1053
|
let laneSpan = row.cells.length;
|
|
383
1054
|
if (row.kind === 'node') {
|
|
384
1055
|
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
385
|
-
if (
|
|
1056
|
+
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
386
1057
|
laneSpan = 1;
|
|
387
1058
|
} else {
|
|
388
|
-
|
|
1059
|
+
let lastActiveColumn = -1;
|
|
1060
|
+
for (let column = row.cells.length - 1; column >= 0; column--) {
|
|
1061
|
+
if (row.cells[column]?.kind !== 'empty') {
|
|
1062
|
+
lastActiveColumn = column;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
|
|
389
1067
|
}
|
|
390
1068
|
}
|
|
391
1069
|
const labelColumn =
|
|
392
1070
|
row.kind === 'edge'
|
|
393
|
-
?
|
|
1071
|
+
? maxEdgePrefixWidth
|
|
394
1072
|
: wideLabelColumn !== undefined &&
|
|
395
1073
|
(nodeHasArcDecoration(row) || row.contractHash !== undefined)
|
|
396
1074
|
? wideLabelColumn
|
|
@@ -400,49 +1078,130 @@ export function renderMigrationGraphTree(
|
|
|
400
1078
|
row.edge?.from === EMPTY_CONTRACT_HASH &&
|
|
401
1079
|
(row.laneIndex ?? 0) === 0
|
|
402
1080
|
) {
|
|
1081
|
+
// Init edge (∅ → first): only the first cell is rendered (the edge-lane cell).
|
|
1082
|
+
// rowLaneOverride is correct here — it comes from the edge's own annotation.
|
|
403
1083
|
gutter = row.cells
|
|
404
1084
|
.slice(0, 1)
|
|
405
|
-
.map((cell) =>
|
|
1085
|
+
.map((cell, column) =>
|
|
1086
|
+
renderCellPair(
|
|
1087
|
+
cell,
|
|
1088
|
+
column,
|
|
1089
|
+
cellColors,
|
|
1090
|
+
opts.colorize,
|
|
1091
|
+
style,
|
|
1092
|
+
palette,
|
|
1093
|
+
rowLaneOverride,
|
|
1094
|
+
rowArrowOverride,
|
|
1095
|
+
),
|
|
1096
|
+
)
|
|
406
1097
|
.join('');
|
|
407
1098
|
} else if (row.kind === 'node' && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) {
|
|
1099
|
+
// Node gutter slice: may contain vertical-pass cells belonging to other edges.
|
|
1100
|
+
// Classify each cell by its own migrationHash so pass-through lanes carry the
|
|
1101
|
+
// correct colour, not the node's highlight.
|
|
408
1102
|
gutter = row.cells
|
|
409
1103
|
.slice(0, laneSpan)
|
|
410
|
-
.map((cell) =>
|
|
1104
|
+
.map((cell, column) => {
|
|
1105
|
+
let cellLaneOverride = rowLaneOverride;
|
|
1106
|
+
let cellArrowOverride = rowArrowOverride;
|
|
1107
|
+
if (pathHighlightActive && cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1108
|
+
const hashForCell =
|
|
1109
|
+
cell.kind === 'arc-crossing' &&
|
|
1110
|
+
'arcMigrationHash' in cell &&
|
|
1111
|
+
cell.arcMigrationHash !== undefined
|
|
1112
|
+
? cell.arcMigrationHash
|
|
1113
|
+
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1114
|
+
? cell.migrationHash
|
|
1115
|
+
: undefined;
|
|
1116
|
+
if (hashForCell !== undefined) {
|
|
1117
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1118
|
+
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
1119
|
+
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return renderCellPair(
|
|
1123
|
+
cell,
|
|
1124
|
+
column,
|
|
1125
|
+
cellColors,
|
|
1126
|
+
opts.colorize,
|
|
1127
|
+
style,
|
|
1128
|
+
palette,
|
|
1129
|
+
cellLaneOverride,
|
|
1130
|
+
cellArrowOverride,
|
|
1131
|
+
);
|
|
1132
|
+
})
|
|
411
1133
|
.join('');
|
|
412
1134
|
} else if (gutter.length < laneSpan * 2) {
|
|
413
1135
|
gutter = gutter.padEnd(laneSpan * 2, ' ');
|
|
414
1136
|
}
|
|
415
|
-
const dirNameWidth =
|
|
416
|
-
|
|
1137
|
+
const dirNameWidth =
|
|
1138
|
+
row.kind === 'edge'
|
|
1139
|
+
? edgeDirNameWidth
|
|
1140
|
+
: rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
|
|
417
1141
|
const gutterPad = padVisible(gutter, labelColumn);
|
|
418
1142
|
|
|
419
1143
|
if (row.kind === 'node') {
|
|
420
1144
|
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
421
1145
|
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
1146
|
+
// The ∅ node row's trailing cells are vertical-pass lanes belonging to arc edges.
|
|
1147
|
+
// Classify each by its own migrationHash so they carry the correct path-highlight
|
|
1148
|
+
// colour rather than the rotation code that falls out of the ambient lane styler.
|
|
422
1149
|
const trailingLanes = row.cells
|
|
423
1150
|
.slice(1)
|
|
424
|
-
.map((cell) =>
|
|
1151
|
+
.map((cell, offset) => {
|
|
1152
|
+
let cellLaneOverride = rowLaneOverride;
|
|
1153
|
+
let cellArrowOverride = rowArrowOverride;
|
|
1154
|
+
if (pathHighlightActive && cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1155
|
+
const hashForCell =
|
|
1156
|
+
cell.kind === 'arc-crossing' &&
|
|
1157
|
+
'arcMigrationHash' in cell &&
|
|
1158
|
+
cell.arcMigrationHash !== undefined
|
|
1159
|
+
? cell.arcMigrationHash
|
|
1160
|
+
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1161
|
+
? cell.migrationHash
|
|
1162
|
+
: undefined;
|
|
1163
|
+
if (hashForCell !== undefined) {
|
|
1164
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1165
|
+
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
1166
|
+
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return renderCellPair(
|
|
1170
|
+
cell,
|
|
1171
|
+
offset + 1,
|
|
1172
|
+
cellColors,
|
|
1173
|
+
opts.colorize,
|
|
1174
|
+
style,
|
|
1175
|
+
palette,
|
|
1176
|
+
cellLaneOverride,
|
|
1177
|
+
cellArrowOverride,
|
|
1178
|
+
);
|
|
1179
|
+
})
|
|
425
1180
|
.join('');
|
|
426
1181
|
const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
lines.push(emptyGutter
|
|
1182
|
+
const overlays = overlayNamesForContract(contractHash, opts);
|
|
1183
|
+
if (overlays.markers.length === 0 && overlays.refs.length === 0) {
|
|
1184
|
+
lines.push(trimTrailingWhitespace(emptyGutter));
|
|
430
1185
|
continue;
|
|
431
1186
|
}
|
|
432
|
-
const overlay = style.refs
|
|
433
|
-
lines.push(`${
|
|
1187
|
+
const overlay = formatContractNodeOverlays(style, overlays.markers, overlays.refs);
|
|
1188
|
+
lines.push(trimTrailingWhitespace(`${emptyGutter}${' '.repeat(LABEL_GAP)}${overlay}`));
|
|
434
1189
|
continue;
|
|
435
1190
|
}
|
|
436
|
-
|
|
1191
|
+
// In path-highlight mode, off-path nodes use `rowStyle.hashOverride` (uniform dim) so
|
|
1192
|
+
// inner ANSI codes (e.g. dim+cyan of `style.sourceHash`) cannot override the outer dim.
|
|
1193
|
+
// On-path nodes use `style.sourceHash` as normal (neutral purple-ish hash colour).
|
|
1194
|
+
const hashTextStyler = rowStyle?.hashOverride ?? style.sourceHash;
|
|
1195
|
+
const hashText = hashTextStyler(
|
|
437
1196
|
abbreviateHash(contractHash, hashLength, palette.emptySource),
|
|
438
1197
|
);
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
lines.push(`${gutterPad}${hashText}${overlayPad}${overlay}
|
|
1198
|
+
const overlays = overlayNamesForContract(contractHash, opts);
|
|
1199
|
+
const hasOverlays = overlays.markers.length > 0 || overlays.refs.length > 0;
|
|
1200
|
+
const overlayPad = hasOverlays ? ' '.repeat(LABEL_GAP) : '';
|
|
1201
|
+
const overlay = hasOverlays
|
|
1202
|
+
? formatContractNodeOverlays(style, overlays.markers, overlays.refs)
|
|
1203
|
+
: '';
|
|
1204
|
+
lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
|
|
446
1205
|
continue;
|
|
447
1206
|
}
|
|
448
1207
|
|
|
@@ -450,10 +1209,129 @@ export function renderMigrationGraphTree(
|
|
|
450
1209
|
if (edge === undefined) continue;
|
|
451
1210
|
|
|
452
1211
|
const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
1212
|
+
const laneIndex = row.laneIndex ?? 0;
|
|
1213
|
+
|
|
1214
|
+
// The gutter is already coloured via the per-cell overrides threaded into renderCellPair.
|
|
1215
|
+
const edgeGutterPad = padVisible(gutter, labelColumn);
|
|
1216
|
+
|
|
1217
|
+
let dirName: string;
|
|
1218
|
+
if (rowStyle !== undefined) {
|
|
1219
|
+
// Path-highlight mode (on-path or off-path annotation present):
|
|
1220
|
+
// `rowStyle.dirName` is set by PATH_HIGHLIGHT_STYLES — bold for on-path, forcedDim for off-path.
|
|
1221
|
+
// Rotation is suppressed entirely for both roles.
|
|
1222
|
+
// When rowStyle is undefined (unannotated row or non-show command), this branch is not entered.
|
|
1223
|
+
const dirNameStyler = rowStyle.dirName ?? style.dirName;
|
|
1224
|
+
dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
|
|
1225
|
+
} else {
|
|
1226
|
+
// Normal mode: lane hue for branched lanes (column ≥ 1), bold-only for column 0.
|
|
1227
|
+
const dirNameStyler =
|
|
1228
|
+
opts.colorize && laneIndex > NEUTRAL_LANE_COLUMN
|
|
1229
|
+
? (text: string) => forcedBold(laneColorForColumn(laneIndex)(text))
|
|
1230
|
+
: style.dirName;
|
|
1231
|
+
dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Pass hashOverride from path-highlight styles so formatEdgeHashColumn applies it to ALL
|
|
1235
|
+
// sub-stylers (sourceHash, destHash, arrow glyph). Wrapping already-styled text in an outer
|
|
1236
|
+
// colour does not work — inner ANSI codes override the outer at the terminal level.
|
|
1237
|
+
const hashColumnOverride = rowStyle?.hashOverride;
|
|
1238
|
+
const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette, hashColumnOverride);
|
|
1239
|
+
const annotationSuffix = formatEdgeAnnotationSuffix(edge.migrationHash, opts, style);
|
|
1240
|
+
lines.push(
|
|
1241
|
+
trimTrailingWhitespace(`${edgeGutterPad}${dirName}${hashColumn}${annotationSuffix}`),
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return lines.join('\n');
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Format a single on-path migration row for the `migrate --show` run-list.
|
|
1250
|
+
*
|
|
1251
|
+
* Uses the SAME styling as the tree renderer's on-path rows (PATH_HIGHLIGHT_STYLES.onPath)
|
|
1252
|
+
* so the run-list and graph tree are byte-for-byte identical in their name/hash columns.
|
|
1253
|
+
* The gutter is omitted — the list has no graph structure.
|
|
1254
|
+
*
|
|
1255
|
+
* This is the SINGLE code path for on-path row styling shared by both the graph tree
|
|
1256
|
+
* and the "Will run, in order:" list. To change the on-path colour, edit PATH_HIGHLIGHT_STYLES.
|
|
1257
|
+
*/
|
|
1258
|
+
export function formatOnPathMigrationRow(
|
|
1259
|
+
dirName: string,
|
|
1260
|
+
from: string,
|
|
1261
|
+
to: string,
|
|
1262
|
+
dirNameWidth: number,
|
|
1263
|
+
colorize: boolean,
|
|
1264
|
+
glyphMode: GlyphMode,
|
|
1265
|
+
): string {
|
|
1266
|
+
const palette = paletteFor(glyphMode);
|
|
1267
|
+
const style = createAnsiMigrationListStyler({ useColor: colorize });
|
|
1268
|
+
// Use PATH_HIGHLIGHT_STYLES.onPath as the single seam for on-path colour.
|
|
1269
|
+
// Pass `style` and `colorize` so the lane/glyph stylers respect the colour gate.
|
|
1270
|
+
const s = PATH_HIGHLIGHT_STYLES.onPath(style, colorize);
|
|
1271
|
+
const styledDirName = `${s.dirName(dirName)}${' '.repeat(Math.max(0, dirNameWidth - dirName.length))}`;
|
|
1272
|
+
const hashLength = MIGRATION_LIST_HASH_WIDTH;
|
|
1273
|
+
const emptySource = palette.emptySource;
|
|
1274
|
+
const fromAbbr =
|
|
1275
|
+
from === EMPTY_CONTRACT_HASH
|
|
1276
|
+
? padFromHashColumn(style.glyph(emptySource), hashLength)
|
|
1277
|
+
: padFromHashColumn(style.sourceHash(abbreviateHashShort(from, hashLength)), hashLength);
|
|
1278
|
+
const toAbbr =
|
|
1279
|
+
to === EMPTY_CONTRACT_HASH
|
|
1280
|
+
? style.glyph(emptySource)
|
|
1281
|
+
: style.destHash(abbreviateHashShort(to, hashLength));
|
|
1282
|
+
const arrow = style.glyph(palette.forwardArrow);
|
|
1283
|
+
return `${styledDirName} ${fromAbbr} ${arrow} ${toAbbr}`;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function abbreviateHashShort(hash: string, length: number): string {
|
|
1287
|
+
const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
1288
|
+
return stripped.slice(0, length);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
export interface RenderMigrationGraphLegendOptions {
|
|
1292
|
+
readonly colorize: boolean;
|
|
1293
|
+
readonly glyphMode?: GlyphMode;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function formatLegendExampleMarkers(colorize: boolean): string {
|
|
1297
|
+
if (!colorize) {
|
|
1298
|
+
return '@contract @db';
|
|
456
1299
|
}
|
|
1300
|
+
const sigil = green('@');
|
|
1301
|
+
return `${sigil + bold(green('contract'))} ${sigil}${green('db')}`;
|
|
1302
|
+
}
|
|
457
1303
|
|
|
1304
|
+
/**
|
|
1305
|
+
* A compact key for the tree visual language: the contract node glyph, the
|
|
1306
|
+
* in-lane direction arrows, the empty baseline, the system-marker `<…>` and
|
|
1307
|
+
* user-ref `(…)` bracket conventions (two illustrative example lines), and a
|
|
1308
|
+
* worked sample of the data-column `from → to` migration hash arrow.
|
|
1309
|
+
*
|
|
1310
|
+
* Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
|
|
1311
|
+
* tree renderer, so the key matches whatever the graph itself drew and stays
|
|
1312
|
+
* pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
|
|
1313
|
+
* line that separates this stderr key from the tree on stdout.
|
|
1314
|
+
*/
|
|
1315
|
+
export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptions): string {
|
|
1316
|
+
const palette = paletteFor(opts.glyphMode ?? 'unicode');
|
|
1317
|
+
const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
1318
|
+
const node = palette.node.trimEnd();
|
|
1319
|
+
const sampleArrow = `${style.sourceHash('aaaaaa')} ${style.glyph(palette.forwardArrow)} ${style.destHash('bbbbbb')}`;
|
|
1320
|
+
const statusGlyphs = overlayStatusGlyphs(opts.glyphMode ?? 'unicode');
|
|
1321
|
+
const appliedPending = opts.colorize
|
|
1322
|
+
? ` ${green(statusGlyphs.applied)} ${style.summary('applied')} ${yellow(statusGlyphs.pending)} ${style.summary('pending')}`
|
|
1323
|
+
: ` ${statusGlyphs.applied} ${style.summary('applied')} ${statusGlyphs.pending} ${style.summary('pending')}`;
|
|
1324
|
+
const exampleMarkers = formatLegendExampleMarkers(opts.colorize);
|
|
1325
|
+
const exampleRefs = opts.colorize ? style.refs(['prod', 'staging']) : '(prod, staging)';
|
|
1326
|
+
const lines = [
|
|
1327
|
+
'Legend:',
|
|
1328
|
+
` ${style.kind(node)} ${style.summary('contract')} ${style.kind(palette.edgeArrow.forward)} ${style.summary('forward')} ${style.kind(palette.edgeArrow.rollback)} ${style.summary('rollback')}`,
|
|
1329
|
+
` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
|
|
1330
|
+
appliedPending,
|
|
1331
|
+
` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
|
|
1332
|
+
` ${exampleMarkers} ${style.summary('reserved markers — also typeable as --from/--to tokens')}`,
|
|
1333
|
+
` ${exampleRefs} ${style.summary('user-defined refs')}`,
|
|
1334
|
+
` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
|
|
1335
|
+
];
|
|
458
1336
|
return lines.join('\n');
|
|
459
1337
|
}
|