@prisma-next/cli 0.4.0-dev.5 → 0.4.0-dev.7

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 (92) hide show
  1. package/README.md +8 -7
  2. package/dist/cli-errors-BUuJr6py.mjs +5 -0
  3. package/dist/{cli-errors-DStABy9d.d.mts → cli-errors-Dic2eADK.d.mts} +1 -0
  4. package/dist/cli.mjs +9 -16
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-tdnbk0OR.mjs → client-CJxHfhze.mjs} +4 -2
  7. package/dist/client-CJxHfhze.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +1 -6
  9. package/dist/commands/contract-infer.mjs +1 -7
  10. package/dist/commands/db-init.mjs +5 -6
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -6
  13. package/dist/commands/db-schema.mjs.map +1 -1
  14. package/dist/commands/db-sign.mjs +4 -5
  15. package/dist/commands/db-sign.mjs.map +1 -1
  16. package/dist/commands/db-update.mjs +5 -6
  17. package/dist/commands/db-update.mjs.map +1 -1
  18. package/dist/commands/db-verify.mjs +4 -5
  19. package/dist/commands/db-verify.mjs.map +1 -1
  20. package/dist/commands/migration-apply.mjs +6 -7
  21. package/dist/commands/migration-apply.mjs.map +1 -1
  22. package/dist/commands/migration-emit.d.mts +38 -0
  23. package/dist/commands/migration-emit.d.mts.map +1 -0
  24. package/dist/commands/migration-emit.mjs +81 -0
  25. package/dist/commands/migration-emit.mjs.map +1 -0
  26. package/dist/commands/migration-new.d.mts.map +1 -1
  27. package/dist/commands/migration-new.mjs +33 -10
  28. package/dist/commands/migration-new.mjs.map +1 -1
  29. package/dist/commands/migration-plan.d.mts.map +1 -1
  30. package/dist/commands/migration-plan.mjs +93 -81
  31. package/dist/commands/migration-plan.mjs.map +1 -1
  32. package/dist/commands/migration-ref.d.mts +1 -1
  33. package/dist/commands/migration-ref.mjs +2 -2
  34. package/dist/commands/migration-show.d.mts +1 -1
  35. package/dist/commands/migration-show.mjs +4 -4
  36. package/dist/commands/migration-show.mjs.map +1 -1
  37. package/dist/commands/migration-status.mjs +1 -6
  38. package/dist/contract-emit-C2_J2U7A.mjs +4 -0
  39. package/dist/{contract-emit-CRoS1nx5.mjs → contract-emit-CKig_Lra.mjs} +4 -4
  40. package/dist/{contract-emit-CRoS1nx5.mjs.map → contract-emit-CKig_Lra.mjs.map} +1 -1
  41. package/dist/{contract-emit-Ctn6mH9H.mjs → contract-emit-gpJNLGs7.mjs} +5 -5
  42. package/dist/{contract-emit-Ctn6mH9H.mjs.map → contract-emit-gpJNLGs7.mjs.map} +1 -1
  43. package/dist/{contract-infer-Ba1SE57Q.mjs → contract-infer-BDJgg7Xb.mjs} +3 -3
  44. package/dist/{contract-infer-Ba1SE57Q.mjs.map → contract-infer-BDJgg7Xb.mjs.map} +1 -1
  45. package/dist/exports/control-api.mjs +2 -4
  46. package/dist/exports/index.mjs +1 -6
  47. package/dist/exports/index.mjs.map +1 -1
  48. package/dist/{framework-components-BAsliT4V.mjs → framework-components-Bsr1GaIj.mjs} +2 -2
  49. package/dist/{framework-components-BAsliT4V.mjs.map → framework-components-Bsr1GaIj.mjs.map} +1 -1
  50. package/dist/{init-CYWnL7gq.mjs → init-DlFLMBaU.mjs} +2 -2
  51. package/dist/{init-CYWnL7gq.mjs.map → init-DlFLMBaU.mjs.map} +1 -1
  52. package/dist/{inspect-live-schema-gYQiWfpl.mjs → inspect-live-schema-ChqrALmw.mjs} +4 -4
  53. package/dist/{inspect-live-schema-gYQiWfpl.mjs.map → inspect-live-schema-ChqrALmw.mjs.map} +1 -1
  54. package/dist/{migration-command-scaffold-x4n_ZhAh.mjs → migration-command-scaffold-B0oH_hyB.mjs} +4 -4
  55. package/dist/{migration-command-scaffold-x4n_ZhAh.mjs.map → migration-command-scaffold-B0oH_hyB.mjs.map} +1 -1
  56. package/dist/migration-emit-Du4DBMqz.mjs +125 -0
  57. package/dist/migration-emit-Du4DBMqz.mjs.map +1 -0
  58. package/dist/{migration-status-DyVDf5NI.mjs → migration-status-CPamfEPj.mjs} +5 -5
  59. package/dist/{migration-status-DyVDf5NI.mjs.map → migration-status-CPamfEPj.mjs.map} +1 -1
  60. package/dist/{migrations-DTZBYXm1.mjs → migrations-BIsjFjSV.mjs} +6 -15
  61. package/dist/migrations-BIsjFjSV.mjs.map +1 -0
  62. package/dist/{result-handler-oK_vA-Fn.mjs → result-handler-AFK4hxyX.mjs} +2 -2
  63. package/dist/result-handler-AFK4hxyX.mjs.map +1 -0
  64. package/dist/{validate-contract-deps-esa-VQ0h.mjs → validate-contract-deps-DBH6iTAD.mjs} +1 -1
  65. package/dist/{validate-contract-deps-esa-VQ0h.mjs.map → validate-contract-deps-DBH6iTAD.mjs.map} +1 -1
  66. package/dist/{verify-DlFQ2FOw.mjs → verify-C56CuQc7.mjs} +2 -2
  67. package/dist/{verify-DlFQ2FOw.mjs.map → verify-C56CuQc7.mjs.map} +1 -1
  68. package/package.json +19 -19
  69. package/src/cli.ts +4 -4
  70. package/src/commands/migration-apply.ts +2 -2
  71. package/src/commands/migration-emit.ts +134 -0
  72. package/src/commands/migration-new.ts +50 -15
  73. package/src/commands/migration-plan.ts +138 -130
  74. package/src/commands/migration-show.ts +1 -1
  75. package/src/commands/migration-status.ts +1 -1
  76. package/src/control-api/operations/db-init.ts +3 -0
  77. package/src/control-api/operations/db-update.ts +3 -0
  78. package/src/lib/migration-emit.ts +125 -0
  79. package/src/lib/migration-strategy.ts +49 -0
  80. package/src/utils/cli-errors.ts +7 -0
  81. package/src/utils/formatters/help.ts +1 -1
  82. package/src/utils/formatters/migrations.ts +6 -20
  83. package/dist/cli-errors-BDCYR5ap.mjs +0 -4
  84. package/dist/client-tdnbk0OR.mjs.map +0 -1
  85. package/dist/commands/migration-verify.d.mts +0 -16
  86. package/dist/commands/migration-verify.d.mts.map +0 -1
  87. package/dist/commands/migration-verify.mjs +0 -110
  88. package/dist/commands/migration-verify.mjs.map +0 -1
  89. package/dist/contract-emit-CVUWsFfx.mjs +0 -6
  90. package/dist/migrations-DTZBYXm1.mjs.map +0 -1
  91. package/dist/result-handler-oK_vA-Fn.mjs.map +0 -1
  92. package/src/commands/migration-verify.ts +0 -180
