@prisma-next/cli 0.5.0-dev.9 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +61 -26
  2. package/dist/cli-errors-B9OBbled.d.mts +3 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  4. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-BCnP7cHo.mjs +1485 -0
  8. package/dist/client-BCnP7cHo.mjs.map +1 -0
  9. package/dist/{result-handler-Ba3zWQsI.mjs → command-helpers-BeZHkxV8.mjs} +70 -47
  10. package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -4
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -4
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +16 -13
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.d.mts.map +1 -1
  19. package/dist/commands/db-schema.mjs +6 -7
  20. package/dist/commands/db-schema.mjs.map +1 -1
  21. package/dist/commands/db-sign.d.mts.map +1 -1
  22. package/dist/commands/db-sign.mjs +9 -9
  23. package/dist/commands/db-sign.mjs.map +1 -1
  24. package/dist/commands/db-update.d.mts.map +1 -1
  25. package/dist/commands/db-update.mjs +15 -13
  26. package/dist/commands/db-update.mjs.map +1 -1
  27. package/dist/commands/db-verify.d.mts.map +1 -1
  28. package/dist/commands/db-verify.mjs +1 -321
  29. package/dist/commands/migration-apply.d.mts +28 -13
  30. package/dist/commands/migration-apply.d.mts.map +1 -1
  31. package/dist/commands/migration-apply.mjs +55 -151
  32. package/dist/commands/migration-apply.mjs.map +1 -1
  33. package/dist/commands/migration-new.d.mts +0 -1
  34. package/dist/commands/migration-new.d.mts.map +1 -1
  35. package/dist/commands/migration-new.mjs +34 -40
  36. package/dist/commands/migration-new.mjs.map +1 -1
  37. package/dist/commands/migration-plan.d.mts +33 -6
  38. package/dist/commands/migration-plan.d.mts.map +1 -1
  39. package/dist/commands/migration-plan.mjs +2 -348
  40. package/dist/commands/migration-ref.d.mts +1 -1
  41. package/dist/commands/migration-ref.d.mts.map +1 -1
  42. package/dist/commands/migration-ref.mjs +8 -12
  43. package/dist/commands/migration-ref.mjs.map +1 -1
  44. package/dist/commands/migration-show.d.mts +64 -10
  45. package/dist/commands/migration-show.d.mts.map +1 -1
  46. package/dist/commands/migration-show.mjs +166 -60
  47. package/dist/commands/migration-show.mjs.map +1 -1
  48. package/dist/commands/migration-status.d.mts +126 -5
  49. package/dist/commands/migration-status.d.mts.map +1 -1
  50. package/dist/commands/migration-status.mjs +2 -4
  51. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  52. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  53. package/dist/config-loader.d.mts +0 -1
  54. package/dist/config-loader.d.mts.map +1 -1
  55. package/dist/config-loader.mjs +2 -3
  56. package/dist/contract-emit-9DBda5Ou.mjs +150 -0
  57. package/dist/contract-emit-9DBda5Ou.mjs.map +1 -0
  58. package/dist/contract-emit-B77TsJqf.mjs +327 -0
  59. package/dist/contract-emit-B77TsJqf.mjs.map +1 -0
  60. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-Dani0mMW.mjs} +4 -6
  61. package/dist/contract-enrichment-Dani0mMW.mjs.map +1 -0
  62. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-ByxhPjpW.mjs} +13 -22
  63. package/dist/contract-infer-ByxhPjpW.mjs.map +1 -0
  64. package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs +160 -0
  65. package/dist/contract-space-aggregate-loader-BrwKK6Q6.mjs.map +1 -0
  66. package/dist/db-verify-Czm5T-J4.mjs +404 -0
  67. package/dist/db-verify-Czm5T-J4.mjs.map +1 -0
  68. package/dist/exports/config-types.mjs +1 -2
  69. package/dist/exports/control-api.d.mts +101 -586
  70. package/dist/exports/control-api.d.mts.map +1 -1
  71. package/dist/exports/control-api.mjs +4 -6
  72. package/dist/exports/index.d.mts.map +1 -1
  73. package/dist/exports/index.mjs +28 -30
  74. package/dist/exports/index.mjs.map +1 -1
  75. package/dist/exports/init-output.d.mts +2 -4
  76. package/dist/exports/init-output.d.mts.map +1 -1
  77. package/dist/exports/init-output.mjs +2 -3
  78. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-ChqVUxR-.mjs} +3 -4
  79. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
  80. package/dist/global-flags-Icqpxk23.d.mts +12 -0
  81. package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
  82. package/dist/helpers-eqdN8tH6.mjs +25 -0
  83. package/dist/helpers-eqdN8tH6.mjs.map +1 -0
  84. package/dist/{init-C5220SY9.mjs → init-DETSgw3h.mjs} +40 -49
  85. package/dist/init-DETSgw3h.mjs.map +1 -0
  86. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-DxdBd4Er.mjs} +10 -11
  87. package/dist/inspect-live-schema-DxdBd4Er.mjs.map +1 -0
  88. package/dist/migration-cli.d.mts +41 -12
  89. package/dist/migration-cli.d.mts.map +1 -1
  90. package/dist/migration-cli.mjs +309 -86
  91. package/dist/migration-cli.mjs.map +1 -1
  92. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-BdV8JYXV.mjs} +8 -9
  93. package/dist/migration-command-scaffold-BdV8JYXV.mjs.map +1 -0
  94. package/dist/migration-plan-mRu5K81L.mjs +494 -0
  95. package/dist/migration-plan-mRu5K81L.mjs.map +1 -0
  96. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-By9G5p2H.mjs} +270 -65
  97. package/dist/migration-status-By9G5p2H.mjs.map +1 -0
  98. package/dist/migrations-CTsyBXCA.mjs +229 -0
  99. package/dist/migrations-CTsyBXCA.mjs.map +1 -0
  100. package/dist/{output-BpcQrnnq.mjs → output-B16Kefzx.mjs} +9 -3
  101. package/dist/output-B16Kefzx.mjs.map +1 -0
  102. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DFfvZcYL.mjs} +2 -2
  103. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
  104. package/dist/result-handler-rmPVKIP2.mjs +25 -0
  105. package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
  106. package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
  107. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-C_hFNbAn.mjs} +4 -28
  108. package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
  109. package/dist/types-LItU7E4l.d.mts +856 -0
  110. package/dist/types-LItU7E4l.d.mts.map +1 -0
  111. package/dist/{verify-Bkycc-Tf.mjs → verify-CiwNWM9N.mjs} +3 -4
  112. package/dist/verify-CiwNWM9N.mjs.map +1 -0
  113. package/package.json +28 -26
  114. package/src/cli.ts +32 -6
  115. package/src/commands/contract-emit.ts +67 -163
  116. package/src/commands/contract-infer.ts +7 -20
  117. package/src/commands/db-init.ts +15 -3
  118. package/src/commands/db-update.ts +9 -4
  119. package/src/commands/db-verify.ts +47 -15
  120. package/src/commands/init/index.ts +1 -1
  121. package/src/commands/init/init.ts +2 -2
  122. package/src/commands/init/templates/code-templates.ts +26 -18
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +114 -212
  125. package/src/commands/migration-new.ts +42 -45
  126. package/src/commands/migration-plan.ts +213 -75
  127. package/src/commands/migration-ref.ts +8 -7
  128. package/src/commands/migration-show.ts +274 -70
  129. package/src/commands/migration-status.ts +491 -64
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +85 -5
  132. package/src/control-api/contract-enrichment.ts +6 -4
  133. package/src/control-api/operations/apply-aggregate.ts +290 -0
  134. package/src/control-api/operations/contract-emit.ts +198 -115
  135. package/src/control-api/operations/db-apply-aggregate.ts +399 -0
  136. package/src/control-api/operations/db-init.ts +51 -253
  137. package/src/control-api/operations/db-update.ts +66 -183
  138. package/src/control-api/operations/db-verify.ts +342 -0
  139. package/src/control-api/operations/migration-apply.ts +430 -131
  140. package/src/control-api/types.ts +278 -29
  141. package/src/exports/control-api.ts +15 -3
  142. package/src/load-ts-contract.ts +28 -26
  143. package/src/migration-cli.ts +445 -122
  144. package/src/utils/cli-errors.ts +49 -2
  145. package/src/utils/combine-schema-results.ts +84 -0
  146. package/src/utils/command-helpers.ts +69 -25
  147. package/src/utils/contract-space-aggregate-loader.ts +177 -0
  148. package/src/utils/contract-space-seed-phase.ts +201 -0
  149. package/src/utils/emit-queue.ts +26 -0
  150. package/src/utils/extension-pack-inputs.ts +162 -0
  151. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  152. package/src/utils/formatters/migrations.ts +255 -77
  153. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  154. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  155. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  156. package/dist/client-CrsnY58k.mjs +0 -997
  157. package/dist/client-CrsnY58k.mjs.map +0 -1
  158. package/dist/commands/db-verify.mjs.map +0 -1
  159. package/dist/commands/migration-plan.mjs.map +0 -1
  160. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  161. package/dist/contract-emit--feXyNd7.mjs +0 -4
  162. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  163. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  164. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  165. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  166. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  167. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  168. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  169. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  170. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  171. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  172. package/dist/init-C5220SY9.mjs.map +0 -1
  173. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  174. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  175. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  176. package/dist/migrations-Bo5WtTla.mjs +0 -153
  177. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  178. package/dist/output-BpcQrnnq.mjs.map +0 -1
  179. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  180. package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
  181. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  182. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  183. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  184. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  185. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,19 +1,40 @@
