@prisma-next/cli 0.16.0-dev.30 → 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 (95) hide show
  1. package/dist/cli.mjs +13 -13
  2. package/dist/{client-CMusjGV5.mjs → client-B1UjQc_x.mjs} +30 -37
  3. package/dist/client-B1UjQc_x.mjs.map +1 -0
  4. package/dist/{command-helpers-CVn3U9Uz.mjs → command-helpers-D-jBl0Qt.mjs} +5 -5
  5. package/dist/command-helpers-D-jBl0Qt.mjs.map +1 -0
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.d.mts.map +1 -1
  9. package/dist/commands/db-init.mjs +6 -5
  10. package/dist/commands/db-init.mjs.map +1 -1
  11. package/dist/commands/db-schema.mjs +3 -3
  12. package/dist/commands/db-sign.mjs +6 -5
  13. package/dist/commands/db-sign.mjs.map +1 -1
  14. package/dist/commands/db-update.d.mts.map +1 -1
  15. package/dist/commands/db-update.mjs +7 -6
  16. package/dist/commands/db-update.mjs.map +1 -1
  17. package/dist/commands/db-verify.mjs +1 -1
  18. package/dist/commands/migrate.mjs +6 -6
  19. package/dist/commands/migration-check.mjs +1 -1
  20. package/dist/commands/migration-graph.mjs +4 -4
  21. package/dist/commands/migration-list.mjs +1 -1
  22. package/dist/commands/migration-log.mjs +1 -1
  23. package/dist/commands/migration-new.mjs +3 -3
  24. package/dist/commands/migration-plan.mjs +1 -1
  25. package/dist/commands/migration-show.mjs +3 -3
  26. package/dist/commands/migration-status.mjs +1 -1
  27. package/dist/commands/ref.mjs +2 -2
  28. package/dist/commands/telemetry/index.mjs +1 -1
  29. package/dist/{contract-at-errors-C-l4-JEY.mjs → contract-at-errors-DvZ_DzJK.mjs} +2 -2
  30. package/dist/{contract-at-errors-C-l4-JEY.mjs.map → contract-at-errors-DvZ_DzJK.mjs.map} +1 -1
  31. package/dist/{contract-emit-PaY3qRdN.mjs → contract-emit-BT46Mu8Q.mjs} +3 -3
  32. package/dist/{contract-emit-PaY3qRdN.mjs.map → contract-emit-BT46Mu8Q.mjs.map} +1 -1
  33. package/dist/{contract-emit-CfIbTnsR.mjs → contract-emit-hVdlaF2U.mjs} +3 -3
  34. package/dist/{contract-emit-CfIbTnsR.mjs.map → contract-emit-hVdlaF2U.mjs.map} +1 -1
  35. package/dist/{contract-infer-DCju2fTk.mjs → contract-infer-DpqTQECL.mjs} +3 -3
  36. package/dist/{contract-infer-DCju2fTk.mjs.map → contract-infer-DpqTQECL.mjs.map} +1 -1
  37. package/dist/{contract-space-aggregate-loader-B6E9LGa6.mjs → contract-space-aggregate-loader-CNd-ZB6C.mjs} +2 -2
  38. package/dist/{contract-space-aggregate-loader-B6E9LGa6.mjs.map → contract-space-aggregate-loader-CNd-ZB6C.mjs.map} +1 -1
  39. package/dist/{db-verify-B_dKu6Yp.mjs → db-verify-FEN6k1m7.mjs} +9 -7
  40. package/dist/db-verify-FEN6k1m7.mjs.map +1 -0
  41. package/dist/exports/control-api.d.mts.map +1 -1
  42. package/dist/exports/control-api.mjs +2 -2
  43. package/dist/exports/index.d.mts +2 -1
  44. package/dist/exports/index.d.mts.map +1 -1
  45. package/dist/exports/index.mjs +61 -21
  46. package/dist/exports/index.mjs.map +1 -1
  47. package/dist/{format-cCyDfmRE.mjs → format-DI5qVr22.mjs} +3 -3
  48. package/dist/{format-cCyDfmRE.mjs.map → format-DI5qVr22.mjs.map} +1 -1
  49. package/dist/{framework-components-DUN-TYDg.mjs → framework-components-C4XACYNB.mjs} +2 -2
  50. package/dist/{framework-components-DUN-TYDg.mjs.map → framework-components-C4XACYNB.mjs.map} +1 -1
  51. package/dist/{init-C9U3ffHH.mjs → init-ClrRkbwd.mjs} +25 -23
  52. package/dist/init-ClrRkbwd.mjs.map +1 -0
  53. package/dist/{inspect-live-schema-DHXE7skX.mjs → inspect-live-schema-C0Rgdtw_.mjs} +3 -3
  54. package/dist/{inspect-live-schema-DHXE7skX.mjs.map → inspect-live-schema-C0Rgdtw_.mjs.map} +1 -1
  55. package/dist/{migration-check-W5XEx4d-.mjs → migration-check-zEkHpPDT.mjs} +4 -4
  56. package/dist/{migration-check-W5XEx4d-.mjs.map → migration-check-zEkHpPDT.mjs.map} +1 -1
  57. package/dist/{migration-command-scaffold-hI5cnoGe.mjs → migration-command-scaffold-CkNsp9g1.mjs} +3 -3
  58. package/dist/{migration-command-scaffold-hI5cnoGe.mjs.map → migration-command-scaffold-CkNsp9g1.mjs.map} +1 -1
  59. package/dist/{migration-graph-command-render-DgQg5App.mjs → migration-graph-command-render-DEwFpx-w.mjs} +3 -2
  60. package/dist/migration-graph-command-render-DEwFpx-w.mjs.map +1 -0
  61. package/dist/{migration-list-ClkEaoL8.mjs → migration-list-BkafFVXS.mjs} +4 -4
  62. package/dist/{migration-list-ClkEaoL8.mjs.map → migration-list-BkafFVXS.mjs.map} +1 -1
  63. package/dist/{migration-log-CjafbZ2Z.mjs → migration-log-BxhAtbhA.mjs} +4 -4
  64. package/dist/{migration-log-CjafbZ2Z.mjs.map → migration-log-BxhAtbhA.mjs.map} +1 -1
  65. package/dist/{migration-path-target-C0TQuV5n.mjs → migration-path-target-B5HkjSTO.mjs} +2 -2
  66. package/dist/{migration-path-target-C0TQuV5n.mjs.map → migration-path-target-B5HkjSTO.mjs.map} +1 -1
  67. package/dist/{migration-plan-Pc4hCF1H.mjs → migration-plan-Bk2pbPQH.mjs} +5 -5
  68. package/dist/{migration-plan-Pc4hCF1H.mjs.map → migration-plan-Bk2pbPQH.mjs.map} +1 -1
  69. package/dist/{migration-status-CqAdRzs8.mjs → migration-status-BzlARseq.mjs} +6 -6
  70. package/dist/{migration-status-CqAdRzs8.mjs.map → migration-status-BzlARseq.mjs.map} +1 -1
  71. package/dist/{telemetry-BEyNMfco.mjs → telemetry-BbniBUbP.mjs} +2 -2
  72. package/dist/{telemetry-BEyNMfco.mjs.map → telemetry-BbniBUbP.mjs.map} +1 -1
  73. package/dist/{verify-C4RazDE1.mjs → verify-BSaNeaSL.mjs} +2 -2
  74. package/dist/{verify-C4RazDE1.mjs.map → verify-BSaNeaSL.mjs.map} +1 -1
  75. package/package.json +21 -21
  76. package/src/commands/db-init.ts +4 -3
  77. package/src/commands/db-sign.ts +2 -2
  78. package/src/commands/db-update.ts +4 -3
  79. package/src/commands/db-verify.ts +3 -3
  80. package/src/commands/init/probe-db.ts +9 -2
  81. package/src/commands/init/templates/render.ts +2 -1
  82. package/src/control-api/client.ts +36 -17
  83. package/src/control-api/operations/db-run.ts +2 -1
  84. package/src/control-api/operations/migrate.ts +3 -2
  85. package/src/control-api/operations/run-migration.ts +2 -1
  86. package/src/load-ts-contract.ts +81 -16
  87. package/src/utils/combine-verify-results.ts +2 -1
  88. package/src/utils/command-helpers.ts +15 -5
  89. package/src/utils/formatters/migration-graph-grid-layout.ts +2 -1
  90. package/dist/client-CMusjGV5.mjs.map +0 -1
  91. package/dist/command-helpers-CVn3U9Uz.mjs.map +0 -1
  92. package/dist/db-verify-B_dKu6Yp.mjs.map +0 -1
  93. package/dist/init-C9U3ffHH.mjs.map +0 -1
  94. package/dist/migration-graph-command-render-DgQg5App.mjs.map +0 -1
  95. package/src/control-api/errors.ts +0 -9
