@prisma-next/cli 0.12.0-dev.5 → 0.12.0-dev.50

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