@prisma-next/cli 0.11.0-dev.5 → 0.11.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 (170) hide show
  1. package/dist/cli-errors-DFF1LlfU.mjs +215 -0
  2. package/dist/cli-errors-DFF1LlfU.mjs.map +1 -0
  3. package/dist/cli.mjs +9 -10
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-oXO2WCPD.mjs → client-5uvDppD8.mjs} +23 -21
  6. package/dist/client-5uvDppD8.mjs.map +1 -0
  7. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-4UNsRRc4.mjs} +427 -9
  8. package/dist/command-helpers-4UNsRRc4.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +1 -1
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +33 -7
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +3 -4
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +6 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +36 -8
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -1
  27. package/dist/commands/migrate.d.mts +5 -1
  28. package/dist/commands/migrate.d.mts.map +1 -1
  29. package/dist/commands/migrate.mjs +79 -39
  30. package/dist/commands/migrate.mjs.map +1 -1
  31. package/dist/commands/migration-check.d.mts +4 -3
  32. package/dist/commands/migration-check.d.mts.map +1 -1
  33. package/dist/commands/migration-check.mjs +1 -280
  34. package/dist/commands/migration-graph.d.mts +1 -1
  35. package/dist/commands/migration-graph.d.mts.map +1 -1
  36. package/dist/commands/migration-graph.mjs +3 -4
  37. package/dist/commands/migration-graph.mjs.map +1 -1
  38. package/dist/commands/migration-list.d.mts +63 -12
  39. package/dist/commands/migration-list.d.mts.map +1 -1
  40. package/dist/commands/migration-list.mjs +2 -103
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +3 -4
  43. package/dist/commands/migration-log.mjs.map +1 -1
  44. package/dist/commands/migration-new.d.mts.map +1 -1
  45. package/dist/commands/migration-new.mjs +33 -38
  46. package/dist/commands/migration-new.mjs.map +1 -1
  47. package/dist/commands/migration-plan.d.mts +2 -1
  48. package/dist/commands/migration-plan.d.mts.map +1 -1
  49. package/dist/commands/migration-plan.mjs +1 -1
  50. package/dist/commands/migration-show.d.mts +4 -55
  51. package/dist/commands/migration-show.d.mts.map +1 -1
  52. package/dist/commands/migration-show.mjs +62 -153
  53. package/dist/commands/migration-show.mjs.map +1 -1
  54. package/dist/commands/migration-status.d.mts +5 -40
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +93 -67
  57. package/dist/commands/migration-status.mjs.map +1 -1
  58. package/dist/commands/ref.d.mts +1 -1
  59. package/dist/commands/ref.d.mts.map +1 -1
  60. package/dist/commands/ref.mjs +34 -9
  61. package/dist/commands/ref.mjs.map +1 -1
  62. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  63. package/dist/config-loader.d.mts.map +1 -1
  64. package/dist/{contract-emit-o-8VmdQX.mjs → contract-emit-C-CFGZsI.mjs} +9 -6
  65. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-C-CFGZsI.mjs.map} +1 -1
  66. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-CuUzzM46.mjs} +5 -6
  67. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-CuUzzM46.mjs.map} +1 -1
  68. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-XmUPhmsS.mjs} +4 -25
  69. package/dist/contract-enrichment-XmUPhmsS.mjs.map +1 -0
  70. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-C98ZaRhp.mjs} +3 -4
  71. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-C98ZaRhp.mjs.map} +1 -1
  72. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs +170 -0
  73. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs.map +1 -0
  74. package/dist/{db-verify-AoIUriL4.mjs → db-verify-BWl1Yxi-.mjs} +6 -7
  75. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-BWl1Yxi-.mjs.map} +1 -1
  76. package/dist/exports/control-api.d.mts +1 -1
  77. package/dist/exports/control-api.d.mts.map +1 -1
  78. package/dist/exports/control-api.mjs +3 -3
  79. package/dist/exports/index.d.mts.map +1 -1
  80. package/dist/exports/index.mjs +1 -1
  81. package/dist/exports/index.mjs.map +1 -1
  82. package/dist/exports/init-output.d.mts.map +1 -1
  83. package/dist/exports/init-output.mjs +1 -1
  84. package/dist/extension-pack-inputs-BiY86HbQ.mjs +62 -0
  85. package/dist/extension-pack-inputs-BiY86HbQ.mjs.map +1 -0
  86. package/dist/{framework-components-65gOHkHB.mjs → framework-components-DTcjouhS.mjs} +2 -2
  87. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-DTcjouhS.mjs.map} +1 -1
  88. package/dist/{global-flags-CdE7M0d9.d.mts → global-flags-DWsQ6SSI.d.mts} +1 -1
  89. package/dist/global-flags-DWsQ6SSI.d.mts.map +1 -0
  90. package/dist/glyph-mode-CBB4emzO.d.mts +5 -0
  91. package/dist/glyph-mode-CBB4emzO.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-D2FnLpuK.mjs} +1 -1
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-D2FnLpuK.mjs.map} +1 -1
  94. package/dist/{init-Db5Itt5r.mjs → init-C7PvN163.mjs} +5 -5
  95. package/dist/{init-Db5Itt5r.mjs.map → init-C7PvN163.mjs.map} +1 -1
  96. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-BRCWQ-Sr.mjs} +5 -5
  97. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-BRCWQ-Sr.mjs.map} +1 -1
  98. package/dist/migration-check-DoskM1nB.mjs +341 -0
  99. package/dist/migration-check-DoskM1nB.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts.map +1 -1
  101. package/dist/migration-cli.mjs +4 -4
  102. package/dist/migration-cli.mjs.map +1 -1
  103. package/dist/{migration-command-scaffold-BtkunvFQ.mjs → migration-command-scaffold-CXLkoIJx.mjs} +5 -5
  104. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-CXLkoIJx.mjs.map} +1 -1
  105. package/dist/migration-list-B2-iQ5Jd.mjs +646 -0
  106. package/dist/migration-list-B2-iQ5Jd.mjs.map +1 -0
  107. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-BqmIKQpZ.mjs} +341 -88
  108. package/dist/migration-plan-BqmIKQpZ.mjs.map +1 -0
  109. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-q64xAI_J.d.mts} +1 -1
  110. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-q64xAI_J.d.mts.map} +1 -1
  111. package/dist/{migrations-CwZMa1Ck.mjs → migrations-BcVTutso.mjs} +12 -13
  112. package/dist/migrations-BcVTutso.mjs.map +1 -0
  113. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  114. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  115. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-xASh41wr.mjs} +1 -1
  116. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  117. package/dist/ref-advancement-DRh5Nquq.mjs +50 -0
  118. package/dist/ref-advancement-DRh5Nquq.mjs.map +1 -0
  119. package/dist/{types-C9FfXb1l.d.mts → types-CEtm6v6a.d.mts} +5 -11
  120. package/dist/types-CEtm6v6a.d.mts.map +1 -0
  121. package/dist/{verify-Bom75OYI.mjs → verify-DOHbbrub.mjs} +2 -2
  122. package/dist/{verify-Bom75OYI.mjs.map → verify-DOHbbrub.mjs.map} +1 -1
  123. package/package.json +20 -20
  124. package/src/commands/db-init.ts +48 -2
  125. package/src/commands/db-update.ts +45 -0
  126. package/src/commands/migrate.ts +120 -40
  127. package/src/commands/migration-check.ts +43 -83
  128. package/src/commands/migration-list.ts +173 -74
  129. package/src/commands/migration-new.ts +44 -48
  130. package/src/commands/migration-plan.ts +359 -128
  131. package/src/commands/migration-show.ts +65 -284
  132. package/src/commands/migration-status.ts +131 -99
  133. package/src/commands/ref.ts +46 -6
  134. package/src/control-api/client.ts +0 -1
  135. package/src/control-api/contract-enrichment.ts +6 -42
  136. package/src/control-api/operations/contract-emit.ts +7 -2
  137. package/src/control-api/operations/db-verify.ts +9 -5
  138. package/src/control-api/operations/migration-apply.ts +11 -19
  139. package/src/control-api/types.ts +0 -7
  140. package/src/migration-cli.ts +4 -4
  141. package/src/utils/cli-errors.ts +224 -0
  142. package/src/utils/command-helpers.ts +9 -4
  143. package/src/utils/contract-space-aggregate-loader.ts +221 -117
  144. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  145. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  146. package/src/utils/formatters/migration-list-graph-render.ts +314 -0
  147. package/src/utils/formatters/migration-list-render.ts +194 -0
  148. package/src/utils/formatters/migration-list-styler.ts +61 -0
  149. package/src/utils/formatters/migrations.ts +29 -38
  150. package/src/utils/glyph-mode.ts +22 -0
  151. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  152. package/src/utils/plan-resolution.ts +257 -0
  153. package/src/utils/ref-advancement.ts +68 -0
  154. package/src/utils/terminal-ui.ts +42 -1
  155. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  156. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  157. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  158. package/dist/client-oXO2WCPD.mjs.map +0 -1
  159. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  160. package/dist/commands/migration-check.mjs.map +0 -1
  161. package/dist/commands/migration-list.mjs.map +0 -1
  162. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  163. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  164. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  165. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  166. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  167. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  168. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  169. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  170. package/dist/types-C9FfXb1l.d.mts.map +0 -1
