@prisma-next/cli 0.12.0-dev.9 → 0.13.0-dev.1
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-CJzuo5wX.mjs} +222 -107
- package/dist/client-CJzuo5wX.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
- package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +4 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +6 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- 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.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +37 -3
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +298 -12
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +55 -13
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +3 -2
- package/dist/commands/migration-graph.d.mts +16 -25
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +185 -2
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +26 -27
- 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 +9 -19
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +6 -5
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +17 -21
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +24 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +42 -144
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +4 -4
- package/dist/commands/ref.mjs.map +1 -1
- 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/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
- package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
- package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
- package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
- package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
- package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +5 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +1 -3
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
- package/dist/init-0HwB-Vh8.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
- package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
- package/dist/migration-check-soB5uZEQ.mjs +573 -0
- package/dist/migration-check-soB5uZEQ.mjs.map +1 -0
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
- package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
- package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
- package/dist/migration-list-DlJJ_38Z.mjs +230 -0
- package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
- package/dist/migration-log-CG0qQAFm.mjs +222 -0
- package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
- package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
- package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
- package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
- package/dist/migration-status-CgWSoI_g.mjs +446 -0
- package/dist/migration-status-CgWSoI_g.mjs.map +1 -0
- package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
- package/dist/schemas-CeGMYFYX.d.mts +191 -0
- package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
- package/dist/schemas-KhXMzNA_.mjs +112 -0
- package/dist/schemas-KhXMzNA_.mjs.map +1 -0
- package/dist/telemetry-BIM4beEO.mjs +122 -0
- package/dist/telemetry-BIM4beEO.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
- package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
- package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
- package/dist/types-C_tYiJYx.d.mts.map +1 -0
- package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
- package/package.json +26 -22
- package/src/cli.ts +5 -0
- package/src/commands/contract-infer.ts +2 -2
- 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/inspect-live-schema.ts +10 -0
- package/src/commands/json/schemas.ts +195 -0
- package/src/commands/migrate.ts +527 -8
- package/src/commands/migration-check.ts +469 -134
- package/src/commands/migration-graph.ts +151 -119
- package/src/commands/migration-list.ts +72 -39
- package/src/commands/migration-log.ts +52 -102
- package/src/commands/migration-new.ts +2 -1
- package/src/commands/migration-plan.ts +2 -1
- package/src/commands/migration-show.ts +31 -66
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +458 -1066
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +70 -9
- package/src/control-api/operations/contract-emit.ts +22 -2
- package/src/control-api/operations/db-init.ts +6 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
- package/src/control-api/operations/db-update.ts +7 -4
- package/src/control-api/operations/db-verify.ts +15 -5
- package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
- package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
- package/src/control-api/types.ts +56 -29
- package/src/utils/cli-errors.ts +70 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-command-render.ts +239 -0
- package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
- package/src/utils/formatters/migration-graph-labels.ts +408 -0
- package/src/utils/formatters/migration-graph-model.ts +103 -0
- package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +188 -0
- package/src/utils/formatters/migration-list-data-column.ts +4 -91
- package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
- package/src/utils/formatters/migration-list-render.ts +135 -71
- package/src/utils/formatters/migration-list-styler.ts +46 -5
- package/src/utils/formatters/migration-list-types.ts +5 -21
- package/src/utils/formatters/migration-log-table.ts +205 -0
- package/src/utils/formatters/migrations.ts +33 -11
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/integrity-violation-to-check-failure.ts +28 -19
- package/src/utils/legend.ts +38 -0
- package/src/utils/migration-path-target.ts +60 -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/contract-emit-D-4jrNve.mjs.map +0 -1
- package/dist/contract-infer-D8uEbJuu.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/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
- package/dist/migration-check-BiBJoYYW.mjs +0 -341
- package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
- package/dist/migration-graph-C9WC-7eO.mjs +0 -1478
- package/dist/migration-graph-C9WC-7eO.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-plan-9DJ7q7_z.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/terminal-ui-5Y6mrg93.d.mts.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
- package/src/utils/formatters/migration-graph-lane-colors.ts +0 -31
- package/src/utils/formatters/migration-graph-layout.ts +0 -1141
- package/src/utils/formatters/migration-graph-tree-render.ts +0 -768
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from '@prisma-next/framework-components/control';
|
|
6
|
-
import {
|
|
7
|
-
type ContractMarkerRecordLike,
|
|
8
|
-
type ContractSpaceAggregate,
|
|
9
|
-
graphWalkStrategy,
|
|
10
|
-
loadContractSpaceAggregate,
|
|
11
|
-
requireHeadRef,
|
|
1
|
+
import type { LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
ContractMarkerRecordLike,
|
|
4
|
+
ContractSpaceMember,
|
|
12
5
|
} from '@prisma-next/migration-tools/aggregate';
|
|
13
6
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
14
7
|
import {
|
|
@@ -16,29 +9,22 @@ import {
|
|
|
16
9
|
errorUnknownInvariant,
|
|
17
10
|
MigrationToolsError,
|
|
18
11
|
} from '@prisma-next/migration-tools/errors';
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
findPath,
|
|
22
|
-
findPathWithDecision,
|
|
23
|
-
findReachableLeaves,
|
|
24
|
-
} from '@prisma-next/migration-tools/migration-graph';
|
|
25
|
-
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
12
|
+
import { findPath, findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
|
|
26
13
|
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
27
14
|
import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
|
|
28
15
|
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
29
16
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
30
17
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
31
|
-
import {
|
|
18
|
+
import { dim, yellow } from 'colorette';
|
|
32
19
|
import { Command } from 'commander';
|
|
33
|
-
|
|
34
20
|
import { loadConfig } from '../config-loader';
|
|
35
21
|
import { createControlClient } from '../control-api/client';
|
|
36
22
|
import {
|
|
37
23
|
CliStructuredError,
|
|
38
|
-
errorRuntime,
|
|
39
24
|
errorUnexpected,
|
|
40
25
|
mapMigrationToolsError,
|
|
41
26
|
mapRefResolutionError,
|
|
27
|
+
requireLiveDatabase,
|
|
42
28
|
} from '../utils/cli-errors';
|
|
43
29
|
import {
|
|
44
30
|
addGlobalOptions,
|
|
@@ -49,537 +35,269 @@ import {
|
|
|
49
35
|
setCommandDescriptions,
|
|
50
36
|
setCommandExamples,
|
|
51
37
|
setCommandSeeAlso,
|
|
52
|
-
toPathDecisionResult,
|
|
53
38
|
toStructuralEdge,
|
|
54
39
|
} from '../utils/command-helpers';
|
|
55
40
|
import {
|
|
56
|
-
|
|
41
|
+
buildReadAggregate,
|
|
57
42
|
loadContractRawSafely,
|
|
58
|
-
refuseContractSpaceIntegrity,
|
|
59
43
|
refusePackageCorruptionOnAggregate,
|
|
60
44
|
} from '../utils/contract-space-aggregate-loader';
|
|
61
|
-
import { toDeclaredExtensionsFromRaw } from '../utils/extension-pack-inputs';
|
|
62
45
|
import {
|
|
63
|
-
type
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from '../utils/formatters/graph-migration-mapper';
|
|
46
|
+
type MigrationEdgeAnnotation,
|
|
47
|
+
renderMigrationGraphLegend,
|
|
48
|
+
} from '../utils/formatters/migration-graph-labels';
|
|
67
49
|
import {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
computeGlobalMaxDirNameWidth,
|
|
51
|
+
computeGlobalMaxEdgeTreePrefixWidth,
|
|
52
|
+
indentMigrationGraphTreeBlock,
|
|
53
|
+
renderMigrationGraphSpaceTree,
|
|
54
|
+
} from '../utils/formatters/migration-graph-space-render';
|
|
55
|
+
import type { MigrationListEntry } from '../utils/formatters/migration-list-types';
|
|
72
56
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
73
57
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
74
58
|
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
|
|
75
|
-
import
|
|
59
|
+
import { shouldShowLegend, validateLegendOptions } from '../utils/legend';
|
|
76
60
|
import { handleResult } from '../utils/result-handler';
|
|
77
61
|
import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
|
|
78
|
-
|
|
79
|
-
|
|
62
|
+
import type {
|
|
63
|
+
MigrationStatusEntry,
|
|
64
|
+
MigrationStatusResult,
|
|
65
|
+
MigrationStatusSpace,
|
|
66
|
+
StatusDiagnosticJson,
|
|
67
|
+
} from './json/schemas';
|
|
68
|
+
import { migrationStatusJsonResultSchema } from './json/schemas';
|
|
69
|
+
import {
|
|
70
|
+
listRefsByContractHash,
|
|
71
|
+
migrationSpaceListEntriesFromAggregate,
|
|
72
|
+
runMigrationList,
|
|
73
|
+
} from './migration-list';
|
|
74
|
+
import {
|
|
75
|
+
appliedHashesFromLedger,
|
|
76
|
+
deriveStatusEdgeAnnotations,
|
|
77
|
+
statusForMigrationHash,
|
|
78
|
+
} from './migration-status-overlay';
|
|
79
|
+
|
|
80
|
+
export type { StatusRef } from '../utils/migration-types';
|
|
81
|
+
export type {
|
|
82
|
+
MigrationStatusEntry,
|
|
83
|
+
MigrationStatusResult,
|
|
84
|
+
MigrationStatusSpace,
|
|
85
|
+
StatusDiagnosticJson,
|
|
86
|
+
};
|
|
87
|
+
export { migrationStatusJsonResultSchema };
|
|
88
|
+
|
|
89
|
+
export interface MigrationStatusOptions extends CommonCommandOptions {
|
|
80
90
|
readonly db?: string;
|
|
81
91
|
readonly config?: string;
|
|
82
92
|
readonly to?: string;
|
|
83
93
|
readonly from?: string;
|
|
94
|
+
readonly space?: string;
|
|
95
|
+
readonly legend?: boolean;
|
|
96
|
+
readonly ascii?: boolean;
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
export interface
|
|
87
|
-
readonly
|
|
88
|
-
readonly
|
|
89
|
-
readonly
|
|
90
|
-
readonly migrationHash: string;
|
|
91
|
-
readonly operationCount: number;
|
|
92
|
-
readonly operationSummary: string;
|
|
93
|
-
readonly hasDestructive: boolean;
|
|
94
|
-
readonly status: EdgeStatusKind | 'unknown';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Per-space status row in the aggregate-shaped status output.
|
|
99
|
-
*
|
|
100
|
-
* Surfaces, for each contract space:
|
|
101
|
-
*
|
|
102
|
-
* - `headHash`: the on-disk head ref's hash (where the space is going).
|
|
103
|
-
* - `markerHash`: the live marker hash for the space, or null if no
|
|
104
|
-
* marker has been written yet (greenfield, or pre-`migrate`).
|
|
105
|
-
* - `pendingCount`: number of migration edges between marker and head.
|
|
106
|
-
* Computed via {@link graphWalkStrategy}; 0 means the space is
|
|
107
|
-
* already at head.
|
|
108
|
-
* - `status`: convenience tag the formatter uses to pick a glyph.
|
|
109
|
-
* `'never-planned'` is reserved for spaces with non-empty head but
|
|
110
|
-
* no on-disk migrations — which shouldn't happen if the loader's
|
|
111
|
-
* integrity check passes.
|
|
112
|
-
*
|
|
113
|
-
* Online-only fields (`markerHash`, `status`) are absent when the
|
|
114
|
-
* command runs without a database connection.
|
|
115
|
-
*/
|
|
116
|
-
export interface MigrationStatusSpaceEntry {
|
|
117
|
-
readonly spaceId: string;
|
|
118
|
-
readonly kind: 'app' | 'extension';
|
|
119
|
-
readonly headHash: string;
|
|
120
|
-
readonly markerHash?: string | null;
|
|
121
|
-
readonly pendingCount?: number;
|
|
122
|
-
readonly status?: 'up-to-date' | 'pending' | 'no-marker' | 'never-planned' | 'unreachable';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Sum per-space `pendingCount` into a cross-space total, but only when
|
|
127
|
-
* every loaded space reports a defined `pendingCount`. Returns
|
|
128
|
-
* `undefined` if any space is on the marker-unknown / offline path
|
|
129
|
-
* (where `pendingCount` is intentionally absent), so JSON consumers can
|
|
130
|
-
* distinguish "no pending" from "unknown".
|
|
131
|
-
*/
|
|
132
|
-
export function computeTotalPendingAcrossSpaces(
|
|
133
|
-
spaces: readonly MigrationStatusSpaceEntry[],
|
|
134
|
-
): number | undefined {
|
|
135
|
-
if (spaces.length === 0) return undefined;
|
|
136
|
-
let total = 0;
|
|
137
|
-
for (const s of spaces) {
|
|
138
|
-
if (s.pendingCount === undefined) return undefined;
|
|
139
|
-
total += s.pendingCount;
|
|
140
|
-
}
|
|
141
|
-
return total;
|
|
99
|
+
export interface MigrationStatusTreeSection {
|
|
100
|
+
readonly space: string;
|
|
101
|
+
readonly tree: string;
|
|
102
|
+
readonly showHeading: boolean;
|
|
142
103
|
}
|
|
143
104
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export interface MigrationStatusResult {
|
|
105
|
+
interface MigrationStatusCommandResult {
|
|
147
106
|
readonly ok: true;
|
|
148
|
-
readonly
|
|
149
|
-
readonly migrations: readonly MigrationStatusEntry[];
|
|
150
|
-
readonly markerHash?: string;
|
|
151
|
-
readonly targetHash: string;
|
|
152
|
-
readonly contractHash: string;
|
|
153
|
-
readonly refs?: readonly StatusRef[];
|
|
154
|
-
/** Required invariants from the active ref, sorted ascending. Always present (`[]` when no `--ref` or the ref declares none) — knowable offline. */
|
|
155
|
-
readonly requiredInvariants: readonly string[];
|
|
156
|
-
/**
|
|
157
|
-
* Invariants the marker has applied at least once, intersected with
|
|
158
|
-
* `requiredInvariants` for display relevance. JSON consumers see only the
|
|
159
|
-
* subset overlapping the active ref's required set — the full unfiltered
|
|
160
|
-
* marker invariant list lives on `marker.invariants` (control plane) and
|
|
161
|
-
* is not surfaced here. Present only in `mode === 'online'`; absent when
|
|
162
|
-
* offline (the marker is unknown, not empty).
|
|
163
|
-
*/
|
|
164
|
-
readonly appliedInvariants?: readonly string[];
|
|
165
|
-
/** required − applied. Present only in `mode === 'online'`; absent when offline. */
|
|
166
|
-
readonly missingInvariants?: readonly string[];
|
|
167
|
-
readonly pathDecision?: {
|
|
168
|
-
readonly fromHash: string;
|
|
169
|
-
readonly toHash: string;
|
|
170
|
-
readonly alternativeCount: number;
|
|
171
|
-
readonly tieBreakReasons: readonly string[];
|
|
172
|
-
readonly refName?: string;
|
|
173
|
-
readonly requiredInvariants: readonly string[];
|
|
174
|
-
readonly satisfiedInvariants: readonly string[];
|
|
175
|
-
readonly selectedPath: readonly {
|
|
176
|
-
readonly dirName: string;
|
|
177
|
-
readonly migrationHash: string;
|
|
178
|
-
readonly from: string;
|
|
179
|
-
readonly to: string;
|
|
180
|
-
readonly invariants: readonly string[];
|
|
181
|
-
}[];
|
|
182
|
-
};
|
|
107
|
+
readonly spaces: readonly MigrationStatusSpace[];
|
|
183
108
|
readonly summary: string;
|
|
184
|
-
readonly diagnostics: readonly
|
|
185
|
-
|
|
186
|
-
* Aggregate enumeration of every on-disk contract space (app +
|
|
187
|
-
* extensions), in canonical schedule order (extensions
|
|
188
|
-
* alphabetically, then app). Present whenever the aggregate loader
|
|
189
|
-
* succeeded; absent in early-error returns (e.g. unreadable
|
|
190
|
-
* migrations directory) where the existing diagnostics already
|
|
191
|
-
* surface the failure.
|
|
192
|
-
*
|
|
193
|
-
* The top-level fields (`migrations`, `markerHash`, `targetHash`,
|
|
194
|
-
* `pathDecision`, …) describe the **app member** specifically.
|
|
195
|
-
* Per-space detail for extension members lives only on this list.
|
|
196
|
-
*/
|
|
197
|
-
readonly spaces?: readonly MigrationStatusSpaceEntry[];
|
|
198
|
-
/** Cross-space pending-migration total (sum of `spaces[].pendingCount`). Present when `spaces` is. */
|
|
199
|
-
readonly totalPendingAcrossSpaces?: number;
|
|
200
|
-
readonly graph?: MigrationGraph;
|
|
201
|
-
readonly bundles?: readonly OnDiskMigrationPackage[];
|
|
202
|
-
readonly edgeStatuses?: readonly EdgeStatus[];
|
|
203
|
-
readonly activeRefHash?: string;
|
|
204
|
-
readonly activeRefName?: string;
|
|
205
|
-
readonly diverged?: boolean;
|
|
109
|
+
readonly diagnostics: readonly StatusDiagnosticJson[];
|
|
110
|
+
readonly treeSections: readonly MigrationStatusTreeSection[];
|
|
206
111
|
}
|
|
207
112
|
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} {
|
|
212
|
-
if (ops.length === 0) return { summary: '0 ops', hasDestructive: false };
|
|
213
|
-
|
|
214
|
-
const classes = new Map<string, number>();
|
|
215
|
-
for (const op of ops) {
|
|
216
|
-
classes.set(op.operationClass, (classes.get(op.operationClass) ?? 0) + 1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const hasDestructive = classes.has('destructive');
|
|
220
|
-
const count = ops.length;
|
|
221
|
-
const noun = count === 1 ? 'op' : 'ops';
|
|
222
|
-
|
|
223
|
-
if (classes.size === 1) {
|
|
224
|
-
const cls = [...classes.keys()][0]!;
|
|
225
|
-
return { summary: `${count} ${noun} (all ${cls})`, hasDestructive };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const destructiveCount = classes.get('destructive');
|
|
229
|
-
if (destructiveCount) {
|
|
230
|
-
return { summary: `${count} ${noun} (${destructiveCount} destructive)`, hasDestructive };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const parts = [...classes.entries()].map(([cls, n]) => `${n} ${cls}`);
|
|
234
|
-
return { summary: `${count} ${noun} (${parts.join(', ')})`, hasDestructive };
|
|
113
|
+
function shortDisplayHash(hash: string): string {
|
|
114
|
+
const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
115
|
+
return stripped.slice(0, 12);
|
|
235
116
|
}
|
|
236
117
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
* - **applied**: edge is on the path from root to the DB marker
|
|
241
|
-
* - **pending**: edge is on the path from the DB marker to the target
|
|
242
|
-
* (and the marker is reachable from root, i.e. it's on the same branch)
|
|
243
|
-
* - **unreachable**: edge is on the path from root to the target but the DB
|
|
244
|
-
* marker is on a different branch — `apply` can't reach these edges
|
|
245
|
-
* without the DB first moving to this branch
|
|
246
|
-
*
|
|
247
|
-
* Returns statuses only for edges that have a known status (skips offline
|
|
248
|
-
* and edges not on any relevant path).
|
|
249
|
-
*
|
|
250
|
-
* @internal Exported for testing only.
|
|
251
|
-
*/
|
|
252
|
-
export function deriveEdgeStatuses(
|
|
253
|
-
graph: MigrationGraph,
|
|
254
|
-
targetHash: string,
|
|
255
|
-
contractHash: string,
|
|
256
|
-
markerHash: string | undefined,
|
|
257
|
-
mode: 'online' | 'offline',
|
|
258
|
-
): EdgeStatus[] {
|
|
259
|
-
if (mode === 'offline') return [];
|
|
260
|
-
|
|
261
|
-
const edgeKey = (e: MigrationEdge) => `${e.from}\0${e.to}`;
|
|
262
|
-
|
|
263
|
-
// No marker = empty DB — treat root as the marker (nothing applied, everything pending)
|
|
264
|
-
const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
265
|
-
|
|
266
|
-
const appliedPath =
|
|
267
|
-
markerHash !== undefined ? findPath(graph, EMPTY_CONTRACT_HASH, markerHash) : null;
|
|
118
|
+
function resolveTarget(contractHash: string, activeRefHash: string | undefined): string {
|
|
119
|
+
return activeRefHash ?? contractHash;
|
|
120
|
+
}
|
|
268
121
|
|
|
269
|
-
|
|
270
|
-
|
|
122
|
+
function buildStatusMigrations(
|
|
123
|
+
listMigrations: readonly MigrationListEntry[],
|
|
124
|
+
annotations: ReadonlyMap<string, MigrationEdgeAnnotation>,
|
|
125
|
+
): readonly MigrationStatusEntry[] {
|
|
126
|
+
return listMigrations.map((migration) => ({
|
|
127
|
+
...migration,
|
|
128
|
+
status: statusForMigrationHash(migration.hash, annotations),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
271
131
|
|
|
272
|
-
|
|
273
|
-
|
|
132
|
+
function renderSpaceTree(args: {
|
|
133
|
+
readonly member: ContractSpaceMember;
|
|
134
|
+
readonly liveContractHash: string;
|
|
135
|
+
readonly migrations: readonly MigrationListEntry[];
|
|
136
|
+
readonly markerHash: string | undefined;
|
|
137
|
+
readonly showDbMarker: boolean;
|
|
138
|
+
readonly statusOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>;
|
|
139
|
+
readonly colorize: boolean;
|
|
140
|
+
readonly glyphMode: 'unicode' | 'ascii';
|
|
141
|
+
readonly isAppSpace: boolean;
|
|
142
|
+
readonly globalMaxEdgeTreePrefixWidth?: number;
|
|
143
|
+
readonly globalMaxDirNameWidth?: number;
|
|
144
|
+
}): string {
|
|
145
|
+
const graph = args.member.graph();
|
|
146
|
+
if (graph.nodes.size === 0) {
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
return renderMigrationGraphSpaceTree({
|
|
150
|
+
graph,
|
|
151
|
+
migrations: args.migrations,
|
|
152
|
+
liveContractHash: args.liveContractHash,
|
|
153
|
+
refsByHash: listRefsByContractHash(args.member),
|
|
154
|
+
statusOverlayByHash: args.statusOverlay,
|
|
155
|
+
colorize: args.colorize,
|
|
156
|
+
glyphMode: args.glyphMode,
|
|
157
|
+
isAppSpace: args.isAppSpace,
|
|
158
|
+
...(args.showDbMarker && args.markerHash !== undefined ? { dbHash: args.markerHash } : {}),
|
|
159
|
+
...(args.globalMaxEdgeTreePrefixWidth !== undefined
|
|
160
|
+
? { globalMaxEdgeTreePrefixWidth: args.globalMaxEdgeTreePrefixWidth }
|
|
161
|
+
: {}),
|
|
162
|
+
...(args.globalMaxDirNameWidth !== undefined
|
|
163
|
+
? { globalMaxDirNameWidth: args.globalMaxDirNameWidth }
|
|
164
|
+
: {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
274
167
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
assignedKeys.add(edgeKey(e));
|
|
279
|
-
statuses.push({ dirName: e.dirName, status: 'applied' });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
168
|
+
function countPending(migrations: readonly MigrationStatusEntry[]): number {
|
|
169
|
+
return migrations.filter((m) => m.status === 'pending').length;
|
|
170
|
+
}
|
|
282
171
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
172
|
+
export function buildNoPathSummary(args: {
|
|
173
|
+
readonly markerHash: string | undefined;
|
|
174
|
+
readonly targetHash: string;
|
|
175
|
+
readonly explicitTarget: boolean;
|
|
176
|
+
readonly refName: string | undefined;
|
|
177
|
+
}): string {
|
|
178
|
+
const markerPart =
|
|
179
|
+
args.markerHash !== undefined
|
|
180
|
+
? `the database state (${shortDisplayHash(args.markerHash)})`
|
|
181
|
+
: 'the database state';
|
|
182
|
+
const targetShort = shortDisplayHash(args.targetHash);
|
|
183
|
+
if (!args.explicitTarget) {
|
|
184
|
+
return `No migration path from ${markerPart} to the application's contract (${targetShort}). Run \`prisma-next migration plan --name <name>\` to author one.`;
|
|
185
|
+
}
|
|
186
|
+
const targetLabel =
|
|
187
|
+
args.refName !== undefined
|
|
188
|
+
? `the target (${targetShort} via \`${args.refName}\`)`
|
|
189
|
+
: `the target (${targetShort})`;
|
|
190
|
+
return `No migration path from ${markerPart} to ${targetLabel}. Run \`prisma-next migration plan --name <name>\` to author one, or pass \`--to <contract>\` to pick a reachable target.`;
|
|
191
|
+
}
|
|
290
192
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
) {
|
|
298
|
-
|
|
299
|
-
if (beyondTarget) {
|
|
300
|
-
for (const e of beyondTarget) {
|
|
301
|
-
if (!assignedKeys.has(edgeKey(e))) {
|
|
302
|
-
assignedKeys.add(edgeKey(e));
|
|
303
|
-
statuses.push({ dirName: e.dirName, status: 'pending' });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
193
|
+
export function buildStatusHeadline(args: {
|
|
194
|
+
readonly pendingCount: number;
|
|
195
|
+
readonly targetHash: string;
|
|
196
|
+
readonly markerDiverged: boolean;
|
|
197
|
+
readonly markerHash: string | undefined;
|
|
198
|
+
}): string {
|
|
199
|
+
if (args.markerDiverged && args.markerHash !== undefined) {
|
|
200
|
+
return `Database marker ${shortDisplayHash(args.markerHash)} is not in the on-disk migration graph`;
|
|
307
201
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// nor pending. This covers two cases:
|
|
311
|
-
// 1. Marker can't reach target at all (different branch entirely)
|
|
312
|
-
// 2. Marker reaches target via a different route, leaving some root→target
|
|
313
|
-
// edges orphaned (e.g. a fork where one branch was applied and apply
|
|
314
|
-
// will continue through the other)
|
|
315
|
-
if (targetPath) {
|
|
316
|
-
for (const e of targetPath) {
|
|
317
|
-
if (!assignedKeys.has(edgeKey(e))) {
|
|
318
|
-
statuses.push({ dirName: e.dirName, status: 'unreachable' });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
202
|
+
if (args.pendingCount === 0) {
|
|
203
|
+
return 'Up to date';
|
|
321
204
|
}
|
|
322
|
-
|
|
323
|
-
return statuses;
|
|
205
|
+
return `${args.pendingCount} pending — run \`prisma-next migrate --to ${shortDisplayHash(args.targetHash)}\``;
|
|
324
206
|
}
|
|
325
207
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const entries: MigrationStatusEntry[] = [];
|
|
345
|
-
let reachedMarker = mode === 'online' && markerHash === undefined;
|
|
346
|
-
|
|
347
|
-
for (const migration of chain) {
|
|
348
|
-
const pkg = pkgByDirName.get(migration.dirName);
|
|
349
|
-
const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
|
|
350
|
-
const { summary, hasDestructive } = summarizeOps(ops);
|
|
351
|
-
|
|
352
|
-
let status: EdgeStatusKind | 'unknown';
|
|
353
|
-
const edgeStatus = statusByDirName?.get(migration.dirName);
|
|
354
|
-
if (edgeStatus) {
|
|
355
|
-
status = edgeStatus;
|
|
356
|
-
} else if (mode === 'offline' || !markerInChain) {
|
|
357
|
-
status = 'unknown';
|
|
358
|
-
} else if (reachedMarker) {
|
|
359
|
-
status = 'pending';
|
|
360
|
-
} else {
|
|
361
|
-
status = 'applied';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
entries.push({
|
|
365
|
-
dirName: migration.dirName,
|
|
366
|
-
from: migration.from,
|
|
367
|
-
to: migration.to,
|
|
368
|
-
migrationHash: migration.migrationHash,
|
|
369
|
-
operationCount: ops.length,
|
|
370
|
-
operationSummary: summary,
|
|
371
|
-
hasDestructive,
|
|
372
|
-
status,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
if (!reachedMarker && migration.to === markerHash) {
|
|
376
|
-
reachedMarker = true;
|
|
377
|
-
}
|
|
208
|
+
export function formatStatusSummary(
|
|
209
|
+
result: MigrationStatusCommandResult,
|
|
210
|
+
colorize: boolean,
|
|
211
|
+
): string {
|
|
212
|
+
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
const pendingTotal = result.spaces.reduce(
|
|
215
|
+
(sum, space) => sum + countPending(space.migrations),
|
|
216
|
+
0,
|
|
217
|
+
);
|
|
218
|
+
const hasDivergence = result.diagnostics.some(
|
|
219
|
+
(d) => d.code === 'MIGRATION.MARKER_NOT_IN_HISTORY',
|
|
220
|
+
);
|
|
221
|
+
if (hasDivergence || pendingTotal > 0) {
|
|
222
|
+
lines.push(c(yellow, result.summary));
|
|
223
|
+
} else {
|
|
224
|
+
lines.push(result.summary);
|
|
378
225
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
* Resolve the migration chain to display in status output.
|
|
385
|
-
*
|
|
386
|
-
* When offline or the marker is at EMPTY, the chain is simply the shortest
|
|
387
|
-
* path from EMPTY to the target — all structural paths are equivalent per
|
|
388
|
-
* the spec, so the deterministic shortest path is the canonical display.
|
|
389
|
-
*
|
|
390
|
-
* When online with a non-empty marker, the chain routes *through* the marker:
|
|
391
|
-
* EMPTY→marker (applied history) + marker→target (pending edges). This ensures
|
|
392
|
-
* the displayed chain includes the marker node so applied/pending status is
|
|
393
|
-
* correct. Without this, BFS from EMPTY to target could pick a shortest path
|
|
394
|
-
* that bypasses the marker entirely (e.g. in a diamond graph), causing the
|
|
395
|
-
* marker to appear "diverged" when it isn't.
|
|
396
|
-
*/
|
|
397
|
-
function resolveDisplayChain(
|
|
398
|
-
graph: MigrationGraph,
|
|
399
|
-
targetHash: string,
|
|
400
|
-
markerHash: string | undefined,
|
|
401
|
-
): readonly MigrationEdge[] | null {
|
|
402
|
-
if (markerHash === undefined) {
|
|
403
|
-
return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
226
|
+
const missingInvariantsDiagnostic = result.diagnostics.find(
|
|
227
|
+
(d) => d.code === 'MIGRATION.MISSING_INVARIANTS',
|
|
228
|
+
);
|
|
229
|
+
if (missingInvariantsDiagnostic !== undefined) {
|
|
230
|
+
lines.push(c(dim, missingInvariantsDiagnostic.message));
|
|
404
231
|
}
|
|
405
|
-
|
|
406
|
-
const toMarker = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);
|
|
407
|
-
// Marker unreachable from EMPTY — show the target chain anyway.
|
|
408
|
-
// The caller detects this via markerInChain and emits a divergence diagnostic.
|
|
409
|
-
if (!toMarker) return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
410
|
-
|
|
411
|
-
if (markerHash === targetHash) return toMarker;
|
|
412
|
-
|
|
413
|
-
const fromMarker = findPath(graph, markerHash, targetHash);
|
|
414
|
-
if (fromMarker) return [...toMarker, ...fromMarker];
|
|
415
|
-
|
|
416
|
-
// Marker is ahead of target (or on a disconnected branch).
|
|
417
|
-
// Try the inverse: target→marker. If it succeeds, the marker is ahead —
|
|
418
|
-
// show the full chain from EMPTY through the target and on to the marker.
|
|
419
|
-
const toTarget = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
|
|
420
|
-
if (!toTarget) return null;
|
|
421
|
-
|
|
422
|
-
const targetToMarker = findPath(graph, targetHash, markerHash);
|
|
423
|
-
if (targetToMarker) return [...toTarget, ...targetToMarker];
|
|
424
|
-
|
|
425
|
-
// Genuinely disconnected — show EMPTY→target; caller handles divergence diagnostic.
|
|
426
|
-
return toTarget;
|
|
232
|
+
return lines.join('\n');
|
|
427
233
|
}
|
|
428
234
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
* state alongside the cross-space totals.
|
|
438
|
-
*/
|
|
439
|
-
export async function loadAggregateStatusSpaces(args: {
|
|
440
|
-
readonly aggregate: ContractSpaceAggregate;
|
|
441
|
-
readonly extensionPacks: ReadonlyArray<unknown>;
|
|
442
|
-
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
|
|
443
|
-
}): Promise<readonly MigrationStatusSpaceEntry[]> {
|
|
444
|
-
const declaredExtensions = toDeclaredExtensionsFromRaw(args.extensionPacks);
|
|
445
|
-
if (
|
|
446
|
-
refuseContractSpaceIntegrity(args.aggregate, {
|
|
447
|
-
declaredExtensions,
|
|
448
|
-
checkContracts: true,
|
|
449
|
-
})
|
|
450
|
-
) {
|
|
451
|
-
// Full integrity refusal (drift, layout violation, etc.) — surfacing
|
|
452
|
-
// it as a status diagnostic would duplicate `migration plan`'s job.
|
|
453
|
-
// The app pipeline still runs; extensions are simply not enumerated.
|
|
454
|
-
return [];
|
|
455
|
-
}
|
|
456
|
-
const aggregate = args.aggregate;
|
|
457
|
-
|
|
458
|
-
const orderedMembers = [...aggregate.extensions, aggregate.app];
|
|
459
|
-
const rows: MigrationStatusSpaceEntry[] = [];
|
|
460
|
-
for (const member of orderedMembers) {
|
|
461
|
-
const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
|
|
462
|
-
const isApp = member.spaceId === aggregate.app.spaceId;
|
|
463
|
-
// The aggregate passed the integrity gate above, so every member has
|
|
464
|
-
// a resolved head ref (a missing one would have refused the load).
|
|
465
|
-
const headRef = requireHeadRef(member);
|
|
466
|
-
|
|
467
|
-
if (member.graph().nodes.size === 0) {
|
|
468
|
-
rows.push({
|
|
469
|
-
spaceId: member.spaceId,
|
|
470
|
-
kind: isApp ? 'app' : 'extension',
|
|
471
|
-
headHash: headRef.hash,
|
|
472
|
-
...(args.markersBySpace !== null
|
|
473
|
-
? {
|
|
474
|
-
markerHash: liveMarker?.storageHash ?? null,
|
|
475
|
-
status: headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
|
|
476
|
-
pendingCount: 0,
|
|
477
|
-
}
|
|
478
|
-
: {}),
|
|
479
|
-
});
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (args.markersBySpace === null) {
|
|
484
|
-
rows.push({
|
|
485
|
-
spaceId: member.spaceId,
|
|
486
|
-
kind: isApp ? 'app' : 'extension',
|
|
487
|
-
headHash: headRef.hash,
|
|
488
|
-
});
|
|
489
|
-
continue;
|
|
235
|
+
export function formatStatusHumanOutput(
|
|
236
|
+
result: MigrationStatusCommandResult,
|
|
237
|
+
colorize: boolean,
|
|
238
|
+
): string {
|
|
239
|
+
const sections: string[] = [];
|
|
240
|
+
for (const section of result.treeSections) {
|
|
241
|
+
if (section.showHeading) {
|
|
242
|
+
sections.push(`${section.space}:`);
|
|
490
243
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
aggregateTargetId: aggregate.targetId,
|
|
494
|
-
member,
|
|
495
|
-
currentMarker: liveMarker,
|
|
496
|
-
});
|
|
497
|
-
let pendingCount = 0;
|
|
498
|
-
let status: MigrationStatusSpaceEntry['status'];
|
|
499
|
-
if (walked.kind === 'ok') {
|
|
500
|
-
// Count pending *migrations* (graph edges), not operations: a
|
|
501
|
-
// single authored migration that lowers to N ops or zero ops
|
|
502
|
-
// both count as exactly one pending unit of work for the user.
|
|
503
|
-
pendingCount = walked.result.migrationEdges?.length ?? 0;
|
|
504
|
-
if (liveMarker === null) {
|
|
505
|
-
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
506
|
-
} else {
|
|
507
|
-
status = pendingCount === 0 ? 'up-to-date' : 'pending';
|
|
508
|
-
}
|
|
244
|
+
if (section.tree.length > 0) {
|
|
245
|
+
sections.push(section.tree);
|
|
509
246
|
} else {
|
|
510
|
-
|
|
247
|
+
sections.push('(no migrations)');
|
|
511
248
|
}
|
|
512
|
-
|
|
513
|
-
rows.push({
|
|
514
|
-
spaceId: member.spaceId,
|
|
515
|
-
kind: isApp ? 'app' : 'extension',
|
|
516
|
-
headHash: headRef.hash,
|
|
517
|
-
markerHash: liveMarker?.storageHash ?? null,
|
|
518
|
-
pendingCount,
|
|
519
|
-
...(status ? { status } : {}),
|
|
520
|
-
});
|
|
249
|
+
sections.push('');
|
|
521
250
|
}
|
|
522
|
-
|
|
251
|
+
sections.push(formatStatusSummary(result, colorize));
|
|
252
|
+
return sections.join('\n').trimEnd();
|
|
523
253
|
}
|
|
524
254
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
family: config.family,
|
|
543
|
-
target: config.target,
|
|
544
|
-
adapter: config.adapter,
|
|
545
|
-
driver,
|
|
546
|
-
extensionPacks: config.extensionPacks ?? [],
|
|
547
|
-
});
|
|
548
|
-
try {
|
|
549
|
-
await client.connect(dbConnection);
|
|
550
|
-
await client.readMarker();
|
|
551
|
-
return ok(undefined);
|
|
552
|
-
} catch (error) {
|
|
553
|
-
if (CliStructuredError.is(error)) {
|
|
554
|
-
return notOk(error);
|
|
555
|
-
}
|
|
556
|
-
return notOk(
|
|
557
|
-
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
558
|
-
why: `Failed to read database marker: ${error instanceof Error ? error.message : String(error)}`,
|
|
559
|
-
}),
|
|
560
|
-
);
|
|
561
|
-
} finally {
|
|
562
|
-
await client.close();
|
|
563
|
-
}
|
|
255
|
+
async function readMarkersAndLedgers(args: {
|
|
256
|
+
readonly client: ReturnType<typeof createControlClient>;
|
|
257
|
+
readonly spaceIds: readonly string[];
|
|
258
|
+
}): Promise<{
|
|
259
|
+
readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike>;
|
|
260
|
+
readonly ledgersBySpace: ReadonlyMap<string, readonly LedgerEntryRecord[]>;
|
|
261
|
+
}> {
|
|
262
|
+
const markersBySpace = new Map<string, ContractMarkerRecordLike>();
|
|
263
|
+
const all = await args.client.readAllMarkers();
|
|
264
|
+
for (const [spaceId, marker] of all) {
|
|
265
|
+
markersBySpace.set(spaceId, marker);
|
|
266
|
+
}
|
|
267
|
+
const ledgersBySpace = new Map<string, readonly LedgerEntryRecord[]>();
|
|
268
|
+
for (const spaceId of args.spaceIds) {
|
|
269
|
+
ledgersBySpace.set(spaceId, await args.client.readLedger(spaceId));
|
|
270
|
+
}
|
|
271
|
+
return { markersBySpace, ledgersBySpace };
|
|
564
272
|
}
|
|
565
273
|
|
|
566
|
-
async function executeMigrationStatusCommand(
|
|
274
|
+
export async function executeMigrationStatusCommand(
|
|
567
275
|
options: MigrationStatusOptions,
|
|
568
276
|
flags: GlobalFlags,
|
|
569
277
|
ui: TerminalUI,
|
|
570
|
-
): Promise<Result<
|
|
278
|
+
): Promise<Result<MigrationStatusCommandResult, CliStructuredError>> {
|
|
571
279
|
const config = await loadConfig(options.config);
|
|
572
|
-
const { configPath,
|
|
280
|
+
const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
|
|
573
281
|
options.config,
|
|
574
282
|
config,
|
|
575
283
|
);
|
|
576
284
|
|
|
577
285
|
const dbConnection = options.db ?? config.db?.connection;
|
|
578
286
|
const hasDriver = !!config.driver;
|
|
287
|
+
const usingFromOverride = options.from !== undefined;
|
|
288
|
+
|
|
289
|
+
if (!usingFromOverride) {
|
|
290
|
+
const missingDb = requireLiveDatabase({
|
|
291
|
+
dbConnection,
|
|
292
|
+
hasDriver,
|
|
293
|
+
why: 'migration status needs a database connection to read the marker and ledger (or pass --from for offline path preview)',
|
|
294
|
+
retryCommand: 'prisma-next migration status --from <contract>',
|
|
295
|
+
});
|
|
296
|
+
if (missingDb) {
|
|
297
|
+
return notOk(missingDb);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
579
300
|
|
|
580
|
-
let activeRefName: string | undefined;
|
|
581
|
-
let activeRefHash: string | undefined;
|
|
582
|
-
let activeRefEntry: RefEntry | undefined;
|
|
583
301
|
let allRefs: Refs = {};
|
|
584
302
|
try {
|
|
585
303
|
allRefs = await readRefs(refsDir);
|
|
@@ -590,7 +308,7 @@ async function executeMigrationStatusCommand(
|
|
|
590
308
|
throw error;
|
|
591
309
|
}
|
|
592
310
|
|
|
593
|
-
const diagnostics:
|
|
311
|
+
const diagnostics: StatusDiagnosticJson[] = [];
|
|
594
312
|
let contractHash: string = EMPTY_CONTRACT_HASH;
|
|
595
313
|
try {
|
|
596
314
|
const envelope = await readContractEnvelope(config);
|
|
@@ -604,93 +322,52 @@ async function executeMigrationStatusCommand(
|
|
|
604
322
|
});
|
|
605
323
|
}
|
|
606
324
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const deserializeContract = (json: unknown): Contract => familyInstance.deserializeContract(json);
|
|
611
|
-
const appContractStandIn = appContractStandInFromIdentity({
|
|
612
|
-
contractHash,
|
|
613
|
-
targetId: config.target.id,
|
|
614
|
-
targetFamily: config.target.familyId,
|
|
615
|
-
});
|
|
616
|
-
let appContractForLoad: Contract = appContractStandIn;
|
|
617
|
-
if (contractRawForAggregate !== null) {
|
|
618
|
-
try {
|
|
619
|
-
appContractForLoad = deserializeContract(contractRawForAggregate);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
diagnostics.push({
|
|
622
|
-
code: 'CONTRACT.UNREADABLE',
|
|
623
|
-
severity: 'warn',
|
|
624
|
-
message: `Could not deserialize contract: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
625
|
-
hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
let aggregate: ContractSpaceAggregate;
|
|
631
|
-
try {
|
|
632
|
-
aggregate = await loadContractSpaceAggregate({
|
|
633
|
-
migrationsDir,
|
|
634
|
-
deserializeContract,
|
|
635
|
-
appContract: appContractForLoad,
|
|
636
|
-
});
|
|
637
|
-
} catch (error) {
|
|
638
|
-
if (MigrationToolsError.is(error)) {
|
|
639
|
-
return notOk(mapMigrationToolsError(error));
|
|
640
|
-
}
|
|
641
|
-
return notOk(
|
|
642
|
-
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
643
|
-
why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
644
|
-
}),
|
|
645
|
-
);
|
|
325
|
+
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
326
|
+
if (!loaded.ok) {
|
|
327
|
+
return notOk(loaded.failure);
|
|
646
328
|
}
|
|
647
329
|
|
|
330
|
+
const { aggregate } = loaded.value;
|
|
331
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
648
332
|
if (contractRawForAggregate !== null) {
|
|
649
333
|
const corruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
|
|
650
334
|
if (corruptionFailure) {
|
|
651
335
|
return notOk(corruptionFailure);
|
|
652
336
|
}
|
|
653
337
|
}
|
|
654
|
-
|
|
655
338
|
const appGraph = aggregate.app.graph();
|
|
656
339
|
|
|
340
|
+
let activeRefHash: string | undefined;
|
|
341
|
+
let activeRefName: string | undefined;
|
|
342
|
+
let activeRefEntry: RefEntry | undefined;
|
|
657
343
|
let fromOverrideHash: string | undefined;
|
|
658
344
|
|
|
659
|
-
if (options.to
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return notOk(mapRefResolutionError(refResult.failure));
|
|
664
|
-
}
|
|
665
|
-
activeRefHash = refResult.value.hash;
|
|
666
|
-
if (refResult.value.provenance.kind === 'ref') {
|
|
667
|
-
const resolvedRefName = refResult.value.provenance.refName;
|
|
668
|
-
activeRefName = resolvedRefName;
|
|
669
|
-
activeRefEntry = allRefs[resolvedRefName];
|
|
670
|
-
}
|
|
345
|
+
if (options.to) {
|
|
346
|
+
const refResult = parseContractRef(options.to, { graph: appGraph, refs: allRefs });
|
|
347
|
+
if (!refResult.ok) {
|
|
348
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
671
349
|
}
|
|
350
|
+
activeRefHash = refResult.value.hash;
|
|
351
|
+
if (refResult.value.provenance.kind === 'ref') {
|
|
352
|
+
activeRefName = refResult.value.provenance.refName;
|
|
353
|
+
activeRefEntry = allRefs[activeRefName];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
672
356
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
fromOverrideHash = fromResult.value.hash;
|
|
357
|
+
if (options.from) {
|
|
358
|
+
const fromResult = parseContractRef(options.from, { graph: appGraph, refs: allRefs });
|
|
359
|
+
if (!fromResult.ok) {
|
|
360
|
+
return notOk(mapRefResolutionError(fromResult.failure));
|
|
679
361
|
}
|
|
362
|
+
fromOverrideHash = fromResult.value.hash;
|
|
680
363
|
}
|
|
681
364
|
|
|
682
365
|
const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
|
|
683
366
|
|
|
684
|
-
const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
|
|
685
|
-
name,
|
|
686
|
-
hash: entry.hash,
|
|
687
|
-
active: name === activeRefName,
|
|
688
|
-
}));
|
|
689
|
-
|
|
690
367
|
if (!flags.json && !flags.quiet) {
|
|
691
368
|
const details: Array<{ label: string; value: string }> = [
|
|
692
369
|
{ label: 'config', value: configPath },
|
|
693
|
-
{ label: 'migrations', value:
|
|
370
|
+
{ label: 'migrations', value: migrationsRelative },
|
|
694
371
|
];
|
|
695
372
|
if (dbConnection && hasDriver) {
|
|
696
373
|
details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
|
|
@@ -701,11 +378,8 @@ async function executeMigrationStatusCommand(
|
|
|
701
378
|
if (options.from) {
|
|
702
379
|
details.push({ label: 'from', value: options.from });
|
|
703
380
|
}
|
|
704
|
-
if (
|
|
705
|
-
details.push({
|
|
706
|
-
label: 'required',
|
|
707
|
-
value: formatInvariantList(activeRefEntry.invariants),
|
|
708
|
-
});
|
|
381
|
+
if (options.space) {
|
|
382
|
+
details.push({ label: 'space', value: options.space });
|
|
709
383
|
}
|
|
710
384
|
const header = formatStyledHeader({
|
|
711
385
|
command: 'migration status',
|
|
@@ -714,69 +388,34 @@ async function executeMigrationStatusCommand(
|
|
|
714
388
|
flags,
|
|
715
389
|
});
|
|
716
390
|
ui.stderr(header);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (!markerProbe.ok) {
|
|
726
|
-
return markerProbe;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (contractHash !== EMPTY_CONTRACT_HASH) {
|
|
730
|
-
diagnostics.push({
|
|
731
|
-
code: 'CONTRACT.AHEAD',
|
|
732
|
-
severity: 'warn',
|
|
733
|
-
message: 'No migration exists for the current contract',
|
|
734
|
-
hints: [
|
|
735
|
-
"Run 'prisma-next migration plan' to generate a migration for the current contract",
|
|
736
|
-
],
|
|
737
|
-
});
|
|
391
|
+
if (shouldShowLegend(options, flags)) {
|
|
392
|
+
ui.stderr(
|
|
393
|
+
renderMigrationGraphLegend({
|
|
394
|
+
colorize: flags.color !== false,
|
|
395
|
+
glyphMode: ui.resolveGlyphMode(options.ascii === true),
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
ui.stderr('');
|
|
738
399
|
}
|
|
739
|
-
return ok({
|
|
740
|
-
ok: true,
|
|
741
|
-
mode: dbConnection && hasDriver ? 'online' : 'offline',
|
|
742
|
-
migrations: [],
|
|
743
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
744
|
-
contractHash,
|
|
745
|
-
summary: 'No migrations found',
|
|
746
|
-
diagnostics,
|
|
747
|
-
requiredInvariants,
|
|
748
|
-
});
|
|
749
400
|
}
|
|
750
401
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
|
|
759
|
-
if (leaves.length === 1) {
|
|
760
|
-
targetHash = leaves[0];
|
|
761
|
-
} else {
|
|
762
|
-
diagnostics.push({
|
|
763
|
-
code: 'MIGRATION.DIVERGED',
|
|
764
|
-
severity: 'warn',
|
|
765
|
-
message: 'There are multiple valid migration paths — you must select a target',
|
|
766
|
-
hints: [
|
|
767
|
-
"Use '--to <contract>' to select a target",
|
|
768
|
-
"Or 'prisma-next ref set <name> <hash>' to create one",
|
|
769
|
-
],
|
|
770
|
-
});
|
|
771
|
-
}
|
|
402
|
+
const listSpaces = await migrationSpaceListEntriesFromAggregate(aggregate, migrationsDir);
|
|
403
|
+
const listResult = runMigrationList({
|
|
404
|
+
spaces: listSpaces,
|
|
405
|
+
...ifDefined('spaceFilter', options.space),
|
|
406
|
+
});
|
|
407
|
+
if (!listResult.ok) {
|
|
408
|
+
return listResult;
|
|
772
409
|
}
|
|
773
410
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
let
|
|
411
|
+
const scopedSpaces = listResult.value.spaces;
|
|
412
|
+
const showSpaceHeadings = scopedSpaces.length > 1;
|
|
413
|
+
|
|
414
|
+
let markersBySpace = new Map<string, ContractMarkerRecordLike>();
|
|
415
|
+
let ledgersBySpace = new Map<string, readonly LedgerEntryRecord[]>();
|
|
416
|
+
let connected = false;
|
|
778
417
|
|
|
779
|
-
if (dbConnection && hasDriver) {
|
|
418
|
+
if (dbConnection && hasDriver && !usingFromOverride) {
|
|
780
419
|
const client = createControlClient({
|
|
781
420
|
family: config.family,
|
|
782
421
|
target: config.target,
|
|
@@ -786,74 +425,32 @@ async function executeMigrationStatusCommand(
|
|
|
786
425
|
});
|
|
787
426
|
try {
|
|
788
427
|
await client.connect(dbConnection);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// here it powers the aggregate status output.
|
|
797
|
-
//
|
|
798
|
-
// Probe for the method first so we only swallow the
|
|
799
|
-
// unsupported-method case: older family instances may not
|
|
800
|
-
// implement `readAllMarkers` (per-space enumeration then falls
|
|
801
|
-
// back to "marker unknown"). Real query / runtime errors from
|
|
802
|
-
// an instance that *does* expose the method must propagate up
|
|
803
|
-
// — otherwise transient DB failures would silently degrade
|
|
804
|
-
// status to "markers unknown".
|
|
805
|
-
if (typeof client.readAllMarkers === 'function') {
|
|
806
|
-
allMarkers = await client.readAllMarkers();
|
|
807
|
-
} else {
|
|
808
|
-
// Leaving `allMarkers` as `null` signals "unknown" to the
|
|
809
|
-
// aggregate loader (an empty `Map` would instead mean "every
|
|
810
|
-
// space has no marker", which is a different condition).
|
|
811
|
-
allMarkers = null;
|
|
812
|
-
}
|
|
428
|
+
connected = true;
|
|
429
|
+
const read = await readMarkersAndLedgers({
|
|
430
|
+
client,
|
|
431
|
+
spaceIds: scopedSpaces.map((s) => s.space),
|
|
432
|
+
});
|
|
433
|
+
markersBySpace = new Map(read.markersBySpace);
|
|
434
|
+
ledgersBySpace = new Map(read.ledgersBySpace);
|
|
813
435
|
} catch (error) {
|
|
814
436
|
if (CliStructuredError.is(error)) {
|
|
815
437
|
return notOk(error);
|
|
816
438
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
439
|
+
return notOk(
|
|
440
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
441
|
+
why: `Failed to read database state: ${error instanceof Error ? error.message : String(error)}`,
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
820
444
|
} finally {
|
|
821
445
|
await client.close();
|
|
822
446
|
}
|
|
823
447
|
}
|
|
824
448
|
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
allMarkers = null;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
|
|
832
|
-
if (contractRawForAggregate !== null) {
|
|
833
|
-
try {
|
|
834
|
-
aggregateSpaces = await loadAggregateStatusSpaces({
|
|
835
|
-
aggregate,
|
|
836
|
-
extensionPacks: config.extensionPacks ?? [],
|
|
837
|
-
markersBySpace: allMarkers,
|
|
838
|
-
});
|
|
839
|
-
} catch {
|
|
840
|
-
aggregateSpaces = [];
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
const totalPendingAcrossSpaces = computeTotalPendingAcrossSpaces(aggregateSpaces);
|
|
844
|
-
|
|
845
|
-
// Pre-check unknown invariants. Online: union the graph's declared
|
|
846
|
-
// invariants with the marker's recorded set so a retired-but-applied
|
|
847
|
-
// invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
|
|
848
|
-
// route fine because marker subtraction empties `effectiveRequired`.
|
|
849
|
-
// Offline: keep the check graph-strict (the marker is unknown, and a
|
|
850
|
-
// missing declarer is the dominant signal we can offer).
|
|
851
|
-
if (activeRefEntry && activeRefEntry.invariants.length > 0) {
|
|
852
|
-
const declared = collectDeclaredInvariants(graph);
|
|
449
|
+
if (activeRefEntry && activeRefEntry.invariants.length > 0 && connected) {
|
|
450
|
+
const declared = collectDeclaredInvariants(appGraph);
|
|
451
|
+
const markerInvariants = markersBySpace.get(aggregate.app.spaceId)?.invariants ?? [];
|
|
853
452
|
const known = new Set<string>(declared);
|
|
854
|
-
|
|
855
|
-
for (const id of markerInvariants) known.add(id);
|
|
856
|
-
}
|
|
453
|
+
for (const id of markerInvariants) known.add(id);
|
|
857
454
|
const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
|
|
858
455
|
if (unknown.length > 0) {
|
|
859
456
|
return notOk(
|
|
@@ -868,247 +465,185 @@ async function executeMigrationStatusCommand(
|
|
|
868
465
|
}
|
|
869
466
|
}
|
|
870
467
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
468
|
+
const showAppliedOverlay = connected && !usingFromOverride;
|
|
469
|
+
const showDbMarker = connected && !usingFromOverride;
|
|
470
|
+
const glyphMode = ui.resolveGlyphMode(options.ascii === true);
|
|
471
|
+
const colorize = flags.color !== false;
|
|
472
|
+
|
|
473
|
+
const statusSpaces: MigrationStatusSpace[] = [];
|
|
474
|
+
const treeSections: MigrationStatusTreeSection[] = [];
|
|
475
|
+
let markerDiverged = false;
|
|
476
|
+
let markerCannotReachTarget = false;
|
|
477
|
+
let headlineTargetHash = activeRefHash ?? contractHash;
|
|
478
|
+
let totalPending = 0;
|
|
479
|
+
|
|
480
|
+
const globalLayoutInputs = showSpaceHeadings
|
|
481
|
+
? scopedSpaces
|
|
482
|
+
.filter((spaceEntry) => spaceEntry.migrations.length > 0)
|
|
483
|
+
.map((spaceEntry) => ({
|
|
484
|
+
graph: aggregate.space(spaceEntry.space)!.graph(),
|
|
485
|
+
liveContractHash: contractHash,
|
|
486
|
+
}))
|
|
487
|
+
: [];
|
|
488
|
+
const globalMaxEdgeTreePrefixWidth =
|
|
489
|
+
globalLayoutInputs.length > 0
|
|
490
|
+
? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs)
|
|
491
|
+
: undefined;
|
|
492
|
+
const globalMaxDirNameWidth =
|
|
493
|
+
globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : undefined;
|
|
494
|
+
|
|
495
|
+
for (const spaceEntry of scopedSpaces) {
|
|
496
|
+
const member = aggregate.space(spaceEntry.space);
|
|
497
|
+
if (member === undefined) {
|
|
498
|
+
continue;
|
|
899
499
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
hints,
|
|
906
|
-
});
|
|
907
|
-
return ok({
|
|
908
|
-
ok: true,
|
|
909
|
-
mode,
|
|
910
|
-
migrations: [],
|
|
911
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
912
|
-
contractHash,
|
|
913
|
-
summary: `${bundles.length} migration(s) on disk`,
|
|
914
|
-
diagnostics,
|
|
915
|
-
markerHash,
|
|
916
|
-
requiredInvariants,
|
|
917
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (mode === 'online' && markerHash === undefined) {
|
|
922
|
-
diagnostics.push({
|
|
923
|
-
code: 'MIGRATION.NO_MARKER',
|
|
924
|
-
severity: 'warn',
|
|
925
|
-
message: 'Database has not been initialized — no migration marker found',
|
|
926
|
-
hints: ["Run 'prisma-next migrate' to apply pending migrations"],
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Contract diagnostic — fires when no migration produces the current contract hash.
|
|
931
|
-
// Suppressed when: (a) graph is diverged (MIGRATION.DIVERGED already guides the user),
|
|
932
|
-
// (b) marker === contract and both off-graph (marker-not-in-graph diagnostic covers it).
|
|
933
|
-
if (
|
|
934
|
-
targetHash &&
|
|
935
|
-
contractHash !== EMPTY_CONTRACT_HASH &&
|
|
936
|
-
!graph.nodes.has(contractHash) &&
|
|
937
|
-
markerHash !== contractHash
|
|
938
|
-
) {
|
|
939
|
-
diagnostics.push({
|
|
940
|
-
code: 'CONTRACT.AHEAD',
|
|
941
|
-
severity: 'warn',
|
|
942
|
-
message: 'Contract has changed since the last migration was planned',
|
|
943
|
-
hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"],
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (!targetHash) {
|
|
948
|
-
return ok({
|
|
949
|
-
ok: true,
|
|
950
|
-
mode,
|
|
951
|
-
migrations: [],
|
|
952
|
-
targetHash: EMPTY_CONTRACT_HASH,
|
|
953
|
-
contractHash,
|
|
954
|
-
summary: `${bundles.length} migration(s) on disk`,
|
|
955
|
-
diagnostics,
|
|
956
|
-
...ifDefined('markerHash', markerHash),
|
|
957
|
-
requiredInvariants,
|
|
958
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
959
|
-
graph,
|
|
960
|
-
bundles,
|
|
961
|
-
diverged: true,
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
const chain = resolveDisplayChain(graph, targetHash, markerHash);
|
|
966
|
-
|
|
967
|
-
if (!chain) {
|
|
968
|
-
return notOk(
|
|
969
|
-
errorRuntime('Cannot reconstruct migration history', {
|
|
970
|
-
why: `No path from ${EMPTY_CONTRACT_HASH} to target ${targetHash}`,
|
|
971
|
-
fix: 'The migration history may have gaps. Check the migrations directory for missing or corrupted packages.',
|
|
972
|
-
}),
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
|
|
977
|
-
const entries = buildMigrationEntries(chain, bundles, mode, markerHash, edgeStatuses);
|
|
978
|
-
|
|
979
|
-
const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
|
|
980
|
-
const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
|
|
981
|
-
|
|
982
|
-
let appliedInvariants: readonly string[] | undefined;
|
|
983
|
-
let missingInvariants: readonly string[] | undefined;
|
|
984
|
-
let effectiveRequired = new Set<string>();
|
|
985
|
-
if (mode === 'online') {
|
|
986
|
-
// Mirrors `migrate.ts`: compute `effectiveRequired = required −
|
|
987
|
-
// marker.invariants` directly, then derive the display fields from it.
|
|
988
|
-
// `appliedInvariants` is the intersection (`required ∩ marker`), which
|
|
989
|
-
// is what JSON consumers see for the active ref; the unfiltered set
|
|
990
|
-
// lives on `marker.invariants`.
|
|
991
|
-
const markerSet = new Set(markerInvariants);
|
|
992
|
-
effectiveRequired = new Set(requiredInvariants.filter((id) => !markerSet.has(id)));
|
|
993
|
-
appliedInvariants = requiredInvariants.filter((id) => markerSet.has(id));
|
|
994
|
-
missingInvariants = [...effectiveRequired].sort();
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// The marker can match the structural target while still missing required
|
|
998
|
-
// invariants — for example, a self-edge that provides X, applied via a ref
|
|
999
|
-
// declaring X. `pendingCount` (structural) says zero in that case but
|
|
1000
|
-
// `effectiveRequired` is non-empty, so up-to-date messaging would mislead.
|
|
1001
|
-
const hasInvariantWork = effectiveRequired.size > 0;
|
|
1002
|
-
const missingList = [...effectiveRequired].sort().join(', ');
|
|
1003
|
-
|
|
1004
|
-
let summary: string;
|
|
1005
|
-
if (mode === 'online') {
|
|
1006
|
-
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
1007
|
-
summary = `${bundles.length} migration(s) on disk`;
|
|
1008
|
-
} else if (activeRefHash && activeRefName && markerHash !== undefined) {
|
|
1009
|
-
const distance = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName);
|
|
1010
|
-
summary = hasInvariantWork ? `${distance} — missing invariant(s): ${missingList}` : distance;
|
|
1011
|
-
} else if (pendingCount === 0 && !hasInvariantWork) {
|
|
1012
|
-
summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
|
|
1013
|
-
} else if (pendingCount === 0 && hasInvariantWork) {
|
|
1014
|
-
summary = `Missing invariant(s): ${missingList} — run 'prisma-next migrate --to ${activeRefName ?? '<ref>'}' to apply`;
|
|
1015
|
-
} else if (markerHash === undefined) {
|
|
1016
|
-
summary = `${pendingCount} pending migration(s) — database has no marker`;
|
|
1017
|
-
} else {
|
|
1018
|
-
summary = `${pendingCount} pending migration(s) — run 'prisma-next migrate' to apply`;
|
|
500
|
+
const graph = member.graph();
|
|
501
|
+
const spaceContractHash = member.contract().storage.storageHash;
|
|
502
|
+
const targetHash = resolveTarget(spaceContractHash, activeRefHash);
|
|
503
|
+
if (spaceEntry.space === aggregate.app.spaceId) {
|
|
504
|
+
headlineTargetHash = targetHash;
|
|
1019
505
|
}
|
|
1020
|
-
} else {
|
|
1021
|
-
summary = `${entries.length} migration(s) on disk`;
|
|
1022
|
-
}
|
|
1023
506
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
507
|
+
const markerRecord = markersBySpace.get(spaceEntry.space);
|
|
508
|
+
const markerHash = usingFromOverride
|
|
509
|
+
? fromOverrideHash
|
|
510
|
+
: (markerRecord?.storageHash ?? undefined);
|
|
1027
511
|
const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
}),
|
|
1043
|
-
),
|
|
1044
|
-
);
|
|
1045
|
-
} else {
|
|
1046
|
-
// outcome.kind === 'unreachable' — origin (marker) has no structural
|
|
1047
|
-
// path to the active target. `pendingCount` and `hasInvariantWork`
|
|
1048
|
-
// both report zero in this case, but emitting MIGRATION.UP_TO_DATE
|
|
1049
|
-
// would be wrong: the database simply cannot reach the requested
|
|
1050
|
-
// ref/contract from its current state. Suppress UP_TO_DATE below.
|
|
1051
|
-
routingUnreachable = true;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
if (mode === 'online') {
|
|
1056
|
-
if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
|
|
512
|
+
const markerInGraph =
|
|
513
|
+
markerHash === undefined || graph.nodes.has(markerHash) || markerHash === spaceContractHash;
|
|
514
|
+
if (
|
|
515
|
+
connected &&
|
|
516
|
+
!usingFromOverride &&
|
|
517
|
+
markerInGraph &&
|
|
518
|
+
originHash !== targetHash &&
|
|
519
|
+
findPath(graph, originHash, targetHash) === null
|
|
520
|
+
) {
|
|
521
|
+
markerCannotReachTarget = true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (connected && !usingFromOverride && markerHash !== undefined && !markerInGraph) {
|
|
525
|
+
markerDiverged = true;
|
|
1057
526
|
diagnostics.push({
|
|
1058
527
|
code: 'MIGRATION.MARKER_NOT_IN_HISTORY',
|
|
1059
528
|
severity: 'warn',
|
|
1060
|
-
message:
|
|
1061
|
-
|
|
1062
|
-
});
|
|
1063
|
-
} else if (pendingCount > 0) {
|
|
1064
|
-
diagnostics.push({
|
|
1065
|
-
code: 'MIGRATION.DATABASE_BEHIND',
|
|
1066
|
-
severity: 'info',
|
|
1067
|
-
message: `${pendingCount} migration(s) pending`,
|
|
1068
|
-
hints: ["Run 'prisma-next migrate' to apply pending migrations"],
|
|
1069
|
-
});
|
|
1070
|
-
} else if (hasInvariantWork) {
|
|
1071
|
-
diagnostics.push({
|
|
1072
|
-
code: 'MIGRATION.INVARIANTS_PENDING',
|
|
1073
|
-
severity: 'info',
|
|
1074
|
-
message: `Missing required invariant(s): ${missingList}`,
|
|
529
|
+
message:
|
|
530
|
+
'Database was updated outside the migration system (marker does not match any migration)',
|
|
1075
531
|
hints: [
|
|
1076
|
-
|
|
532
|
+
"Run 'prisma-next db sign' to overwrite the marker if the database already matches the contract",
|
|
533
|
+
"Run 'prisma-next db update' to push the current contract to the database",
|
|
1077
534
|
],
|
|
1078
535
|
});
|
|
1079
|
-
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const ledger = ledgersBySpace.get(spaceEntry.space) ?? [];
|
|
539
|
+
const appliedHashes = showAppliedOverlay ? appliedHashesFromLedger(ledger) : new Set<string>();
|
|
540
|
+
|
|
541
|
+
const annotations = deriveStatusEdgeAnnotations({
|
|
542
|
+
graph,
|
|
543
|
+
targetHash,
|
|
544
|
+
originHash,
|
|
545
|
+
appliedMigrationHashes: appliedHashes,
|
|
546
|
+
showAppliedOverlay,
|
|
547
|
+
});
|
|
548
|
+
const isAppSpace = spaceEntry.space === aggregate.app.spaceId;
|
|
549
|
+
const tree = renderSpaceTree({
|
|
550
|
+
member,
|
|
551
|
+
liveContractHash: contractHash,
|
|
552
|
+
migrations: spaceEntry.migrations,
|
|
553
|
+
markerHash,
|
|
554
|
+
showDbMarker,
|
|
555
|
+
statusOverlay: annotations,
|
|
556
|
+
colorize,
|
|
557
|
+
glyphMode,
|
|
558
|
+
isAppSpace,
|
|
559
|
+
...(globalMaxEdgeTreePrefixWidth !== undefined ? { globalMaxEdgeTreePrefixWidth } : {}),
|
|
560
|
+
...(globalMaxDirNameWidth !== undefined ? { globalMaxDirNameWidth } : {}),
|
|
561
|
+
});
|
|
562
|
+
const migrations = buildStatusMigrations(spaceEntry.migrations, annotations);
|
|
563
|
+
const pending = countPending(migrations);
|
|
564
|
+
totalPending += pending;
|
|
565
|
+
|
|
566
|
+
statusSpaces.push({
|
|
567
|
+
space: spaceEntry.space,
|
|
568
|
+
currentContract: markerHash ?? null,
|
|
569
|
+
targetContract: targetHash,
|
|
570
|
+
migrations: [...migrations],
|
|
571
|
+
});
|
|
572
|
+
const displayTree =
|
|
573
|
+
showSpaceHeadings && tree.length > 0 ? indentMigrationGraphTreeBlock(tree, ' ') : tree;
|
|
574
|
+
treeSections.push({
|
|
575
|
+
space: spaceEntry.space,
|
|
576
|
+
tree: displayTree,
|
|
577
|
+
showHeading: showSpaceHeadings,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (connected && requiredInvariants.length > 0) {
|
|
582
|
+
const markerInvariants = markersBySpace.get(aggregate.app.spaceId)?.invariants ?? [];
|
|
583
|
+
const markerSet = new Set(markerInvariants);
|
|
584
|
+
const missing = requiredInvariants.filter((id) => !markerSet.has(id));
|
|
585
|
+
if (missing.length > 0) {
|
|
1080
586
|
diagnostics.push({
|
|
1081
|
-
code: 'MIGRATION.
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
587
|
+
code: 'MIGRATION.MISSING_INVARIANTS',
|
|
588
|
+
...ifDefined('ref', activeRefName),
|
|
589
|
+
invariants: missing,
|
|
590
|
+
message: `missing invariant(s): ${missing.join(', ')}`,
|
|
1085
591
|
});
|
|
592
|
+
if (activeRefHash !== undefined) {
|
|
593
|
+
const originHash =
|
|
594
|
+
markersBySpace.get(aggregate.app.spaceId)?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
595
|
+
const outcome = findPathWithDecision(appGraph, originHash, activeRefHash, {
|
|
596
|
+
...ifDefined('refName', activeRefName),
|
|
597
|
+
required: new Set(missing),
|
|
598
|
+
});
|
|
599
|
+
if (outcome.kind === 'unsatisfiable') {
|
|
600
|
+
return notOk(
|
|
601
|
+
mapMigrationToolsError(
|
|
602
|
+
errorNoInvariantPath({
|
|
603
|
+
...ifDefined('refName', activeRefName),
|
|
604
|
+
required: [...missing].sort(),
|
|
605
|
+
missing: outcome.missing,
|
|
606
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge),
|
|
607
|
+
}),
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
1086
612
|
}
|
|
1087
613
|
}
|
|
1088
614
|
|
|
1089
|
-
const
|
|
615
|
+
const appMarkerHash = markersBySpace.get(aggregate.app.spaceId)?.storageHash;
|
|
616
|
+
const summary = markerCannotReachTarget
|
|
617
|
+
? buildNoPathSummary({
|
|
618
|
+
markerHash: appMarkerHash,
|
|
619
|
+
targetHash: headlineTargetHash,
|
|
620
|
+
explicitTarget: options.to !== undefined,
|
|
621
|
+
refName: activeRefName,
|
|
622
|
+
})
|
|
623
|
+
: buildStatusHeadline({
|
|
624
|
+
pendingCount: totalPending,
|
|
625
|
+
targetHash: headlineTargetHash,
|
|
626
|
+
markerDiverged,
|
|
627
|
+
markerHash: appMarkerHash,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (scopedSpaces.every((s) => s.migrations.length === 0)) {
|
|
631
|
+
return ok({
|
|
632
|
+
ok: true,
|
|
633
|
+
spaces: statusSpaces,
|
|
634
|
+
summary: 'No migrations found',
|
|
635
|
+
diagnostics,
|
|
636
|
+
treeSections,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return ok({
|
|
1090
641
|
ok: true,
|
|
1091
|
-
|
|
1092
|
-
migrations: entries,
|
|
1093
|
-
targetHash,
|
|
1094
|
-
contractHash,
|
|
642
|
+
spaces: statusSpaces,
|
|
1095
643
|
summary,
|
|
1096
644
|
diagnostics,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
...ifDefined('appliedInvariants', appliedInvariants),
|
|
1100
|
-
...ifDefined('missingInvariants', missingInvariants),
|
|
1101
|
-
...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
|
|
1102
|
-
...ifDefined('pathDecision', pathDecision),
|
|
1103
|
-
graph,
|
|
1104
|
-
bundles,
|
|
1105
|
-
edgeStatuses,
|
|
1106
|
-
...ifDefined('activeRefHash', activeRefHash),
|
|
1107
|
-
...ifDefined('activeRefName', activeRefName),
|
|
1108
|
-
spaces: aggregateSpaces,
|
|
1109
|
-
...ifDefined('totalPendingAcrossSpaces', totalPendingAcrossSpaces),
|
|
1110
|
-
};
|
|
1111
|
-
return ok(result);
|
|
645
|
+
treeSections,
|
|
646
|
+
});
|
|
1112
647
|
}
|
|
1113
648
|
|
|
1114
649
|
export function createMigrationStatusCommand(): Command {
|
|
@@ -1117,13 +652,18 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1117
652
|
command,
|
|
1118
653
|
'Show migration path and pending status',
|
|
1119
654
|
'Shows which migrations are pending between the database marker and\n' +
|
|
1120
|
-
'the target contract. Requires a database connection
|
|
655
|
+
'the target contract. Requires a database connection.\n' +
|
|
656
|
+
'Pass --from for an offline path preview without a database.\n' +
|
|
1121
657
|
'Use `migration graph` for topology, `migration log` for history,\n' +
|
|
1122
658
|
'and `migration list` for on-disk enumeration.',
|
|
1123
659
|
);
|
|
1124
660
|
setCommandExamples(command, [
|
|
1125
661
|
'prisma-next migration status --db $DATABASE_URL',
|
|
1126
662
|
'prisma-next migration status --to production --db $DATABASE_URL',
|
|
663
|
+
'prisma-next migration status --from sha256:abc --to production',
|
|
664
|
+
'prisma-next migration status --from sha256:abc --to production --json',
|
|
665
|
+
'prisma-next migration status --ascii --from sha256:abc --to production',
|
|
666
|
+
'prisma-next migration status --legend --from sha256:abc --to production',
|
|
1127
667
|
]);
|
|
1128
668
|
setCommandSeeAlso(command, [
|
|
1129
669
|
{ verb: 'migration log', oneLiner: 'Show executed migration history' },
|
|
@@ -1134,6 +674,7 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1134
674
|
addGlobalOptions(command)
|
|
1135
675
|
.option('--db <url>', 'Database connection string')
|
|
1136
676
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
677
|
+
.option('--space <id>', 'Narrow output to a single contract space')
|
|
1137
678
|
.option(
|
|
1138
679
|
'--to <contract>',
|
|
1139
680
|
'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
@@ -1142,56 +683,30 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1142
683
|
'--from <contract>',
|
|
1143
684
|
'Origin contract reference; same grammar as --to. Supplying --from switches to offline path computation.',
|
|
1144
685
|
)
|
|
686
|
+
.option('--legend', 'Print a key for the tree glyphs and lane colors')
|
|
687
|
+
.option('--ascii', 'Use ASCII glyphs (pipe-friendly)')
|
|
1145
688
|
.action(async (options: MigrationStatusOptions) => {
|
|
1146
689
|
const flags = parseGlobalFlagsOrExit(options);
|
|
1147
690
|
const ui = createTerminalUI(flags);
|
|
1148
691
|
|
|
692
|
+
const legendValidation = validateLegendOptions(options, flags);
|
|
693
|
+
if (!legendValidation.ok) {
|
|
694
|
+
process.exit(handleResult(legendValidation, flags, ui));
|
|
695
|
+
}
|
|
696
|
+
|
|
1149
697
|
const result = await executeMigrationStatusCommand(options, flags, ui);
|
|
1150
698
|
|
|
1151
699
|
const exitCode = handleResult(result, flags, ui, (statusResult) => {
|
|
1152
700
|
if (flags.json) {
|
|
1153
|
-
const {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
diverged: _diverged,
|
|
1160
|
-
...jsonResult
|
|
1161
|
-
} = statusResult;
|
|
701
|
+
const jsonResult: MigrationStatusResult = {
|
|
702
|
+
ok: true,
|
|
703
|
+
spaces: [...statusResult.spaces],
|
|
704
|
+
summary: statusResult.summary,
|
|
705
|
+
diagnostics: [...statusResult.diagnostics],
|
|
706
|
+
};
|
|
1162
707
|
ui.output(JSON.stringify(jsonResult, null, 2));
|
|
1163
708
|
} else if (!flags.quiet) {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (statusResult.graph) {
|
|
1167
|
-
const renderInput = migrationGraphToRenderInput({
|
|
1168
|
-
graph: statusResult.graph,
|
|
1169
|
-
mode: statusResult.mode,
|
|
1170
|
-
markerHash: statusResult.markerHash,
|
|
1171
|
-
contractHash: statusResult.contractHash,
|
|
1172
|
-
refs: statusResult.refs,
|
|
1173
|
-
activeRefHash: statusResult.activeRefHash,
|
|
1174
|
-
activeRefName: statusResult.activeRefName,
|
|
1175
|
-
edgeStatuses: statusResult.edgeStatuses,
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
const graphToRender = statusResult.diverged
|
|
1179
|
-
? renderInput.graph
|
|
1180
|
-
: extractRelevantSubgraph(renderInput.graph, renderInput.relevantPaths);
|
|
1181
|
-
const dagreOptions = isLinearGraph(graphToRender) ? { ranksep: 1 } : undefined;
|
|
1182
|
-
const renderOptions = {
|
|
1183
|
-
...renderInput.options,
|
|
1184
|
-
colorize,
|
|
1185
|
-
...ifDefined('dagreOptions', dagreOptions),
|
|
1186
|
-
};
|
|
1187
|
-
const graphOutput = graphRenderer.render(graphToRender, renderOptions);
|
|
1188
|
-
ui.log(graphOutput);
|
|
1189
|
-
if (statusResult.mode === 'online') {
|
|
1190
|
-
ui.log(formatLegend(colorize));
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
ui.log('');
|
|
1194
|
-
ui.log(formatStatusSummary(statusResult, colorize));
|
|
709
|
+
ui.output(formatStatusHumanOutput(statusResult, flags.color !== false));
|
|
1195
710
|
}
|
|
1196
711
|
});
|
|
1197
712
|
|
|
@@ -1200,126 +715,3 @@ export function createMigrationStatusCommand(): Command {
|
|
|
1200
715
|
|
|
1201
716
|
return command;
|
|
1202
717
|
}
|
|
1203
|
-
|
|
1204
|
-
function formatLegend(colorize: boolean): string {
|
|
1205
|
-
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
1206
|
-
const parts = [
|
|
1207
|
-
`${c(cyan, '✓')} applied`,
|
|
1208
|
-
`${c(yellow, '⧗')} pending`,
|
|
1209
|
-
`${c(magenta, '✗')} unreachable`,
|
|
1210
|
-
];
|
|
1211
|
-
return c(dim, parts.join(' '));
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
|
|
1215
|
-
const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
|
|
1216
|
-
const lines: string[] = [];
|
|
1217
|
-
|
|
1218
|
-
const hasUnknown = result.migrations.some((e) => e.status === 'unknown');
|
|
1219
|
-
const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
|
|
1220
|
-
|
|
1221
|
-
const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
|
|
1222
|
-
// INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
|
|
1223
|
-
// warn-severity check above doesn't see it. It still represents pending
|
|
1224
|
-
// work, so it must promote the summary off the success icon.
|
|
1225
|
-
const hasInvariantPending =
|
|
1226
|
-
result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
|
|
1227
|
-
|
|
1228
|
-
if (result.mode === 'online') {
|
|
1229
|
-
if (hasUnknown || hasWarnings) {
|
|
1230
|
-
lines.push(`${c(yellow, '⚠')} ${result.summary}`);
|
|
1231
|
-
} else if (pendingCount === 0 && !hasInvariantPending) {
|
|
1232
|
-
lines.push(`${c(cyan, '✔')} ${result.summary}`);
|
|
1233
|
-
} else {
|
|
1234
|
-
lines.push(`${c(yellow, '⧗')} ${result.summary}`);
|
|
1235
|
-
}
|
|
1236
|
-
} else {
|
|
1237
|
-
lines.push(result.summary);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (result.requiredInvariants.length > 0) {
|
|
1241
|
-
if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
|
|
1242
|
-
lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
|
|
1243
|
-
lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
|
|
1244
|
-
} else {
|
|
1245
|
-
lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
|
|
1250
|
-
for (const diag of warnings) {
|
|
1251
|
-
lines.push(`${c(yellow, '⚠')} ${diag.message}`);
|
|
1252
|
-
for (const hint of diag.hints) {
|
|
1253
|
-
lines.push(` ${c(dim, hint)}`);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Per-space section. Suppressed when there's no extension space —
|
|
1258
|
-
// the top-level output already covers the app member.
|
|
1259
|
-
// When extensions exist, render every space (including the app)
|
|
1260
|
-
// for consistency, plus a cross-space pending total + apply hint.
|
|
1261
|
-
if (result.spaces?.some((s) => s.kind === 'extension')) {
|
|
1262
|
-
const total = result.totalPendingAcrossSpaces ?? 0;
|
|
1263
|
-
lines.push('');
|
|
1264
|
-
lines.push(c(dim, 'spaces'));
|
|
1265
|
-
for (const space of result.spaces) {
|
|
1266
|
-
lines.push(formatSpaceLine(space, c));
|
|
1267
|
-
}
|
|
1268
|
-
if (total > 0) {
|
|
1269
|
-
lines.push('');
|
|
1270
|
-
lines.push(
|
|
1271
|
-
`${c(yellow, '⧗')} ${total} pending migration(s) across ${result.spaces.length} space(s) — run 'prisma-next migrate' to apply`,
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return lines.join('\n');
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function formatSpaceLine(
|
|
1280
|
-
space: MigrationStatusSpaceEntry,
|
|
1281
|
-
c: (fn: (s: string) => string, s: string) => string,
|
|
1282
|
-
): string {
|
|
1283
|
-
const glyph = (() => {
|
|
1284
|
-
if (space.status === 'up-to-date' || space.status === 'no-marker') return c(cyan, '✓');
|
|
1285
|
-
if (space.status === 'pending') return c(yellow, '⧗');
|
|
1286
|
-
if (space.status === 'unreachable' || space.status === 'never-planned') return c(magenta, '✗');
|
|
1287
|
-
return ' ';
|
|
1288
|
-
})();
|
|
1289
|
-
const tag = space.kind === 'app' ? '[app]' : '[ext]';
|
|
1290
|
-
const head = space.headHash.slice(0, 8);
|
|
1291
|
-
const marker =
|
|
1292
|
-
space.markerHash === undefined
|
|
1293
|
-
? '(unknown)'
|
|
1294
|
-
: space.markerHash === null
|
|
1295
|
-
? '(no marker)'
|
|
1296
|
-
: space.markerHash.slice(0, 8);
|
|
1297
|
-
const pending =
|
|
1298
|
-
space.pendingCount === undefined
|
|
1299
|
-
? ''
|
|
1300
|
-
: space.pendingCount === 0
|
|
1301
|
-
? c(dim, ' (up to date)')
|
|
1302
|
-
: c(yellow, ` (${space.pendingCount} pending)`);
|
|
1303
|
-
return ` ${glyph} ${c(dim, tag)} ${space.spaceId} → head ${c(dim, head)}, marker ${c(dim, marker)}${pending}`;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function formatInvariantList(ids: readonly string[]): string {
|
|
1307
|
-
return ids.length === 0 ? '(none)' : ids.join(', ');
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
function summarizeRefDistance(
|
|
1311
|
-
graph: MigrationGraph,
|
|
1312
|
-
markerHash: string,
|
|
1313
|
-
refHash: string,
|
|
1314
|
-
refName: string,
|
|
1315
|
-
): string {
|
|
1316
|
-
if (markerHash === refHash) return `At ref "${refName}" target`;
|
|
1317
|
-
|
|
1318
|
-
const pathToRef = findPath(graph, markerHash, refHash);
|
|
1319
|
-
if (pathToRef) return `${pathToRef.length} migration(s) behind ref "${refName}"`;
|
|
1320
|
-
|
|
1321
|
-
const pathFromRef = findPath(graph, refHash, markerHash);
|
|
1322
|
-
if (pathFromRef) return `${pathFromRef.length} migration(s) ahead of ref "${refName}"`;
|
|
1323
|
-
|
|
1324
|
-
return `No path between database marker and ref "${refName}" target`;
|
|
1325
|
-
}
|