@@ -0,0 +1,134 @@
1
+ import { MigrationToolsError } from '@prisma-next/migration-tools/types';
2
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
3
+ import { Command } from 'commander';
4
+ import { loadConfig } from '../config-loader';
5
+ import { emitMigration } from '../lib/migration-emit';
6
+ import {
7
+ CliStructuredError,
8
+ errorRuntime,
9
+ errorTargetMigrationNotSupported,
10
+ errorUnexpected,
11
+ } from '../utils/cli-errors';
12
+ import {
13
+ addGlobalOptions,
14
+ getTargetMigrations,
15
+ setCommandDescriptions,
16
+ setCommandExamples,
17
+ } from '../utils/command-helpers';
18
+ import { formatMigrationEmitCommandOutput } from '../utils/formatters/migrations';
19
+ import { formatStyledHeader } from '../utils/formatters/styled';
20
+ import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
21
+ import type { CommonCommandOptions } from '../utils/global-flags';
22
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
23
+ import { handleResult } from '../utils/result-handler';
24
+ import { TerminalUI } from '../utils/terminal-ui';
25
+
26
+ export interface MigrationEmitOptions extends CommonCommandOptions {
27
+ readonly dir: string;
28
+ readonly config?: string;
29
+ }
30
+
31
+ export interface MigrationEmitResult {
32
+ readonly ok: boolean;
33
+ readonly dir: string;
34
+ readonly migrationId: string;
35
+ readonly summary: string;
36
+ }
37
+
38
+ async function executeMigrationEmitCommand(
39
+ options: MigrationEmitOptions,
40
+ flags: GlobalFlags,
41
+ ui: TerminalUI,
42
+ ): Promise<Result<MigrationEmitResult, CliStructuredError>> {
43
+ const dir = options.dir;
44
+
45
+ if (!flags.json && !flags.quiet) {
46
+ const header = formatStyledHeader({
47
+ command: 'migration emit',
48
+ description: 'Emit ops.json from migration.ts and compute migrationId',
49
+ details: [{ label: 'dir', value: dir }],
50
+ flags,
51
+ });
52
+ ui.stderr(header);
53
+ }
54
+
55
+ try {
56
+ const config = await loadConfig(options.config);
57
+ const migrations = getTargetMigrations(config.target);
58
+ if (!migrations) {
59
+ throw errorTargetMigrationNotSupported({
60
+ why: `Target "${config.target.id}" does not support migrations`,
61
+ });
62
+ }
63
+ const frameworkComponents = assertFrameworkComponentsCompatible(
64
+ config.family.familyId,
65
+ config.target.targetId,
66
+ [config.target, config.adapter, ...(config.extensionPacks ?? [])],
67
+ );
68
+
69
+ const { migrationId } = await emitMigration(dir, {
70
+ targetId: config.target.targetId,
71
+ migrations,
72
+ frameworkComponents,
73
+ });
74
+
75
+ return ok({
76
+ ok: true,
77
+ dir,
78
+ migrationId,
79
+ summary: `Emitted ops.json and attested migrationId: ${migrationId}`,
80
+ });
81
+ } catch (error) {
82
+ if (CliStructuredError.is(error)) {
83
+ return notOk(error);
84
+ }
85
+ if (MigrationToolsError.is(error)) {
86
+ return notOk(
87
+ errorRuntime(error.message, {
88
+ why: error.why,
89
+ fix: error.fix,
90
+ meta: { code: error.code, ...(error.details ?? {}) },
91
+ }),
92
+ );
93
+ }
94
+ return notOk(
95
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
96
+ why: `Failed to emit migration: ${error instanceof Error ? error.message : String(error)}`,
97
+ }),
98
+ );
99
+ }
100
+ }
101
+
102
+ export function createMigrationEmitCommand(): Command {
103
+ const command = new Command('emit');
104
+ setCommandDescriptions(
105
+ command,
106
+ 'Emit ops.json from migration.ts and compute migrationId',
107
+ 'Evaluates migration.ts in the package directory, resolves it to ops.json,\n' +
108
+ 'then computes and persists the content-addressed migrationId in migration.json.\n' +
109
+ 'If the file contains unfilled placeholder() slots, emit fails with PN-MIG-2001\n' +
110
+ 'and reports the slot to fill in.',
111
+ );
112
+ setCommandExamples(command, ['prisma-next migration emit --dir migrations/20250101-add-users']);
113
+ addGlobalOptions(command)
114
+ .requiredOption('--dir <path>', 'Path to the migration package directory')
115
+ .option('--config <path>', 'Path to prisma-next.config.ts')
116
+ .action(async (options: MigrationEmitOptions) => {
117
+ const flags = parseGlobalFlags(options);
118
+ const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
119
+
120
+ const result = await executeMigrationEmitCommand(options, flags, ui);
121
+
122
+ const exitCode = handleResult(result, flags, ui, (emitResult) => {
123
+ if (flags.json) {
124
+ ui.output(JSON.stringify(emitResult, null, 2));
125
+ } else if (!flags.quiet) {
126
+ ui.log(formatMigrationEmitCommandOutput(emitResult, flags));
127
+ }
128
+ });
129
+
130
+ process.exit(exitCode);
131
+ });
132
+
133
+ return command;
134
+ }
@@ -1,33 +1,47 @@
1
1
  /**
2
- * `migration new` — scaffolds a migration package with a migration.ts file
3
- * for manual authoring. The user writes operation descriptors and data
4
- * transforms; `migration verify` resolves them to ops.json.
2
+ * `migration new` — scaffolds a migration package with a `migration.ts` file
3
+ * for manual authoring.
4
+ *
5
+ * Both descriptor-flow (Postgres) and class-flow (Mongo) targets go through
6
+ * the same path here: the planner's `emptyMigration(context)` returns a
7
+ * `MigrationPlanWithAuthoringSurface`, whose `renderTypeScript()` produces
8
+ * the target-appropriate empty stub. The CLI writes the returned source
9
+ * verbatim.
5
10
  */
