@prisma-next/cli 0.11.0-dev.4 → 0.11.0-dev.41

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 (143) hide show
  1. package/dist/cli-errors-DFF1LlfU.mjs +215 -0
  2. package/dist/cli-errors-DFF1LlfU.mjs.map +1 -0
  3. package/dist/cli.mjs +8 -9
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-oXO2WCPD.mjs → client-a5NJce0-.mjs} +5 -5
  6. package/dist/{client-oXO2WCPD.mjs.map → client-a5NJce0-.mjs.map} +1 -1
  7. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-BnqwTptC.mjs} +380 -6
  8. package/dist/command-helpers-BnqwTptC.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +1 -1
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +33 -7
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +3 -4
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +6 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +36 -8
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -1
  27. package/dist/commands/migrate.d.mts +5 -1
  28. package/dist/commands/migrate.d.mts.map +1 -1
  29. package/dist/commands/migrate.mjs +44 -9
  30. package/dist/commands/migrate.mjs.map +1 -1
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +2 -3
  33. package/dist/commands/migration-check.mjs.map +1 -1
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +2 -3
  36. package/dist/commands/migration-graph.mjs.map +1 -1
  37. package/dist/commands/migration-list.d.mts +57 -13
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -103
  40. package/dist/commands/migration-log.d.mts.map +1 -1
  41. package/dist/commands/migration-log.mjs +3 -4
  42. package/dist/commands/migration-log.mjs.map +1 -1
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +3 -10
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -0
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +1 -1
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +6 -7
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts.map +1 -1
  54. package/dist/commands/migration-status.mjs +6 -7
  55. package/dist/commands/migration-status.mjs.map +1 -1
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +34 -9
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  61. package/dist/config-loader.d.mts.map +1 -1
  62. package/dist/{contract-emit-bcrpT-wD.mjs → contract-emit-DYBHfZqL.mjs} +8 -7
  63. package/dist/contract-emit-DYBHfZqL.mjs.map +1 -0
  64. package/dist/{contract-emit-uwT-Mj8-.mjs → contract-emit-aFcOi3aw.mjs} +20 -14
  65. package/dist/contract-emit-aFcOi3aw.mjs.map +1 -0
  66. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-XmUPhmsS.mjs} +4 -25
  67. package/dist/contract-enrichment-XmUPhmsS.mjs.map +1 -0
  68. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-BpJeg-Eu.mjs} +3 -4
  69. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-BpJeg-Eu.mjs.map} +1 -1
  70. package/dist/{contract-space-aggregate-loader-BmNQwlws.mjs → contract-space-aggregate-loader-EVU3n9YE.mjs} +2 -2
  71. package/dist/{contract-space-aggregate-loader-BmNQwlws.mjs.map → contract-space-aggregate-loader-EVU3n9YE.mjs.map} +1 -1
  72. package/dist/{db-verify-AoIUriL4.mjs → db-verify-CxtdGiL3.mjs} +6 -7
  73. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-CxtdGiL3.mjs.map} +1 -1
  74. package/dist/exports/control-api.d.mts +1 -1
  75. package/dist/exports/control-api.d.mts.map +1 -1
  76. package/dist/exports/control-api.mjs +3 -3
  77. package/dist/exports/index.d.mts.map +1 -1
  78. package/dist/exports/index.mjs +1 -1
  79. package/dist/exports/index.mjs.map +1 -1
  80. package/dist/exports/init-output.d.mts.map +1 -1
  81. package/dist/exports/init-output.mjs +1 -1
  82. package/dist/{framework-components-65gOHkHB.mjs → framework-components-DTcjouhS.mjs} +2 -2
  83. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-DTcjouhS.mjs.map} +1 -1
  84. package/dist/global-flags-CdE7M0d9.d.mts.map +1 -1
  85. package/dist/graph-render-DJVv0_uf.mjs.map +1 -1
  86. package/dist/{init-YX6lCJpG.mjs → init-eGkSo7hi.mjs} +5 -5
  87. package/dist/{init-YX6lCJpG.mjs.map → init-eGkSo7hi.mjs.map} +1 -1
  88. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-B1GCyjAJ.mjs} +5 -5
  89. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-B1GCyjAJ.mjs.map} +1 -1
  90. package/dist/migration-cli.d.mts.map +1 -1
  91. package/dist/migration-cli.mjs +4 -4
  92. package/dist/migration-cli.mjs.map +1 -1
  93. package/dist/{migration-command-scaffold-BtkunvFQ.mjs → migration-command-scaffold-CNdZl60X.mjs} +5 -5
  94. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-CNdZl60X.mjs.map} +1 -1
  95. package/dist/migration-list-CnYiHrNV.mjs +288 -0
  96. package/dist/migration-list-CnYiHrNV.mjs.map +1 -0
  97. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-ulpJu26J.mjs} +340 -88
  98. package/dist/migration-plan-ulpJu26J.mjs.map +1 -0
  99. package/dist/{migrations-CwZMa1Ck.mjs → migrations-C7YTBnLy.mjs} +11 -2
  100. package/dist/migrations-C7YTBnLy.mjs.map +1 -0
  101. package/dist/{output-BlsrGMEF.mjs → output-CUIdfYo5.mjs} +1 -1
  102. package/dist/{output-BlsrGMEF.mjs.map → output-CUIdfYo5.mjs.map} +1 -1
  103. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-xASh41wr.mjs} +1 -1
  104. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  105. package/dist/ref-advancement-CHJ_8HxQ.mjs +50 -0
  106. package/dist/ref-advancement-CHJ_8HxQ.mjs.map +1 -0
  107. package/dist/{types--CqjMdk0.d.mts → types-UWB2-rrw.d.mts} +12 -4
  108. package/dist/types-UWB2-rrw.d.mts.map +1 -0
  109. package/dist/{verify-Bom75OYI.mjs → verify-DX4RQwq4.mjs} +2 -2
  110. package/dist/{verify-Bom75OYI.mjs.map → verify-DX4RQwq4.mjs.map} +1 -1
  111. package/package.json +20 -20
  112. package/src/commands/contract-emit.ts +19 -7
  113. package/src/commands/db-init.ts +48 -2
  114. package/src/commands/db-update.ts +45 -0
  115. package/src/commands/migrate.ts +73 -3
  116. package/src/commands/migration-list.ts +145 -74
  117. package/src/commands/migration-new.ts +0 -6
  118. package/src/commands/migration-plan.ts +359 -128
  119. package/src/commands/ref.ts +46 -6
  120. package/src/control-api/contract-enrichment.ts +6 -42
  121. package/src/control-api/operations/contract-emit.ts +7 -4
  122. package/src/control-api/types.ts +7 -0
  123. package/src/migration-cli.ts +4 -4
  124. package/src/utils/cli-errors.ts +224 -0
  125. package/src/utils/command-helpers.ts +1 -1
  126. package/src/utils/formatters/migration-list-render.ts +171 -0
  127. package/src/utils/formatters/migration-list-styler.ts +56 -0
  128. package/src/utils/formatters/migrations.ts +25 -0
  129. package/src/utils/plan-resolution.ts +257 -0
  130. package/src/utils/ref-advancement.ts +68 -0
  131. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  132. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  133. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  134. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  135. package/dist/commands/migration-list.mjs.map +0 -1
  136. package/dist/contract-emit-bcrpT-wD.mjs.map +0 -1
  137. package/dist/contract-emit-uwT-Mj8-.mjs.map +0 -1
  138. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  139. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  140. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  141. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  142. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  143. package/dist/types--CqjMdk0.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,122 @@ 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
