@prisma-next/cli 0.12.0-dev.4 → 0.12.0-dev.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.mjs +180 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-Dk-zRFuT.mjs} +83 -58
- package/dist/client-Dk-zRFuT.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-xvg9oq4T.mjs} +301 -23
- package/dist/command-helpers-xvg9oq4T.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 +4 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +10 -7
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +2 -2
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +6 -8
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +55 -1
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +2 -2
- package/dist/commands/migration-graph.d.mts +25 -7
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +170 -2
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +24 -26
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +2 -190
- package/dist/commands/migration-log.d.mts +20 -15
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-new.mjs +3 -3
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -4
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +13 -25
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +41 -141
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +3 -3
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-Wj3u4Xco.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-Wj3u4Xco.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-CZU0UO6M.mjs} +3 -3
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-CZU0UO6M.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-FetLZ3jn.mjs} +5 -5
- package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-FetLZ3jn.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-3wtOsS_H.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-3wtOsS_H.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-BdRPfM3Q.mjs} +63 -5
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-BdRPfM3Q.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-BisylXFZ.mjs} +4 -4
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-BisylXFZ.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +2 -2
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-Be4inY3I.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-Be4inY3I.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-BTE2U7lG.mjs} +5 -58
- package/dist/init-BTE2U7lG.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-Cy9Y4wsL.mjs} +3 -3
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-Cy9Y4wsL.mjs.map} +1 -1
- package/dist/{migration-check-BiBJoYYW.mjs → migration-check-CUavU7U9.mjs} +236 -88
- package/dist/migration-check-CUavU7U9.mjs.map +1 -0
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-BxOxtyJ6.mjs} +3 -3
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-BxOxtyJ6.mjs.map} +1 -1
- package/dist/migration-graph-space-render-ByJ83gxp.mjs +1966 -0
- package/dist/migration-graph-space-render-ByJ83gxp.mjs.map +1 -0
- package/dist/migration-list-jK6QeczE.mjs +228 -0
- package/dist/migration-list-jK6QeczE.mjs.map +1 -0
- package/dist/migration-list-types-DS63IdFd.d.mts +23 -0
- package/dist/migration-list-types-DS63IdFd.d.mts.map +1 -0
- package/dist/migration-log-DWI6dZyi.mjs +215 -0
- package/dist/migration-log-DWI6dZyi.mjs.map +1 -0
- package/dist/migration-path-target-DqcrbOis.mjs +24 -0
- package/dist/migration-path-target-DqcrbOis.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-NHdlUwPG.mjs} +5 -6
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-NHdlUwPG.mjs.map} +1 -1
- package/dist/migration-status-DI6Ldjbo.mjs +439 -0
- package/dist/migration-status-DI6Ldjbo.mjs.map +1 -0
- package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-CJY9zOv7.mjs} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-CJY9zOv7.mjs.map} +1 -1
- package/dist/telemetry-DQP0BvKv.mjs +122 -0
- package/dist/telemetry-DQP0BvKv.mjs.map +1 -0
- package/dist/{types-Dt_SfqFm.d.mts → types-Cculk5KV.d.mts} +44 -31
- package/dist/types-Cculk5KV.d.mts.map +1 -0
- package/dist/{verify-DCA9Sldu.mjs → verify-tvHRBBVP.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-tvHRBBVP.mjs.map} +1 -1
- package/package.json +22 -19
- package/src/cli.ts +5 -0
- package/src/commands/db-update.ts +7 -1
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/migrate.ts +6 -6
- package/src/commands/migration-check.ts +340 -117
- package/src/commands/migration-graph.ts +163 -90
- package/src/commands/migration-list.ts +55 -25
- package/src/commands/migration-log.ts +49 -98
- package/src/commands/migration-show.ts +10 -38
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +440 -1056
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +20 -9
- package/src/control-api/operations/contract-emit.ts +2 -2
- package/src/control-api/operations/db-init.ts +3 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +37 -10
- package/src/control-api/operations/db-update.ts +4 -4
- package/src/control-api/operations/{migration-apply.ts → migrate.ts} +32 -24
- package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
- package/src/control-api/types.ts +46 -29
- package/src/utils/cli-errors.ts +47 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
- package/src/utils/formatters/migration-graph-layout.ts +51 -7
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +138 -0
- package/src/utils/formatters/migration-graph-tree-render.ts +405 -77
- package/src/utils/formatters/migration-list-data-column.ts +4 -91
- package/src/utils/formatters/migration-list-graph-topology.ts +68 -90
- package/src/utils/formatters/migration-list-render.ts +122 -70
- package/src/utils/formatters/migration-list-styler.ts +48 -5
- package/src/utils/formatters/migration-log-table.ts +200 -0
- package/src/utils/formatters/migrations.ts +25 -1
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/legend.ts +38 -0
- package/src/utils/migration-path-target.ts +39 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
- package/dist/commands/migration-list.mjs.map +0 -1
- package/dist/commands/migration-log.mjs.map +0 -1
- package/dist/commands/migration-status.mjs.map +0 -1
- package/dist/extension-pack-inputs-IDvjRCi3.mjs +0 -62
- package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +0 -1
- package/dist/graph-render-rFAqZujX.mjs +0 -1081
- package/dist/graph-render-rFAqZujX.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs +0 -1232
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
- package/dist/migration-types-D2FW63pr.d.mts +0 -15
- package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
- package/dist/migrations-Cv2jxNNK.mjs +0 -228
- package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
- package/dist/types-Dt_SfqFm.d.mts.map +0 -1
- package/src/utils/formatters/graph-migration-mapper.ts +0 -235
- package/src/utils/formatters/graph-render.ts +0 -1323
- package/src/utils/formatters/graph-types.ts +0 -120
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { bold, cyan, cyanBright, dim, green, yellow } from 'colorette';
|
|
2
2
|
import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
|
|
3
3
|
|
|
4
|
+
export type MigrationListStylerWithMarkers = MigrationListStyler & {
|
|
5
|
+
markers(names: readonly string[]): string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function hasMarkersFormatter(
|
|
9
|
+
styler: MigrationListStyler,
|
|
10
|
+
): styler is MigrationListStylerWithMarkers {
|
|
11
|
+
return 'markers' in styler && typeof styler.markers === 'function';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function styleMarkerName(name: string): string {
|
|
15
|
+
return name === CONTRACT_MARKER_NAME ? bold(green(name)) : green(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function plainMarkers(names: readonly string[]): string {
|
|
19
|
+
return `<${names.join(', ')}>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatContractNodeOverlays(
|
|
23
|
+
styler: MigrationListStyler,
|
|
24
|
+
markers: readonly string[],
|
|
25
|
+
refs: readonly string[],
|
|
26
|
+
): string {
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
if (markers.length > 0) {
|
|
29
|
+
parts.push(hasMarkersFormatter(styler) ? styler.markers(markers) : plainMarkers(markers));
|
|
30
|
+
}
|
|
31
|
+
if (refs.length > 0) {
|
|
32
|
+
parts.push(styler.refs(refs));
|
|
33
|
+
}
|
|
34
|
+
return parts.join(' ');
|
|
35
|
+
}
|
|
36
|
+
|
|
4
37
|
/**
|
|
5
38
|
* The current contract overlay marker. Unlike user refs, this names the user's
|
|
6
39
|
* declared desired state — the implicit base/target for `plan` / `migrate` —
|
|
@@ -10,7 +43,7 @@ import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migr
|
|
|
10
43
|
export const CONTRACT_MARKER_NAME = 'contract';
|
|
11
44
|
|
|
12
45
|
function styleRefName(name: string): string {
|
|
13
|
-
return
|
|
46
|
+
return green(name);
|
|
14
47
|
}
|
|
15
48
|
|
|
16
49
|
/**
|
|
@@ -29,17 +62,21 @@ function styleRefName(name: string): string {
|
|
|
29
62
|
* - `glyph` (`→` / `⟲` / `∅`): dim
|
|
30
63
|
* - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
|
|
31
64
|
* - `invariants` (`{...}`): yellow
|
|
32
|
-
* - `
|
|
33
|
-
* green-bold (
|
|
65
|
+
* - `markers` (`<...>`): green; the `contract` desired-state marker inside is
|
|
66
|
+
* green-bold (`db` is plain green)
|
|
67
|
+
* - `refs` (`(...)`): green (the active ref is bolded separately by the tree styler)
|
|
34
68
|
* - `spaceHeading` (`<spaceId>:`): bold
|
|
35
69
|
* - `summary`: dim
|
|
36
70
|
* - `emptyState`: dim
|
|
37
71
|
*/
|
|
38
72
|
export function createAnsiMigrationListStyler(opts: {
|
|
39
73
|
readonly useColor: boolean;
|
|
40
|
-
}):
|
|
74
|
+
}): MigrationListStylerWithMarkers {
|
|
41
75
|
if (!opts.useColor) {
|
|
42
|
-
return
|
|
76
|
+
return {
|
|
77
|
+
...IDENTITY_MIGRATION_LIST_STYLER,
|
|
78
|
+
markers: plainMarkers,
|
|
79
|
+
};
|
|
43
80
|
}
|
|
44
81
|
return {
|
|
45
82
|
// Kind glyphs stay bright in both flat and graph views; lanes carry the dim gutter.
|
|
@@ -50,6 +87,12 @@ export function createAnsiMigrationListStyler(opts: {
|
|
|
50
87
|
glyph: (text) => dim(text),
|
|
51
88
|
lane: (text) => dim(text),
|
|
52
89
|
invariants: (ids) => yellow(`{${ids.join(', ')}}`),
|
|
90
|
+
markers: (names) => {
|
|
91
|
+
const open = green('<');
|
|
92
|
+
const close = green('>');
|
|
93
|
+
const separator = green(', ');
|
|
94
|
+
return open + names.map(styleMarkerName).join(separator) + close;
|
|
95
|
+
},
|
|
53
96
|
refs: (names) => {
|
|
54
97
|
const open = green('(');
|
|
55
98
|
const close = green(')');
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
import type { GlyphMode } from '../glyph-mode';
|
|
4
|
+
import {
|
|
5
|
+
abbreviateContractHash,
|
|
6
|
+
migrationListEmptySource,
|
|
7
|
+
migrationListForwardArrow,
|
|
8
|
+
} from './migration-list-data-column';
|
|
9
|
+
import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
|
|
10
|
+
|
|
11
|
+
export type LedgerTimestampMode = 'local' | 'utc' | 'iso';
|
|
12
|
+
|
|
13
|
+
export interface RenderMigrationLogTableOptions {
|
|
14
|
+
readonly utc?: boolean;
|
|
15
|
+
readonly styler?: MigrationListStyler;
|
|
16
|
+
readonly glyphMode?: GlyphMode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SerializedLedgerEntryRecord {
|
|
20
|
+
readonly space: string;
|
|
21
|
+
readonly migrationName: string;
|
|
22
|
+
readonly migrationHash: string;
|
|
23
|
+
readonly from: string | null;
|
|
24
|
+
readonly to: string;
|
|
25
|
+
readonly appliedAt: string;
|
|
26
|
+
readonly operationCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const HEADING_APPLIED_AT = 'Applied at';
|
|
30
|
+
const HEADING_SPACE = 'Space';
|
|
31
|
+
const HEADING_MIGRATION = 'Migration';
|
|
32
|
+
const HEADING_CHANGE = 'Change';
|
|
33
|
+
const HEADING_OPS = 'Ops';
|
|
34
|
+
const COLUMN_SEPARATOR = ' ';
|
|
35
|
+
const DIVIDER_CHAR = '─';
|
|
36
|
+
const ASCII_DIVIDER_CHAR = '-';
|
|
37
|
+
|
|
38
|
+
export function sortLedgerEntries(entries: readonly LedgerEntryRecord[]): LedgerEntryRecord[] {
|
|
39
|
+
return [...entries].sort((left, right) => {
|
|
40
|
+
const timeDiff = left.appliedAt.getTime() - right.appliedAt.getTime();
|
|
41
|
+
if (timeDiff !== 0) {
|
|
42
|
+
return timeDiff;
|
|
43
|
+
}
|
|
44
|
+
const spaceDiff = left.space.localeCompare(right.space);
|
|
45
|
+
if (spaceDiff !== 0) {
|
|
46
|
+
return spaceDiff;
|
|
47
|
+
}
|
|
48
|
+
return left.migrationName.localeCompare(right.migrationName);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pad2(value: number): string {
|
|
53
|
+
return String(value).padStart(2, '0');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatLedgerAppliedAt(date: Date, mode: LedgerTimestampMode): string {
|
|
57
|
+
if (mode === 'iso') {
|
|
58
|
+
return date.toISOString();
|
|
59
|
+
}
|
|
60
|
+
if (mode === 'utc') {
|
|
61
|
+
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}Z`;
|
|
62
|
+
}
|
|
63
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
64
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
65
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
66
|
+
const offsetHours = pad2(Math.floor(absoluteOffset / 60));
|
|
67
|
+
const offsetMins = pad2(absoluteOffset % 60);
|
|
68
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())} ${sign}${offsetHours}:${offsetMins}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatHashEndpoint(hash: string | null, glyphMode: GlyphMode = 'unicode'): string {
|
|
72
|
+
if (hash === null) {
|
|
73
|
+
return migrationListEmptySource(glyphMode);
|
|
74
|
+
}
|
|
75
|
+
return abbreviateContractHash(hash);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatHashTransition(
|
|
79
|
+
from: string | null,
|
|
80
|
+
to: string,
|
|
81
|
+
glyphMode: GlyphMode = 'unicode',
|
|
82
|
+
): string {
|
|
83
|
+
return `${formatHashEndpoint(from, glyphMode)} ${migrationListForwardArrow(glyphMode)} ${abbreviateContractHash(to)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function styleHashTransition(
|
|
87
|
+
from: string | null,
|
|
88
|
+
to: string,
|
|
89
|
+
styler: MigrationListStyler,
|
|
90
|
+
glyphMode: GlyphMode = 'unicode',
|
|
91
|
+
): string {
|
|
92
|
+
const fromPart =
|
|
93
|
+
from === null
|
|
94
|
+
? styler.glyph(migrationListEmptySource(glyphMode))
|
|
95
|
+
: styler.sourceHash(abbreviateContractHash(from));
|
|
96
|
+
const arrow = styler.glyph(migrationListForwardArrow(glyphMode));
|
|
97
|
+
const dest = styler.destHash(abbreviateContractHash(to));
|
|
98
|
+
return `${fromPart} ${arrow} ${dest}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function padVisible(text: string, targetWidth: number): string {
|
|
102
|
+
const padding = Math.max(0, targetWidth - stringWidth(text));
|
|
103
|
+
return text + ' '.repeat(padding);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function columnWidth(values: readonly string[]): number {
|
|
107
|
+
return values.reduce((max, value) => Math.max(max, stringWidth(value)), 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function padDividerCell(valueWidth: number, dividerChar: string): string {
|
|
111
|
+
return dividerChar.repeat(valueWidth + 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function padTextCell(value: string, valueWidth: number): string {
|
|
115
|
+
return ` ${padVisible(value, valueWidth)} `;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function padOpsCell(value: string, valueWidth: number): string {
|
|
119
|
+
const padding = Math.max(0, valueWidth - stringWidth(value));
|
|
120
|
+
return ` ${' '.repeat(padding)}${value} `;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function renderMigrationLogTable(
|
|
124
|
+
entries: readonly LedgerEntryRecord[],
|
|
125
|
+
options: RenderMigrationLogTableOptions = {},
|
|
126
|
+
): string {
|
|
127
|
+
const sorted = sortLedgerEntries(entries);
|
|
128
|
+
if (sorted.length === 0) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const styler = options.styler ?? IDENTITY_MIGRATION_LIST_STYLER;
|
|
133
|
+
const glyphMode = options.glyphMode ?? 'unicode';
|
|
134
|
+
const dividerChar = glyphMode === 'ascii' ? ASCII_DIVIDER_CHAR : DIVIDER_CHAR;
|
|
135
|
+
const showSpace = new Set(sorted.map((entry) => entry.space)).size > 1;
|
|
136
|
+
const timestampMode: LedgerTimestampMode = options.utc ? 'utc' : 'local';
|
|
137
|
+
const rows = sorted.map((entry) => ({
|
|
138
|
+
appliedAt: formatLedgerAppliedAt(entry.appliedAt, timestampMode),
|
|
139
|
+
space: entry.space,
|
|
140
|
+
migrationName: entry.migrationName,
|
|
141
|
+
transition: formatHashTransition(entry.from, entry.to, glyphMode),
|
|
142
|
+
ops: `${entry.operationCount} ops`,
|
|
143
|
+
from: entry.from,
|
|
144
|
+
to: entry.to,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const appliedAtWidth = columnWidth([HEADING_APPLIED_AT, ...rows.map((row) => row.appliedAt)]);
|
|
148
|
+
const spaceWidth = showSpace ? columnWidth([HEADING_SPACE, ...rows.map((row) => row.space)]) : 0;
|
|
149
|
+
const nameWidth = columnWidth([HEADING_MIGRATION, ...rows.map((row) => row.migrationName)]);
|
|
150
|
+
const transitionWidth = columnWidth([HEADING_CHANGE, ...rows.map((row) => row.transition)]);
|
|
151
|
+
const opsWidth = columnWidth([HEADING_OPS, ...rows.map((row) => row.ops)]);
|
|
152
|
+
|
|
153
|
+
const headingParts = [padTextCell(HEADING_APPLIED_AT, appliedAtWidth)];
|
|
154
|
+
if (showSpace) {
|
|
155
|
+
headingParts.push(padTextCell(HEADING_SPACE, spaceWidth));
|
|
156
|
+
}
|
|
157
|
+
headingParts.push(
|
|
158
|
+
padTextCell(HEADING_MIGRATION, nameWidth),
|
|
159
|
+
padTextCell(HEADING_CHANGE, transitionWidth),
|
|
160
|
+
padOpsCell(HEADING_OPS, opsWidth),
|
|
161
|
+
);
|
|
162
|
+
const heading = headingParts.join(COLUMN_SEPARATOR);
|
|
163
|
+
|
|
164
|
+
const dividerParts = [padDividerCell(appliedAtWidth, dividerChar)];
|
|
165
|
+
if (showSpace) {
|
|
166
|
+
dividerParts.push(padDividerCell(spaceWidth, dividerChar));
|
|
167
|
+
}
|
|
168
|
+
dividerParts.push(
|
|
169
|
+
padDividerCell(nameWidth, dividerChar),
|
|
170
|
+
padDividerCell(transitionWidth, dividerChar),
|
|
171
|
+
padDividerCell(opsWidth, dividerChar),
|
|
172
|
+
);
|
|
173
|
+
const divider = dividerParts.map((cell) => styler.summary(cell)).join(COLUMN_SEPARATOR);
|
|
174
|
+
|
|
175
|
+
const dataRows = rows.map((row) => {
|
|
176
|
+
const parts = [padTextCell(row.appliedAt, appliedAtWidth)];
|
|
177
|
+
if (showSpace) {
|
|
178
|
+
parts.push(padTextCell(row.space, spaceWidth));
|
|
179
|
+
}
|
|
180
|
+
parts.push(
|
|
181
|
+
padTextCell(styler.dirName(row.migrationName), nameWidth),
|
|
182
|
+
padTextCell(styleHashTransition(row.from, row.to, styler, glyphMode), transitionWidth),
|
|
183
|
+
padOpsCell(row.ops, opsWidth),
|
|
184
|
+
);
|
|
185
|
+
return parts.join(COLUMN_SEPARATOR);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return [heading, divider, ...dataRows].join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function serializeLedgerEntriesForJson(
|
|
192
|
+
entries: readonly LedgerEntryRecord[],
|
|
193
|
+
): SerializedLedgerEntryRecord[] {
|
|
194
|
+
return sortLedgerEntries(entries).map(({ appliedAt, ...rest }) => ({
|
|
195
|
+
...rest,
|
|
196
|
+
appliedAt: formatLedgerAppliedAt(appliedAt, 'iso'),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const MIGRATION_LOG_EMPTY_MESSAGE = 'No migrations have been applied to this database.';
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MigrationPlannerConflict,
|
|
3
|
+
OperationPreview,
|
|
4
|
+
} from '@prisma-next/framework-components/control';
|
|
2
5
|
import { cyan, green, yellow } from 'colorette';
|
|
3
6
|
|
|
4
7
|
import type { PerSpaceExecutionEntry } from '../../control-api/types';
|
|
@@ -85,11 +88,24 @@ export interface MigrationCommandResult {
|
|
|
85
88
|
readonly advancedRef?: { readonly name: string; readonly hash: string } | null;
|
|
86
89
|
readonly plannedAdvanceRef?: { readonly name: string; readonly hash: string } | null;
|
|
87
90
|
readonly summary: string;
|
|
91
|
+
readonly warnings?: readonly MigrationPlannerConflict[];
|
|
88
92
|
readonly timings: {
|
|
89
93
|
readonly total: number;
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
export function formatPlannerWarningsBlock(
|
|
98
|
+
warnings: readonly MigrationPlannerConflict[],
|
|
99
|
+
useColor: boolean,
|
|
100
|
+
): readonly string[] {
|
|
101
|
+
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
102
|
+
const lines: string[] = ['', 'Warnings:'];
|
|
103
|
+
for (const warning of warnings) {
|
|
104
|
+
lines.push(` ${formatDimText(`- ${warning.summary}`)}`);
|
|
105
|
+
}
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
/**
|
|
94
110
|
* Render the shared per-space execution block consumed by the `db init`
|
|
95
111
|
* / `db update` / `migrate` summaries. Always shows: space
|
|
@@ -165,6 +181,10 @@ export function formatMigrationPlanOutput(
|
|
|
165
181
|
lines.push(`${formatGreen('✔')} Planned ${operationCount} operation(s)`);
|
|
166
182
|
}
|
|
167
183
|
|
|
184
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
185
|
+
lines.push(...formatPlannerWarningsBlock(result.warnings, useColor));
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
const formatYellow = createColorFormatter(useColor, yellow);
|
|
169
189
|
|
|
170
190
|
// Per-space breakdown takes precedence over the flat ops tree when
|
|
@@ -427,6 +447,10 @@ export function formatMigrationApplyOutput(
|
|
|
427
447
|
lines.push(`${formatGreen('✔')} Applied ${executed} operation(s)`);
|
|
428
448
|
}
|
|
429
449
|
|
|
450
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
451
|
+
lines.push(...formatPlannerWarningsBlock(result.warnings, useColor));
|
|
452
|
+
}
|
|
453
|
+
|
|
430
454
|
// Per-space breakdown — replaces the single ambiguous `Signature:`
|
|
431
455
|
// line with a per-space marker + ops listing.
|
|
432
456
|
if (result.perSpace && result.perSpace.length > 0) {
|
|
@@ -180,3 +180,38 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
|
|
|
180
180
|
|
|
181
181
|
return flags as GlobalFlags;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Bridges the two TTY checks (stdout via `flags`, stdin via
|
|
186
|
+
* `process.stdin.isTTY`) into the `canPrompt` boolean the interactive
|
|
187
|
+
* `init` flow consumes.
|
|
188
|
+
*
|
|
189
|
+
* Per the [Style Guide § Interactivity](../../../../../../../docs/CLI%20Style%20Guide.md#interactivity):
|
|
190
|
+
*
|
|
191
|
+
* - `flags.interactive` governs *decoration* (TerminalUI, intro/outro,
|
|
192
|
+
* spinners) and is derived from stdout-TTY by `parseGlobalFlags`,
|
|
193
|
+
* honouring `--interactive` / `--no-interactive`.
|
|
194
|
+
* - Prompting additionally requires a stdin TTY — closing stdin is a
|
|
195
|
+
* common signal in CI / agent environments even when stdout stays
|
|
196
|
+
* attached.
|
|
197
|
+
* - `--interactive` is the explicit override: when the user passes it,
|
|
198
|
+
* we honour it (e.g. testing flows where stdin is stubbed).
|
|
199
|
+
*
|
|
200
|
+
* Single source of truth for the interactive-prompt decision: both the
|
|
201
|
+
* `init` action handler and the preAction telemetry bridge derive
|
|
202
|
+
* prompt-eligibility from this helper so they cannot drift. Lives in
|
|
203
|
+
* `global-flags` (alongside `parseGlobalFlags`) to keep
|
|
204
|
+
* `utils/telemetry` and `commands/init/index` free of an import cycle.
|
|
205
|
+
*
|
|
206
|
+
* Exported so callers and tests can derive the same value without
|
|
207
|
+
* touching `process` globals.
|
|
208
|
+
*/
|
|
209
|
+
export function deriveCanPrompt(opts: {
|
|
210
|
+
readonly flagsInteractive: boolean | undefined;
|
|
211
|
+
readonly optionInteractive: boolean | undefined;
|
|
212
|
+
readonly stdinIsTTY: boolean;
|
|
213
|
+
}): boolean {
|
|
214
|
+
if (opts.optionInteractive === true) return true;
|
|
215
|
+
if (opts.flagsInteractive === false) return false;
|
|
216
|
+
return opts.stdinIsTTY;
|
|
217
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
2
|
+
import { type CliStructuredError, errorLegendHumanOnly } from './cli-errors';
|
|
3
|
+
import type { GlobalFlags } from './global-flags';
|
|
4
|
+
|
|
5
|
+
export interface LegendCliOptions {
|
|
6
|
+
readonly legend?: boolean;
|
|
7
|
+
readonly dot?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The legend is decoration printed alongside the command header on stderr, so
|
|
12
|
+
* it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
|
|
13
|
+
* `--quiet`) exactly as the header is.
|
|
14
|
+
*/
|
|
15
|
+
export function shouldShowLegend(options: LegendCliOptions, flags: GlobalFlags): boolean {
|
|
16
|
+
return (
|
|
17
|
+
options.legend === true && options.dot !== true && flags.json !== true && flags.quiet !== true
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateLegendOptions(
|
|
22
|
+
options: LegendCliOptions,
|
|
23
|
+
flags: GlobalFlags,
|
|
24
|
+
): Result<void, CliStructuredError> {
|
|
25
|
+
if (options.legend !== true) {
|
|
26
|
+
return ok(undefined);
|
|
27
|
+
}
|
|
28
|
+
if (flags.json === true) {
|
|
29
|
+
return notOk(errorLegendHumanOnly('--json'));
|
|
30
|
+
}
|
|
31
|
+
if (flags.quiet === true) {
|
|
32
|
+
return notOk(errorLegendHumanOnly('--quiet'));
|
|
33
|
+
}
|
|
34
|
+
if (options.dot === true) {
|
|
35
|
+
return notOk(errorLegendHumanOnly('--dot'));
|
|
36
|
+
}
|
|
37
|
+
return ok(undefined);
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'pathe';
|
|
4
|
+
import { type CliStructuredError, errorRuntime } from './cli-errors';
|
|
5
|
+
|
|
6
|
+
export function looksLikePath(target: string): boolean {
|
|
7
|
+
return target.includes('/') || target.includes('\\');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveAppTargetPath(
|
|
11
|
+
target: string,
|
|
12
|
+
appMigrationsDir: string,
|
|
13
|
+
appMigrationsRelative: string,
|
|
14
|
+
): Result<string, CliStructuredError> {
|
|
15
|
+
const targetPath = resolve(target);
|
|
16
|
+
const relativeToApp = relative(appMigrationsDir, targetPath);
|
|
17
|
+
const isOutsideAppDir =
|
|
18
|
+
relativeToApp === '' ||
|
|
19
|
+
relativeToApp === '.' ||
|
|
20
|
+
relativeToApp.startsWith('..') ||
|
|
21
|
+
isAbsolute(relativeToApp);
|
|
22
|
+
if (isOutsideAppDir) {
|
|
23
|
+
return notOk(
|
|
24
|
+
errorRuntime('Target must point to an app-space migration', {
|
|
25
|
+
why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
|
|
26
|
+
fix: 'Pass an app-space migration directory or use a hash prefix.',
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return ok(targetPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findPackageByDirPath(
|
|
34
|
+
packages: readonly OnDiskMigrationPackage[],
|
|
35
|
+
resolvedDirPath: string,
|
|
36
|
+
): OnDiskMigrationPackage | undefined {
|
|
37
|
+
const normalized = resolve(resolvedDirPath);
|
|
38
|
+
return packages.find((p) => resolve(p.dirPath) === normalized);
|
|
39
|
+
}
|
package/src/utils/telemetry.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
2
2
|
import {
|
|
3
3
|
type CommanderOptionShape,
|
|
4
4
|
type CommanderResultShape,
|
|
5
|
+
ensureInstallationId,
|
|
5
6
|
readUserConfig,
|
|
6
7
|
resolveGating,
|
|
7
8
|
runTelemetry,
|
|
8
9
|
type TelemetryRunOutcome,
|
|
9
10
|
type UserConfig,
|
|
11
|
+
userConfigPath,
|
|
10
12
|
} from '@prisma-next/cli-telemetry';
|
|
11
|
-
import { ifDefined } from '@prisma-next/utils/defined';
|
|
12
13
|
import type { Command } from 'commander';
|
|
13
14
|
import { version as CLI_VERSION } from '../../package.json' with { type: 'json' };
|
|
14
15
|
import { isCI } from './is-ci';
|
|
@@ -78,11 +79,7 @@ function senderPath(): string {
|
|
|
78
79
|
return fileURLToPath(new URL(import.meta.resolve('@prisma-next/cli-telemetry/sender')));
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function fireTelemetry(
|
|
82
|
-
actionCommand: Command,
|
|
83
|
-
userConfig: UserConfig,
|
|
84
|
-
overrides: { readonly databaseTarget?: string } = {},
|
|
85
|
-
): TelemetryRunOutcome {
|
|
82
|
+
function fireTelemetry(actionCommand: Command, userConfig: UserConfig): TelemetryRunOutcome {
|
|
86
83
|
return runTelemetry({
|
|
87
84
|
command: commanderSnapshotForTelemetry(actionCommand),
|
|
88
85
|
version: CLI_VERSION,
|
|
@@ -91,7 +88,6 @@ function fireTelemetry(
|
|
|
91
88
|
isCI: isCI(),
|
|
92
89
|
env: process.env,
|
|
93
90
|
userConfig,
|
|
94
|
-
...ifDefined('databaseTarget', overrides.databaseTarget),
|
|
95
91
|
});
|
|
96
92
|
}
|
|
97
93
|
|
|
@@ -107,35 +103,75 @@ function fireTelemetry(
|
|
|
107
103
|
* config touches disk. The child loading user TS code is acceptable
|
|
108
104
|
* only because it's gated behind the same resolved-enabled signal.
|
|
109
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Builds the one-time first-run disclosure. The resolved absolute path to
|
|
108
|
+
* the user-level config file is substituted in so the user can see exactly
|
|
109
|
+
* which file to edit (it must not be confused with `prisma-next.config.ts`).
|
|
110
|
+
* `prisma-next telemetry disable` is named as the primary, friendliest
|
|
111
|
+
* opt-out, alongside the env vars and the config edit.
|
|
112
|
+
*/
|
|
113
|
+
function firstRunNotice(configPath: string): string {
|
|
114
|
+
return [
|
|
115
|
+
'Prisma Next collects anonymous CLI usage data, enabled by default.',
|
|
116
|
+
"What's collected and why: https://prisma-next.dev/docs/cli/telemetry.",
|
|
117
|
+
'Opt out: run "prisma-next telemetry disable", set DO_NOT_TRACK=1 or',
|
|
118
|
+
`PRISMA_NEXT_DISABLE_TELEMETRY=1, or set "enableTelemetry": false in ${configPath}.`,
|
|
119
|
+
].join(' ');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Best-effort first-run disclosure + installationId mint. Runs only on the
|
|
124
|
+
* gating-enabled path. Prints the notice to stderr (never stdout) and mints
|
|
125
|
+
* a persistent id without touching `enableTelemetry`, so the opt-out default
|
|
126
|
+
* stays intact and no unasked-for consent is recorded.
|
|
127
|
+
*
|
|
128
|
+
* Every step is wrapped so an un-writable config dir (or any other failure)
|
|
129
|
+
* never throws and never blocks the command. Returns the minted (or
|
|
130
|
+
* pre-existing) id so the caller can forward it to `runTelemetry` without a
|
|
131
|
+
* redundant disk read. On mint failure it returns `undefined`: the notice may
|
|
132
|
+
* reprint next run, and `runTelemetry` no-ops on the missing id.
|
|
133
|
+
*/
|
|
134
|
+
function discloseAndMintOnFirstRun(): string | undefined {
|
|
135
|
+
try {
|
|
136
|
+
process.stderr.write(`${firstRunNotice(userConfigPath())}\n`);
|
|
137
|
+
} catch {}
|
|
138
|
+
try {
|
|
139
|
+
return ensureInstallationId();
|
|
140
|
+
} catch {}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* True when the run is the `telemetry` command (or one of its
|
|
146
|
+
* subcommands). The usage-telemetry preAction fire is exempted for it:
|
|
147
|
+
* it would be absurd for `telemetry disable` to send a usage event before
|
|
148
|
+
* disabling, or for `telemetry status` to mint an id + send while merely
|
|
149
|
+
* reporting state. This is the only command-specific exemption.
|
|
150
|
+
*
|
|
151
|
+
* The check is rooted at the program: the path must be
|
|
152
|
+
* `['prisma-next', 'telemetry', …]`, so it matches the top-level
|
|
153
|
+
* `telemetry` command and its subcommands without matching a hypothetical
|
|
154
|
+
* nested `… telemetry` elsewhere.
|
|
155
|
+
*/
|
|
156
|
+
function isTelemetryCommand(actionCommand: Command): boolean {
|
|
157
|
+
return commandPathFor(actionCommand)[1] === 'telemetry';
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
export function fireTelemetryFromPreAction(actionCommand: Command): TelemetryRunOutcome {
|
|
161
|
+
if (isTelemetryCommand(actionCommand)) {
|
|
162
|
+
return { spawned: false, reason: 'gated-off' };
|
|
163
|
+
}
|
|
111
164
|
const gate = resolveTelemetryGate();
|
|
112
165
|
if (!gate.enabled) {
|
|
113
166
|
return gate.outcome;
|
|
114
167
|
}
|
|
168
|
+
const storedId = gate.userConfig.installationId;
|
|
169
|
+
if (typeof storedId !== 'string' || storedId.length === 0) {
|
|
170
|
+
const installationId = discloseAndMintOnFirstRun();
|
|
171
|
+
return fireTelemetry(
|
|
172
|
+
actionCommand,
|
|
173
|
+
installationId === undefined ? gate.userConfig : { ...gate.userConfig, installationId },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
115
176
|
return fireTelemetry(actionCommand, gate.userConfig);
|
|
116
177
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Manual one-shot telemetry path for the first `init` run where the user
|
|
120
|
-
* explicitly answers Yes to the consent prompt. The preAction hook for
|
|
121
|
-
* that same run has already resolved before consent existed, so it is
|
|
122
|
-
* default-off. After consent is persisted, `runInit` calls this helper
|
|
123
|
-
* exactly for that first affirmative answer; subsequent init runs skip
|
|
124
|
-
* it because the prompt is not shown again.
|
|
125
|
-
*
|
|
126
|
-
* The child's c12 load would return `databaseTarget: null` for this
|
|
127
|
-
* specific invocation because `prisma-next.config.*` is not yet on
|
|
128
|
-
* disk (init writes it later in the same run). To preserve the
|
|
129
|
-
* prompt-chosen target in the first-init telemetry event, this
|
|
130
|
-
* helper forwards the value as a parent-side IPC override on
|
|
131
|
-
* `ParentToSenderPayload.databaseTarget` — the child consults the
|
|
132
|
-
* override first and falls back to its c12 result when absent.
|
|
133
|
-
*/
|
|
134
|
-
export function fireTelemetryAfterInitConsent(
|
|
135
|
-
actionCommand: Command,
|
|
136
|
-
inputs: { readonly databaseTarget: string },
|
|
137
|
-
): TelemetryRunOutcome {
|
|
138
|
-
return fireTelemetry(actionCommand, readUserConfig(), {
|
|
139
|
-
databaseTarget: inputs.databaseTarget,
|
|
140
|
-
});
|
|
141
|
-
}
|