6
11
 
7
12
  import { readFileSync } from 'node:fs';
8
13
  import type { Contract } from '@prisma-next/contract/types';
14
+ import { createControlStack } from '@prisma-next/framework-components/control';
9
15
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
10
16
  import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
11
17
  import {
18
+ copyContractToMigrationDir,
12
19
  formatMigrationDirName,
13
20
  readMigrationsDir,
14
21
  writeMigrationPackage,
15
22
  } from '@prisma-next/migration-tools/io';
16
- import { scaffoldMigrationTs } from '@prisma-next/migration-tools/migration-ts';
23
+ import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
17
24
  import type { MigrationManifest } from '@prisma-next/migration-tools/types';
18
25
  import { isAttested, MigrationToolsError } from '@prisma-next/migration-tools/types';
19
26
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
20
27
  import { Command } from 'commander';
21
28
  import { join, relative, resolve } from 'pathe';
22
29
  import { loadConfig } from '../config-loader';
23
- import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
30
+ import {
31
+ CliStructuredError,
32
+ errorRuntime,
33
+ errorTargetMigrationNotSupported,
34
+ errorUnexpected,
35
+ } from '../utils/cli-errors';
24
36
  import {
25
37
  addGlobalOptions,
38
+ getTargetMigrations,
26
39
  resolveMigrationPaths,
27
40
  setCommandDescriptions,
28
41
  setCommandExamples,
29
42
  } from '../utils/command-helpers';
