@prisma-next/cli 0.11.0-dev.47 → 0.11.0-dev.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cli.mjs +9 -9
  2. package/dist/{command-helpers-CI8P5Xyd.mjs → command-helpers-4UNsRRc4.mjs} +3 -181
  3. package/dist/command-helpers-4UNsRRc4.mjs.map +1 -0
  4. package/dist/commands/contract-emit.mjs +1 -1
  5. package/dist/commands/contract-infer.mjs +1 -1
  6. package/dist/commands/db-init.mjs +3 -3
  7. package/dist/commands/db-schema.mjs +3 -3
  8. package/dist/commands/db-sign.mjs +2 -2
  9. package/dist/commands/db-update.mjs +3 -3
  10. package/dist/commands/db-verify.mjs +1 -1
  11. package/dist/commands/migrate.mjs +2 -2
  12. package/dist/commands/migration-check.mjs +1 -1
  13. package/dist/commands/migration-graph.mjs +1 -1
  14. package/dist/commands/migration-list.d.mts +1 -1
  15. package/dist/commands/migration-list.d.mts.map +1 -1
  16. package/dist/commands/migration-list.mjs +1 -1
  17. package/dist/commands/migration-log.mjs +1 -1
  18. package/dist/commands/migration-new.mjs +1 -1
  19. package/dist/commands/migration-plan.mjs +1 -1
  20. package/dist/commands/migration-show.mjs +2 -2
  21. package/dist/commands/migration-status.mjs +2 -2
  22. package/dist/commands/ref.mjs +1 -1
  23. package/dist/{contract-emit-DmBG2Nnc.mjs → contract-emit-Bbdnpcjl.mjs} +2 -2
  24. package/dist/{contract-emit-DmBG2Nnc.mjs.map → contract-emit-Bbdnpcjl.mjs.map} +1 -1
  25. package/dist/{contract-infer-BSWFKgI1.mjs → contract-infer-C98ZaRhp.mjs} +3 -3
  26. package/dist/{contract-infer-BSWFKgI1.mjs.map → contract-infer-C98ZaRhp.mjs.map} +1 -1
  27. package/dist/{db-verify-BzpwFyLg.mjs → db-verify-BWl1Yxi-.mjs} +3 -3
  28. package/dist/{db-verify-BzpwFyLg.mjs.map → db-verify-BWl1Yxi-.mjs.map} +1 -1
  29. package/dist/exports/index.mjs +1 -1
  30. package/dist/exports/init-output.d.mts.map +1 -1
  31. package/dist/glyph-mode-CBB4emzO.d.mts +5 -0
  32. package/dist/glyph-mode-CBB4emzO.d.mts.map +1 -0
  33. package/dist/{init-DEssiJ8j.mjs → init-C3Swd5QB.mjs} +2 -2
  34. package/dist/{init-DEssiJ8j.mjs.map → init-C3Swd5QB.mjs.map} +1 -1
  35. package/dist/{inspect-live-schema-DlBM84nh.mjs → inspect-live-schema-BRCWQ-Sr.mjs} +2 -2
  36. package/dist/{inspect-live-schema-DlBM84nh.mjs.map → inspect-live-schema-BRCWQ-Sr.mjs.map} +1 -1
  37. package/dist/{migration-check-CzLbAqIQ.mjs → migration-check-DoskM1nB.mjs} +2 -2
  38. package/dist/{migration-check-CzLbAqIQ.mjs.map → migration-check-DoskM1nB.mjs.map} +1 -1
  39. package/dist/{migration-command-scaffold-Bp8UHnvJ.mjs → migration-command-scaffold-CXLkoIJx.mjs} +2 -2
  40. package/dist/{migration-command-scaffold-Bp8UHnvJ.mjs.map → migration-command-scaffold-CXLkoIJx.mjs.map} +1 -1
  41. package/dist/migration-list-B2-iQ5Jd.mjs +646 -0
  42. package/dist/migration-list-B2-iQ5Jd.mjs.map +1 -0
  43. package/dist/{migration-plan-BLvOmNCu.mjs → migration-plan-BqmIKQpZ.mjs} +2 -2
  44. package/dist/{migration-plan-BLvOmNCu.mjs.map → migration-plan-BqmIKQpZ.mjs.map} +1 -1
  45. package/dist/{migrations-vzQt9LI2.mjs → migrations-BcVTutso.mjs} +2 -2
  46. package/dist/{migrations-vzQt9LI2.mjs.map → migrations-BcVTutso.mjs.map} +1 -1
  47. package/dist/{verify-ktSRQvIS.mjs → verify-DOHbbrub.mjs} +2 -2
  48. package/dist/{verify-ktSRQvIS.mjs.map → verify-DOHbbrub.mjs.map} +1 -1
  49. package/package.json +18 -18
  50. package/src/commands/migration-list.ts +7 -3
  51. package/src/utils/formatters/migration-list-data-column.ts +33 -2
  52. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  53. package/src/utils/formatters/migration-list-graph-render.ts +45 -50
  54. package/src/utils/formatters/migration-list-render.ts +47 -17
  55. package/src/utils/formatters/migration-list-styler.ts +3 -1
  56. package/src/utils/glyph-mode.ts +22 -0
  57. package/src/utils/terminal-ui.ts +1 -5
  58. package/dist/command-helpers-CI8P5Xyd.mjs.map +0 -1
  59. package/dist/migration-list-C2xnaYsT.mjs +0 -279
  60. package/dist/migration-list-C2xnaYsT.mjs.map +0 -1
  61. package/dist/migration-list-graph-render-DKw1AT-e.d.mts +0 -7
  62. package/dist/migration-list-graph-render-DKw1AT-e.d.mts.map +0 -1
