@prisma-next/cli 0.12.0-dev.27 → 0.12.0-dev.29

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 (114) hide show
  1. package/dist/cli.mjs +12 -12
  2. package/dist/{client-V7BkIQrQ.mjs → client-xeWpMlq1.mjs} +22 -11
  3. package/dist/client-xeWpMlq1.mjs.map +1 -0
  4. package/dist/{command-helpers-DlrUCI7s.mjs → command-helpers-DK_5ItoJ.mjs} +253 -2
  5. package/dist/command-helpers-DK_5ItoJ.mjs.map +1 -0
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.mjs +4 -5
  9. package/dist/commands/db-init.mjs.map +1 -1
  10. package/dist/commands/db-schema.mjs +3 -3
  11. package/dist/commands/db-sign.mjs +4 -4
  12. package/dist/commands/db-update.d.mts.map +1 -1
  13. package/dist/commands/db-update.mjs +10 -7
  14. package/dist/commands/db-update.mjs.map +1 -1
  15. package/dist/commands/db-verify.mjs +1 -1
  16. package/dist/commands/migrate.d.mts +1 -1
  17. package/dist/commands/migrate.mjs +5 -6
  18. package/dist/commands/migrate.mjs.map +1 -1
  19. package/dist/commands/migration-check.mjs +1 -1
  20. package/dist/commands/migration-graph.d.mts +12 -15
  21. package/dist/commands/migration-graph.d.mts.map +1 -1
  22. package/dist/commands/migration-graph.mjs +84 -51
  23. package/dist/commands/migration-graph.mjs.map +1 -1
  24. package/dist/commands/migration-list.d.mts +14 -5
  25. package/dist/commands/migration-list.d.mts.map +1 -1
  26. package/dist/commands/migration-list.mjs +1 -187
  27. package/dist/commands/migration-log.d.mts +2 -2
  28. package/dist/commands/migration-log.mjs +1 -1
  29. package/dist/commands/migration-new.mjs +3 -3
  30. package/dist/commands/migration-plan.mjs +1 -1
  31. package/dist/commands/migration-show.d.mts +1 -1
  32. package/dist/commands/migration-show.mjs +3 -4
  33. package/dist/commands/migration-show.mjs.map +1 -1
  34. package/dist/commands/migration-status.d.mts +21 -3
  35. package/dist/commands/migration-status.d.mts.map +1 -1
  36. package/dist/commands/migration-status.mjs +2 -3
  37. package/dist/commands/ref.d.mts +1 -1
  38. package/dist/commands/ref.mjs +3 -3
  39. package/dist/commands/telemetry/index.mjs +1 -1
  40. package/dist/{contract-at-errors-DlZHXSkI.mjs → contract-at-errors-DG3kjgoz.mjs} +2 -2
  41. package/dist/{contract-at-errors-DlZHXSkI.mjs.map → contract-at-errors-DG3kjgoz.mjs.map} +1 -1
  42. package/dist/{contract-emit-S53EyBRV.mjs → contract-emit-BO0l6fnT.mjs} +3 -3
  43. package/dist/{contract-emit-S53EyBRV.mjs.map → contract-emit-BO0l6fnT.mjs.map} +1 -1
  44. package/dist/{contract-emit-CaKp92-Q.mjs → contract-emit-C0Bs0VRj.mjs} +3 -3
  45. package/dist/{contract-emit-CaKp92-Q.mjs.map → contract-emit-C0Bs0VRj.mjs.map} +1 -1
  46. package/dist/{contract-infer-Cebb-_Qx.mjs → contract-infer-2wtPflGH.mjs} +3 -3
  47. package/dist/{contract-infer-Cebb-_Qx.mjs.map → contract-infer-2wtPflGH.mjs.map} +1 -1
  48. package/dist/{contract-space-aggregate-loader-Dvl1SJ4C.mjs → contract-space-aggregate-loader-Dbr3-jHF.mjs} +3 -3
  49. package/dist/{contract-space-aggregate-loader-Dvl1SJ4C.mjs.map → contract-space-aggregate-loader-Dbr3-jHF.mjs.map} +1 -1
  50. package/dist/{db-verify-B1OoWEWn.mjs → db-verify-CxHiSiTG.mjs} +4 -4
  51. package/dist/{db-verify-B1OoWEWn.mjs.map → db-verify-CxHiSiTG.mjs.map} +1 -1
  52. package/dist/exports/control-api.d.mts +1 -1
  53. package/dist/exports/control-api.mjs +2 -2
  54. package/dist/exports/index.mjs +1 -1
  55. package/dist/{framework-components-DCAT1uUC.mjs → framework-components-CxOVKAAh.mjs} +2 -2
  56. package/dist/{framework-components-DCAT1uUC.mjs.map → framework-components-CxOVKAAh.mjs.map} +1 -1
  57. package/dist/{init-Kf3T4A4W.mjs → init-R272pxux.mjs} +3 -3
  58. package/dist/{init-Kf3T4A4W.mjs.map → init-R272pxux.mjs.map} +1 -1
  59. package/dist/{inspect-live-schema-DTqflZ8X.mjs → inspect-live-schema-RekOwfi5.mjs} +3 -3
  60. package/dist/{inspect-live-schema-DTqflZ8X.mjs.map → inspect-live-schema-RekOwfi5.mjs.map} +1 -1
  61. package/dist/{migration-check-Ccyd0QKb.mjs → migration-check-Dc0cOhKH.mjs} +2 -2
  62. package/dist/{migration-check-Ccyd0QKb.mjs.map → migration-check-Dc0cOhKH.mjs.map} +1 -1
  63. package/dist/{migration-command-scaffold-DI7_SFL0.mjs → migration-command-scaffold-ApB3NxWY.mjs} +3 -3
  64. package/dist/{migration-command-scaffold-DI7_SFL0.mjs.map → migration-command-scaffold-ApB3NxWY.mjs.map} +1 -1
  65. package/dist/{migration-graph-tree-render-DyDBuJEX.mjs → migration-graph-space-render-dmLLWift.mjs} +389 -210
  66. package/dist/migration-graph-space-render-dmLLWift.mjs.map +1 -0
  67. package/dist/migration-list-C5sXrl0U.mjs +228 -0
  68. package/dist/migration-list-C5sXrl0U.mjs.map +1 -0
  69. package/dist/{migration-list-types-DV9PBc7Z.d.mts → migration-list-types-DS63IdFd.d.mts} +1 -1
  70. package/dist/{migration-list-types-DV9PBc7Z.d.mts.map → migration-list-types-DS63IdFd.d.mts.map} +1 -1
  71. package/dist/{migration-log-Des4seHP.mjs → migration-log-DD_vCbYW.mjs} +4 -4
  72. package/dist/{migration-log-Des4seHP.mjs.map → migration-log-DD_vCbYW.mjs.map} +1 -1
  73. package/dist/{migration-plan-DxDTBzGS.mjs → migration-plan-CeTjQOIG.mjs} +5 -5
  74. package/dist/{migration-plan-DxDTBzGS.mjs.map → migration-plan-CeTjQOIG.mjs.map} +1 -1
  75. package/dist/{migration-status-CCwqA-vi.mjs → migration-status-qV8ctwPy.mjs} +61 -45
  76. package/dist/migration-status-qV8ctwPy.mjs.map +1 -0
  77. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-V1o-9LVK.mjs} +1 -1
  78. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-V1o-9LVK.mjs.map} +1 -1
  79. package/dist/{telemetry-LFFQmqHd.mjs → telemetry-S-NGi9U6.mjs} +2 -2
  80. package/dist/{telemetry-LFFQmqHd.mjs.map → telemetry-S-NGi9U6.mjs.map} +1 -1
  81. package/dist/{terminal-ui-C3xGyxW-.d.mts → terminal-ui-5Y6mrg93.d.mts} +1 -1
  82. package/dist/{terminal-ui-C3xGyxW-.d.mts.map → terminal-ui-5Y6mrg93.d.mts.map} +1 -1
  83. package/dist/{types-BdS8PoKM.d.mts → types-Mh7mdPHM.d.mts} +5 -1
  84. package/dist/types-Mh7mdPHM.d.mts.map +1 -0
  85. package/dist/{verify-C0TARc6h.mjs → verify-BdI-BgYi.mjs} +2 -2
  86. package/dist/{verify-C0TARc6h.mjs.map → verify-BdI-BgYi.mjs.map} +1 -1
  87. package/package.json +18 -18
  88. package/src/commands/db-update.ts +7 -1
  89. package/src/commands/migration-graph.ts +125 -58
  90. package/src/commands/migration-list.ts +43 -9
  91. package/src/commands/migration-status.ts +106 -74
  92. package/src/control-api/operations/db-apply.ts +24 -1
  93. package/src/control-api/types.ts +4 -0
  94. package/src/utils/cli-errors.ts +17 -0
  95. package/src/utils/formatters/errors.ts +11 -0
  96. package/src/utils/formatters/migration-graph-lane-colors.ts +164 -1
  97. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  98. package/src/utils/formatters/migration-graph-space-render.ts +138 -0
  99. package/src/utils/formatters/migration-graph-tree-render.ts +149 -239
  100. package/src/utils/formatters/migration-list-data-column.ts +6 -0
  101. package/src/utils/formatters/migration-list-render.ts +43 -23
  102. package/src/utils/formatters/migration-list-styler.ts +48 -5
  103. package/src/utils/formatters/migrations.ts +25 -1
  104. package/src/utils/legend.ts +38 -0
  105. package/dist/client-V7BkIQrQ.mjs.map +0 -1
  106. package/dist/command-helpers-DlrUCI7s.mjs.map +0 -1
  107. package/dist/commands/migration-list.mjs.map +0 -1
  108. package/dist/migration-graph-tree-render-DyDBuJEX.mjs.map +0 -1
  109. package/dist/migration-status-CCwqA-vi.mjs.map +0 -1
  110. package/dist/migration-types-CAQ-0TEE.d.mts +0 -15
  111. package/dist/migration-types-CAQ-0TEE.d.mts.map +0 -1
  112. package/dist/migrations-B3H6RTXb.mjs +0 -228
  113. package/dist/migrations-B3H6RTXb.mjs.map +0 -1
  114. package/dist/types-BdS8PoKM.d.mts.map +0 -1
@@ -3,18 +3,13 @@ import type {
3
3
  ContractMarkerRecordLike,
4
4
  ContractSpaceMember,
5
5
  } from '@prisma-next/migration-tools/aggregate';
6
- import { requireHeadRef } from '@prisma-next/migration-tools/aggregate';
7
6
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
8
7
  import {
9
8
  errorNoInvariantPath,
10
9
  errorUnknownInvariant,
11
10
  MigrationToolsError,
12
11
  } from '@prisma-next/migration-tools/errors';
13
- import {
14
- findPath,
15
- findPathWithDecision,
16
- findReachableLeaves,
17
- } from '@prisma-next/migration-tools/migration-graph';
12
+ import { findPath, findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
18
13
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
19
14
  import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
20
15
  import { readRefs } from '@prisma-next/migration-tools/refs';
@@ -47,16 +42,19 @@ import {
47
42
  loadContractRawSafely,
48
43
  refusePackageCorruptionOnAggregate,
49
44
  } from '../utils/contract-space-aggregate-loader';
50
- import { buildMigrationGraphLayout } from '../utils/formatters/migration-graph-layout';
51
- import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
52
45
  import {
53
- type MigrationEdgeAnnotation,
54
- renderMigrationGraphTree,
55
- } from '../utils/formatters/migration-graph-tree-render';
46
+ computeGlobalMaxDirNameWidth,
47
+ computeGlobalMaxEdgeTreePrefixWidth,
48
+ indentMigrationGraphTreeBlock,
49
+ renderMigrationGraphSpaceTree,
50
+ } from '../utils/formatters/migration-graph-space-render';
51
+ import type { MigrationEdgeAnnotation } from '../utils/formatters/migration-graph-tree-render';
52
+ import { renderMigrationGraphLegend } from '../utils/formatters/migration-graph-tree-render';
56
53
  import type { MigrationListEntry } from '../utils/formatters/migration-list-types';