30
43
  import { formatStyledHeader } from '../utils/formatters/styled';
44
+ import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
31
45
  import type { CommonCommandOptions } from '../utils/global-flags';
32
46
  import { parseGlobalFlags } from '../utils/global-flags';
33
47
  import { handleResult } from '../utils/result-handler';
@@ -53,7 +67,6 @@ async function executeMigrationNewCommand(
53
67
  const config = await loadConfig(options.config);
54
68
  const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
55
69
 
56
- // Read the emitted contract (destination)
57
70
  const contractPath = config.contract?.output ?? 'contract.json';
58
71
  const contractPathAbsolute = resolve(
59
72
  options.config ? resolve(options.config, '..') : process.cwd(),
@@ -101,7 +114,6 @@ async function executeMigrationNewCommand(
101
114
  );
102
115
  }
103
116
 
104
- // Determine "from" hash
105
117
  let fromContract: Contract | null = null;
106
118
  let fromHash: string = EMPTY_CONTRACT_HASH;
107
119
 
@@ -113,7 +125,6 @@ async function executeMigrationNewCommand(
113
125
  const graph = reconstructGraph(attested);
114
126
 
115
127
  if (options.from) {
116
- // Explicit --from: find the migration with matching to hash
117
128
  const match = attested.find((p) => p.manifest.to.startsWith(options.from!));
118
129
  if (!match) {
119
130
  return notOk(
@@ -151,7 +162,6 @@ async function executeMigrationNewCommand(
151
162
  throw error;
152
163
  }
153
164
 
154
- // Check for no-op
155
165
  if (fromHash === toStorageHash) {
156
166
  return notOk(
157
167
  errorRuntime('No changes detected', {
@@ -161,7 +171,6 @@ async function executeMigrationNewCommand(
161
171
  );
162
172
  }
163
173
 
164
- // Build manifest and write package
165
174
  const timestamp = new Date();
166
175
  const slug = options.name ?? 'migration';
167
176
  const dirName = formatMigrationDirName(timestamp, slug);
@@ -184,12 +193,35 @@ async function executeMigrationNewCommand(
184
193
  createdAt: timestamp.toISOString(),
185
194
  };
186
195
 
196
+ const migrations = getTargetMigrations(config.target);
197
+ if (!migrations) {
198
+ return notOk(
199
+ errorTargetMigrationNotSupported({
200
+ why: `Target "${config.target.targetId}" does not support migrations`,
201
+ }),
202
+ );
203
+ }
204
+
187
205
  try {
188
- // Write package with empty ops (draft)
206
+ assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
207
+ config.target,
208
+ config.adapter,
209
+ ...(config.extensionPacks ?? []),
210
+ ]);
211
+
189
212
  await writeMigrationPackage(packageDir, manifest, []);
213
+ await copyContractToMigrationDir(packageDir, contractPathAbsolute);
190
214
 
191
- // Scaffold migration.ts
192
- await scaffoldMigrationTs(packageDir);
215
+ const stack = createControlStack(config);
216
+ const familyInstance = config.family.create(stack);
217
+ const planner = migrations.createPlanner(familyInstance);
218
+ const emptyPlan = planner.emptyMigration({
219
+ packageDir,
220
+ contractJsonPath: join(packageDir, 'contract.json'),
221
+ fromHash,
222
+ toHash: toStorageHash,
223
+ });
224
+ await writeMigrationTs(packageDir, emptyPlan.renderTypeScript());
193
225
 
194
226
  return ok({
195
227
  ok: true as const,
@@ -199,6 +231,9 @@ async function executeMigrationNewCommand(
199
231
  summary: `Scaffolded migration at ${relative(process.cwd(), packageDir)}`,
200
232
  });
201
233
  } catch (error) {
234
+ if (CliStructuredError.is(error)) {
235
+ return notOk(error);
236
+ }
202
237
  return notOk(
203
238
  errorUnexpected(error instanceof Error ? error.message : String(error), {
204
239
  why: `Failed to scaffold migration: ${error instanceof Error ? error.message : String(error)}`,
@@ -214,7 +249,7 @@ export function createMigrationNewCommand(): Command {
214
249
  'Scaffold a new migration for manual authoring',
215
250
  'Creates a migration package with a migration.ts file for manual authoring.\n' +
216
251
  'Write operation descriptors and data transforms in migration.ts, then run\n' +
217
- '`migration verify` to resolve and attest the package.',
252
+ '`migration emit` to resolve and attest the package.',
218
253
  );
219
254
  setCommandExamples(command, [
220
255
  'prisma-next migration new --name split-name',
@@ -248,7 +283,7 @@ export function createMigrationNewCommand(): Command {
248
283
  ui.output(` from: ${value.from}`);
249
284
  ui.output(` to: ${value.to}`);
250
285
  ui.output(
251
- `\nEdit migration.ts, then run \`prisma-next migration verify --dir ${value.dir}\` to attest.`,
286
+ `\nEdit migration.ts, then run \`prisma-next migration emit --dir "${value.dir}"\` to attest.`,
252
287
  );
253
288
  }
254
289
  });
@@ -1,24 +1,22 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
- import type { OperationDescriptor } from '@prisma-next/framework-components/control';
4
- import { attestMigration } from '@prisma-next/migration-tools/attestation';
3
+ import { createControlStack } from '@prisma-next/framework-components/control';
5
4
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
6
5
  import { findLatestMigration } from '@prisma-next/migration-tools/dag';
7
6
  import {
7
+ copyContractToMigrationDir,
8
8
  formatMigrationDirName,
9
- writeMigrationOps,
10
9
  writeMigrationPackage,
11
10
  } from '@prisma-next/migration-tools/io';
12
- import {
13
- evaluateMigrationTs,
14
- scaffoldMigrationTs,
15
- } from '@prisma-next/migration-tools/migration-ts';
11
+ import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
16
12
  import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
17
13
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
18
14
  import { Command } from 'commander';
19
15
  import { join, relative } from 'pathe';
20
16
  import { loadConfig } from '../config-loader';
21
17
  import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
18
+ import { emitMigration } from '../lib/migration-emit';
19
+ import { migrationStrategy } from '../lib/migration-strategy';
22
20
  import {
23
21
  type CliErrorConflict,
24
22
  CliStructuredError,
@@ -178,7 +176,7 @@ async function executeMigrationPlanCommand(
178
176
  return notOk(
179
177
  errorRuntime('A draft migration to this contract already exists', {
180
178
  why: `Draft migration at "${existingDraft.dirName}" already targets ${toStorageHash}`,
181
- fix: `Run 'prisma-next migration verify --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
179
+ fix: `Run 'prisma-next migration emit --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
182
180
  }),
183
181
  );
184
182
  }
@@ -247,138 +245,148 @@ async function executeMigrationPlanCommand(
247
245
  [config.target, config.adapter, ...(config.extensionPacks ?? [])],
248
246
  );
249
247
 
250
- // Use descriptor-based planner if available, fall back to old planner
251
- if (migrations.planWithDescriptors) {
252
- const descriptorResult = migrations.planWithDescriptors({
253
- fromContract,
254
- toContract: toContractJson,
255
- frameworkComponents,
256
- });
248
+ const strategy = migrationStrategy(migrations, config.target.targetId);
249
+
250
+ // Build manifest and write migration package
251
+ const timestamp = new Date();
252
+ const slug = options.name ?? 'migration';
253
+ const dirName = formatMigrationDirName(timestamp, slug);
254
+ const packageDir = join(migrationsDir, dirName);
255
+
256
+ const manifest: MigrationManifest = {
257
+ from: fromHash,
258
+ to: toStorageHash,
259
+ migrationId: null,
260
+ kind: 'regular',
261
+ fromContract,
262
+ toContract: toContractJson,
263
+ hints: {
264
+ used: [],
265
+ applied: [],
266
+ plannerVersion: '2.0.0',
267
+ planningStrategy: strategy === 'descriptor' ? 'descriptors' : 'class-based',
268
+ },
269
+ labels: [],
270
+ createdAt: timestamp.toISOString(),
271
+ };
257
272
 
258
- if (!descriptorResult.ok) {
259
- return notOk(
260
- errorMigrationPlanningFailed({
261
- conflicts: descriptorResult.conflicts as readonly CliErrorConflict[],
262
- }),
263
- );
264
- }
273
+ const scaffoldContext = {
274
+ packageDir,
275
+ contractJsonPath: contractPathAbsolute,
276
+ fromHash,
277
+ toHash: toStorageHash,
278
+ };
265
279
 
266
- if (descriptorResult.descriptors.length === 0) {
267
- return notOk(
268
- errorMigrationPlanningFailed({
269
- conflicts: [
270
- {
271
- kind: 'unsupportedChange',
272
- summary:
273
- 'Contract changed but planner produced no operations. ' +
274
- 'This indicates unsupported or ignored changes.',
275
- },
276
- ],
277
- }),
280
+ try {
281
+ let migrationTsContent: string;
282
+
283
+ if (strategy === 'descriptor') {
284
+ if (!migrations.planWithDescriptors || !migrations.renderDescriptorTypeScript) {
285
+ throw errorTargetMigrationNotSupported({
286
+ why: `Target "${config.target.targetId}" advertises descriptor flow but is missing required hooks`,
287
+ });
288
+ }
289
+ const descriptorResult = migrations.planWithDescriptors({
290
+ fromContract,
291
+ toContract: toContractJson,
292
+ frameworkComponents,
293
+ });
294
+ if (!descriptorResult.ok) {
295
+ return notOk(
296
+ errorMigrationPlanningFailed({
297
+ conflicts: descriptorResult.conflicts as readonly CliErrorConflict[],
298
+ }),
299
+ );
300
+ }
301
+ if (descriptorResult.descriptors.length === 0) {
302
+ return notOk(
303
+ errorMigrationPlanningFailed({
304
+ conflicts: [
305
+ {
306
+ kind: 'unsupportedChange',
307
+ summary:
308
+ 'Contract changed but planner produced no operations. ' +
309
+ 'This indicates unsupported or ignored changes.',
310
+ },
311
+ ],
312
+ }),
313
+ );
314
+ }
315
+ migrationTsContent = migrations.renderDescriptorTypeScript(
316
+ descriptorResult.descriptors,
317
+ scaffoldContext,
278
318
  );
319
+ } else {
320
+ const stack = createControlStack(config);
321
+ const familyInstance = config.family.create(stack);
322
+ const planner = migrations.createPlanner(familyInstance);
323
+ const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
324
+ const plannerResult = planner.plan({
325
+ contract: toContractJson,
326
+ schema: fromSchema,
327
+ policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
328
+ fromHash,
329
+ frameworkComponents,
330
+ });
331
+ if (plannerResult.kind === 'failure') {
332
+ return notOk(
333
+ errorMigrationPlanningFailed({
334
+ conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
335
+ }),
336
+ );
337
+ }
338
+ if (plannerResult.plan.operations.length === 0) {
339
+ return notOk(
340
+ errorMigrationPlanningFailed({
341
+ conflicts: [
342
+ {
343
+ kind: 'unsupportedChange',
344
+ summary:
345
+ 'Contract changed but planner produced no operations. ' +
346
+ 'This indicates unsupported or ignored changes.',
347
+ },
348
+ ],
349
+ }),
350
+ );
351
+ }
352
+ migrationTsContent = plannerResult.plan.renderTypeScript();
279
353
  }
280
354
 
281
- // Build manifest and write migration package
282
- const timestamp = new Date();
283
- const slug = options.name ?? 'migration';
284
- const dirName = formatMigrationDirName(timestamp, slug);
285
- const packageDir = join(migrationsDir, dirName);
355
+ await writeMigrationPackage(packageDir, manifest, []);
356
+ await copyContractToMigrationDir(packageDir, contractPathAbsolute);
357
+ await writeMigrationTs(packageDir, migrationTsContent);
358
+
359
+ // Always run emit inline. If migration.ts contains unfilled
360
+ // placeholders (e.g. user must hand-author a dataTransform body),
361
+ // emitMigration throws errorUnfilledPlaceholder (PN-MIG-2001) and
362
+ // we propagate that structured error to the user.
363
+ const { operations, migrationId } = await emitMigration(packageDir, {
364
+ targetId: config.target.targetId,
365
+ migrations,
366
+ frameworkComponents,
367
+ });
286
368
 
287
- const manifest: MigrationManifest = {
369
+ const sql = extractSqlDdl(operations);
370
+ const result: MigrationPlanResult = {
371
+ ok: true,
372
+ noOp: false,
288
373
  from: fromHash,
289
374
  to: toStorageHash,
290
- migrationId: null,
291
- kind: 'regular',
292
- fromContract,
293
- toContract: toContractJson,
294
- hints: {
295
- used: [],
296
- applied: [],
297
- plannerVersion: '2.0.0',
298
- planningStrategy: 'descriptors',
299
- },
300
- labels: [],
301
- createdAt: timestamp.toISOString(),
375
+ migrationId,
376
+ dir: relative(process.cwd(), packageDir),
377
+ operations: operations.map((op) => ({
378
+ id: op.id,
379
+ label: op.label,
380
+ operationClass: op.operationClass,
381
+ })),
382
+ sql,
383
+ summary: `Planned ${operations.length} operation(s)`,
384
+ timings: { total: Date.now() - startTime },
302
385
  };
303
-
304
- try {
305
- // Always write migration.ts with the descriptors
306
- // Write package with empty ops first (draft)
307
- await writeMigrationPackage(packageDir, manifest, []);
308
- await scaffoldMigrationTs(packageDir, {
309
- descriptors: descriptorResult.descriptors,
310
- contractJsonPath: contractPathAbsolute,
311
- });
312
-
313
- if (descriptorResult.needsDataMigration) {
314
- // Draft — user must fill in dataTransform and run verify
315
- const result: MigrationPlanResult = {
316
- ok: true,
317
- noOp: false,
318
- from: fromHash,
319
- to: toStorageHash,
320
- dir: relative(process.cwd(), packageDir),
321
- operations: descriptorResult.descriptors.map((d) => ({
322
- id: (d as { kind: string }).kind,
323
- label: (d as { kind: string }).kind,
324
- operationClass: 'data' as const,
325
- })),
326
- sql: [],
327
- summary: `Planned ${descriptorResult.descriptors.length} operation(s) — data migration required. Edit migration.ts and run \`migration verify --dir ${relative(process.cwd(), packageDir)}\` to attest.`,
328
- timings: { total: Date.now() - startTime },
329
- };
330
- return ok(result);
331
- }
332
-
333
- // No data migration — evaluate, resolve, write ops, attest
334
- const evaluatedDescriptors = await evaluateMigrationTs(packageDir);
335
-
336
- if (!migrations.resolveDescriptors) {
337
- throw errorTargetMigrationNotSupported({
338
- why: `Target "${config.target.targetId}" does not implement resolveDescriptors; cannot finalize migration plan with migration.ts`,
339
- });
340
- }
341
-
342
- const resolvedOps = migrations.resolveDescriptors(
343
- evaluatedDescriptors as OperationDescriptor[],
344
- {
345
- fromContract,
346
- toContract: toContractJson,
347
- frameworkComponents,
348
- },
349
- );
350
-
351
- await writeMigrationOps(packageDir, resolvedOps);
352
- const migrationId = await attestMigration(packageDir);
353
-
354
- const sql = extractSqlDdl(resolvedOps);
355
- const result: MigrationPlanResult = {
356
- ok: true,
357
- noOp: false,
358
- from: fromHash,
359
- to: toStorageHash,
360
- migrationId,
361
- dir: relative(process.cwd(), packageDir),
362
- operations: resolvedOps.map((op) => ({
363
- id: op.id,
364
- label: op.label,
365
- operationClass: op.operationClass,
366
- })),
367
- sql,
368
- summary: `Planned ${resolvedOps.length} operation(s)`,
369
- timings: { total: Date.now() - startTime },
370
- };
371
- return ok(result);
372
- } catch (error) {
373
- return notOk(mapMigrationToolsError(error));
374
- }
386
+ return ok(result);
387
+ } catch (error) {
388
+ return notOk(mapMigrationToolsError(error));
375
389
  }
376
-
377
- return notOk(
378
- errorTargetMigrationNotSupported({
379
- why: `Target "${config.target.id}" does not support planWithDescriptors`,
380
- }),
381
- );
382
390
  }
383
391
 
384
392
  export function createMigrationPlanCommand(): Command {
@@ -137,7 +137,7 @@ async function executeMigrationShowCommand(
137
137
  return notOk(
138
138
  errorRuntime('No attested migrations found', {
139
139
  why: `All migrations in ${migrationsRelative} are drafts (migrationId: null)`,
140
- fix: 'Run `prisma-next migration verify --dir <path>` to attest a draft migration.',
140
+ fix: 'Run `prisma-next migration emit --dir <path>` to attest a draft migration.',
141
141
  }),
142
142
  );
143
143
  }
@@ -460,7 +460,7 @@ async function executeMigrationStatusCommand(
460
460
  severity: 'warn',
461
461
  message: `${drafts.length} draft migration(s) found: ${drafts.map((d) => d.dirName).join(', ')}`,
462
462
  hints: [
463
- "Run 'prisma-next migration verify --dir <path>' to attest draft migrations before applying",
463
+ "Run 'prisma-next migration emit --dir <path>' to attest draft migrations before applying",
464
464
  ],
465
465
  });
466
466
  }
@@ -83,6 +83,9 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
83
83
  contract,
84
84
  schema: schemaIR,
85
85
  policy,
86
+ // `db init` does not produce a `migration.ts`, so the from-hash on the
87
+ // resulting plan is never surfaced to authoring — pass empty string.
88
+ fromHash: '',
86
89
  frameworkComponents,
87
90
  });
88
91
 
@@ -83,6 +83,9 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
83
83
  contract,
84
84
  schema: schemaIR,
85
85
  policy,
86
+ // `db update` does not produce a `migration.ts`, so the from-hash on the
87
+ // resulting plan is never surfaced to authoring — pass empty string.
88
+ fromHash: '',
86
89
  frameworkComponents,
87
90
  });
88
91
  if (plannerResult.kind === 'failure') {