@prisma-next/cli 0.3.0-dev.53 → 0.3.0-dev.55

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 (62) hide show
  1. package/README.md +24 -0
  2. package/dist/cli.mjs +5 -3
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-BSZKpZTF.mjs → client-B7f4PZZ1.mjs} +367 -170
  5. package/dist/client-B7f4PZZ1.mjs.map +1 -0
  6. package/dist/commands/contract-emit.d.mts.map +1 -1
  7. package/dist/commands/contract-emit.mjs +7 -6
  8. package/dist/commands/contract-emit.mjs.map +1 -1
  9. package/dist/commands/db-init.d.mts.map +1 -1
  10. package/dist/commands/db-init.mjs +28 -76
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-introspect.d.mts.map +1 -1
  13. package/dist/commands/db-introspect.mjs +12 -17
  14. package/dist/commands/db-introspect.mjs.map +1 -1
  15. package/dist/commands/db-schema-verify.d.mts.map +1 -1
  16. package/dist/commands/db-schema-verify.mjs +5 -4
  17. package/dist/commands/db-schema-verify.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -5
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts +7 -0
  22. package/dist/commands/db-update.d.mts.map +1 -0
  23. package/dist/commands/db-update.mjs +120 -0
  24. package/dist/commands/db-update.mjs.map +1 -0
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +5 -4
  27. package/dist/commands/db-verify.mjs.map +1 -1
  28. package/dist/{config-loader-BJ8HsEdA.mjs → config-loader-DqKf1qSa.mjs} +1 -1
  29. package/dist/{config-loader-BJ8HsEdA.mjs.map → config-loader-DqKf1qSa.mjs.map} +1 -1
  30. package/dist/config-loader.mjs +1 -1
  31. package/dist/exports/control-api.d.mts +96 -6
  32. package/dist/exports/control-api.d.mts.map +1 -1
  33. package/dist/exports/control-api.mjs +2 -2
  34. package/dist/exports/index.mjs +1 -3
  35. package/dist/exports/index.mjs.map +1 -1
  36. package/dist/migration-command-scaffold-BELw_do2.mjs +95 -0
  37. package/dist/migration-command-scaffold-BELw_do2.mjs.map +1 -0
  38. package/dist/{result-handler-BZPY7HX4.mjs → result-handler-BhmrXIvT.mjs} +63 -13
  39. package/dist/result-handler-BhmrXIvT.mjs.map +1 -0
  40. package/package.json +14 -10
  41. package/src/cli.ts +5 -0
  42. package/src/commands/contract-emit.ts +22 -6
  43. package/src/commands/db-init.ts +89 -197
  44. package/src/commands/db-introspect.ts +4 -8
  45. package/src/commands/db-schema-verify.ts +11 -2
  46. package/src/commands/db-sign.ts +13 -4
  47. package/src/commands/db-update.ts +220 -0
  48. package/src/commands/db-verify.ts +11 -2
  49. package/src/control-api/client.ts +109 -145
  50. package/src/control-api/errors.ts +9 -0
  51. package/src/control-api/operations/db-init.ts +39 -34
  52. package/src/control-api/operations/db-update.ts +221 -0
  53. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  54. package/src/control-api/operations/migration-helpers.ts +49 -0
  55. package/src/control-api/types.ts +104 -4
  56. package/src/exports/control-api.ts +5 -0
  57. package/src/utils/cli-errors.ts +2 -0
  58. package/src/utils/command-helpers.ts +81 -3
  59. package/src/utils/migration-command-scaffold.ts +189 -0
  60. package/src/utils/output.ts +43 -13
  61. package/dist/client-BSZKpZTF.mjs.map +0 -1
  62. package/dist/result-handler-BZPY7HX4.mjs.map +0 -1
@@ -1,51 +1,34 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { relative, resolve } from 'node:path';
3
1
  import { ifDefined } from '@prisma-next/utils/defined';
4
2
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
3
  import { Command } from 'commander';
6
- import { loadConfig } from '../config-loader';
7
- import { createControlClient } from '../control-api/client';
4
+ import { ContractValidationError } from '../control-api/errors';
8
5
  import type { DbInitFailure } from '../control-api/types';
