@prisma-next/cli 0.5.0-dev.7 → 0.5.0-dev.71

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 (171) hide show
  1. package/README.md +56 -21
  2. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  4. package/dist/cli-errors-QH8kf-C2.d.mts +3 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-0ZX24FXF.mjs +1398 -0
  8. package/dist/client-0ZX24FXF.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +2 -4
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +2 -4
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +14 -13
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +5 -7
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -9
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +13 -13
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -321
  27. package/dist/commands/migration-apply.d.mts +5 -2
  28. package/dist/commands/migration-apply.d.mts.map +1 -1
  29. package/dist/commands/migration-apply.mjs +64 -66
  30. package/dist/commands/migration-apply.mjs.map +1 -1
  31. package/dist/commands/migration-new.d.mts +0 -1
  32. package/dist/commands/migration-new.d.mts.map +1 -1
  33. package/dist/commands/migration-new.mjs +33 -40
  34. package/dist/commands/migration-new.mjs.map +1 -1
  35. package/dist/commands/migration-plan.d.mts +14 -5
  36. package/dist/commands/migration-plan.d.mts.map +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -347
  38. package/dist/commands/migration-ref.d.mts +1 -1
  39. package/dist/commands/migration-ref.d.mts.map +1 -1
  40. package/dist/commands/migration-ref.mjs +7 -12
  41. package/dist/commands/migration-ref.mjs.map +1 -1
  42. package/dist/commands/migration-show.d.mts +13 -7
  43. package/dist/commands/migration-show.d.mts.map +1 -1
  44. package/dist/commands/migration-show.mjs +34 -36
  45. package/dist/commands/migration-show.mjs.map +1 -1
  46. package/dist/commands/migration-status.d.mts +23 -5
  47. package/dist/commands/migration-status.d.mts.map +1 -1
  48. package/dist/commands/migration-status.mjs +2 -4
  49. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  50. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  51. package/dist/config-loader.d.mts +0 -1
  52. package/dist/config-loader.d.mts.map +1 -1
  53. package/dist/config-loader.mjs +2 -3
  54. package/dist/contract-emit-B3ChISB_.mjs +338 -0
  55. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  56. package/dist/contract-emit-DkMqO7f2.mjs +148 -0
  57. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  58. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-CF6ogEJ_.mjs} +4 -6
  59. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  60. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BDKAE0B0.mjs} +12 -22
  61. package/dist/contract-infer-BDKAE0B0.mjs.map +1 -0
  62. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  63. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  64. package/dist/exports/config-types.mjs +1 -2
  65. package/dist/exports/control-api.d.mts +287 -29
  66. package/dist/exports/control-api.d.mts.map +1 -1
  67. package/dist/exports/control-api.mjs +4 -6
  68. package/dist/exports/index.d.mts.map +1 -1
  69. package/dist/exports/index.mjs +28 -30
  70. package/dist/exports/index.mjs.map +1 -1
  71. package/dist/exports/init-output.d.mts +2 -4
  72. package/dist/exports/init-output.d.mts.map +1 -1
  73. package/dist/exports/init-output.mjs +2 -3
  74. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  75. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  76. package/dist/{init-C5220SY9.mjs → init-Deo7U8_U.mjs} +26 -35
  77. package/dist/init-Deo7U8_U.mjs.map +1 -0
  78. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-BAgQMYpD.mjs} +10 -11
  79. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  80. package/dist/migration-cli.d.mts +41 -12
  81. package/dist/migration-cli.d.mts.map +1 -1
  82. package/dist/migration-cli.mjs +309 -86
  83. package/dist/migration-cli.mjs.map +1 -1
  84. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B8J702Uh.mjs} +8 -9
  85. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  86. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  87. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  88. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CjwB2of-.mjs} +117 -64
  89. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  90. package/dist/{migrations-Bo5WtTla.mjs → migrations-CIK94AJf.mjs} +43 -23
  91. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  92. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  93. package/dist/output-DnjfCC_u.mjs.map +1 -0
  94. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  95. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  96. package/dist/{result-handler-Ba3zWQsI.mjs → result-handler-DWb1rFS-.mjs} +52 -27
  97. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  98. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  99. package/dist/{terminal-ui-C3ZLwQxK.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  100. package/dist/{verify-Bkycc-Tf.mjs → verify-BEIa9638.mjs} +3 -4
  101. package/dist/verify-BEIa9638.mjs.map +1 -0
  102. package/package.json +28 -26
  103. package/src/cli.ts +32 -6
  104. package/src/commands/contract-emit.ts +67 -163
  105. package/src/commands/contract-infer.ts +7 -20
  106. package/src/commands/db-init.ts +14 -3
  107. package/src/commands/db-update.ts +8 -4
  108. package/src/commands/db-verify.ts +47 -15
  109. package/src/commands/init/index.ts +1 -1
  110. package/src/commands/init/init.ts +2 -2
  111. package/src/commands/init/templates/code-templates.ts +12 -4
  112. package/src/commands/inspect-live-schema.ts +10 -5
  113. package/src/commands/migration-apply.ts +92 -71
  114. package/src/commands/migration-new.ts +42 -45
  115. package/src/commands/migration-plan.ts +147 -64
  116. package/src/commands/migration-ref.ts +8 -7
  117. package/src/commands/migration-show.ts +60 -41
  118. package/src/commands/migration-status.ts +196 -60
  119. package/src/config-path-validation.ts +0 -1
  120. package/src/control-api/client.ts +69 -1
  121. package/src/control-api/contract-enrichment.ts +6 -4
  122. package/src/control-api/operations/contract-emit.ts +198 -115
  123. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  124. package/src/control-api/operations/db-init.ts +51 -253
  125. package/src/control-api/operations/db-update.ts +66 -183
  126. package/src/control-api/operations/db-verify.ts +342 -0
  127. package/src/control-api/operations/migration-apply.ts +37 -9
  128. package/src/control-api/types.ts +125 -7
  129. package/src/exports/control-api.ts +15 -3
  130. package/src/load-ts-contract.ts +28 -26
  131. package/src/migration-cli.ts +445 -122
  132. package/src/utils/cli-errors.ts +49 -2
  133. package/src/utils/combine-schema-results.ts +84 -0
  134. package/src/utils/command-helpers.ts +69 -25
  135. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  136. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  137. package/src/utils/contract-space-migrate-pass.ts +156 -0
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  143. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  144. package/dist/client-CrsnY58k.mjs +0 -997
  145. package/dist/client-CrsnY58k.mjs.map +0 -1
  146. package/dist/commands/db-verify.mjs.map +0 -1
  147. package/dist/commands/migration-plan.mjs.map +0 -1
  148. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  149. package/dist/contract-emit--feXyNd7.mjs +0 -4
  150. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  151. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  152. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  153. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  154. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  155. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  157. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  159. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  160. package/dist/init-C5220SY9.mjs.map +0 -1
  161. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  162. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  163. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  164. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  165. package/dist/output-BpcQrnnq.mjs.map +0 -1
  166. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  167. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  168. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  169. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  170. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  171. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
package/src/cli.ts CHANGED
@@ -50,8 +50,18 @@ program.configureOutput({
50
50
  writeErr: () => {
51
51
  // Suppress all default error output - we handle errors in exitOverride
52
52
  },
53
- writeOut: () => {
54
- // Suppress all default output - our custom formatters handle everything
53
+ writeOut: (str) => {
54
+ // Commander routes explicitly-requested `--help` (success-path help)
55
+ // through writeOut; per the Style Guide § Output Conventions rule 8,
56
+ // user-requested help is data and goes to stdout. Error-path help
57
+ // (e.g. usage shown after an unknown command) goes through writeErr,
58
+ // which stays suppressed because we render that ourselves with the
59
+ // matching error envelope.
60
+ //
61
+ // Explicit `--version` is short-circuited before `program.parse()`
62
+ // (see the argv pre-scan at the bottom of this file), so it does not
63
+ // reach this writer.
64
+ process.stdout.write(str);
55
65
  },
56
66
  });
57
67
 
@@ -261,14 +271,25 @@ const helpCommand = new Command('help')
261
271
  .action(() => {
262
272
  const flags = parseGlobalFlags({});
263
273
  const helpText = formatRootHelp({ program, flags });
264
- // Help is decoration stderr
265
- process.stderr.write(`${helpText}\n`);
274
+ // The `help` command was invoked explicitly: help is the data the
275
+ // caller asked for. Per Style Guide § Output Conventions rule 8,
276
+ // explicit help goes to stdout with exit code 0.
277
+ process.stdout.write(`${helpText}\n`);
266
278
  process.exit(0);
267
279
  });
268
280
 
269
281
  program.addCommand(helpCommand);
270
282
 
271
- // Set help as the default action when no command is provided
283
+ // Set help as the default action when no command is provided. The user
284
+ // did not invoke `--help`; we are voluntarily showing usage to help them
285
+ // recover from an underspecified invocation, so the help text is
286
+ // decoration around an implicit "what did you want me to do?" and goes
287
+ // to stderr (Style Guide § Output Conventions rule 8).
288
+ //
289
+ // FOLLOW-UP: the exit code here is 0 today, but a no-arg invocation is
290
+ // arguably a usage error (PRECONDITION → exit 2) for consistency with
291
+ // the unknown-command path. Out of scope for the explicit-help routing
292
+ // work; revisit when tightening exit-code semantics across the CLI.
272
293
  program.action(() => {
273
294
  const flags = parseGlobalFlags({});
274
295
  const helpText = formatRootHelp({ program, flags });
@@ -304,7 +325,12 @@ if (args.length > 0) {
304
325
  process.stderr.write(`${helpText}\n`);
305
326
  process.exit(2);
306
327
  } else if (command.commands.length > 0 && args.length === 1) {
307
- // Parent command called with no subcommand - show help and exit with 0
328
+ // Parent command called with no subcommand. Same shape as the
329
+ // no-args case above: the user did not request help, we are
330
+ // voluntarily rendering it as decoration around an underspecified
331
+ // invocation, so it goes to stderr per Style Guide § Output
332
+ // Conventions rule 8. Exit code 0 today; the FOLLOW-UP note on
333
+ // `program.action` applies here too (arguably should be 2).
308
334
  const flags = parseGlobalFlags({});
309
335
  const helpText = formatCommandHelp({ command, flags });
310
336
  process.stderr.write(`${helpText}\n`);
@@ -1,18 +1,13 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
3
2
  import { errorContractConfigMissing } from '@prisma-next/errors/control';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
4
4
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
5
  import { Command } from 'commander';
6
6
  import { dirname, relative, resolve } from 'pathe';
7
7
  import { loadConfig } from '../config-loader';
8
- import { createControlClient } from '../control-api/client';
9
- import type { EmitFailure } from '../control-api/types';
10
- import {
11
- CliStructuredError,
12
- errorContractValidationFailed,
13
- errorRuntime,
14
- errorUnexpected,
15
- } from '../utils/cli-errors';
8
+ import { executeContractEmit } from '../control-api/operations/contract-emit';
9
+ import type { ContractEmitResult } from '../control-api/types';
10
+ import { CliStructuredError, errorUnexpected } from '../utils/cli-errors';
16
11
  import {
17
12
  addGlobalOptions,
18
13
  setCommandDescriptions,
@@ -34,74 +29,29 @@ interface ContractEmitOptions extends CommonCommandOptions {
34
29
  readonly config?: string;
35
30
  }
36
31
 
37
- function mapDiagnosticsToIssues(
38
- failure: EmitFailure,
39
- ): ReadonlyArray<{ kind: string; message: string }> {
40
- const diagnostics = failure.diagnostics?.diagnostics ?? [];
41
- return diagnostics.map((diagnostic) => {
42
- const location =
43
- diagnostic.sourceId && diagnostic.span
44
- ? ` (${diagnostic.sourceId}:${diagnostic.span.start.line}:${diagnostic.span.start.column})`
45
- : diagnostic.sourceId
46
- ? ` (${diagnostic.sourceId})`
47
- : '';
48
- return {
49
- kind: diagnostic.code,
50
- message: `${diagnostic.message}${location}`,
51
- };
52
- });
32
+ interface HeaderPaths {
33
+ readonly displayConfigPath: string;
34
+ readonly outputJsonPath: string;
35
+ readonly outputDtsPath: string;
53
36
  }
54
37
 
55
38
  /**
56
- * Maps an EmitFailure to a CliStructuredError for consistent error handling.
39
+ * Pre-load the config just to compute display paths for the styled header. The
40
+ * actual emit work goes through `executeContractEmit`, which loads the config
41
+ * itself; the redundant load here is bounded and lets the header render before
42
+ * the emit spans start.
57
43
  */
58
- function mapEmitFailure(
59
- failure: EmitFailure,
60
- context?: { readonly configPath?: string },
61
- ): CliStructuredError {
62
- if (failure.code === 'CONTRACT_SOURCE_INVALID') {
63
- const issues = mapDiagnosticsToIssues(failure);
64
- return errorRuntime(failure.summary, {
65
- why: failure.why ?? 'Contract source provider failed',
66
- fix: 'Check your contract source provider in prisma-next.config.ts and ensure it returns Result<Contract, Diagnostics>',
67
- ...(issues.length > 0 ? { meta: { issues } } : {}),
68
- });
69
- }
70
-
71
- if (failure.code === 'CONTRACT_VALIDATION_FAILED') {
72
- return errorContractValidationFailed(
73
- failure.why ?? 'Contract validation failed while emitting',
74
- context?.configPath ? { where: { path: context.configPath } } : undefined,
75
- );
76
- }
77
-
78
- if (failure.code === 'EMIT_FAILED') {
79
- return errorRuntime(failure.summary, {
80
- why: failure.why ?? 'Failed to emit contract',
81
- fix: 'Check your contract configuration and ensure the source is valid',
82
- });
83
- }
84
-
85
- // Exhaustive check - TypeScript will error if a new code is added but not handled
86
- const exhaustive: never = failure.code;
87
- throw new Error(`Unhandled EmitFailure code: ${exhaustive}`);
88
- }
44
+ async function resolveHeaderPaths(
45
+ configOption: string | undefined,
46
+ ): Promise<Result<HeaderPaths, CliStructuredError>> {
47
+ const displayConfigPath = configOption
48
+ ? relative(process.cwd(), resolve(configOption))
49
+ : 'prisma-next.config.ts';
89
50
 
90
- /**
91
- * Executes the contract emit command and returns a structured Result.
92
- */
93
- async function executeContractEmitCommand(
94
- options: ContractEmitOptions,
95
- flags: GlobalFlags,
96
- ui: TerminalUI,
97
- startTime: number,
98
- ): Promise<Result<EmitContractResult, CliStructuredError>> {
99
- // Load config
100
51
  let config: Awaited<ReturnType<typeof loadConfig>>;
101
52
  try {
102
- config = await loadConfig(options.config);
53
+ config = await loadConfig(configOption);
103
54
  } catch (error) {
104
- // Convert thrown CliStructuredError to Result
105
55
  if (error instanceof CliStructuredError) {
106
56
  return notOk(error);
107
57
  }
@@ -112,33 +62,19 @@ async function executeContractEmitCommand(
112
62
  );
113
63
  }
114
64
 
115
- const configPath = options.config
116
- ? relative(process.cwd(), resolve(options.config))
117
- : 'prisma-next.config.ts';
118
-
119
- // Resolve contract from config
120
- if (!config.contract) {
65
+ if (!config.contract?.output) {
121
66
  return notOk(
122
67
  errorContractConfigMissing({
123
- why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
68
+ why: 'Config.contract.output is required for emit. Define it in your config: contract: { source: ..., output: ... }',
124
69
  }),
125
70
  );
126
71
  }
127
72
 
128
- // Contract config is already normalized by defineConfig() with defaults applied
129
- const contractConfig = config.contract;
130
-
131
- // Resolve artifact paths from config (already normalized by defineConfig() with defaults)
132
- if (!contractConfig.output) {
133
- return notOk(
134
- errorContractConfigMissing({
135
- why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
136
- }),
137
- );
138
- }
139
- let outputPaths: ReturnType<typeof getEmittedArtifactPaths>;
140
73
  try {
141
- outputPaths = getEmittedArtifactPaths(contractConfig.output);
74
+ const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = getEmittedArtifactPaths(
75
+ config.contract.output,
76
+ );
77
+ return ok({ displayConfigPath, outputJsonPath, outputDtsPath });
142
78
  } catch (error) {
143
79
  return notOk(
144
80
  errorContractConfigMissing({
@@ -146,96 +82,65 @@ async function executeContractEmitCommand(
146
82
  }),
147
83
  );
148
84
  }
149
- const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
85
+ }
150
86
 
151
- // Output header to stderr (decoration)
152
- if (!flags.json && !flags.quiet) {
153
- const contractPath = relative(process.cwd(), outputJsonPath);
154
- const typesPath = relative(process.cwd(), outputDtsPath);
155
- const header = formatStyledHeader({
156
- command: 'contract emit',
157
- description: 'Emit your contract artifacts',
158
- url: 'https://pris.ly/contract-emit',
159
- details: [
160
- { label: 'config', value: configPath },
161
- { label: 'contract', value: contractPath },
162
- { label: 'types', value: typesPath },
163
- ],
164
- flags,
165
- });
166
- ui.stderr(header);
87
+ async function executeContractEmitCommand(
88
+ options: ContractEmitOptions,
89
+ flags: GlobalFlags,
90
+ ui: TerminalUI,
91
+ startTime: number,
92
+ ): Promise<Result<EmitContractResult, CliStructuredError>> {
93
+ const headerPathsResult = await resolveHeaderPaths(options.config);
94
+ if (!headerPathsResult.ok) {
95
+ return headerPathsResult;
167
96
  }
97
+ const { displayConfigPath, outputJsonPath, outputDtsPath } = headerPathsResult.value;
168
98
 
169
- // Create control client (no driver needed for emit)
170
- const client = createControlClient({
171
- family: config.family,
172
- target: config.target,
173
- adapter: config.adapter,
174
- extensionPacks: config.extensionPacks ?? [],
175
- });
99
+ if (!flags.json && !flags.quiet) {
100
+ ui.stderr(
101
+ formatStyledHeader({
102
+ command: 'contract emit',
103
+ description: 'Emit your contract artifacts',
104
+ url: 'https://pris.ly/contract-emit',
105
+ details: [
106
+ { label: 'config', value: displayConfigPath },
107
+ { label: 'contract', value: relative(process.cwd(), outputJsonPath) },
108
+ { label: 'types', value: relative(process.cwd(), outputDtsPath) },
109
+ ],
110
+ flags,
111
+ }),
112
+ );
113
+ }
176
114
 
177
- // Create progress adapter
178
115
  const onProgress = createProgressAdapter({ ui, flags });
116
+ const configPath = options.config ? resolve(options.config) : 'prisma-next.config.ts';
179
117
 
118
+ let result: ContractEmitResult;
180
119
  try {
181
- // Call emit with progress callback
182
- const result = await client.emit({
183
- contractConfig: {
184
- source: contractConfig.source,
185
- output: outputJsonPath,
186
- },
187
- onProgress,
188
- });
189
-
190
- // Handle failures by mapping to CLI structured error
191
- if (!result.ok) {
192
- return notOk(mapEmitFailure(result.failure, { configPath }));
193
- }
194
-
195
- // Create directories if needed
196
- mkdirSync(dirname(outputJsonPath), { recursive: true });
197
- mkdirSync(dirname(outputDtsPath), { recursive: true });
198
-
199
- // Write the results to files
200
- writeFileSync(outputJsonPath, result.value.contractJson, 'utf-8');
201
- writeFileSync(outputDtsPath, result.value.contractDts, 'utf-8');
202
-
203
- // Validate that contract.d.ts type imports are resolvable
204
- const { validateContractDeps } = await import('../utils/validate-contract-deps');
205
- const depsValidation = validateContractDeps(result.value.contractDts, dirname(outputDtsPath));
206
- if (depsValidation.warning) {
207
- ui.warn(depsValidation.warning);
208
- }
209
-
210
- // Convert success result to CLI output format
211
- const emitResult: EmitContractResult = {
212
- storageHash: result.value.storageHash,
213
- ...(result.value.executionHash ? { executionHash: result.value.executionHash } : {}),
214
- profileHash: result.value.profileHash,
215
- outDir: dirname(outputJsonPath),
216
- files: {
217
- json: outputJsonPath,
218
- dts: outputDtsPath,
219
- },
220
- timings: { total: Date.now() - startTime },
221
- };
222
-
223
- return ok(emitResult);
120
+ result = await executeContractEmit({ configPath, onProgress });
224
121
  } catch (error) {
225
- // Use static type guard to work across module boundaries
226
122
  if (CliStructuredError.is(error)) {
227
123
  return notOk(error);
228
124
  }
229
-
230
- // Wrap unexpected errors
231
125
  return notOk(
232
126
  errorUnexpected('Unexpected error during contract emit', {
233
127
  why: error instanceof Error ? error.message : String(error),
234
128
  }),
235
129
  );
236
- } finally {
237
- await client.close();
238
130
  }
131
+
132
+ if (result.validationWarning) {
133
+ ui.warn(result.validationWarning);
134
+ }
135
+
136
+ return ok({
137
+ storageHash: result.storageHash,
138
+ ...ifDefined('executionHash', result.executionHash),
139
+ profileHash: result.profileHash,
140
+ outDir: dirname(result.files.json),
141
+ files: result.files,
142
+ timings: { total: Date.now() - startTime },
143
+ });
239
144
  }
240
145
 
241
146
  export function createContractEmitCommand(): Command {
@@ -260,7 +165,6 @@ export function createContractEmitCommand(): Command {
260
165
 
261
166
  const result = await executeContractEmitCommand(options, flags, ui, startTime);
262
167
 
263
- // Handle result - formats output and returns exit code
264
168
  const exitCode = handleResult(result, flags, ui, (emitResult) => {
265
169
  if (flags.json) {
266
170
  ui.output(formatEmitJson(emitResult));
@@ -1,12 +1,6 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { errorRuntime } from '@prisma-next/errors/execution';
3
- import { printPsl, validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
4
- import {
5
- createPostgresDefaultMapping,
6
- createPostgresTypeMap,
7
- extractEnumInfo,
8
- parseRawDefault,
9
- } from '@prisma-next/psl-printer/postgres';
3
+ import { printPsl } from '@prisma-next/psl-printer';
10
4
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
11
5
  import { Command } from 'commander';
12
6
  import { dirname, relative } from 'pathe';
@@ -59,26 +53,19 @@ async function executeContractInferCommand(
59
53
  return inspectResult;
60
54
  }
61
55
 
62
- const { config, target, meta } = inspectResult.value;
56
+ const { config, target, meta, pslContractAst } = inspectResult.value;
63
57
 
64
- if (target.familyId !== 'sql') {
58
+ if (!pslContractAst) {
65
59
  return notOk(
66
- errorRuntime(`contract infer is not supported for family "${target.familyId}"`, {
67
- why: 'contract infer currently supports SQL targets only',
68
- fix: 'Use an SQL target (e.g. Postgres) with this command',
60
+ errorRuntime('contract infer is not supported for this family', {
61
+ why: 'The configured family does not implement the PslContractInferCapable capability, so an inferred PSL contract cannot be produced from the live database schema.',
62
+ fix: 'Use a family that supports contract inference (e.g. SQL/Postgres).',
69
63
  }),
70
64
  );
71
65
  }
72
66
 
73
- const schema = validatePrintableSqlSchemaIR(inspectResult.value.schema);
74
67
  const outputPath = resolveContractInferOutputPath(options, config.contract?.output);
75
- const enumInfo = extractEnumInfo(schema.annotations);
76
- const pslContent = printPsl(schema, {
77
- defaultMapping: createPostgresDefaultMapping(),
78
- typeMap: createPostgresTypeMap(enumInfo.typeNames),
79
- enumInfo,
80
- parseRawDefault,
81
- });
68
+ const pslContent = printPsl(pslContractAst);
82
69
 
83
70
  if (existsSync(outputPath) && !flags.json && !flags.quiet) {
84
71
  ui.stderr(`\u26A0 Overwriting existing file: ${relative(process.cwd(), outputPath)}`);
@@ -13,6 +13,7 @@ import {
13
13
  } from '../utils/cli-errors';
14
14
  import type { MigrationCommandOptions } from '../utils/command-helpers';
15
15
  import {
16
+ resolveMigrationPaths,
16
17
  sanitizeErrorMessage,
17
18
  setCommandDescriptions,
18
19
  setCommandExamples,
@@ -111,14 +112,23 @@ async function executeDbInitCommand(
111
112
  if (!ctxResult.ok) {
112
113
  return ctxResult;
113
114
  }
114
- const { client, contractJson, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
115
+ const { client, config, contractJson, dbConnection, onProgress, contractPathAbsolute } =
116
+ ctxResult.value;
117
+
118
+ // The aggregate loader (loader → planner → runner pipeline) catches
119
+ // layout / drift / disjointness violations on its own; the legacy
120
+ // per-space precheck + marker-check helpers are no longer needed at
121
+ // this surface. Marker-vs-on-disk drift surfaces through the planner's
122
+ // graph-walk strategy.
123
+ const { migrationsDir } = resolveMigrationPaths(options.config, config);
115
124
 
116
125
  try {
117
- // Call dbInit with connection and progress callback
126
+ await client.connect(dbConnection);
127
+
118
128
  const result = await client.dbInit({
119
129
  contract: contractJson,
120
130
  mode: options.dryRun ? 'plan' : 'apply',
121
- connection: dbConnection,
131
+ migrationsDir,
122
132
  onProgress,
123
133
  });
124
134
 
@@ -142,6 +152,7 @@ async function executeDbInitCommand(
142
152
  label: op.label,
143
153
  operationClass: op.operationClass,
144
154
  })),
155
+ ...ifDefined('preview', result.value.plan.preview),
145
156
  },
146
157
  ...(result.value.execution
147
158
  ? {
@@ -14,6 +14,7 @@ import {
14
14
  } from '../utils/cli-errors';
15
15
  import type { MigrationCommandOptions } from '../utils/command-helpers';
16
16
  import {
17
+ resolveMigrationPaths,
17
18
  sanitizeErrorMessage,
18
19
  setCommandDescriptions,
19
20
  setCommandExamples,
@@ -80,14 +81,17 @@ async function executeDbUpdateCommand(
80
81
  if (!ctxResult.ok) {
81
82
  return ctxResult;
82
83
  }
83
- const { client, contractJson, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
84
+ const { client, config, contractJson, dbConnection, onProgress, contractPathAbsolute } =
85
+ ctxResult.value;
86
+ const { migrationsDir } = resolveMigrationPaths(options.config, config);
84
87
 
85
88
  try {
86
- // Call dbUpdate with connection and progress callback
89
+ await client.connect(dbConnection);
90
+
87
91
  const result = await client.dbUpdate({
88
92
  contract: contractJson,
89
93
  mode: options.dryRun ? 'plan' : 'apply',
90
- connection: dbConnection,
94
+ migrationsDir,
91
95
  ...(flags.yes ? { acceptDataLoss: true } : {}),
92
96
  onProgress,
93
97
  });
@@ -112,7 +116,7 @@ async function executeDbUpdateCommand(
112
116
  label: op.label,
113
117
  operationClass: op.operationClass,
114
118
  })),
115
- ...ifDefined('sql', result.value.plan.sql),
119
+ ...ifDefined('preview', result.value.plan.preview),
116
120
  },
117
121
  ...ifDefined(
118
122
  'execution',
@@ -27,10 +27,12 @@ import {
27
27
  errorTargetMismatch,
28
28
  errorUnexpected,
29
29
  } from '../utils/cli-errors';
30
+ import { combineSchemaResults } from '../utils/combine-schema-results';
30
31
  import {
31
32
  addGlobalOptions,
32
33
  maskConnectionUrl,
33
34
  resolveContractPath,
35
+ resolveMigrationPaths,
34
36
  setCommandDescriptions,
35
37
  setCommandExamples,
36
38
  } from '../utils/command-helpers';
@@ -346,22 +348,42 @@ async function executeDbVerifyCommand(
346
348
  const setupResult = await resolveVerifySetup(paths, options, mode);
347
349
  if (!setupResult.ok) return setupResult;
348
350
  const { contractJson, dbConnection, contractPathAbsolute } = setupResult.value;
351
+ const { migrationsDir } = resolveMigrationPaths(options.config, setupResult.value.config);
349
352
 
350
353
  const client = createVerifyClient(setupResult.value);
351
354
  const onProgress = createProgressAdapter({ ui, flags });
352
355
 
353
356
  try {
357
+ // Single-contract marker verification preserved for the existing
358
+ // marker / target / hash failure surface (`PN-RUN-3001/3002/3003`).
359
+ // The aggregate verifier (run below for the per-space marker /
360
+ // schema checks) does not duplicate this: it concerns itself with
361
+ // marker-vs-on-disk and orphan-marker drift, not the
362
+ // hash-mismatch-against-the-app-contract lane that today's
363
+ // `client.verify` covers.
354
364
  const verifyResult = await client.verify({
355
365
  contract: contractJson,
356
366
  connection: dbConnection,
357
367
  onProgress,
358
368
  });
359
369
 
360
- // If verification failed, map to CLI structured error
361
370
  if (!verifyResult.ok) {
362
371
  return notOk(mapVerifyFailure(verifyResult));
363
372
  }
364
373
 
374
+ // Aggregate verifier (loader → verifier pipeline). Runs the layout
375
+ // precheck, marker-aware per-space verifier, and (full mode only)
376
+ // per-space pre-projected schema verification (closes F23).
377
+ const aggregateResult = await client.dbVerify({
378
+ contract: contractJson,
379
+ migrationsDir,
380
+ strict: options.strict ?? false,
381
+ skipSchema: mode === 'marker-only',
382
+ skipMarker: false,
383
+ onProgress,
384
+ });
385
+ if (!aggregateResult.ok) return notOk(aggregateResult.failure);
386
+
365
387
  if (mode === 'marker-only') {
366
388
  return ok({
367
389
  ok: true,
@@ -381,14 +403,13 @@ async function executeDbVerifyCommand(
381
403
  });
382
404
  }
383
405
 
384
- const schemaVerifyResult = await client.schemaVerify({
385
- contract: contractJson,
386
- strict: options.strict ?? false,
387
- onProgress,
388
- });
389
-
390
- if (!schemaVerifyResult.ok) {
391
- return notOk(schemaVerifyResult);
406
+ const combined = combineSchemaResults(
407
+ aggregateResult.value.schemaResults,
408
+ aggregateResult.value.appSpaceId,
409
+ options.strict ?? false,
410
+ );
411
+ if (!combined.ok) {
412
+ return notOk(combined);
392
413
  }
393
414
 
394
415
  return ok({
@@ -401,9 +422,9 @@ async function executeDbVerifyCommand(
401
422
  ...ifDefined('missingCodecs', verifyResult.missingCodecs),
402
423
  ...ifDefined('codecCoverageSkipped', verifyResult.codecCoverageSkipped),
403
424
  schema: {
404
- summary: schemaVerifyResult.summary,
405
- counts: schemaVerifyResult.schema.counts,
406
- strict: schemaVerifyResult.meta?.strict ?? false,
425
+ summary: combined.summary,
426
+ counts: combined.schema.counts,
427
+ strict: combined.meta?.strict ?? false,
407
428
  },
408
429
  meta: {
409
430
  ...(verifyResult.meta ?? {}),
@@ -429,19 +450,30 @@ async function executeDbSchemaOnlyVerifyCommand(
429
450
  const setupResult = await resolveVerifySetup(paths, options, 'schema-only');
430
451
  if (!setupResult.ok) return setupResult;
431
452
  const { contractJson, dbConnection, contractPathAbsolute } = setupResult.value;
453
+ const { migrationsDir } = resolveMigrationPaths(options.config, setupResult.value.config);
432
454
 
433
455
  const client = createVerifyClient(setupResult.value);
434
456
  const onProgress = createProgressAdapter({ ui, flags });
435
457
 
436
458
  try {
437
- const schemaVerifyResult = await client.schemaVerify({
459
+ await client.connect(dbConnection);
460
+ const aggregateResult = await client.dbVerify({
438
461
  contract: contractJson,
462
+ migrationsDir,
439
463
  strict: options.strict ?? false,
440
- connection: dbConnection,
464
+ skipSchema: false,
465
+ skipMarker: true,
441
466
  onProgress,
442
467
  });
468
+ if (!aggregateResult.ok) return notOk(aggregateResult.failure);
443
469
 
444
- return ok(schemaVerifyResult);
470
+ return ok(
471
+ combineSchemaResults(
472
+ aggregateResult.value.schemaResults,
473
+ aggregateResult.value.appSpaceId,
474
+ options.strict ?? false,
475
+ ),
476
+ );
445
477
  } catch (error) {
446
478
  return wrapVerifyError(error, contractPathAbsolute, 'db verify --schema-only');
447
479
  } finally {
@@ -113,7 +113,7 @@ export function createInitCommand(): Command {
113
113
  * we honour it (e.g. testing flows where stdin is stubbed).
114
114
  *
115
115
  * Exported so callers and tests can derive the same value without
116
- * touching `process` globals — F14 of the M1/M2 review.
116
+ * touching `process` globals.
117
117
  */
118
118
  export function deriveCanPrompt(opts: {
119
119
  readonly flagsInteractive: boolean | undefined;
@@ -569,7 +569,7 @@ async function runInstall(ctx: {
569
569
  /**
570
570
  * FR2.1 — set when the user already declares `@types/node` directly in
571
571
  * `dependencies` or `devDependencies`. We then skip adding it so a
572
- * pinned major (e.g. `^18` for a Node 18 runtime) survives `init`
572
+ * locked major (e.g. `^18` for a Node 18 runtime) survives `init`
573
573
  * unchanged. Transitive presence is intentionally ignored: detecting
574
574
  * it requires lockfile introspection and the realistic clobber risk
575
575
  * is the direct-pin case.
@@ -585,7 +585,7 @@ async function runInstall(ctx: {
585
585
  // Pin it as a devDep rather than relying on a transitive resolution
586
586
  // through `dotenv` (whose types bundle is internal and not guaranteed
587
587
  // across versions). Skip when the user already declares `@types/node`
588
- // directly so a pinned major (e.g. `^18` for a Node 18 runtime) is
588
+ // directly so a locked major (e.g. `^18` for a Node 18 runtime) is
589
589
  // preserved. Listed last so the install log still leads with the
590
590
  // user-relevant `prisma-next` line.
591
591
  const devDeps = hasTypesNode ? ['prisma-next'] : ['prisma-next', '@types/node'];