@prisma-next/cli 0.11.0-dev.7 → 0.11.0-dev.70

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 (191) hide show
  1. package/README.md +13 -9
  2. package/dist/cli.mjs +10 -11
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-oXO2WCPD.mjs → client-6WehTnUh.mjs} +72 -60
  5. package/dist/client-6WehTnUh.mjs.map +1 -0
  6. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-CoceqqMl.mjs} +642 -45
  7. package/dist/command-helpers-CoceqqMl.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 +32 -7
  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 -4
  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 +12 -10
  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 +41 -11
  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 +6 -2
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +75 -40
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +4 -3
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +1 -280
  33. package/dist/commands/migration-graph.d.mts +13 -2
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +2 -137
  36. package/dist/commands/migration-list.d.mts +67 -4
  37. package/dist/commands/migration-list.d.mts.map +1 -1
  38. package/dist/commands/migration-list.mjs +2 -103
  39. package/dist/commands/migration-log.d.mts +10 -1
  40. package/dist/commands/migration-log.d.mts.map +1 -1
  41. package/dist/commands/migration-log.mjs +10 -15
  42. package/dist/commands/migration-log.mjs.map +1 -1
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +32 -38
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +3 -2
  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 +4 -55
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +61 -153
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +12 -49
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +85 -81
  56. package/dist/commands/migration-status.mjs.map +1 -1
  57. package/dist/commands/ref.d.mts +1 -1
  58. package/dist/commands/ref.d.mts.map +1 -1
  59. package/dist/commands/ref.mjs +38 -10
  60. package/dist/commands/ref.mjs.map +1 -1
  61. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  62. package/dist/config-loader.d.mts.map +1 -1
  63. package/dist/contract-at-errors-Bhf2jnkp.mjs +42 -0
  64. package/dist/contract-at-errors-Bhf2jnkp.mjs.map +1 -0
  65. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-C47r1loe.mjs} +4 -6
  66. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-C47r1loe.mjs.map} +1 -1
  67. package/dist/{contract-emit-o-8VmdQX.mjs → contract-emit-DxEfEc-M.mjs} +21 -7
  68. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-DxEfEc-M.mjs.map} +1 -1
  69. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-a0V5Y_mL.mjs} +4 -25
  70. package/dist/contract-enrichment-a0V5Y_mL.mjs.map +1 -0
  71. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-BLiomU8g.mjs} +3 -4
  72. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-BLiomU8g.mjs.map} +1 -1
  73. package/dist/contract-space-aggregate-loader-lafgkTwG.mjs +247 -0
  74. package/dist/contract-space-aggregate-loader-lafgkTwG.mjs.map +1 -0
  75. package/dist/{db-verify-AoIUriL4.mjs → db-verify-D44Qj3w9.mjs} +5 -7
  76. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-D44Qj3w9.mjs.map} +1 -1
  77. package/dist/exports/control-api.d.mts +3 -3
  78. package/dist/exports/control-api.d.mts.map +1 -1
  79. package/dist/exports/control-api.mjs +3 -3
  80. package/dist/exports/index.d.mts.map +1 -1
  81. package/dist/exports/index.mjs +1 -1
  82. package/dist/exports/index.mjs.map +1 -1
  83. package/dist/exports/init-output.d.mts.map +1 -1
  84. package/dist/exports/init-output.mjs +1 -1
  85. package/dist/extension-pack-inputs-IDvjRCi3.mjs +62 -0
  86. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +1 -0
  87. package/dist/{framework-components-65gOHkHB.mjs → framework-components-R_O3y5IW.mjs} +2 -2
  88. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-R_O3y5IW.mjs.map} +1 -1
  89. package/dist/global-flags-DG4uY5tV.d.mts +34 -0
  90. package/dist/global-flags-DG4uY5tV.d.mts.map +1 -0
  91. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-rFAqZujX.mjs} +2 -2
  92. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-rFAqZujX.mjs.map} +1 -1
  93. package/dist/{init-Db5Itt5r.mjs → init-DE-phHWK.mjs} +4 -5
  94. package/dist/{init-Db5Itt5r.mjs.map → init-DE-phHWK.mjs.map} +1 -1
  95. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-Ccnmg5bz.mjs} +4 -5
  96. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-Ccnmg5bz.mjs.map} +1 -1
  97. package/dist/migration-check-CKfQlAWR.mjs +341 -0
  98. package/dist/migration-check-CKfQlAWR.mjs.map +1 -0
  99. package/dist/migration-cli.d.mts.map +1 -1
  100. package/dist/migration-cli.mjs +4 -4
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-BtkunvFQ.mjs → migration-command-scaffold-C_KuV0Gm.mjs} +4 -5
  103. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-C_KuV0Gm.mjs.map} +1 -1
  104. package/dist/migration-graph-kPluRdF2.mjs +1232 -0
  105. package/dist/migration-graph-kPluRdF2.mjs.map +1 -0
  106. package/dist/migration-list-CE35R5Ag.mjs +505 -0
  107. package/dist/migration-list-CE35R5Ag.mjs.map +1 -0
  108. package/dist/migration-list-styler-DeAwACt3.mjs +402 -0
  109. package/dist/migration-list-styler-DeAwACt3.mjs.map +1 -0
  110. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-DHLa2Khm.mjs} +372 -133
  111. package/dist/migration-plan-DHLa2Khm.mjs.map +1 -0
  112. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-CAQ-0TEE.d.mts} +1 -1
  113. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-CAQ-0TEE.d.mts.map} +1 -1
  114. package/dist/{migrations-CwZMa1Ck.mjs → migrations-CjO1DsYe.mjs} +12 -13
  115. package/dist/migrations-CjO1DsYe.mjs.map +1 -0
  116. package/dist/{output-BlsrGMEF.mjs → output-CF_hqzI-.mjs} +1 -1
  117. package/dist/{output-BlsrGMEF.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
  118. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
  119. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
  120. package/dist/ref-advancement-DUZqsue6.mjs +50 -0
  121. package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
  122. package/dist/terminal-ui-BbtqsQYY.d.mts +133 -0
  123. package/dist/terminal-ui-BbtqsQYY.d.mts.map +1 -0
  124. package/dist/{types-C9FfXb1l.d.mts → types-Ci7TndCS.d.mts} +21 -28
  125. package/dist/types-Ci7TndCS.d.mts.map +1 -0
  126. package/dist/{verify-Bom75OYI.mjs → verify-vl983Ed-.mjs} +2 -2
  127. package/dist/{verify-Bom75OYI.mjs.map → verify-vl983Ed-.mjs.map} +1 -1
  128. package/package.json +19 -19
  129. package/src/commands/db-init.ts +48 -2
  130. package/src/commands/db-sign.ts +9 -5
  131. package/src/commands/db-update.ts +54 -8
  132. package/src/commands/migrate.ts +125 -44
  133. package/src/commands/migration-check.ts +43 -83
  134. package/src/commands/migration-graph.ts +75 -60
  135. package/src/commands/migration-list.ts +231 -74
  136. package/src/commands/migration-log.ts +8 -14
  137. package/src/commands/migration-new.ts +44 -48
  138. package/src/commands/migration-plan.ts +412 -197
  139. package/src/commands/migration-show.ts +65 -284
  140. package/src/commands/migration-status.ts +127 -124
  141. package/src/commands/ref.ts +53 -8
  142. package/src/control-api/client.ts +0 -1
  143. package/src/control-api/contract-enrichment.ts +6 -42
  144. package/src/control-api/operations/{apply-aggregate.ts → apply.ts} +44 -75
  145. package/src/control-api/operations/contract-emit.ts +7 -2
  146. package/src/control-api/operations/{db-apply-aggregate.ts → db-apply.ts} +19 -19
  147. package/src/control-api/operations/db-init.ts +4 -4
  148. package/src/control-api/operations/db-update.ts +4 -4
  149. package/src/control-api/operations/db-verify.ts +15 -11
  150. package/src/control-api/operations/migration-apply.ts +56 -47
  151. package/src/control-api/types.ts +19 -27
  152. package/src/migration-cli.ts +4 -4
  153. package/src/utils/cli-errors.ts +234 -0
  154. package/src/utils/command-helpers.ts +1 -20
  155. package/src/utils/contract-at-errors.ts +96 -0
  156. package/src/utils/contract-space-aggregate-loader.ts +336 -117
  157. package/src/utils/formatters/migration-graph-layout.ts +1119 -0
  158. package/src/utils/formatters/migration-graph-rows.ts +336 -0
  159. package/src/utils/formatters/migration-graph-tree-render.ts +459 -0
  160. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  161. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  162. package/src/utils/formatters/migration-list-graph-render.ts +311 -0
  163. package/src/utils/formatters/migration-list-graph-topology.ts +368 -0
  164. package/src/utils/formatters/migration-list-render.ts +191 -0
  165. package/src/utils/formatters/migration-list-styler.ts +63 -0
  166. package/src/utils/formatters/migration-list-types.ts +21 -0
  167. package/src/utils/formatters/migrations.ts +37 -46
  168. package/src/utils/glyph-mode.ts +22 -0
  169. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  170. package/src/utils/plan-resolution.ts +258 -0
  171. package/src/utils/ref-advancement.ts +68 -0
  172. package/src/utils/terminal-ui.ts +42 -1
  173. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  174. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  175. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  176. package/dist/client-oXO2WCPD.mjs.map +0 -1
  177. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  178. package/dist/commands/migration-check.mjs.map +0 -1
  179. package/dist/commands/migration-graph.mjs.map +0 -1
  180. package/dist/commands/migration-list.mjs.map +0 -1
  181. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  182. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  183. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  184. package/dist/global-flags-CdE7M0d9.d.mts +0 -15
  185. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  186. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  187. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  188. package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
  189. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  190. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  191. package/dist/types-C9FfXb1l.d.mts.map +0 -1
@@ -1,26 +1,22 @@
1
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
- import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
2
  import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
4
- import { readRefs } from '@prisma-next/migration-tools/refs';
5
- import { notOk, ok, type Result } from '@prisma-next/utils/result';
3
+ import { ok, type Result } from '@prisma-next/utils/result';
6
4
  import { Command } from 'commander';
7
5
  import { loadConfig } from '../config-loader';
8
- import {
9
- type CliStructuredError,
10
- errorUnexpected,
11
- mapMigrationToolsError,
12
- } from '../utils/cli-errors';
6
+ import type { CliStructuredError } from '../utils/cli-errors';
13
7
  import {
14
8
  addGlobalOptions,
15
- loadMigrationPackages,
16
- readContractEnvelope,
17
9
  resolveMigrationPaths,
18
10
  setCommandDescriptions,
19
11
  setCommandExamples,
20
12
  setCommandSeeAlso,
21
13
  } from '../utils/command-helpers';
14
+ import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
22
15
  import { migrationGraphToRenderInput } from '../utils/formatters/graph-migration-mapper';
23
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';
24
20
  import { formatStyledHeader } from '../utils/formatters/styled';
25
21
  import type { CommonCommandOptions } from '../utils/global-flags';
26
22
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
@@ -31,6 +27,8 @@ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
31
27
  interface MigrationGraphOptions extends CommonCommandOptions {
32
28
  readonly config?: string;
33
29
  readonly dot?: boolean;
30
+ readonly tree?: boolean;
31
+ readonly ascii?: boolean;
34
32
  }
35
33
 
36
34
  export interface MigrationGraphResult {
@@ -41,13 +39,13 @@ export interface MigrationGraphResult {
41
39
  readonly summary: string;
42
40
  }
43
41
 
44
- async function executeMigrationGraphCommand(
42
+ export async function executeMigrationGraphCommand(
45
43
  options: MigrationGraphOptions,
46
44
  flags: GlobalFlags,
47
45
  ui: TerminalUI,
48
46
  ): Promise<Result<MigrationGraphResult, CliStructuredError>> {
49
47
  const config = await loadConfig(options.config);
50
- const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } = resolveMigrationPaths(
48
+ const { configPath, appMigrationsRelative, migrationsDir } = resolveMigrationPaths(
51
49
  options.config,
52
50
  config,
53
51
  );
@@ -65,37 +63,18 @@ async function executeMigrationGraphCommand(
65
63
  ui.stderr(header);
66
64
  }
67
65
 
68
- let graph: MigrationGraph;
69
- try {
70
- ({ graph } = await loadMigrationPackages(appMigrationsDir));
71
- } catch (error) {
72
- if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
73
- return notOk(
74
- errorUnexpected(error instanceof Error ? error.message : String(error), {
75
- why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
76
- }),
77
- );
66
+ const loaded = await buildReadAggregate(config, { migrationsDir });
67
+ if (!loaded.ok) {
68
+ return loaded;
78
69
  }
79
70
 
80
- let contractHash: string | null = null;
81
- try {
82
- const envelope = await readContractEnvelope(config);
83
- contractHash = envelope.storageHash;
84
- } catch {
85
- // Contract unreadable — render graph without contract marker
86
- }
87
-
88
- let refs: readonly StatusRef[] = [];
89
- try {
90
- const allRefs = await readRefs(refsDir);
91
- refs = Object.entries(allRefs).map(([name, entry]) => ({
92
- name,
93
- hash: entry.hash,
94
- active: false,
95
- }));
96
- } catch {
97
- // Refs unreadable — render graph without ref markers
98
- }
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
+ }));
99
78
 
100
79
  return ok({
101
80
  ok: true,
@@ -111,14 +90,18 @@ export function createMigrationGraphCommand(): Command {
111
90
  setCommandDescriptions(
112
91
  command,
113
92
  'Show the migration graph topology',
114
- 'Renders the migration graph as an ASCII tree. Offline — does not\n' +
115
- 'consult the database. Use --json for machine-readable output or\n' +
116
- '--dot for Graphviz DOT format.',
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' +
97
+ 'format.',
117
98
  );
118
99
  setCommandExamples(command, [
119
100
  'prisma-next migration graph',
120
101
  'prisma-next migration graph --json',
121
102
  'prisma-next migration graph --dot',
103
+ 'prisma-next migration graph --tree',
104
+ 'prisma-next migration graph --tree --ascii',
122
105
  ]);
123
106
  setCommandSeeAlso(command, [
124
107
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
@@ -129,6 +112,8 @@ export function createMigrationGraphCommand(): Command {
129
112
  addGlobalOptions(command)
130
113
  .option('--config <path>', 'Path to prisma-next.config.ts')
131
114
  .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)')
132
117
  .action(async (options: MigrationGraphOptions) => {
133
118
  const flags = parseGlobalFlagsOrExit(options);
134
119
  const ui = createTerminalUI(flags);
@@ -160,22 +145,52 @@ export function createMigrationGraphCommand(): Command {
160
145
  JSON.stringify({ ok: true, nodes, edges, summary: graphResult.summary }, null, 2),
161
146
  );
162
147
  } else if (!flags.quiet) {
163
- const renderInput = migrationGraphToRenderInput({
164
- graph: graphResult.graph,
165
- mode: 'offline',
166
- markerHash: undefined,
167
- contractHash: graphResult.contractHash ?? EMPTY_CONTRACT_HASH,
168
- refs: graphResult.refs,
169
- activeRefHash: undefined,
170
- activeRefName: undefined,
171
- edgeStatuses: [],
172
- });
173
- const graphOutput = graphRenderer.render(renderInput.graph, {
174
- ...renderInput.options,
175
- colorize: flags.color !== false,
176
- });
177
- ui.log(graphOutput);
178
- ui.log(`\n${graphResult.summary}`);
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 like `migration list --graph`,
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
+ }
179
194
  }
180
195
  });
181
196
  process.exit(exitCode);
@@ -1,55 +1,228 @@
1
- import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
- import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
4
- import { findPath } from '@prisma-next/migration-tools/migration-graph';
1
+ import type {
2
+ ContractSpaceAggregate,
3
+ ContractSpaceMember,
4
+ } from '@prisma-next/migration-tools/aggregate';
5
+ import { HEAD_REF_NAME, refsByContractHash } from '@prisma-next/migration-tools/refs';
6
+ import {
7
+ APP_SPACE_ID,
8
+ isValidSpaceId,
9
+ listContractSpaceDirectories,
10
+ RESERVED_SPACE_SUBDIR_NAMES,
11
+ } from '@prisma-next/migration-tools/spaces';
12
+ import { ifDefined } from '@prisma-next/utils/defined';
5
13
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
6
14
  import { Command } from 'commander';
7
15
  import { loadConfig } from '../config-loader';
8
16
  import {
9
17
  type CliStructuredError,
10
- errorUnexpected,
11
- mapMigrationToolsError,
18
+ errorInvalidSpaceId,
19
+ errorSpaceNotFound,
12
20
  } from '../utils/cli-errors';
13
21
  import {
14
22
  addGlobalOptions,
15
- loadMigrationPackages,
16
23
  resolveMigrationPaths,
17
24
  setCommandDescriptions,
18
25
  setCommandExamples,
19
26
  setCommandSeeAlso,
20
27
  } from '../utils/command-helpers';
28
+ import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
29
+ import {
30
+ type GlyphMode,
31
+ renderMigrationListGraphResult,
32
+ } from '../utils/formatters/migration-list-graph-render';
33
+ import {
34
+ buildMigrationListTopologyBySpace,
35
+ renderMigrationListWithStyle,
36
+ } from '../utils/formatters/migration-list-render';
37
+ import { createAnsiMigrationListStyler } from '../utils/formatters/migration-list-styler';
38
+ import type {
39
+ MigrationListEntry,
40
+ MigrationListResult,
41
+ MigrationSpaceListEntry,
42
+ } from '../utils/formatters/migration-list-types';
21
43
  import { formatStyledHeader } from '../utils/formatters/styled';
22
44
  import type { CommonCommandOptions } from '../utils/global-flags';
23
45
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
24
46
  import { handleResult } from '../utils/result-handler';
25
47
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
26
48
 
49
+ function compareSpaceIds(a: string, b: string): number {
50
+ if (a === APP_SPACE_ID) return b === APP_SPACE_ID ? 0 : -1;
51
+ if (b === APP_SPACE_ID) return 1;
52
+ if (a < b) return -1;
53
+ if (a > b) return 1;
54
+ return 0;
55
+ }
56
+
57
+ function compareDirNamesDescending(a: MigrationListEntry, b: MigrationListEntry): number {
58
+ if (a.dirName < b.dirName) return 1;
59
+ if (a.dirName > b.dirName) return -1;
60
+ return 0;
61
+ }
62
+
63
+ /**
64
+ * Ref names decorating a space's destination contract hashes. The
65
+ * tolerant `member.refs` deliberately omits the structural `head.json`;
66
+ * for extension spaces the old enumerator surfaced it as a `head`
67
+ * decoration on the tip migration, so fold `member.headRef` back in to
68
+ * keep that output. The app space synthesises its head, so it carries
69
+ * no on-disk `head` ref to restore.
70
+ */
71
+ function listRefsByContractHash(
72
+ member: ContractSpaceMember,
73
+ ): ReadonlyMap<string, readonly string[]> {
74
+ const byHash = new Map(refsByContractHash(member.refs));
75
+ if (member.spaceId !== APP_SPACE_ID && member.headRef !== null) {
76
+ const hash = member.headRef.hash;
77
+ const bucket = byHash.get(hash) ?? [];
78
+ if (!bucket.includes(HEAD_REF_NAME)) {
79
+ byHash.set(hash, [...bucket, HEAD_REF_NAME].sort());
80
+ }
81
+ }
82
+ return byHash;
83
+ }
84
+
85
+ async function orderedOnDiskSpaceIds(projectMigrationsDir: string): Promise<readonly string[]> {
86
+ const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
87
+ return candidateDirs
88
+ .filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
89
+ .filter(isValidSpaceId)
90
+ .sort(compareSpaceIds);
91
+ }
92
+
93
+ /**
94
+ * Project the loaded {@link ContractSpaceAggregate} into the render-ready
95
+ * {@link MigrationSpaceListEntry} rows `migration list` displays.
96
+ *
97
+ * Space membership matches the on-disk contract-space directories (not the
98
+ * aggregate's always-present synthesized app member when `migrations/app/`
99
+ * is absent); package and ref data come from `aggregate.space(id)`.
100
+ */
101
+ export async function migrationSpaceListEntriesFromAggregate(
102
+ aggregate: ContractSpaceAggregate,
103
+ projectMigrationsDir: string,
104
+ ): Promise<readonly MigrationSpaceListEntry[]> {
105
+ const spaceIds = await orderedOnDiskSpaceIds(projectMigrationsDir);
106
+ const spaces: MigrationSpaceListEntry[] = [];
107
+
108
+ for (const spaceId of spaceIds) {
109
+ const member = aggregate.space(spaceId);
110
+ if (member === undefined) {
111
+ continue;
112
+ }
113
+ const refsByHash = listRefsByContractHash(member);
114
+ const migrations: MigrationListEntry[] = member.packages
115
+ .map((pkg) => ({
116
+ dirName: pkg.dirName,
117
+ from: pkg.metadata.from,
118
+ to: pkg.metadata.to,
119
+ migrationHash: pkg.metadata.migrationHash,
120
+ operationCount: pkg.ops.length,
121
+ createdAt: pkg.metadata.createdAt,
122
+ refs: refsByHash.get(pkg.metadata.to) ?? [],
123
+ providedInvariants: pkg.metadata.providedInvariants,
124
+ }))
125
+ .sort(compareDirNamesDescending);
126
+
127
+ spaces.push({ spaceId, migrations });
128
+ }
129
+
130
+ return spaces;
131
+ }
132
+
27
133
  interface MigrationListOptions extends CommonCommandOptions {
28
134
  readonly config?: string;
135
+ readonly space?: string;
136
+ readonly graph?: boolean;
137
+ readonly ascii?: boolean;
138
+ }
139
+
140
+ export interface MigrationListHumanRenderOptions {
141
+ readonly graph: boolean;
142
+ readonly glyphMode: GlyphMode;
143
+ readonly useColor: boolean;
144
+ }
145
+
146
+ export function renderMigrationListHumanOutput(
147
+ result: MigrationListResult,
148
+ options: MigrationListHumanRenderOptions,
149
+ ): string {
150
+ const styler = createAnsiMigrationListStyler({ useColor: options.useColor });
151
+ const topologyBySpaceId = buildMigrationListTopologyBySpace(result);
152
+ if (options.graph) {
153
+ return renderMigrationListGraphResult(result, styler, options.glyphMode, topologyBySpaceId);
154
+ }
155
+ return renderMigrationListWithStyle(result, styler, options.glyphMode, topologyBySpaceId);
29
156
  }
30
157
 
31
- export interface MigrationListEntry {
32
- readonly dirName: string;
33
- readonly from: string;
34
- readonly to: string;
35
- readonly migrationHash: string;
36
- readonly operationCount: number;
37
- readonly createdAt: string;
158
+ /**
159
+ * Inputs for {@link runMigrationList} — the policy core of `migration list`
160
+ * that tests exercise directly.
161
+ *
162
+ * The core does not call `loadConfig`, parse CLI flags, render a styled
163
+ * header, or write to any stream. Enumeration is supplied by the caller
164
+ * (the CLI shell builds it from {@link migrationSpaceListEntriesFromAggregate}).
165
+ */
166
+ export interface RunMigrationListInputs {
167
+ readonly spaces: readonly MigrationSpaceListEntry[];
168
+ readonly spaceFilter?: string;
38
169
  }
39
170
 
40
- export interface MigrationListResult {
41
- readonly ok: true;
42
- readonly migrations: readonly MigrationListEntry[];
43
- readonly summary: string;
171
+ function computeSummary(spaces: readonly MigrationSpaceListEntry[]): string {
172
+ const totalMigrations = spaces.reduce((count, space) => count + space.migrations.length, 0);
173
+ if (spaces.length <= 1) {
174
+ return `${totalMigrations} migration(s) on disk`;
175
+ }
176
+ return `${totalMigrations} migration(s) across ${spaces.length} contract space(s)`;
44
177
  }
45
178
 
46
- async function executeMigrationListCommand(
179
+ /**
180
+ * Policy core of `migration list`: validates `--space`, narrows the
181
+ * pre-enumerated spaces, and assembles a {@link MigrationListResult}.
182
+ *
183
+ * - `migrations/` missing or contains no valid space directories →
184
+ * caller passes `spaces: []`; this synthesizes `[{ spaceId: APP_SPACE_ID, migrations: [] }]`.
185
+ * - `--space <id>` on an existing-but-empty space → `{ spaceId, migrations: [] }` in the input.
186
+ * - `--space <id>` on a non-existent (or reserved) space → `SPACE_NOT_FOUND`.
187
+ */
188
+ export function runMigrationList(
189
+ inputs: RunMigrationListInputs,
190
+ ): Result<MigrationListResult, CliStructuredError> {
191
+ const { spaces, spaceFilter } = inputs;
192
+
193
+ if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
194
+ return notOk(errorInvalidSpaceId(spaceFilter));
195
+ }
196
+
197
+ if (spaceFilter !== undefined && !spaces.some((s) => s.spaceId === spaceFilter)) {
198
+ return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()));
199
+ }
200
+
201
+ const scopedSpaces =
202
+ spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
203
+
204
+ const resultSpaces: readonly MigrationSpaceListEntry[] =
205
+ scopedSpaces.length === 0 ? [{ spaceId: APP_SPACE_ID, migrations: [] }] : scopedSpaces;
206
+
207
+ return ok({
208
+ ok: true,
209
+ spaces: resultSpaces,
210
+ summary: computeSummary(resultSpaces),
211
+ });
212
+ }
213
+
214
+ /**
215
+ * CLI shell: loads config, resolves paths, prints the styled header on
216
+ * stderr (interactive mode only), and delegates to {@link runMigrationList}.
217
+ * Kept intentionally thin so the unit-testable surface lives in the core.
218
+ */
219
+ export async function executeMigrationListCommand(
47
220
  options: MigrationListOptions,
48
221
  flags: GlobalFlags,
49
222
  ui: TerminalUI,
50
223
  ): Promise<Result<MigrationListResult, CliStructuredError>> {
51
224
  const config = await loadConfig(options.config);
52
- const { configPath, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
225
+ const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
53
226
  options.config,
54
227
  config,
55
228
  );
@@ -57,58 +230,30 @@ async function executeMigrationListCommand(
57
230
  if (!flags.json && !flags.quiet) {
58
231
  const header = formatStyledHeader({
59
232
  command: 'migration list',
60
- description: 'List on-disk migrations in topological order',
233
+ description: 'List on-disk migrations, latest first, per contract space',
61
234
  details: [
62
235
  { label: 'config', value: configPath },
63
- { label: 'migrations', value: appMigrationsRelative },
236
+ { label: 'migrations', value: migrationsRelative },
237
+ ...(options.space !== undefined ? [{ label: 'space', value: options.space }] : []),
64
238
  ],
65
239
  flags,
66
240
  });
67
241
  ui.stderr(header);
68
242
  }
69
243
 
70
- let bundles: Awaited<ReturnType<typeof loadMigrationPackages>>['bundles'];
71
- let graph: Awaited<ReturnType<typeof loadMigrationPackages>>['graph'];
72
- try {
73
- ({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
74
- } catch (error) {
75
- if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
76
- return notOk(
77
- errorUnexpected(error instanceof Error ? error.message : String(error), {
78
- why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
79
- }),
80
- );
81
- }
82
-
83
- if (bundles.length === 0) {
84
- return ok({ ok: true, migrations: [], summary: 'No migrations found' });
244
+ const loaded = await buildReadAggregate(config, { migrationsDir });
245
+ if (!loaded.ok) {
246
+ return notOk(loaded.failure);
85
247
  }
86
248
 
87
- const leaves = [...graph.nodes].filter(
88
- (n) => !graph.forwardChain.has(n) || graph.forwardChain.get(n)!.length === 0,
249
+ const spaces = await migrationSpaceListEntriesFromAggregate(
250
+ loaded.value.aggregate,
251
+ migrationsDir,
89
252
  );
90
- const targetHash =
91
- leaves.length === 1 ? leaves[0]! : ([...graph.nodes].values().next().value as string);
92
- const chain = findPath(graph, EMPTY_CONTRACT_HASH, targetHash) ?? [];
93
-
94
- const pkgByDirName = new Map(bundles.map((p) => [p.dirName, p]));
95
- const entries: MigrationListEntry[] = chain.map((edge) => {
96
- const pkg = pkgByDirName.get(edge.dirName);
97
- const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
98
- return {
99
- dirName: edge.dirName,
100
- from: edge.from,
101
- to: edge.to,
102
- migrationHash: edge.migrationHash,
103
- operationCount: ops.length,
104
- createdAt: edge.createdAt,
105
- };
106
- });
107
253
 
108
- return ok({
109
- ok: true,
110
- migrations: entries,
111
- summary: `${entries.length} migration(s) on disk`,
254
+ return runMigrationList({
255
+ spaces,
256
+ ...ifDefined('spaceFilter', options.space),
112
257
  });
113
258
  }
114
259
 
@@ -116,11 +261,23 @@ export function createMigrationListCommand(): Command {
116
261
  const command = new Command('list');
117
262
  setCommandDescriptions(
118
263
  command,
119
- 'List on-disk migrations in topological order',
120
- 'Enumerates all migration packages under migrations/<space>/ in\n' +
121
- 'topological order. Offline — does not consult the database.',
264
+ 'List on-disk migrations, latest first, per contract space',
265
+ 'Enumerates every on-disk migration under migrations/<space>/ for every\n' +
266
+ 'contract space found on disk, latest first. Offline — does not consult\n' +
267
+ 'the database. Each row leads with a kind glyph (* forward, ↩ rollback,\n' +
268
+ '⟲ self), then dirName, then source → destination contract hashes\n' +
269
+ '(7-char git-style). Self-edges show a single hash. Invariants render as\n' +
270
+ '{...}; refs on the destination as (production, db). Pass --space <id>\n' +
271
+ 'to narrow to one contract space. --graph draws the forward spine with\n' +
272
+ 'lane gutters; --ascii forces ASCII glyphs (orthogonal to --no-color).',
122
273
  );
123
- setCommandExamples(command, ['prisma-next migration list']);
274
+ setCommandExamples(command, [
275
+ 'prisma-next migration list',
276
+ 'prisma-next migration list --graph',
277
+ 'prisma-next migration list --space app',
278
+ 'prisma-next migration list --graph --ascii',
279
+ 'prisma-next migration list --json',
280
+ ]);
124
281
  setCommandSeeAlso(command, [
125
282
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
126
283
  { verb: 'migration log', oneLiner: 'Show executed migration history' },
@@ -129,6 +286,9 @@ export function createMigrationListCommand(): Command {
129
286
  ]);
130
287
  addGlobalOptions(command)
131
288
  .option('--config <path>', 'Path to prisma-next.config.ts')
289
+ .option('--space <id>', 'Narrow output to a single contract space')
290
+ .option('--graph', 'Draw migration relationships as an annotated tree')
291
+ .option('--ascii', 'Use ASCII glyphs for --graph (pipe-friendly)')
132
292
  .action(async (options: MigrationListOptions) => {
133
293
  const flags = parseGlobalFlagsOrExit(options);
134
294
  const ui = createTerminalUI(flags);
@@ -137,16 +297,13 @@ export function createMigrationListCommand(): Command {
137
297
  if (flags.json) {
138
298
  ui.output(JSON.stringify(listResult, null, 2));
139
299
  } else if (!flags.quiet) {
140
- if (listResult.migrations.length === 0) {
141
- ui.log('No migrations found');
142
- } else {
143
- for (const entry of listResult.migrations) {
144
- ui.log(
145
- `${entry.dirName} ${entry.migrationHash.slice(0, 16)}… ${entry.operationCount} op(s)`,
146
- );
147
- }
148
- ui.log(`\n${listResult.summary}`);
149
- }
300
+ ui.output(
301
+ renderMigrationListHumanOutput(listResult, {
302
+ graph: options.graph === true,
303
+ glyphMode: ui.resolveGlyphMode(options.ascii === true),
304
+ useColor: ui.useColor,
305
+ }),
306
+ );
150
307
  }
151
308
  });
152
309
  process.exit(exitCode);
@@ -16,7 +16,6 @@ import {
16
16
  } from '../utils/cli-errors';
17
17
  import {
18
18
  addGlobalOptions,
19
- loadMigrationPackages,
20
19
  maskConnectionUrl,
21
20
  resolveMigrationPaths,
22
21
  setCommandDescriptions,
@@ -24,6 +23,7 @@ import {
24
23
  setCommandSeeAlso,
25
24
  targetSupportsMigrations,
26
25
  } from '../utils/command-helpers';
26
+ import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
27
27
  import { formatStyledHeader } from '../utils/formatters/styled';
28
28
  import type { CommonCommandOptions } from '../utils/global-flags';
29
29
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
@@ -51,13 +51,13 @@ export interface MigrationLogResult {
51
51
  readonly summary: string;
52
52
  }
53
53
 
54
- async function executeMigrationLogCommand(
54
+ export async function executeMigrationLogCommand(
55
55
  options: MigrationLogOptions,
56
56
  flags: GlobalFlags,
57
57
  ui: TerminalUI,
58
58
  ): Promise<Result<MigrationLogResult, CliStructuredError>> {
59
59
  const config = await loadConfig(options.config);
60
- const { configPath, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
60
+ const { configPath, appMigrationsRelative, migrationsDir } = resolveMigrationPaths(
61
61
  options.config,
62
62
  config,
63
63
  );
@@ -94,18 +94,12 @@ async function executeMigrationLogCommand(
94
94
  ui.stderr(header);
95
95
  }
96
96
 
97
- let bundles: Awaited<ReturnType<typeof loadMigrationPackages>>['bundles'];
98
- let graph: Awaited<ReturnType<typeof loadMigrationPackages>>['graph'];
99
- try {
100
- ({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
101
- } catch (error) {
102
- if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
103
- return notOk(
104
- errorUnexpected(error instanceof Error ? error.message : String(error), {
105
- why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
106
- }),
107
- );
97
+ const loaded = await buildReadAggregate(config, { migrationsDir });
98
+ if (!loaded.ok) {
99
+ return loaded;
108
100
  }
101
+ const graph = loaded.value.aggregate.app.graph();
102
+ const bundles = loaded.value.aggregate.app.packages;
109
103
 
110
104
  const client = createControlClient({
111
105
  family: config.family,