@@ -1,23 +1,37 @@
1
- import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
1
+ import { enumerateMigrationSpaces } from '@prisma-next/migration-tools/enumerate-migration-spaces';
3
2
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
4
- import { findPath } from '@prisma-next/migration-tools/migration-graph';
3
+ import type {
4
+ MigrationListResult,
5
+ MigrationSpaceListEntry,
6
+ } from '@prisma-next/migration-tools/migration-list-types';
7
+ import { APP_SPACE_ID, isValidSpaceId } from '@prisma-next/migration-tools/spaces';
8
+ import { ifDefined } from '@prisma-next/utils/defined';
5
9
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
6
10
  import { Command } from 'commander';
7
11
  import { loadConfig } from '../config-loader';
8
12
  import {
9
13
  type CliStructuredError,
14
+ errorInvalidSpaceId,
15
+ errorSpaceNotFound,
10
16
  errorUnexpected,
11
17
  mapMigrationToolsError,
12
18
  } from '../utils/cli-errors';
13
19
  import {
14
20
  addGlobalOptions,
15
- loadMigrationPackages,
16
21
  resolveMigrationPaths,
17
22
  setCommandDescriptions,
18
23
  setCommandExamples,
19
24
  setCommandSeeAlso,
20
25
  } from '../utils/command-helpers';
