@prisma-next/cli 0.11.0-dev.48 → 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.
- package/dist/cli.mjs +9 -9
- package/dist/{command-helpers-CI8P5Xyd.mjs → command-helpers-4UNsRRc4.mjs} +3 -181
- package/dist/command-helpers-4UNsRRc4.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +3 -3
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +2 -2
- package/dist/commands/db-update.mjs +3 -3
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.mjs +2 -2
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.mjs +1 -1
- package/dist/commands/migration-list.d.mts +1 -1
- 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-new.mjs +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.mjs +2 -2
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/commands/ref.mjs +1 -1
- package/dist/{contract-emit-DmBG2Nnc.mjs → contract-emit-Bbdnpcjl.mjs} +2 -2
- package/dist/{contract-emit-DmBG2Nnc.mjs.map → contract-emit-Bbdnpcjl.mjs.map} +1 -1
- package/dist/{contract-infer-BSWFKgI1.mjs → contract-infer-C98ZaRhp.mjs} +3 -3
- package/dist/{contract-infer-BSWFKgI1.mjs.map → contract-infer-C98ZaRhp.mjs.map} +1 -1
- package/dist/{db-verify-BzpwFyLg.mjs → db-verify-BWl1Yxi-.mjs} +3 -3
- package/dist/{db-verify-BzpwFyLg.mjs.map → db-verify-BWl1Yxi-.mjs.map} +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/glyph-mode-CBB4emzO.d.mts +5 -0
- package/dist/glyph-mode-CBB4emzO.d.mts.map +1 -0
- package/dist/{init-DEssiJ8j.mjs → init-C3Swd5QB.mjs} +2 -2
- package/dist/{init-DEssiJ8j.mjs.map → init-C3Swd5QB.mjs.map} +1 -1
- package/dist/{inspect-live-schema-DlBM84nh.mjs → inspect-live-schema-BRCWQ-Sr.mjs} +2 -2
- package/dist/{inspect-live-schema-DlBM84nh.mjs.map → inspect-live-schema-BRCWQ-Sr.mjs.map} +1 -1
- package/dist/{migration-check-CzLbAqIQ.mjs → migration-check-DoskM1nB.mjs} +2 -2
- package/dist/{migration-check-CzLbAqIQ.mjs.map → migration-check-DoskM1nB.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-Bp8UHnvJ.mjs → migration-command-scaffold-CXLkoIJx.mjs} +2 -2
- package/dist/{migration-command-scaffold-Bp8UHnvJ.mjs.map → migration-command-scaffold-CXLkoIJx.mjs.map} +1 -1
- package/dist/migration-list-B2-iQ5Jd.mjs +646 -0
- package/dist/migration-list-B2-iQ5Jd.mjs.map +1 -0
- package/dist/{migration-plan-BLvOmNCu.mjs → migration-plan-BqmIKQpZ.mjs} +2 -2
- package/dist/{migration-plan-BLvOmNCu.mjs.map → migration-plan-BqmIKQpZ.mjs.map} +1 -1
- package/dist/{migrations-vzQt9LI2.mjs → migrations-BcVTutso.mjs} +2 -2
- package/dist/{migrations-vzQt9LI2.mjs.map → migrations-BcVTutso.mjs.map} +1 -1
- package/dist/{verify-ktSRQvIS.mjs → verify-DOHbbrub.mjs} +2 -2
- package/dist/{verify-ktSRQvIS.mjs.map → verify-DOHbbrub.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migration-list.ts +7 -3
- package/src/utils/formatters/migration-list-data-column.ts +33 -2
- package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
- package/src/utils/formatters/migration-list-graph-render.ts +45 -50
- package/src/utils/formatters/migration-list-render.ts +47 -17
- package/src/utils/formatters/migration-list-styler.ts +3 -1
- package/src/utils/glyph-mode.ts +22 -0
- package/src/utils/terminal-ui.ts +1 -5
- package/dist/command-helpers-CI8P5Xyd.mjs.map +0 -1
- package/dist/migration-list-C2xnaYsT.mjs +0 -279
- package/dist/migration-list-C2xnaYsT.mjs.map +0 -1
- package/dist/migration-list-graph-render-DKw1AT-e.d.mts +0 -7
- 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
|
-
|
|
20
|
-
|
|
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
|
|
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:
|
|
34
|
+
readonly kind: typeof MIGRATION_LIST_UNICODE_KIND_GLYPH;
|
|
37
35
|
readonly fanBelow: (branchCount: number) => string;
|
|
38
|
-
readonly
|
|
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:
|
|
45
|
-
emptySource:
|
|
46
|
-
kind:
|
|
42
|
+
forwardArrow: migrationListForwardArrow('unicode'),
|
|
43
|
+
emptySource: migrationListEmptySource('unicode'),
|
|
44
|
+
kind: MIGRATION_LIST_UNICODE_KIND_GLYPH,
|
|
47
45
|
fanBelow: (branchCount) => (branchCount === 2 ? '├─┐' : '├─┬─┐'),
|
|
48
|
-
|
|
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:
|
|
52
|
+
forwardArrow: migrationListForwardArrow('ascii'),
|
|
53
|
+
emptySource: migrationListEmptySource('ascii'),
|
|
54
|
+
kind: MIGRATION_LIST_ASCII_KIND_GLYPH,
|
|
57
55
|
fanBelow: (branchCount) => (branchCount === 2 ? '+-\\' : '+-|-\\'),
|
|
58
|
-
|
|
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
|
|
66
|
-
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
14
|
+
migrationListEmptySource,
|
|
15
|
+
migrationListForwardArrow,
|
|
16
|
+
migrationListKindGlyph,
|
|
13
17
|
} from './migration-list-data-column';
|
|
14
18
|
|
|
15
|
-
export type {
|
|
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,
|
|
73
|
-
):
|
|
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:
|
|
78
|
+
edgeKind: MigrationEdgeKind,
|
|
79
|
+
glyphMode: GlyphMode,
|
|
81
80
|
style: MigrationListStyler,
|
|
82
81
|
): string {
|
|
83
|
-
const kindColumn = `${style.kind(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/terminal-ui.ts
CHANGED
|
@@ -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 {}
|