57
54
  import { formatStyledHeader } from '../utils/formatters/styled';
58
55
  import type { CommonCommandOptions } from '../utils/global-flags';
59
56
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
57
+ import { shouldShowLegend, validateLegendOptions } from '../utils/legend';
60
58
  import type { StatusDiagnostic } from '../utils/migration-types';
61
59
  import { handleResult } from '../utils/result-handler';
62
60
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
@@ -77,6 +75,7 @@ interface MigrationStatusOptions extends CommonCommandOptions {
77
75
  readonly to?: string;
78
76
  readonly from?: string;
79
77
  readonly space?: string;
78
+ readonly legend?: boolean;
80
79
  }
81
80
 
82
81
  export interface MigrationStatusMigrationEntry extends MigrationListEntry {
@@ -112,26 +111,8 @@ function shortDisplayHash(hash: string): string {
112
111
  return stripped.slice(0, 12);
113
112
  }
114
113
 
115
- function resolveTargetHashForSpace(
116
- member: ContractSpaceMember,
117
- contractHash: string,
118
- activeRefHash: string | undefined,
119
- ): string | undefined {
120
- const graph = member.graph();
121
- if (activeRefHash !== undefined && graph.nodes.has(activeRefHash)) {
122
- return activeRefHash;
123
- }
124
- if (graph.nodes.has(contractHash)) {
125
- return contractHash;
126
- }
127
- if (graph.nodes.size === 0) {
128
- return requireHeadRef(member).hash;
129
- }
130
- const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
131
- if (leaves.length === 1) {
132
- return leaves[0];
133
- }
134
- return undefined;
114
+ function resolveTarget(contractHash: string, activeRefHash: string | undefined): string {
115
+ return activeRefHash ?? contractHash;
135
116
  }
136
117
 
137
118
  function buildStatusMigrations(
@@ -146,28 +127,35 @@ function buildStatusMigrations(
146
127
 
147
128
  function renderSpaceTree(args: {
148
129
  readonly member: ContractSpaceMember;
149
- readonly contractHash: string;
130
+ readonly liveContractHash: string;
131
+ readonly migrations: readonly MigrationListEntry[];
150
132
  readonly markerHash: string | undefined;
151
133
  readonly showDbMarker: boolean;
152
- readonly targetHash: string;
153
- readonly edgeAnnotations: ReadonlyMap<string, MigrationEdgeAnnotation>;
134
+ readonly statusOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>;
154
135
  readonly colorize: boolean;
155
136
  readonly glyphMode: 'unicode' | 'ascii';
137
+ readonly globalMaxEdgeTreePrefixWidth?: number;
138
+ readonly globalMaxDirNameWidth?: number;
156
139
  }): string {
157
140
  const graph = args.member.graph();
158
141
  if (graph.nodes.size === 0) {
159
142
  return '';
160
143
  }
161
- const refsByHash = listRefsByContractHash(args.member);
162
- const rowModel = buildMigrationGraphRows(graph, { contractHash: args.contractHash });
163
- const layout = buildMigrationGraphLayout(rowModel);
164
- return renderMigrationGraphTree(layout, {
165
- refsByHash,
166
- ...(args.showDbMarker && args.markerHash !== undefined ? { dbHash: args.markerHash } : {}),
167
- contractHash: args.contractHash,
168
- edgeAnnotationsByHash: args.edgeAnnotations,
144
+ return renderMigrationGraphSpaceTree({
145
+ graph,
146
+ migrations: args.migrations,
147
+ liveContractHash: args.liveContractHash,
148
+ refsByHash: listRefsByContractHash(args.member),
149
+ statusOverlayByHash: args.statusOverlay,
169
150
  colorize: args.colorize,
170
151
  glyphMode: args.glyphMode,
152
+ ...(args.showDbMarker && args.markerHash !== undefined ? { dbHash: args.markerHash } : {}),
153
+ ...(args.globalMaxEdgeTreePrefixWidth !== undefined
154
+ ? { globalMaxEdgeTreePrefixWidth: args.globalMaxEdgeTreePrefixWidth }
155
+ : {}),
156
+ ...(args.globalMaxDirNameWidth !== undefined
157
+ ? { globalMaxDirNameWidth: args.globalMaxDirNameWidth }
158
+ : {}),
171
159
  });
172
160
  }
173
161
 
@@ -175,6 +163,27 @@ function countPending(migrations: readonly MigrationStatusMigrationEntry[]): num
175
163
  return migrations.filter((m) => m.status === 'pending').length;
176
164
  }
177
165
 
166
+ export function buildNoPathSummary(args: {
167
+ readonly markerHash: string | undefined;
168
+ readonly targetHash: string;
169
+ readonly explicitTarget: boolean;
170
+ readonly refName: string | undefined;
171
+ }): string {
172
+ const markerPart =
173
+ args.markerHash !== undefined
174
+ ? `the database state (${shortDisplayHash(args.markerHash)})`
175
+ : 'the database state';
176
+ const targetShort = shortDisplayHash(args.targetHash);
177
+ if (!args.explicitTarget) {
178
+ return `No migration path from ${markerPart} to the application's contract (${targetShort}). Run \`prisma-next migration plan --name <name>\` to author one.`;
179
+ }
180
+ const targetLabel =
181
+ args.refName !== undefined
182
+ ? `the target (${targetShort} via \`${args.refName}\`)`
183
+ : `the target (${targetShort})`;
184
+ 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.`;
185
+ }
186
+
178
187
  export function buildStatusHeadline(args: {
179
188
  readonly pendingCount: number;
180
189
  readonly targetHash: string;
@@ -185,7 +194,7 @@ export function buildStatusHeadline(args: {
185
194
  return `Database marker ${shortDisplayHash(args.markerHash)} is not in the on-disk migration graph`;
186
195
  }
187
196
  if (args.pendingCount === 0) {
188
- return 'up to date';
197
+ return 'Up to date';
189
198
  }
190
199
  return `${args.pendingCount} pending — run \`prisma-next migrate --to ${shortDisplayHash(args.targetHash)}\``;
191
200
  }
@@ -361,6 +370,15 @@ async function executeMigrationStatusCommand(
361
370
  flags,
362
371
  });
363
372
  ui.stderr(header);
373
+ if (shouldShowLegend(options, flags)) {
374
+ ui.stderr(
375
+ renderMigrationGraphLegend({
376
+ colorize: flags.color !== false,
377
+ glyphMode: ui.resolveGlyphMode(false),
378
+ }),
379
+ );
380
+ ui.stderr('');
381
+ }
364
382
  }
365
383
 
366
384
  const listSpaces = await migrationSpaceListEntriesFromAggregate(aggregate, migrationsDir);
@@ -440,7 +458,21 @@ async function executeMigrationStatusCommand(
440
458
  let markerCannotReachTarget = false;
441
459
  let headlineTargetHash = activeRefHash ?? contractHash;
442
460
  let totalPending = 0;
443
- let hasAmbiguousTarget = false;
461
+
462
+ const globalLayoutInputs = showSpaceHeadings
463
+ ? scopedSpaces
464
+ .filter((spaceEntry) => spaceEntry.migrations.length > 0)
465
+ .map((spaceEntry) => ({
466
+ graph: aggregate.space(spaceEntry.spaceId)!.graph(),
467
+ liveContractHash: contractHash,
468
+ }))
469
+ : [];
470
+ const globalMaxEdgeTreePrefixWidth =
471
+ globalLayoutInputs.length > 0
472
+ ? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs)
473
+ : undefined;
474
+ const globalMaxDirNameWidth =
475
+ globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : undefined;
444
476
 
445
477
  for (const spaceEntry of scopedSpaces) {
446
478
  const member = aggregate.space(spaceEntry.spaceId);
@@ -449,20 +481,7 @@ async function executeMigrationStatusCommand(
449
481
  }
450
482
  const graph = member.graph();
451
483
  const spaceContractHash = member.contract().storage.storageHash;
452
- const targetHash = resolveTargetHashForSpace(member, spaceContractHash, activeRefHash);
453
- if (targetHash === undefined) {
454
- hasAmbiguousTarget = true;
455
- diagnostics.push({
456
- code: 'MIGRATION.DIVERGED',
457
- severity: 'warn',
458
- message: 'There are multiple valid migration paths — you must select a target',
459
- hints: [
460
- "Use '--to <contract>' to select a target",
461
- "Or 'prisma-next ref set <name> <hash>' to create one",
462
- ],
463
- });
464
- continue;
465
- }
484
+ const targetHash = resolveTarget(spaceContractHash, activeRefHash);
466
485
  if (spaceEntry.spaceId === aggregate.app.spaceId) {
467
486
  headlineTargetHash = targetHash;
468
487
  }
@@ -477,9 +496,8 @@ async function executeMigrationStatusCommand(
477
496
  if (
478
497
  connected &&
479
498
  !usingFromOverride &&
480
- markerHash !== undefined &&
481
499
  markerInGraph &&
482
- markerHash !== targetHash &&
500
+ originHash !== targetHash &&
483
501
  findPath(graph, originHash, targetHash) === null
484
502
  ) {
485
503
  markerCannotReachTarget = true;
@@ -511,13 +529,15 @@ async function executeMigrationStatusCommand(
511
529
  });
512
530
  const tree = renderSpaceTree({
513
531
  member,
514
- contractHash: spaceContractHash,
532
+ liveContractHash: contractHash,
533
+ migrations: spaceEntry.migrations,
515
534
  markerHash,
516
535
  showDbMarker,
517
- targetHash,
518
- edgeAnnotations: annotations,
536
+ statusOverlay: annotations,
519
537
  colorize,
520
538
  glyphMode,
539
+ ...(globalMaxEdgeTreePrefixWidth !== undefined ? { globalMaxEdgeTreePrefixWidth } : {}),
540
+ ...(globalMaxDirNameWidth !== undefined ? { globalMaxDirNameWidth } : {}),
521
541
  });
522
542
  const migrations = buildStatusMigrations(spaceEntry.migrations, annotations);
523
543
  const pending = countPending(migrations);
@@ -529,9 +549,11 @@ async function executeMigrationStatusCommand(
529
549
  targetHash,
530
550
  migrations,
531
551
  });
552
+ const displayTree =
553
+ showSpaceHeadings && tree.length > 0 ? indentMigrationGraphTreeBlock(tree, ' ') : tree;
532
554
  treeSections.push({
533
555
  spaceId: spaceEntry.spaceId,
534
- tree,
556
+ tree: displayTree,
535
557
  showHeading: showSpaceHeadings,
536
558
  });
537
559
  }
@@ -567,16 +589,19 @@ async function executeMigrationStatusCommand(
567
589
  }
568
590
 
569
591
  const appMarkerHash = markersBySpace.get(aggregate.app.spaceId)?.storageHash;
570
- const summary = hasAmbiguousTarget
571
- ? 'Multiple valid migration paths — select a target with --to'
572
- : markerCannotReachTarget
573
- ? 'Database marker cannot reach the selected target'
574
- : buildStatusHeadline({
575
- pendingCount: totalPending,
576
- targetHash: headlineTargetHash,
577
- markerDiverged,
578
- markerHash: appMarkerHash,
579
- });
592
+ const summary = markerCannotReachTarget
593
+ ? buildNoPathSummary({
594
+ markerHash: appMarkerHash,
595
+ targetHash: headlineTargetHash,
596
+ explicitTarget: options.to !== undefined,
597
+ refName: activeRefName,
598
+ })
599
+ : buildStatusHeadline({
600
+ pendingCount: totalPending,
601
+ targetHash: headlineTargetHash,
602
+ markerDiverged,
603
+ markerHash: appMarkerHash,
604
+ });
580
605
 
581
606
  if (scopedSpaces.every((s) => s.migrations.length === 0)) {
582
607
  return ok({
@@ -613,6 +638,7 @@ export function createMigrationStatusCommand(): Command {
613
638
  'prisma-next migration status --db $DATABASE_URL',
614
639
  'prisma-next migration status --to production --db $DATABASE_URL',
615
640
  'prisma-next migration status --from sha256:abc --to production',
641
+ 'prisma-next migration status --legend --from sha256:abc --to production',
616
642
  ]);
617
643
  setCommandSeeAlso(command, [
618
644
  { verb: 'migration log', oneLiner: 'Show executed migration history' },
@@ -632,10 +658,16 @@ export function createMigrationStatusCommand(): Command {
632
658
  '--from <contract>',
633
659
  'Origin contract reference; same grammar as --to. Supplying --from switches to offline path computation.',
634
660
  )
661
+ .option('--legend', 'Print a key for the tree glyphs and lane colors')
635
662
  .action(async (options: MigrationStatusOptions) => {
636
663
  const flags = parseGlobalFlagsOrExit(options);
637
664
  const ui = createTerminalUI(flags);
638
665
 
666
+ const legendValidation = validateLegendOptions(options, flags);
667
+ if (!legendValidation.ok) {
668
+ process.exit(handleResult(legendValidation, flags, ui));
669
+ }
670
+
639
671
  const result = await executeMigrationStatusCommand(options, flags, ui);
640
672
 
641
673
  const exitCode = handleResult(result, flags, ui, (statusResult) => {
@@ -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
  }
@@ -395,6 +395,7 @@ export interface DbInitSuccess {
395
395
  */
396
396
  readonly perSpace?: ReadonlyArray<PerSpaceExecutionEntry>;
397
397
  readonly summary: string;
398
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
398
399
  }
399
400
 
400
401
  /**
@@ -410,6 +411,7 @@ export interface DbInitFailure {
410
411
  readonly summary: string;
411
412
  readonly why: string | undefined;
412
413
  readonly conflicts: ReadonlyArray<MigrationPlannerConflict> | undefined;
414
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
413
415
  readonly meta: Record<string, unknown> | undefined;
414
416
  readonly marker?: {
415
417
  readonly storageHash?: string;
@@ -465,6 +467,7 @@ export interface DbUpdateSuccess {
465
467
  */
466
468
  readonly perSpace?: ReadonlyArray<PerSpaceExecutionEntry>;
467
469
  readonly summary: string;
470
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
468
471
  }
469
472
 
470
473
  /**
@@ -480,6 +483,7 @@ export interface DbUpdateFailure {
480
483
  readonly summary: string;
481
484
  readonly why: string | undefined;
482
485
  readonly conflicts: ReadonlyArray<MigrationPlannerConflict> | undefined;
486
+ readonly warnings?: ReadonlyArray<MigrationPlannerConflict>;
483
487
  readonly meta: Record<string, unknown> | undefined;
484
488
  }
485
489
 
@@ -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
  }