26
+ import {
27
+ type GlyphMode,
28
+ renderMigrationListGraphResult,
29
+ } from '../utils/formatters/migration-list-graph-render';
30
+ import {
31
+ buildMigrationListTopologyBySpace,
32
+ renderMigrationListWithStyle,
33
+ } from '../utils/formatters/migration-list-render';
34
+ import { createAnsiMigrationListStyler } from '../utils/formatters/migration-list-styler';
21
35
  import { formatStyledHeader } from '../utils/formatters/styled';
22
36
  import type { CommonCommandOptions } from '../utils/global-flags';
23
37
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
@@ -26,30 +40,141 @@ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
26
40
 
27
41
  interface MigrationListOptions extends CommonCommandOptions {
28
42
  readonly config?: string;
43
+ readonly space?: string;
44
+ readonly graph?: boolean;
45
+ readonly ascii?: boolean;
46
+ }
47
+
48
+ export interface MigrationListHumanRenderOptions {
49
+ readonly graph: boolean;
50
+ readonly glyphMode: GlyphMode;
51
+ readonly useColor: boolean;
52
+ }
53
+
54
+ export function renderMigrationListHumanOutput(
55
+ result: MigrationListResult,
56
+ options: MigrationListHumanRenderOptions,
57
+ ): string {
58
+ const styler = createAnsiMigrationListStyler({ useColor: options.useColor });
59
+ const topologyBySpaceId = buildMigrationListTopologyBySpace(result);
60
+ if (options.graph) {
61
+ return renderMigrationListGraphResult(result, styler, options.glyphMode, topologyBySpaceId);
62
+ }
63
+ return renderMigrationListWithStyle(result, styler, options.glyphMode, topologyBySpaceId);
29
64
  }
