@prisma-next/cli 0.11.0-dev.7 → 0.11.0-dev.9

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 (89) hide show
  1. package/dist/cli-errors-Bw2GlweY.mjs +175 -0
  2. package/dist/cli-errors-Bw2GlweY.mjs.map +1 -0
  3. package/dist/cli.mjs +7 -7
  4. package/dist/{client-oXO2WCPD.mjs → client-UnIveZxZ.mjs} +4 -4
  5. package/dist/{client-oXO2WCPD.mjs.map → client-UnIveZxZ.mjs.map} +1 -1
  6. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-CRfjbZRz.mjs} +2 -2
  7. package/dist/{command-helpers-DtavI0wJ.mjs.map → command-helpers-CRfjbZRz.mjs.map} +1 -1
  8. package/dist/commands/contract-emit.mjs +1 -1
  9. package/dist/commands/contract-infer.mjs +1 -1
  10. package/dist/commands/db-init.d.mts.map +1 -1
  11. package/dist/commands/db-init.mjs +33 -6
  12. package/dist/commands/db-init.mjs.map +1 -1
  13. package/dist/commands/db-schema.mjs +3 -3
  14. package/dist/commands/db-sign.mjs +5 -5
  15. package/dist/commands/db-update.d.mts.map +1 -1
  16. package/dist/commands/db-update.mjs +36 -7
  17. package/dist/commands/db-update.mjs.map +1 -1
  18. package/dist/commands/db-verify.mjs +1 -1
  19. package/dist/commands/migrate.d.mts +5 -1
  20. package/dist/commands/migrate.d.mts.map +1 -1
  21. package/dist/commands/migrate.mjs +44 -8
  22. package/dist/commands/migrate.mjs.map +1 -1
  23. package/dist/commands/migration-check.mjs +2 -2
  24. package/dist/commands/migration-graph.mjs +3 -3
  25. package/dist/commands/migration-list.mjs +2 -2
  26. package/dist/commands/migration-log.mjs +3 -3
  27. package/dist/commands/migration-new.mjs +3 -3
  28. package/dist/commands/migration-plan.d.mts +1 -0
  29. package/dist/commands/migration-plan.d.mts.map +1 -1
  30. package/dist/commands/migration-plan.mjs +1 -1
  31. package/dist/commands/migration-show.d.mts +1 -1
  32. package/dist/commands/migration-show.mjs +6 -6
  33. package/dist/commands/migration-status.mjs +7 -7
  34. package/dist/commands/ref.d.mts +1 -1
  35. package/dist/commands/ref.d.mts.map +1 -1
  36. package/dist/commands/ref.mjs +34 -8
  37. package/dist/commands/ref.mjs.map +1 -1
  38. package/dist/{contract-emit-o-8VmdQX.mjs → contract-emit-C6rlsljO.mjs} +3 -3
  39. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-C6rlsljO.mjs.map} +1 -1
  40. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-mqXmapxB.mjs} +4 -4
  41. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-mqXmapxB.mjs.map} +1 -1
  42. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-C4jxc1aZ.mjs} +3 -3
  43. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-C4jxc1aZ.mjs.map} +1 -1
  44. package/dist/{contract-space-aggregate-loader-BmNQwlws.mjs → contract-space-aggregate-loader-CGakRlKM.mjs} +2 -2
  45. package/dist/{contract-space-aggregate-loader-BmNQwlws.mjs.map → contract-space-aggregate-loader-CGakRlKM.mjs.map} +1 -1
  46. package/dist/{db-verify-AoIUriL4.mjs → db-verify-1d8tDoFN.mjs} +5 -5
  47. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-1d8tDoFN.mjs.map} +1 -1
  48. package/dist/exports/control-api.d.mts +1 -1
  49. package/dist/exports/control-api.mjs +2 -2
  50. package/dist/exports/index.mjs +1 -1
  51. package/dist/exports/init-output.mjs +1 -1
  52. package/dist/{framework-components-65gOHkHB.mjs → framework-components-Bexd0f4E.mjs} +2 -2
  53. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-Bexd0f4E.mjs.map} +1 -1
  54. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-BE8vmJ_7.mjs} +1 -1
  55. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-BE8vmJ_7.mjs.map} +1 -1
  56. package/dist/{init-Db5Itt5r.mjs → init-ByoeQphC.mjs} +4 -4
  57. package/dist/{init-Db5Itt5r.mjs.map → init-ByoeQphC.mjs.map} +1 -1
  58. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-B1Q49RF0.mjs} +4 -4
  59. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-B1Q49RF0.mjs.map} +1 -1
  60. package/dist/{migration-command-scaffold-BtkunvFQ.mjs → migration-command-scaffold-oY4P1Qto.mjs} +4 -4
  61. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-oY4P1Qto.mjs.map} +1 -1
  62. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-jdAHg_gK.mjs} +346 -87
  63. package/dist/migration-plan-jdAHg_gK.mjs.map +1 -0
  64. package/dist/{migrations-CwZMa1Ck.mjs → migrations-B7n518mT.mjs} +10 -1
  65. package/dist/migrations-B7n518mT.mjs.map +1 -0
  66. package/dist/{output-BlsrGMEF.mjs → output-CUIdfYo5.mjs} +1 -1
  67. package/dist/{output-BlsrGMEF.mjs.map → output-CUIdfYo5.mjs.map} +1 -1
  68. package/dist/ref-advancement-DRh5Nquq.mjs +50 -0
  69. package/dist/ref-advancement-DRh5Nquq.mjs.map +1 -0
  70. package/dist/{types-C9FfXb1l.d.mts → types-UWB2-rrw.d.mts} +5 -4
  71. package/dist/types-UWB2-rrw.d.mts.map +1 -0
  72. package/dist/{verify-Bom75OYI.mjs → verify-C5UvbrF1.mjs} +1 -1
  73. package/dist/{verify-Bom75OYI.mjs.map → verify-C5UvbrF1.mjs.map} +1 -1
  74. package/package.json +18 -18
  75. package/src/commands/db-init.ts +48 -2
  76. package/src/commands/db-update.ts +45 -0
  77. package/src/commands/migrate.ts +73 -3
  78. package/src/commands/migration-plan.ts +365 -128
  79. package/src/commands/ref.ts +46 -6
  80. package/src/utils/cli-errors.ts +173 -0
  81. package/src/utils/formatters/migrations.ts +25 -0
  82. package/src/utils/plan-resolution.ts +257 -0
  83. package/src/utils/ref-advancement.ts +68 -0
  84. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  85. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  86. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  87. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  88. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  89. package/dist/types-C9FfXb1l.d.mts.map +0 -1