@@ -4,6 +4,7 @@ import type {
4
4
  LedgerEntryRecord,
5
5
  } from '@prisma-next/contract/types';
6
6
  import { emit as emitContractArtifacts } from '@prisma-next/emitter';
7
+ import { CliStructuredError } from '@prisma-next/errors/control';
7
8
  import type { AuthoringPslBlockDescriptorNamespace } from '@prisma-next/framework-components/authoring';
8
9
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
9
10
  import type {
@@ -27,13 +28,13 @@ import {
27
28
  hasSchemaView,
28
29
  } from '@prisma-next/framework-components/control';
29
30
  import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
30
-
31
31
  import { ifDefined } from '@prisma-next/utils/defined';
32
+ import { InternalError } from '@prisma-next/utils/internal-error';
32
33
  import { notOk, ok } from '@prisma-next/utils/result';
34
+ import { structuredError } from '@prisma-next/utils/structured-error';
33
35
 
34
36
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
35
37
  import { enrichContract } from './contract-enrichment';
36
- import { ContractValidationError } from './errors';
37
38
  import { executeDbInit } from './operations/db-init';
38
39
  import { executeDbUpdate } from './operations/db-update';
39
40
  import { type ExecuteDbVerifyResult, executeDbVerify } from './operations/db-verify';
@@ -128,20 +129,25 @@ class ControlClientImpl implements ControlClient {
128
129
  this.init();
129
130
 
130
131
  if (this.driver) {
131
- throw new Error('Already connected. Call close() before reconnecting.');
132
+ throw new CliStructuredError(
133
+ 'DRIVER.ALREADY_CONNECTED',
134
+ 'Already connected. Call close() before reconnecting.',
135
+ );
132
136
  }
133
137
 
134
138
  // Resolve connection: argument > default from options
135
139
  const resolvedConnection = connection ?? this.defaultConnection;
136
140
  if (resolvedConnection === undefined) {
137
- throw new Error(
141
+ throw new CliStructuredError(
142
+ 'CONFIG.DB_CONNECTION_REQUIRED',
138
143
  'No connection provided. Pass a connection to connect() or provide a default connection when creating the client.',
139
144
  );
140
145
  }
141
146
 
142
147
  // Check for driver descriptor
143
148
  if (!this.stack?.driver) {
144
- throw new Error(
149
+ throw new CliStructuredError(
150
+ 'CONFIG.DRIVER_REQUIRED',
145
151
  'Driver is not configured. Pass a driver descriptor when creating the control client to enable database operations.',
146
152
  );
147
153
  }
@@ -167,7 +173,8 @@ class ControlClientImpl implements ControlClient {
167
173
  private buildControlAdapter(): ControlAdapterInstance<string, string> {
168
174
  this.init();
169
175
  if (!this.stack?.adapter) {
170
- throw new Error(
176
+ throw new CliStructuredError(
177
+ 'MIGRATION.TARGET_UNSUPPORTED',
171
178
  `Target "${this.options.target.targetId}" requires an adapter for migrations`,
172
179
  );
173
180
  }
@@ -188,7 +195,10 @@ class ControlClientImpl implements ControlClient {
188
195
  }
189
196
 
190
197
  if (!this.driver || !this.familyInstance || !this.frameworkComponents) {
191
- throw new Error('Not connected. Call connect(connection) first.');
198
+ throw new CliStructuredError(
199
+ 'DRIVER.NOT_CONNECTED',
200
+ 'Not connected. Call connect(connection) first.',
201
+ );
192
202
  }
193
203
  return {
194
204
  driver: this.driver,
@@ -229,7 +239,7 @@ class ControlClientImpl implements ControlClient {
229
239
  contract = familyInstance.deserializeContract(options.contract);
230
240
  } catch (error) {
231
241
  const message = error instanceof Error ? error.message : String(error);
232
- throw new ContractValidationError(message, error);
242
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
233
243
  }
234
244
 
235
245
  // Emit verify span
@@ -282,7 +292,7 @@ class ControlClientImpl implements ControlClient {
282
292
  contract = familyInstance.deserializeContract(options.contract);
283
293
  } catch (error) {
284
294
  const message = error instanceof Error ? error.message : String(error);
285
- throw new ContractValidationError(message, error);
295
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
286
296
  }
287
297
 
288
298
  // Emit schemaVerify span
@@ -336,7 +346,7 @@ class ControlClientImpl implements ControlClient {
336
346
  contract = familyInstance.deserializeContract(options.contract);
337
347
  } catch (error) {
338
348
  const message = error instanceof Error ? error.message : String(error);
339
- throw new ContractValidationError(message, error);
349
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
340
350
  }
341
351
 
342
352
  // Emit sign span
@@ -381,7 +391,10 @@ class ControlClientImpl implements ControlClient {
381
391
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
382
392
 
383
393
  if (!hasMigrations(this.options.target)) {
384
- throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
394
+ throw new CliStructuredError(
395
+ 'MIGRATION.TARGET_UNSUPPORTED',
396
+ `Target "${this.options.target.targetId}" does not support migrations`,
397
+ );
385
398
  }
386
399
 
387
400
  const adapter = this.buildControlAdapter();
@@ -391,7 +404,7 @@ class ControlClientImpl implements ControlClient {
391
404
  contract = familyInstance.deserializeContract(options.contract);
392
405
  } catch (error) {
393
406
  const message = error instanceof Error ? error.message : String(error);
394
- throw new ContractValidationError(message, error);
407
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
395
408
  }
396
409
 
397
410
  return executeDbInit({
@@ -415,7 +428,10 @@ class ControlClientImpl implements ControlClient {
415
428
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
416
429
 
417
430
  if (!hasMigrations(this.options.target)) {
418
- throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
431
+ throw new CliStructuredError(
432
+ 'MIGRATION.TARGET_UNSUPPORTED',
433
+ `Target "${this.options.target.targetId}" does not support migrations`,
434
+ );
419
435
  }
420
436
 
421
437
  const adapter = this.buildControlAdapter();
@@ -425,7 +441,7 @@ class ControlClientImpl implements ControlClient {
425
441
  contract = familyInstance.deserializeContract(options.contract);
426
442
  } catch (error) {
427
443
  const message = error instanceof Error ? error.message : String(error);
428
- throw new ContractValidationError(message, error);
444
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
429
445
  }
430
446
 
431
447
  return executeDbUpdate({
@@ -490,7 +506,10 @@ class ControlClientImpl implements ControlClient {
490
506
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
491
507
 
492
508
  if (!hasMigrations(this.options.target)) {
493
- throw new Error(`Target "${this.options.target.targetId}" does not support migrations`);
509
+ throw new CliStructuredError(
510
+ 'MIGRATION.TARGET_UNSUPPORTED',
511
+ `Target "${this.options.target.targetId}" does not support migrations`,
512
+ );
494
513
  }
495
514
 
496
515
  let contract: Contract;
@@ -498,7 +517,7 @@ class ControlClientImpl implements ControlClient {
498
517
  contract = familyInstance.deserializeContract(options.contract);
499
518
  } catch (error) {
500
519
  const message = error instanceof Error ? error.message : String(error);
501
- throw new ContractValidationError(message, error);
520
+ throw structuredError('CONTRACT.VALIDATION_FAILED', message, { cause: error });
502
521
  }
503
522
 
504
523
  return executeMigrate({
@@ -593,7 +612,7 @@ class ControlClientImpl implements ControlClient {
593
612
  this.init();
594
613
 
595
614
  if (!this.familyInstance) {
596
- throw new Error('Family instance was not initialized. This is a bug.');
615
+ throw new InternalError('Family instance was not initialized. This is a bug.');
597
616
  }
598
617
 
599
618
  let contractRaw: unknown;
@@ -24,6 +24,7 @@ import {
24
24
  } from '@prisma-next/migration-tools/aggregate';
25
25
  import { blindCast } from '@prisma-next/utils/casts';
26
26
  import { ifDefined } from '@prisma-next/utils/defined';
27
+ import { InternalError } from '@prisma-next/utils/internal-error';
27
28
  import { notOk, ok } from '@prisma-next/utils/result';
28
29
  import { CliStructuredError } from '../../utils/cli-errors';
29
30
  import {
@@ -200,7 +201,7 @@ export async function executeRun<TFamilyId extends string, TTargetId extends str
200
201
  // `destination` is the storage hash users see in CLI output.
201
202
  const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
202
203
  if (!appResolution) {
203
- throw new Error(
204
+ throw new InternalError(
204
205
  'Aggregate planner returned no plan for the app space — the planner is supposed to always emit one.',
205
206
  );
206
207
  }
@@ -24,6 +24,7 @@ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
24
24
  import { errorNoInvariantPath } from '@prisma-next/migration-tools/errors';
25
25
  import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
26
26
  import { ifDefined } from '@prisma-next/utils/defined';
27
+ import { InternalError } from '@prisma-next/utils/internal-error';
27
28
  import { notOk, ok } from '@prisma-next/utils/result';
28
29
  import {
29
30
  type BuildAggregateInputs,
@@ -247,7 +248,7 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
247
248
  .map((spaceId) => {
248
249
  const entry = perSpacePlans.get(spaceId) ?? atHeadResolutions.get(spaceId);
249
250
  if (entry === undefined) {
250
- throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
251
+ throw new InternalError(`Unreachable: missing per-space plan for "${spaceId}"`);
251
252
  }
252
253
  return { spaceId, entry };
253
254
  });
@@ -306,7 +307,7 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
306
307
  }
307
308
  const entry = atHeadResolutions.get(spaceId);
308
309
  if (entry === undefined) {
309
- throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
310
+ throw new InternalError(`Unreachable: missing per-space plan for "${spaceId}"`);
310
311
  }
311
312
  return { spaceId, entry };
312
313
  });
@@ -12,6 +12,7 @@ import type {
12
12
  } from '@prisma-next/framework-components/control';
13
13
  import type { ContractSpaceAggregate, PerSpacePlan } from '@prisma-next/migration-tools/aggregate';
14
14
  import { ifDefined } from '@prisma-next/utils/defined';
15
+ import { InternalError } from '@prisma-next/utils/internal-error';
15
16
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
16
17
  import type { OnControlProgress, PerSpaceExecutionEntry } from '../types';
17
18
 
@@ -243,7 +244,7 @@ export function collectOrdered(
243
244
  return applyOrder.map((spaceId) => {
244
245
  const entry = perSpace.get(spaceId);
245
246
  if (!entry) {
246
- throw new Error(`planner output missing per-space plan for "${spaceId}"`);
247
+ throw new InternalError(`planner output missing per-space plan for "${spaceId}"`);
247
248
  }
248
249
  return { spaceId, entry };
249
250
  });
@@ -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
  }
@@ -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
 
@@ -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
  }