+ providedInvariants: deriveProvidedInvariants(opsForWrite),
210
+ createdAt: createdAt.toISOString(),
211
+ };
212
+ const metadata: MigrationMetadata = {
213
+ ...metadataWithInvariants,
214
+ migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite),
215
+ };
216
+ await writeMigrationPackage(packageDir, metadata, opsForWrite);
217
+ await writeMigrationTs(packageDir, leg.migrationTsContent);
218
+ }
219
+
113
220
  export interface MigrationPlanResult {
114
221
  readonly ok: boolean;
115
222
  readonly noOp: boolean;
116
223
  readonly from: string | null;
117
224
  readonly to: string;
118
225
  readonly dir?: string;
226
+ readonly baselineDir?: string;
119
227
  /**
120
228
  * Extension-space migration packages materialised onto disk during this
121
229
  * `plan` run. Each entry names a `migrations/<spaceId>/<dirName>/`
@@ -236,62 +344,64 @@ async function executeMigrationPlanCommand(
236
344
  }
237
345
  const toStorageHash = rawStorageHash;
238
346
 
239
- // Read existing migrations and determine "from" contract
347
+ const { refsDir } = resolveMigrationPaths(options.config, config);
348
+
240
349
  let fromContract: Contract | null = null;
241
350
  let fromHash: string | null = null;
242
351
  let fromContractSourceDir: string | null = null;
352
+ let snapshotStartContract: { contractJson: unknown; contractDts: string } | null = null;
353
+ let isAutoBaseline = false;
243
354
 
244
355
  try {
245
356
  const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
246
357
 
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
- }
358
+ const resolutionResult = await resolveFromForPlan({
359
+ optionsFrom: options.from,
360
+ refsDir,
361
+ bundles,
362
+ graph,
363
+ familyInstance,
364
+ readBundleEndContract: (migrationDir) =>
365
+ readPredecessorEndContract(migrationDir, familyInstance),
366
+ });
367
+
368
+ if (!resolutionResult.ok) {
369
+ return notOk(resolutionResult.failure);
370
+ }
371
+
372
+ switch (resolutionResult.value.kind) {
373
+ case 'greenfield':
374
+ break;
375
+ case 'graph-node':
376
+ fromHash = resolutionResult.value.fromHash;
377
+ fromContract = resolutionResult.value.fromContract;
378
+ fromContractSourceDir = resolutionResult.value.sourceDir;
379
+ break;
380
+ case 'snapshot':
381
+ fromHash = resolutionResult.value.fromHash;
382
+ fromContract = resolutionResult.value.fromContract;
383
+ snapshotStartContract = {
384
+ contractJson: resolutionResult.value.contractJson,
385
+ contractDts: resolutionResult.value.contractDts,
386
+ };
387
+ break;
388
+ case 'auto-baseline':
389
+ fromHash = resolutionResult.value.fromHash;
390
+ fromContract = resolutionResult.value.fromContract;
391
+ snapshotStartContract = {
392
+ contractJson: resolutionResult.value.contractJson,
393
+ contractDts: resolutionResult.value.contractDts,
394
+ };
395
+ isAutoBaseline = true;
396
+ break;
280
397
  }
281
398
  } catch (error) {
282
399
  if (MigrationToolsError.is(error)) {
283
400
  return notOk(mapMigrationToolsError(error));
284
401
  }
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
402
  if (CliStructuredError.is(error)) {
289
403
  return notOk(error);
290
404
  }
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
405
  const message = error instanceof Error ? error.message : String(error);
296
406
  return notOk(
297
407
  errorUnexpected(message, {
@@ -325,8 +435,10 @@ async function executeMigrationPlanCommand(
325
435
  r.newMigrationDirs.map((dirName) => ({ spaceId: r.spaceId, dirName })),
326
436
  );
327
437
 
328
- // Check for no-op (same hash means no changes)
329
- if (fromHash === toStorageHash) {
438
+ // Check for no-op (same hash means no changes). Auto-baseline is exempt:
439
+ // an empty graph with db ref at the current contract still needs a
440
+ // null → fromHash baseline bundle so migrate can anchor the marker.
441
+ if (fromHash === toStorageHash && !isAutoBaseline) {
330
442
  const result: MigrationPlanResult = {
331
443
  ok: true,
332
444
  noOp: true,
@@ -375,92 +487,187 @@ async function executeMigrationPlanCommand(
375
487
  [config.target, config.adapter, ...(config.extensionPacks ?? [])],
376
488
  );
377
489
 
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
490
  try {
397
491
  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
- }),
492
+
493
+ if (
494
+ isAutoBaseline &&
495
+ fromHash !== null &&
496
+ fromContract !== null &&
497
+ snapshotStartContract !== null
498
+ ) {
499
+ const baselineTimestamp = new Date();
500
+ const deltaTimestamp = new Date(baselineTimestamp.getTime() + 60_000);
501
+ const baselineDirName = formatMigrationDirName(baselineTimestamp, 'baseline');
502
+ const deltaDirName = formatMigrationDirName(deltaTimestamp, options.name ?? 'migration');
503
+ const baselinePackageDir = join(appMigrationsDir, baselineDirName);
504
+ const deltaPackageDir = join(appMigrationsDir, deltaDirName);
505
+
506
+ const baselineLeg = await runPlannerLeg(
507
+ planner,
508
+ migrations,
509
+ frameworkComponents,
510
+ fromContract,
511
+ null,
512
+ aggregate.app.spaceId,
412
513
  );
413
- }
514
+ if (!baselineLeg.ok) {
515
+ return notOk(baselineLeg.failure);
516
+ }
517
+
518
+ await writePlannedMigrationPackage(
519
+ baselinePackageDir,
520
+ null,
521
+ fromHash,
522
+ baselineTimestamp,
523
+ baselineLeg.value,
524
+ );
525
+ await writeSnapshotContractArtifacts(
526
+ baselinePackageDir,
527
+ snapshotStartContract.contractJson,
528
+ snapshotStartContract.contractDts,
529
+ 'end-contract',
530
+ );
531
+
532
+ if (fromHash === toStorageHash) {
533
+ const baselineOps = baselineLeg.value.hasPlaceholders ? [] : baselineLeg.value.plannedOps;
534
+ if (baselineLeg.value.hasPlaceholders) {
535
+ const baselineDir = relative(process.cwd(), baselinePackageDir);
536
+ const result: MigrationPlanResult = {
537
+ ok: true,
538
+ noOp: false,
539
+ from: fromHash,
540
+ to: toStorageHash,
541
+ dir: baselineDir,
542
+ baselineDir,
543
+ operations: [],
544
+ emittedExtensionDirs,
545
+ pendingPlaceholders: true,
546
+ summary:
547
+ 'Planned baseline with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
548
+ timings: { total: Date.now() - startTime },
549
+ };
550
+ return ok(result);
551
+ }
552
+
553
+ const preview = hasOperationPreview(familyInstance)
554
+ ? familyInstance.toOperationPreview(baselineOps)
555
+ : undefined;
556
+ const result: MigrationPlanResult = {
557
+ ok: true,
558
+ noOp: false,
559
+ from: fromHash,
560
+ to: toStorageHash,
561
+ baselineDir: relative(process.cwd(), baselinePackageDir),
562
+ operations: baselineOps.map((op) => ({
563
+ id: op.id,
564
+ label: op.label,
565
+ operationClass: op.operationClass,
566
+ })),
567
+ emittedExtensionDirs,
568
+ ...(preview !== undefined ? { preview } : {}),
569
+ summary: buildAutoBaselinePlanSummary(0, emittedExtensionDirs.length),
570
+ timings: { total: Date.now() - startTime },
571
+ };
572
+ return ok(result);
573
+ }
414
574
 
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
- );
575
+ const deltaLeg = await runPlannerLeg(
576
+ planner,
577
+ migrations,
578
+ frameworkComponents,
579
+ aggregate.app.contract,
580
+ fromContract,
581
+ aggregate.app.spaceId,
582
+ );
583
+ if (!deltaLeg.ok) {
584
+ return notOk(deltaLeg.failure);
437
585
  }
438
- } catch (e) {
439
- if (CliStructuredError.is(e) && e.domain === 'MIG' && e.code === '2001') {
440
- hasPlaceholders = true;
441
- } else {
442
- throw e;
586
+
587
+ await writePlannedMigrationPackage(
588
+ deltaPackageDir,
589
+ fromHash,
590
+ toStorageHash,
591
+ deltaTimestamp,
592
+ deltaLeg.value,
593
+ );
594
+ const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
595
+ await copyFilesWithRename(deltaPackageDir, [
596
+ { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
597
+ { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
598
+ ]);
599
+ await writeSnapshotStartContract(
600
+ deltaPackageDir,
601
+ snapshotStartContract.contractJson,
602
+ snapshotStartContract.contractDts,
603
+ );
604
+
605
+ const deltaOps = deltaLeg.value.hasPlaceholders ? [] : deltaLeg.value.plannedOps;
606
+ if (deltaLeg.value.hasPlaceholders) {
607
+ const result: MigrationPlanResult = {
608
+ ok: true,
609
+ noOp: false,
610
+ from: fromHash,
611
+ to: toStorageHash,
612
+ dir: relative(process.cwd(), deltaPackageDir),
613
+ baselineDir: relative(process.cwd(), baselinePackageDir),
614
+ operations: [],
615
+ emittedExtensionDirs,
616
+ pendingPlaceholders: true,
617
+ summary:
618
+ 'Planned baseline + migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit',
619
+ timings: { total: Date.now() - startTime },
620
+ };
621
+ return ok(result);
443
622
  }
623
+
624
+ const preview = hasOperationPreview(familyInstance)
625
+ ? familyInstance.toOperationPreview(deltaOps)
626
+ : undefined;
627
+ const result: MigrationPlanResult = {
628
+ ok: true,
629
+ noOp: false,
630
+ from: fromHash,
631
+ to: toStorageHash,
632
+ dir: relative(process.cwd(), deltaPackageDir),
633
+ baselineDir: relative(process.cwd(), baselinePackageDir),
634
+ operations: deltaOps.map((op) => ({
635
+ id: op.id,
636
+ label: op.label,
637
+ operationClass: op.operationClass,
638
+ })),
639
+ emittedExtensionDirs,
640
+ ...(preview !== undefined ? { preview } : {}),
641
+ summary: buildAutoBaselinePlanSummary(deltaOps.length, emittedExtensionDirs.length),
642
+ timings: { total: Date.now() - startTime },
643
+ };
644
+ return ok(result);
444
645
  }
445
646
 
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
- };
647
+ const timestamp = new Date();
648
+ const slug = options.name ?? 'migration';
649
+ const dirName = formatMigrationDirName(timestamp, slug);
650
+ const packageDir = join(appMigrationsDir, dirName);
462
651
 
463
- await writeMigrationPackage(packageDir, metadata, opsForWrite);
652
+ const deltaLeg = await runPlannerLeg(
653
+ planner,
654
+ migrations,
655
+ frameworkComponents,
656
+ aggregate.app.contract,
657
+ fromContract,
658
+ aggregate.app.spaceId,
659
+ );
660
+ if (!deltaLeg.ok) {
661
+ return notOk(deltaLeg.failure);
662
+ }
663
+
664
+ await writePlannedMigrationPackage(
665
+ packageDir,
666
+ fromHash,
667
+ toStorageHash,
668
+ timestamp,
669
+ deltaLeg.value,
670
+ );
464
671
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
465
672
  await copyFilesWithRename(packageDir, [
466
673
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
@@ -474,10 +681,15 @@ async function executeMigrationPlanCommand(
474
681
  { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
475
682
  { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
476
683
  ]);
684
+ } else if (snapshotStartContract !== null) {
685
+ await writeSnapshotStartContract(
686
+ packageDir,
687
+ snapshotStartContract.contractJson,
688
+ snapshotStartContract.contractDts,
689
+ );
477
690
  }
478
- await writeMigrationTs(packageDir, migrationTsContent);
479
691
 
480
- if (hasPlaceholders) {
692
+ if (deltaLeg.value.hasPlaceholders) {
481
693
  const result: MigrationPlanResult = {
482
694
  ok: true,
483
695
  noOp: false,
@@ -494,6 +706,7 @@ async function executeMigrationPlanCommand(
494
706
  return ok(result);
495
707
  }
496
708
 
709
+ const plannedOps = deltaLeg.value.plannedOps;
497
710
  const preview = hasOperationPreview(familyInstance)
498
711
  ? familyInstance.toOperationPreview(plannedOps)
499
712
  : undefined;
@@ -593,6 +806,17 @@ function buildPlanSummary(plannedOpsCount: number, emittedExtensionDirsCount: nu
593
806
  return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
594
807
  }
595
808
 
809
+ function buildAutoBaselinePlanSummary(
810
+ deltaOpsCount: number,
811
+ emittedExtensionDirsCount: number,
812
+ ): string {
813
+ const base = `Planned baseline + ${deltaOpsCount} operation(s)`;
814
+ if (emittedExtensionDirsCount === 0) return base;
815
+ const noun =
816
+ emittedExtensionDirsCount === 1 ? 'extension-space migration' : 'extension-space migrations';
817
+ return `${base}; materialised ${emittedExtensionDirsCount} ${noun}`;
818
+ }
819
+
596
820
  export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
597
821
  const lines: string[] = [];
598
822
  const useColor = flags.color !== false;
@@ -672,6 +896,9 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
672
896
 
673
897
  lines.push(dim_(`from: ${result.from}`));
674
898
  lines.push(dim_(`to: ${result.to}`));
899
+ if (result.baselineDir) {
900
+ lines.push(dim_(`Baseline → ${result.baselineDir}`));
901
+ }
675
902
  if (result.dir) {
676
903
  lines.push(dim_(`App space → ${result.dir}`));
677
904
  }
@@ -689,8 +916,12 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
689
916
  // (`prisma-next migrate`) regardless of how many spaces were
690
917
  // materialised — `db update` is a dev-time convenience, not the
691
918
  // canonical replay step.
919
+ const reviewTarget =
920
+ result.baselineDir !== undefined && result.dir !== undefined
921
+ ? `${result.baselineDir} and ${result.dir}`
922
+ : (result.baselineDir ?? result.dir ?? '<dir>');
692
923
  lines.push(
693
- `Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migrate')}.`,
924
+ `Next: review ${green_(reviewTarget)} if needed, then run ${green_('prisma-next migrate')}.`,
694
925
  );
695
926
 
696
927
  if (result.preview && result.preview.statements.length > 0) {