@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.
Files changed (214) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
  5. package/dist/client-CJzuo5wX.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
  7. package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +4 -5
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -3
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -6
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -7
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +37 -3
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +298 -12
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +55 -13
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +3 -2
  33. package/dist/commands/migration-graph.d.mts +16 -25
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +185 -2
  36. package/dist/commands/migration-graph.mjs.map +1 -0
  37. package/dist/commands/migration-list.d.mts +26 -27
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -190
  40. package/dist/commands/migration-log.d.mts +9 -19
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +1 -137
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +6 -5
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -1
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +17 -21
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +24 -36
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +42 -144
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +3 -759
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +4 -4
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/commands/telemetry/index.d.mts +7 -0
  61. package/dist/commands/telemetry/index.d.mts.map +1 -0
  62. package/dist/commands/telemetry/index.mjs +2 -0
  63. package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
  64. package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
  65. package/dist/config-loader.mjs +1 -1
  66. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
  67. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
  68. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
  69. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
  70. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
  71. package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
  72. package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
  73. package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
  74. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
  75. package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
  76. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
  77. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
  78. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +5 -3
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +3 -3
  83. package/dist/exports/index.mjs +1 -1
  84. package/dist/exports/index.mjs.map +1 -1
  85. package/dist/exports/init-output.d.mts +1 -3
  86. package/dist/exports/init-output.d.mts.map +1 -1
  87. package/dist/exports/init-output.mjs +1 -1
  88. package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
  89. package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
  90. package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
  91. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
  92. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
  93. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
  94. package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
  95. package/dist/init-0HwB-Vh8.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
  97. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
  98. package/dist/migration-check-soB5uZEQ.mjs +573 -0
  99. package/dist/migration-check-soB5uZEQ.mjs.map +1 -0
  100. package/dist/migration-cli.mjs +1 -1
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
  104. package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
  105. package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
  106. package/dist/migration-list-DlJJ_38Z.mjs +230 -0
  107. package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
  108. package/dist/migration-log-CG0qQAFm.mjs +222 -0
  109. package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
  110. package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
  111. package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
  112. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
  113. package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
  114. package/dist/migration-status-CgWSoI_g.mjs +446 -0
  115. package/dist/migration-status-CgWSoI_g.mjs.map +1 -0
  116. package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
  117. package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
  118. package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
  119. package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
  120. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
  121. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
  122. package/dist/schemas-CeGMYFYX.d.mts +191 -0
  123. package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
  124. package/dist/schemas-KhXMzNA_.mjs +112 -0
  125. package/dist/schemas-KhXMzNA_.mjs.map +1 -0
  126. package/dist/telemetry-BIM4beEO.mjs +122 -0
  127. package/dist/telemetry-BIM4beEO.mjs.map +1 -0
  128. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
  129. package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
  130. package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
  131. package/dist/types-C_tYiJYx.d.mts.map +1 -0
  132. package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
  133. package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
  134. package/package.json +26 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/contract-infer.ts +2 -2
  137. package/src/commands/db-update.ts +7 -1
  138. package/src/commands/init/index.ts +6 -35
  139. package/src/commands/init/init.ts +1 -14
  140. package/src/commands/init/inputs.ts +0 -75
  141. package/src/commands/inspect-live-schema.ts +10 -0
  142. package/src/commands/json/schemas.ts +195 -0
  143. package/src/commands/migrate.ts +527 -8
  144. package/src/commands/migration-check.ts +469 -134
  145. package/src/commands/migration-graph.ts +151 -119
  146. package/src/commands/migration-list.ts +72 -39
  147. package/src/commands/migration-log.ts +52 -102
  148. package/src/commands/migration-new.ts +2 -1
  149. package/src/commands/migration-plan.ts +2 -1
  150. package/src/commands/migration-show.ts +31 -66
  151. package/src/commands/migration-status-overlay.ts +61 -0
  152. package/src/commands/migration-status.ts +458 -1066
  153. package/src/commands/telemetry/index.ts +107 -0
  154. package/src/commands/telemetry/status.ts +67 -0
  155. package/src/control-api/client.ts +70 -9
  156. package/src/control-api/operations/contract-emit.ts +22 -2
  157. package/src/control-api/operations/db-init.ts +6 -3
  158. package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
  159. package/src/control-api/operations/db-update.ts +7 -4
  160. package/src/control-api/operations/db-verify.ts +15 -5
  161. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  162. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  163. package/src/control-api/types.ts +56 -29
  164. package/src/utils/cli-errors.ts +70 -2
  165. package/src/utils/formatters/errors.ts +11 -0
  166. package/src/utils/formatters/migration-graph-command-render.ts +239 -0
  167. package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
  168. package/src/utils/formatters/migration-graph-labels.ts +408 -0
  169. package/src/utils/formatters/migration-graph-model.ts +103 -0
  170. package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
  171. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  172. package/src/utils/formatters/migration-graph-space-render.ts +188 -0
  173. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  174. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  175. package/src/utils/formatters/migration-list-render.ts +135 -71
  176. package/src/utils/formatters/migration-list-styler.ts +46 -5
  177. package/src/utils/formatters/migration-list-types.ts +5 -21
  178. package/src/utils/formatters/migration-log-table.ts +205 -0
  179. package/src/utils/formatters/migrations.ts +33 -11
  180. package/src/utils/global-flags.ts +35 -0
  181. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  182. package/src/utils/legend.ts +38 -0
  183. package/src/utils/migration-path-target.ts +60 -0
  184. package/src/utils/telemetry.ts +68 -32
  185. package/dist/client-KgJorIvG.mjs.map +0 -1
  186. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  187. package/dist/commands/migration-list.mjs.map +0 -1
  188. package/dist/commands/migration-log.mjs.map +0 -1
  189. package/dist/commands/migration-status.mjs.map +0 -1
  190. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  191. package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
  192. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  193. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  194. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  195. package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
  196. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  197. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  198. package/dist/migration-graph-C9WC-7eO.mjs +0 -1478
  199. package/dist/migration-graph-C9WC-7eO.mjs.map +0 -1
  200. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  201. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  202. package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
  203. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  204. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  205. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  206. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  207. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  208. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  209. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  210. package/src/utils/formatters/graph-render.ts +0 -1323
  211. package/src/utils/formatters/graph-types.ts +0 -120
  212. package/src/utils/formatters/migration-graph-lane-colors.ts +0 -31
  213. package/src/utils/formatters/migration-graph-layout.ts +0 -1141
  214. package/src/utils/formatters/migration-graph-tree-render.ts +0 -768
@@ -1,5 +1,6 @@
1
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
2
  import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
3
4
  import { ok, type Result } from '@prisma-next/utils/result';
4
5
  import { Command } from 'commander';
5
6
  import { loadConfig } from '../config-loader';
@@ -12,69 +13,80 @@ import {
12
13
  setCommandSeeAlso,
13
14
  } from '../utils/command-helpers';
14
15
  import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
15
- import { migrationGraphToRenderInput } from '../utils/formatters/graph-migration-mapper';
16
- import { graphRenderer } from '../utils/formatters/graph-render';
17
- import { buildMigrationGraphLayout } from '../utils/formatters/migration-graph-layout';
18
- import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
16
+ import { renderMigrationGraphLegend } from '../utils/formatters/migration-graph-labels';
19
17
  import {
20
- renderMigrationGraphLegend,
21
- renderMigrationGraphTree,
22
- } from '../utils/formatters/migration-graph-tree-render';
18
+ computeGlobalMaxDirNameWidth,
19
+ computeGlobalMaxEdgeTreePrefixWidth,
20
+ indentMigrationGraphTreeBlock,
21
+ renderMigrationGraphSpaceTree,
22
+ } from '../utils/formatters/migration-graph-space-render';
23
23
  import { formatStyledHeader } from '../utils/formatters/styled';
24
24
  import type { CommonCommandOptions } from '../utils/global-flags';
25
25
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
26
- import type { StatusRef } from '../utils/migration-types';
26
+ import { shouldShowLegend, validateLegendOptions } from '../utils/legend';
27
27
  import { handleResult } from '../utils/result-handler';
28
28
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
29
+ import type { MigrationGraphJsonResult, MigrationSpaceGraphEntry } from './json/schemas';
30
+ import {
31
+ listRefsByContractHash,
32
+ migrationSpaceListEntriesFromAggregate,
33
+ runMigrationList,
34
+ } from './migration-list';
29
35
 
30
36
  interface MigrationGraphOptions extends CommonCommandOptions {
31
37
  readonly config?: string;
32
38
  readonly dot?: boolean;
33
- readonly tree?: boolean;
39
+ readonly space?: string;
34
40
  readonly ascii?: boolean;
35
41
  readonly legend?: boolean;
36
42
  }
37
43
 
38
- /**
39
- * `--legend` describes the `--tree` visual language, so passing it auto-enables
40
- * the tree path (it has nothing to say about the legacy dagre default).
41
- */
42
- export function migrationGraphUsesTree(options: {
43
- readonly tree?: boolean;
44
- readonly legend?: boolean;
45
- }): boolean {
46
- return options.tree === true || options.legend === true;
47
- }
48
-
49
- /**
50
- * The legend is decoration printed alongside the command header on stderr, so
51
- * it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
52
- * `--quiet`) exactly as the header is.
53
- */
54
- export function migrationGraphShowsLegend(
55
- options: { readonly legend?: boolean; readonly dot?: boolean },
56
- flags: GlobalFlags,
57
- ): boolean {
58
- return (
59
- options.legend === true && options.dot !== true && flags.json !== true && flags.quiet !== true
60
- );
44
+ export interface MigrationGraphTreeSection {
45
+ readonly space: string;
46
+ readonly tree: string;
47
+ readonly showHeading: boolean;
61
48
  }
62
49
 
63
50
  export interface MigrationGraphResult {
64
51
  readonly ok: true;
52
+ /** App-space graph for the `--dot` Graphviz output. */
65
53
  readonly graph: MigrationGraph;
66
- readonly contractHash: string | null;
67
- readonly refs: readonly StatusRef[];
54
+ /** Nested per-space contracts + migrations for `--json`. */
55
+ readonly spaces: readonly MigrationSpaceGraphEntry[];
56
+ readonly treeSections: readonly MigrationGraphTreeSection[];
68
57
  readonly summary: string;
69
58
  }
70
59
 
60
+ function computeGraphSummary(spaces: readonly MigrationSpaceGraphEntry[]): string {
61
+ const contractCount = spaces.reduce((count, space) => count + space.contracts.length, 0);
62
+ const migrationCount = spaces.reduce((count, space) => count + space.migrations.length, 0);
63
+ return `${spaces.length} space(s), ${contractCount} contract(s), ${migrationCount} migration(s)`;
64
+ }
65
+
66
+ export function formatMigrationGraphHumanOutput(result: MigrationGraphResult): string {
67
+ const sections: string[] = [];
68
+ for (const section of result.treeSections) {
69
+ if (section.showHeading) {
70
+ sections.push(`${section.space}:`);
71
+ }
72
+ if (section.tree.length > 0) {
73
+ sections.push(section.tree);
74
+ } else {
75
+ sections.push('(no migrations)');
76
+ }
77
+ sections.push('');
78
+ }
79
+ sections.push(result.summary);
80
+ return sections.join('\n').trimEnd();
81
+ }
82
+
71
83
  export async function executeMigrationGraphCommand(
72
84
  options: MigrationGraphOptions,
73
85
  flags: GlobalFlags,
74
86
  ui: TerminalUI,
75
87
  ): Promise<Result<MigrationGraphResult, CliStructuredError>> {
76
88
  const config = await loadConfig(options.config);
77
- const { configPath, appMigrationsRelative, migrationsDir } = resolveMigrationPaths(
89
+ const { configPath, migrationsRelative, migrationsDir } = resolveMigrationPaths(
78
90
  options.config,
79
91
  config,
80
92
  );
@@ -85,19 +97,19 @@ export async function executeMigrationGraphCommand(
85
97
  description: 'Show the migration graph topology',
86
98
  details: [
87
99
  { label: 'config', value: configPath },
88
- { label: 'migrations', value: appMigrationsRelative },
100
+ { label: 'migrations', value: migrationsRelative },
101
+ ...(options.space !== undefined ? [{ label: 'space', value: options.space }] : []),
89
102
  ],
90
103
  flags,
91
104
  });
92
105
  ui.stderr(header);
93
- if (migrationGraphShowsLegend(options, flags)) {
106
+ if (shouldShowLegend(options, flags)) {
94
107
  ui.stderr(
95
108
  renderMigrationGraphLegend({
96
109
  colorize: flags.color !== false,
97
110
  glyphMode: ui.resolveGlyphMode(options.ascii === true),
98
111
  }),
99
112
  );
100
- // Blank line separating the stderr key from the graph that follows on stdout.
101
113
  ui.stderr('');
102
114
  }
103
115
  }
@@ -107,20 +119,90 @@ export async function executeMigrationGraphCommand(
107
119
  return loaded;
108
120
  }
109
121
 
110
- const { aggregate, contractHash } = loaded.value;
111
- const graph = aggregate.app.graph();
112
- const refs: readonly StatusRef[] = Object.entries(aggregate.app.refs).map(([name, entry]) => ({
113
- name,
114
- hash: entry.hash,
115
- active: false,
116
- }));
122
+ const { aggregate, contractHash: liveContractHash } = loaded.value;
123
+ const appGraph = aggregate.app.graph();
124
+
125
+ const listSpaces = await migrationSpaceListEntriesFromAggregate(aggregate, migrationsDir);
126
+ const listResult = runMigrationList({
127
+ spaces: listSpaces,
128
+ ...ifDefined('spaceFilter', options.space),
129
+ });
130
+ if (!listResult.ok) {
131
+ return listResult;
132
+ }
133
+
134
+ const scopedSpaces = listResult.value.spaces;
135
+ const showSpaceHeadings = scopedSpaces.length > 1;
136
+ const glyphMode = ui.resolveGlyphMode(options.ascii === true);
137
+ const colorize = flags.color !== false;
138
+
139
+ const globalLayoutInputs = showSpaceHeadings
140
+ ? scopedSpaces
141
+ .filter((spaceEntry) => spaceEntry.migrations.length > 0)
142
+ .map((spaceEntry) => ({
143
+ graph: aggregate.space(spaceEntry.space)!.graph(),
144
+ liveContractHash,
145
+ }))
146
+ : [];
147
+ const globalMaxEdgeTreePrefixWidth =
148
+ globalLayoutInputs.length > 0
149
+ ? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs)
150
+ : undefined;
151
+ const globalMaxDirNameWidth =
152
+ globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : undefined;
153
+
154
+ const treeSections: MigrationGraphTreeSection[] = [];
155
+ const spaces: MigrationSpaceGraphEntry[] = [];
156
+ for (const spaceEntry of scopedSpaces) {
157
+ const member = aggregate.space(spaceEntry.space);
158
+ if (member === undefined) {
159
+ continue;
160
+ }
161
+ const graph = member.graph();
162
+ const isAppSpace = spaceEntry.space === aggregate.app.spaceId;
163
+ const refsByHash = listRefsByContractHash(member);
164
+ const tree =
165
+ spaceEntry.migrations.length === 0
166
+ ? ''
167
+ : renderMigrationGraphSpaceTree({
168
+ graph,
169
+ migrations: spaceEntry.migrations,
170
+ liveContractHash,
171
+ glyphMode,
172
+ colorize,
173
+ isAppSpace,
174
+ refsByHash,
175
+ ...(globalMaxEdgeTreePrefixWidth !== undefined ? { globalMaxEdgeTreePrefixWidth } : {}),
176
+ ...(globalMaxDirNameWidth !== undefined ? { globalMaxDirNameWidth } : {}),
177
+ });
178
+ const displayTree =
179
+ showSpaceHeadings && tree.length > 0 ? indentMigrationGraphTreeBlock(tree, ' ') : tree;
180
+ treeSections.push({
181
+ space: spaceEntry.space,
182
+ tree: displayTree,
183
+ showHeading: showSpaceHeadings,
184
+ });
185
+ spaces.push({
186
+ space: spaceEntry.space,
187
+ contracts: [...graph.nodes].map((hash) => ({
188
+ hash,
189
+ refs: [...(refsByHash.get(hash) ?? [])],
190
+ })),
191
+ migrations: [...graph.migrationByHash.values()].map((edge) => ({
192
+ name: edge.dirName,
193
+ hash: edge.migrationHash,
194
+ fromContract: edge.from === EMPTY_CONTRACT_HASH ? null : edge.from,
195
+ toContract: edge.to,
196
+ })),
197
+ });
198
+ }
117
199
 
118
200
  return ok({
119
201
  ok: true,
120
- graph,
121
- contractHash,
122
- refs,
123
- summary: `${graph.nodes.size} node(s), ${graph.migrationByHash.size} edge(s)`,
202
+ graph: appGraph,
203
+ spaces,
204
+ treeSections,
205
+ summary: computeGraphSummary(spaces),
124
206
  });
125
207
  }
