@prisma-next/cli 0.16.0-dev.3 → 0.16.0-dev.31

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 (173) hide show
  1. package/README.md +44 -45
  2. package/dist/cli.mjs +13 -13
  3. package/dist/{client-KuBQftxz.mjs → client-B1UjQc_x.mjs} +50 -58
  4. package/dist/client-B1UjQc_x.mjs.map +1 -0
  5. package/dist/{command-helpers-Cpq44aVa.mjs → command-helpers-D-jBl0Qt.mjs} +16 -8
  6. package/dist/command-helpers-D-jBl0Qt.mjs.map +1 -0
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.d.mts.map +1 -1
  10. package/dist/commands/db-init.mjs +8 -6
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -3
  13. package/dist/commands/db-sign.d.mts.map +1 -1
  14. package/dist/commands/db-sign.mjs +11 -12
  15. package/dist/commands/db-sign.mjs.map +1 -1
  16. package/dist/commands/db-update.d.mts.map +1 -1
  17. package/dist/commands/db-update.mjs +14 -15
  18. package/dist/commands/db-update.mjs.map +1 -1
  19. package/dist/commands/db-verify.mjs +1 -1
  20. package/dist/commands/migrate.d.mts +1 -1
  21. package/dist/commands/migrate.d.mts.map +1 -1
  22. package/dist/commands/migrate.mjs +13 -13
  23. package/dist/commands/migrate.mjs.map +1 -1
  24. package/dist/commands/migration-check.d.mts +4 -2
  25. package/dist/commands/migration-check.d.mts.map +1 -1
  26. package/dist/commands/migration-check.mjs +1 -1
  27. package/dist/commands/migration-graph.d.mts +1 -1
  28. package/dist/commands/migration-graph.mjs +4 -4
  29. package/dist/commands/migration-list.d.mts +1 -1
  30. package/dist/commands/migration-list.d.mts.map +1 -1
  31. package/dist/commands/migration-list.mjs +1 -1
  32. package/dist/commands/migration-log.d.mts +1 -1
  33. package/dist/commands/migration-log.mjs +1 -1
  34. package/dist/commands/migration-new.d.mts.map +1 -1
  35. package/dist/commands/migration-new.mjs +16 -40
  36. package/dist/commands/migration-new.mjs.map +1 -1
  37. package/dist/commands/migration-plan.d.mts +3 -4
  38. package/dist/commands/migration-plan.d.mts.map +1 -1
  39. package/dist/commands/migration-plan.mjs +1 -1
  40. package/dist/commands/migration-show.mjs +5 -5
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +1 -1
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +1 -1
  45. package/dist/commands/ref.d.mts +1 -1
  46. package/dist/commands/ref.d.mts.map +1 -1
  47. package/dist/commands/ref.mjs +13 -18
  48. package/dist/commands/ref.mjs.map +1 -1
  49. package/dist/commands/telemetry/index.mjs +1 -1
  50. package/dist/contract-at-errors-DvZ_DzJK.mjs +36 -0
  51. package/dist/contract-at-errors-DvZ_DzJK.mjs.map +1 -0
  52. package/dist/{contract-emit-CgnXSANN.mjs → contract-emit-BT46Mu8Q.mjs} +3 -3
  53. package/dist/{contract-emit-CgnXSANN.mjs.map → contract-emit-BT46Mu8Q.mjs.map} +1 -1
  54. package/dist/{contract-emit-cJQBlvGb.mjs → contract-emit-hVdlaF2U.mjs} +6 -7
  55. package/dist/{contract-emit-cJQBlvGb.mjs.map → contract-emit-hVdlaF2U.mjs.map} +1 -1
  56. package/dist/{contract-enrichment-gn9sWbPw.mjs → contract-enrichment-JUhLv-BX.mjs} +8 -8
  57. package/dist/{contract-enrichment-gn9sWbPw.mjs.map → contract-enrichment-JUhLv-BX.mjs.map} +1 -1
  58. package/dist/{contract-infer-Cg8evbeg.mjs → contract-infer-DpqTQECL.mjs} +3 -3
  59. package/dist/{contract-infer-Cg8evbeg.mjs.map → contract-infer-DpqTQECL.mjs.map} +1 -1
  60. package/dist/{contract-space-aggregate-loader-D_3VeHVb.mjs → contract-space-aggregate-loader-CNd-ZB6C.mjs} +14 -14
  61. package/dist/contract-space-aggregate-loader-CNd-ZB6C.mjs.map +1 -0
  62. package/dist/{db-verify-BEclUzki.mjs → db-verify-FEN6k1m7.mjs} +10 -8
  63. package/dist/db-verify-FEN6k1m7.mjs.map +1 -0
  64. package/dist/exports/control-api.d.mts +4 -4
  65. package/dist/exports/control-api.d.mts.map +1 -1
  66. package/dist/exports/control-api.mjs +3 -3
  67. package/dist/exports/index.d.mts +2 -1
  68. package/dist/exports/index.d.mts.map +1 -1
  69. package/dist/exports/index.mjs +61 -21
  70. package/dist/exports/index.mjs.map +1 -1
  71. package/dist/exports/init-output.d.mts.map +1 -1
  72. package/dist/exports/init-output.mjs +1 -1
  73. package/dist/{format-DAUAZqPt.mjs → format-DI5qVr22.mjs} +8 -7
  74. package/dist/format-DI5qVr22.mjs.map +1 -0
  75. package/dist/{framework-components-CnbUbiJw.mjs → framework-components-C4XACYNB.mjs} +3 -3
  76. package/dist/framework-components-C4XACYNB.mjs.map +1 -0
  77. package/dist/{init-C3JhkHg-.mjs → init-ClrRkbwd.mjs} +77 -67
  78. package/dist/init-ClrRkbwd.mjs.map +1 -0
  79. package/dist/{inspect-live-schema-CX7EA5C3.mjs → inspect-live-schema-C0Rgdtw_.mjs} +4 -4
  80. package/dist/{inspect-live-schema-CX7EA5C3.mjs.map → inspect-live-schema-C0Rgdtw_.mjs.map} +1 -1
  81. package/dist/{migration-check-MHT3w7_v.mjs → migration-check-zEkHpPDT.mjs} +58 -30
  82. package/dist/migration-check-zEkHpPDT.mjs.map +1 -0
  83. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs → migration-command-scaffold-CkNsp9g1.mjs} +4 -4
  84. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs.map → migration-command-scaffold-CkNsp9g1.mjs.map} +1 -1
  85. package/dist/{migration-graph-command-render-B83xrnNy.mjs → migration-graph-command-render-DEwFpx-w.mjs} +5 -4
  86. package/dist/migration-graph-command-render-DEwFpx-w.mjs.map +1 -0
  87. package/dist/{migration-list-BAKujUjh.mjs → migration-list-BkafFVXS.mjs} +6 -6
  88. package/dist/migration-list-BkafFVXS.mjs.map +1 -0
  89. package/dist/{migration-log-C_xam2HZ.mjs → migration-log-BxhAtbhA.mjs} +5 -5
  90. package/dist/{migration-log-C_xam2HZ.mjs.map → migration-log-BxhAtbhA.mjs.map} +1 -1
  91. package/dist/{migration-path-target-DpPTsRCg.mjs → migration-path-target-B5HkjSTO.mjs} +2 -2
  92. package/dist/{migration-path-target-DpPTsRCg.mjs.map → migration-path-target-B5HkjSTO.mjs.map} +1 -1
  93. package/dist/{migration-plan-CreSVtWZ.mjs → migration-plan-Bk2pbPQH.mjs} +64 -74
  94. package/dist/migration-plan-Bk2pbPQH.mjs.map +1 -0
  95. package/dist/{migration-status-YgEvXf-u.mjs → migration-status-BzlARseq.mjs} +13 -12
  96. package/dist/migration-status-BzlARseq.mjs.map +1 -0
  97. package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
  98. package/dist/output-DWNYqZGc.mjs.map +1 -0
  99. package/dist/{ref-advancement-BkXlikCA.mjs → ref-advancement-BCz7X7qJ.mjs} +13 -6
  100. package/dist/ref-advancement-BCz7X7qJ.mjs.map +1 -0
  101. package/dist/{telemetry-DVr6RFnq.mjs → telemetry-BbniBUbP.mjs} +2 -2
  102. package/dist/{telemetry-DVr6RFnq.mjs.map → telemetry-BbniBUbP.mjs.map} +1 -1
  103. package/dist/{types-D4r_HI9-.d.mts → types-DgXprE3w.d.mts} +3 -3
  104. package/dist/types-DgXprE3w.d.mts.map +1 -0
  105. package/dist/{verify-CgDVZjhc.mjs → verify-BSaNeaSL.mjs} +2 -2
  106. package/dist/{verify-CgDVZjhc.mjs.map → verify-BSaNeaSL.mjs.map} +1 -1
  107. package/package.json +21 -21
  108. package/src/commands/db-init.ts +5 -3
  109. package/src/commands/db-sign.ts +9 -7
  110. package/src/commands/db-update.ts +18 -9
  111. package/src/commands/db-verify.ts +4 -4
  112. package/src/commands/format.ts +1 -1
  113. package/src/commands/init/hygiene-gitattributes.ts +37 -21
  114. package/src/commands/init/init.ts +6 -6
  115. package/src/commands/init/output.ts +4 -5
  116. package/src/commands/init/probe-db.ts +9 -2
  117. package/src/commands/init/reinit-cleanup.ts +16 -17
  118. package/src/commands/init/templates/render.ts +2 -1
  119. package/src/commands/inspect-live-schema.ts +1 -1
  120. package/src/commands/migrate.ts +6 -5
  121. package/src/commands/migration-check.ts +72 -34
  122. package/src/commands/migration-list.ts +1 -5
  123. package/src/commands/migration-log.ts +1 -1
  124. package/src/commands/migration-new.ts +17 -41
  125. package/src/commands/migration-plan.ts +64 -88
  126. package/src/commands/migration-show.ts +2 -2
  127. package/src/commands/migration-status.ts +6 -7
  128. package/src/commands/ref.ts +19 -14
  129. package/src/control-api/client.ts +43 -25
  130. package/src/control-api/contract-enrichment.ts +7 -7
  131. package/src/control-api/operations/contract-emit.ts +2 -3
  132. package/src/control-api/operations/db-init.ts +3 -3
  133. package/src/control-api/operations/db-run.ts +8 -7
  134. package/src/control-api/operations/db-update.ts +2 -2
  135. package/src/control-api/operations/db-verify.ts +4 -4
  136. package/src/control-api/operations/format.ts +5 -4
  137. package/src/control-api/operations/migrate.ts +6 -5
  138. package/src/control-api/operations/run-migration.ts +2 -1
  139. package/src/control-api/types.ts +1 -1
  140. package/src/load-ts-contract.ts +81 -16
  141. package/src/utils/cli-errors.ts +15 -7
  142. package/src/utils/combine-verify-results.ts +2 -1
  143. package/src/utils/command-helpers.ts +15 -5
  144. package/src/utils/contract-at-errors.ts +13 -22
  145. package/src/utils/contract-space-aggregate-loader.ts +8 -8
  146. package/src/utils/contract-space-seed-phase.ts +11 -9
  147. package/src/utils/extension-pack-inputs.ts +8 -8
  148. package/src/utils/formatters/migration-graph-grid-layout.ts +2 -1
  149. package/src/utils/formatters/migration-graph-labels.ts +1 -2
  150. package/src/utils/formatters/migration-list-data-column.ts +1 -2
  151. package/src/utils/framework-components.ts +9 -9
  152. package/src/utils/integrity-violation-to-check-failure.ts +3 -3
  153. package/src/utils/migration-command-scaffold.ts +1 -1
  154. package/src/utils/plan-resolution.ts +7 -10
  155. package/src/utils/ref-advancement.ts +22 -3
  156. package/dist/client-KuBQftxz.mjs.map +0 -1
  157. package/dist/command-helpers-Cpq44aVa.mjs.map +0 -1
  158. package/dist/contract-at-errors-D9gEjYtg.mjs +0 -42
  159. package/dist/contract-at-errors-D9gEjYtg.mjs.map +0 -1
  160. package/dist/contract-space-aggregate-loader-D_3VeHVb.mjs.map +0 -1
  161. package/dist/db-verify-BEclUzki.mjs.map +0 -1
  162. package/dist/format-DAUAZqPt.mjs.map +0 -1
  163. package/dist/framework-components-CnbUbiJw.mjs.map +0 -1
  164. package/dist/init-C3JhkHg-.mjs.map +0 -1
  165. package/dist/migration-check-MHT3w7_v.mjs.map +0 -1
  166. package/dist/migration-graph-command-render-B83xrnNy.mjs.map +0 -1
  167. package/dist/migration-list-BAKujUjh.mjs.map +0 -1
  168. package/dist/migration-plan-CreSVtWZ.mjs.map +0 -1
  169. package/dist/migration-status-YgEvXf-u.mjs.map +0 -1
  170. package/dist/output-mEQ74_nd.mjs.map +0 -1
  171. package/dist/ref-advancement-BkXlikCA.mjs.map +0 -1
  172. package/dist/types-D4r_HI9-.d.mts.map +0 -1
  173. package/src/control-api/errors.ts +0 -9
