@prisma-next/cli 0.12.0-dev.3 → 0.12.0-dev.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/cli.mjs +180 -163
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/{client-KgJorIvG.mjs → client-xeWpMlq1.mjs} +39 -15
  4. package/dist/client-xeWpMlq1.mjs.map +1 -0
  5. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DK_5ItoJ.mjs} +284 -24
  6. package/dist/command-helpers-DK_5ItoJ.mjs.map +1 -0
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.mjs +4 -5
  10. package/dist/commands/db-init.mjs.map +1 -1
  11. package/dist/commands/db-schema.mjs +3 -3
  12. package/dist/commands/db-sign.mjs +4 -4
  13. package/dist/commands/db-update.d.mts.map +1 -1
  14. package/dist/commands/db-update.mjs +10 -7
  15. package/dist/commands/db-update.mjs.map +1 -1
  16. package/dist/commands/db-verify.mjs +1 -1
  17. package/dist/commands/migrate.d.mts +1 -1
  18. package/dist/commands/migrate.mjs +5 -6
  19. package/dist/commands/migrate.mjs.map +1 -1
  20. package/dist/commands/migration-check.mjs +1 -1
  21. package/dist/commands/migration-graph.d.mts +13 -7
  22. package/dist/commands/migration-graph.d.mts.map +1 -1
  23. package/dist/commands/migration-graph.mjs +171 -2
  24. package/dist/commands/migration-graph.mjs.map +1 -0
  25. package/dist/commands/migration-list.d.mts +24 -26
  26. package/dist/commands/migration-list.d.mts.map +1 -1
  27. package/dist/commands/migration-list.mjs +2 -190
  28. package/dist/commands/migration-log.d.mts +6 -18
  29. package/dist/commands/migration-log.d.mts.map +1 -1
  30. package/dist/commands/migration-log.mjs +1 -137
  31. package/dist/commands/migration-new.mjs +3 -3
  32. package/dist/commands/migration-plan.d.mts +1 -1
  33. package/dist/commands/migration-plan.mjs +1 -1
  34. package/dist/commands/migration-show.d.mts +1 -1
  35. package/dist/commands/migration-show.mjs +3 -4
  36. package/dist/commands/migration-show.mjs.map +1 -1
  37. package/dist/commands/migration-status.d.mts +41 -141
  38. package/dist/commands/migration-status.d.mts.map +1 -1
  39. package/dist/commands/migration-status.mjs +2 -759
  40. package/dist/commands/ref.d.mts +1 -1
  41. package/dist/commands/ref.mjs +3 -3
  42. package/dist/commands/telemetry/index.d.mts +7 -0
  43. package/dist/commands/telemetry/index.d.mts.map +1 -0
  44. package/dist/commands/telemetry/index.mjs +2 -0
  45. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-DG3kjgoz.mjs} +2 -2
  46. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-DG3kjgoz.mjs.map} +1 -1
  47. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-BO0l6fnT.mjs} +3 -3
  48. package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-BO0l6fnT.mjs.map} +1 -1
  49. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-C0Bs0VRj.mjs} +3 -3
  50. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-C0Bs0VRj.mjs.map} +1 -1
  51. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-2wtPflGH.mjs} +3 -3
  52. package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-2wtPflGH.mjs.map} +1 -1
  53. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-Dbr3-jHF.mjs} +4 -4
  54. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-Dbr3-jHF.mjs.map} +1 -1
  55. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-CxHiSiTG.mjs} +4 -4
  56. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-CxHiSiTG.mjs.map} +1 -1
  57. package/dist/exports/control-api.d.mts +1 -1
  58. package/dist/exports/control-api.d.mts.map +1 -1
  59. package/dist/exports/control-api.mjs +2 -2
  60. package/dist/exports/index.mjs +1 -1
  61. package/dist/exports/init-output.mjs +1 -1
  62. package/dist/{framework-components-fYXjz_in.mjs → framework-components-CxOVKAAh.mjs} +2 -2
  63. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-CxOVKAAh.mjs.map} +1 -1
  64. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
  65. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
  66. package/dist/{init-Cv9UzWL5.mjs → init-R272pxux.mjs} +5 -58
  67. package/dist/init-R272pxux.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-RekOwfi5.mjs} +3 -3
  69. package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-RekOwfi5.mjs.map} +1 -1
  70. package/dist/{migration-check-BiBJoYYW.mjs → migration-check-Dc0cOhKH.mjs} +2 -2
  71. package/dist/{migration-check-BiBJoYYW.mjs.map → migration-check-Dc0cOhKH.mjs.map} +1 -1
  72. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-ApB3NxWY.mjs} +3 -3
  73. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-ApB3NxWY.mjs.map} +1 -1
  74. package/dist/migration-graph-space-render-dmLLWift.mjs +1966 -0
  75. package/dist/migration-graph-space-render-dmLLWift.mjs.map +1 -0
  76. package/dist/migration-list-C5sXrl0U.mjs +228 -0
  77. package/dist/migration-list-C5sXrl0U.mjs.map +1 -0
  78. package/dist/migration-list-types-DS63IdFd.d.mts +23 -0
  79. package/dist/migration-list-types-DS63IdFd.d.mts.map +1 -0
  80. package/dist/migration-log-DD_vCbYW.mjs +203 -0
  81. package/dist/migration-log-DD_vCbYW.mjs.map +1 -0
  82. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CeTjQOIG.mjs} +5 -5
  83. package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CeTjQOIG.mjs.map} +1 -1
  84. package/dist/migration-status-qV8ctwPy.mjs +432 -0
  85. package/dist/migration-status-qV8ctwPy.mjs.map +1 -0
  86. package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
  87. package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
  88. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-V1o-9LVK.mjs} +1 -1
  89. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-V1o-9LVK.mjs.map} +1 -1
  90. package/dist/telemetry-S-NGi9U6.mjs +122 -0
  91. package/dist/telemetry-S-NGi9U6.mjs.map +1 -0
  92. package/dist/{types-Dt_SfqFm.d.mts → types-Mh7mdPHM.d.mts} +13 -2
  93. package/dist/types-Mh7mdPHM.d.mts.map +1 -0
  94. package/dist/{verify-DCA9Sldu.mjs → verify-BdI-BgYi.mjs} +2 -2
  95. package/dist/{verify-DCA9Sldu.mjs.map → verify-BdI-BgYi.mjs.map} +1 -1
  96. package/package.json +22 -19
  97. package/src/cli.ts +5 -0
  98. package/src/commands/db-update.ts +7 -1
  99. package/src/commands/init/index.ts +6 -35
  100. package/src/commands/init/init.ts +1 -14
  101. package/src/commands/init/inputs.ts +0 -75
  102. package/src/commands/migration-graph.ts +143 -82
  103. package/src/commands/migration-list.ts +55 -25
  104. package/src/commands/migration-log.ts +23 -89
  105. package/src/commands/migration-status-overlay.ts +61 -0
  106. package/src/commands/migration-status.ts +431 -1055
  107. package/src/commands/telemetry/index.ts +107 -0
  108. package/src/commands/telemetry/status.ts +67 -0
  109. package/src/control-api/client.ts +11 -1
  110. package/src/control-api/operations/apply.ts +1 -0
  111. package/src/control-api/operations/db-apply.ts +24 -1
  112. package/src/control-api/operations/migration-apply.ts +10 -3
  113. package/src/control-api/types.ts +16 -1
  114. package/src/utils/cli-errors.ts +17 -0
  115. package/src/utils/formatters/errors.ts +11 -0
  116. package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
  117. package/src/utils/formatters/migration-graph-layout.ts +51 -7
  118. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  119. package/src/utils/formatters/migration-graph-space-render.ts +138 -0
  120. package/src/utils/formatters/migration-graph-tree-render.ts +405 -77
  121. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  122. package/src/utils/formatters/migration-list-graph-topology.ts +68 -90
  123. package/src/utils/formatters/migration-list-render.ts +122 -70
  124. package/src/utils/formatters/migration-list-styler.ts +48 -5
  125. package/src/utils/formatters/migration-log-table.ts +190 -0
  126. package/src/utils/formatters/migrations.ts +25 -1
  127. package/src/utils/global-flags.ts +35 -0
  128. package/src/utils/legend.ts +38 -0
  129. package/src/utils/telemetry.ts +68 -32
  130. package/dist/client-KgJorIvG.mjs.map +0 -1
  131. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  132. package/dist/commands/migration-list.mjs.map +0 -1
  133. package/dist/commands/migration-log.mjs.map +0 -1
  134. package/dist/commands/migration-status.mjs.map +0 -1
  135. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  136. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  137. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  138. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  139. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  140. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  141. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  142. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  143. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  144. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  145. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  146. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  147. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  148. package/src/utils/formatters/graph-render.ts +0 -1323
  149. package/src/utils/formatters/graph-types.ts +0 -120