1
- import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
- import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import {
4
+ createControlStack,
5
+ type MigrationPlanOperation,
6
+ type OperationPreview,
7
+ } from '@prisma-next/framework-components/control';
8
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
9
  import { readMigrationPackage, readMigrationsDir } from '@prisma-next/migration-tools/io';
4
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
5
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
10
+ import {
11
+ findLatestMigration,
12
+ reconstructGraph,
13
+ } from '@prisma-next/migration-tools/migration-graph';
14
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
15
+ import { spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
16
+ import { ifDefined } from '@prisma-next/utils/defined';
6
17
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
7
18
  import { Command } from 'commander';
8
- import { relative, resolve } from 'pathe';
19
+ import { isAbsolute, relative, resolve } from 'pathe';
9
20
  import { loadConfig } from '../config-loader';
10
- import { extractOperationStatements } from '../control-api/operations/extract-operation-statements';
11
- import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
21
+ import { createControlClient } from '../control-api/client';
22
+ import {
23
+ type CliStructuredError,
24
+ errorContractValidationFailed,
25
+ errorFileNotFound,
26
+ errorRuntime,
27
+ errorUnexpected,
28
+ mapMigrationToolsError,
29
+ } from '../utils/cli-errors';
12
30
  import {
13
31
  addGlobalOptions,
32
+ resolveContractPath,
33
+ resolveMigrationPaths,
14
34
  setCommandDescriptions,
15
35
  setCommandExamples,
16
36
  } from '../utils/command-helpers';
37
+ import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
17
38
  import { formatMigrationShowOutput } from '../utils/formatters/migrations';
18
39
  import { formatStyledHeader } from '../utils/formatters/styled';
19
40
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -25,34 +46,102 @@ interface MigrationShowOptions extends CommonCommandOptions {
25
46
  readonly config?: string;
26
47
  }
27
48
 
28
- export interface MigrationShowResult {
29
- readonly ok: true;
49
+ /**
50
+ * Details of one space's latest (or targeted) migration package.
51
+ */
52
+ export interface MigrationShowSpacePresent {
53
+ readonly kind: 'present';
54
+ readonly spaceId: string;
30
55
  readonly dirName: string;
31
56
  readonly dirPath: string;
32
- readonly from: string;
57
+ readonly from: string | null;
33
58
  readonly to: string;
34
- readonly migrationId: string;
35
- readonly kind: string;
59
+ readonly migrationHash: string;
36
60
  readonly createdAt: string;
37
61
  readonly operations: readonly {
38
62
  readonly id: string;
39
63
  readonly label: string;
40
64
  readonly operationClass: string;
41
65
  }[];
42
- readonly sql: readonly string[];
66
+ /**
67
+ * Family-agnostic textual preview of the migration's operations. Always
68
+ * defined; statements is empty for a no-op migration or a family that does
69
+ * not implement the `OperationPreviewCapable` capability.
70
+ */
71
+ readonly preview: OperationPreview;
43
72
  readonly summary: string;
44
73
  }
45
74
 
75
+ /**
76
+ * Placeholder for a loaded contract space that has no on-disk migration
77
+ * package — the extension descriptor declared the space but no migrations
78
+ * directory has been materialised for it yet. Surfaces the space in the
79
+ * response so JSON consumers see every loaded extension instead of having
80
+ * silently-skipped entries.
81
+ */
82
+ export interface MigrationShowSpaceMissing {
83
+ readonly kind: 'missing';
84
+ readonly spaceId: string;
85
+ readonly summary: string;
86
+ }
87
+
88
+ export type MigrationShowSpaceResult = MigrationShowSpacePresent | MigrationShowSpaceMissing;
89
+
90
+ export interface MigrationShowResult {
91
+ readonly ok: true;
92
+ /**
93
+ * Per-space results, ordered: app first, then extensions alphabetically
94
+ * (matching the aggregate's canonical ordering).
95
+ */
96
+ readonly spaces: readonly MigrationShowSpaceResult[];
97
+ }
98
+
46
99
  function looksLikePath(target: string): boolean {
47
100
  return target.includes('/') || target.includes('\\');
48
101
  }
49
102
 
103
+ /**
104
+ * Validate that a path-like `migration show` target resolves inside the app
105
+ * migrations directory. The returned result is always emitted under
106
+ * `aggregate.app.spaceId`, so accepting an extension-space (or otherwise
107
+ * external) path here would silently mislabel the result. Returns the
108
+ * resolved absolute path on success.
109
+ *
110
+ * `pathe.relative` can return an absolute path when the target cannot be
111
+ * expressed relative to the base (e.g. on Windows when `target` is on a
112
+ * different drive than `appMigrationsDir`). That case does not start with
113
+ * `..`, so the absolute-check below is required to reject cross-drive
114
+ * targets rather than mislabeling them as app-space.
115
+ */
116
+ export function resolveAppTargetPath(
117
+ target: string,
118
+ appMigrationsDir: string,
119
+ appMigrationsRelative: string,
120
+ ): Result<string, CliStructuredError> {
121
+ const targetPath = resolve(target);
122
+ const relativeToApp = relative(appMigrationsDir, targetPath);
123
+ const isOutsideAppDir =
124
+ relativeToApp === '' ||
125
+ relativeToApp === '.' ||
126
+ relativeToApp.startsWith('..') ||
127
+ isAbsolute(relativeToApp);
128
+ if (isOutsideAppDir) {
129
+ return notOk(
130
+ errorRuntime('Target must point to an app-space migration', {
131
+ why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
132
+ fix: 'Pass an app-space migration directory or use a hash prefix.',
133
+ }),
134
+ );
135
+ }
136
+ return ok(targetPath);
137
+ }
138
+
50
139
  export function resolveByHashPrefix(
51
- packages: readonly MigrationBundle[],
140
+ packages: readonly OnDiskMigrationPackage[],
52
141
  prefix: string,
53
- ): Result<MigrationBundle, CliStructuredError> {
142
+ ): Result<OnDiskMigrationPackage, CliStructuredError> {
54
143
  const normalizedPrefix = prefix.startsWith('sha256:') ? prefix : `sha256:${prefix}`;
55
- const matches = packages.filter((p) => p.manifest.migrationId.startsWith(normalizedPrefix));
144
+ const matches = packages.filter((p) => p.metadata.migrationHash.startsWith(normalizedPrefix));
56
145
 
57
146
  if (matches.length === 1) {
58
147
  return ok(matches[0]!);
@@ -61,13 +150,13 @@ export function resolveByHashPrefix(
61
150
  if (matches.length === 0) {
62
151
  return notOk(
63
152
  errorRuntime('No migration found matching prefix', {
64
- why: `No migration has a migrationId starting with "${normalizedPrefix}"`,
153
+ why: `No migration has a migrationHash starting with "${normalizedPrefix}"`,
65
154
  fix: 'Run `prisma-next migration show` (no argument) to see the latest migration, or check the migrations directory for available packages.',
66
155
  }),
67
156
  );
68
157
  }
69
158
 
70
- const candidates = matches.map((p) => ` ${p.dirName} ${p.manifest.migrationId}`).join('\n');
159
+ const candidates = matches.map((p) => ` ${p.dirName} ${p.metadata.migrationHash}`).join('\n');
71
160
  return notOk(
72
161
  errorRuntime('Ambiguous hash prefix', {
73
162
  why: `Multiple migrations match prefix "${normalizedPrefix}":\n${candidates}`,
@@ -76,6 +165,71 @@ export function resolveByHashPrefix(
76
165
  );
77
166
  }
78
167
 
168
+ /**
169
+ * Resolve the latest migration from a space directory.
170
+ *
171
+ * Returns `ok(null)` only when the directory is empty or absent (ENOENT is
172
+ * absorbed by `readMigrationsDir`). If `readMigrationsDir` returned packages
173
+ * but `findLatestMigration` cannot pick a leaf, the on-disk history is
174
+ * corrupt — return a runtime error rather than collapsing it to a `missing`
175
+ * placeholder, which would hide the corruption from the caller.
176
+ */
177
+ export async function resolveLatestFromDir(
178
+ spaceDir: string,
179
+ ): Promise<Result<OnDiskMigrationPackage | null, CliStructuredError>> {
180
+ try {
181
+ const allPackages = await readMigrationsDir(spaceDir);
182
+ if (allPackages.length === 0) return ok(null);
183
+ const graph = reconstructGraph(allPackages);
184
+ const latestMigration = findLatestMigration(graph);
185
+ if (!latestMigration) {
186
+ return notOk(
187
+ errorRuntime('Could not resolve latest migration', {
188
+ why: `No latest migration found in ${relative(process.cwd(), spaceDir)}`,
189
+ fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
190
+ }),
191
+ );
192
+ }
193
+ const leafPkg = allPackages.find(
194
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
195
+ );
196
+ return ok(leafPkg ?? null);
197
+ } catch (error) {
198
+ if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
199
+ return notOk(
200
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
201
+ why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
202
+ }),
203
+ );
204
+ }
205
+ }
206
+
207
+ function pkgToSpaceResult(
208
+ spaceId: string,
209
+ pkg: OnDiskMigrationPackage,
210
+ client: ReturnType<typeof createControlClient>,
211
+ ): MigrationShowSpacePresent {
212
+ const ops = pkg.ops as readonly MigrationPlanOperation[];
213
+ const preview: OperationPreview = client.toOperationPreview(ops) ?? { statements: [] };
214
+ return {
215
+ kind: 'present',
216
+ spaceId,
217
+ dirName: pkg.dirName,
218
+ dirPath: relative(process.cwd(), pkg.dirPath),
219
+ from: pkg.metadata.from,
220
+ to: pkg.metadata.to,
221
+ migrationHash: pkg.metadata.migrationHash,
222
+ createdAt: pkg.metadata.createdAt,
223
+ operations: ops.map((op) => ({
224
+ id: op.id,
225
+ label: op.label,
226
+ operationClass: op.operationClass,
227
+ })),
228
+ preview,
229
+ summary: `${ops.length} operation(s)`,
230
+ };
231
+ }
232
+
79
233
  async function executeMigrationShowCommand(
80
234
  target: string | undefined,
81
235
  options: MigrationShowOptions,
@@ -83,20 +237,17 @@ async function executeMigrationShowCommand(
83
237
  ui: TerminalUI,
84
238
  ): Promise<Result<MigrationShowResult, CliStructuredError>> {
85
239
  const config = await loadConfig(options.config);
86
- const configPath = options.config
87
- ? relative(process.cwd(), resolve(options.config))
88
- : 'prisma-next.config.ts';
240
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
241
+ resolveMigrationPaths(options.config, config);
89
242
 
90
- const migrationsDir = resolve(
91
- options.config ? resolve(options.config, '..') : process.cwd(),
92
- config.migrations?.dir ?? 'migrations',
93
- );
94
- const migrationsRelative = relative(process.cwd(), migrationsDir);
243
+ const contractPathAbsolute = resolveContractPath(config);
244
+ const contractPath = relative(process.cwd(), contractPathAbsolute);
95
245
 
96
246
  if (!flags.json && !flags.quiet) {
97
247
  const details: Array<{ label: string; value: string }> = [
98
248
  { label: 'config', value: configPath },
99
- { label: 'migrations', value: migrationsRelative },
249
+ { label: 'contract', value: contractPath },
250
+ { label: 'migrations', value: appMigrationsRelative },
100
251
  ];
101
252
  if (target) {
102
253
  details.push({ label: 'target', value: target });
@@ -110,26 +261,87 @@ async function executeMigrationShowCommand(
110
261
  ui.stderr(header);
111
262
  }
112
263
 
113
- let pkg: MigrationBundle;
264
+ // Load the app contract so the aggregate loader can validate it.
265
+ let contractJsonContent: string;
266
+ try {
267
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
268
+ } catch (error) {
269
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
270
+ return notOk(
271
+ errorFileNotFound(contractPathAbsolute, {
272
+ why: `Contract file not found at ${contractPathAbsolute}`,
273
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}`,
274
+ }),
275
+ );
276
+ }
277
+ return notOk(
278
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
279
+ why: 'Failed to read contract file',
280
+ }),
281
+ );
282
+ }
283
+
284
+ let appContract: Contract;
285
+ try {
286
+ appContract = JSON.parse(contractJsonContent) as Contract;
287
+ } catch (error) {
288
+ return notOk(
289
+ errorContractValidationFailed(
290
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
291
+ { where: { path: contractPathAbsolute } },
292
+ ),
293
+ );
294
+ }
295
+
296
+ // Build the aggregate against current disk state to enumerate all spaces.
297
+ const stack = createControlStack(config);
298
+ const familyInstance = config.family.create(stack);
299
+ const aggregateResult = await buildContractSpaceAggregate({
300
+ targetId: config.target.targetId,
301
+ migrationsDir,
302
+ appContract,
303
+ extensionPacks: config.extensionPacks ?? [],
304
+ validateContract: (json: unknown) => familyInstance.validateContract(json),
305
+ });
306
+ if (!aggregateResult.ok) {
307
+ return notOk(aggregateResult.failure);
308
+ }
309
+ const aggregate = aggregateResult.value;
310
+
311
+ // `migration show` is an offline command; the control client is constructed
312
+ // purely to dispatch the family-specific `toOperationPreview` capability and
313
+ // is not connected to a database.
314
+ const client = createControlClient({
315
+ family: config.family,
316
+ target: config.target,
317
+ adapter: config.adapter,
318
+ ...ifDefined('driver', config.driver),
319
+ extensionPacks: config.extensionPacks ?? [],
320
+ });
321
+
322
+ const spaces: MigrationShowSpaceResult[] = [];
114
323
 
324
+ // App space: honour the `target` argument (path or hash prefix) when provided.
115
325
  try {
326
+ let appPkg: OnDiskMigrationPackage;
116
327
  if (target && looksLikePath(target)) {
117
- pkg = await readMigrationPackage(resolve(target));
328
+ const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
329
+ if (!resolved.ok) return resolved;
330
+ appPkg = await readMigrationPackage(resolved.value);
118
331
  } else {
119
- const allPackages = await readMigrationsDir(migrationsDir);
332
+ const allPackages = await readMigrationsDir(appMigrationsDir);
120
333
  if (allPackages.length === 0) {
121
334
  return notOk(
122
335
  errorRuntime('No migrations found', {
123
- why: `No migration packages found in ${migrationsRelative}`,
336
+ why: `No migration packages found in ${appMigrationsRelative}`,
124
337
  fix: 'Run `prisma-next migration plan` to create a migration first.',
125
338
  }),
126
339
  );
127
340
  }
128
-
129
341
  if (target) {
130
342
  const resolved = resolveByHashPrefix(allPackages, target);
131
343
  if (!resolved.ok) return resolved;
132
- pkg = resolved.value;
344
+ appPkg = resolved.value;
133
345
  } else {
134
346
  const graph = reconstructGraph(allPackages);
135
347
  const latestMigration = findLatestMigration(graph);
@@ -142,7 +354,7 @@ async function executeMigrationShowCommand(
142
354
  );
143
355
  }
144
356
  const leafPkg = allPackages.find(
145
- (p) => p.manifest.migrationId === latestMigration.migrationId,
357
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
146
358
  );
147
359
  if (!leafPkg) {
148
360
  return notOk(
@@ -152,47 +364,42 @@ async function executeMigrationShowCommand(
152
364
  }),
153
365
  );
154
366
  }
155
- pkg = leafPkg;
367
+ appPkg = leafPkg;
156
368
  }
157
369
  }
370
+ spaces.push(pkgToSpaceResult(aggregate.app.spaceId, appPkg, client));
158
371
  } catch (error) {
159
372
  if (MigrationToolsError.is(error)) {
160
- return notOk(
161
- errorRuntime(error.message, {
162
- why: error.why,
163
- fix: error.fix,
164
- meta: { code: error.code, ...(error.details ?? {}) },
165
- }),
166
- );
373
+ return notOk(mapMigrationToolsError(error));
167
374
  }
168
375
  return notOk(
169
376
  errorUnexpected(error instanceof Error ? error.message : String(error), {
170
- why: `Failed to read migration: ${error instanceof Error ? error.message : String(error)}`,
377
+ why: `Failed to read app-space migration: ${error instanceof Error ? error.message : String(error)}`,
171
378
  }),
172
379
  );
173
380
  }
174
381
 
175
- const ops = pkg.ops as readonly MigrationPlanOperation[];
176
- const sql = extractOperationStatements(config.family.familyId, ops) ?? [];
382
+ // Extension spaces: always emit one entry per loaded extension so the
383
+ // response enumerates every space the aggregate knows about. Spaces
384
+ // with no on-disk migration package yet (e.g. an extension was declared
385
+ // but never `migrate`d) become `kind: 'missing'` placeholders instead
386
+ // of being silently skipped.
387
+ for (const ext of aggregate.extensions) {
388
+ const extSpaceDir = spaceMigrationDirectory(migrationsDir, ext.spaceId);
389
+ const extPkgResult = await resolveLatestFromDir(extSpaceDir);
390
+ if (!extPkgResult.ok) return extPkgResult;
391
+ if (extPkgResult.value !== null) {
392
+ spaces.push(pkgToSpaceResult(ext.spaceId, extPkgResult.value, client));
393
+ } else {
394
+ spaces.push({
395
+ kind: 'missing',
396
+ spaceId: ext.spaceId,
397
+ summary: 'No on-disk migration package for this space',
398
+ });
399
+ }
400
+ }
177
401
 
178
- const result: MigrationShowResult = {
179
- ok: true,
180
- dirName: pkg.dirName,
181
- dirPath: relative(process.cwd(), pkg.dirPath),
182
- from: pkg.manifest.from,
183
- to: pkg.manifest.to,
184
- migrationId: pkg.manifest.migrationId,
185
- kind: pkg.manifest.kind,
186
- createdAt: pkg.manifest.createdAt,
187
- operations: ops.map((op) => ({
188
- id: op.id,
189
- label: op.label,
190
- operationClass: op.operationClass,
191
- })),
192
- sql,
193
- summary: `${ops.length} operation(s)`,
194
- };
195
- return ok(result);
402
+ return ok({ ok: true, spaces });
196
403
  }
197
404
 
198
405
  export function createMigrationShowCommand(): Command {
@@ -200,19 +407,16 @@ export function createMigrationShowCommand(): Command {
200
407
  setCommandDescriptions(
201
408
  command,
202
409
  'Display migration package contents',
203
- 'Shows the operations, DDL preview, and metadata for a migration package.\n' +
204
- 'Accepts a directory path, a hash prefix (git-style), or defaults to the\n' +
205
- 'latest migration.',
410
+ 'Shows the operations, statement preview, and metadata for every loaded contract\n' +
411
+ 'space (app + extensions). Accepts a directory path or hash prefix to target a\n' +
412
+ 'specific app-space migration; defaults to the latest per space.',
206
413
  );
207
414
  setCommandExamples(command, [
208
415
  'prisma-next migration show',
209
416
  'prisma-next migration show sha256:a1b2c3',
210
417
  ]);
211
418
  addGlobalOptions(command)
212
- .argument(
213
- '[target]',
214
- 'Migration directory path or migrationId hash prefix (defaults to latest)',
215
- )
419
+ .argument('[target]', 'App-space migration path or migrationHash prefix (defaults to latest)')
216
420
  .option('--config <path>', 'Path to prisma-next.config.ts')
217
421
  .action(async (target: string | undefined, options: MigrationShowOptions) => {
218
422
  const flags = parseGlobalFlags(options);