@@ -2,6 +2,8 @@ import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import type { Contract } from '@prisma-next/contract/types';
5
+ import { CliStructuredError } from '@prisma-next/errors/control';
6
+ import { InternalError } from '@prisma-next/utils/internal-error';
5
7
  import type { Plugin } from 'esbuild';
6
8
  import { build } from 'esbuild';
7
9
  import { join, resolve as resolvePath } from 'pathe';
@@ -10,6 +12,30 @@ export interface LoadTsContractOptions {
10
12
  readonly allowlist?: ReadonlyArray<string>;
11
13
  }
12
14
 
15
+ type ContractSourceErrorCode =
16
+ | 'CONTRACT.EXPORT_INVALID'
17
+ | 'CONTRACT.SOURCE_LOAD_FAILED'
18
+ | 'CONTRACT.SOURCE_IMPORT_DISALLOWED';
19
+
20
+ function contractSourceError(
21
+ code: ContractSourceErrorCode,
22
+ message: string,
23
+ options?: {
24
+ readonly meta?: Record<string, unknown>;
25
+ readonly cause?: unknown;
26
+ },
27
+ ): CliStructuredError {
28
+ const error = new CliStructuredError(
29
+ code,
30
+ message,
31
+ options?.meta !== undefined ? { meta: options.meta } : undefined,
32
+ );
33
+ if (options?.cause !== undefined) {
34
+ error.cause = options.cause;
35
+ }
36
+ return error;
37
+ }
38
+
13
39
  const DEFAULT_ALLOWLIST = ['@prisma-next/*', 'node:crypto'];