@@ -1,4 +1,4 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import {
@@ -8,6 +8,7 @@ import {
8
8
  type MigrationPlanOperation,
9
9
  type OperationPreview,
10
10
  } from '@prisma-next/framework-components/control';
11
+ import { canonicalizeJson } from '@prisma-next/framework-components/utils';
11
12
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
12
13
  import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
13
14
  import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
@@ -17,10 +18,7 @@ import {
17
18
  writeMigrationPackage,
18
19
  } from '@prisma-next/migration-tools/io';
19
20
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
20
- import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
21
21
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
22
- import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
23
- import { readRefs } from '@prisma-next/migration-tools/refs';
24
22
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
25
23
  import { Command } from 'commander';
26
24
  import { join, relative } from 'pathe';
@@ -34,7 +32,6 @@ import {
34
32
  errorTargetMigrationNotSupported,
35
33
  errorUnexpected,
36
34
  mapMigrationToolsError,
37
- mapRefResolutionError,
38
35
  } from '../utils/cli-errors';
39
36
  import {
40
37
  addGlobalOptions,
@@ -52,6 +49,7 @@ import { formatStyledHeader } from '../utils/formatters/styled';
52
49
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
53
50
  import type { CommonCommandOptions } from '../utils/global-flags';
54
51
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
52
+ import { resolveFromForPlan } from '../utils/plan-resolution';
55
53
  import { handleResult } from '../utils/result-handler';
56
54
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
57
55
 
@@ -110,12 +108,128 @@ async function readPredecessorEndContract(
110
108
  }
111
109
  }
112
110
 
111
+ async function writeSnapshotContractArtifacts(
112
+ packageDir: string,
113
+ contractJson: unknown,
114
+ contractDts: string,
115
+ artifactBasename: 'start-contract' | 'end-contract',
116
+ ): Promise<void> {
117
+ await mkdir(packageDir, { recursive: true });
118
+ const jsonContent = `${canonicalizeJson(contractJson)}\n`;
119
+ const dtsContent = contractDts.endsWith('\n') ? contractDts : `${contractDts}\n`;
120
+ await writeFile(join(packageDir, `${artifactBasename}.json`), jsonContent);
121
+ await writeFile(join(packageDir, `${artifactBasename}.d.ts`), dtsContent);
122
+ }
123
+
124
+ async function writeSnapshotStartContract(
125
+ packageDir: string,
126
+ contractJson: unknown,
127
+ contractDts: string,
128
+ ): Promise<void> {
129
+ await writeSnapshotContractArtifacts(packageDir, contractJson, contractDts, 'start-contract');
130
+ }
131
+
132
+ type PlannerSuccess = {
133
+ readonly plannedOps: readonly MigrationPlanOperation[];
134
+ readonly migrationTsContent: string;
135
+ readonly hasPlaceholders: boolean;
136
+ };
137
+
138
+ type TargetMigrationsApi = NonNullable<ReturnType<typeof getTargetMigrations>>;
139
+
140
+ async function runPlannerLeg(
141
+ planner: ReturnType<TargetMigrationsApi['createPlanner']>,
142
+ migrations: TargetMigrationsApi,
143
+ frameworkComponents: ReturnType<typeof assertFrameworkComponentsCompatible>,
144
+ contract: Contract,
145
+ fromContract: Contract | null,
146
+ spaceId: string,
147
+ ): Promise<Result<PlannerSuccess, CliStructuredError>> {
148
+ const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
149
+ const plannerResult = planner.plan({
150
+ contract,
151
+ schema: fromSchema,
152
+ policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
153
+ fromContract,
154
+ frameworkComponents,
155
+ spaceId,
156
+ });
157
+ if (plannerResult.kind === 'failure') {
158
+ return notOk(
159
+ errorMigrationPlanningFailed({
160
+ conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
161
+ }),
162
+ );
163
+ }
164
+
165
+ let plannedOps: readonly MigrationPlanOperation[] = [];
166
+ let hasPlaceholders = false;
167
+ try {
168
+ plannedOps = plannerResult.plan.operations;
169
+ if (plannedOps.length === 0) {
170
+ return notOk(
171
+ errorMigrationPlanningFailed({
172
+ conflicts: [
173
+ {
174
+ kind: 'unsupportedChange',
175
+ summary:
176
+ 'Contract changed but planner produced no operations. ' +
177
+ 'This indicates unsupported or ignored changes.',
178
+ },
179
+ ],
180
+ }),
181
+ );
182
+ }
183
+ } catch (e) {
184
+ if (CliStructuredError.is(e) && e.domain === 'MIG' && e.code === '2001') {
185
+ hasPlaceholders = true;
186
+ } else {
187
+ throw e;
188
+ }
189
+ }
190
+
191
+ return ok({
192
+ plannedOps,
193
+ migrationTsContent: plannerResult.plan.renderTypeScript(),
194
+ hasPlaceholders,
195
+ });
196
+ }
197
+
198
+ async function writePlannedMigrationPackage(
199
+ packageDir: string,
200
+ fromHash: string | null,
201
+ toHash: string,
202
+ createdAt: Date,
203
+ leg: PlannerSuccess,
204
+ ): Promise<void> {
205
+ const opsForWrite = leg.hasPlaceholders ? [] : leg.plannedOps;
206
+ const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
207
+ from: fromHash,
208
+ to: toHash,
209
+ hints: {
210
+ used: [],
211
+ applied: [],
212
+ plannerVersion: '2.0.0',
213
+ },
214
+ labels: [],
215
+ providedInvariants: deriveProvidedInvariants(opsForWrite),
216
+ createdAt: createdAt.toISOString(),
217
+ };
218
+ const metadata: MigrationMetadata = {
219
+ ...metadataWithInvariants,
220
+ migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
221
+ };
222
+ await writeMigrationPackage(packageDir, metadata, opsForWrite);
223
+ await writeMigrationTs(packageDir, leg.migrationTsContent);
224
+ }
225
+
113
226
  export interface MigrationPlanResult {
114
227
  readonly ok: boolean;
115
228
  readonly noOp: boolean;
116
229
  readonly from: string | null;
117
230
  readonly to: string;
118
231
  readonly dir?: string;
232
+ readonly baselineDir?: string;
119
233
  /**
120
234
  * Extension-space migration packages materialised onto disk during this
121
235
  * `plan` run. Each entry names a `migrations/<spaceId>/<dirName>/`
@@ -236,62 +350,64 @@ async function executeMigrationPlanCommand(
236
350
  }
237
351
  const toStorageHash = rawStorageHash;
238
352
 
239
- // Read existing migrations and determine "from" contract
353
+ const { refsDir } = resolveMigrationPaths(options.config, config);
354
+
240
355
  let fromContract: Contract | null = null;
241
356
  let fromHash: string | null = null;
242
357
  let fromContractSourceDir: string | null = null;
358
+ let snapshotStartContract: { contractJson: unknown; contractDts: string } | null = null;
359
+ let isAutoBaseline = false;
243
360
 
244
361
  try {
245
362
  const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
246
363
 
247
- if (options.from) {
248
- const refs = await readRefs(resolveMigrationPaths(options.config, config).refsDir);
249
- const refResult = parseContractRef(options.from, { graph, refs });
250
- if (!refResult.ok) {
251
- return notOk(mapRefResolutionError(refResult.failure));
252
- }
253
- fromHash = refResult.value.hash;
254
- const matchingBundle = bundles.find((p) => p.metadata.to === fromHash);
255
- if (!matchingBundle) {
256
- return notOk(
257
- errorUnexpected(
258
- `No migration bundle found for --from "${options.from}" (resolved hash: ${fromHash})`,
259
- {
260
- why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${fromHash}.`,
261
- fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
262
- },
263
- ),
264
- );
265
- }
266
- fromContractSourceDir = matchingBundle.dirPath;
267
- fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
268
- } else {
269
- const latestMigration = findLatestMigration(graph);
270
- if (latestMigration) {
271
- fromHash = latestMigration.to;
272
- const leafPkg = bundles.find(
273
- (p) => p.metadata.migrationHash === latestMigration.migrationHash,
274
- );
275
- if (leafPkg) {
276
- fromContractSourceDir = leafPkg.dirPath;
277
- fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
278
- }
279
- }
364
+ const resolutionResult = await resolveFromForPlan({
365
+ optionsFrom: options.from,
366
+ refsDir,
367
+ bundles,
368
+ graph,
369
+ familyInstance,
370
+ readBundleEndContract: (migrationDir) =>
371
+ readPredecessorEndContract(migrationDir, familyInstance),
372
+ });
373
+
374
+ if (!resolutionResult.ok) {
375
+ return notOk(resolutionResult.failure);
376
+ }
377
+
378
+ switch (resolutionResult.value.kind) {
379
+ case 'greenfield':
380
+ break;
381
+ case 'graph-node':
382
+ fromHash = resolutionResult.value.fromHash;
383
+ fromContract = resolutionResult.value.fromContract;
384
+ fromContractSourceDir = resolutionResult.value.sourceDir;
385
+ break;
386
+ case 'snapshot':
387
+ fromHash = resolutionResult.value.fromHash;
388
+ fromContract = resolutionResult.value.fromContract;
389
+ snapshotStartContract = {
390
+ contractJson: resolutionResult.value.contractJson,
391
+ contractDts: resolutionResult.value.contractDts,
392
+ };
393
+ break;
394
+ case 'auto-baseline':
395
+ fromHash = resolutionResult.value.fromHash;
396
+ fromContract = resolutionResult.value.fromContract;
397
+ snapshotStartContract = {
398
+ contractJson: resolutionResult.value.contractJson,
399
+ contractDts: resolutionResult.value.contractDts,
400
+ };
401
+ isAutoBaseline = true;
402
+ break;
280
403
  }
281
404
  } catch (error) {
282
405
  if (MigrationToolsError.is(error)) {
283
406
  return notOk(mapMigrationToolsError(error));
284
407
  }
285
- // `readPredecessorEndContract` raises a `CliStructuredError` directly
286
- // for the missing-snapshot case so the operator gets a precise
287
- // why/fix; pass it through unchanged rather than re-wrapping.
288
408
  if (CliStructuredError.is(error)) {
289
409
  return notOk(error);
290
410
  }
291
- // Wrap unexpected (non-MigrationToolsError) failures from the migration
292
- // load phase in a structured CLI envelope. Letting them throw would
293
- // bypass `handleResult()` and crash the command — see CLI structured-
294
- // errors guideline (CliStructuredError + Result pattern).
295
411
  const message = error instanceof Error ? error.message : String(error);
296
412
  return notOk(
297
413
  errorUnexpected(message, {
@@ -325,8 +441,10 @@ async function executeMigrationPlanCommand(
325
441
  r.newMigrationDirs.map((dirName) => ({ spaceId: r.spaceId, dirName })),
326
442
  );
327
443
 
328
- // Check for no-op (same hash means no changes)
329
- if (fromHash === toStorageHash) {
444
+ // Check for no-op (same hash means no changes). Auto-baseline is exempt:
445
+ // an empty graph with db ref at the current contract still needs a
446
+ // null → fromHash baseline bundle so migrate can anchor the marker.
447
+ if (fromHash === toStorageHash && !isAutoBaseline) {
330
448
  const result: MigrationPlanResult = {
331
449
  ok: true,
332
450
  noOp: true,
@@ -375,92 +493,187 @@ async function executeMigrationPlanCommand(
375
493
  [config.target, config.adapter, ...(config.extensionPacks ?? [])],
376
494
  );
377
495
 
378
- // Build manifest and write migration package
379
- const timestamp = new Date();
380
- const slug = options.name ?? 'migration';
381
- const dirName = formatMigrationDirName(timestamp, slug);
382
- const packageDir = join(appMigrationsDir, dirName);
383
-
384
- const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
385
- from: fromHash,
386
- to: toStorageHash,
387
- hints: {
388
- used: [],
389
- applied: [],
390
- plannerVersion: '2.0.0',
391
- },
392
- labels: [],
393
- createdAt: timestamp.toISOString(),
394
- };
395
-
396
496
  try {
397
497
  const planner = migrations.createPlanner(familyInstance);
398
- const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
399
- const plannerResult = planner.plan({
400
- contract: aggregate.app.contract,
401
- schema: fromSchema,
402
- policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
403
- fromContract,
404
- frameworkComponents,
405
- spaceId: aggregate.app.spaceId,
406
- });
407
- if (plannerResult.kind === 'failure') {
408
- return notOk(
409
- errorMigrationPlanningFailed({
410
- conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
411
- }),
498
+
499
+ if (
500
+ isAutoBaseline &&
501
+ fromHash !== null &&
502
+ fromContract !== null &&
503
+ snapshotStartContract !== null
504
+ ) {
505
+ const baselineTimestamp = new Date();
506
+ const deltaTimestamp = new Date(baselineTimestamp.getTime() + 60_000);
507
+ const baselineDirName = formatMigrationDirName(baselineTimestamp, 'baseline');
508
+ const deltaDirName = formatMigrationDirName(deltaTimestamp, options.name ?? 'migration');
509
+ const baselinePackageDir = join(appMigrationsDir, baselineDirName);
510
+ const deltaPackageDir = join(appMigrationsDir, deltaDirName);
511
+
512
+ const baselineLeg = await runPlannerLeg(
513
+ planner,
514
+ migrations,
515
+ frameworkComponents,
516
+ fromContract,
517
+ null,
518
+ aggregate.app.spaceId,
412
519
  );
413
- }
520
+ if (!baselineLeg.ok) {
521
+ return notOk(baselineLeg.failure);
522
+ }
414
523
 
415
- // Accessing .operations triggers toOp() on each call. If any call
416
- // is a DataTransformCall with an unfilled placeholder stub, toOp()
417
- // throws PN-MIG-2001. We catch that here so the migration can still
418
- // be scaffolded with `ops: []`; the user fills the placeholder, then
419
- // re-runs `node migration.ts` to attest with the real ops.
420
- let plannedOps: readonly MigrationPlanOperation[] = [];
421
- let hasPlaceholders = false;
422
- try {
423
- plannedOps = plannerResult.plan.operations;
424
- if (plannedOps.length === 0) {
425
- return notOk(
426
- errorMigrationPlanningFailed({
427
- conflicts: [
428
- {
429
- kind: 'unsupportedChange',
430
- summary:
431
- 'Contract changed but planner produced no operations. ' +
432
- 'This indicates unsupported or ignored changes.',
433
- },
434
- ],
435
- }),
436
- );
524
+ await writePlannedMigrationPackage(
525
+ baselinePackageDir,
526
+ null,
527
+ fromHash,
528
+ baselineTimestamp,
529
+ baselineLeg.value,
530
+ );
531
+ await writeSnapshotContractArtifacts(
532
+ baselinePackageDir,
533
+ snapshotStartContract.contractJson,
534
+ snapshotStartContract.contractDts,
535
+ 'end-contract',
536
+ );
537
+
538
+ if (fromHash === toStorageHash) {
539
+ const baselineOps = baselineLeg.value.hasPlaceholders ? [] : baselineLeg.value.plannedOps;
540
+ if (baselineLeg.value.hasPlaceholders) {
541
+ const baselineDir = relative(process.cwd(), baselinePackageDir);
542
+ const result: MigrationPlanResult = {
543
+ ok: true,
544
+ noOp: false,
545
+ from: fromHash,
546
+ to: toStorageHash,
547
+ dir: baselineDir,
548
+ baselineDir,
549
+ operations: [],
550
+ emittedExtensionDirs,
551
+ pendingPlaceholders: true,
552
+ summary:
553
+ 'Planned baseline with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
554
+ timings: { total: Date.now() - startTime },
555
+ };
556
+ return ok(result);
557
+ }
558
+
559
+ const preview = hasOperationPreview(familyInstance)
560
+ ? familyInstance.toOperationPreview(baselineOps)
561
+ : undefined;
562
+ const result: MigrationPlanResult = {
563
+ ok: true,
564
+ noOp: false,
565
+ from: fromHash,
566
+ to: toStorageHash,
567
+ baselineDir: relative(process.cwd(), baselinePackageDir),
568
+ operations: baselineOps.map((op) => ({
569
+ id: op.id,
570
+ label: op.label,
571
+ operationClass: op.operationClass,
572
+ })),
573
+ emittedExtensionDirs,
574
+ ...(preview !== undefined ? { preview } : {}),
575
+ summary: buildAutoBaselinePlanSummary(0, emittedExtensionDirs.length),
576
+ timings: { total: Date.now() - startTime },
577
+ };
578
+ return ok(result);
437
579
  }
438
- } catch (e) {
439
- if (CliStructuredError.is(e) && e.domain === 'MIG' && e.code === '2001') {
440
- hasPlaceholders = true;
441
- } else {
442
- throw e;
580
+
581
+ const deltaLeg = await runPlannerLeg(
582
+ planner,
583
+ migrations,
584
+ frameworkComponents,
585
+ aggregate.app.contract,
586
+ fromContract,
587
+ aggregate.app.spaceId,
588
+ );
589
+ if (!deltaLeg.ok) {
590
+ return notOk(deltaLeg.failure);
591
+ }
592
+
593
+ await writePlannedMigrationPackage(
594
+ deltaPackageDir,
595
+ fromHash,
596
+ toStorageHash,
597
+ deltaTimestamp,
598
+ deltaLeg.value,
599
+ );
600
+ const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
601
+ await copyFilesWithRename(deltaPackageDir, [
602
+ { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
603
+ { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
604
+ ]);
605
+ await writeSnapshotStartContract(
606
+ deltaPackageDir,
607
+ snapshotStartContract.contractJson,
608
+ snapshotStartContract.contractDts,
609
+ );
610
+
611
+ const deltaOps = deltaLeg.value.hasPlaceholders ? [] : deltaLeg.value.plannedOps;
612
+ if (deltaLeg.value.hasPlaceholders) {
613
+ const result: MigrationPlanResult = {
614
+ ok: true,
615
+ noOp: false,
616
+ from: fromHash,
617
+ to: toStorageHash,
618
+ dir: relative(process.cwd(), deltaPackageDir),
619
+ baselineDir: relative(process.cwd(), baselinePackageDir),
620
+ operations: [],
621
+ emittedExtensionDirs,
622
+ pendingPlaceholders: true,
623
+ summary:
624
+ 'Planned baseline + migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
625
+ timings: { total: Date.now() - startTime },
626
+ };
627
+ return ok(result);
443
628
  }
629
+
630
+ const preview = hasOperationPreview(familyInstance)
631
+ ? familyInstance.toOperationPreview(deltaOps)
632
+ : undefined;
633
+ const result: MigrationPlanResult = {
634
+ ok: true,
635
+ noOp: false,
636
+ from: fromHash,
637
+ to: toStorageHash,
638
+ dir: relative(process.cwd(), deltaPackageDir),
639
+ baselineDir: relative(process.cwd(), baselinePackageDir),
640
+ operations: deltaOps.map((op) => ({
641
+ id: op.id,
642
+ label: op.label,
643
+ operationClass: op.operationClass,
644
+ })),
645
+ emittedExtensionDirs,
646
+ ...(preview !== undefined ? { preview } : {}),
647
+ summary: buildAutoBaselinePlanSummary(deltaOps.length, emittedExtensionDirs.length),
648
+ timings: { total: Date.now() - startTime },
649
+ };
650
+ return ok(result);
444
651
  }
445
652
 
446
- const migrationTsContent = plannerResult.plan.renderTypeScript();
447
-
448
- // Always-attest: compute migrationHash over (metadata, ops). When
449
- // placeholders blocked lowering, ops is `[]` and the hash is computed
450
- // over the empty list — re-emitting after the user fills the placeholder
451
- // produces a different hash (over the real ops). This is intentional;
452
- // there is no on-disk "draft" state.
453
- const opsForWrite = hasPlaceholders ? [] : plannedOps;
454
- const metadataWithInvariants: Omit<MigrationMetadata, 'migrationHash'> = {
455
- ...baseMetadata,
456
- providedInvariants: deriveProvidedInvariants(opsForWrite),
457
- };
458
- const metadata: MigrationMetadata = {
459
- ...metadataWithInvariants,
460
- migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
461
- };
653
+ const timestamp = new Date();
654
+ const slug = options.name ?? 'migration';
655
+ const dirName = formatMigrationDirName(timestamp, slug);
656
+ const packageDir = join(appMigrationsDir, dirName);
657
+
658
+ const deltaLeg = await runPlannerLeg(
659
+ planner,
660
+ migrations,
661
+ frameworkComponents,
662
+ aggregate.app.contract,
663
+ fromContract,
664
+ aggregate.app.spaceId,
665
+ );
666
+ if (!deltaLeg.ok) {
667
+ return notOk(deltaLeg.failure);
668
+ }
462
669
 
463
- await writeMigrationPackage(packageDir, metadata, opsForWrite);
670
+ await writePlannedMigrationPackage(
671
+ packageDir,
672
+ fromHash,
673
+ toStorageHash,
674
+ timestamp,
675
+ deltaLeg.value,
676
+ );
464
677
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
465
678
  await copyFilesWithRename(packageDir, [
466
679
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -474,10 +687,15 @@ async function executeMigrationPlanCommand(
474
687
  { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
475
688
  { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
476
689
  ]);
690
+ } else if (snapshotStartContract !== null) {
691
+ await writeSnapshotStartContract(
692
+ packageDir,
693
+ snapshotStartContract.contractJson,
694
+ snapshotStartContract.contractDts,
695
+ );
477
696
  }
478
- await writeMigrationTs(packageDir, migrationTsContent);
479
697
 
480
- if (hasPlaceholders) {
698
+ if (deltaLeg.value.hasPlaceholders) {
481
699
  const result: MigrationPlanResult = {
482
700
  ok: true,
483
701
  noOp: false,
@@ -494,6 +712,7 @@ async function executeMigrationPlanCommand(
494
712
  return ok(result);
495
713
  }
496
714
 
715
+ const plannedOps = deltaLeg.value.plannedOps;
497
716
  const preview = hasOperationPreview(familyInstance)
498
717
  ? familyInstance.toOperationPreview(plannedOps)
499
718
  : undefined;
@@ -593,6 +812,17 @@ function buildPlanSummary(plannedOpsCount: number, emittedExtensionDirsCount: nu
593
812
  return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
594
813
  }
595
814
 
815
+ function buildAutoBaselinePlanSummary(
816
+ deltaOpsCount: number,
817
+ emittedExtensionDirsCount: number,
818
+ ): string {
819
+ const base = `Planned baseline + ${deltaOpsCount} operation(s)`;
820
+ if (emittedExtensionDirsCount === 0) return base;
821
+ const noun =
822
+ emittedExtensionDirsCount === 1 ? 'extension-space migration' : 'extension-space migrations';
823
+ return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
824
+ }
825
+
596
826
  export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
597
827
  const lines: string[] = [];
598
828
  const useColor = flags.color !== false;
@@ -672,6 +902,9 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
672
902
 
673
903
  lines.push(dim_(`from: ${result.from}`));
674
904
  lines.push(dim_(`to: ${result.to}`));
905
+ if (result.baselineDir) {
906
+ lines.push(dim_(`Baseline → ${result.baselineDir}`));
907
+ }
675
908
  if (result.dir) {
676
909
  lines.push(dim_(`App space → ${result.dir}`));
677
910
  }
@@ -689,8 +922,12 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
689
922
  // (`prisma-next migrate`) regardless of how many spaces were
690
923
  // materialised — `db update` is a dev-time convenience, not the
691
924
  // canonical replay step.
925
+ const reviewTarget =
926
+ result.baselineDir !== undefined && result.dir !== undefined
927
+ ? `${result.baselineDir} and ${result.dir}`
928
+ : (result.baselineDir ?? result.dir ?? '<dir>');
692
929
  lines.push(
693
- `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migrate')}.`,
930
+ `Next: review ${green_(reviewTarget)} if needed, then run ${green_('prisma-next migrate')}.`,
694
931
  );
695
932
 
696
933
  if (result.preview && result.preview.statements.length > 0) {