9
6
  import {
10
7
  CliStructuredError,
11
8
  errorContractValidationFailed,
12
- errorDatabaseConnectionRequired,
13
- errorDriverRequired,
14
- errorFileNotFound,
15
9
  errorJsonFormatNotSupported,
16
10
  errorMigrationPlanningFailed,
11
+ errorRunnerFailed,
17
12
  errorRuntime,
18
- errorTargetMigrationNotSupported,
19
13
  errorUnexpected,
20
14
  } from '../utils/cli-errors';
21
- import { setCommandDescriptions } from '../utils/command-helpers';
15
+ import type { MigrationCommandOptions } from '../utils/command-helpers';
16
+ import { sanitizeErrorMessage, setCommandDescriptions } from '../utils/command-helpers';
22
17
  import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
23
18
  import {
24
- type DbInitResult,
19
+ addMigrationCommandOptions,
20
+ prepareMigrationContext,
21
+ } from '../utils/migration-command-scaffold';
22
+ import {
25
23
  formatCommandHelp,
26
- formatDbInitApplyOutput,
27
- formatDbInitJson,
28
- formatDbInitPlanOutput,
29
- formatStyledHeader,
24
+ formatMigrationApplyOutput,
25
+ formatMigrationJson,
26
+ formatMigrationPlanOutput,
27
+ type MigrationCommandResult,
30
28
  } from '../utils/output';
31
- import { createProgressAdapter } from '../utils/progress-adapter';
32
29
  import { handleResult } from '../utils/result-handler';
33
30
 
34
- interface DbInitOptions {
35
- readonly db?: string;
36
- readonly config?: string;
37
- readonly plan?: boolean;
38
- readonly json?: string | boolean;
39
- readonly quiet?: boolean;
40
- readonly q?: boolean;
41
- readonly verbose?: boolean;
42
- readonly v?: boolean;
43
- readonly vv?: boolean;
44
- readonly trace?: boolean;
45
- readonly timestamps?: boolean;
46
- readonly color?: boolean;
47
- readonly 'no-color'?: boolean;
48
- }
31
+ type DbInitOptions = MigrationCommandOptions;
49
32
 
