@prisma-next/cli 0.12.0-dev.52 → 0.12.0-dev.54
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 +7 -7
- package/dist/{client-BfNYTr8w.mjs → client-DIcitJdy.mjs} +95 -38
- package/dist/client-DIcitJdy.mjs.map +1 -0
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +2 -2
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +2 -2
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +35 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +287 -6
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.mjs +2 -2
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +4 -2
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +1 -0
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-show.mjs +2 -2
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/{contract-infer-sUml0McE.mjs → contract-infer-BAdhYGQH.mjs} +2 -2
- package/dist/{contract-infer-sUml0McE.mjs.map → contract-infer-BAdhYGQH.mjs.map} +1 -1
- package/dist/{db-verify-DSZcaGQs.mjs → db-verify-CiUCDXnv.mjs} +2 -2
- package/dist/{db-verify-DSZcaGQs.mjs.map → db-verify-CiUCDXnv.mjs.map} +1 -1
- package/dist/exports/control-api.mjs +1 -1
- package/dist/{inspect-live-schema-B44OravN.mjs → inspect-live-schema-DegaqKFT.mjs} +2 -2
- package/dist/{inspect-live-schema-B44OravN.mjs.map → inspect-live-schema-DegaqKFT.mjs.map} +1 -1
- package/dist/{migration-check-BxWlQBOs.mjs → migration-check-ldvGvsgM.mjs} +2 -3
- package/dist/{migration-check-BxWlQBOs.mjs.map → migration-check-ldvGvsgM.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-BvVTTjzK.mjs → migration-command-scaffold-D6UeN71F.mjs} +2 -2
- package/dist/{migration-command-scaffold-BvVTTjzK.mjs.map → migration-command-scaffold-D6UeN71F.mjs.map} +1 -1
- package/dist/{migration-graph-space-render-CeNXh_Wy.mjs → migration-graph-space-render-B0HkTNj3.mjs} +488 -84
- package/dist/migration-graph-space-render-B0HkTNj3.mjs.map +1 -0
- package/dist/{migration-list-vJWFuXca.mjs → migration-list-mYmj2j33.mjs} +6 -4
- package/dist/migration-list-mYmj2j33.mjs.map +1 -0
- package/dist/{migration-log-DD01Jm_i.mjs → migration-log-Dzs18GU7.mjs} +3 -3
- package/dist/{migration-log-DD01Jm_i.mjs.map → migration-log-Dzs18GU7.mjs.map} +1 -1
- package/dist/{migration-path-target-UkxkgXnv.mjs → migration-path-target-DK-B7POa.mjs} +1 -1
- package/dist/{migration-path-target-UkxkgXnv.mjs.map → migration-path-target-DK-B7POa.mjs.map} +1 -1
- package/dist/{migration-status-jL5ajRrB.mjs → migration-status-BGKGc0iR.mjs} +7 -5
- package/dist/migration-status-BGKGc0iR.mjs.map +1 -0
- package/dist/{schemas-DJY2O09F.mjs → schemas-B4xeMrNt.mjs} +1 -1
- package/dist/{schemas-DJY2O09F.mjs.map → schemas-B4xeMrNt.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migrate.ts +512 -2
- package/src/commands/migration-graph.ts +2 -0
- package/src/commands/migration-list.ts +3 -0
- package/src/commands/migration-status.ts +4 -0
- package/src/control-api/operations/migrate.ts +149 -56
- package/src/utils/formatters/migration-graph-layout.ts +187 -42
- package/src/utils/formatters/migration-graph-space-render.ts +11 -1
- package/src/utils/formatters/migration-graph-tree-render.ts +609 -59
- package/src/utils/formatters/migration-list-render.ts +12 -0
- package/src/utils/formatters/migration-list-styler.ts +5 -7
- package/dist/client-BfNYTr8w.mjs.map +0 -1
- package/dist/migration-graph-space-render-CeNXh_Wy.mjs.map +0 -1
- package/dist/migration-list-vJWFuXca.mjs.map +0 -1
- package/dist/migration-status-jL5ajRrB.mjs.map +0 -1
|
@@ -46,6 +46,12 @@ export interface MigrationEdgeAnnotation {
|
|
|
46
46
|
readonly status?: 'applied' | 'pending';
|
|
47
47
|
readonly operationCount?: number;
|
|
48
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';
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export interface RenderMigrationGraphTreeOptions {
|
|
@@ -53,6 +59,13 @@ export interface RenderMigrationGraphTreeOptions {
|
|
|
53
59
|
readonly edgeAnnotationsByHash?: ReadonlyMap<string, MigrationEdgeAnnotation>;
|
|
54
60
|
readonly dbHash?: string;
|
|
55
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;
|
|
56
69
|
readonly activeRefName?: string;
|
|
57
70
|
readonly hashLength?: number;
|
|
58
71
|
readonly globalMaxEdgeTreePrefixWidth?: number;
|
|
@@ -153,12 +166,76 @@ function arrowForEdgeKind(
|
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
/**
|
|
156
|
-
* Forced
|
|
157
|
-
* (
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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.
|
|
160
180
|
*/
|
|
161
|
-
const {
|
|
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;
|
|
162
239
|
|
|
163
240
|
function laneStylerForColumn(
|
|
164
241
|
colorColumn: number,
|
|
@@ -226,6 +303,11 @@ function renderConnectorTee(
|
|
|
226
303
|
* marker takes its own lane's hue (so each node visibly belongs to its branch);
|
|
227
304
|
* the connector follows the arc it belongs to (its owning back-lane hue).
|
|
228
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.
|
|
229
311
|
*/
|
|
230
312
|
function renderNodeMarkerPair(
|
|
231
313
|
pair: string,
|
|
@@ -233,9 +315,12 @@ function renderNodeMarkerPair(
|
|
|
233
315
|
arcColumn: number,
|
|
234
316
|
colorize: boolean,
|
|
235
317
|
style: MigrationListStyler,
|
|
318
|
+
laneOverride?: (text: string) => string,
|
|
319
|
+
arcLaneOverride?: (text: string) => string,
|
|
236
320
|
): string {
|
|
237
|
-
const marker = laneStylerForColumn(nodeColumn, colorize, style);
|
|
238
|
-
const connector =
|
|
321
|
+
const marker = laneOverride ?? laneStylerForColumn(nodeColumn, colorize, style);
|
|
322
|
+
const connector =
|
|
323
|
+
arcLaneOverride ?? laneOverride ?? laneStylerForColumn(arcColumn, colorize, style);
|
|
239
324
|
return marker(pair.slice(0, 1)) + connector(pair.slice(1));
|
|
240
325
|
}
|
|
241
326
|
|
|
@@ -246,17 +331,44 @@ function renderCellPair(
|
|
|
246
331
|
colorize: boolean,
|
|
247
332
|
style: MigrationListStyler,
|
|
248
333
|
palette: MigrationGraphTreeGlyphPalette,
|
|
334
|
+
laneOverride?: (text: string) => string,
|
|
335
|
+
arrowOverride?: (text: string) => string,
|
|
336
|
+
arcLaneOverride?: (text: string) => string,
|
|
249
337
|
): string {
|
|
250
338
|
const laneColumn = colors.lane[column] ?? column;
|
|
251
|
-
|
|
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));
|
|
252
348
|
switch (cell.kind) {
|
|
253
349
|
case 'node': {
|
|
254
350
|
const arcColumn = colors.connector[column] ?? NEUTRAL_LANE_COLUMN;
|
|
255
351
|
if (cell.arcLand === true) {
|
|
256
|
-
return renderNodeMarkerPair(
|
|
352
|
+
return renderNodeMarkerPair(
|
|
353
|
+
palette.arcLand,
|
|
354
|
+
column,
|
|
355
|
+
arcColumn,
|
|
356
|
+
colorize,
|
|
357
|
+
style,
|
|
358
|
+
laneOverride,
|
|
359
|
+
arcLaneOverride,
|
|
360
|
+
);
|
|
257
361
|
}
|
|
258
362
|
if (cell.arcTee === true) {
|
|
259
|
-
return renderNodeMarkerPair(
|
|
363
|
+
return renderNodeMarkerPair(
|
|
364
|
+
palette.arcTee,
|
|
365
|
+
column,
|
|
366
|
+
arcColumn,
|
|
367
|
+
colorize,
|
|
368
|
+
style,
|
|
369
|
+
laneOverride,
|
|
370
|
+
arcLaneOverride,
|
|
371
|
+
);
|
|
260
372
|
}
|
|
261
373
|
return lane(palette.node);
|
|
262
374
|
}
|
|
@@ -264,12 +376,7 @@ function renderCellPair(
|
|
|
264
376
|
return lane(palette.verticalPass);
|
|
265
377
|
case 'edge-lane':
|
|
266
378
|
return cell.ownsLabel
|
|
267
|
-
? lane(palette.verticalPass.trimEnd()) +
|
|
268
|
-
branchStylerOrDefault(
|
|
269
|
-
column,
|
|
270
|
-
colorize,
|
|
271
|
-
style.kind,
|
|
272
|
-
)(arrowForEdgeKind(cell.edgeKind, palette))
|
|
379
|
+
? lane(palette.verticalPass.trimEnd()) + arrow(arrowForEdgeKind(cell.edgeKind, palette))
|
|
273
380
|
: lane(palette.verticalPass);
|
|
274
381
|
case 'branch-tee':
|
|
275
382
|
return lane(palette.branchTee);
|
|
@@ -286,13 +393,17 @@ function renderCellPair(
|
|
|
286
393
|
case 'arc-land-corner':
|
|
287
394
|
return lane(palette.arcLandCorner);
|
|
288
395
|
case 'arc-land-tee':
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
);
|
|
296
407
|
case 'arc-crossing':
|
|
297
408
|
return lane(palette.arcLandBridge);
|
|
298
409
|
case 'arc-land-bridge':
|
|
@@ -304,13 +415,28 @@ function renderCellPair(
|
|
|
304
415
|
}
|
|
305
416
|
}
|
|
306
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
|
+
*/
|
|
307
429
|
function renderConnectorRow(
|
|
308
430
|
row: MigrationGraphGridRow,
|
|
309
431
|
gridWidth: number,
|
|
310
432
|
colorize: boolean,
|
|
311
433
|
style: MigrationListStyler,
|
|
312
434
|
palette: MigrationGraphTreeGlyphPalette,
|
|
435
|
+
columnLaneOverride?: ReadonlyMap<number, (text: string) => string>,
|
|
313
436
|
): string {
|
|
437
|
+
const resolvedLane = (column: number): ((text: string) => string) =>
|
|
438
|
+
columnLaneOverride?.get(column) ?? laneStylerForColumn(column, colorize, style);
|
|
439
|
+
|
|
314
440
|
const isMerge = row.kind === 'merge-connector';
|
|
315
441
|
if (row.cells.length > 0) {
|
|
316
442
|
const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
|
|
@@ -321,6 +447,67 @@ function renderConnectorRow(
|
|
|
321
447
|
if (cell === undefined) continue;
|
|
322
448
|
const glyphColumn = colors.glyph[column] ?? column;
|
|
323
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
|
+
}
|
|
324
511
|
const lane = laneStylerForColumn(glyphColumn, colorize, style);
|
|
325
512
|
switch (cell.kind) {
|
|
326
513
|
case 'branch-tee':
|
|
@@ -375,7 +562,7 @@ function renderConnectorRow(
|
|
|
375
562
|
const end = row.endLane ?? start;
|
|
376
563
|
// The whole fork/merge run reads as one line in the served lane's hue (the
|
|
377
564
|
// corner it reaches); pass-through columns outside the run keep their own.
|
|
378
|
-
const runLane =
|
|
565
|
+
const runLane = resolvedLane(end);
|
|
379
566
|
let out = '';
|
|
380
567
|
for (let column = 0; column < gridWidth; column++) {
|
|
381
568
|
if (column < start || column > end) out += ' ';
|
|
@@ -411,7 +598,11 @@ function overlayNamesForContract(
|
|
|
411
598
|
if (userRefs) {
|
|
412
599
|
refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
|
|
413
600
|
}
|
|
414
|
-
if (
|
|
601
|
+
if (
|
|
602
|
+
opts.isAppSpace !== false &&
|
|
603
|
+
opts.contractHash === contractHash &&
|
|
604
|
+
contractHash !== EMPTY_CONTRACT_HASH
|
|
605
|
+
) {
|
|
415
606
|
markers.push(CONTRACT_MARKER_NAME);
|
|
416
607
|
}
|
|
417
608
|
if (opts.dbHash === contractHash) {
|
|
@@ -453,6 +644,7 @@ function formatEdgeAnnotationSuffix(
|
|
|
453
644
|
if (annotation === undefined) {
|
|
454
645
|
return '';
|
|
455
646
|
}
|
|
647
|
+
const isOffPath = annotation.pathHighlight === 'off-path';
|
|
456
648
|
const segments: string[] = [];
|
|
457
649
|
if (annotation.operationCount !== undefined) {
|
|
458
650
|
segments.push(`${annotation.operationCount} ops`);
|
|
@@ -472,32 +664,49 @@ function formatEdgeAnnotationSuffix(
|
|
|
472
664
|
segments.push(styler(`${glyph} ${label}`));
|
|
473
665
|
}
|
|
474
666
|
}
|
|
667
|
+
if (annotation.pathHighlight === 'on-path') {
|
|
668
|
+
const glyph = opts.glyphMode === 'ascii' ? '>' : '↑';
|
|
669
|
+
segments.push(`${glyph} will run`);
|
|
670
|
+
}
|
|
475
671
|
if (segments.length === 0) {
|
|
476
672
|
return '';
|
|
477
673
|
}
|
|
478
|
-
|
|
674
|
+
const suffix = ` ${segments.join(' ')}`;
|
|
675
|
+
return opts.colorize && isOffPath ? forcedDim(suffix) : suffix;
|
|
479
676
|
}
|
|
480
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
|
+
*/
|
|
481
686
|
function formatEdgeHashColumn(
|
|
482
687
|
edge: ClassifiedEdge,
|
|
483
688
|
style: MigrationListStyler,
|
|
484
689
|
hashLength: number,
|
|
485
690
|
palette: MigrationGraphTreeGlyphPalette,
|
|
691
|
+
hashOverride?: (text: string) => string,
|
|
486
692
|
): string {
|
|
693
|
+
const src = hashOverride ?? style.sourceHash;
|
|
694
|
+
const dst = hashOverride ?? style.destHash;
|
|
695
|
+
const glyph = hashOverride ?? style.glyph;
|
|
487
696
|
if (edge.kind === 'self') {
|
|
488
697
|
const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
|
|
489
|
-
const source = padFromHashColumn(
|
|
490
|
-
return `${source} ${
|
|
698
|
+
const source = padFromHashColumn(src(hash), hashLength);
|
|
699
|
+
return `${source} ${glyph(palette.forwardArrow)} ${dst(hash)}`;
|
|
491
700
|
}
|
|
492
701
|
const source =
|
|
493
702
|
edge.from === EMPTY_CONTRACT_HASH
|
|
494
|
-
? padFromHashColumn(
|
|
703
|
+
? padFromHashColumn(glyph(palette.emptySource), hashLength)
|
|
495
704
|
: padFromHashColumn(
|
|
496
|
-
|
|
705
|
+
src(abbreviateHash(edge.from, hashLength, palette.emptySource)),
|
|
497
706
|
hashLength,
|
|
498
707
|
);
|
|
499
|
-
const arrow =
|
|
500
|
-
const dest =
|
|
708
|
+
const arrow = glyph(palette.forwardArrow);
|
|
709
|
+
const dest = dst(abbreviateHash(edge.to, hashLength, palette.emptySource));
|
|
501
710
|
return `${source} ${arrow} ${dest}`;
|
|
502
711
|
}
|
|
503
712
|
|
|
@@ -610,6 +819,85 @@ export function renderMigrationGraphTree(
|
|
|
610
819
|
opts.globalMaxEdgeTreePrefixWidth ?? maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
611
820
|
const edgeDirNameWidth = rowDirNameWidth(maxEdgePrefixWidth, effectiveMaxDirNameLen, dirNameGap);
|
|
612
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
|
+
}
|
|
900
|
+
|
|
613
901
|
const lines: string[] = [];
|
|
614
902
|
|
|
615
903
|
for (let rowIndex = 0; rowIndex < model.rows.length; rowIndex++) {
|
|
@@ -622,17 +910,145 @@ export function renderMigrationGraphTree(
|
|
|
622
910
|
}
|
|
623
911
|
|
|
624
912
|
if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
|
|
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
|
+
}
|
|
625
940
|
lines.push(
|
|
626
|
-
trimTrailingWhitespace(
|
|
941
|
+
trimTrailingWhitespace(
|
|
942
|
+
renderConnectorRow(
|
|
943
|
+
row,
|
|
944
|
+
gridWidth,
|
|
945
|
+
opts.colorize,
|
|
946
|
+
style,
|
|
947
|
+
palette,
|
|
948
|
+
connectorColumnOverride,
|
|
949
|
+
),
|
|
950
|
+
),
|
|
627
951
|
);
|
|
628
952
|
continue;
|
|
629
953
|
}
|
|
630
954
|
|
|
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.
|
|
631
985
|
const cellColors = resolveRowArcLaneColors(row.cells);
|
|
632
986
|
let gutter = row.cells
|
|
633
|
-
.map((cell, column) =>
|
|
634
|
-
|
|
635
|
-
|
|
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
|
+
})
|
|
636
1052
|
.join('');
|
|
637
1053
|
let laneSpan = row.cells.length;
|
|
638
1054
|
if (row.kind === 'node') {
|
|
@@ -662,18 +1078,58 @@ export function renderMigrationGraphTree(
|
|
|
662
1078
|
row.edge?.from === EMPTY_CONTRACT_HASH &&
|
|
663
1079
|
(row.laneIndex ?? 0) === 0
|
|
664
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.
|
|
665
1083
|
gutter = row.cells
|
|
666
1084
|
.slice(0, 1)
|
|
667
1085
|
.map((cell, column) =>
|
|
668
|
-
renderCellPair(
|
|
1086
|
+
renderCellPair(
|
|
1087
|
+
cell,
|
|
1088
|
+
column,
|
|
1089
|
+
cellColors,
|
|
1090
|
+
opts.colorize,
|
|
1091
|
+
style,
|
|
1092
|
+
palette,
|
|
1093
|
+
rowLaneOverride,
|
|
1094
|
+
rowArrowOverride,
|
|
1095
|
+
),
|
|
669
1096
|
)
|
|
670
1097
|
.join('');
|
|
671
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.
|
|
672
1102
|
gutter = row.cells
|
|
673
1103
|
.slice(0, laneSpan)
|
|
674
|
-
.map((cell, column) =>
|
|
675
|
-
|
|
676
|
-
|
|
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
|
+
})
|
|
677
1133
|
.join('');
|
|
678
1134
|
} else if (gutter.length < laneSpan * 2) {
|
|
679
1135
|
gutter = gutter.padEnd(laneSpan * 2, ' ');
|
|
@@ -687,11 +1143,40 @@ export function renderMigrationGraphTree(
|
|
|
687
1143
|
if (row.kind === 'node') {
|
|
688
1144
|
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
689
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.
|
|
690
1149
|
const trailingLanes = row.cells
|
|
691
1150
|
.slice(1)
|
|
692
|
-
.map((cell, offset) =>
|
|
693
|
-
|
|
694
|
-
|
|
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
|
+
})
|
|
695
1180
|
.join('');
|
|
696
1181
|
const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
|
|
697
1182
|
const overlays = overlayNamesForContract(contractHash, opts);
|
|
@@ -703,7 +1188,11 @@ export function renderMigrationGraphTree(
|
|
|
703
1188
|
lines.push(trimTrailingWhitespace(`${emptyGutter}${' '.repeat(LABEL_GAP)}${overlay}`));
|
|
704
1189
|
continue;
|
|
705
1190
|
}
|
|
706
|
-
|
|
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(
|
|
707
1196
|
abbreviateHash(contractHash, hashLength, palette.emptySource),
|
|
708
1197
|
);
|
|
709
1198
|
const overlays = overlayNamesForContract(contractHash, opts);
|
|
@@ -721,21 +1210,84 @@ export function renderMigrationGraphTree(
|
|
|
721
1210
|
|
|
722
1211
|
const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
723
1212
|
const laneIndex = row.laneIndex ?? 0;
|
|
724
|
-
|
|
725
|
-
//
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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);
|
|
732
1239
|
const annotationSuffix = formatEdgeAnnotationSuffix(edge.migrationHash, opts, style);
|
|
733
|
-
lines.push(
|
|
1240
|
+
lines.push(
|
|
1241
|
+
trimTrailingWhitespace(`${edgeGutterPad}${dirName}${hashColumn}${annotationSuffix}`),
|
|
1242
|
+
);
|
|
734
1243
|
}
|
|
735
1244
|
|
|
736
1245
|
return lines.join('\n');
|
|
737
1246
|
}
|
|
738
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
|
+
|
|
739
1291
|
export interface RenderMigrationGraphLegendOptions {
|
|
740
1292
|
readonly colorize: boolean;
|
|
741
1293
|
readonly glyphMode?: GlyphMode;
|
|
@@ -743,12 +1295,10 @@ export interface RenderMigrationGraphLegendOptions {
|
|
|
743
1295
|
|
|
744
1296
|
function formatLegendExampleMarkers(colorize: boolean): string {
|
|
745
1297
|
if (!colorize) {
|
|
746
|
-
return '
|
|
1298
|
+
return '@contract @db';
|
|
747
1299
|
}
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
const separator = green(', ');
|
|
751
|
-
return open + green('contract') + separator + green('db') + close;
|
|
1300
|
+
const sigil = green('@');
|
|
1301
|
+
return `${sigil + bold(green('contract'))} ${sigil}${green('db')}`;
|
|
752
1302
|
}
|
|
753
1303
|
|
|
754
1304
|
/**
|
|
@@ -779,7 +1329,7 @@ export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptio
|
|
|
779
1329
|
` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
|
|
780
1330
|
appliedPending,
|
|
781
1331
|
` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
|
|
782
|
-
` ${exampleMarkers} ${style.summary('
|
|
1332
|
+
` ${exampleMarkers} ${style.summary('reserved markers — also typeable as --from/--to tokens')}`,
|
|
783
1333
|
` ${exampleRefs} ${style.summary('user-defined refs')}`,
|
|
784
1334
|
` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
|
|
785
1335
|
];
|