126
208
 
@@ -129,19 +211,19 @@ export function createMigrationGraphCommand(): Command {
129
211
  setCommandDescriptions(
130
212
  command,
131
213
  'Show the migration graph topology',
132
- 'Renders the migration graph topology. Offline — does not consult\n' +
133
- 'the database. Use --tree for the condensed annotated tree\n' +
134
- '(--ascii swaps box-drawing for pipe-friendly ASCII glyphs),\n' +
135
- '--json for machine-readable output, or --dot for Graphviz DOT\n' +
214
+ 'Renders the migration graph topology.\n' +
215
+ 'Offline does not consult the database.\n' +
216
+ '--ascii swaps box-drawing for pipe-friendly ASCII glyphs.\n' +
217
+ 'Use --json for machine-readable output, or --dot for Graphviz DOT\n' +
136
218
  'format.',
137
219
  );
138
220
  setCommandExamples(command, [
139
221
  'prisma-next migration graph',
140
222
  'prisma-next migration graph --json',
141
223
  'prisma-next migration graph --dot',
142
- 'prisma-next migration graph --tree',
143
- 'prisma-next migration graph --tree --ascii',
224
+ 'prisma-next migration graph --ascii',
144
225
  'prisma-next migration graph --legend',
226
+ 'prisma-next migration graph --space app',
145
227
  ]);
146
228
  setCommandSeeAlso(command, [
147
229
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
@@ -151,20 +233,19 @@ export function createMigrationGraphCommand(): Command {
151
233
  ]);
152
234
  addGlobalOptions(command)
153
235
  .option('--config <path>', 'Path to prisma-next.config.ts')
236
+ .option('--space <id>', 'Narrow output to a single contract space')
154
237
  .option('--dot', 'Output in Graphviz DOT format')
155
- .option('--tree', 'Experimental condensed annotated tree renderer')
156
- .option('--ascii', 'Use ASCII glyphs for --tree (pipe-friendly)')
157
- .option('--legend', 'Print a key for the --tree glyphs and lane colors (implies --tree)')
238
+ .option('--ascii', 'Use ASCII glyphs (pipe-friendly)')
239
+ .option('--legend', 'Print a key for the tree glyphs and lane colors')
158
240
  .action(async (options: MigrationGraphOptions) => {
159
241
  const flags = parseGlobalFlagsOrExit(options);
160
242
  const ui = createTerminalUI(flags);
243
+ const legendValidation = validateLegendOptions(options, flags);
244
+ if (!legendValidation.ok) {
245
+ process.exit(handleResult(legendValidation, flags, ui));
246
+ }
161
247
  const result = await executeMigrationGraphCommand(options, flags, ui);
162
248
  const exitCode = handleResult(result, flags, ui, (graphResult) => {
163
- // Explicit format flags win over the auto-JSON default. `flags.json`
164
- // is auto-enabled when stdout is non-TTY (per CLI Style Guide §
165
- // JSON Semantics); without this ordering, `migration graph --dot |
166
- // dot -Tsvg` pipes JSON into the GraphViz binary, which then
167
- // errors. `--dot` is the more specific instruction; honour it.
168
249
  if (options.dot) {
169
250
  const lines = ['digraph migrations {'];
170
251
  for (const edge of graphResult.graph.migrationByHash.values()) {
@@ -175,63 +256,14 @@ export function createMigrationGraphCommand(): Command {
175
256
  lines.push('}');
176
257
  ui.output(lines.join('\n'));
177
258
  } else if (flags.json) {
178
- const nodes = [...graphResult.graph.nodes];
179
- const edges = [...graphResult.graph.migrationByHash.values()].map((e) => ({
180
- dirName: e.dirName,
181
- from: e.from,
182
- to: e.to,
183
- migrationHash: e.migrationHash,
184
- }));
185
- ui.output(
186
- JSON.stringify({ ok: true, nodes, edges, summary: graphResult.summary }, null, 2),
187
- );
259
+ const jsonResult: MigrationGraphJsonResult = {
260
+ ok: true,
261
+ spaces: [...graphResult.spaces],
262
+ summary: graphResult.summary,
263
+ };
264
+ ui.output(JSON.stringify(jsonResult, null, 2));
188
265
  } else if (!flags.quiet) {
189
- if (migrationGraphUsesTree(options)) {
190
- const refsByHash = new Map<string, string[]>();
191
- for (const ref of graphResult.refs) {
192
- const existing = refsByHash.get(ref.hash);
193
- refsByHash.set(ref.hash, existing ? [...existing, ref.name] : [ref.name]);
194
- }
195
- const rowModel = buildMigrationGraphRows(graphResult.graph, {
196
- ...(graphResult.contractHash !== null
197
- ? { contractHash: graphResult.contractHash }
198
- : {}),
199
- });
200
- const layout = buildMigrationGraphLayout(rowModel);
201
- const activeRef = graphResult.refs.find((ref) => ref.active);
202
- const treeOutput = renderMigrationGraphTree(layout, {
203
- refsByHash,
204
- ...(graphResult.contractHash !== null
205
- ? { contractHash: graphResult.contractHash }
206
- : {}),
207
- ...(activeRef !== undefined ? { activeRefName: activeRef.name } : {}),
208
- colorize: flags.color !== false,
209
- glyphMode: ui.resolveGlyphMode(options.ascii === true),
210
- });
211
- // Emit the rendered tree to stdout (same stream as flat `migration list`),
212
- // not through clack's `log.message` rail: the graph is the command's
213
- // result (and its own box-drawing is the only vertical structure it
214
- // should carry), not a status line that needs the prompt gutter.
215
- ui.output(treeOutput);
216
- ui.output(`\n${graphResult.summary}`);
217
- } else {
218
- const renderInput = migrationGraphToRenderInput({
219
- graph: graphResult.graph,
220
- mode: 'offline',
221
- markerHash: undefined,
222
- contractHash: graphResult.contractHash ?? EMPTY_CONTRACT_HASH,
223
- refs: graphResult.refs,
224
- activeRefHash: undefined,
225
- activeRefName: undefined,
226
- edgeStatuses: [],
227
- });
228
- const graphOutput = graphRenderer.render(renderInput.graph, {
229
- ...renderInput.options,
230
- colorize: flags.color !== false,
231
- });
232
- ui.log(graphOutput);
233
- ui.log(`\n${graphResult.summary}`);
234
- }
266
+ ui.output(formatMigrationGraphHumanOutput(graphResult));
235
267
  }
236
268
  });
237
269
  process.exit(exitCode);
@@ -2,6 +2,7 @@ import type {
2
2
  ContractSpaceAggregate,
3
3
  ContractSpaceMember,
4
4
  } from '@prisma-next/migration-tools/aggregate';
5
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
5
6
  import { HEAD_REF_NAME, refsByContractHash } from '@prisma-next/migration-tools/refs';
6
7
  import {
7
8
  APP_SPACE_ID,
@@ -26,10 +27,8 @@ import {
26
27
  setCommandSeeAlso,
27
28
  } from '../utils/command-helpers';
28
29
  import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
29
- import {
30
- buildMigrationListTopologyBySpace,
31
- renderMigrationListWithStyle,
32
- } from '../utils/formatters/migration-list-render';
30
+ import { renderMigrationGraphLegend } from '../utils/formatters/migration-graph-labels';
31
+ import { renderMigrationListWithStyle } from '../utils/formatters/migration-list-render';
33
32
  import { createAnsiMigrationListStyler } from '../utils/formatters/migration-list-styler';
34
33
  import type {
35
34
  MigrationListEntry,
@@ -40,6 +39,7 @@ import { formatStyledHeader } from '../utils/formatters/styled';
40
39
  import type { CommonCommandOptions } from '../utils/global-flags';
41
40
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
42
41
  import type { GlyphMode } from '../utils/glyph-mode';
42
+ import { shouldShowLegend, validateLegendOptions } from '../utils/legend';
43
43
  import { handleResult } from '../utils/result-handler';
44
44
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
45
45
 
@@ -52,8 +52,8 @@ function compareSpaceIds(a: string, b: string): number {
52
52
  }
53
53
 
54
54
  function compareDirNamesDescending(a: MigrationListEntry, b: MigrationListEntry): number {
55
- if (a.dirName < b.dirName) return 1;
56
- if (a.dirName > b.dirName) return -1;
55
+ if (a.name < b.name) return 1;
56
+ if (a.name > b.name) return -1;
57
57
  return 0;
58
58
  }
59
59
 
@@ -65,7 +65,7 @@ function compareDirNamesDescending(a: MigrationListEntry, b: MigrationListEntry)
65
65
  * keep that output. The app space synthesises its head, so it carries
66
66
  * no on-disk `head` ref to restore.
67
67
  */
68
- function listRefsByContractHash(
68
+ export function listRefsByContractHash(
69
69
  member: ContractSpaceMember,
70
70
  ): ReadonlyMap<string, readonly string[]> {
71
71
  const byHash = new Map(refsByContractHash(member.refs));
@@ -110,18 +110,18 @@ export async function migrationSpaceListEntriesFromAggregate(
110
110
  const refsByHash = listRefsByContractHash(member);
111
111
  const migrations: MigrationListEntry[] = member.packages
112
112
  .map((pkg) => ({
113
- dirName: pkg.dirName,
114
- from: pkg.metadata.from,
115
- to: pkg.metadata.to,
116
- migrationHash: pkg.metadata.migrationHash,
113
+ name: pkg.dirName,
114
+ hash: pkg.metadata.migrationHash,
115
+ fromContract: pkg.metadata.from,
116
+ toContract: pkg.metadata.to,
117
117
  operationCount: pkg.ops.length,
118
118
  createdAt: pkg.metadata.createdAt,
119
- refs: refsByHash.get(pkg.metadata.to) ?? [],
120
- providedInvariants: pkg.metadata.providedInvariants,
119
+ refs: [...(refsByHash.get(pkg.metadata.to) ?? [])],
120
+ providedInvariants: [...pkg.metadata.providedInvariants],
121
121
  }))
122
122
  .sort(compareDirNamesDescending);
123
123
 
124
- spaces.push({ spaceId, migrations });
124
+ spaces.push({ space: spaceId, migrations });
125
125
  }
126
126
 
127
127
  return spaces;
@@ -131,11 +131,21 @@ interface MigrationListOptions extends CommonCommandOptions {
131
131
  readonly config?: string;
132
132
  readonly space?: string;
133
133
  readonly ascii?: boolean;
134
+ readonly legend?: boolean;
135
+ }
136
+
137
+ export interface MigrationListExecuteResult {
138
+ readonly list: MigrationListResult;
139
+ readonly liveContractHash: string;
140
+ readonly aggregate: ContractSpaceAggregate;
134
141
  }
135
142
 
136
143
  export interface MigrationListHumanRenderOptions {
137
144
  readonly glyphMode: GlyphMode;
138
145
  readonly useColor: boolean;
146
+ readonly liveContractHash: string;
147
+ readonly graphForSpace: (spaceId: string) => MigrationGraph | undefined;
148
+ readonly appSpaceId?: string;
139
149
  }
140
150
 
141
151
  export function renderMigrationListHumanOutput(
@@ -143,8 +153,12 @@ export function renderMigrationListHumanOutput(
143
153
  options: MigrationListHumanRenderOptions,
144
154
  ): string {
145
155
  const styler = createAnsiMigrationListStyler({ useColor: options.useColor });
146
- const topologyBySpaceId = buildMigrationListTopologyBySpace(result);
147
- return renderMigrationListWithStyle(result, styler, options.glyphMode, topologyBySpaceId);
156
+ return renderMigrationListWithStyle(result, styler, options.glyphMode, {
157
+ colorize: options.useColor,
158
+ liveContractHash: options.liveContractHash,
159
+ graphForSpace: options.graphForSpace,
160
+ ...(options.appSpaceId !== undefined ? { appSpaceId: options.appSpaceId } : {}),
161
+ });
148
162
  }
149
163
 
150
164
  /**
@@ -186,19 +200,19 @@ export function runMigrationList(
186
200
  return notOk(errorInvalidSpaceId(spaceFilter));
187
201
  }
188
202
 
189
- if (spaceFilter !== undefined && !spaces.some((s) => s.spaceId === spaceFilter)) {
190
- return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()));
203
+ if (spaceFilter !== undefined && !spaces.some((s) => s.space === spaceFilter)) {
204
+ return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.space).sort()));
191
205
  }
192
206
 
193
207
  const scopedSpaces =
194
- spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
208
+ spaceFilter !== undefined ? spaces.filter((s) => s.space === spaceFilter) : spaces;
195
209
 
196
210
  const resultSpaces: readonly MigrationSpaceListEntry[] =
197
- scopedSpaces.length === 0 ? [{ spaceId: APP_SPACE_ID, migrations: [] }] : scopedSpaces;
211
+ scopedSpaces.length === 0 ? [{ space: APP_SPACE_ID, migrations: [] }] : scopedSpaces;
198
212
 
199
213
  return ok({
200
214
  ok: true,
201
- spaces: resultSpaces,
215
+ spaces: [...resultSpaces],
202
216
  summary: computeSummary(resultSpaces),
203
217
  });
204
218
  }
@@ -212,7 +226,7 @@ export async function executeMigrationListCommand(
212
226
  options: MigrationListOptions,
213
227
  flags: GlobalFlags,
214
228
  ui: TerminalUI,
215
- ): Promise<Result<MigrationListResult, CliStructuredError>> {
229
+ ): Promise<Result<MigrationListExecuteResult, CliStructuredError>> {
216
230
  const config = await loadConfig(options.config);
217
231
  const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
218
232
  options.config,
@@ -222,7 +236,7 @@ export async function executeMigrationListCommand(
222
236
  if (!flags.json && !flags.quiet) {
223
237
  const header = formatStyledHeader({
224
238
  command: 'migration list',
225
- description: 'List on-disk migrations, latest first, per contract space',
239
+ description: 'List on-disk migrations per contract space',
226
240
  details: [
227
241
  { label: 'config', value: configPath },
228
242
  { label: 'migrations', value: migrationsRelative },
@@ -231,6 +245,15 @@ export async function executeMigrationListCommand(
231
245
  flags,
232
246
  });
233
247
  ui.stderr(header);
248
+ if (shouldShowLegend(options, flags)) {
249
+ ui.stderr(
250
+ renderMigrationGraphLegend({
251
+ colorize: flags.color !== false,
252
+ glyphMode: ui.resolveGlyphMode(options.ascii === true),
253
+ }),
254
+ );
255
+ ui.stderr('');
256
+ }
234
257
  }
235
258
 
236
259
  const loaded = await buildReadAggregate(config, { migrationsDir });
@@ -238,35 +261,37 @@ export async function executeMigrationListCommand(
238
261
  return notOk(loaded.failure);
239
262
  }
240
263
 
241
- const spaces = await migrationSpaceListEntriesFromAggregate(
242
- loaded.value.aggregate,
243
- migrationsDir,
244
- );
264
+ const { aggregate, contractHash: liveContractHash } = loaded.value;
245
265
 
246
- return runMigrationList({
266
+ const spaces = await migrationSpaceListEntriesFromAggregate(aggregate, migrationsDir);
267
+
268
+ const listResult = runMigrationList({
247
269
  spaces,
248
270
  ...ifDefined('spaceFilter', options.space),
249
271
  });
272
+ if (!listResult.ok) {
273
+ return listResult;
274
+ }
275
+ return ok({ list: listResult.value, liveContractHash, aggregate });
250
276
  }
251
277
 
252
278
  export function createMigrationListCommand(): Command {
253
279
  const command = new Command('list');
254
280
  setCommandDescriptions(
255
281
  command,
256
- 'List on-disk migrations, latest first, per contract space',
282
+ 'List on-disk migrations per contract space',
257
283
  'Enumerates every on-disk migration under migrations/<space>/ for every\n' +
258
- 'contract space found on disk, latest first. Offline — does not consult\n' +
259
- 'the database. Each row leads with a kind glyph (* forward, ↩ rollback,\n' +
260
- ' self), then dirName, then source destination contract hashes\n' +
261
- '(7-char git-style). Self-edges show a single hash. Invariants render as\n' +
262
- '{...}; refs on the destination as (production, db). Pass --space <id>\n' +
263
- 'to narrow to one contract space. --ascii forces ASCII kind glyphs\n' +
264
- '(orthogonal to --no-color).',
284
+ 'contract space found on disk. Offline — does not consult the database.\n' +
285
+ 'Human output draws the shared migration graph tree with operation counts,\n' +
286
+ 'invariants on each migration row, and refs on destination contract nodes.\n' +
287
+ 'Pass --space <id> to narrow to one contract space. --ascii forces ASCII\n' +
288
+ 'tree glyphs (orthogonal to --no-color).',
265
289
  );
266
290
  setCommandExamples(command, [
267
291
  'prisma-next migration list',
268
292
  'prisma-next migration list --space app',
269
293
  'prisma-next migration list --ascii',
294
+ 'prisma-next migration list --legend',
270
295
  'prisma-next migration list --json',
271
296
  ]);
272
297
  setCommandSeeAlso(command, [
@@ -279,18 +304,26 @@ export function createMigrationListCommand(): Command {
279
304
  .option('--config <path>', 'Path to prisma-next.config.ts')
280
305
  .option('--space <id>', 'Narrow output to a single contract space')
281
306
  .option('--ascii', 'Use ASCII kind glyphs (pipe-friendly)')
307
+ .option('--legend', 'Print a key for the tree glyphs and lane colors')
282
308
  .action(async (options: MigrationListOptions) => {
283
309
  const flags = parseGlobalFlagsOrExit(options);
284
310
  const ui = createTerminalUI(flags);
311
+ const legendValidation = validateLegendOptions(options, flags);
312
+ if (!legendValidation.ok) {
313
+ process.exit(handleResult(legendValidation, flags, ui));
314
+ }
285
315
  const result = await executeMigrationListCommand(options, flags, ui);
286
- const exitCode = handleResult(result, flags, ui, (listResult) => {
316
+ const exitCode = handleResult(result, flags, ui, ({ list, liveContractHash, aggregate }) => {
287
317
  if (flags.json) {
288
- ui.output(JSON.stringify(listResult, null, 2));
318
+ ui.output(JSON.stringify(list, null, 2));
289
319
  } else if (!flags.quiet) {
290
320
  ui.output(
291
- renderMigrationListHumanOutput(listResult, {
321
+ renderMigrationListHumanOutput(list, {
292
322
  glyphMode: ui.resolveGlyphMode(options.ascii === true),
293
323
  useColor: ui.useColor,
324
+ liveContractHash,
325
+ graphForSpace: (spaceId) => aggregate.space(spaceId)?.graph(),
326
+ appSpaceId: aggregate.app.spaceId,
294
327
  }),
295
328
  );
296
329
  }