14
40
 
15
41
  function isAllowedImport(importPath: string, allowlist: ReadonlyArray<string>): boolean {
@@ -31,7 +57,7 @@ function isAllowedImport(importPath: string, allowlist: ReadonlyArray<string>):
31
57
  return false;
32
58
  }
33
59
 
34
- function validatePurity(value: unknown): void {
60
+ function validatePurity(value: unknown, entryPath: string): void {
35
61
  if (typeof value !== 'object' || value === null) {
36
62
  return;
37
63
  }
@@ -44,7 +70,11 @@ function validatePurity(value: unknown): void {
44
70
  }
45
71
 
46
72
  if (path.has(value)) {
47
- throw new Error('Contract export contains circular references');
73
+ throw contractSourceError(
74
+ 'CONTRACT.EXPORT_INVALID',
75
+ 'Contract export contains circular references',
76
+ { meta: { path: entryPath, reason: 'circular' } },
77
+ );
48
78
  }
49
79
  path.add(value);
50
80
 
@@ -52,10 +82,18 @@ function validatePurity(value: unknown): void {
52
82
  for (const key in value) {
53
83
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
54
84
  if (descriptor && (descriptor.get || descriptor.set)) {
55
- throw new Error(`Contract export contains getter/setter at key "${key}"`);
85
+ throw contractSourceError(
86
+ 'CONTRACT.EXPORT_INVALID',
87
+ `Contract export contains getter/setter at key "${key}"`,
88
+ { meta: { path: entryPath, reason: 'getter', key } },
89
+ );
56
90
  }
57
91
  if (descriptor && typeof descriptor.value === 'function') {
58
- throw new Error(`Contract export contains function at key "${key}"`);
92
+ throw contractSourceError(
93
+ 'CONTRACT.EXPORT_INVALID',
94
+ `Contract export contains function at key "${key}"`,
95
+ { meta: { path: entryPath, reason: 'function', key } },
96
+ );
59
97
  }
60
98
  check((value as Record<string, unknown>)[key]);
61
99
  }
@@ -72,9 +110,17 @@ function validatePurity(value: unknown): void {
72
110
  if (error.message.includes('getter') || error.message.includes('circular')) {
73
111
  throw error;
74
112
  }
75
- throw new Error(`Contract export is not JSON-serializable: ${error.message}`);
113
+ throw contractSourceError(
114
+ 'CONTRACT.EXPORT_INVALID',
115
+ `Contract export is not JSON-serializable: ${error.message}`,
116
+ { meta: { path: entryPath, reason: 'not-json-serializable' }, cause: error },
117
+ );
76
118
  }
77
- throw new Error('Contract export is not JSON-serializable');
119
+ throw contractSourceError(
120
+ 'CONTRACT.EXPORT_INVALID',
121
+ 'Contract export is not JSON-serializable',
122
+ { meta: { path: entryPath, reason: 'not-json-serializable' } },
123
+ );
78
124
  }
79
125
  }
80
126
 
@@ -129,7 +175,8 @@ function createImportAllowlistPlugin(
129
175
  * @param entryPath - Path to the TypeScript contract file
130
176
  * @param options - Optional configuration (import allowlist)
131
177
  * @returns The contract as Contract (should already be normalized)
132
- * @throws Error if the contract cannot be loaded or is not JSON-serializable
178
+ * @throws structured errors: `CLI.FILE_NOT_FOUND`, `CONTRACT.SOURCE_LOAD_FAILED`,
179
+ * `CONTRACT.SOURCE_IMPORT_DISALLOWED`, `CONTRACT.MODULE_EXPORT_MISSING`, `CONTRACT.EXPORT_INVALID`
133
180
  */
134
181
  export async function loadContractFromTs(
135
182
  entryPath: string,
@@ -138,7 +185,9 @@ export async function loadContractFromTs(
138
185
  const allowlist = options?.allowlist ?? DEFAULT_ALLOWLIST;
139
186
 
140
187
  if (!existsSync(entryPath)) {
141
- throw new Error(`Contract file not found: ${entryPath}`);
188
+ throw new CliStructuredError('CLI.FILE_NOT_FOUND', `Contract file not found: ${entryPath}`, {
189
+ where: { path: entryPath },
190
+ });
142
191
  }
143
192
 
144
193
  const tempFile = join(
@@ -169,22 +218,28 @@ export async function loadContractFromTs(
169
218
 
170
219
  if (result.errors.length > 0) {
171
220
  const errorMessages = result.errors.map((e: { text: string }) => e.text).join('\n');
172
- throw new Error(`Failed to bundle contract file: ${errorMessages}`);
221
+ throw contractSourceError(
222
+ 'CONTRACT.SOURCE_LOAD_FAILED',
223
+ `Failed to bundle contract file: ${errorMessages}`,
224
+ { meta: { path: entryPath, stage: 'bundle' } },
225
+ );
173
226
  }
174
227
 
175
228
  if (!result.outputFiles || result.outputFiles.length === 0) {
176
- throw new Error('No output files generated from bundling');
229
+ throw new InternalError('No output files generated from bundling');
177
230
  }
178
231
 
179
232
  if (disallowedFromEntry.size > 0) {
180
- throw new Error(
233
+ throw contractSourceError(
234
+ 'CONTRACT.SOURCE_IMPORT_DISALLOWED',
181
235
  `Disallowed imports detected. Only imports matching the allowlist are permitted:\n Allowlist: ${allowlist.join(', ')}\n Disallowed imports: ${[...disallowedFromEntry].join(', ')}`,
236
+ { meta: { allowlist: [...allowlist], disallowed: [...disallowedFromEntry] } },
182
237
  );
183
238
  }
184
239
 
185
240
  const bundleContent = result.outputFiles[0]?.text;
186
241
  if (bundleContent === undefined) {
187
- throw new Error('Bundle content is undefined');
242
+ throw new InternalError('Bundle content is undefined');
188
243
  }
189
244
  writeFileSync(tempFile, bundleContent, 'utf-8');
190
245
 
@@ -201,16 +256,22 @@ export async function loadContractFromTs(
201
256
  } else if (module.contract !== undefined) {
202
257
  contract = module.contract;
203
258
  } else {
204
- throw new Error(
259
+ throw new CliStructuredError(
260
+ 'CONTRACT.MODULE_EXPORT_MISSING',
205
261
  `Contract file must export a contract as default export or named export 'contract'. Found exports: ${Object.keys(module as Record<string, unknown>).join(', ') || 'none'}`,
262
+ { where: { path: entryPath } },
206
263
  );
207
264
  }
208
265
 
209
266
  if (typeof contract !== 'object' || contract === null) {
210
- throw new Error(`Contract export must be an object, got ${typeof contract}`);
267
+ throw contractSourceError(
268
+ 'CONTRACT.EXPORT_INVALID',
269
+ `Contract export must be an object, got ${typeof contract}`,
270
+ { meta: { path: entryPath, reason: 'not-object' } },
271
+ );
211
272
  }
212
273
 
213
- validatePurity(contract);
274
+ validatePurity(contract, entryPath);
214
275
 
215
276
  // Blind cast: the loaded module was authored by user code
216
277
  // (typically via `defineContract` / a contract builder) and
@@ -233,6 +294,10 @@ export async function loadContractFromTs(
233
294
  if (error instanceof Error) {
234
295
  throw error;
235
296
  }
236
- throw new Error(`Failed to load contract from ${entryPath}: ${String(error)}`);
297
+ throw contractSourceError(
298
+ 'CONTRACT.SOURCE_LOAD_FAILED',
299
+ `Failed to load contract from ${entryPath}: ${String(error)}`,
300
+ { meta: { path: entryPath, stage: 'import' }, cause: error },
301
+ );
237
302
  }
238
303
  }
@@ -10,7 +10,7 @@ import {
10
10
  errorConfigFileNotFound,
11
11
  errorConfigValidation,
12
12
  errorContractConfigMissing,
13
- errorContractMissingExtensionPacks,
13
+ errorContractMissingExtensions,
14
14
  errorContractValidationFailed,
15
15
  errorDatabaseConnectionRequired,
16
16
  errorDriverRequired,
@@ -54,7 +54,7 @@ export {
54
54
  errorConfigFileNotFound,
55
55
  errorConfigValidation,
56
56
  errorContractConfigMissing,
57
- errorContractMissingExtensionPacks,
57
+ errorContractMissingExtensions,
58
58
  errorContractValidationFailed,
59
59
  errorDatabaseConnectionRequired,
60
60
  errorDriverRequired,
@@ -215,22 +215,30 @@ export function errorPlanForgotTheFlag(
215
215
  });
216
216
  }
217
217
 
218
+ /**
219
+ * `viaRef: true` (the default) mirrors migration-tools' `errorRefNotResolvable`:
220
+ * a ref name with no pointer file, where the fallback hash isn't a graph
221
+ * node either — there's nothing to materialize a contract from.
222
+ * `viaRef: false` is a distinct, ref-independent case: an explicit `--from
223
+ * <hash>` that doesn't name a ref, on an empty migration graph, so there is
224
+ * no graph node and no ref to resolve a contract through.
225
+ */
218
226
  export function errorSnapshotMissing(
219
227
  identifier: string,
220
228
  options?: { readonly viaRef?: boolean },
221
229
  ): CliStructuredError {
222
230
  const viaRef = options?.viaRef !== false;
223
231
  const fix = viaRef
224
- ? `Run "prisma-next db update --advance-ref ${identifier}" to repopulate the snapshot, or "prisma-next ref delete ${identifier}" to clear the orphan pointer.`
225
- : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name that has a paired snapshot, or run db update first.`;
232
+ ? `Create the ref with "prisma-next ref set ${identifier} <hash>" (or advance it via "prisma-next db update --advance-ref ${identifier}"), or pass a hash that is a node in the migration graph.`
233
+ : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name (its contract resolves through the snapshot store), or run db update first.`;
226
234
  return errorRuntime(
227
235
  viaRef
228
- ? `Ref "${identifier}" has no paired contract snapshot`
236
+ ? `Ref "${identifier}" is not resolvable`
229
237
  : `No contract source for from-hash "${identifier}"`,
230
238
  {
231
239
  why: viaRef
232
- ? `Ref "${identifier}" exists but its paired snapshot files are missing.`
233
- : `Hash "${identifier}" is not a graph node and no paired ref snapshot supplies a contract.`,
240
+ ? `Ref "${identifier}" has no pointer file, and the hash being resolved is not a node in the migration graph either.`
241
+ : `Hash "${identifier}" is not a node in the migration graph (the graph is empty), and it does not name a ref either.`,
234
242
  fix,
235
243
  meta: {
236
244
  code: 'MIGRATION.SNAPSHOT_MISSING',
@@ -1,4 +1,5 @@
1
1
  import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
2
+ import { InternalError } from '@prisma-next/utils/internal-error';
2
3
 
3
4
  /**
4
5
  * The combined per-space contract-satisfaction result plus the standalone
@@ -42,7 +43,7 @@ export function combineVerifyResults(
42
43
  ): CombinedVerifyResult {
43
44
  const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
44
45
  if (appResult === undefined) {
45
- throw new Error(
46
+ throw new InternalError(
46
47
  'Aggregate verifier returned no per-space verify results — this is a wiring bug.',
47
48
  );
48
49
  }
@@ -8,7 +8,7 @@ import { APP_SPACE_ID, spaceMigrationDirectory } from '@prisma-next/migration-to
8
8
  import { ifDefined } from '@prisma-next/utils/defined';
9
9
  import type { Command } from 'commander';
10
10
  import { relative, resolve } from 'pathe';
11
- import { errorRuntime } from './cli-errors';
11
+ import { CliStructuredError, errorRuntime } from './cli-errors';
12
12
  import { formatCommandHelp } from './formatters/help';
13
13
  import type { CommonCommandOptions } from './global-flags';
14
14
  import { parseGlobalFlags } from './global-flags';
@@ -263,21 +263,31 @@ export async function readContractEnvelope(config: {
263
263
  const storageHash = storage?.['storageHash'];
264
264
 
265
265
  if (typeof storageHash !== 'string') {
266
- throw new Error(
266
+ throw new CliStructuredError(
267
+ 'CONTRACT.VALIDATION_FAILED',
267
268
  `Contract at ${relative(process.cwd(), contractPath)} is missing a valid storage.storageHash. Run \`prisma-next contract emit\` to regenerate.`,
269
+ { where: { path: contractPath } },
268
270
  );
269
271
  }
270
272
  if (typeof schemaVersion !== 'string') {
271
- throw new Error(
273
+ throw new CliStructuredError(
274
+ 'CONTRACT.VALIDATION_FAILED',
272
275
  `Contract at ${relative(process.cwd(), contractPath)} is missing schemaVersion.`,
276
+ { where: { path: contractPath } },
273
277
  );
274
278
  }
275
279
  if (typeof target !== 'string') {
276
- throw new Error(`Contract at ${relative(process.cwd(), contractPath)} is missing target.`);
280
+ throw new CliStructuredError(
281
+ 'CONTRACT.VALIDATION_FAILED',
282
+ `Contract at ${relative(process.cwd(), contractPath)} is missing target.`,
283
+ { where: { path: contractPath } },
284
+ );
277
285
  }
278
286
  if (typeof targetFamily !== 'string') {
279
- throw new Error(
287
+ throw new CliStructuredError(
288
+ 'CONTRACT.VALIDATION_FAILED',
280
289
  `Contract at ${relative(process.cwd(), contractPath)} is missing targetFamily.`,
290
+ { where: { path: contractPath } },
281
291
  );
282
292
  }
283
293
 
@@ -1,6 +1,5 @@
1
1
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
2
2
  import { notOk, type Result } from '@prisma-next/utils/result';
3
- import { join } from 'pathe';
4
3
  import {
5
4
  CliStructuredError,
6
5
  errorContractValidationFailed,
@@ -16,7 +15,7 @@ export function mapContractAtError(
16
15
  ): Result<never, CliStructuredError> {
17
16
  if (MigrationToolsError.is(error)) {
18
17
  switch (error.code) {
19
- case 'MIGRATION.SNAPSHOT_MISSING': {
18
+ case 'MIGRATION.REF_NOT_RESOLVABLE': {
20
19
  const refName =
21
20
  typeof error.details?.['refName'] === 'string'
22
21
  ? error.details['refName']
@@ -27,18 +26,13 @@ export function mapContractAtError(
27
26
  }
28
27
  case 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED': {
29
28
  const filePath =
30
- typeof error.details?.['filePath'] === 'string'
31
- ? error.details['filePath']
32
- : 'ref-snapshot';
29
+ typeof error.details?.['filePath'] === 'string' ? error.details['filePath'] : 'unknown';
33
30
  const message =
34
31
  typeof error.details?.['message'] === 'string' ? error.details['message'] : error.message;
35
- const isRefSnapshot = filePath.endsWith('.contract.json');
36
32
  return notOk(
37
33
  errorContractValidationFailed(
38
- isRefSnapshot
39
- ? `Ref snapshot contract failed to deserialize: ${message}`
40
- : `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
41
- { where: { path: isRefSnapshot ? 'ref-snapshot' : filePath } },
34
+ `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
35
+ { where: { path: filePath } },
42
36
  ),
43
37
  );
44
38
  }
@@ -66,22 +60,19 @@ export function mapContractAtError(
66
60
  fix: error.fix,
67
61
  }),
68
62
  );
69
- case 'MIGRATION.FILE_MISSING': {
70
- const file =
71
- typeof error.details?.['file'] === 'string' ? error.details['file'] : 'end-contract.json';
72
- const dir = typeof error.details?.['dir'] === 'string' ? error.details['dir'] : '';
73
- const jsonPath = dir ? join(dir, 'end-contract.json') : file;
63
+ case 'MIGRATION.CONTRACT_SNAPSHOT_MISSING': {
64
+ const expectedPath =
65
+ typeof error.details?.['expectedPath'] === 'string'
66
+ ? error.details['expectedPath']
67
+ : 'migrations/snapshots/';
74
68
  const role = options?.artifactRole ?? 'from';
75
69
  return notOk(
76
- errorFileNotFound(jsonPath, {
70
+ errorFileNotFound(expectedPath, {
77
71
  why:
78
72
  role === 'to'
79
- ? `Target migration is missing its destination contract snapshot at ${jsonPath}`
80
- : `Predecessor migration is missing its destination contract snapshot at ${jsonPath}`,
81
- fix:
82
- role === 'to'
83
- ? 'Re-emit the target migration so its sibling `end-contract.json` / `end-contract.d.ts` are restored, then re-run this command.'
84
- : 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
73
+ ? `Target migration is missing its contract snapshot at ${expectedPath}`
74
+ : `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
75
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
85
76
  }),
86
77
  );
87
78
  }
@@ -114,8 +114,8 @@ export function mapIntegrityViolations(
114
114
  ? 'Contract-space layout violation detected'
115
115
  : `Contract-space layout violations detected (${layout.length})`;
116
116
  return new CliStructuredError('MIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION', summary, {
117
- why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
118
- fix: 'Declare the extension in `extensionPacks` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.',
117
+ why: `The on-disk \`migrations/\` directory and your \`extensions\` declaration are not in agreement.\n${lines.join('\n')}`,
118
+ fix: 'Declare the extension in `extensions` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.',
119
119
  docsUrl: CONTRACT_SPACES_DOCS_URL,
120
120
  meta: { violations: layout },
121
121
  });
@@ -177,14 +177,14 @@ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extend
177
177
  readonly targetId: TTargetId;
178
178
  readonly migrationsDir: string;
179
179
  readonly appContract: Contract;
180
- readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
180
+ readonly extensions: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
181
181
  readonly deserializeContract: (contractJson: unknown) => Contract;
182
182
  }
183
183
 
184
184
  function declaredExtensionsFromInputs(
185
- extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'],
185
+ extensions: BuildAggregateInputs<string, string>['extensions'],
186
186
  ): readonly DeclaredExtensionEntry[] {
187
- return toDeclaredExtensionsFromRaw(extensionPacks as ReadonlyArray<unknown>);
187
+ return toDeclaredExtensionsFromRaw(extensions as ReadonlyArray<unknown>);
188
188
  }
189
189
 
190
190
  /**
@@ -195,7 +195,7 @@ export function refuseDeclaredExtensionTargetMismatch<
195
195
  TFamilyId extends string,
196
196
  TTargetId extends string,
197
197
  >(inputs: BuildAggregateInputs<TFamilyId, TTargetId>): CliStructuredError | null {
198
- for (const declared of declaredExtensionsFromInputs(inputs.extensionPacks)) {
198
+ for (const declared of declaredExtensionsFromInputs(inputs.extensions)) {
199
199
  if (declared.targetId !== inputs.targetId) {
200
200
  return targetMismatchError(declared.id, inputs.targetId, declared.targetId);
201
201
  }
@@ -271,7 +271,7 @@ export async function buildContractSpaceAggregate<
271
271
  >(
272
272
  inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
273
273
  ): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
274
- const declaredExtensions = declaredExtensionsFromInputs(inputs.extensionPacks);
274
+ const declaredExtensions = declaredExtensionsFromInputs(inputs.extensions);
275
275
  const loaded = await loadContractSpaceAggregateForCli(inputs);
276
276
  if (!loaded.ok) {
277
277
  return loaded;
@@ -375,7 +375,7 @@ export async function buildReadAggregate(
375
375
  targetId: config.target.id,
376
376
  migrationsDir: options.migrationsDir,
377
377
  appContract: appContractForLoad,
378
- extensionPacks: config.extensionPacks ?? [],
378
+ extensions: config.extensions ?? [],
379
379
  deserializeContract,
380
380
  });
381
381
  if (!loaded.ok) {
@@ -2,7 +2,7 @@ import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migr
2
2
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
3
3
  import type { MigrationOps } from '@prisma-next/migration-tools/package';
4
4
  import {
5
- emitContractSpaceArtefacts,
5
+ emitContractSpaceArtifacts,
6
6
  planAllSpaces,
7
7
  readContractSpaceHeadRef,
8
8
  type SpacePlanOutput,
@@ -39,7 +39,7 @@ export interface SeedPhaseExtensionInput {
39
39
 
40
40
  export interface ContractSpaceSeedPhaseInputs {
41
41
  readonly migrationsDir: string;
42
- readonly extensionPacks: ReadonlyArray<SeedPhaseExtensionInput>;
42
+ readonly extensions: ReadonlyArray<SeedPhaseExtensionInput>;
43
43
  }
44
44
 
45
45
  /**
@@ -53,7 +53,7 @@ export interface ContractSpaceSeedPhaseInputs {
53
53
  * - `action: 'unchanged'` — the on-disk head already matched the
54
54
  * descriptor and no new migration packages needed to be written.
55
55
  *
56
- * Either way, the artefacts (`contract.json`, `contract.d.ts`,
56
+ * Either way, the artifacts (`contract.json`, `contract.d.ts`,
57
57
  * `refs/head.json`) are re-emitted: the framework owns those files and
58
58
  * makes the re-emit observably idempotent at the byte level.
59
59
  */
@@ -75,9 +75,11 @@ export interface ContractSpaceSeedPhaseResult {
75
75
  * For every extension that exposes a `contractSpace`:
76
76
  *
77
77
  * 1. Read the on-disk head ref (returns `null` on first emit).
78
- * 2. Re-emit `contract.json` / `contract.d.ts` / `refs/head.json`
79
- * unconditionally via {@link emitContractSpaceArtefacts}. The
80
- * framework owns these files; re-emit is the contract.
78
+ * 2. Write the head contract into the migrations-root snapshot store
79
+ * (write-if-absent, keyed by hash) and unconditionally re-emit
80
+ * `refs/head.json`, via {@link emitContractSpaceArtifacts}. The
81
+ * framework owns `refs/head.json`; re-emit is the contract for that
82
+ * file. The snapshot itself is only written once per distinct hash.
81
83
  * 3. Materialise any descriptor-shipped migration packages not yet on
82
84
  * disk via {@link materialiseExtensionMigrationPackageIfMissing}.
83
85
  * Existing packages are left untouched (by-existence skip).
@@ -96,7 +98,7 @@ export interface ContractSpaceSeedPhaseResult {
96
98
  export async function runContractSpaceSeedPhase(
97
99
  inputs: ContractSpaceSeedPhaseInputs,
98
100
  ): Promise<ContractSpaceSeedPhaseResult> {
99
- const planInputs = inputs.extensionPacks
101
+ const planInputs = inputs.extensions
100
102
  .filter(
101
103
  (
102
104
  pack,
@@ -132,7 +134,7 @@ export async function runContractSpaceSeedPhase(
132
134
  string,
133
135
  NonNullable<SeedPhaseExtensionInput['contractSpace']>
134
136
  >();
135
- for (const pack of inputs.extensionPacks) {
137
+ for (const pack of inputs.extensions) {
136
138
  if (pack.contractSpace !== undefined) descriptorBySpace.set(pack.id, pack.contractSpace);
137
139
  }
138
140
 
@@ -144,7 +146,7 @@ export async function runContractSpaceSeedPhase(
144
146
  const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, space.spaceId);
145
147
  const priorHash = onDiskHeadRef?.hash ?? null;
146
148
 
147
- await emitContractSpaceArtefacts(inputs.migrationsDir, space.spaceId, {
149
+ await emitContractSpaceArtifacts(inputs.migrationsDir, space.spaceId, {
148
150
  contract: descriptor.contractJson,
149
151
  contractDts: buildPlaceholderContractDts(space.spaceId),
150
152
  headRef: { hash: descriptor.headRef.hash, invariants: descriptor.headRef.invariants },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Single descriptor-import boundary for CLI consumers of `Config.extensionPacks`.
2
+ * Single descriptor-import boundary for CLI consumers of `Config.extensions`.
3
3
  *
4
4
  * Every CLI command / utility that reads an extension descriptor's
5
5
  * `contractSpace` projection (loader, migrate-pass, extension-migrations
@@ -54,9 +54,9 @@ export interface ExtensionPackInput {
54
54
  }
55
55
 
56
56
  /**
57
- * Structural shape we read off each `Config.extensionPacks` entry.
57
+ * Structural shape we read off each `Config.extensions` entry.
58
58
  *
59
- * The CLI is the descriptor-import boundary; `extensionPacks` is the only
59
+ * The CLI is the descriptor-import boundary; `extensions` is the only
60
60
  * surface where the SQL-family-typed `ControlExtensionDescriptor` flows
61
61
  * into framework-neutral helpers. The structural cast lives here, and
62
62
  * here alone — every other CLI consumer reads the canonical
@@ -76,14 +76,14 @@ type ExtensionPackLike = {
76
76
  };
77
77
 
78
78
  /**
79
- * Project the CLI's `Config.extensionPacks` array into the canonical
79
+ * Project the CLI's `Config.extensions` array into the canonical
80
80
  * {@link ExtensionPackInput} shape. The single `as ExtensionPackLike`
81
81
  * structural cast in the CLI lives inside this function.
82
82
  */
83
83
  export function toExtensionInputs(
84
- extensionPacks: ReadonlyArray<unknown>,
84
+ extensions: ReadonlyArray<unknown>,
85
85
  ): readonly ExtensionPackInput[] {
86
- return extensionPacks.map((raw) => {
86
+ return extensions.map((raw) => {
87
87
  const pack = raw as ExtensionPackLike;
88
88
  if (pack.contractSpace === undefined) {
89
89
  return { id: pack.id, targetId: pack.targetId };
@@ -147,10 +147,10 @@ export function toDeclaredExtensions(
147
147
  * always reads the contract from on-disk artefacts instead.
148
148
  */
149
149
  export function toDeclaredExtensionsFromRaw(
150
- extensionPacks: ReadonlyArray<unknown>,
150
+ extensions: ReadonlyArray<unknown>,
151
151
  ): readonly DeclaredExtensionEntry[] {
152
152
  const entries: DeclaredExtensionEntry[] = [];
153
- for (const raw of extensionPacks) {
153
+ for (const raw of extensions) {
154
154
  if (typeof raw !== 'object' || raw === null) continue;
155
155
  const descriptor = Object.getOwnPropertyDescriptor(raw, 'contractSpace');
156
156
  if (descriptor === undefined) continue;
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
10
+ import { InternalError } from '@prisma-next/utils/internal-error';
10
11
  import {
11
12
  type Cell,
12
13
  type CellLine,
@@ -1124,7 +1125,7 @@ function assertSingleOwner(row: CellsRow, isFocus: boolean): void {
1124
1125
  if (isFocus) {
1125
1126
  const roles = new Set(top.map((cl) => cl.line.role));
1126
1127
  if (roles.size > 1) {
1127
- throw new Error(
1128
+ throw new InternalError(
1128
1129
  'migration-graph layout: single-owner invariant violated — two differently-roled lines share the top plane in one cell',
1129
1130
  );
1130
1131
  }
@@ -104,8 +104,7 @@ function abbreviateHash(hash: string, hashLength: number, emptySource: string):
104
104
  if (hash === EMPTY_CONTRACT_HASH) {
105
105
  return emptySource;
106
106
  }
107
- const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
108
- return stripped.slice(0, hashLength);
107
+ return hash.slice(0, hashLength);
109
108
  }
110
109
 
111
110
  interface ContractOverlayNames {
@@ -18,8 +18,7 @@ export function migrationListEmptySource(glyphMode: GlyphMode): string {
18
18
  }
19
19
 
20
20
  export function abbreviateContractHash(hash: string): string {
21
- const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
22
- return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);
21
+ return hash.slice(0, MIGRATION_LIST_HASH_WIDTH);
23
22
  }
24
23
 
25
24
  export function padFromHashColumn(text: string, width: number): string {
@@ -4,7 +4,7 @@ import {
4
4
  type TargetBoundComponentDescriptor,
5
5
  } from '@prisma-next/framework-components/components';
6
6
  import type { ControlStack } from '@prisma-next/framework-components/control';
7
- import { errorConfigValidation, errorContractMissingExtensionPacks } from './cli-errors';
7
+ import { errorConfigValidation, errorContractMissingExtensions } from './cli-errors';
8
8
 
9
9
  /**
10
10
  * Asserts that all framework components are compatible with the expected family and target.
@@ -27,7 +27,7 @@ import { errorConfigValidation, errorContractMissingExtensionPacks } from './cli
27
27
  * @example
28
28
  * ```ts
29
29
  * const config = await loadConfig();
30
- * const frameworkComponents = [config.target, config.adapter, ...(config.extensionPacks ?? [])];
30
+ * const frameworkComponents = [config.target, config.adapter, ...(config.extensions ?? [])];
31
31
  *
32
32
  * // Validate and type-narrow components before passing to planner
33
33
  * const typedComponents = assertFrameworkComponentsCompatible(
@@ -114,12 +114,12 @@ export function assertFrameworkComponentsCompatible<
114
114
  * This check ensures the emitted contract matches the CLI config before running
115
115
  * commands that depend on the contract (e.g., db verify, db sign).
116
116
  *
117
- * @param contract - The contract to validate (must include targetFamily, target, extensionPacks).
118
- * @param stack - The control plane stack (target, adapter, driver, extensionPacks).
117
+ * @param contract - The contract to validate (must include targetFamily, target, extensions).
118
+ * @param stack - The control plane stack (target, adapter, driver, extensions).
119
119
  *
120
120
  * @throws {CliStructuredError} errorConfigValidation when contract.targetFamily or contract.target
121
121
  * doesn't match the configured family/target.
122
- * @throws {CliStructuredError} errorContractMissingExtensionPacks when the contract requires
122
+ * @throws {CliStructuredError} errorContractMissingExtensions when the contract requires
123
123
  * extension packs that are not provided in the config (includes all missing packs in error.meta).
124
124
  *
125
125
  * @example
@@ -141,14 +141,14 @@ export function assertContractRequirementsSatisfied<
141
141
  contract,
142
142
  stack,
143
143
  }: {
144
- readonly contract: Pick<Contract, 'targetFamily' | 'target' | 'extensionPacks'>;
144
+ readonly contract: Pick<Contract, 'targetFamily' | 'target' | 'extensions'>;
145
145
  readonly stack: ControlStack<TFamilyId, TTargetId>;
146
146
  }): void {
147
147
  const providedComponentIds = new Set<string>([
148
148
  stack.target.id,
149
149
  ...(stack.adapter ? [stack.adapter.id] : []),
150
150
  ]);
151
- for (const extension of stack.extensionPacks) {
151
+ for (const extension of stack.extensions) {
152
152
  providedComponentIds.add(extension.id);
153
153
  }
154
154
 
@@ -172,8 +172,8 @@ export function assertContractRequirementsSatisfied<
172
172
  }
173
173
 
174
174
  if (result.missingExtensionPackIds.length > 0) {
175
- throw errorContractMissingExtensionPacks({
176
- missingExtensionPacks: result.missingExtensionPackIds,
175
+ throw errorContractMissingExtensions({
176
+ missingExtensions: result.missingExtensionPackIds,
177
177
  providedComponentIds: [...providedComponentIds],
178
178
  });
179
179
  }