30
65
 
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;
66
+ /**
67
+ * Inputs for {@link runMigrationList} — the pure-ish data-and-policy core
68
+ * of `migration list` that tests exercise directly.
69
+ *
70
+ * The core depends only on the filesystem rooted at `migrationsDir`. It
71
+ * does NOT call `loadConfig`, parse CLI flags, render a styled header,
72
+ * or write to any stream. Output rendering is the caller's concern (the
73
+ * CLI shell renders via {@link renderMigrationList}; JSON callers
74
+ * serialize the {@link MigrationListResult} directly).
75
+ */
76
+ export interface RunMigrationListInputs {
77
+ /** Absolute path to the project's `migrations/` directory. */
78
+ readonly migrationsDir: string;
79
+ /**
80
+ * Optional contract-space id to narrow the result to a single space.
81
+ * Same validation rules as {@link isValidSpaceId}. When absent, every
82
+ * on-disk space contributes.
83
+ */
84
+ readonly spaceFilter?: string;
38
85
  }
39
86
 
40
- export interface MigrationListResult {
41
- readonly ok: true;
42
- readonly migrations: readonly MigrationListEntry[];
43
- readonly summary: string;
87
+ function computeSummary(spaces: readonly MigrationSpaceListEntry[]): string {
88
+ const totalMigrations = spaces.reduce((count, space) => count + space.migrations.length, 0);
89
+ if (spaces.length <= 1) {
90
+ return `${totalMigrations} migration(s) on disk`;
91
+ }
92
+ return `${totalMigrations} migration(s) across ${spaces.length} contract space(s)`;
44
93
  }
45
94
 
95
+ /**
96
+ * The unit-testable core of `migration list`. Given an absolute
97
+ * `migrationsDir` and an optional `spaceFilter`, enumerates every
98
+ * on-disk migration (via {@link enumerateMigrationSpaces}), narrows to
99
+ * the requested space if any, and assembles a {@link MigrationListResult}
100
+ * ready for the renderer or JSON serializer.
101
+ *
102
+ * The enumerator is the single source of truth for "what is a contract
103
+ * space": existence, the `--space` candidate-suggestion list, and
104
+ * scoping are all derived from one {@link enumerateMigrationSpaces}
105
+ * traversal. This means the reserved-name exclusion the enumerator
106
+ * applies (e.g. the per-space `refs/` subdirectory) is honoured here for
107
+ * free — a `--space refs` request resolves to `SPACE_NOT_FOUND`, not a
108
+ * synthesized empty-state.
109
+ *
110
+ * Distinct empty-state paths:
111
+ *
112
+ * - `migrations/` missing or contains no valid space directories →
113
+ * synthesizes `[{ spaceId: APP_SPACE_ID, migrations: [] }]` so the
114
+ * renderer's empty-state path can name a directory (spec § Empty-state +
115
+ * the `migrations/` missing edge case).
116
+ * - `--space <id>` on an existing-but-empty space dir → the enumerator
117
+ * surfaces `{ spaceId, migrations: [] }`; `<id>` is in the set, so it
118
+ * scopes to that entry and renders the empty-state the same way.
119
+ * - `--space <id>` on a non-existent (or reserved) space → structured
120
+ * `MIGRATION.SPACE_NOT_FOUND` error (NOT empty-state).
121
+ *
122
+ * Errors caught here:
123
+ *
124
+ * - {@link MigrationToolsError} from the enumerator → mapped through
125
+ * {@link mapMigrationToolsError} so callers see the catalogue code.
126
+ * - Anything else (filesystem etc.) → wrapped via {@link errorUnexpected}.
127
+ */
128
+ export async function runMigrationList(
129
+ inputs: RunMigrationListInputs,
130
+ ): Promise<Result<MigrationListResult, CliStructuredError>> {
131
+ const { migrationsDir, spaceFilter } = inputs;
132
+
133
+ if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
134
+ return notOk(errorInvalidSpaceId(spaceFilter));
135
+ }
136
+
137
+ let spaces: readonly MigrationSpaceListEntry[];
138
+ try {
139
+ spaces = await enumerateMigrationSpaces({ projectMigrationsDir: migrationsDir });
140
+ } catch (error) {
141
+ if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
142
+ return notOk(
143
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
144
+ why: `Failed to enumerate migrations: ${error instanceof Error ? error.message : String(error)}`,
145
+ }),
146
+ );
147
+ }
148
+
149
+ if (spaceFilter !== undefined && !spaces.some((s) => s.spaceId === spaceFilter)) {
150
+ return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()));
151
+ }
152
+
153
+ const scopedSpaces =
154
+ spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
155
+
156
+ const resultSpaces: readonly MigrationSpaceListEntry[] =
157
+ scopedSpaces.length === 0 ? [{ spaceId: APP_SPACE_ID, migrations: [] }] : scopedSpaces;
158
+
159
+ return ok({
160
+ ok: true,
161
+ spaces: resultSpaces,
162
+ summary: computeSummary(resultSpaces),
163
+ });
164
+ }
165
+
166
+ /**
167
+ * CLI shell: loads config, resolves paths, prints the styled header on
168
+ * stderr (interactive mode only), and delegates to {@link runMigrationList}.
169
+ * Kept intentionally thin so the unit-testable surface lives in the core.
170
+ */
46
171
  async function executeMigrationListCommand(
47
172
  options: MigrationListOptions,
48
173
  flags: GlobalFlags,
49
174
  ui: TerminalUI,
50
175
  ): Promise<Result<MigrationListResult, CliStructuredError>> {
51
176
  const config = await loadConfig(options.config);
52
- const { configPath, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
177
+ const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
53
178
  options.config,
54
179
  config,
55
180
  );
@@ -57,58 +182,20 @@ async function executeMigrationListCommand(
57
182
  if (!flags.json && !flags.quiet) {
58
183
  const header = formatStyledHeader({
59
184
  command: 'migration list',
60
- description: 'List on-disk migrations in topological order',
185
+ description: 'List on-disk migrations, latest first, per contract space',
61
186
  details: [
62
187
  { label: 'config', value: configPath },
63
- { label: 'migrations', value: appMigrationsRelative },
188
+ { label: 'migrations', value: migrationsRelative },
189
+ ...(options.space !== undefined ? [{ label: 'space', value: options.space }] : []),
64
190
  ],
65
191
  flags,
66
192
  });
67
193
  ui.stderr(header);
68
194
  }
69
195
 
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' });
85
- }
86
-
87
- const leaves = [...graph.nodes].filter(
88
- (n) => !graph.forwardChain.has(n) || graph.forwardChain.get(n)!.length === 0,
89
- );
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
-
108
- return ok({
109
- ok: true,
110
- migrations: entries,
111
- summary: `${entries.length} migration(s) on disk`,
196
+ return runMigrationList({
197
+ migrationsDir,
198
+ ...ifDefined('spaceFilter', options.space),
112
199
  });
113
200
  }
114
201
 
@@ -116,11 +203,23 @@ export function createMigrationListCommand(): Command {
116
203
  const command = new Command('list');
117
204
  setCommandDescriptions(
118
205
  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.',
206
+ 'List on-disk migrations, latest first, per contract space',
207
+ 'Enumerates every on-disk migration under migrations/<space>/ for every\n' +
208
+ 'contract space found on disk, latest first. Offline — does not consult\n' +
209
+ 'the database. Each row leads with a kind glyph (* forward, ↩ rollback,\n' +
210
+ '⟲ self), then dirName, then source → destination contract hashes\n' +
211
+ '(7-char git-style). Self-edges show a single hash. Invariants render as\n' +
212
+ '{...}; refs on the destination as (production, db). Pass --space <id>\n' +
213
+ 'to narrow to one contract space. --graph draws the forward spine with\n' +
214
+ 'lane gutters; --ascii forces ASCII glyphs (orthogonal to --no-color).',
122
215
  );
123
- setCommandExamples(command, ['prisma-next migration list']);
216
+ setCommandExamples(command, [
217
+ 'prisma-next migration list',
218
+ 'prisma-next migration list --graph',
219
+ 'prisma-next migration list --space app',
220
+ 'prisma-next migration list --graph --ascii',
221
+ 'prisma-next migration list --json',
222
+ ]);
124
223
  setCommandSeeAlso(command, [
125
224
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
126
225
  { verb: 'migration log', oneLiner: 'Show executed migration history' },
@@ -129,6 +228,9 @@ export function createMigrationListCommand(): Command {
129
228
  ]);
130
229
  addGlobalOptions(command)
131
230
  .option('--config <path>', 'Path to prisma-next.config.ts')
231
+ .option('--space <id>', 'Narrow output to a single contract space')
232
+ .option('--graph', 'Draw migration relationships as an annotated tree')
233
+ .option('--ascii', 'Use ASCII glyphs for --graph (pipe-friendly)')
132
234
  .action(async (options: MigrationListOptions) => {
133
235
  const flags = parseGlobalFlagsOrExit(options);
134
236
  const ui = createTerminalUI(flags);
@@ -137,16 +239,13 @@ export function createMigrationListCommand(): Command {
137
239
  if (flags.json) {
138
240
  ui.output(JSON.stringify(listResult, null, 2));
139
241
  } 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
- }
242
+ ui.output(
243
+ renderMigrationListHumanOutput(listResult, {
244
+ graph: options.graph === true,
245
+ glyphMode: ui.resolveGlyphMode(options.ascii === true),
246
+ useColor: ui.useColor,
247
+ }),
248
+ );
150
249
  }
151
250
  });
152
251
  process.exit(exitCode);
@@ -12,19 +12,15 @@ import { readFile } from 'node:fs/promises';
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
13
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
14
14
  import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
15
- import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
15
+ import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
16
16
  import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
17
17
  import {
18
18
  copyFilesWithRename,
19
19
  formatMigrationDirName,
20
- readMigrationsDir,
21
20
  writeMigrationPackage,
22
21
  } from '@prisma-next/migration-tools/io';
23
22
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
- import {
25
- findLatestMigration,
26
- reconstructGraph,
27
- } from '@prisma-next/migration-tools/migration-graph';
23
+ import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
28
24
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
29
25
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
30
26
  import { Command } from 'commander';
@@ -36,7 +32,6 @@ import {
36
32
  errorRuntime,
37
33
  errorTargetMigrationNotSupported,
38
34
  errorUnexpected,
39
- mapMigrationToolsError,
40
35
  } from '../utils/cli-errors';
41
36
  import {
42
37
  addGlobalOptions,
@@ -46,6 +41,7 @@ import {
46
41
  setCommandDescriptions,
47
42
  setCommandExamples,
48
43
  } from '../utils/command-helpers';
44
+ import { refusePackageCorruptionOnAggregate } from '../utils/contract-space-aggregate-loader';
49
45
  import { formatStyledHeader } from '../utils/formatters/styled';
50
46
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
51
47
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -71,7 +67,10 @@ async function executeMigrationNewCommand(
71
67
  options: MigrationNewOptions,
72
68
  ): Promise<Result<MigrationNewResult, CliStructuredError>> {
73
69
  const config = await loadConfig(options.config);
74
- const { appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
70
+ const { migrationsDir, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
71
+ options.config,
72
+ config,
73
+ );
75
74
 
76
75
  // Construct the family instance up-front so the on-disk contract read
77
76
  // below crosses the serializer seam (`familyInstance.deserializeContract`)
@@ -98,7 +97,8 @@ async function executeMigrationNewCommand(
98
97
 
99
98
  let toContract: Contract;
100
99
  try {
101
- toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
100
+ const parsedContract: unknown = JSON.parse(contractJsonContent);
101
+ toContract = familyInstance.deserializeContract(parsedContract);
102
102
  } catch (error) {
103
103
  return notOk(
104
104
  errorRuntime('Contract JSON is invalid', {
@@ -118,45 +118,47 @@ async function executeMigrationNewCommand(
118
118
  );
119
119
  }
120
120
 
121
- let fromHash: string | null = null;
122
- let fromContractSourceDir: string | null = null;
121
+ const aggregate = await loadContractSpaceAggregate({
122
+ migrationsDir,
123
+ deserializeContract: (json) => familyInstance.deserializeContract(json),
124
+ appContract: toContract,
125
+ });
126
+ const packageCorruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
127
+ if (packageCorruptionFailure) {
128
+ return notOk(packageCorruptionFailure);
129
+ }
123
130
 
124
- try {
125
- const packages = await readMigrationsDir(appMigrationsDir);
131
+ const packages = aggregate.app.packages;
132
+ const graph = aggregate.app.graph();
126
133
 
127
- if (packages.length > 0) {
128
- const graph = reconstructGraph(packages);
134
+ let fromHash: string | null = null;
135
+ let fromContractSourceDir: string | null = null;
129
136
 
130
- if (options.from) {
131
- const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
132
- if (!match) {
133
- return notOk(
134
- errorRuntime('Starting contract not found', {
135
- why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
136
- fix: 'Check that the --from hash matches a known migration target hash.',
137
- }),
138
- );
139
- }
140
- fromHash = match.metadata.to;
141
- fromContractSourceDir = match.dirPath;
142
- } else {
143
- const latestMigration = findLatestMigration(graph);
144
- if (latestMigration) {
145
- fromHash = latestMigration.to;
146
- const leafPkg = packages.find(
147
- (p) => p.metadata.migrationHash === latestMigration.migrationHash,
148
- );
149
- if (leafPkg) {
150
- fromContractSourceDir = leafPkg.dirPath;
151
- }
137
+ if (packages.length > 0) {
138
+ if (options.from) {
139
+ const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
140
+ if (!match) {
141
+ return notOk(
142
+ errorRuntime('Starting contract not found', {
143
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
144
+ fix: 'Check that the --from hash matches a known migration target hash.',
145
+ }),
146
+ );
147
+ }
148
+ fromHash = match.metadata.to;
149
+ fromContractSourceDir = match.dirPath;
150
+ } else {
151
+ const latestMigration = findLatestMigration(graph);
152
+ if (latestMigration) {
153
+ fromHash = latestMigration.to;
154
+ const leafPkg = packages.find(
155
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
156
+ );
157
+ if (leafPkg) {
158
+ fromContractSourceDir = leafPkg.dirPath;
152
159
  }
153
160
  }
154
161
  }
155
- } catch (error) {
156
- if (MigrationToolsError.is(error)) {
157
- return notOk(mapMigrationToolsError(error));
158
- }
159
- throw error;
160
162
  }
161
163
 
162
164
  if (fromHash === toStorageHash && !options.from) {
@@ -180,12 +182,6 @@ async function executeMigrationNewCommand(
180
182
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
181
183
  from: fromHash,
182
184
  to: toStorageHash,
183
- hints: {
184
- used: [],
185
- applied: [],
186
- plannerVersion: '1.0.0',
187
- },
188
- labels: [],
189
185
  providedInvariants: [],
190
186
  createdAt: timestamp.toISOString(),
191
187
  };