@@ -0,0 +1,107 @@
1
+ import { userConfigPath, writeUserConfig } from '@prisma-next/cli-telemetry';
2
+ import { Command } from 'commander';
3
+ import {
4
+ addGlobalOptions,
5
+ setCommandDescriptions,
6
+ setCommandExamples,
7
+ } from '../../utils/command-helpers';
8
+ import { formatCommandHelp } from '../../utils/formatters/help';
9
+ import {
10
+ type CommonCommandOptions,
11
+ parseGlobalFlags,
12
+ parseGlobalFlagsOrExit,
13
+ } from '../../utils/global-flags';
14
+ import { isCI } from '../../utils/is-ci';
15
+ import { createTerminalUI } from '../../utils/terminal-ui';
16
+ import { formatTelemetryStatusLines, resolveTelemetryStatus } from './status';
17
+
18
+ function createTelemetryStatusCommand(): Command {
19
+ const command = new Command('status');
20
+ setCommandDescriptions(
21
+ command,
22
+ 'Show whether anonymous CLI telemetry is enabled and why',
23
+ 'Reports whether telemetry is currently enabled or disabled and the reason\n' +
24
+ '(default-on, stored opt-out, environment opt-out, or CI), the path to your\n' +
25
+ 'user-level config file, and whether an installation ID has been stored.\n' +
26
+ 'Read-only: never sends an event, never mints an ID, never writes anything.',
27
+ );
28
+ return addGlobalOptions(command).action((options: CommonCommandOptions) => {
29
+ const flags = parseGlobalFlagsOrExit(options);
30
+ const ui = createTerminalUI(flags);
31
+ const status = resolveTelemetryStatus({ env: process.env, inCI: isCI() });
32
+ if (flags.json) {
33
+ ui.output(JSON.stringify(status));
34
+ } else {
35
+ for (const line of formatTelemetryStatusLines(status)) {
36
+ ui.output(line);
37
+ }
38
+ }
39
+ process.exit(0);
40
+ });
41
+ }
42
+
43
+ function createTelemetryEnableCommand(): Command {
44
+ const command = new Command('enable');
45
+ setCommandDescriptions(
46
+ command,
47
+ 'Enable anonymous CLI telemetry',
48
+ 'Stores "enableTelemetry": true in your user-level config and mints an\n' +
49
+ 'installation ID if one is not already stored.',
50
+ );
51
+ return addGlobalOptions(command).action((options: CommonCommandOptions) => {
52
+ const flags = parseGlobalFlagsOrExit(options);
53
+ writeUserConfig({ enableTelemetry: true });
54
+ const ui = createTerminalUI(flags);
55
+ if (flags.json) {
56
+ ui.output(JSON.stringify({ enableTelemetry: true, configPath: userConfigPath() }));
57
+ } else {
58
+ ui.output(`Telemetry enabled. Preference stored in ${userConfigPath()}.`);
59
+ }
60
+ process.exit(0);
61
+ });
62
+ }
63
+
64
+ function createTelemetryDisableCommand(): Command {
65
+ const command = new Command('disable');
66
+ setCommandDescriptions(
67
+ command,
68
+ 'Disable anonymous CLI telemetry',
69
+ 'Stores "enableTelemetry": false in your user-level config. No installation\n' +
70
+ 'ID is minted and no event is sent.',
71
+ );
72
+ return addGlobalOptions(command).action((options: CommonCommandOptions) => {
73
+ const flags = parseGlobalFlagsOrExit(options);
74
+ writeUserConfig({ enableTelemetry: false });
75
+ const ui = createTerminalUI(flags);
76
+ if (flags.json) {
77
+ ui.output(JSON.stringify({ enableTelemetry: false, configPath: userConfigPath() }));
78
+ } else {
79
+ ui.output(`Telemetry disabled. Preference stored in ${userConfigPath()}.`);
80
+ }
81
+ process.exit(0);
82
+ });
83
+ }
84
+
85
+ export function createTelemetryCommand(): Command {
86
+ const command = new Command('telemetry');
87
+ setCommandDescriptions(
88
+ command,
89
+ 'Inspect and change anonymous CLI telemetry',
90
+ 'Show telemetry status, or enable / disable anonymous CLI usage data.\n' +
91
+ 'Telemetry is on by default (opt-out); see https://prisma-next.dev/docs/cli/telemetry\n' +
92
+ 'for what is collected and why.',
93
+ );
94
+ setCommandExamples(command, [
95
+ 'prisma-next telemetry status',
96
+ 'prisma-next telemetry disable',
97
+ 'prisma-next telemetry enable',
98
+ ]);
99
+ command.configureHelp({
100
+ formatHelp: (cmd) => formatCommandHelp({ command: cmd, flags: parseGlobalFlags({}) }),
101
+ subcommandDescription: () => '',
102
+ });
103
+ command.addCommand(createTelemetryStatusCommand());
104
+ command.addCommand(createTelemetryEnableCommand());
105
+ command.addCommand(createTelemetryDisableCommand());
106
+ return command;
107
+ }
@@ -0,0 +1,67 @@
1
+ import { readUserConfig, resolveGating, userConfigPath } from '@prisma-next/cli-telemetry';
2
+
3
+ /**
4
+ * Why telemetry resolves the way it does, in the order the CLI's
5
+ * `resolveTelemetryGate` evaluates: CI hard-disables first, then the env
6
+ * opt-outs, then the stored `enableTelemetry`, then the opt-out default.
7
+ */
8
+ export type TelemetryStatusReason =
9
+ | 'ci'
10
+ | 'env-opt-out'
11
+ | 'stored-opt-out'
12
+ | 'stored-opt-in'
13
+ | 'default-on';
14
+
15
+ export interface TelemetryStatus {
16
+ readonly enabled: boolean;
17
+ readonly reason: TelemetryStatusReason;
18
+ readonly configPath: string;
19
+ readonly installationIdStored: boolean;
20
+ }
21
+
22
+ /**
23
+ * Resolves the same gate the runtime uses (CI check + `resolveGating`) and
24
+ * projects it into a user-facing status. Pure read: never mints, never
25
+ * writes. The `installationId` value itself is never surfaced — only its
26
+ * presence — so `status` discloses nothing identifying.
27
+ */
28
+ export function resolveTelemetryStatus(inputs: {
29
+ readonly env: Readonly<Record<string, string | undefined>>;
30
+ readonly inCI: boolean;
31
+ }): TelemetryStatus {
32
+ const config = readUserConfig();
33
+ const configPath = userConfigPath();
34
+ const installationIdStored =
35
+ typeof config.installationId === 'string' && config.installationId.length > 0;
36
+
37
+ if (inputs.inCI) {
38
+ return { enabled: false, reason: 'ci', configPath, installationIdStored };
39
+ }
40
+
41
+ const gating = resolveGating({ env: inputs.env, config });
42
+ if (!gating.enabled) {
43
+ const reason: TelemetryStatusReason =
44
+ gating.reason === 'env-override' ? 'env-opt-out' : 'stored-opt-out';
45
+ return { enabled: false, reason, configPath, installationIdStored };
46
+ }
47
+
48
+ const reason: TelemetryStatusReason =
49
+ config.enableTelemetry === true ? 'stored-opt-in' : 'default-on';
50
+ return { enabled: true, reason, configPath, installationIdStored };
51
+ }
52
+
53
+ const REASON_EXPLANATION: Record<TelemetryStatusReason, string> = {
54
+ ci: 'CI environment detected — telemetry is hard-disabled.',
55
+ 'env-opt-out': 'an environment opt-out is set (DO_NOT_TRACK / PRISMA_NEXT_DISABLE_TELEMETRY).',
56
+ 'stored-opt-out': '"enableTelemetry": false is stored in your config.',
57
+ 'stored-opt-in': '"enableTelemetry": true is stored in your config.',
58
+ 'default-on': 'no explicit choice is stored, so the opt-out default applies.',
59
+ };
60
+
61
+ export function formatTelemetryStatusLines(status: TelemetryStatus): string[] {
62
+ return [
63
+ `Telemetry is ${status.enabled ? 'enabled' : 'disabled'}: ${REASON_EXPLANATION[status.reason]}`,
64
+ `Config file: ${status.configPath}`,
65
+ `Installation ID: ${status.installationIdStored ? 'stored' : 'not stored'}`,
66
+ ];
67
+ }
@@ -1,4 +1,8 @@
1
- import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
1
+ import type {
2
+ Contract,
3
+ ContractMarkerRecord,
4
+ LedgerEntryRecord,
5
+ } from '@prisma-next/contract/types';
2
6
  import { emit as emitContractArtifacts } from '@prisma-next/emitter';