50
33
  /**
51
34
  * Maps a DbInitFailure to a CliStructuredError for consistent error handling.
@@ -77,9 +60,9 @@ function mapDbInitFailure(failure: DbInitFailure): CliStructuredError {
77
60
  }
78
61
 
79
62
  return errorRuntime(
80
- `Existing contract marker does not match plan destination.${mismatchParts.length > 0 ? ` Mismatch in ${mismatchParts.join(' and ')}.` : ''}`,
63
+ `Existing database signature does not match plan destination.${mismatchParts.length > 0 ? ` Mismatch in ${mismatchParts.join(' and ')}.` : ''}`,
81
64
  {
82
- why: 'Database has an existing contract marker that does not match the target contract',
65
+ why: 'Database has an existing signature (marker) that does not match the target contract',
83
66
  fix: 'If bootstrapping, drop/reset the database then re-run `prisma-next db init`; otherwise reconcile schema/marker using your migration workflow',
84
67
  meta: {
85
68
  code: 'MARKER_ORIGIN_MISMATCH',
@@ -93,13 +76,12 @@ function mapDbInitFailure(failure: DbInitFailure): CliStructuredError {
93
76
  }
94
77
 
95
78
  if (failure.code === 'RUNNER_FAILED') {
96
- return errorRuntime(failure.summary, {
79
+ return errorRunnerFailed(failure.summary, {
97
80
  why: failure.why ?? 'Migration runner failed',
98
81
  fix: 'Fix the schema mismatch (db init is additive-only), or drop/reset the database and re-run `prisma-next db init`',
99
- meta: {
100
- code: 'RUNNER_FAILED',
101
- ...(failure.meta ?? {}),
102
- },
82
+ ...(failure.meta
83
+ ? { meta: { code: 'RUNNER_FAILED', ...failure.meta } }
84
+ : { meta: { code: 'RUNNER_FAILED' } }),
103
85
  });
104
86
  }
105
87
 
@@ -115,110 +97,20 @@ async function executeDbInitCommand(
115
97
  options: DbInitOptions,
116
98
  flags: GlobalFlags,
117
99
  startTime: number,
118
- ): Promise<Result<DbInitResult, CliStructuredError>> {
119
- // Load config
120
- const config = await loadConfig(options.config);
121
- const configPath = options.config
122
- ? relative(process.cwd(), resolve(options.config))
123
- : 'prisma-next.config.ts';
124
- const contractPathAbsolute = config.contract?.output
125
- ? resolve(config.contract.output)
126
- : resolve('src/prisma/contract.json');
127
- const contractPath = relative(process.cwd(), contractPathAbsolute);
128
-
129
- // Output header
130
- if (flags.json !== 'object' && !flags.quiet) {
131
- const details: Array<{ label: string; value: string }> = [
132
- { label: 'config', value: configPath },
133
- { label: 'contract', value: contractPath },
134
- ];
135
- if (options.db) {
136
- details.push({ label: 'database', value: options.db });
137
- }
138
- if (options.plan) {
139
- details.push({ label: 'mode', value: 'plan (dry run)' });
140
- }
141
- const header = formatStyledHeader({
142
- command: 'db init',
143
- description: 'Bootstrap a database to match the current contract',
144
- url: 'https://pris.ly/db-init',
145
- details,
146
- flags,
147
- });
148
- console.log(header);
149
- }
150
-
151
- // Load contract file
152
- let contractJsonContent: string;
153
- try {
154
- contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
155
- } catch (error) {
156
- if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
157
- return notOk(
158
- errorFileNotFound(contractPathAbsolute, {
159
- why: `Contract file not found at ${contractPathAbsolute}`,
160
- fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
161
- }),
162
- );
163
- }
164
- return notOk(
165
- errorUnexpected(error instanceof Error ? error.message : String(error), {
166
- why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
167
- }),
168
- );
169
- }
170
-
171
- let contractJson: Record<string, unknown>;
172
- try {
173
- contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
174
- } catch (error) {
175
- return notOk(
176
- errorContractValidationFailed(
177
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
178
- { where: { path: contractPathAbsolute } },
179
- ),
180
- );
181
- }
182
-
183
- // Resolve database connection (--db flag or config.db.connection)
184
- const dbConnection = options.db ?? config.db?.connection;
185
- if (!dbConnection) {
186
- return notOk(
187
- errorDatabaseConnectionRequired({
188
- why: `Database connection is required for db init (set db.connection in ${configPath}, or pass --db <url>)`,
189
- }),
190
- );
191
- }
192
-
193
- // Check for driver
194
- if (!config.driver) {
195
- return notOk(errorDriverRequired({ why: 'Config.driver is required for db init' }));
196
- }
197
-
198
- // Check target supports migrations via the migrations capability
199
- if (!config.target.migrations) {
200
- return notOk(
201
- errorTargetMigrationNotSupported({
202
- why: `Target "${config.target.id}" does not support migrations`,
203
- }),
204
- );
205
- }
206
-
207
- // Create control client
208
- const client = createControlClient({
209
- family: config.family,
210
- target: config.target,
211
- adapter: config.adapter,
212
- driver: config.driver,
213
- extensionPacks: config.extensionPacks ?? [],
100
+ ): Promise<Result<MigrationCommandResult, CliStructuredError>> {
101
+ // Prepare shared migration context (config, contract, connection, client)
102
+ const ctxResult = await prepareMigrationContext(options, flags, {
103
+ commandName: 'db init',
104
+ description: 'Bootstrap a database to match the current contract',
105
+ url: 'https://pris.ly/db-init',
214
106
  });
215
-
216
- // Create progress adapter
217
- const onProgress = createProgressAdapter({ flags });
107
+ if (!ctxResult.ok) {
108
+ return ctxResult;
109
+ }
110
+ const { client, contractJson, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
218
111
 
219
112
  try {
220
113
  // Call dbInit with connection and progress callback
221
- // Connection happens inside dbInit with a 'connect' progress span
222
114
  const result = await client.dbInit({
223
115
  contractIR: contractJson,
224
116
  mode: options.plan ? 'plan' : 'apply',
@@ -232,15 +124,14 @@ async function executeDbInitCommand(
232
124
  }
233
125
 
234
126
  // Convert success result to CLI output format
235
- const profileHash = result.value.marker?.profileHash;
236
- const dbInitResult: DbInitResult = {
127
+ const dbInitResult: MigrationCommandResult = {
237
128
  ok: true,
238
129
  mode: result.value.mode,
239
130
  plan: {
240
- targetId: config.target.targetId,
131
+ targetId: ctxResult.value.config.target.targetId,
241
132
  destination: {
242
- storageHash: result.value.marker?.storageHash ?? '',
243
- ...ifDefined('profileHash', profileHash),
133
+ storageHash: result.value.destination.storageHash,
134
+ ...ifDefined('profileHash', result.value.destination.profileHash),
244
135
  },
245
136
  operations: result.value.plan.operations.map((op) => ({
246
137
  id: op.id,
@@ -271,15 +162,26 @@ async function executeDbInitCommand(
271
162
  return ok(dbInitResult);
272
163
  } catch (error) {
273
164
  // Driver already throws CliStructuredError for connection failures
274
- // Use static type guard to work across module boundaries
275
165
  if (CliStructuredError.is(error)) {
276
166
  return notOk(error);
277
167
  }
278
168
 
279
- // Wrap unexpected errors
169
+ if (error instanceof ContractValidationError) {
170
+ return notOk(
171
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
172
+ where: { path: contractPathAbsolute },
173
+ }),
174
+ );
175
+ }
176
+
177
+ const rawMessage = error instanceof Error ? error.message : String(error);
178
+ const safeMessage = sanitizeErrorMessage(
179
+ rawMessage,
180
+ typeof dbConnection === 'string' ? dbConnection : undefined,
181
+ );
280
182
  return notOk(
281
- errorUnexpected(error instanceof Error ? error.message : String(error), {
282
- why: `Unexpected error during db init: ${error instanceof Error ? error.message : String(error)}`,
183
+ errorUnexpected(safeMessage, {
184
+ why: `Unexpected error during db init: ${safeMessage}`,
283
185
  }),
284
186
  );
285
187
  } finally {
@@ -291,65 +193,55 @@ export function createDbInitCommand(): Command {
291
193
  const command = new Command('init');
292
194
  setCommandDescriptions(
293
195
  command,
294
- 'Bootstrap a database to match the current contract and write the contract marker',
196
+ 'Bootstrap a database to match the current contract and sign it',
295
197
  'Initializes a database to match your emitted contract using additive-only operations.\n' +
296
198
  'Creates any missing tables, columns, indexes, and constraints defined in your contract.\n' +
297
199
  'Leaves existing compatible structures in place, surfaces conflicts when destructive changes\n' +
298
- 'would be required, and writes a contract marker to track the database state. Use --plan to\n' +
200
+ 'would be required, and signs the database to track contract state. Use --plan to\n' +
299
201
  'preview changes without applying.',
300
202
  );
301
- command
302
- .configureHelp({
303
- formatHelp: (cmd) => {
304
- const flags = parseGlobalFlags({});
305
- return formatCommandHelp({ command: cmd, flags });
306
- },
307
- })
308
- .option('--db <url>', 'Database connection string')
309
- .option('--config <path>', 'Path to prisma-next.config.ts')
310
- .option('--plan', 'Preview planned operations without applying', false)
311
- .option('--json [format]', 'Output as JSON (object)', false)
312
- .option('-q, --quiet', 'Quiet mode: errors only')
313
- .option('-v, --verbose', 'Verbose output: debug info, timings')
314
- .option('-vv, --trace', 'Trace output: deep internals, stack traces')
315
- .option('--timestamps', 'Add timestamps to output')
316
- .option('--color', 'Force color output')
317
- .option('--no-color', 'Disable color output')
318
- .action(async (options: DbInitOptions) => {
319
- const flags = parseGlobalFlags(options);
320
- const startTime = Date.now();
321
-
322
- // Validate JSON format option
323
- if (flags.json === 'ndjson') {
324
- const result = notOk(
325
- errorJsonFormatNotSupported({
326
- command: 'db init',
327
- format: 'ndjson',
328
- supportedFormats: ['object'],
329
- }),
330
- );
331
- const exitCode = handleResult(result, flags);
332
- process.exit(exitCode);
333
- }
334
-
335
- const result = await executeDbInitCommand(options, flags, startTime);
203
+ addMigrationCommandOptions(command);
204
+ command.configureHelp({
205
+ formatHelp: (cmd) => {
206
+ const flags = parseGlobalFlags({});
207
+ return formatCommandHelp({ command: cmd, flags });
208
+ },
209
+ });
210
+ command.action(async (options: DbInitOptions) => {
211
+ const flags = parseGlobalFlags(options);
212
+ const startTime = Date.now();
213
+
214
+ // Validate JSON format option
215
+ if (flags.json === 'ndjson') {
216
+ const result = notOk(
217
+ errorJsonFormatNotSupported({
218
+ command: 'db init',
219
+ format: 'ndjson',
220
+ supportedFormats: ['object'],
221
+ }),
222
+ );
223
+ const exitCode = handleResult(result, flags);
224
+ process.exit(exitCode);
225
+ }
336
226
 
337
- const exitCode = handleResult(result, flags, (dbInitResult) => {
338
- if (flags.json === 'object') {
339
- console.log(formatDbInitJson(dbInitResult));
340
- } else {
341
- const output =
342
- dbInitResult.mode === 'plan'
343
- ? formatDbInitPlanOutput(dbInitResult, flags)
344
- : formatDbInitApplyOutput(dbInitResult, flags);
345
- if (output) {
346
- console.log(output);
347
- }
227
+ const result = await executeDbInitCommand(options, flags, startTime);
228
+
229
+ const exitCode = handleResult(result, flags, (dbInitResult) => {
230
+ if (flags.json === 'object') {
231
+ console.log(formatMigrationJson(dbInitResult));
232
+ } else {
233
+ const output =
234
+ dbInitResult.mode === 'plan'
235
+ ? formatMigrationPlanOutput(dbInitResult, flags)
236
+ : formatMigrationApplyOutput(dbInitResult, flags);
237
+ if (output) {
238
+ console.log(output);
348
239
  }
349
- });
350
-
351
- process.exit(exitCode);
240
+ }
352
241
  });
353
242
 
243
+ process.exit(exitCode);
244
+ });
245
+
354
246
  return command;
355
247
  }
@@ -12,7 +12,7 @@ import {
12
12
  errorJsonFormatNotSupported,
13
13
  errorUnexpected,
14
14
  } from '../utils/cli-errors';
15
- import { setCommandDescriptions } from '../utils/command-helpers';
15
+ import { maskConnectionUrl, setCommandDescriptions } from '../utils/command-helpers';
16
16
  import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
17
17
  import {
18
18
  formatCommandHelp,
@@ -63,13 +63,9 @@ async function executeDbIntrospectCommand(
63
63
  { label: 'config', value: configPath },
64
64
  ];
65
65
  if (options.db) {
66
- // Mask password in URL for security
67
- const maskedUrl = options.db.replace(/:([^:@]+)@/, ':****@');
68
- details.push({ label: 'database', value: maskedUrl });
66
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
69
67
  } else if (config.db?.connection && typeof config.db.connection === 'string') {
70
- // Mask password in URL for security
71
- const maskedUrl = config.db.connection.replace(/:([^:@]+)@/, ':****@');
72
- details.push({ label: 'database', value: maskedUrl });
68
+ details.push({ label: 'database', value: maskConnectionUrl(config.db.connection) });
73
69
  }
74
70
  const header = formatStyledHeader({
75
71
  command: 'db introspect',
@@ -127,7 +123,7 @@ async function executeDbIntrospectCommand(
127
123
 
128
124
  // Get masked connection URL for meta (only for string connections)
129
125
  const connectionForMeta =
130
- typeof dbConnection === 'string' ? dbConnection.replace(/:([^:@]+)@/, ':****@') : undefined;
126
+ typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
131
127
 
132
128
  const introspectResult: IntrospectSchemaResult<unknown> = {
133
129
  ok: true,
@@ -5,6 +5,7 @@ import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
5
  import { Command } from 'commander';
6
6
  import { loadConfig } from '../config-loader';
7
7
  import { createControlClient } from '../control-api/client';
8
+ import { ContractValidationError } from '../control-api/errors';
8
9
  import {
9
10
  CliStructuredError,
10
11
  errorContractValidationFailed,
@@ -14,7 +15,7 @@ import {
14
15
  errorJsonFormatNotSupported,
15
16
  errorUnexpected,
16
17
  } from '../utils/cli-errors';
17
- import { setCommandDescriptions } from '../utils/command-helpers';
18
+ import { maskConnectionUrl, setCommandDescriptions } from '../utils/command-helpers';
18
19
  import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
19
20
  import {
20
21
  formatCommandHelp,
@@ -65,7 +66,7 @@ async function executeDbSchemaVerifyCommand(
65
66
  { label: 'contract', value: contractPath },
66
67
  ];
67
68
  if (options.db) {
68
- details.push({ label: 'database', value: options.db });
69
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
69
70
  }
70
71
  const header = formatStyledHeader({
71
72
  command: 'db schema-verify',
@@ -156,6 +157,14 @@ async function executeDbSchemaVerifyCommand(
156
157
  return notOk(error);
157
158
  }
158
159
 
160
+ if (error instanceof ContractValidationError) {
161
+ return notOk(
162
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
163
+ where: { path: contractPathAbsolute },
164
+ }),
165
+ );
166
+ }
167
+
159
168
  // Wrap unexpected errors
160
169
  return notOk(
161
170
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -8,6 +8,7 @@ import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
8
  import { Command } from 'commander';
9
9
  import { loadConfig } from '../config-loader';
10
10
  import { createControlClient } from '../control-api/client';
11
+ import { ContractValidationError } from '../control-api/errors';
11
12
  import {
12
13
  CliStructuredError,
13
14
  errorContractValidationFailed,
@@ -17,7 +18,7 @@ import {
17
18
  errorJsonFormatNotSupported,
18
19
  errorUnexpected,
19
20
  } from '../utils/cli-errors';
20
- import { setCommandDescriptions } from '../utils/command-helpers';
21
+ import { maskConnectionUrl, setCommandDescriptions } from '../utils/command-helpers';
21
22
  import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
22
23
  import {
23
24
  formatCommandHelp,
@@ -77,7 +78,7 @@ async function executeDbSignCommand(
77
78
  { label: 'contract', value: contractPath },
78
79
  ];
79
80
  if (options.db) {
80
- details.push({ label: 'database', value: options.db });
81
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
81
82
  }
82
83
  const header = formatStyledHeader({
83
84
  command: 'db sign',
@@ -186,6 +187,14 @@ async function executeDbSignCommand(
186
187
  return notOk(error);
187
188
  }
188
189
 
190
+ if (error instanceof ContractValidationError) {
191
+ return notOk(
192
+ errorContractValidationFailed(`Contract validation failed: ${error.message}`, {
193
+ where: { path: contractPathAbsolute },
194
+ }),
195
+ );
196
+ }
197
+
189
198
  // Wrap unexpected errors
190
199
  return notOk(
191
200
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -203,8 +212,8 @@ export function createDbSignCommand(): Command {
203
212
  command,
204
213
  'Sign the database with your contract so you can safely run queries',
205
214
  'Verifies that your database schema satisfies the emitted contract, and if so, writes or\n' +
206
- 'updates the contract marker in the database. This command is idempotent and safe to run\n' +
207
- 'in CI/deployment pipelines. The marker records that this database instance is aligned\n' +
215
+ 'updates the database signature. This command is idempotent and safe to run\n' +
216
+ 'in CI/deployment pipelines. The signature records that this database instance is aligned\n' +
208
217
  'with a specific contract version.',
209
218
  );
210
219
  command