@prisma-next/cli 0.8.0 → 0.9.0-dev.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 (163) hide show
  1. package/README.md +8 -9
  2. package/dist/{cli-errors-D3_sMh2K.mjs → cli-errors-CF60g2cG.mjs} +40 -2
  3. package/dist/cli-errors-CF60g2cG.mjs.map +1 -0
  4. package/dist/cli.mjs +67 -19
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-BCnP7cHo.mjs → client-Brv4qlfB.mjs} +28 -30
  7. package/dist/client-Brv4qlfB.mjs.map +1 -0
  8. package/dist/{command-helpers-BeZHkxV8.mjs → command-helpers-D3vL5yi8.mjs} +29 -6
  9. package/dist/command-helpers-D3vL5yi8.mjs.map +1 -0
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.mjs +7 -7
  13. package/dist/commands/db-schema.mjs +5 -5
  14. package/dist/commands/db-sign.d.mts.map +1 -1
  15. package/dist/commands/db-sign.mjs +67 -25
  16. package/dist/commands/db-sign.mjs.map +1 -1
  17. package/dist/commands/db-update.d.mts.map +1 -1
  18. package/dist/commands/db-update.mjs +37 -9
  19. package/dist/commands/db-update.mjs.map +1 -1
  20. package/dist/commands/db-verify.d.mts.map +1 -1
  21. package/dist/commands/db-verify.mjs +1 -1
  22. package/dist/commands/migrate.d.mts +28 -0
  23. package/dist/commands/migrate.d.mts.map +1 -0
  24. package/dist/commands/{migration-apply.mjs → migrate.mjs} +65 -39
  25. package/dist/commands/migrate.mjs.map +1 -0
  26. package/dist/commands/migration-check.d.mts +18 -0
  27. package/dist/commands/migration-check.d.mts.map +1 -0
  28. package/dist/commands/migration-check.mjs +284 -0
  29. package/dist/commands/migration-check.mjs.map +1 -0
  30. package/dist/commands/migration-graph.d.mts +16 -0
  31. package/dist/commands/migration-graph.d.mts.map +1 -0
  32. package/dist/commands/migration-graph.mjs +141 -0
  33. package/dist/commands/migration-graph.mjs.map +1 -0
  34. package/dist/commands/migration-list.d.mts +20 -0
  35. package/dist/commands/migration-list.d.mts.map +1 -0
  36. package/dist/commands/migration-list.mjs +107 -0
  37. package/dist/commands/migration-list.mjs.map +1 -0
  38. package/dist/commands/migration-log.d.mts +21 -0
  39. package/dist/commands/migration-log.d.mts.map +1 -0
  40. package/dist/commands/migration-log.mjs +146 -0
  41. package/dist/commands/migration-log.mjs.map +1 -0
  42. package/dist/commands/migration-new.d.mts.map +1 -1
  43. package/dist/commands/migration-new.mjs +30 -29
  44. package/dist/commands/migration-new.mjs.map +1 -1
  45. package/dist/commands/migration-plan.d.mts +2 -2
  46. package/dist/commands/migration-plan.d.mts.map +1 -1
  47. package/dist/commands/migration-plan.mjs +1 -1
  48. package/dist/commands/migration-show.d.mts +1 -1
  49. package/dist/commands/migration-show.d.mts.map +1 -1
  50. package/dist/commands/migration-show.mjs +90 -52
  51. package/dist/commands/migration-show.mjs.map +1 -1
  52. package/dist/commands/migration-status.d.mts +5 -17
  53. package/dist/commands/migration-status.d.mts.map +1 -1
  54. package/dist/commands/migration-status.mjs +732 -1
  55. package/dist/commands/migration-status.mjs.map +1 -0
  56. package/dist/commands/ref.d.mts +34 -0
  57. package/dist/commands/ref.d.mts.map +1 -0
  58. package/dist/commands/{migration-ref.mjs → ref.mjs} +28 -57
  59. package/dist/commands/ref.mjs.map +1 -0
  60. package/dist/{contract-emit-9DBda5Ou.mjs → contract-emit-C3STUIBg.mjs} +6 -6
  61. package/dist/{contract-emit-9DBda5Ou.mjs.map → contract-emit-C3STUIBg.mjs.map} +1 -1
  62. package/dist/{contract-emit-B77TsJqf.mjs → contract-emit-iynA3BCA.mjs} +9 -5
  63. package/dist/contract-emit-iynA3BCA.mjs.map +1 -0
  64. package/dist/{contract-infer-ByxhPjpW.mjs → contract-infer-Cnj8G1E2.mjs} +5 -5
  65. package/dist/{contract-infer-ByxhPjpW.mjs.map → contract-infer-Cnj8G1E2.mjs.map} +1 -1
  66. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs → contract-space-aggregate-loader-pAc8CDfY.mjs} +4 -4
  67. package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs.map → contract-space-aggregate-loader-pAc8CDfY.mjs.map} +1 -1
  68. package/dist/{db-verify-Czm5T-J4.mjs → db-verify-D7cyH_zz.mjs} +12 -9
  69. package/dist/db-verify-D7cyH_zz.mjs.map +1 -0
  70. package/dist/errors-Cw6kyTyV.mjs +56 -0
  71. package/dist/errors-Cw6kyTyV.mjs.map +1 -0
  72. package/dist/exports/control-api.d.mts +1 -1
  73. package/dist/exports/control-api.d.mts.map +1 -1
  74. package/dist/exports/control-api.mjs +2 -2
  75. package/dist/exports/index.mjs +1 -1
  76. package/dist/exports/index.mjs.map +1 -1
  77. package/dist/exports/init-output.mjs +1 -1
  78. package/dist/{framework-components-ChqVUxR-.mjs → framework-components-xFLFpZUO.mjs} +2 -2
  79. package/dist/{framework-components-ChqVUxR-.mjs.map → framework-components-xFLFpZUO.mjs.map} +1 -1
  80. package/dist/{global-flags-Icqpxk23.d.mts → global-flags-DGmw6Kqg.d.mts} +1 -1
  81. package/dist/{global-flags-Icqpxk23.d.mts.map → global-flags-DGmw6Kqg.d.mts.map} +1 -1
  82. package/dist/{migration-status-By9G5p2H.mjs → graph-render-eJDcLWny.mjs} +3 -692
  83. package/dist/graph-render-eJDcLWny.mjs.map +1 -0
  84. package/dist/{init-B-k3a1Qw.mjs → init-Bqg5JWg7.mjs} +133 -61
  85. package/dist/init-Bqg5JWg7.mjs.map +1 -0
  86. package/dist/{inspect-live-schema-DxdBd4Er.mjs → inspect-live-schema-CWLK_lgs.mjs} +4 -4
  87. package/dist/{inspect-live-schema-DxdBd4Er.mjs.map → inspect-live-schema-CWLK_lgs.mjs.map} +1 -1
  88. package/dist/migration-cli.mjs +1 -1
  89. package/dist/migration-cli.mjs.map +1 -1
  90. package/dist/{migration-command-scaffold-BdV8JYXV.mjs → migration-command-scaffold-CmXXC1UZ.mjs} +4 -4
  91. package/dist/{migration-command-scaffold-BdV8JYXV.mjs.map → migration-command-scaffold-CmXXC1UZ.mjs.map} +1 -1
  92. package/dist/{migration-plan-mRu5K81L.mjs → migration-plan-CHyUlBV0.mjs} +76 -37
  93. package/dist/migration-plan-CHyUlBV0.mjs.map +1 -0
  94. package/dist/migration-types-D2FW63pr.d.mts +15 -0
  95. package/dist/migration-types-D2FW63pr.d.mts.map +1 -0
  96. package/dist/{migrations-CTsyBXCA.mjs → migrations-DyUf5lTt.mjs} +2 -2
  97. package/dist/migrations-DyUf5lTt.mjs.map +1 -0
  98. package/dist/{output-BVj6a971.mjs → output-B60Gw5fu.mjs} +12 -11
  99. package/dist/{output-BVj6a971.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  100. package/dist/{result-handler-rmPVKIP2.mjs → result-handler-Bm_6dDYg.mjs} +2 -2
  101. package/dist/{result-handler-rmPVKIP2.mjs.map → result-handler-Bm_6dDYg.mjs.map} +1 -1
  102. package/dist/{terminal-ui-C_hFNbAn.mjs → terminal-ui-XtOQsqe9.mjs} +2 -54
  103. package/dist/terminal-ui-XtOQsqe9.mjs.map +1 -0
  104. package/dist/{types-LItU7E4l.d.mts → types-0aS865QN.d.mts} +14 -8
  105. package/dist/types-0aS865QN.d.mts.map +1 -0
  106. package/dist/{verify-CiwNWM9N.mjs → verify-D7ypCCe6.mjs} +1 -1
  107. package/dist/{verify-CiwNWM9N.mjs.map → verify-D7ypCCe6.mjs.map} +1 -1
  108. package/package.json +39 -23
  109. package/src/cli.ts +78 -15
  110. package/src/commands/db-sign.ts +102 -32
  111. package/src/commands/db-update.ts +56 -4
  112. package/src/commands/db-verify.ts +19 -3
  113. package/src/commands/init/agent-skill-install.ts +145 -43
  114. package/src/commands/init/errors.ts +2 -2
  115. package/src/commands/init/exit-codes.ts +2 -2
  116. package/src/commands/init/index.ts +1 -1
  117. package/src/commands/init/init.ts +15 -6
  118. package/src/commands/init/inputs.ts +1 -1
  119. package/src/commands/init/output.ts +22 -17
  120. package/src/commands/{migration-apply.ts → migrate.ts} +77 -73
  121. package/src/commands/migration-check/exit-codes.ts +3 -0
  122. package/src/commands/migration-check.ts +369 -0
  123. package/src/commands/migration-graph.ts +184 -0
  124. package/src/commands/migration-list.ts +155 -0
  125. package/src/commands/migration-log.ts +218 -0
  126. package/src/commands/migration-new.ts +30 -22
  127. package/src/commands/migration-plan.ts +104 -35
  128. package/src/commands/migration-show.ts +141 -65
  129. package/src/commands/migration-status.ts +82 -69
  130. package/src/commands/{migration-ref.ts → ref.ts} +32 -86
  131. package/src/control-api/client.ts +30 -21
  132. package/src/control-api/operations/apply-aggregate.ts +4 -4
  133. package/src/control-api/operations/contract-emit.ts +26 -3
  134. package/src/control-api/operations/db-apply-aggregate.ts +4 -3
  135. package/src/control-api/operations/db-verify.ts +2 -2
  136. package/src/control-api/operations/migration-apply.ts +5 -4
  137. package/src/control-api/types.ts +12 -7
  138. package/src/load-ts-contract.ts +9 -1
  139. package/src/migration-cli.ts +1 -1
  140. package/src/utils/cli-errors.ts +37 -0
  141. package/src/utils/command-helpers.ts +28 -3
  142. package/src/utils/contract-space-aggregate-loader.ts +4 -4
  143. package/src/utils/contract-space-seed-phase.ts +2 -2
  144. package/src/utils/formatters/help.ts +12 -2
  145. package/src/utils/formatters/migrations.ts +2 -2
  146. package/dist/cli-errors-D3_sMh2K.mjs.map +0 -1
  147. package/dist/client-BCnP7cHo.mjs.map +0 -1
  148. package/dist/command-helpers-BeZHkxV8.mjs.map +0 -1
  149. package/dist/commands/migration-apply.d.mts +0 -51
  150. package/dist/commands/migration-apply.d.mts.map +0 -1
  151. package/dist/commands/migration-apply.mjs.map +0 -1
  152. package/dist/commands/migration-ref.d.mts +0 -45
  153. package/dist/commands/migration-ref.d.mts.map +0 -1
  154. package/dist/commands/migration-ref.mjs.map +0 -1
  155. package/dist/contract-emit-B77TsJqf.mjs.map +0 -1
  156. package/dist/db-verify-Czm5T-J4.mjs.map +0 -1
  157. package/dist/init-B-k3a1Qw.mjs.map +0 -1
  158. package/dist/migration-plan-mRu5K81L.mjs.map +0 -1
  159. package/dist/migration-status-By9G5p2H.mjs.map +0 -1
  160. package/dist/migrations-CTsyBXCA.mjs.map +0 -1
  161. package/dist/terminal-ui-C_hFNbAn.mjs.map +0 -1
  162. package/dist/types-LItU7E4l.d.mts.map +0 -1
  163. /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
package/src/cli.ts CHANGED
@@ -12,17 +12,44 @@ import { createDbSchemaCommand } from './commands/db-schema';
12
12
  import { createDbSignCommand } from './commands/db-sign';
13
13
  import { createDbUpdateCommand } from './commands/db-update';
14
14
  import { createDbVerifyCommand } from './commands/db-verify';
15
- import { createMigrationApplyCommand } from './commands/migration-apply';
15
+ import { createMigrateCommand } from './commands/migrate';
16
+ import { createMigrationCheckCommand } from './commands/migration-check';
17
+ import { createMigrationGraphCommand } from './commands/migration-graph';
18
+ import { createMigrationListCommand } from './commands/migration-list';
19
+ import { createMigrationLogCommand } from './commands/migration-log';
16
20
  import { createMigrationNewCommand } from './commands/migration-new';
17
21
  import { createMigrationPlanCommand } from './commands/migration-plan';
18
- import { createMigrationRefCommand } from './commands/migration-ref';
19
22
  import { createMigrationShowCommand } from './commands/migration-show';
20
23
  import { createMigrationStatusCommand } from './commands/migration-status';
24
+ import { createRefCommand } from './commands/ref';
21
25
  import { setCommandDescriptions } from './utils/command-helpers';
22
26
  import { formatCommandHelp, formatRootHelp } from './utils/formatters/help';
23
27
  import { parseGlobalFlags } from './utils/global-flags';
24
28
  import { suggestCommands } from './utils/suggest-command';
25
29
 
30
+ /**
31
+ * Lookup table mapping removed subcommands to their replacement verbs.
32
+ * Keyed by `<parent>:<subcommand>` (e.g. `migration:apply`).
33
+ * The handler consults this before falling back to the fuzzy suggest engine.
34
+ */
35
+ const removedVerbRedirects: Record<string, string> = {
36
+ 'migration:apply': 'Use `prisma-next migrate --to <contract>` instead.',
37
+ 'migration:ref': 'Use `prisma-next ref set|list|delete` instead.',
38
+ };
39
+
40
+ /**
41
+ * Removed flags on specific subcommands. Keyed by `<parent>:<sub>:<flag>`.
42
+ * Checked during the pre-parse argv scan before commander sees the flags.
43
+ */
44
+ const removedFlagRedirects: Record<string, string> = {
45
+ 'migration:status:graph': 'Use `prisma-next migration graph` to view the migration graph.',
46
+ 'migration:status:all':
47
+ 'Use `prisma-next migration log --db <url>` to view the full execution history.',
48
+ 'migration:status:limit':
49
+ 'Use `prisma-next migration log --db <url>` to view the full execution history.',
50
+ 'migration:status:ref': 'Use `--to <contract>` instead of `--ref`.',
51
+ };
52
+
26
53
  /**
27
54
  * Formats the "Did you mean ...?" hint for an unknown command.
28
55
  */
@@ -177,9 +204,6 @@ contractCommand.addCommand(contractEmitCommand);
177
204
  const contractInferCommand = createContractInferCommand();
178
205
  contractCommand.addCommand(contractInferCommand);
179
206
 
180
- // Register contract command
181
- program.addCommand(contractCommand);
182
-
183
207
  // Register db subcommand
184
208
  const dbCommand = new Command('db');
185
209
  setCommandDescriptions(
@@ -216,9 +240,6 @@ dbCommand.addCommand(dbSchemaCommand);
216
240
  const dbSignCommand = createDbSignCommand();
217
241
  dbCommand.addCommand(dbSignCommand);
218
242
 
219
- // Register db command
220
- program.addCommand(dbCommand);
221
-
222
243
  // Register migration subcommand
223
244
  const migrationCommand = new Command('migration');
224
245
  setCommandDescriptions(
@@ -247,17 +268,38 @@ migrationCommand.addCommand(migrationShowCommand);
247
268
  const migrationStatusCommand = createMigrationStatusCommand();
248
269
  migrationCommand.addCommand(migrationStatusCommand);
249
270
 
250
- const migrationApplyCommand = createMigrationApplyCommand();
251
- migrationCommand.addCommand(migrationApplyCommand);
271
+ const migrationLogCommand = createMigrationLogCommand();
272
+ migrationCommand.addCommand(migrationLogCommand);
252
273
 
253
- const migrationRefCommand = createMigrationRefCommand();
254
- migrationCommand.addCommand(migrationRefCommand);
274
+ const migrationListCommand = createMigrationListCommand();
275
+ migrationCommand.addCommand(migrationListCommand);
255
276
 
256
- program.addCommand(migrationCommand);
277
+ const migrationGraphCommand = createMigrationGraphCommand();
278
+ migrationCommand.addCommand(migrationGraphCommand);
279
+
280
+ const migrationCheckCommand = createMigrationCheckCommand();
281
+ migrationCommand.addCommand(migrationCheckCommand);
282
+
283
+ // Top-level migrate command
284
+ const migrateCommand = createMigrateCommand();
257
285
 
258
- // Register init command (top-level, not nested)
286
+ // Top-level ref command (replaces `migration ref`)
287
+ const refCommand = createRefCommand();
288
+
289
+ // Top-level init command
259
290
  const initCommand = createInitCommand();
291
+
292
+ // Register top-level commands in the order the spec's intended-surface
293
+ // diagram lists them: verbs (init, migrate) first, then subject
294
+ // namespaces (contract, db, migration, ref). The order shows up in
295
+ // `prisma-next --help` and is the first thing a new user sees, so it
296
+ // matches the order spec.md uses to introduce the surface.
260
297
  program.addCommand(initCommand);
298
+ program.addCommand(migrateCommand);
299
+ program.addCommand(contractCommand);
300
+ program.addCommand(dbCommand);
301
+ program.addCommand(migrationCommand);
302
+ program.addCommand(refCommand);
261
303
 
262
304
  // Create help command
263
305
  const helpCommand = new Command('help')
@@ -324,7 +366,28 @@ if (args.length > 0) {
324
366
  const helpText = formatRootHelp({ program, flags });
325
367
  process.stderr.write(`${helpText}\n`);
326
368
  process.exit(2);
327
- } else if (command.commands.length > 0 && args.length === 1) {
369
+ } else if (command.commands.length > 0 && args.length >= 2) {
370
+ const subcommandName = args[1];
371
+ const redirectKey = `${commandName}:${subcommandName}`;
372
+ const redirect = removedVerbRedirects[redirectKey];
373
+ if (redirect) {
374
+ process.stderr.write(`Unknown command: ${subcommandName}\n${redirect}\n`);
375
+ process.exit(2);
376
+ }
377
+ for (let i = 2; i < args.length; i++) {
378
+ const arg = args[i]!;
379
+ if (!arg.startsWith('--')) continue;
380
+ const flagName = arg.slice(2);
381
+ const flagKey = `${commandName}:${subcommandName}:${flagName}`;
382
+ const flagRedirect = removedFlagRedirects[flagKey];
383
+ if (flagRedirect) {
384
+ process.stderr.write(`Unknown option: ${arg}\n${flagRedirect}\n`);
385
+ process.exit(2);
386
+ }
387
+ }
388
+ }
389
+
390
+ if (command.commands.length > 0 && args.length === 1) {
328
391
  // Parent command called with no subcommand. Same shape as the
329
392
  // no-args case above: the user did not request help, we are
330
393
  // voluntarily rendering it as decoration around an underspecified
@@ -3,9 +3,12 @@ import type {
3
3
  SignDatabaseResult,
4
4
  VerifyDatabaseSchemaResult,
5
5
  } from '@prisma-next/framework-components/control';
6
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
7
+ import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
8
+ import { readRefs } from '@prisma-next/migration-tools/refs';
6
9
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
7
10
  import { Command } from 'commander';
8
- import { relative, resolve } from 'pathe';
11
+ import { join, relative, resolve } from 'pathe';
9
12
  import { loadConfig } from '../config-loader';
10
13
  import { createControlClient } from '../control-api/client';
11
14
  import { ContractValidationError } from '../control-api/errors';
@@ -15,12 +18,17 @@ import {
15
18
  errorDatabaseConnectionRequired,
16
19
  errorDriverRequired,
17
20
  errorFileNotFound,
21
+ errorRuntime,
18
22
  errorUnexpected,
23
+ mapMigrationToolsError,
24
+ mapRefResolutionError,
19
25
  } from '../utils/cli-errors';
20
26
  import {
21
27
  addGlobalOptions,
28
+ loadMigrationPackages,
22
29
  maskConnectionUrl,
23
30
  resolveContractPath,
31
+ resolveMigrationPaths,
24
32
  setCommandDescriptions,
25
33
  setCommandExamples,
26
34
  } from '../utils/command-helpers';
@@ -40,6 +48,7 @@ import { TerminalUI } from '../utils/terminal-ui';
40
48
  interface DbSignOptions extends CommonCommandOptions {
41
49
  readonly db?: string;
42
50
  readonly config?: string;
51
+ readonly contract?: string;
43
52
  }
44
53
 
45
54
  /**
@@ -54,11 +63,11 @@ type DbSignFailure = CliStructuredError | VerifyDatabaseSchemaResult;
54
63
  * Failure: CliStructuredError (infra error) or VerifyDatabaseSchemaResult (schema mismatch)
55
64
  */
56
65
  async function executeDbSignCommand(
66
+ contractArg: string | undefined,
57
67
  options: DbSignOptions,
58
68
  flags: GlobalFlags,
59
69
  ui: TerminalUI,
60
70
  ): Promise<Result<SignDatabaseResult, DbSignFailure>> {
61
- // Load config
62
71
  const config = await loadConfig(options.config);
63
72
  const configPath = options.config
64
73
  ? relative(process.cwd(), resolve(options.config))
@@ -66,11 +75,12 @@ async function executeDbSignCommand(
66
75
  const contractPathAbsolute = resolveContractPath(config);
67
76
  const contractPath = relative(process.cwd(), contractPathAbsolute);
68
77
 
69
- // Output header
78
+ const effectiveContractArg = contractArg ?? options.contract;
79
+
70
80
  if (!flags.json && !flags.quiet) {
71
81
  const details: Array<{ label: string; value: string }> = [
72
82
  { label: 'config', value: configPath },
73
- { label: 'contract', value: contractPath },
83
+ { label: 'contract', value: effectiveContractArg ?? contractPath },
74
84
  ];
75
85
  if (options.db) {
76
86
  details.push({ label: 'database', value: maskConnectionUrl(options.db) });
@@ -85,36 +95,79 @@ async function executeDbSignCommand(
85
95
  ui.stderr(header);
86
96
  }
87
97
 
88
- // Load contract file
89
- let contractJsonContent: string;
90
- try {
91
- contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
92
- } catch (error) {
93
- if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
98
+ let contractJson: Record<string, unknown>;
99
+
100
+ if (effectiveContractArg) {
101
+ try {
102
+ const { appMigrationsDir, refsDir } = resolveMigrationPaths(options.config, config);
103
+ const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
104
+ const refs = await readRefs(refsDir);
105
+ const refResult = parseContractRef(effectiveContractArg, { graph, refs });
106
+ if (!refResult.ok) {
107
+ return notOk(mapRefResolutionError(refResult.failure));
108
+ }
109
+ const targetHash = refResult.value.hash;
110
+ const matchingBundle = bundles.find((p) => p.metadata.to === targetHash);
111
+ if (matchingBundle) {
112
+ const endContractPath = join(matchingBundle.dirPath, 'end-contract.json');
113
+ const raw = await readFile(endContractPath, 'utf-8');
114
+ contractJson = JSON.parse(raw) as Record<string, unknown>;
115
+ } else {
116
+ const defaultRaw = await readFile(contractPathAbsolute, 'utf-8');
117
+ const defaultContract = JSON.parse(defaultRaw) as Record<string, unknown>;
118
+ const storageHash = (defaultContract['storage'] as Record<string, unknown> | undefined)?.[
119
+ 'storageHash'
120
+ ];
121
+ if (storageHash === targetHash) {
122
+ contractJson = defaultContract;
123
+ } else {
124
+ return notOk(
125
+ errorRuntime(`No contract file found for hash "${targetHash}"`, {
126
+ why: `Resolved contract reference "${effectiveContractArg}" to hash "${targetHash}" but no migration produces that hash and the emitted contract does not match.`,
127
+ fix: 'Ensure the target contract exists on disk — either as a migration endpoint or as the emitted contract.json.',
128
+ }),
129
+ );
130
+ }
131
+ }
132
+ } catch (error) {
133
+ if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
134
+ if (error instanceof CliStructuredError) return notOk(error);
94
135
  return notOk(
95
- errorFileNotFound(contractPathAbsolute, {
96
- why: `Contract file not found at ${contractPathAbsolute}`,
97
- fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
136
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
137
+ why: `Failed to resolve contract reference: ${error instanceof Error ? error.message : String(error)}`,
138
+ }),
139
+ );
140
+ }
141
+ } else {
142
+ let contractJsonContent: string;
143
+ try {
144
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
145
+ } catch (error) {
146
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
147
+ return notOk(
148
+ errorFileNotFound(contractPathAbsolute, {
149
+ why: `Contract file not found at ${contractPathAbsolute}`,
150
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
151
+ }),
152
+ );
153
+ }
154
+ return notOk(
155
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
156
+ why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
98
157
  }),
99
158
  );
100
159
  }
101
- return notOk(
102
- errorUnexpected(error instanceof Error ? error.message : String(error), {
103
- why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
104
- }),
105
- );
106
- }
107
160
 
108
- let contractJson: Record<string, unknown>;
109
- try {
110
- contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
111
- } catch (error) {
112
- return notOk(
113
- errorContractValidationFailed(
114
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
115
- { where: { path: contractPathAbsolute } },
116
- ),
117
- );
161
+ try {
162
+ contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
163
+ } catch (error) {
164
+ return notOk(
165
+ errorContractValidationFailed(
166
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
167
+ { where: { path: contractPathAbsolute } },
168
+ ),
169
+ );
170
+ }
118
171
  }
119
172
 
120
173
  // Resolve database connection (--db flag or config.db.connection)
@@ -203,16 +256,33 @@ export function createDbSignCommand(): Command {
203
256
  'in CI/deployment pipelines. The signature records that this database instance is aligned\n' +
204
257
  'with a specific contract version.',
205
258
  );
206
- setCommandExamples(command, ['prisma-next db sign --db $DATABASE_URL']);
259
+ setCommandExamples(command, [
260
+ 'prisma-next db sign --db $DATABASE_URL',
261
+ 'prisma-next db sign production --db $DATABASE_URL',
262
+ 'prisma-next db sign --contract production --db $DATABASE_URL',
263
+ ]);
207
264
  addGlobalOptions(command)
265
+ .argument('[contract]', 'Contract reference (hash, prefix, ref name, or migration dir name)')
208
266
  .option('--db <url>', 'Database connection string')
209
267
  .option('--config <path>', 'Path to prisma-next.config.ts')
210
- .action(async (options: DbSignOptions) => {
268
+ .option(
269
+ '--contract <contract>',
270
+ 'Contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
271
+ )
272
+ .action(async (positionalContract: string | undefined, options: DbSignOptions) => {
211
273
  const flags = parseGlobalFlags(options);
212
274
 
275
+ if (positionalContract && options.contract) {
276
+ process.stderr.write(
277
+ 'Cannot specify both a positional contract argument and --contract flag.\n',
278
+ );
279
+ process.exit(2);
280
+ return;
281
+ }
282
+
213
283
  const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
214
284
 
215
- const result = await executeDbSignCommand(options, flags, ui);
285
+ const result = await executeDbSignCommand(positionalContract, options, flags, ui);
216
286
 
217
287
  if (result.ok) {
218
288
  // Success - format sign output
@@ -1,6 +1,11 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
+ import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
4
+ import { readRefs } from '@prisma-next/migration-tools/refs';
1
5
  import { ifDefined } from '@prisma-next/utils/defined';
2
6
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
3
7
  import { Command } from 'commander';
8
+ import { join } from 'pathe';
4
9
  import { ContractValidationError } from '../control-api/errors';
5
10
  import type { DbUpdateFailure } from '../control-api/types';
6
11
  import {
@@ -11,9 +16,12 @@ import {
11
16
  errorMigrationPlanningFailed,
12
17
  errorRunnerFailed,
13
18
  errorUnexpected,
19
+ mapMigrationToolsError,
20
+ mapRefResolutionError,
14
21
  } from '../utils/cli-errors';
15
22
  import type { MigrationCommandOptions } from '../utils/command-helpers';
16
23
  import {
24
+ loadMigrationPackages,
17
25
  resolveMigrationPaths,
18
26
  sanitizeErrorMessage,
19
27
  setCommandDescriptions,
@@ -33,7 +41,9 @@ import {
33
41
  import { handleResult } from '../utils/result-handler';
34
42
  import { TerminalUI } from '../utils/terminal-ui';
35
43
 
36
- type DbUpdateOptions = MigrationCommandOptions;
44
+ interface DbUpdateOptions extends MigrationCommandOptions {
45
+ readonly to?: string;
46
+ }
37
47
 
38
48
  /**
39
49
  * Maps a DbUpdateFailure to a CliStructuredError for consistent error handling.
@@ -81,9 +91,47 @@ async function executeDbUpdateCommand(
81
91
  if (!ctxResult.ok) {
82
92
  return ctxResult;
83
93
  }
84
- const { client, config, contractJson, dbConnection, onProgress, contractPathAbsolute } =
85
- ctxResult.value;
86
- const { migrationsDir } = resolveMigrationPaths(options.config, config);
94
+ const { client, config, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
95
+ let { contractJson } = ctxResult.value;
96
+ const { migrationsDir, appMigrationsDir, refsDir } = resolveMigrationPaths(
97
+ options.config,
98
+ config,
99
+ );
100
+
101
+ if (options.to) {
102
+ try {
103
+ const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
104
+ const refs = await readRefs(refsDir);
105
+ const refResult = parseContractRef(options.to, { graph, refs });
106
+ if (!refResult.ok) {
107
+ return notOk(mapRefResolutionError(refResult.failure));
108
+ }
109
+ const targetHash = refResult.value.hash;
110
+ const matchingBundle = bundles.find((p) => p.metadata.to === targetHash);
111
+ if (!matchingBundle) {
112
+ return notOk(
113
+ errorUnexpected(
114
+ `No migration bundle found for --to "${options.to}" (resolved hash: ${targetHash})`,
115
+ {
116
+ why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${targetHash}.`,
117
+ fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
118
+ },
119
+ ),
120
+ );
121
+ }
122
+ const endContractPath = join(matchingBundle.dirPath, 'end-contract.json');
123
+ const raw = await readFile(endContractPath, 'utf-8');
124
+ contractJson = JSON.parse(raw) as Record<string, unknown>;
125
+ } catch (error) {
126
+ if (MigrationToolsError.is(error)) {
127
+ return notOk(mapMigrationToolsError(error));
128
+ }
129
+ if (CliStructuredError.is(error)) {
130
+ return notOk(error);
131
+ }
132
+ throw error;
133
+ }
134
+ }
87
135
 
88
136
  try {
89
137
  await client.connect(dbConnection);
@@ -185,6 +233,10 @@ export function createDbUpdateCommand(): Command {
185
233
  'prisma-next db update --db $DATABASE_URL --dry-run',
186
234
  ]);
187
235
  addMigrationCommandOptions(command);
236
+ command.option(
237
+ '--to <contract>',
238
+ 'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
239
+ );
188
240
  command.action(async (options: DbUpdateOptions) => {
189
241
  const flags = parseGlobalFlags(options);
190
242
  const startTime = Date.now();
@@ -1,9 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import type { Contract } from '@prisma-next/contract/types';
2
3
  import type {
3
4
  VerifyDatabaseResult,
4
5
  VerifyDatabaseSchemaResult,
5
6
  } from '@prisma-next/framework-components/control';
6
7
  import {
8
+ createControlStack,
7
9
  VERIFY_CODE_HASH_MISMATCH,
8
10
  VERIFY_CODE_MARKER_MISSING,
9
11
  VERIFY_CODE_TARGET_MISMATCH,
@@ -236,7 +238,7 @@ async function resolveVerifyPaths(options: DbVerifyOptions) {
236
238
  type VerifyPaths = Awaited<ReturnType<typeof resolveVerifyPaths>>;
237
239
 
238
240
  interface VerifySetup extends VerifyPaths {
239
- readonly contractJson: Record<string, unknown>;
241
+ readonly contractJson: Contract;
240
242
  readonly dbConnection: string;
241
243
  }
242
244
 
@@ -266,10 +268,24 @@ async function resolveVerifySetup(
266
268
  );
267
269
  }
268
270
 
269
- let contractJson: Record<string, unknown>;
271
+ // Cross the family `deserializeContract` seam at the read site, just
272
+ // like every other CLI on-disk read (TML-2536). The downstream
273
+ // `dbVerify` op accepts the hydrated `Contract` directly and no
274
+ // longer re-deserializes.
275
+ const stack = createControlStack(config);
276
+ const familyInstance = config.family.create(stack);
277
+
278
+ let contractJson: Contract;
270
279
  try {
271
- contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
280
+ contractJson = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
272
281
  } catch (error) {
282
+ if (error instanceof ContractValidationError) {
283
+ return notOk(
284
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
285
+ where: { path: contractPathAbsolute },
286
+ }),
287
+ );
288
+ }
273
289
  return notOk(
274
290
  errorContractValidationFailed(
275
291
  `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,