3
7
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
4
8
  import type {
@@ -447,6 +451,12 @@ class ControlClientImpl implements ControlClient {
447
451
  return familyInstance.readAllMarkers({ driver });
448
452
  }
449
453
 
454
+ /** Reads the per-migration journal; omit `space` to return every space. */
455
+ async readLedger(space?: string): Promise<readonly LedgerEntryRecord[]> {
456
+ const { driver, familyInstance } = await this.ensureConnected();
457
+ return familyInstance.readLedger({ driver, ...ifDefined('space', space) });
458
+ }
459
+
450
460
  async migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult> {
451
461
  const { onProgress } = options;
452
462
  await this.connectWithProgress(options.connection, 'migrationApply', onProgress);
@@ -141,6 +141,7 @@ export async function applyMigration<TFamilyId extends string, TTargetId extends
141
141
  destinationContract: r.entry.destinationContract,
142
142
  policy,
143
143
  frameworkComponents,
144
+ migrationEdges: r.entry.migrationEdges,
144
145
  // Per-space post-apply schema verification is non-strict: each
145
146
  // space's `destinationContract` describes only its own slice; a
146
147
  // strict verifier would treat every other space's tables as
@@ -5,6 +5,7 @@ import type {
5
5
  ControlExtensionDescriptor,
6
6
  ControlFamilyInstance,
7
7
  MigrationOperationPolicy,
8
+ MigrationPlannerConflict,
8
9
  MigrationPlanOperation,
9
10
  OperationPreview,
10
11
  TargetMigrationsCapability,
@@ -32,7 +33,12 @@ import type {
32
33
  OnControlProgress,
33
34
  PerSpaceExecutionEntry,
34
35
  } from '../types';
35
- import { applyMigration, buildPerSpaceBreakdown, collectOrdered } from './apply';
36
+ import {
37
+ applyMigration,
38
+ buildPerSpaceBreakdown,
39
+ collectOrdered,
40
+ type OrderedResolution,
41
+ } from './apply';
36
42
  import { stripOperations } from './migration-helpers';
37
43
 
38
44
  /**
@@ -175,6 +181,7 @@ export async function executeApply<TFamilyId extends string, TTargetId extends s
175
181
  onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.plan, outcome: 'ok' });
176
182
 
177
183
  const orderedResolutions = collectOrdered(planResult.value.applyOrder, planResult.value.perSpace);
184
+ const plannerWarnings = aggregatePlannerWarnings(orderedResolutions);
178
185
 
179
186
  // The destination's structural shape comes from the app's plan — its
180
187
  // `destination` is the storage hash users see in CLI output.
@@ -202,6 +209,7 @@ export async function executeApply<TFamilyId extends string, TTargetId extends s
202
209
  preview,
203
210
  perSpace,
204
211
  summary,
212
+ ...ifDefined('warnings', plannerWarnings),
205
213
  });
206
214
  }
207
215
 
@@ -228,6 +236,7 @@ export async function executeApply<TFamilyId extends string, TTargetId extends s
228
236
  summary: applied.failure.summary,
229
237
  ...ifDefined('why', applied.failure.why),
230
238
  meta: applied.failure.meta,
239
+ ...ifDefined('warnings', plannerWarnings),
231
240
  });
232
241
  }
233
242
 
@@ -246,9 +255,17 @@ export async function executeApply<TFamilyId extends string, TTargetId extends s
246
255
  operationsExecuted: applied.value.totalOpsExecuted,
247
256
  perSpace: applied.value.perSpace,
248
257
  summary,
258
+ ...ifDefined('warnings', plannerWarnings),
249
259
  });
250
260
  }
251
261
 
262
+ function aggregatePlannerWarnings(
263
+ orderedResolutions: readonly OrderedResolution[],
264
+ ): readonly MigrationPlannerConflict[] | undefined {
265
+ const warnings = orderedResolutions.flatMap((r) => r.entry.warnings ?? []);
266
+ return warnings.length > 0 ? warnings : undefined;
267
+ }
268
+
252
269
  /**
253
270
  * Compare the live `_prisma_marker` rows against the aggregate's
254
271
  * declared members. Any marker row whose `space` is not a member of
@@ -339,6 +356,7 @@ function wrapPlanResult(args: {
339
356
  readonly preview: OperationPreview | undefined;
340
357
  readonly perSpace: readonly PerSpaceExecutionEntry[];
341
358
  readonly summary: string;
359
+ readonly warnings?: readonly MigrationPlannerConflict[];
342
360
  }): DbInitResult | DbUpdateResult {
343
361
  const success: DbInitSuccess | DbUpdateSuccess = {
344
362
  mode: 'plan',
@@ -352,6 +370,7 @@ function wrapPlanResult(args: {
352
370
  },
353
371
  perSpace: args.perSpace,
354
372
  summary: args.summary,
373
+ ...ifDefined('warnings', args.warnings),
355
374
  };
356
375
  return ok(success);
357
376
  }
@@ -363,6 +382,7 @@ function wrapApplyResult(args: {
363
382
  readonly operationsExecuted: number;
364
383
  readonly perSpace: readonly PerSpaceExecutionEntry[];
365
384
  readonly summary: string;
385
+ readonly warnings?: readonly MigrationPlannerConflict[];
366
386
  }): DbInitResult | DbUpdateResult {
367
387
  const success: DbInitSuccess | DbUpdateSuccess = {
368
388
  mode: 'apply',
@@ -380,6 +400,7 @@ function wrapApplyResult(args: {
380
400
  : { storageHash: args.destination.storageHash },
381
401
  perSpace: args.perSpace,
382
402
  summary: args.summary,
403
+ ...ifDefined('warnings', args.warnings),
383
404
  };
384
405
  return ok(success);
385
406
  }
@@ -388,6 +409,7 @@ function buildRunnerFailure(args: {
388
409
  readonly summary: string;
389
410
  readonly why?: string;
390
411
  readonly meta: Record<string, unknown>;
412
+ readonly warnings?: readonly MigrationPlannerConflict[];
391
413
  }): DbInitResult | DbUpdateResult {
392
414
  const failure: DbInitFailure | DbUpdateFailure = {
393
415
  code: 'RUNNER_FAILED',
@@ -395,6 +417,7 @@ function buildRunnerFailure(args: {
395
417
  why: args.why,
396
418
  meta: args.meta,
397
419
  conflicts: undefined,
420
+ ...ifDefined('warnings', args.warnings),
398
421
  };
399
422
  return notOk(failure) as DbInitResult | DbUpdateResult;
400
423
  }
@@ -7,6 +7,7 @@ import type {
7
7
  TargetMigrationsCapability,
8
8
  } from '@prisma-next/framework-components/control';
9
9
  import {
10
+ buildSynthMigrationEdge,
10
11
  type ContractMarkerRecordLike,
11
12
  type ContractSpaceAggregate,
12
13
  type ContractSpaceMember,
@@ -319,7 +320,7 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
319
320
  includeMarkers: true,
320
321
  });
321
322
  const totalMigrationsApplied = applied.value.orderedResolutions.reduce(
322
- (sum, r) => sum + (r.entry.migrationEdges?.length ?? 0),
323
+ (sum, r) => sum + r.entry.migrationEdges.length,
323
324
  0,
324
325
  );
325
326
  const summary = `Applied ${totalMigrationsApplied} migration(s) (${applied.value.totalOpsExecuted} operation(s)) across ${orderedAll.length} contract space(s)`;
@@ -361,7 +362,13 @@ function buildAtHeadResolution(args: {
361
362
  displayOps: [],
362
363
  destinationContract: member.contract(),
363
364
  strategy: 'graph-walk',
364
- migrationEdges: [],
365
+ migrationEdges: [
366
+ buildSynthMigrationEdge({
367
+ currentMarkerStorageHash: liveMarker?.storageHash,
368
+ destinationStorageHash: targetHash,
369
+ operationCount: 0,
370
+ }),
371
+ ],
365
372
  };
366
373
  }
367
374
 
@@ -404,7 +411,7 @@ function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
404
411
  // JSON-shape consumers (e.g. `parsed.applied.length` in integration
405
412
  // tests). The aggregate per-space breakdown lives on `perSpace[]`.
406
413
  const applied = args.orderedResolutions.flatMap((r) => {
407
- const edges = r.entry.migrationEdges ?? [];
414
+ const edges = r.entry.migrationEdges;
408
415
  return edges.map((edge) => ({
409
416
  spaceId: r.spaceId,
410
417
  dirName: edge.dirName,
@@ -2,7 +2,11 @@ import type {
2
2
  ContractSourceDiagnostics,
3
3
  ContractSourceProvider,
4
4
  } from '@prisma-next/config/config-types';
5
- import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
5
+ import type {
6
+ Contract,
7
+ ContractMarkerRecord,
8
+ LedgerEntryRecord,
9
+ } from '@prisma-next/contract/types';
6
10
  import type {
7
11
  ControlAdapterDescriptor,
8
12
  ControlDriverDescriptor,
@@ -391,6 +395,7 @@ export interface DbInitSuccess {
391
395
  */
392
396
  readonly perSpace?: ReadonlyArray<PerSpaceExecutionEntry>;
393
397
  readonly summary: string;
398
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
394
399
  }
395
400
 
396
401
  /**
@@ -406,6 +411,7 @@ export interface DbInitFailure {
406
411
  readonly summary: string;
407
412
  readonly why: string | undefined;
408
413
  readonly conflicts: ReadonlyArray<MigrationPlannerConflict> | undefined;
414
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
409
415
  readonly meta: Record<string, unknown> | undefined;
410
416
  readonly marker?: {
411
417
  readonly storageHash?: string;
@@ -461,6 +467,7 @@ export interface DbUpdateSuccess {
461
467
  */
462
468
  readonly perSpace?: ReadonlyArray<PerSpaceExecutionEntry>;
463
469
  readonly summary: string;
470
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
464
471
  }
465
472
 
466
473
  /**
@@ -476,6 +483,7 @@ export interface DbUpdateFailure {
476
483
  readonly summary: string;
477
484
  readonly why: string | undefined;
478
485
  readonly conflicts: ReadonlyArray<MigrationPlannerConflict> | undefined;
486
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
479
487
  readonly meta: Record<string, unknown> | undefined;
480
488
  }
481
489
 
@@ -876,6 +884,13 @@ export interface ControlClient {
876
884
  */
877
885
  readAllMarkers(): Promise<ReadonlyMap<string, ContractMarkerRecord>>;
878
886
 
887
+ /**
888
+ * Reads the per-migration ledger journal for `space` in apply order.
889
+ * Returns an empty array when the ledger store does not yet exist or
890
+ * has no rows for that space.
891
+ */
892
+ readLedger(space?: string): Promise<readonly LedgerEntryRecord[]>;
893
+
879
894
  /**
880
895
  * Applies pre-planned on-disk migrations to the database.
881
896
  * Each migration runs in its own transaction with full execution checks.
@@ -108,6 +108,23 @@ export function errorRefSetEmptySentinel(hash: string): CliStructuredError {
108
108
  });
109
109
  }
110
110
 
111
+ /**
112
+ * `--legend` was combined with a machine-readable or silent output flag.
113
+ * The legend is human-only decoration on stderr.
114
+ */
115
+ export function errorLegendHumanOnly(
116
+ conflictingFlag: '--json' | '--dot' | '--quiet',
117
+ ): CliStructuredError {
118
+ return errorRuntime('`--legend` is only available for human-readable output', {
119
+ why: `\`--legend\` prints a glyph key to stderr and cannot be combined with ${conflictingFlag}.`,
120
+ fix: `Omit ${conflictingFlag} to print the legend alongside the tree, or omit --legend when using ${conflictingFlag}.`,
121
+ meta: {
122
+ code: 'MIGRATION.LEGEND_HUMAN_ONLY',
123
+ conflictingFlag,
124
+ },
125
+ });
126
+ }
127
+
111
128
  /**
112
129
  * `--space <id>` was given a value that doesn't satisfy the contract-space
113
130
  * naming rule (`[a-z][a-z0-9_-]{0,63}` per `isValidSpaceId`). Fires before
@@ -1,8 +1,11 @@
1
+ import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
2
+ import { blindCast } from '@prisma-next/utils/casts';
1
3
  import { red } from 'colorette';
2
4
 
3
5
  import type { CliErrorConflict, CliErrorEnvelope } from '../cli-errors';
4
6
  import type { GlobalFlags } from '../global-flags';
5
7
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
8
+ import { formatPlannerWarningsBlock } from './migrations';
6
9
 
7
10
  /**
8
11
  * Formats error output for human-readable display.
@@ -67,6 +70,14 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
67
70
  if (error.docsUrl && isVerbose(flags, 1)) {
68
71
  lines.push(formatDimText(error.docsUrl));
69
72
  }
73
+ const plannerWarnings = error.meta?.['plannerWarnings'];
74
+ if (Array.isArray(plannerWarnings) && plannerWarnings.length > 0) {
75
+ const typedWarnings = blindCast<
76
+ readonly MigrationPlannerConflict[],
77
+ 'mapDbUpdateFailure (db-update.ts) writes meta.plannerWarnings as MigrationPlannerConflict[]; meta is typed Record<string, unknown> so the channel erases the element type'
78
+ >(plannerWarnings);
79
+ lines.push(...formatPlannerWarningsBlock(typedWarnings, useColor));
80
+ }
70
81
  if (isVerbose(flags, 2) && error.meta) {
71
82
  lines.push(`${formatDimText(` Meta: ${JSON.stringify(error.meta, null, 2)}`)}`);
72
83
  }
@@ -0,0 +1,194 @@
1
+ import { createColors } from 'colorette';
2
+ import type { StructuralCell } from './migration-graph-layout';
3
+
4
+ export type LaneColorizer = (text: string) => string;
5
+
6
+ const { magenta, cyan, green, yellow, blueBright, red } = createColors({ useColor: true });
7
+
8
+ export const LANE_COLOR_CYCLE: readonly LaneColorizer[] = [
9
+ magenta,
10
+ cyan,
11
+ green,
12
+ yellow,
13
+ blueBright,
14
+ red,
15
+ ];
16
+
17
+ /**
18
+ * The leftmost lane (column 0) renders neutral — no palette hue. Columns ≥ 1
19
+ * rotate through {@link LANE_COLOR_CYCLE}.
20
+ */
21
+ export const NEUTRAL_LANE_COLUMN = 0;
22
+
23
+ /**
24
+ * The hue for a gutter column. The leftmost lane (column 0) is **neutral** — it
25
+ * has nothing to be told apart from in the common single-lane linear case, so
26
+ * the renderer dims it rather than tinting it; the rotating palette is reserved
27
+ * for columns ≥ 1 (where a second lane exists to distinguish). Callers must dim
28
+ * column 0 themselves; this returns identity for it as a guard. A lane freed and
29
+ * reused by a later branch keeps its column's hue — coloring is by position, not
30
+ * branch identity, exactly like `git log --graph`.
31
+ */
32
+ export function laneColorForColumn(column: number): LaneColorizer {
33
+ if (column <= NEUTRAL_LANE_COLUMN) {
34
+ return (text) => text;
35
+ }
36
+ const colorizer = LANE_COLOR_CYCLE[(column - 1) % LANE_COLOR_CYCLE.length];
37
+ return colorizer ?? ((text) => text);
38
+ }
39
+
40
+ /**
41
+ * Style a structural glyph by its resolved colour column. Column 0 and the
42
+ * neutral sentinel render dim (`dimLane`); columns ≥ 1 take a palette hue.
43
+ */
44
+ export function stylerForLaneColumn(
45
+ colorColumn: number,
46
+ colorize: boolean,
47
+ dimLane: (text: string) => string,
48
+ ): LaneColorizer {
49
+ if (!colorize || colorColumn <= NEUTRAL_LANE_COLUMN) {
50
+ return dimLane;
51
+ }
52
+ return laneColorForColumn(colorColumn);
53
+ }
54
+
55
+ /**
56
+ * The colour-source column for each cell of a row, resolved together because a
57
+ * routed back-arc spans columns and must read as **one hue** rather than a
58
+ * per-column "rainbow".
59
+ */
60
+ export interface RowArcLaneColors {
61
+ /** Colour column for a cell's structural glyph (lane / spine / arc body). */
62
+ readonly lane: readonly number[];
63
+ /** Colour column for a node arc-pair's connector half (`◂` / `─`). */
64
+ readonly connector: readonly number[];
65
+ /**
66
+ * Colour column for the trailing `─` of a landing tee (`┴─`). The junction
67
+ * (`lane`) keeps its own column; the dash leads into the next converging arc.
68
+ */
69
+ readonly dash: readonly number[];
70
+ }
71
+
72
+ /**
73
+ * Resolve per-cell colour columns for a node/arc row. Scanning right-to-left
74
+ * lets each arc segment inherit the hue of the arc it leads into.
75
+ */
76
+ export function resolveRowArcLaneColors(cells: readonly StructuralCell[]): RowArcLaneColors {
77
+ const lane = new Array<number>(cells.length);
78
+ const connector = new Array<number>(cells.length);
79
+ const dash = new Array<number>(cells.length);
80
+ let arcCorner = NEUTRAL_LANE_COLUMN;
81
+ let landingAnchor = NEUTRAL_LANE_COLUMN;
82
+ for (let column = cells.length - 1; column >= 0; column--) {
83
+ const cell = cells[column];
84
+ connector[column] = landingAnchor !== NEUTRAL_LANE_COLUMN ? landingAnchor : arcCorner;
85
+ switch (cell?.kind) {
86
+ case 'arc-branch-corner':
87
+ arcCorner = column;
88
+ lane[column] = column;
89
+ dash[column] = column;
90
+ break;
91
+ case 'arc-land-corner':
92
+ arcCorner = column;
93
+ landingAnchor = column;
94
+ lane[column] = column;
95
+ dash[column] = column;
96
+ break;
97
+ case 'arc-branch-tee':
98
+ lane[column] = column;
99
+ dash[column] = column;
100
+ break;
101
+ case 'arc-land-tee':
102
+ lane[column] = column;
103
+ dash[column] = landingAnchor === NEUTRAL_LANE_COLUMN ? column : landingAnchor;
104
+ landingAnchor = column;
105
+ break;
106
+ case 'arc-crossing':
107
+ case 'arc-land-bridge': {
108
+ const served = landingAnchor !== NEUTRAL_LANE_COLUMN ? landingAnchor : arcCorner;
109
+ lane[column] = served;
110
+ dash[column] = served;
111
+ break;
112
+ }
113
+ case 'horizontal-pass':
114
+ lane[column] = arcCorner === NEUTRAL_LANE_COLUMN ? column : arcCorner;
115
+ dash[column] = lane[column] ?? column;
116
+ break;
117
+ case 'node':
118
+ lane[column] = column;
119
+ dash[column] = column;
120
+ arcCorner = NEUTRAL_LANE_COLUMN;
121
+ landingAnchor = NEUTRAL_LANE_COLUMN;
122
+ break;
123
+ default:
124
+ lane[column] = column;
125
+ dash[column] = column;
126
+ arcCorner = NEUTRAL_LANE_COLUMN;
127
+ landingAnchor = NEUTRAL_LANE_COLUMN;
128
+ }
129
+ }
130
+ return { lane, connector, dash };
131
+ }
132
+
133
+ /**
134
+ * Per-cell colour for a forward branch/merge connector row, split into the
135
+ * cell's junction `glyph` and its trailing `dash`.
136
+ */
137
+ export interface ConnectorLaneColors {
138
+ /** Colour column for a cell's junction glyph (`├` / `┬` / `┴` / `╮` / `╯`). */
139
+ readonly glyph: readonly number[];
140
+ /** Colour column for a tee's trailing `─` — the branch it leads into. */
141
+ readonly dash: readonly number[];
142
+ }
143
+
144
+ /**
145
+ * Resolve per-cell connector colours. Scanning right-to-left, a corner or an
146
+ * intermediate tee anchors its own lane, but a tee's trailing dash leads into
147
+ * the branch on its right.
148
+ */
149
+ export function resolveConnectorLaneColors(
150
+ cells: readonly StructuralCell[],
151
+ startLane: number,
152
+ ): ConnectorLaneColors {
153
+ const glyph = new Array<number>(cells.length);
154
+ const dash = new Array<number>(cells.length);
155
+ let owner = NEUTRAL_LANE_COLUMN;
156
+ for (let column = cells.length - 1; column >= 0; column--) {
157
+ const cell = cells[column];
158
+ switch (cell?.kind) {
159
+ case 'branch-corner':
160
+ case 'merge-corner':
161
+ owner = column;
162
+ glyph[column] = column;
163
+ dash[column] = column;
164
+ break;
165
+ case 'branch-tee':
166
+ case 'merge-tee':
167
+ if (column === startLane) {
168
+ const served = owner === NEUTRAL_LANE_COLUMN ? column : owner;
169
+ glyph[column] = column;
170
+ dash[column] = served;
171
+ } else {
172
+ dash[column] = owner === NEUTRAL_LANE_COLUMN ? column : owner;
173
+ glyph[column] = column;
174
+ owner = column;
175
+ }
176
+ break;
177
+ case 'arc-crossing':
178
+ glyph[column] = column;
179
+ dash[column] = owner === NEUTRAL_LANE_COLUMN ? column : owner;
180
+ owner = column;
181
+ break;
182
+ case 'horizontal-pass': {
183
+ const served = owner === NEUTRAL_LANE_COLUMN ? column : owner;
184
+ glyph[column] = served;
185
+ dash[column] = served;
186
+ break;
187
+ }
188
+ default:
189
+ glyph[column] = column;
190
+ dash[column] = column;
191
+ }
192
+ }
193
+ return { glyph, dash };
194
+ }