@@ -0,0 +1,268 @@
1
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
+ import {
3
+ classifyMigrationListGraphTopology,
4
+ type MigrationEdgeKind,
5
+ type MigrationListGraphTopology,
6
+ } from '@prisma-next/migration-tools/migration-list-graph-topology';
7
+ import type { MigrationListEntry } from '@prisma-next/migration-tools/migration-list-types';
8
+
9
+ export type ConnectorKind = 'fanBelow' | 'joinAbove';
10
+
11
+ export interface MigrationLayoutRow {
12
+ readonly kind: 'migration';
13
+ readonly entry: MigrationListEntry;
14
+ readonly edgeKind: MigrationEdgeKind;
15
+ readonly laneIndex: number;
16
+ readonly passThroughLanes: readonly number[];
17
+ readonly woven: boolean;
18
+ }
19
+
20
+ export interface NodeLineLayoutRow {
21
+ readonly kind: 'nodeLine';
22
+ readonly contractHash: string;
23
+ readonly laneIndex: number;
24
+ }
25
+
26
+ export interface ConnectorLayoutRow {
27
+ readonly kind: 'connector';
28
+ readonly connectorKind: ConnectorKind;
29
+ readonly contractHash: string;
30
+ readonly startLane: number;
31
+ readonly endLane: number;
32
+ readonly branchCount: number;
33
+ }
34
+
35
+ export type LayoutRow = MigrationLayoutRow | NodeLineLayoutRow | ConnectorLayoutRow;
36
+
37
+ export interface MigrationListGraphLayout {
38
+ readonly rows: readonly LayoutRow[];
39
+ }
40
+
41
+ interface LaneState {
42
+ want: string;
43
+ active: boolean;
44
+ }
45
+
46
+ function canonicalFrom(from: string | null): string {
47
+ return from ?? EMPTY_CONTRACT_HASH;
48
+ }
49
+
50
+ function forwardInDegree(topology: MigrationListGraphTopology, hash: string): number {
51
+ return topology.forwardInDegree.get(hash) ?? 0;
52
+ }
53
+
54
+ function forwardOutDegree(topology: MigrationListGraphTopology, hash: string): number {
55
+ return topology.forwardOutDegree.get(hash) ?? 0;
56
+ }
57
+
58
+ function buildForwardProducersByTo(
59
+ entries: readonly MigrationListEntry[],
60
+ kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
61
+ ): Map<string, MigrationListEntry[]> {
62
+ const byTo = new Map<string, MigrationListEntry[]>();
63
+ for (const entry of entries) {
64
+ if (kindByMigrationHash.get(entry.migrationHash) !== 'forward') continue;
65
+ const bucket = byTo.get(entry.to);
66
+ if (bucket) bucket.push(entry);
67
+ else byTo.set(entry.to, [entry]);
68
+ }
69
+ return byTo;
70
+ }
71
+
72
+ function countForwardProducersTo(
73
+ forwardProducersByTo: Map<string, MigrationListEntry[]>,
74
+ contract: string,
75
+ ): number {
76
+ return forwardProducersByTo.get(contract)?.length ?? 0;
77
+ }
78
+
79
+ function hasLaterForwardDepartingFrom(
80
+ entries: readonly MigrationListEntry[],
81
+ startIndex: number,
82
+ contract: string,
83
+ kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
84
+ ): boolean {
85
+ for (let index = startIndex + 1; index < entries.length; index++) {
86
+ const later = entries[index];
87
+ if (later === undefined) continue;
88
+ if (kindByMigrationHash.get(later.migrationHash) !== 'forward') continue;
89
+ if (canonicalFrom(later.from) === contract) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ export function computeMigrationListGraphLayout(
95
+ entries: readonly MigrationListEntry[],
96
+ topology: MigrationListGraphTopology = classifyMigrationListGraphTopology(entries),
97
+ ): MigrationListGraphLayout {
98
+ const { kindByMigrationHash } = topology;
99
+ const forwardProducersByTo = buildForwardProducersByTo(entries, kindByMigrationHash);
100
+ const convergencesEmitted = new Set<string>();
101
+ const producerLaneByHash = new Map<string, number>();
102
+ const lanes: LaneState[] = [];
103
+ const rows: LayoutRow[] = [];
104
+
105
+ function emitNodeLine(contractHash: string): void {
106
+ rows.push({ kind: 'nodeLine', contractHash, laneIndex: 0 });
107
+ }
108
+
109
+ function emitConnector(
110
+ connectorKind: ConnectorKind,
111
+ contractHash: string,
112
+ startLane: number,
113
+ endLane: number,
114
+ branchCount: number,
115
+ ): void {
116
+ rows.push({
117
+ kind: 'connector',
118
+ connectorKind,
119
+ contractHash,
120
+ startLane,
121
+ endLane,
122
+ branchCount,
123
+ });
124
+ }
125
+
126
+ function activeLaneIndices(): number[] {
127
+ const indices: number[] = [];
128
+ for (let index = 0; index < lanes.length; index++) {
129
+ if (lanes[index]?.active) indices.push(index);
130
+ }
131
+ return indices;
132
+ }
133
+
134
+ function lanesWanting(contract: string): number[] {
135
+ const indices: number[] = [];
136
+ for (let index = 0; index < lanes.length; index++) {
137
+ const lane = lanes[index];
138
+ if (lane?.active && lane.want === contract) indices.push(index);
139
+ }
140
+ return indices;
141
+ }
142
+
143
+ function ensureLane(index: number): void {
144
+ while (lanes.length <= index) {
145
+ lanes.push({ want: '', active: false });
146
+ }
147
+ }
148
+
149
+ function openLaneAtRight(want: string): number {
150
+ const index = lanes.length;
151
+ lanes.push({ want, active: true });
152
+ return index;
153
+ }
154
+
155
+ function closeLane(index: number): void {
156
+ ensureLane(index);
157
+ const lane = lanes[index];
158
+ if (lane) lane.active = false;
159
+ }
160
+
161
+ function emitJoinAbove(contractHash: string, laneIndices: readonly number[]): void {
162
+ if (laneIndices.length < 2) return;
163
+ const startLane = Math.min(...laneIndices);
164
+ const endLane = Math.max(...laneIndices);
165
+ emitConnector('joinAbove', contractHash, startLane, endLane, laneIndices.length);
166
+ for (const index of laneIndices) {
167
+ if (index !== startLane) closeLane(index);
168
+ }
169
+ }
170
+
171
+ function emitConvergencePreamble(contract: string): void {
172
+ if (convergencesEmitted.has(contract)) return;
173
+ if (forwardInDegree(topology, contract) < 2) return;
174
+
175
+ const consumersWanting = lanesWanting(contract);
176
+ if (forwardOutDegree(topology, contract) >= 2 && consumersWanting.length >= 2) {
177
+ emitJoinAbove(contract, consumersWanting);
178
+ }
179
+
180
+ emitNodeLine(contract);
181
+ const producers = forwardProducersByTo.get(contract) ?? [];
182
+ if (producers.length >= 2) {
183
+ emitConnector('fanBelow', contract, 0, producers.length - 1, producers.length);
184
+ }
185
+
186
+ for (const [producerIndex, producer] of producers.entries()) {
187
+ ensureLane(producerIndex);
188
+ lanes[producerIndex] = { want: canonicalFrom(producer.from), active: true };
189
+ producerLaneByHash.set(producer.migrationHash, producerIndex);
190
+ }
191
+
192
+ convergencesEmitted.add(contract);
193
+ }
194
+
195
+ function placeWoven(
196
+ entry: MigrationListEntry,
197
+ edgeKind: MigrationEdgeKind,
198
+ laneIndex: number,
199
+ ): void {
200
+ const passThroughLanes = activeLaneIndices().filter((index) => index !== laneIndex);
201
+ rows.push({
202
+ kind: 'migration',
203
+ entry,
204
+ edgeKind,
205
+ laneIndex,
206
+ passThroughLanes,
207
+ woven: true,
208
+ });
209
+ ensureLane(laneIndex);
210
+ lanes[laneIndex] = { want: canonicalFrom(entry.from), active: true };
211
+ }
212
+
213
+ function placeUnwoven(entry: MigrationListEntry, edgeKind: MigrationEdgeKind): void {
214
+ const passThroughLanes = activeLaneIndices();
215
+ const laneIndex = passThroughLanes.length === 0 ? 0 : Math.max(...passThroughLanes) + 1;
216
+ rows.push({
217
+ kind: 'migration',
218
+ entry,
219
+ edgeKind,
220
+ laneIndex,
221
+ passThroughLanes,
222
+ woven: false,
223
+ });
224
+ }
225
+
226
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
227
+ const entry = entries[entryIndex]!;
228
+ const edgeKind = kindByMigrationHash.get(entry.migrationHash) ?? 'forward';
229
+ const to = entry.to;
230
+
231
+ if (edgeKind !== 'forward') {
232
+ placeUnwoven(entry, edgeKind);
233
+ continue;
234
+ }
235
+
236
+ if (forwardInDegree(topology, to) >= 2 && !convergencesEmitted.has(to)) {
237
+ emitConvergencePreamble(to);
238
+ }
239
+
240
+ const presetLane = producerLaneByHash.get(entry.migrationHash);
241
+ const wantingTo = lanesWanting(to);
242
+
243
+ if (wantingTo.length >= 2 && countForwardProducersTo(forwardProducersByTo, to) === 1) {
244
+ emitJoinAbove(to, wantingTo);
245
+ }
246
+
247
+ if (presetLane !== undefined) {
248
+ placeWoven(entry, edgeKind, presetLane);
249
+ continue;
250
+ }
251
+
252
+ const firstWanting = wantingTo[0];
253
+ if (firstWanting !== undefined) {
254
+ placeWoven(entry, edgeKind, firstWanting);
255
+ continue;
256
+ }
257
+
258
+ if (hasLaterForwardDepartingFrom(entries, entryIndex, to, kindByMigrationHash)) {
259
+ placeUnwoven(entry, edgeKind);
260
+ continue;
261
+ }
262
+
263
+ const tipLaneIndex = openLaneAtRight(canonicalFrom(entry.from));
264
+ placeWoven(entry, edgeKind, tipLaneIndex);
265
+ }
266
+
267
+ return { rows };
268
+ }
@@ -1,85 +1,67 @@
1
- import type {
2
- ConnectorLayoutRow,
3
- LayoutRow,
4
- MigrationLayoutRow,
5
- MigrationListGraphLayout,
6
- NodeLineLayoutRow,
7
- } from '@prisma-next/migration-tools/migration-list-graph-layout';
8
- import { computeMigrationListGraphLayout } from '@prisma-next/migration-tools/migration-list-graph-layout';
9
- import type { EdgeKind } from '@prisma-next/migration-tools/migration-list-graph-topology';
1
+ import type { MigrationListGraphTopology } from '@prisma-next/migration-tools/migration-list-graph-topology';
10
2
  import type {
11
3
  MigrationListEntry,
12
4
  MigrationListResult,
13
5
  } from '@prisma-next/migration-tools/migration-list-types';
6
+ import type { GlyphMode } from '../glyph-mode';
14
7
  import {
15
8
  abbreviateContractHash,
16
9
  computeMigrationDirNameWidth,
17
10
  formatMigrationDataColumn,
18
11
  formatNodeLineDataColumn,
19
- MIGRATION_LIST_EMPTY_SOURCE,
20
- MIGRATION_LIST_FORWARD_EDGE_GLYPH,
12
+ MIGRATION_LIST_ASCII_KIND_GLYPH,
13
+ MIGRATION_LIST_UNICODE_KIND_GLYPH,
14
+ migrationListEmptySource,
15
+ migrationListForwardArrow,
21
16
  } from './migration-list-data-column';
17
+ import type {
18
+ ConnectorLayoutRow,
19
+ LayoutRow,
20
+ MigrationLayoutRow,
21
+ MigrationListGraphLayout,
22
+ NodeLineLayoutRow,
23
+ } from './migration-list-graph-layout';
24
+ import { computeMigrationListGraphLayout } from './migration-list-graph-layout';
22
25
  import type { MigrationListStyler } from './migration-list-render';
23
26
 
24
- export type GlyphMode = 'unicode' | 'ascii';
25
-
26
- export interface GlyphModeInput {
27
- readonly isTTY: boolean;
28
- readonly env: Readonly<Record<string, string | undefined>>;
29
- }
27
+ export type { GlyphMode } from '../glyph-mode';
30
28
 
31
29
  interface GlyphPalette {
32
30
  readonly lane: string;
33
31
  readonly node: string;
34
32
  readonly forwardArrow: string;
35
33
  readonly emptySource: string;
36
- readonly kind: Record<EdgeKind, string>;
34
+ readonly kind: typeof MIGRATION_LIST_UNICODE_KIND_GLYPH;
37
35
  readonly fanBelow: (branchCount: number) => string;
38
- readonly joinBelow: (branchCount: number) => string;
36
+ readonly joinAbove: (branchCount: number) => string;
39
37
  }
40
38
 
41
39
  const UNICODE_PALETTE: GlyphPalette = {
42
40
  lane: '│',
43
41
  node: 'o',
44
- forwardArrow: MIGRATION_LIST_FORWARD_EDGE_GLYPH,
45
- emptySource: MIGRATION_LIST_EMPTY_SOURCE,
46
- kind: { forward: '*', rollback: '↩', self: '⟲' },
42
+ forwardArrow: migrationListForwardArrow('unicode'),
43
+ emptySource: migrationListEmptySource('unicode'),
44
+ kind: MIGRATION_LIST_UNICODE_KIND_GLYPH,
47
45
  fanBelow: (branchCount) => (branchCount === 2 ? '├─┐' : '├─┬─┐'),
48
- joinBelow: (branchCount) => (branchCount === 2 ? '├─┘' : '└─┴─┘'),
46
+ joinAbove: (branchCount) => (branchCount === 2 ? '├─┘' : '└─┴─┘'),
49
47
  };
50
48
 
51
49
  const ASCII_PALETTE: GlyphPalette = {
52
50
  lane: '|',
53
51
  node: 'o',
54
- forwardArrow: '->',
55
- emptySource: '-',
56
- kind: { forward: '*', rollback: '<', self: '~' },
52
+ forwardArrow: migrationListForwardArrow('ascii'),
53
+ emptySource: migrationListEmptySource('ascii'),
54
+ kind: MIGRATION_LIST_ASCII_KIND_GLYPH,
57
55
  fanBelow: (branchCount) => (branchCount === 2 ? '+-\\' : '+-|-\\'),
58
- joinBelow: (branchCount) => (branchCount === 2 ? '+-/' : '/-+-/'),
56
+ joinAbove: (branchCount) => (branchCount === 2 ? '+-/' : '/-+-/'),
59
57
  };
60
58
 
61
59
  function paletteFor(mode: GlyphMode): GlyphPalette {
62
60
  return mode === 'ascii' ? ASCII_PALETTE : UNICODE_PALETTE;
63
61
  }
64
62
 
65
- function localeString(env: Readonly<Record<string, string | undefined>>): string {
66
- return env['LC_ALL'] ?? env['LC_CTYPE'] ?? env['LANG'] ?? '';
67
- }
68
-
69
- function isUtf8Locale(env: Readonly<Record<string, string | undefined>>): boolean {
70
- const locale = localeString(env);
71
- if (locale.length === 0) return false;
72
- return /UTF-8|utf8/i.test(locale);
73
- }
74
-
75
- export function detectGlyphMode(input: GlyphModeInput): GlyphMode {
76
- if (!input.isTTY) return 'ascii';
77
- if (!isUtf8Locale(input.env)) return 'ascii';
78
- return 'unicode';
79
- }
80
-
81
- function migrationEntries(layout: MigrationListGraphLayout) {
82
- const entries = [];
63
+ function migrationEntries(layout: MigrationListGraphLayout): MigrationListEntry[] {
64
+ const entries: MigrationListEntry[] = [];
83
65
  for (const row of layout.rows) {
84
66
  if (row.kind === 'migration') entries.push(row.entry);
85
67
  }
@@ -117,7 +99,7 @@ function renderMigrationGutter(
117
99
  const cells: string[] = [];
118
100
  for (let lane = 0; lane <= maxLane; lane++) {
119
101
  if (lane === row.laneIndex) {
120
- cells.push(laneCell(palette.kind[row.edgeKind]));
102
+ cells.push(laneCell(style.kind(palette.kind[row.edgeKind])));
121
103
  } else if (row.passThroughLanes.includes(lane)) {
122
104
  cells.push(laneCell(style.lane(palette.lane)));
123
105
  } else {
@@ -159,7 +141,7 @@ function renderConnectorGutter(
159
141
  let spanGlyph = (
160
142
  row.connectorKind === 'fanBelow'
161
143
  ? palette.fanBelow(row.branchCount)
162
- : palette.joinBelow(row.branchCount)
144
+ : palette.joinAbove(row.branchCount)
163
145
  )
164
146
  .padEnd(spanWidth, ' ')
165
147
  .slice(0, spanWidth);
@@ -245,7 +227,7 @@ export function renderMigrationListGraphWithStyle(
245
227
  openLanes = advanceOpenLanes(row, openLanes);
246
228
  }
247
229
 
248
- return lines.join('\n');
230
+ return lines.map((line) => line.trimEnd()).join('\n');
249
231
  }
250
232
 
251
233
  export function renderMigrationListGraph(
@@ -270,6 +252,7 @@ function renderGraphSpaceBlock(
270
252
  multiSpace: boolean,
271
253
  style: MigrationListStyler,
272
254
  glyphMode: GlyphMode,
255
+ topology: MigrationListGraphTopology,
273
256
  ): readonly string[] {
274
257
  if (migrations.length === 0) {
275
258
  const emptyLine = formatGraphEmptyStateLine(spaceId, style);
@@ -279,7 +262,7 @@ function renderGraphSpaceBlock(
279
262
  return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
280
263
  }
281
264
 
282
- const layout = computeMigrationListGraphLayout(migrations);
265
+ const layout = computeMigrationListGraphLayout(migrations, topology);
283
266
  const graphBody = renderMigrationListGraphWithStyle(layout, style, glyphMode);
284
267
  const rows = graphBody.split('\n');
285
268
  if (!multiSpace) {
@@ -292,6 +275,7 @@ export function renderMigrationListGraphResult(
292
275
  result: MigrationListResult,
293
276
  style: MigrationListStyler,
294
277
  glyphMode: GlyphMode,
278
+ topologyBySpaceId: ReadonlyMap<string, MigrationListGraphTopology>,
295
279
  ): string {
296
280
  const multiSpace = result.spaces.length > 1;
297
281
  const lines: string[] = [];
@@ -301,8 +285,19 @@ export function renderMigrationListGraphResult(
301
285
  if (index > 0) {
302
286
  lines.push('');
303
287
  }
288
+ const topology = topologyBySpaceId.get(space.spaceId);
289
+ if (topology === undefined) {
290
+ throw new Error(`missing topology for space ${space.spaceId}`);
291
+ }
304
292
  lines.push(
305
- ...renderGraphSpaceBlock(space.spaceId, space.migrations, multiSpace, style, glyphMode),
293
+ ...renderGraphSpaceBlock(
294
+ space.spaceId,
295
+ space.migrations,
296
+ multiSpace,
297
+ style,
298
+ glyphMode,
299
+ topology,
300
+ ),
306
301
  );
307
302
  }
308
303
 
@@ -1,30 +1,28 @@
1
1
  import {
2
2
  classifyMigrationListGraphTopology,
3
- type EdgeKind,
3
+ type MigrationEdgeKind,
4
+ type MigrationListGraphTopology,
4
5
  } from '@prisma-next/migration-tools/migration-list-graph-topology';
5
6
  import type {
6
7
  MigrationListEntry,
7
8
  MigrationListResult,
8
9
  } from '@prisma-next/migration-tools/migration-list-types';
10
+ import type { GlyphMode } from '../glyph-mode';
9
11
  import {
10
12
  computeMigrationDirNameWidth,
11
13
  formatMigrationDataColumn,
12
- MIGRATION_LIST_FORWARD_EDGE_GLYPH,
14
+ migrationListEmptySource,
15
+ migrationListForwardArrow,
16
+ migrationListKindGlyph,
13
17
  } from './migration-list-data-column';
14
18
 
15
- export type { EdgeKind } from '@prisma-next/migration-tools/migration-list-graph-topology';
16
-
19
+ export type { MigrationEdgeKind } from '@prisma-next/migration-tools/migration-list-graph-topology';
17
20
  export type {
18
21
  MigrationListEntry,
19
22
  MigrationListResult,
20
23
  MigrationSpaceListEntry,
21
24
  } from '@prisma-next/migration-tools/migration-list-types';
22
-
23
- const KIND_GLYPH: Record<EdgeKind, string> = {
24
- forward: '*',
25
- rollback: '↩',
26
- self: '⟲',
27
- };
25
+ export type { GlyphMode } from '../glyph-mode';
28
26
 
29
27
  /**
30
28
  * Semantic styler for `migration list` output tokens. Token-typed so
@@ -69,23 +67,25 @@ export const IDENTITY_MIGRATION_LIST_STYLER: MigrationListStyler = {
69
67
 
70
68
  function resolveEdgeKind(
71
69
  migrationHash: string,
72
- kindByMigrationHash: ReadonlyMap<string, EdgeKind>,
73
- ): EdgeKind {
70
+ kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
71
+ ): MigrationEdgeKind {
74
72
  return kindByMigrationHash.get(migrationHash) ?? 'forward';
75
73
  }
76
74
 
77
75
  function formatMigrationRow(
78
76
  migration: MigrationListEntry,
79
77
  dirNameWidth: number,
80
- edgeKind: EdgeKind,
78
+ edgeKind: MigrationEdgeKind,
79
+ glyphMode: GlyphMode,
81
80
  style: MigrationListStyler,
82
81
  ): string {
83
- const kindColumn = `${style.kind(KIND_GLYPH[edgeKind])} `;
82
+ const kindColumn = `${style.kind(migrationListKindGlyph(glyphMode, edgeKind))} `;
84
83
  const data = formatMigrationDataColumn(migration, {
85
84
  dirNameWidth,
86
85
  edgeKind,
87
86
  style,
88
- forwardArrow: MIGRATION_LIST_FORWARD_EDGE_GLYPH,
87
+ forwardArrow: migrationListForwardArrow(glyphMode),
88
+ emptySource: migrationListEmptySource(glyphMode),
89
89
  });
90
90
  return `${kindColumn}${data}`;
91
91
  }
@@ -98,6 +98,8 @@ function renderSpaceBlock(
98
98
  spaceId: string,
99
99
  migrations: readonly MigrationListEntry[],
100
100
  multiSpace: boolean,
101
+ glyphMode: GlyphMode,
102
+ kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
101
103
  style: MigrationListStyler,
102
104
  ): readonly string[] {
103
105
  if (migrations.length === 0) {
@@ -108,13 +110,13 @@ function renderSpaceBlock(
108
110
  return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
109
111
  }
110
112
 
111
- const kindByMigrationHash = classifyMigrationListGraphTopology(migrations).kindByMigrationHash;
112
113
  const dirNameWidth = computeMigrationDirNameWidth(migrations);
113
114
  const rows = migrations.map((entry) =>
114
115
  formatMigrationRow(
115
116
  entry,
116
117
  dirNameWidth,
117
118
  resolveEdgeKind(entry.migrationHash, kindByMigrationHash),
119
+ glyphMode,
118
120
  style,
119
121
  ),
120
122
  );
@@ -124,6 +126,16 @@ function renderSpaceBlock(
124
126
  return [style.spaceHeading(`${spaceId}:`), ...rows.map((row) => ` ${row}`)];
125
127
  }
126
128
 
129
+ export function buildMigrationListTopologyBySpace(
130
+ result: MigrationListResult,
131
+ ): ReadonlyMap<string, MigrationListGraphTopology> {
132
+ const topologyBySpaceId = new Map<string, MigrationListGraphTopology>();
133
+ for (const space of result.spaces) {
134
+ topologyBySpaceId.set(space.spaceId, classifyMigrationListGraphTopology(space.migrations));
135
+ }
136
+ return topologyBySpaceId;
137
+ }
138
+
127
139
  /**
128
140
  * Compose the styled `migration list` output. The renderer is
129
141
  * presentation-neutral — every token passes through `style` before
@@ -135,6 +147,11 @@ function renderSpaceBlock(
135
147
  export function renderMigrationListWithStyle(
136
148
  result: MigrationListResult,
137
149
  style: MigrationListStyler,
150
+ glyphMode: GlyphMode = 'unicode',
151
+ topologyBySpaceId: ReadonlyMap<
152
+ string,
153
+ MigrationListGraphTopology
154
+ > = buildMigrationListTopologyBySpace(result),
138
155
  ): string {
139
156
  const multiSpace = result.spaces.length > 1;
140
157
  const lines: string[] = [];
@@ -144,7 +161,20 @@ export function renderMigrationListWithStyle(
144
161
  if (index > 0) {
145
162
  lines.push('');
146
163
  }
147
- lines.push(...renderSpaceBlock(space.spaceId, space.migrations, multiSpace, style));
164
+ const topology = topologyBySpaceId.get(space.spaceId);
165
+ const kindByMigrationHash =
166
+ topology?.kindByMigrationHash ??
167
+ classifyMigrationListGraphTopology(space.migrations).kindByMigrationHash;
168
+ lines.push(
169
+ ...renderSpaceBlock(
170
+ space.spaceId,
171
+ space.migrations,
172
+ multiSpace,
173
+ glyphMode,
174
+ kindByMigrationHash,
175
+ style,
176
+ ),
177
+ );
148
178
  }
149
179
 
150
180
  const totalMigrations = result.spaces.reduce(
@@ -24,6 +24,7 @@ function styleRefName(name: string): string {
24
24
  * - `dirName`: bold
25
25
  * - `sourceHash`: dim cyan
26
26
  * - `destHash`: bright cyan
27
+ * - `kind` (`*` / `↩` / `⟲`): bright — the signal; lanes and arrows dim
27
28
  * - `glyph` (`→` / `⟲` / `∅`): dim
28
29
  * - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
29
30
  * - `invariants` (`{...}`): yellow
@@ -39,7 +40,8 @@ export function createAnsiMigrationListStyler(opts: {
39
40
  return IDENTITY_MIGRATION_LIST_STYLER;
40
41
  }
41
42
  return {
42
- kind: (text) => dim(text),
43
+ // Kind glyphs stay bright in both flat and graph views; lanes carry the dim gutter.
44
+ kind: (text) => text,
43
45
  dirName: (text) => bold(text),
44
46
  sourceHash: (text) => dim(cyan(text)),
45
47
  destHash: (text) => cyanBright(text),
@@ -0,0 +1,22 @@
1
+ export type GlyphMode = 'unicode' | 'ascii';
2
+
3
+ export interface GlyphModeInput {
4
+ readonly isTTY: boolean;
5
+ readonly env: Readonly<Record<string, string | undefined>>;
6
+ }
7
+
8
+ function localeString(env: Readonly<Record<string, string | undefined>>): string {
9
+ return env['LC_ALL'] ?? env['LC_CTYPE'] ?? env['LANG'] ?? '';
10
+ }
11
+
12
+ function isUtf8Locale(env: Readonly<Record<string, string | undefined>>): boolean {
13
+ const locale = localeString(env);
14
+ if (locale.length === 0) return false;
15
+ return /UTF-8|utf8/i.test(locale);
16
+ }
17
+
18
+ export function detectGlyphMode(input: GlyphModeInput): GlyphMode {
19
+ if (!input.isTTY) return 'ascii';
20
+ if (!isUtf8Locale(input.env)) return 'ascii';
21
+ return 'unicode';
22
+ }
@@ -1,11 +1,7 @@
1
1
  import * as clack from '@clack/prompts';
2
2
  import { bold, cyan, dim, green, red, yellow } from 'colorette';
3
- import {
4
- detectGlyphMode,
5
- type GlyphMode,
6
- type GlyphModeInput,
7
- } from './formatters/migration-list-graph-render';
8
3
  import type { GlobalFlags } from './global-flags';
4
+ import { detectGlyphMode, type GlyphMode, type GlyphModeInput } from './glyph-mode';
9
5
  import { shutdownSignal } from './shutdown';
10
6
 
11
7
  export interface TerminalUIRuntime extends GlyphModeInput {}