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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +56 -21
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
  7. package/dist/cli.mjs +131 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-TG7rbCWT.mjs → client-keSCAgjW.mjs} +43 -19
  10. package/dist/client-keSCAgjW.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +7 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +8 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +11 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +8 -5
  19. package/dist/commands/db-schema.mjs.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -9
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.mjs +10 -9
  25. package/dist/commands/db-verify.mjs.map +1 -1
  26. package/dist/commands/migration-apply.d.mts +2 -2
  27. package/dist/commands/migration-apply.d.mts.map +1 -1
  28. package/dist/commands/migration-apply.mjs +15 -38
  29. package/dist/commands/migration-apply.mjs.map +1 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +24 -30
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts +14 -5
  34. package/dist/commands/migration-plan.d.mts.map +1 -1
  35. package/dist/commands/migration-plan.mjs +45 -47
  36. package/dist/commands/migration-plan.mjs.map +1 -1
  37. package/dist/commands/migration-ref.d.mts +6 -4
  38. package/dist/commands/migration-ref.d.mts.map +1 -1
  39. package/dist/commands/migration-ref.mjs +31 -40
  40. package/dist/commands/migration-ref.mjs.map +1 -1
  41. package/dist/commands/migration-show.d.mts +13 -7
  42. package/dist/commands/migration-show.d.mts.map +1 -1
  43. package/dist/commands/migration-show.mjs +28 -29
  44. package/dist/commands/migration-show.mjs.map +1 -1
  45. package/dist/commands/migration-status.d.mts +5 -4
  46. package/dist/commands/migration-status.d.mts.map +1 -1
  47. package/dist/commands/migration-status.mjs +7 -2
  48. package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  49. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  50. package/dist/config-loader.mjs +1 -1
  51. package/dist/contract-emit-DS5NzZh2.mjs +6 -0
  52. package/dist/contract-emit-DWtGQYCD.mjs +150 -0
  53. package/dist/contract-emit-DWtGQYCD.mjs.map +1 -0
  54. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  55. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  56. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  57. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  58. package/dist/{contract-infer-BP3DrGgz.mjs → contract-infer-GztVCOCJ.mjs} +11 -19
  59. package/dist/contract-infer-GztVCOCJ.mjs.map +1 -0
  60. package/dist/exports/control-api.d.mts +78 -21
  61. package/dist/exports/control-api.d.mts.map +1 -1
  62. package/dist/exports/control-api.mjs +7 -5
  63. package/dist/exports/index.mjs +8 -3
  64. package/dist/exports/index.mjs.map +1 -1
  65. package/dist/exports/init-output.d.mts +39 -0
  66. package/dist/exports/init-output.d.mts.map +1 -0
  67. package/dist/exports/init-output.mjs +3 -0
  68. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  69. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  70. package/dist/init-DAbQMxIR.mjs +2062 -0
  71. package/dist/init-DAbQMxIR.mjs.map +1 -0
  72. package/dist/{inspect-live-schema-DWzf4Q_m.mjs → inspect-live-schema-BaR9ISwa.mjs} +9 -9
  73. package/dist/inspect-live-schema-BaR9ISwa.mjs.map +1 -0
  74. package/dist/migration-cli.d.mts +41 -11
  75. package/dist/migration-cli.d.mts.map +1 -1
  76. package/dist/migration-cli.mjs +308 -84
  77. package/dist/migration-cli.mjs.map +1 -1
  78. package/dist/{migration-command-scaffold-CLMD302g.mjs → migration-command-scaffold-D1dWuEWQ.mjs} +7 -7
  79. package/dist/{migration-command-scaffold-CLMD302g.mjs.map → migration-command-scaffold-D1dWuEWQ.mjs.map} +1 -1
  80. package/dist/{migration-status-B0HLF7So.mjs → migration-status-CP5k8O5i.mjs} +21 -35
  81. package/dist/migration-status-CP5k8O5i.mjs.map +1 -0
  82. package/dist/{migrations-B0dOQlk0.mjs → migrations-MEoKMiV5.mjs} +42 -21
  83. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  84. package/dist/output-BpcQrnnq.mjs +103 -0
  85. package/dist/output-BpcQrnnq.mjs.map +1 -0
  86. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  87. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  88. package/dist/quick-reference-mongo.md +34 -13
  89. package/dist/quick-reference-postgres.md +11 -9
  90. package/dist/{result-handler-CIyu0Pdt.mjs → result-handler-BmVh8AeV.mjs} +12 -93
  91. package/dist/result-handler-BmVh8AeV.mjs.map +1 -0
  92. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  93. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  94. package/dist/{verify-BxiVp50b.mjs → verify-BT9tgCOH.mjs} +2 -2
  95. package/dist/{verify-BxiVp50b.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  96. package/package.json +21 -15
  97. package/src/cli.ts +32 -6
  98. package/src/commands/contract-emit.ts +67 -163
  99. package/src/commands/contract-infer.ts +7 -20
  100. package/src/commands/db-init.ts +1 -0
  101. package/src/commands/db-update.ts +1 -1
  102. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  103. package/src/commands/init/errors.ts +254 -0
  104. package/src/commands/init/exit-codes.ts +62 -0
  105. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  106. package/src/commands/init/hygiene-gitignore.ts +48 -0
  107. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  108. package/src/commands/init/index.ts +112 -7
  109. package/src/commands/init/init.ts +766 -144
  110. package/src/commands/init/inputs.ts +421 -0
  111. package/src/commands/init/output.ts +147 -0
  112. package/src/commands/init/probe-db.ts +308 -0
  113. package/src/commands/init/reinit-cleanup.ts +83 -0
  114. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  115. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  116. package/src/commands/init/templates/agent-skill.ts +25 -3
  117. package/src/commands/init/templates/code-templates.ts +125 -32
  118. package/src/commands/init/templates/env.ts +80 -0
  119. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  120. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  121. package/src/commands/init/templates/quick-reference.ts +42 -3
  122. package/src/commands/init/templates/tsconfig.ts +167 -5
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +16 -51
  125. package/src/commands/migration-new.ts +26 -32
  126. package/src/commands/migration-plan.ts +80 -55
  127. package/src/commands/migration-ref.ts +40 -54
  128. package/src/commands/migration-show.ts +53 -36
  129. package/src/commands/migration-status.ts +33 -50
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +21 -0
  132. package/src/control-api/operations/contract-emit.ts +198 -115
  133. package/src/control-api/operations/db-init.ts +8 -5
  134. package/src/control-api/operations/db-update.ts +8 -5
  135. package/src/control-api/operations/migration-apply.ts +29 -9
  136. package/src/control-api/types.ts +61 -7
  137. package/src/exports/control-api.ts +2 -1
  138. package/src/exports/init-output.ts +10 -0
  139. package/src/migration-cli.ts +445 -122
  140. package/src/utils/cli-errors.ts +49 -2
  141. package/src/utils/command-helpers.ts +13 -26
  142. package/src/utils/emit-queue.ts +26 -0
  143. package/src/utils/formatters/graph-migration-mapper.ts +2 -2
  144. package/src/utils/formatters/migrations.ts +62 -26
  145. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  146. package/dist/cli-errors-DHq6GQGu.mjs +0 -5
  147. package/dist/client-TG7rbCWT.mjs.map +0 -1
  148. package/dist/config-loader-_W4T21X1.mjs.map +0 -1
  149. package/dist/contract-emit-CNYyzJwF.mjs +0 -195
  150. package/dist/contract-emit-CNYyzJwF.mjs.map +0 -1
  151. package/dist/contract-emit-CQfj7xJn.mjs +0 -122
  152. package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
  153. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  154. package/dist/contract-infer-BP3DrGgz.mjs.map +0 -1
  155. package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
  156. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
  157. package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
  158. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
  159. package/dist/init-CQfo_4Ro.mjs +0 -430
  160. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  161. package/dist/inspect-live-schema-DWzf4Q_m.mjs.map +0 -1
  162. package/dist/migration-status-B0HLF7So.mjs.map +0 -1
  163. package/dist/migrations-B0dOQlk0.mjs.map +0 -1
  164. package/dist/result-handler-CIyu0Pdt.mjs.map +0 -1
  165. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  166. package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
  167. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
  168. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  169. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,5 +1,5 @@
1
1
  import type { CoreSchemaView } from '@prisma-next/framework-components/control';
2
- import { validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
2
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
3
3
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
4
  import { relative, resolve } from 'pathe';
5
5
  import { loadConfig } from '../config-loader';
@@ -33,6 +33,12 @@ export interface InspectLiveSchemaResult {
33
33
  readonly config: LoadedCliConfig;
34
34
  readonly schema: unknown;
35
35
  readonly schemaView: CoreSchemaView | undefined;
36
+ /**
37
+ * PSL AST inferred from the introspected schema, when the configured family
38
+ * implements `PslContractInferCapable`. `undefined` for families that do not
39
+ * support inference (e.g. Mongo today).
40
+ */
41
+ readonly pslContractAst: PslDocumentAst | undefined;
36
42
  readonly target: {
37
43
  readonly familyId: string;
38
44
  readonly id: string;
@@ -122,14 +128,12 @@ export async function inspectLiveSchema(
122
128
  const onProgress = createProgressAdapter({ ui, flags });
123
129
 
124
130
  try {
125
- const schemaIR = await client.introspect({
131
+ const schema = await client.introspect({
126
132
  connection: dbConnection,
127
133
  onProgress,
128
134
  });
129
- // TODO(TML-2251): Remove SQL-specific branching — SQL should use the same family-agnostic path as Mongo.
130
- const schema =
131
- config.family.familyId === 'sql' ? validatePrintableSqlSchemaIR(schemaIR) : schemaIR;
132
135
  const schemaView = client.toSchemaView(schema);
136
+ const pslContractAst = client.inferPslContract(schema);
133
137
 
134
138
  const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
135
139
 
@@ -137,6 +141,7 @@ export async function inspectLiveSchema(
137
141
  config,
138
142
  schema,
139
143
  schemaView,
144
+ pslContractAst,
140
145
  target: {
141
146
  familyId: config.family.familyId,
142
147
  id: config.target.targetId,
@@ -1,9 +1,8 @@
1
- import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
2
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
- import { findPathWithDecision } from '@prisma-next/migration-tools/dag';
2
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
+ import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
4
+ import type { MigrationPackage } from '@prisma-next/migration-tools/package';
4
5
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
5
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
6
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
7
6
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
7
  import { Command } from 'commander';
9
8
 
@@ -18,11 +17,11 @@ import {
18
17
  errorRuntime,
19
18
  errorTargetMigrationNotSupported,
20
19
  errorUnexpected,
20
+ mapMigrationToolsError,
21
21
  } from '../utils/cli-errors';
22
22
  import {
23
23
  addGlobalOptions,
24
- loadAllBundles,
25
- type MigrationBundleSet,
24
+ loadMigrationPackages,
26
25
  maskConnectionUrl,
27
26
  readContractEnvelope,
28
27
  resolveMigrationPaths,
@@ -51,7 +50,7 @@ export interface MigrationApplyResult {
51
50
  readonly markerHash: string;
52
51
  readonly applied: readonly {
53
52
  readonly dirName: string;
54
- readonly from: string;
53
+ readonly from: string | null;
55
54
  readonly to: string;
56
55
  readonly operationsExecuted: number;
57
56
  }[];
@@ -64,7 +63,7 @@ export interface MigrationApplyResult {
64
63
  readonly refName?: string;
65
64
  readonly selectedPath: readonly {
66
65
  readonly dirName: string;
67
- readonly migrationId: string;
66
+ readonly migrationHash: string;
68
67
  readonly from: string;
69
68
  readonly to: string;
70
69
  }[];
@@ -74,19 +73,6 @@ export interface MigrationApplyResult {
74
73
  };
75
74
  }
76
75
 
77
- function mapMigrationToolsError(error: unknown): CliStructuredErrorType {
78
- if (MigrationToolsError.is(error)) {
79
- return errorRuntime(error.message, {
80
- why: error.why,
81
- fix: error.fix,
82
- meta: { code: error.code, ...(error.details ?? {}) },
83
- });
84
- }
85
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
86
- why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
87
- });
88
- }
89
-
90
76
  function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
91
77
  return errorRuntime(failure.summary, {
92
78
  why: failure.why ?? 'Migration runner failed',
@@ -95,12 +81,12 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
95
81
  });
96
82
  }
97
83
 
98
- function packageToStep(pkg: MigrationBundle): MigrationApplyStep {
84
+ function packageToStep(pkg: MigrationPackage): MigrationApplyStep {
99
85
  return {
100
86
  dirName: pkg.dirName,
101
- from: pkg.manifest.from,
102
- to: pkg.manifest.to,
103
- toContract: pkg.manifest.toContract,
87
+ from: pkg.metadata.from,
88
+ to: pkg.metadata.to,
89
+ toContract: pkg.metadata.toContract,
104
90
  operations: pkg.ops,
105
91
  };
106
92
  }
@@ -112,7 +98,7 @@ async function executeMigrationApplyCommand(
112
98
  startTime: number,
113
99
  ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
114
100
  const config = await loadConfig(options.config);
115
- const { configPath, migrationsDir, migrationsRelative, refsPath } = resolveMigrationPaths(
101
+ const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
116
102
  options.config,
117
103
  config,
118
104
  );
@@ -149,8 +135,8 @@ async function executeMigrationApplyCommand(
149
135
  if (options.ref) {
150
136
  refName = options.ref;
151
137
  try {
152
- const refs = await readRefs(refsPath);
153
- destinationHash = resolveRef(refs, refName);
138
+ const refs = await readRefs(refsDir);
139
+ destinationHash = resolveRef(refs, refName).hash;
154
140
  } catch (error) {
155
141
  if (MigrationToolsError.is(error)) {
156
142
  return notOk(mapMigrationToolsError(error));
@@ -196,9 +182,9 @@ async function executeMigrationApplyCommand(
196
182
  }
197
183
 
198
184
  // Read migrations and build migration chain model (offline — no DB needed)
199
- let migrations: MigrationBundleSet;
185
+ let migrations: Awaited<ReturnType<typeof loadMigrationPackages>>;
200
186
  try {
201
- migrations = await loadAllBundles(migrationsDir);
187
+ migrations = await loadMigrationPackages(migrationsDir);
202
188
  } catch (error) {
203
189
  if (MigrationToolsError.is(error)) {
204
190
  return notOk(mapMigrationToolsError(error));
@@ -206,27 +192,6 @@ async function executeMigrationApplyCommand(
206
192
  throw error;
207
193
  }
208
194
 
209
- // Defense in depth: re-hash every bundle and confirm the recorded
210
- // `migrationId` matches the on-disk `(manifest, ops)`. Catches FS
211
- // corruption, partial writes, and post-emit hand edits before we
212
- // start touching the database.
213
- for (const bundle of migrations.bundles) {
214
- const verified = verifyMigrationBundle(bundle);
215
- if (!verified.ok) {
216
- return notOk(
217
- errorRuntime(`Migration package is corrupt: ${bundle.dirName}`, {
218
- why: `Stored migrationId "${verified.storedMigrationId}" does not match the recomputed hash "${verified.computedMigrationId}" for ${migrationsRelative}/${bundle.dirName}. The migration.json or ops.json has been edited or partially written since emit.`,
219
- fix: `Re-emit the package by running \`node "${migrationsRelative}/${bundle.dirName}/migration.ts"\`, or restore the directory from version control.`,
220
- meta: {
221
- dirName: bundle.dirName,
222
- storedMigrationId: verified.storedMigrationId,
223
- computedMigrationId: verified.computedMigrationId,
224
- },
225
- }),
226
- );
227
- }
228
- }
229
-
230
195
  const client = createControlClient({
231
196
  family: config.family,
232
197
  target: config.target,
@@ -12,31 +12,35 @@ import { readFileSync } from 'node:fs';
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
13
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
14
14
  import { createControlStack } from '@prisma-next/framework-components/control';
15
- import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
16
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
17
- import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
15
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
16
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
18
17
  import {
19
18
  copyFilesWithRename,
20
19
  formatMigrationDirName,
21
20
  readMigrationsDir,
22
21
  writeMigrationPackage,
23
22
  } from '@prisma-next/migration-tools/io';
23
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
+ import {
25
+ findLatestMigration,
26
+ reconstructGraph,
27
+ } from '@prisma-next/migration-tools/migration-graph';
24
28
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
25
- import type { MigrationManifest } from '@prisma-next/migration-tools/types';
26
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
27
29
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
28
30
  import { Command } from 'commander';
29
- import { join, relative, resolve } from 'pathe';
31
+ import { join, relative } from 'pathe';
30
32
  import { loadConfig } from '../config-loader';
31
33
  import {
32
34
  CliStructuredError,
33
35
  errorRuntime,
34
36
  errorTargetMigrationNotSupported,
35
37
  errorUnexpected,
38
+ mapMigrationToolsError,
36
39
  } from '../utils/cli-errors';
37
40
  import {
38
41
  addGlobalOptions,
39
42
  getTargetMigrations,
43
+ resolveContractPath,
40
44
  resolveMigrationPaths,
41
45
  setCommandDescriptions,
42
46
  setCommandExamples,
@@ -57,7 +61,7 @@ interface MigrationNewOptions extends CommonCommandOptions {
57
61
  interface MigrationNewResult {
58
62
  readonly ok: true;
59
63
  readonly dir: string;
60
- readonly from: string;
64
+ readonly from: string | null;
61
65
  readonly to: string;
62
66
  readonly summary: string;
63
67
  }
@@ -68,11 +72,7 @@ async function executeMigrationNewCommand(
68
72
  const config = await loadConfig(options.config);
69
73
  const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
70
74
 
71
- const contractPath = config.contract?.output ?? 'contract.json';
72
- const contractPathAbsolute = resolve(
73
- options.config ? resolve(options.config, '..') : process.cwd(),
74
- contractPath,
75
- );
75
+ const contractPathAbsolute = resolveContractPath(config);
76
76
 
77
77
  let contractJsonContent: string;
78
78
  try {
@@ -116,7 +116,7 @@ async function executeMigrationNewCommand(
116
116
  }
117
117
 
118
118
  let fromContract: Contract | null = null;
119
- let fromHash: string = EMPTY_CONTRACT_HASH;
119
+ let fromHash: string | null = null;
120
120
  let fromContractSourceDir: string | null = null;
121
121
 
122
122
  try {
@@ -126,7 +126,7 @@ async function executeMigrationNewCommand(
126
126
  const graph = reconstructGraph(packages);
127
127
 
128
128
  if (options.from) {
129
- const match = packages.find((p) => p.manifest.to.startsWith(options.from!));
129
+ const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
130
130
  if (!match) {
131
131
  return notOk(
132
132
  errorRuntime('Starting contract not found', {
@@ -135,18 +135,18 @@ async function executeMigrationNewCommand(
135
135
  }),
136
136
  );
137
137
  }
138
- fromHash = match.manifest.to;
139
- fromContract = match.manifest.toContract;
138
+ fromHash = match.metadata.to;
139
+ fromContract = match.metadata.toContract;
140
140
  fromContractSourceDir = match.dirPath;
141
141
  } else {
142
142
  const latestMigration = findLatestMigration(graph);
143
143
  if (latestMigration) {
144
144
  fromHash = latestMigration.to;
145
145
  const leafPkg = packages.find(
146
- (p) => p.manifest.migrationId === latestMigration.migrationId,
146
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
147
147
  );
148
148
  if (leafPkg) {
149
- fromContract = leafPkg.manifest.toContract;
149
+ fromContract = leafPkg.metadata.toContract;
150
150
  fromContractSourceDir = leafPkg.dirPath;
151
151
  }
152
152
  }
@@ -154,13 +154,7 @@ async function executeMigrationNewCommand(
154
154
  }
155
155
  } catch (error) {
156
156
  if (MigrationToolsError.is(error)) {
157
- return notOk(
158
- errorRuntime(error.message, {
159
- why: error.why,
160
- fix: error.fix,
161
- meta: { code: error.code },
162
- }),
163
- );
157
+ return notOk(mapMigrationToolsError(error));
164
158
  }
165
159
  throw error;
166
160
  }
@@ -181,12 +175,11 @@ async function executeMigrationNewCommand(
181
175
 
182
176
  // `migration new` scaffolds an empty `migration.ts` for the user to
183
177
  // fill, so we attest over `ops: []`. Re-running self-emit after the
184
- // user adds operations will produce a different `migrationId` (over
178
+ // user adds operations will produce a different `migrationHash` (over
185
179
  // the real ops). This is intentional — there is no on-disk draft.
186
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
180
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
187
181
  from: fromHash,
188
182
  to: toStorageHash,
189
- kind: 'regular',
190
183
  fromContract,
191
184
  toContract: toContractJson,
192
185
  hints: {
@@ -195,11 +188,12 @@ async function executeMigrationNewCommand(
195
188
  plannerVersion: '1.0.0',
196
189
  },
197
190
  labels: [],
191
+ providedInvariants: [],
198
192
  createdAt: timestamp.toISOString(),
199
193
  };
200
- const manifest: MigrationManifest = {
201
- ...baseManifest,
202
- migrationId: computeMigrationId(baseManifest, []),
194
+ const metadata: MigrationMetadata = {
195
+ ...baseMetadata,
196
+ migrationHash: computeMigrationHash(baseMetadata, []),
203
197
  };
204
198
 
205
199
  const migrations = getTargetMigrations(config.target);
@@ -218,7 +212,7 @@ async function executeMigrationNewCommand(
218
212
  ...(config.extensionPacks ?? []),
219
213
  ]);
220
214
 
221
- await writeMigrationPackage(packageDir, manifest, []);
215
+ await writeMigrationPackage(packageDir, metadata, []);
222
216
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
223
217
  await copyFilesWithRename(packageDir, [
224
218
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -3,23 +3,25 @@ import type { Contract } from '@prisma-next/contract/types';
3
3
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import {
5
5
  createControlStack,
6
+ hasOperationPreview,
6
7
  type MigrationPlanOperation,
8
+ type OperationPreview,
7
9
  } from '@prisma-next/framework-components/control';
8
- import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
9
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
10
- import { findLatestMigration } from '@prisma-next/migration-tools/dag';
10
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
11
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
12
+ import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
11
13
  import {
12
14
  copyFilesWithRename,
13
15
  formatMigrationDirName,
14
16
  writeMigrationPackage,
15
17
  } from '@prisma-next/migration-tools/io';
18
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
19
+ import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
16
20
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
17
- import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
18
21
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
19
22
  import { Command } from 'commander';
20
23
  import { join, relative } from 'pathe';
21
24
  import { loadConfig } from '../config-loader';
22
- import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
23
25
  import {
24
26
  type CliErrorConflict,
25
27
  CliStructuredError,
@@ -29,11 +31,12 @@ import {
29
31
  errorRuntime,
30
32
  errorTargetMigrationNotSupported,
31
33
  errorUnexpected,
34
+ mapMigrationToolsError,
32
35
  } from '../utils/cli-errors';
33
36
  import {
34
37
  addGlobalOptions,
35
38
  getTargetMigrations,
36
- loadAllBundles,
39
+ loadMigrationPackages,
37
40
  resolveContractPath,
38
41
  resolveMigrationPaths,
39
42
  setCommandDescriptions,
@@ -55,7 +58,7 @@ interface MigrationPlanOptions extends CommonCommandOptions {
55
58
  export interface MigrationPlanResult {
56
59
  readonly ok: boolean;
57
60
  readonly noOp: boolean;
58
- readonly from: string;
61
+ readonly from: string | null;
59
62
  readonly to: string;
60
63
  readonly dir?: string;
61
64
  readonly operations: readonly {
@@ -63,7 +66,12 @@ export interface MigrationPlanResult {
63
66
  readonly label: string;
64
67
  readonly operationClass: string;
65
68
  }[];
66
- readonly sql?: readonly string[];
69
+ /**
70
+ * Family-agnostic textual preview of the migration plan operations.
71
+ * Replaces the previous `sql?: readonly string[]` field; consumers should
72
+ * read `result.preview?.statements`.
73
+ */
74
+ readonly preview?: OperationPreview;
67
75
  readonly summary: string;
68
76
  /**
69
77
  * When true, `migration.ts` was written but contains unfilled
@@ -76,22 +84,6 @@ export interface MigrationPlanResult {
76
84
  };
77
85
  }
78
86
 
79
- function mapMigrationToolsError(error: unknown): CliStructuredError {
80
- if (CliStructuredError.is(error)) {
81
- return error;
82
- }
83
- if (MigrationToolsError.is(error)) {
84
- return errorRuntime(error.message, {
85
- why: error.why,
86
- fix: error.fix,
87
- meta: { code: error.code, ...(error.details ?? {}) },
88
- });
89
- }
90
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
91
- why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
92
- });
93
- }
94
-
95
87
  async function executeMigrationPlanCommand(
96
88
  options: MigrationPlanOptions,
97
89
  flags: GlobalFlags,
@@ -173,11 +165,11 @@ async function executeMigrationPlanCommand(
173
165
 
174
166
  // Read existing migrations and determine "from" contract
175
167
  let fromContract: Contract | null = null;
176
- let fromHash: string = EMPTY_CONTRACT_HASH;
168
+ let fromHash: string | null = null;
177
169
  let fromContractSourceDir: string | null = null;
178
170
 
179
171
  try {
180
- const { bundles, graph } = await loadAllBundles(migrationsDir);
172
+ const { bundles, graph } = await loadMigrationPackages(migrationsDir);
181
173
 
182
174
  if (options.from) {
183
175
  const resolved = resolveBundleByPrefix(bundles, options.from);
@@ -195,16 +187,18 @@ async function executeMigrationPlanCommand(
195
187
  }),
196
188
  );
197
189
  }
198
- fromHash = resolved.value.manifest.to;
199
- fromContract = resolved.value.manifest.toContract;
190
+ fromHash = resolved.value.metadata.to;
191
+ fromContract = resolved.value.metadata.toContract;
200
192
  fromContractSourceDir = resolved.value.dirPath;
201
193
  } else {
202
194
  const latestMigration = findLatestMigration(graph);
203
195
  if (latestMigration) {
204
196
  fromHash = latestMigration.to;
205
- const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
197
+ const leafPkg = bundles.find(
198
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
199
+ );
206
200
  if (leafPkg) {
207
- fromContract = leafPkg.manifest.toContract;
201
+ fromContract = leafPkg.metadata.toContract;
208
202
  fromContractSourceDir = leafPkg.dirPath;
209
203
  }
210
204
  }
@@ -213,7 +207,16 @@ async function executeMigrationPlanCommand(
213
207
  if (MigrationToolsError.is(error)) {
214
208
  return notOk(mapMigrationToolsError(error));
215
209
  }
216
- throw error;
210
+ // Wrap unexpected (non-MigrationToolsError) failures from the migration
211
+ // load phase in a structured CLI envelope. Letting them throw would
212
+ // bypass `handleResult()` and crash the command — see CLI structured-
213
+ // errors guideline (CliStructuredError + Result pattern).
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ return notOk(
216
+ errorUnexpected(message, {
217
+ why: `Unexpected error while loading migrations: ${message}`,
218
+ }),
219
+ );
217
220
  }
218
221
 
219
222
  // Check for no-op (same hash means no changes)
@@ -251,10 +254,9 @@ async function executeMigrationPlanCommand(
251
254
  const dirName = formatMigrationDirName(timestamp, slug);
252
255
  const packageDir = join(migrationsDir, dirName);
253
256
 
254
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
257
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
255
258
  from: fromHash,
256
259
  to: toStorageHash,
257
- kind: 'regular',
258
260
  fromContract,
259
261
  toContract: toContractJson,
260
262
  hints: {
@@ -320,18 +322,22 @@ async function executeMigrationPlanCommand(
320
322
 
321
323
  const migrationTsContent = plannerResult.plan.renderTypeScript();
322
324
 
323
- // Always-attest: compute migrationId over (manifest, ops). When
324
- // placeholders blocked lowering, ops is `[]` and the id hashes over
325
- // the empty list — re-emitting after the user fills the placeholder
326
- // produces a different id (over the real ops). This is intentional;
325
+ // Always-attest: compute migrationHash over (metadata, ops). When
326
+ // placeholders blocked lowering, ops is `[]` and the hash is computed
327
+ // over the empty list — re-emitting after the user fills the placeholder
328
+ // produces a different hash (over the real ops). This is intentional;
327
329
  // there is no on-disk "draft" state.
328
330
  const opsForWrite = hasPlaceholders ? [] : plannedOps;
329
- const manifest: MigrationManifest = {
330
- ...baseManifest,
331
- migrationId: computeMigrationId(baseManifest, opsForWrite),
331
+ const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
332
+ ...baseMetadata,
333
+ providedInvariants: deriveProvidedInvariants(opsForWrite),
334
+ };
335
+ const metadata: MigrationMetadata = {
336
+ ...metadataWithInvariants,
337
+ migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
332
338
  };
333
339
 
334
- await writeMigrationPackage(packageDir, manifest, opsForWrite);
340
+ await writeMigrationPackage(packageDir, metadata, opsForWrite);
335
341
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
336
342
  await copyFilesWithRename(packageDir, [
337
343
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -364,7 +370,9 @@ async function executeMigrationPlanCommand(
364
370
  return ok(result);
365
371
  }
366
372
 
367
- const sql = extractSqlDdl(plannedOps);
373
+ const preview = hasOperationPreview(familyInstance)
374
+ ? familyInstance.toOperationPreview(plannedOps)
375
+ : undefined;
368
376
  const result: MigrationPlanResult = {
369
377
  ok: true,
370
378
  noOp: false,
@@ -376,13 +384,24 @@ async function executeMigrationPlanCommand(
376
384
  label: op.label,
377
385
  operationClass: op.operationClass,
378
386
  })),
379
- sql,
387
+ ...(preview !== undefined ? { preview } : {}),
380
388
  summary: `Planned ${plannedOps.length} operation(s)`,
381
389
  timings: { total: Date.now() - startTime },
382
390
  };
383
391
  return ok(result);
384
392
  } catch (error) {
385
- return notOk(mapMigrationToolsError(error));
393
+ if (CliStructuredError.is(error)) {
394
+ return notOk(error);
395
+ }
396
+ if (MigrationToolsError.is(error)) {
397
+ return notOk(mapMigrationToolsError(error));
398
+ }
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ return notOk(
401
+ errorUnexpected(message, {
402
+ why: `Unexpected error during migration plan: ${message}`,
403
+ }),
404
+ );
386
405
  }
387
406
  }
388
407
 
@@ -489,17 +508,20 @@ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFla
489
508
 
490
509
  lines.push('');
491
510
  lines.push(
492
- `Next: ${green_(`node ${result.dir ?? '<dir>'}/migration.ts`)} to emit ops.json and attest migrationId before running ${green_('prisma-next migration apply')}.`,
511
+ `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migration apply')}.`,
493
512
  );
494
513
 
495
- if (result.sql && result.sql.length > 0) {
514
+ if (result.preview && result.preview.statements.length > 0) {
515
+ // The non-empty length is already guaranteed by the surrounding check, so
516
+ // a plain `every` here is equivalent to the helper in formatters/migrations.ts.
517
+ const allSql = result.preview.statements.every((s) => s.language === 'sql');
496
518
  lines.push('');
497
- lines.push(dim_('DDL preview'));
519
+ lines.push(dim_(allSql ? 'DDL preview' : 'Operation preview'));
498
520
  lines.push('');
499
- for (const statement of result.sql) {
500
- const trimmed = statement.trim();
521
+ for (const statement of result.preview.statements) {
522
+ const trimmed = statement.text.trim();
501
523
  if (!trimmed) continue;
502
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
524
+ const line = statement.language === 'sql' && !trimmed.endsWith(';') ? `${trimmed};` : trimmed;
503
525
  lines.push(line);
504
526
  }
505
527
  }
@@ -517,24 +539,27 @@ export type PrefixResolutionFailure =
517
539
  | { reason: 'not-found' };
518
540
 
519
541
  /**
520
- * Resolve a migration bundle by exact hash or prefix match.
542
+ * Resolve a migration package by **target contract hash** (`metadata.to`)
543
+ * using exact match or prefix match.
521
544
  *
545
+ * Note: matches `metadata.to` (the contract hash this migration produces),
546
+ * not `metadata.migrationHash` (the package's content-addressed identity).
522
547
  * Tries exact match first, then prefix match (auto-prepending `sha256:` when
523
- * the needle omits the scheme). Returns the matched bundle on success, or a
548
+ * the needle omits the scheme). Returns the matched package on success, or a
524
549
  * discriminated failure indicating whether the prefix was ambiguous or simply
525
550
  * not found.
526
551
  *
527
552
  * @internal Exported for testing only.
528
553
  */
529
- export function resolveBundleByPrefix<T extends { manifest: { to: string } }>(
554
+ export function resolveBundleByPrefix<T extends { metadata: { to: string } }>(
530
555
  bundles: readonly T[],
531
556
  needle: string,
532
557
  ): Result<T, PrefixResolutionFailure> {
533
- const exact = bundles.find((p) => p.manifest.to === needle);
558
+ const exact = bundles.find((p) => p.metadata.to === needle);
534
559
  if (exact) return ok(exact);
535
560
 
536
561
  const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
537
- const candidates = bundles.filter((p) => p.manifest.to.startsWith(prefixWithScheme));
562
+ const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
538
563
 
539
564
  if (candidates.length === 1) return ok(candidates[0]!);
540
565
  if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });