@prisma-next/cli 0.11.0-dev.44 → 0.11.0-dev.45
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 +8 -8
- package/dist/{command-helpers-BnqwTptC.mjs → command-helpers-yLuA78TP.mjs} +226 -4
- package/dist/command-helpers-yLuA78TP.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 +8 -1
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +2 -2
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-new.mjs +1 -1
- package/dist/commands/migration-plan.d.mts +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-aFcOi3aw.mjs → contract-emit-FtDVFs2Q.mjs} +2 -2
- package/dist/{contract-emit-aFcOi3aw.mjs.map → contract-emit-FtDVFs2Q.mjs.map} +1 -1
- package/dist/{contract-infer-BpJeg-Eu.mjs → contract-infer-CVMuoJKk.mjs} +3 -3
- package/dist/{contract-infer-BpJeg-Eu.mjs.map → contract-infer-CVMuoJKk.mjs.map} +1 -1
- package/dist/{db-verify-CxtdGiL3.mjs → db-verify-B00o3LuC.mjs} +3 -3
- package/dist/{db-verify-CxtdGiL3.mjs.map → db-verify-B00o3LuC.mjs.map} +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/{global-flags-CdE7M0d9.d.mts → global-flags-Dvibm2yu.d.mts} +1 -1
- package/dist/{global-flags-CdE7M0d9.d.mts.map → global-flags-Dvibm2yu.d.mts.map} +1 -1
- package/dist/{init-eGkSo7hi.mjs → init-BKgB6EKw.mjs} +2 -2
- package/dist/{init-eGkSo7hi.mjs.map → init-BKgB6EKw.mjs.map} +1 -1
- package/dist/{inspect-live-schema-B1GCyjAJ.mjs → inspect-live-schema-BXUd6RfS.mjs} +2 -2
- package/dist/{inspect-live-schema-B1GCyjAJ.mjs.map → inspect-live-schema-BXUd6RfS.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-CNdZl60X.mjs → migration-command-scaffold-3l3EdmSD.mjs} +2 -2
- package/dist/{migration-command-scaffold-CNdZl60X.mjs.map → migration-command-scaffold-3l3EdmSD.mjs.map} +1 -1
- package/dist/{migration-list-CnYiHrNV.mjs → migration-list-DopkAG7L.mjs} +40 -49
- package/dist/migration-list-DopkAG7L.mjs.map +1 -0
- package/dist/migration-list-graph-render-C-daUZLU.d.mts +7 -0
- package/dist/migration-list-graph-render-C-daUZLU.d.mts.map +1 -0
- package/dist/{migration-plan-ulpJu26J.mjs → migration-plan-BHoeET4O.mjs} +2 -2
- package/dist/{migration-plan-ulpJu26J.mjs.map → migration-plan-BHoeET4O.mjs.map} +1 -1
- package/dist/{migrations-C7YTBnLy.mjs → migrations-D-UCOGtk.mjs} +2 -2
- package/dist/{migrations-C7YTBnLy.mjs.map → migrations-D-UCOGtk.mjs.map} +1 -1
- package/dist/{verify-DX4RQwq4.mjs → verify-9gDJz6cm.mjs} +2 -2
- package/dist/{verify-DX4RQwq4.mjs.map → verify-9gDJz6cm.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migration-list.ts +40 -16
- package/src/utils/formatters/migration-list-data-column.ts +84 -0
- package/src/utils/formatters/migration-list-graph-render.ts +319 -0
- package/src/utils/formatters/migration-list-render.ts +44 -51
- package/src/utils/formatters/migration-list-styler.ts +3 -0
- package/src/utils/terminal-ui.ts +46 -1
- package/dist/command-helpers-BnqwTptC.mjs.map +0 -1
- package/dist/migration-list-CnYiHrNV.mjs.map +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
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';
|
|
10
|
+
import type {
|
|
11
|
+
MigrationListEntry,
|
|
12
|
+
MigrationListResult,
|
|
13
|
+
} from '@prisma-next/migration-tools/migration-list-types';
|
|
14
|
+
import {
|
|
15
|
+
abbreviateContractHash,
|
|
16
|
+
computeMigrationDirNameWidth,
|
|
17
|
+
formatMigrationDataColumn,
|
|
18
|
+
formatNodeLineDataColumn,
|
|
19
|
+
MIGRATION_LIST_EMPTY_SOURCE,
|
|
20
|
+
MIGRATION_LIST_FORWARD_EDGE_GLYPH,
|
|
21
|
+
} from './migration-list-data-column';
|
|
22
|
+
import type { MigrationListStyler } from './migration-list-render';
|
|
23
|
+
|
|
24
|
+
export type GlyphMode = 'unicode' | 'ascii';
|
|
25
|
+
|
|
26
|
+
export interface GlyphModeInput {
|
|
27
|
+
readonly isTTY: boolean;
|
|
28
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface GlyphPalette {
|
|
32
|
+
readonly lane: string;
|
|
33
|
+
readonly node: string;
|
|
34
|
+
readonly forwardArrow: string;
|
|
35
|
+
readonly emptySource: string;
|
|
36
|
+
readonly kind: Record<EdgeKind, string>;
|
|
37
|
+
readonly fanBelow: (branchCount: number) => string;
|
|
38
|
+
readonly joinBelow: (branchCount: number) => string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const UNICODE_PALETTE: GlyphPalette = {
|
|
42
|
+
lane: '│',
|
|
43
|
+
node: 'o',
|
|
44
|
+
forwardArrow: MIGRATION_LIST_FORWARD_EDGE_GLYPH,
|
|
45
|
+
emptySource: MIGRATION_LIST_EMPTY_SOURCE,
|
|
46
|
+
kind: { forward: '*', rollback: '↩', self: '⟲' },
|
|
47
|
+
fanBelow: (branchCount) => (branchCount === 2 ? '├─┐' : '├─┬─┐'),
|
|
48
|
+
joinBelow: (branchCount) => (branchCount === 2 ? '├─┘' : '└─┴─┘'),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ASCII_PALETTE: GlyphPalette = {
|
|
52
|
+
lane: '|',
|
|
53
|
+
node: 'o',
|
|
54
|
+
forwardArrow: '->',
|
|
55
|
+
emptySource: '-',
|
|
56
|
+
kind: { forward: '*', rollback: '<', self: '~' },
|
|
57
|
+
fanBelow: (branchCount) => (branchCount === 2 ? '+-\\' : '+-|-\\'),
|
|
58
|
+
joinBelow: (branchCount) => (branchCount === 2 ? '+-/' : '/-+-/'),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function paletteFor(mode: GlyphMode): GlyphPalette {
|
|
62
|
+
return mode === 'ascii' ? ASCII_PALETTE : UNICODE_PALETTE;
|
|
63
|
+
}
|
|
64
|
+
|
|
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 = [];
|
|
83
|
+
for (const row of layout.rows) {
|
|
84
|
+
if (row.kind === 'migration') entries.push(row.entry);
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function layoutMaxLaneIndex(layout: MigrationListGraphLayout): number {
|
|
90
|
+
let max = 0;
|
|
91
|
+
for (const row of layout.rows) {
|
|
92
|
+
if (row.kind === 'migration') {
|
|
93
|
+
max = Math.max(max, row.laneIndex, ...row.passThroughLanes);
|
|
94
|
+
} else if (row.kind === 'connector') {
|
|
95
|
+
max = Math.max(max, row.endLane);
|
|
96
|
+
} else {
|
|
97
|
+
max = Math.max(max, row.laneIndex);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return max;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function laneCell(glyph: string): string {
|
|
104
|
+
return `${glyph} `;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emptyLaneCell(): string {
|
|
108
|
+
return ' ';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderMigrationGutter(
|
|
112
|
+
row: MigrationLayoutRow,
|
|
113
|
+
maxLane: number,
|
|
114
|
+
palette: GlyphPalette,
|
|
115
|
+
style: MigrationListStyler,
|
|
116
|
+
): string {
|
|
117
|
+
const cells: string[] = [];
|
|
118
|
+
for (let lane = 0; lane <= maxLane; lane++) {
|
|
119
|
+
if (lane === row.laneIndex) {
|
|
120
|
+
cells.push(laneCell(palette.kind[row.edgeKind]));
|
|
121
|
+
} else if (row.passThroughLanes.includes(lane)) {
|
|
122
|
+
cells.push(laneCell(style.lane(palette.lane)));
|
|
123
|
+
} else {
|
|
124
|
+
cells.push(emptyLaneCell());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return cells.join('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderNodeLineGutter(
|
|
131
|
+
row: NodeLineLayoutRow,
|
|
132
|
+
openLanes: ReadonlySet<number>,
|
|
133
|
+
maxLane: number,
|
|
134
|
+
palette: GlyphPalette,
|
|
135
|
+
style: MigrationListStyler,
|
|
136
|
+
): string {
|
|
137
|
+
const cells: string[] = [];
|
|
138
|
+
for (let lane = 0; lane <= maxLane; lane++) {
|
|
139
|
+
if (lane === row.laneIndex) {
|
|
140
|
+
cells.push(laneCell(palette.node));
|
|
141
|
+
} else if (openLanes.has(lane)) {
|
|
142
|
+
cells.push(laneCell(style.lane(palette.lane)));
|
|
143
|
+
} else {
|
|
144
|
+
cells.push(emptyLaneCell());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return cells.join('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderConnectorGutter(
|
|
151
|
+
row: ConnectorLayoutRow,
|
|
152
|
+
openLanes: ReadonlySet<number>,
|
|
153
|
+
maxLane: number,
|
|
154
|
+
palette: GlyphPalette,
|
|
155
|
+
style: MigrationListStyler,
|
|
156
|
+
): string {
|
|
157
|
+
const spanLaneCount = row.endLane - row.startLane + 1;
|
|
158
|
+
const spanWidth = spanLaneCount * 2;
|
|
159
|
+
let spanGlyph = (
|
|
160
|
+
row.connectorKind === 'fanBelow'
|
|
161
|
+
? palette.fanBelow(row.branchCount)
|
|
162
|
+
: palette.joinBelow(row.branchCount)
|
|
163
|
+
)
|
|
164
|
+
.padEnd(spanWidth, ' ')
|
|
165
|
+
.slice(0, spanWidth);
|
|
166
|
+
|
|
167
|
+
const hasOutsideOpen = [...openLanes].some((lane) => lane < row.startLane || lane > row.endLane);
|
|
168
|
+
if (!hasOutsideOpen && spanGlyph.endsWith(' ')) {
|
|
169
|
+
spanGlyph = spanGlyph.slice(0, -1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let gutter = '';
|
|
173
|
+
for (let lane = 0; lane < row.startLane; lane++) {
|
|
174
|
+
gutter += openLanes.has(lane) ? laneCell(style.lane(palette.lane)) : emptyLaneCell();
|
|
175
|
+
}
|
|
176
|
+
gutter += style.lane(spanGlyph);
|
|
177
|
+
for (let lane = row.endLane + 1; lane <= maxLane; lane++) {
|
|
178
|
+
gutter += openLanes.has(lane) ? laneCell(style.lane(palette.lane)) : emptyLaneCell();
|
|
179
|
+
}
|
|
180
|
+
return gutter;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function advanceOpenLanes(row: LayoutRow, openLanes: ReadonlySet<number>): ReadonlySet<number> {
|
|
184
|
+
if (row.kind === 'migration') {
|
|
185
|
+
return new Set([row.laneIndex, ...row.passThroughLanes]);
|
|
186
|
+
}
|
|
187
|
+
if (row.kind === 'connector') {
|
|
188
|
+
if (row.connectorKind === 'fanBelow') {
|
|
189
|
+
const next = new Set(openLanes);
|
|
190
|
+
for (let lane = row.startLane; lane <= row.endLane; lane++) {
|
|
191
|
+
next.add(lane);
|
|
192
|
+
}
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
const next = new Set(openLanes);
|
|
196
|
+
for (let lane = row.startLane + 1; lane <= row.endLane; lane++) {
|
|
197
|
+
next.delete(lane);
|
|
198
|
+
}
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
201
|
+
return openLanes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function renderMigrationListGraphWithStyle(
|
|
205
|
+
layout: MigrationListGraphLayout,
|
|
206
|
+
style: MigrationListStyler,
|
|
207
|
+
glyphMode: GlyphMode,
|
|
208
|
+
): string {
|
|
209
|
+
const palette = paletteFor(glyphMode);
|
|
210
|
+
const migrations = migrationEntries(layout);
|
|
211
|
+
const layoutMaxLane = layoutMaxLaneIndex(layout);
|
|
212
|
+
const dirNameWidth = computeMigrationDirNameWidth(migrations);
|
|
213
|
+
const gutterMaxLane = layoutMaxLane;
|
|
214
|
+
const blockDataStart = (layoutMaxLane + 1) * 2;
|
|
215
|
+
// Migration and node-line gutters always occupy a fixed, ANSI-free visible
|
|
216
|
+
// width of two columns per lane. Padding is computed from this width rather
|
|
217
|
+
// than the rendered string length so dimmed lanes (which carry zero-width
|
|
218
|
+
// SGR bytes) stay column-aligned with the data that follows.
|
|
219
|
+
const gutterVisibleWidth = (gutterMaxLane + 1) * 2;
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
let openLanes: ReadonlySet<number> = new Set();
|
|
222
|
+
|
|
223
|
+
function padToDataColumn(gutter: string, dataStart: number): string {
|
|
224
|
+
return gutter + ' '.repeat(Math.max(0, dataStart - gutterVisibleWidth));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const row of layout.rows) {
|
|
228
|
+
if (row.kind === 'migration') {
|
|
229
|
+
const gutter = renderMigrationGutter(row, gutterMaxLane, palette, style);
|
|
230
|
+
const data = formatMigrationDataColumn(row.entry, {
|
|
231
|
+
dirNameWidth,
|
|
232
|
+
edgeKind: row.edgeKind,
|
|
233
|
+
style,
|
|
234
|
+
forwardArrow: palette.forwardArrow,
|
|
235
|
+
emptySource: palette.emptySource,
|
|
236
|
+
});
|
|
237
|
+
lines.push(`${padToDataColumn(gutter, blockDataStart)}${data}`);
|
|
238
|
+
} else if (row.kind === 'nodeLine') {
|
|
239
|
+
const gutter = renderNodeLineGutter(row, openLanes, gutterMaxLane, palette, style);
|
|
240
|
+
const data = formatNodeLineDataColumn(row.contractHash, style);
|
|
241
|
+
lines.push(`${padToDataColumn(gutter, blockDataStart)}${data}`);
|
|
242
|
+
} else {
|
|
243
|
+
lines.push(renderConnectorGutter(row, openLanes, gutterMaxLane, palette, style));
|
|
244
|
+
}
|
|
245
|
+
openLanes = advanceOpenLanes(row, openLanes);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderMigrationListGraph(
|
|
252
|
+
layout: MigrationListGraphLayout,
|
|
253
|
+
style: MigrationListStyler,
|
|
254
|
+
glyphMode: GlyphMode,
|
|
255
|
+
): string {
|
|
256
|
+
return renderMigrationListGraphWithStyle(layout, style, glyphMode);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function formatGraphNodeLineHash(contractHash: string, style: MigrationListStyler): string {
|
|
260
|
+
return style.sourceHash(abbreviateContractHash(contractHash));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatGraphEmptyStateLine(spaceId: string, style: MigrationListStyler): string {
|
|
264
|
+
return style.emptyState(`There are no migrations in migrations/${spaceId}/ yet`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function renderGraphSpaceBlock(
|
|
268
|
+
spaceId: string,
|
|
269
|
+
migrations: readonly MigrationListEntry[],
|
|
270
|
+
multiSpace: boolean,
|
|
271
|
+
style: MigrationListStyler,
|
|
272
|
+
glyphMode: GlyphMode,
|
|
273
|
+
): readonly string[] {
|
|
274
|
+
if (migrations.length === 0) {
|
|
275
|
+
const emptyLine = formatGraphEmptyStateLine(spaceId, style);
|
|
276
|
+
if (!multiSpace) {
|
|
277
|
+
return [emptyLine];
|
|
278
|
+
}
|
|
279
|
+
return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const layout = computeMigrationListGraphLayout(migrations);
|
|
283
|
+
const graphBody = renderMigrationListGraphWithStyle(layout, style, glyphMode);
|
|
284
|
+
const rows = graphBody.split('\n');
|
|
285
|
+
if (!multiSpace) {
|
|
286
|
+
return rows;
|
|
287
|
+
}
|
|
288
|
+
return [style.spaceHeading(`${spaceId}:`), ...rows.map((row) => ` ${row}`)];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function renderMigrationListGraphResult(
|
|
292
|
+
result: MigrationListResult,
|
|
293
|
+
style: MigrationListStyler,
|
|
294
|
+
glyphMode: GlyphMode,
|
|
295
|
+
): string {
|
|
296
|
+
const multiSpace = result.spaces.length > 1;
|
|
297
|
+
const lines: string[] = [];
|
|
298
|
+
|
|
299
|
+
for (let index = 0; index < result.spaces.length; index++) {
|
|
300
|
+
const space = result.spaces[index]!;
|
|
301
|
+
if (index > 0) {
|
|
302
|
+
lines.push('');
|
|
303
|
+
}
|
|
304
|
+
lines.push(
|
|
305
|
+
...renderGraphSpaceBlock(space.spaceId, space.migrations, multiSpace, style, glyphMode),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const totalMigrations = result.spaces.reduce(
|
|
310
|
+
(count, space) => count + space.migrations.length,
|
|
311
|
+
0,
|
|
312
|
+
);
|
|
313
|
+
if (totalMigrations > 0) {
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push(style.summary(result.summary));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return lines.join('\n');
|
|
319
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyMigrationListGraphTopology,
|
|
3
|
+
type EdgeKind,
|
|
4
|
+
} from '@prisma-next/migration-tools/migration-list-graph-topology';
|
|
1
5
|
import type {
|
|
2
6
|
MigrationListEntry,
|
|
3
7
|
MigrationListResult,
|
|
4
8
|
} from '@prisma-next/migration-tools/migration-list-types';
|
|
9
|
+
import {
|
|
10
|
+
computeMigrationDirNameWidth,
|
|
11
|
+
formatMigrationDataColumn,
|
|
12
|
+
MIGRATION_LIST_FORWARD_EDGE_GLYPH,
|
|
13
|
+
} from './migration-list-data-column';
|
|
14
|
+
|
|
15
|
+
export type { EdgeKind } from '@prisma-next/migration-tools/migration-list-graph-topology';
|
|
5
16
|
|
|
6
17
|
export type {
|
|
7
18
|
MigrationListEntry,
|
|
@@ -9,11 +20,11 @@ export type {
|
|
|
9
20
|
MigrationSpaceListEntry,
|
|
10
21
|
} from '@prisma-next/migration-tools/migration-list-types';
|
|
11
22
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
const KIND_GLYPH: Record<EdgeKind, string> = {
|
|
24
|
+
forward: '*',
|
|
25
|
+
rollback: '↩',
|
|
26
|
+
self: '⟲',
|
|
27
|
+
};
|
|
17
28
|
|
|
18
29
|
/**
|
|
19
30
|
* Semantic styler for `migration list` output tokens. Token-typed so
|
|
@@ -29,10 +40,12 @@ const DECORATION_PREFIX = ' ';
|
|
|
29
40
|
* having to re-parse a joined block.
|
|
30
41
|
*/
|
|
31
42
|
export interface MigrationListStyler {
|
|
43
|
+
kind(text: string): string;
|
|
32
44
|
dirName(text: string): string;
|
|
33
45
|
sourceHash(text: string): string;
|
|
34
46
|
destHash(text: string): string;
|
|
35
47
|
glyph(text: string): string;
|
|
48
|
+
lane(text: string): string;
|
|
36
49
|
invariants(ids: readonly string[]): string;
|
|
37
50
|
refs(names: readonly string[]): string;
|
|
38
51
|
spaceHeading(text: string): string;
|
|
@@ -41,10 +54,12 @@ export interface MigrationListStyler {
|
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
export const IDENTITY_MIGRATION_LIST_STYLER: MigrationListStyler = {
|
|
57
|
+
kind: (text) => text,
|
|
44
58
|
dirName: (text) => text,
|
|
45
59
|
sourceHash: (text) => text,
|
|
46
60
|
destHash: (text) => text,
|
|
47
61
|
glyph: (text) => text,
|
|
62
|
+
lane: (text) => text,
|
|
48
63
|
invariants: (ids) => `{${ids.join(', ')}}`,
|
|
49
64
|
refs: (names) => `(${names.join(', ')})`,
|
|
50
65
|
spaceHeading: (text) => text,
|
|
@@ -52,57 +67,27 @@ export const IDENTITY_MIGRATION_LIST_STYLER: MigrationListStyler = {
|
|
|
52
67
|
emptyState: (text) => text,
|
|
53
68
|
};
|
|
54
69
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function formatSourceColumn(from: string | null, style: MigrationListStyler): string {
|
|
61
|
-
if (from === null) {
|
|
62
|
-
return style.glyph(EMPTY_SOURCE) + ' '.repeat(HASH_WIDTH - EMPTY_SOURCE.length);
|
|
63
|
-
}
|
|
64
|
-
return style.sourceHash(abbreviateContractHash(from));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function formatDestColumn(from: string | null, to: string, style: MigrationListStyler): string {
|
|
68
|
-
if (from !== null && from === to) {
|
|
69
|
-
return ' '.repeat(HASH_WIDTH);
|
|
70
|
-
}
|
|
71
|
-
return style.destHash(abbreviateContractHash(to));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function formatArrowGlyph(from: string | null, to: string, style: MigrationListStyler): string {
|
|
75
|
-
return style.glyph(from !== null && from === to ? SELF_EDGE_GLYPH : FORWARD_EDGE_GLYPH);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatDecorations(
|
|
79
|
-
providedInvariants: readonly string[],
|
|
80
|
-
refs: readonly string[],
|
|
81
|
-
style: MigrationListStyler,
|
|
82
|
-
): string {
|
|
83
|
-
const blocks: string[] = [];
|
|
84
|
-
if (providedInvariants.length > 0) {
|
|
85
|
-
blocks.push(style.invariants(providedInvariants));
|
|
86
|
-
}
|
|
87
|
-
if (refs.length > 0) {
|
|
88
|
-
blocks.push(style.refs(refs));
|
|
89
|
-
}
|
|
90
|
-
if (blocks.length === 0) return '';
|
|
91
|
-
return `${DECORATION_PREFIX}${blocks.join(' ')}`;
|
|
70
|
+
function resolveEdgeKind(
|
|
71
|
+
migrationHash: string,
|
|
72
|
+
kindByMigrationHash: ReadonlyMap<string, EdgeKind>,
|
|
73
|
+
): EdgeKind {
|
|
74
|
+
return kindByMigrationHash.get(migrationHash) ?? 'forward';
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
function formatMigrationRow(
|
|
95
78
|
migration: MigrationListEntry,
|
|
96
79
|
dirNameWidth: number,
|
|
80
|
+
edgeKind: EdgeKind,
|
|
97
81
|
style: MigrationListStyler,
|
|
98
82
|
): string {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
83
|
+
const kindColumn = `${style.kind(KIND_GLYPH[edgeKind])} `;
|
|
84
|
+
const data = formatMigrationDataColumn(migration, {
|
|
85
|
+
dirNameWidth,
|
|
86
|
+
edgeKind,
|
|
87
|
+
style,
|
|
88
|
+
forwardArrow: MIGRATION_LIST_FORWARD_EDGE_GLYPH,
|
|
89
|
+
});
|
|
90
|
+
return `${kindColumn}${data}`;
|
|
106
91
|
}
|
|
107
92
|
|
|
108
93
|
function formatEmptyStateLine(spaceId: string, style: MigrationListStyler): string {
|
|
@@ -123,8 +108,16 @@ function renderSpaceBlock(
|
|
|
123
108
|
return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
|
|
124
109
|
}
|
|
125
110
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
111
|
+
const kindByMigrationHash = classifyMigrationListGraphTopology(migrations).kindByMigrationHash;
|
|
112
|
+
const dirNameWidth = computeMigrationDirNameWidth(migrations);
|
|
113
|
+
const rows = migrations.map((entry) =>
|
|
114
|
+
formatMigrationRow(
|
|
115
|
+
entry,
|
|
116
|
+
dirNameWidth,
|
|
117
|
+
resolveEdgeKind(entry.migrationHash, kindByMigrationHash),
|
|
118
|
+
style,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
128
121
|
if (!multiSpace) {
|
|
129
122
|
return rows;
|
|
130
123
|
}
|
|
@@ -25,6 +25,7 @@ function styleRefName(name: string): string {
|
|
|
25
25
|
* - `sourceHash`: dim cyan
|
|
26
26
|
* - `destHash`: bright cyan
|
|
27
27
|
* - `glyph` (`→` / `⟲` / `∅`): dim
|
|
28
|
+
* - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
|
|
28
29
|
* - `invariants` (`{...}`): yellow
|
|
29
30
|
* - `refs` (`(...)`): green; the live-DB `db` marker inside is green-bold
|
|
30
31
|
* - `spaceHeading` (`<spaceId>:`): bold
|
|
@@ -38,10 +39,12 @@ export function createAnsiMigrationListStyler(opts: {
|
|
|
38
39
|
return IDENTITY_MIGRATION_LIST_STYLER;
|
|
39
40
|
}
|
|
40
41
|
return {
|
|
42
|
+
kind: (text) => dim(text),
|
|
41
43
|
dirName: (text) => bold(text),
|
|
42
44
|
sourceHash: (text) => dim(cyan(text)),
|
|
43
45
|
destHash: (text) => cyanBright(text),
|
|
44
46
|
glyph: (text) => dim(text),
|
|
47
|
+
lane: (text) => dim(text),
|
|
45
48
|
invariants: (ids) => yellow(`{${ids.join(', ')}}`),
|
|
46
49
|
refs: (names) => {
|
|
47
50
|
const open = green('(');
|
package/src/utils/terminal-ui.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
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';
|
|
3
8
|
import type { GlobalFlags } from './global-flags';
|
|
4
9
|
import { shutdownSignal } from './shutdown';
|
|
5
10
|
|
|
11
|
+
export interface TerminalUIRuntime extends GlyphModeInput {}
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* Composable CLI output abstraction.
|
|
8
15
|
*
|
|
@@ -36,17 +43,50 @@ export class TerminalUI {
|
|
|
36
43
|
*/
|
|
37
44
|
readonly forcePretty: boolean;
|
|
38
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Whether stdout is a TTY — used for migration-list graph glyph detection.
|
|
48
|
+
*/
|
|
49
|
+
readonly stdoutIsTTY: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Process environment snapshot for locale-aware glyph detection.
|
|
53
|
+
*/
|
|
54
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
55
|
+
|
|
39
56
|
private static readonly stderrOpts = { output: process.stderr } as const;
|
|
40
57
|
|
|
41
58
|
constructor(options?: {
|
|
42
59
|
readonly color?: boolean | undefined;
|
|
43
60
|
readonly interactive?: boolean | undefined;
|
|
44
61
|
readonly forcePretty?: boolean | undefined;
|
|
62
|
+
readonly stdoutIsTTY?: boolean | undefined;
|
|
63
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
45
64
|
}) {
|
|
46
65
|
// --interactive/--no-interactive override TTY detection
|
|
47
66
|
this.isInteractive = options?.interactive ?? !!process.stdout.isTTY;
|
|
48
67
|
this.forcePretty = options?.forcePretty ?? false;
|
|
49
68
|
this.useColor = options?.color ?? (this.isInteractive || this.forcePretty);
|
|
69
|
+
this.stdoutIsTTY = options?.stdoutIsTTY ?? !!process.stdout.isTTY;
|
|
70
|
+
this.env = options?.env ?? process.env;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get isTTY(): boolean {
|
|
74
|
+
return this.stdoutIsTTY;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve graph glyph mode for `migration list --graph`. `--ascii` forces
|
|
79
|
+
* ASCII; otherwise delegates to the pure {@link detectGlyphMode} helper.
|
|
80
|
+
*/
|
|
81
|
+
resolveGlyphMode(forceAscii: boolean): GlyphMode {
|
|
82
|
+
if (forceAscii) {
|
|
83
|
+
return 'ascii';
|
|
84
|
+
}
|
|
85
|
+
return detectGlyphMode(this.glyphModeInput());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
glyphModeInput(): GlyphModeInput {
|
|
89
|
+
return { isTTY: this.stdoutIsTTY, env: this.env };
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
private get shouldDecorate(): boolean {
|
|
@@ -288,10 +328,15 @@ export class TerminalUI {
|
|
|
288
328
|
}
|
|
289
329
|
}
|
|
290
330
|
|
|
291
|
-
export function createTerminalUI(
|
|
331
|
+
export function createTerminalUI(
|
|
332
|
+
flags: GlobalFlags,
|
|
333
|
+
runtime?: Partial<TerminalUIRuntime>,
|
|
334
|
+
): TerminalUI {
|
|
292
335
|
return new TerminalUI({
|
|
293
336
|
color: flags.color,
|
|
294
337
|
interactive: flags.interactive,
|
|
295
338
|
forcePretty: flags.format === 'pretty' && flags.explicitFormat,
|
|
339
|
+
stdoutIsTTY: runtime?.isTTY,
|
|
340
|
+
env: runtime?.env,
|
|
296
341
|
});
|
|
297
342
|
}
|