@prisma-next/cli 0.5.0-dev.66 → 0.5.0-dev.68

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 (138) hide show
  1. package/dist/{cli-errors-By1iVE3z.mjs → cli-errors-D3_sMh2K.mjs} +2 -3
  2. package/dist/{cli-errors-By1iVE3z.mjs.map → cli-errors-D3_sMh2K.mjs.map} +1 -1
  3. package/dist/{cli-errors-DDeVsP2Y.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -2
  4. package/dist/cli.mjs +12 -76
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/client-0ZX24FXF.mjs +1398 -0
  7. package/dist/client-0ZX24FXF.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +2 -4
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +2 -4
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +11 -11
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +5 -7
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +8 -9
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +11 -11
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -321
  26. package/dist/commands/migration-apply.d.mts.map +1 -1
  27. package/dist/commands/migration-apply.mjs +16 -17
  28. package/dist/commands/migration-apply.mjs.map +1 -1
  29. package/dist/commands/migration-new.d.mts +0 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +10 -11
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -345
  35. package/dist/commands/migration-ref.d.mts +1 -1
  36. package/dist/commands/migration-ref.d.mts.map +1 -1
  37. package/dist/commands/migration-ref.mjs +5 -6
  38. package/dist/commands/migration-ref.mjs.map +1 -1
  39. package/dist/commands/migration-show.d.mts +1 -1
  40. package/dist/commands/migration-show.d.mts.map +1 -1
  41. package/dist/commands/migration-show.mjs +13 -13
  42. package/dist/commands/migration-show.mjs.map +1 -1
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +2 -4
  45. package/dist/{config-loader-ih8ViDb_.mjs → config-loader-B6sJjXTv.mjs} +2 -4
  46. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  47. package/dist/config-loader.d.mts +0 -1
  48. package/dist/config-loader.d.mts.map +1 -1
  49. package/dist/config-loader.mjs +2 -3
  50. package/dist/{contract-emit-CnTXVVbF.mjs → contract-emit-B3ChISB_.mjs} +22 -13
  51. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  52. package/dist/{contract-emit-CcZr3HS9.mjs → contract-emit-DkMqO7f2.mjs} +8 -10
  53. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  54. package/dist/{contract-enrichment-xDeJBC-o.mjs → contract-enrichment-CF6ogEJ_.mjs} +2 -2
  55. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  56. package/dist/{contract-infer-sER84Le-.mjs → contract-infer-BDKAE0B0.mjs} +5 -7
  57. package/dist/{contract-infer-sER84Le-.mjs.map → contract-infer-BDKAE0B0.mjs.map} +1 -1
  58. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  59. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  60. package/dist/exports/config-types.mjs +1 -2
  61. package/dist/exports/control-api.d.mts +202 -7
  62. package/dist/exports/control-api.d.mts.map +1 -1
  63. package/dist/exports/control-api.mjs +4 -6
  64. package/dist/exports/index.d.mts.map +1 -1
  65. package/dist/exports/index.mjs +28 -30
  66. package/dist/exports/index.mjs.map +1 -1
  67. package/dist/exports/init-output.d.mts +2 -4
  68. package/dist/exports/init-output.d.mts.map +1 -1
  69. package/dist/exports/init-output.mjs +2 -3
  70. package/dist/{framework-components-Bgcre3Z6.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  71. package/dist/{framework-components-Bgcre3Z6.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  72. package/dist/{init-DC4sL4Rp.mjs → init-Deo7U8_U.mjs} +13 -30
  73. package/dist/init-Deo7U8_U.mjs.map +1 -0
  74. package/dist/{inspect-live-schema-BQN21nNO.mjs → inspect-live-schema-BAgQMYpD.mjs} +7 -8
  75. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  76. package/dist/migration-cli.d.mts +0 -1
  77. package/dist/migration-cli.d.mts.map +1 -1
  78. package/dist/migration-cli.mjs +2 -3
  79. package/dist/migration-cli.mjs.map +1 -1
  80. package/dist/{migration-command-scaffold-DLmYGRug.mjs → migration-command-scaffold-B8J702Uh.mjs} +7 -8
  81. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  82. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  83. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  84. package/dist/{migration-status-CDW4RDsO.mjs → migration-status-CjwB2of-.mjs} +10 -14
  85. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  86. package/dist/{migrations-MEoKMiV5.mjs → migrations-CIK94AJf.mjs} +3 -4
  87. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  88. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  89. package/dist/output-DnjfCC_u.mjs.map +1 -0
  90. package/dist/{progress-adapter-DgRGldpT.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  91. package/dist/{progress-adapter-DgRGldpT.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  92. package/dist/{result-handler-Ch6hVnOo.mjs → result-handler-DWb1rFS-.mjs} +20 -10
  93. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  94. package/dist/{terminal-ui-u2YgKghu.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  95. package/dist/{terminal-ui-u2YgKghu.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  96. package/dist/{verify-BT9tgCOH.mjs → verify-BEIa9638.mjs} +3 -4
  97. package/dist/verify-BEIa9638.mjs.map +1 -0
  98. package/package.json +24 -24
  99. package/src/commands/db-init.ts +13 -3
  100. package/src/commands/db-update.ts +7 -3
  101. package/src/commands/db-verify.ts +47 -15
  102. package/src/commands/init/index.ts +1 -1
  103. package/src/commands/init/init.ts +2 -2
  104. package/src/commands/migration-apply.ts +9 -9
  105. package/src/commands/migration-new.ts +4 -4
  106. package/src/commands/migration-plan.ts +66 -9
  107. package/src/commands/migration-show.ts +7 -5
  108. package/src/commands/migration-status.ts +3 -3
  109. package/src/control-api/client.ts +42 -0
  110. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  111. package/src/control-api/operations/db-init.ts +51 -258
  112. package/src/control-api/operations/db-update.ts +66 -188
  113. package/src/control-api/operations/db-verify.ts +342 -0
  114. package/src/control-api/types.ts +56 -0
  115. package/src/exports/control-api.ts +13 -2
  116. package/src/load-ts-contract.ts +28 -26
  117. package/src/utils/combine-schema-results.ts +84 -0
  118. package/src/utils/command-helpers.ts +24 -2
  119. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  120. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  121. package/src/utils/contract-space-migrate-pass.ts +156 -0
  122. package/dist/client-hUCMXFE_.mjs +0 -1031
  123. package/dist/client-hUCMXFE_.mjs.map +0 -1
  124. package/dist/commands/db-verify.mjs.map +0 -1
  125. package/dist/commands/migration-plan.mjs.map +0 -1
  126. package/dist/config-loader-ih8ViDb_.mjs.map +0 -1
  127. package/dist/contract-emit-BkRH9lGt.mjs +0 -4
  128. package/dist/contract-emit-CcZr3HS9.mjs.map +0 -1
  129. package/dist/contract-emit-CnTXVVbF.mjs.map +0 -1
  130. package/dist/contract-enrichment-xDeJBC-o.mjs.map +0 -1
  131. package/dist/init-DC4sL4Rp.mjs.map +0 -1
  132. package/dist/inspect-live-schema-BQN21nNO.mjs.map +0 -1
  133. package/dist/migration-command-scaffold-DLmYGRug.mjs.map +0 -1
  134. package/dist/migration-status-CDW4RDsO.mjs.map +0 -1
  135. package/dist/migrations-MEoKMiV5.mjs.map +0 -1
  136. package/dist/output-BpcQrnnq.mjs.map +0 -1
  137. package/dist/result-handler-Ch6hVnOo.mjs.map +0 -1
  138. package/dist/verify-BT9tgCOH.mjs.map +0 -1
@@ -27,10 +27,12 @@ import {
27
27
  errorTargetMismatch,
28
28
  errorUnexpected,
29
29
  } from '../utils/cli-errors';
30
+ import { combineSchemaResults } from '../utils/combine-schema-results';
30
31
  import {
31
32
  addGlobalOptions,
32
33
  maskConnectionUrl,
33
34
  resolveContractPath,
35
+ resolveMigrationPaths,
34
36
  setCommandDescriptions,
35
37
  setCommandExamples,
36
38
  } from '../utils/command-helpers';
@@ -346,22 +348,42 @@ async function executeDbVerifyCommand(
346
348
  const setupResult = await resolveVerifySetup(paths, options, mode);
347
349
  if (!setupResult.ok) return setupResult;
348
350
  const { contractJson, dbConnection, contractPathAbsolute } = setupResult.value;
351
+ const { migrationsDir } = resolveMigrationPaths(options.config, setupResult.value.config);
349
352
 
350
353
  const client = createVerifyClient(setupResult.value);
351
354
  const onProgress = createProgressAdapter({ ui, flags });
352
355
 
353
356
  try {
357
+ // Single-contract marker verification preserved for the existing
358
+ // marker / target / hash failure surface (`PN-RUN-3001/3002/3003`).
359
+ // The aggregate verifier (run below for the per-space marker /
360
+ // schema checks) does not duplicate this: it concerns itself with
361
+ // marker-vs-on-disk and orphan-marker drift, not the
362
+ // hash-mismatch-against-the-app-contract lane that today's
363
+ // `client.verify` covers.
354
364
  const verifyResult = await client.verify({
355
365
  contract: contractJson,
356
366
  connection: dbConnection,
357
367
  onProgress,
358
368
  });
359
369
 
360
- // If verification failed, map to CLI structured error
361
370
  if (!verifyResult.ok) {
362
371
  return notOk(mapVerifyFailure(verifyResult));
363
372
  }
364
373
 
374
+ // Aggregate verifier (loader → verifier pipeline). Runs the layout
375
+ // precheck, marker-aware per-space verifier, and (full mode only)
376
+ // per-space pre-projected schema verification (closes F23).
377
+ const aggregateResult = await client.dbVerify({
378
+ contract: contractJson,
379
+ migrationsDir,
380
+ strict: options.strict ?? false,
381
+ skipSchema: mode === 'marker-only',
382
+ skipMarker: false,
383
+ onProgress,
384
+ });
385
+ if (!aggregateResult.ok) return notOk(aggregateResult.failure);
386
+
365
387
  if (mode === 'marker-only') {
366
388
  return ok({
367
389
  ok: true,
@@ -381,14 +403,13 @@ async function executeDbVerifyCommand(
381
403
  });
382
404
  }
383
405
 
384
- const schemaVerifyResult = await client.schemaVerify({
385
- contract: contractJson,
386
- strict: options.strict ?? false,
387
- onProgress,
388
- });
389
-
390
- if (!schemaVerifyResult.ok) {
391
- return notOk(schemaVerifyResult);
406
+ const combined = combineSchemaResults(
407
+ aggregateResult.value.schemaResults,
408
+ aggregateResult.value.appSpaceId,
409
+ options.strict ?? false,
410
+ );
411
+ if (!combined.ok) {
412
+ return notOk(combined);
392
413
  }
393
414
 
394
415
  return ok({
@@ -401,9 +422,9 @@ async function executeDbVerifyCommand(
401
422
  ...ifDefined('missingCodecs', verifyResult.missingCodecs),
402
423
  ...ifDefined('codecCoverageSkipped', verifyResult.codecCoverageSkipped),
403
424
  schema: {
404
- summary: schemaVerifyResult.summary,
405
- counts: schemaVerifyResult.schema.counts,
406
- strict: schemaVerifyResult.meta?.strict ?? false,
425
+ summary: combined.summary,
426
+ counts: combined.schema.counts,
427
+ strict: combined.meta?.strict ?? false,
407
428
  },
408
429
  meta: {
409
430
  ...(verifyResult.meta ?? {}),
@@ -429,19 +450,30 @@ async function executeDbSchemaOnlyVerifyCommand(
429
450
  const setupResult = await resolveVerifySetup(paths, options, 'schema-only');
430
451
  if (!setupResult.ok) return setupResult;
431
452
  const { contractJson, dbConnection, contractPathAbsolute } = setupResult.value;
453
+ const { migrationsDir } = resolveMigrationPaths(options.config, setupResult.value.config);
432
454
 
433
455
  const client = createVerifyClient(setupResult.value);
434
456
  const onProgress = createProgressAdapter({ ui, flags });
435
457
 
436
458
  try {
437
- const schemaVerifyResult = await client.schemaVerify({
459
+ await client.connect(dbConnection);
460
+ const aggregateResult = await client.dbVerify({
438
461
  contract: contractJson,
462
+ migrationsDir,
439
463
  strict: options.strict ?? false,
440
- connection: dbConnection,
464
+ skipSchema: false,
465
+ skipMarker: true,
441
466
  onProgress,
442
467
  });
468
+ if (!aggregateResult.ok) return notOk(aggregateResult.failure);
443
469
 
444
- return ok(schemaVerifyResult);
470
+ return ok(
471
+ combineSchemaResults(
472
+ aggregateResult.value.schemaResults,
473
+ aggregateResult.value.appSpaceId,
474
+ options.strict ?? false,
475
+ ),
476
+ );
445
477
  } catch (error) {
446
478
  return wrapVerifyError(error, contractPathAbsolute, 'db verify --schema-only');
447
479
  } finally {
@@ -113,7 +113,7 @@ export function createInitCommand(): Command {
113
113
  * we honour it (e.g. testing flows where stdin is stubbed).
114
114
  *
115
115
  * Exported so callers and tests can derive the same value without
116
- * touching `process` globals — F14 of the M1/M2 review.
116
+ * touching `process` globals.
117
117
  */
118
118
  export function deriveCanPrompt(opts: {
119
119
  readonly flagsInteractive: boolean | undefined;
@@ -569,7 +569,7 @@ async function runInstall(ctx: {
569
569
  /**
570
570
  * FR2.1 — set when the user already declares `@types/node` directly in
571
571
  * `dependencies` or `devDependencies`. We then skip adding it so a
572
- * pinned major (e.g. `^18` for a Node 18 runtime) survives `init`
572
+ * locked major (e.g. `^18` for a Node 18 runtime) survives `init`
573
573
  * unchanged. Transitive presence is intentionally ignored: detecting
574
574
  * it requires lockfile introspection and the realistic clobber risk
575
575
  * is the direct-pin case.
@@ -585,7 +585,7 @@ async function runInstall(ctx: {
585
585
  // Pin it as a devDep rather than relying on a transitive resolution
586
586
  // through `dotenv` (whose types bundle is internal and not guaranteed
587
587
  // across versions). Skip when the user already declares `@types/node`
588
- // directly so a pinned major (e.g. `^18` for a Node 18 runtime) is
588
+ // directly so a locked major (e.g. `^18` for a Node 18 runtime) is
589
589
  // preserved. Listed last so the install log still leads with the
590
590
  // user-relevant `prisma-next` line.
591
591
  const devDeps = hasTypesNode ? ['prisma-next'] : ['prisma-next', '@types/node'];
@@ -110,7 +110,7 @@ async function executeMigrationApplyCommand(
110
110
  startTime: number,
111
111
  ): Promise<Result<MigrationApplyResult, CliStructuredErrorType>> {
112
112
  const config = await loadConfig(options.config);
113
- const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
113
+ const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } = resolveMigrationPaths(
114
114
  options.config,
115
115
  config,
116
116
  );
@@ -173,7 +173,7 @@ async function executeMigrationApplyCommand(
173
173
  if (!flags.json && !flags.quiet) {
174
174
  const details: Array<{ label: string; value: string }> = [
175
175
  { label: 'config', value: configPath },
176
- { label: 'migrations', value: migrationsRelative },
176
+ { label: 'migrations', value: appMigrationsRelative },
177
177
  ];
178
178
  if (typeof dbConnection === 'string') {
179
179
  details.push({
@@ -197,7 +197,7 @@ async function executeMigrationApplyCommand(
197
197
  // Read migrations and build migration chain model (offline — no DB needed)
198
198
  let migrations: Awaited<ReturnType<typeof loadMigrationPackages>>;
199
199
  try {
200
- migrations = await loadMigrationPackages(migrationsDir);
200
+ migrations = await loadMigrationPackages(appMigrationsDir);
201
201
  } catch (error) {
202
202
  if (MigrationToolsError.is(error)) {
203
203
  return notOk(mapMigrationToolsError(error));
@@ -248,9 +248,9 @@ async function executeMigrationApplyCommand(
248
248
  if (marker?.storageHash) {
249
249
  return notOk(
250
250
  errorRuntime('Database has state but no migrations exist', {
251
- why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${migrationsRelative}`,
251
+ why: `The database marker hash "${marker.storageHash}" exists but no migrations were found in ${appMigrationsRelative}`,
252
252
  fix: 'Ensure the migrations directory is correct. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
253
- meta: { markerHash: marker.storageHash, migrationsDir: migrationsRelative },
253
+ meta: { markerHash: marker.storageHash, migrationsDir: appMigrationsRelative },
254
254
  }),
255
255
  );
256
256
  }
@@ -258,9 +258,9 @@ async function executeMigrationApplyCommand(
258
258
  if (destinationHash !== EMPTY_CONTRACT_HASH) {
259
259
  return notOk(
260
260
  errorRuntime('Current contract has no planned migrations', {
261
- why: `No migrations were found in ${migrationsRelative}, but current contract hash is "${destinationHash}"`,
261
+ why: `No migrations were found in ${appMigrationsRelative}, but current contract hash is "${destinationHash}"`,
262
262
  fix: 'Run `prisma-next migration plan` to create a migration for the current contract.',
263
- meta: { destinationHash, migrationsDir: migrationsRelative },
263
+ meta: { destinationHash, migrationsDir: appMigrationsRelative },
264
264
  }),
265
265
  );
266
266
  }
@@ -295,7 +295,7 @@ async function executeMigrationApplyCommand(
295
295
  if (markerHash !== undefined && !migrations.graph.nodes.has(markerHash)) {
296
296
  return notOk(
297
297
  errorRuntime('Database marker does not match any known migration', {
298
- why: `The database marker hash "${markerHash}" is not found in the migration history at ${migrationsRelative}`,
298
+ why: `The database marker hash "${markerHash}" is not found in the migration history at ${appMigrationsRelative}`,
299
299
  fix: 'Ensure the migrations directory matches this database. If the database was managed with `db init` or `db update`, run `prisma-next db sign` to update the marker.',
300
300
  meta: { markerHash, knownNodes: [...migrations.graph.nodes] },
301
301
  }),
@@ -305,7 +305,7 @@ async function executeMigrationApplyCommand(
305
305
  if (!migrations.graph.nodes.has(destinationHash)) {
306
306
  return notOk(
307
307
  errorRuntime('Current contract has no planned migration path', {
308
- why: `Current contract hash "${destinationHash}" is not present in the migration history at ${migrationsRelative}`,
308
+ why: `Current contract hash "${destinationHash}" is not present in the migration history at ${appMigrationsRelative}`,
309
309
  fix: 'Run `prisma-next migration plan` to create a migration for the current contract, then re-run apply.',
310
310
  meta: { destinationHash, knownNodes: [...migrations.graph.nodes] },
311
311
  }),
@@ -70,7 +70,7 @@ async function executeMigrationNewCommand(
70
70
  options: MigrationNewOptions,
71
71
  ): Promise<Result<MigrationNewResult, CliStructuredError>> {
72
72
  const config = await loadConfig(options.config);
73
- const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
73
+ const { appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
74
74
 
75
75
  const contractPathAbsolute = resolveContractPath(config);
76
76
 
@@ -120,7 +120,7 @@ async function executeMigrationNewCommand(
120
120
  let fromContractSourceDir: string | null = null;
121
121
 
122
122
  try {
123
- const packages = await readMigrationsDir(migrationsDir);
123
+ const packages = await readMigrationsDir(appMigrationsDir);
124
124
 
125
125
  if (packages.length > 0) {
126
126
  const graph = reconstructGraph(packages);
@@ -130,7 +130,7 @@ async function executeMigrationNewCommand(
130
130
  if (!match) {
131
131
  return notOk(
132
132
  errorRuntime('Starting contract not found', {
133
- why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
133
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
134
134
  fix: 'Check that the --from hash matches a known migration target hash.',
135
135
  }),
136
136
  );
@@ -171,7 +171,7 @@ async function executeMigrationNewCommand(
171
171
  const timestamp = new Date();
172
172
  const slug = options.name ?? 'migration';
173
173
  const dirName = formatMigrationDirName(timestamp, slug);
174
- const packageDir = join(migrationsDir, dirName);
174
+ const packageDir = join(appMigrationsDir, dirName);
175
175
 
176
176
  // `migration new` scaffolds an empty `migration.ts` for the user to
177
177
  // fill, so we attest over `ops: []`. Re-running self-emit after the
@@ -43,6 +43,15 @@ import {
43
43
  setCommandDescriptions,
44
44
  setCommandExamples,
45
45
  } from '../utils/command-helpers';
46
+ import {
47
+ type ExtensionMigrationsExtensionInput,
48
+ runContractSpaceExtensionMigrationsPass,
49
+ } from '../utils/contract-space-extension-migrations-pass';
50
+ import {
51
+ formatContractSpaceDriftWarning,
52
+ type MigrateExtensionInput,
53
+ runContractSpaceMigratePass,
54
+ } from '../utils/contract-space-migrate-pass';
46
55
  import { formatStyledHeader } from '../utils/formatters/styled';
47
56
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
48
57
  import type { CommonCommandOptions } from '../utils/global-flags';
@@ -92,10 +101,8 @@ async function executeMigrationPlanCommand(
92
101
  startTime: number,
93
102
  ): Promise<Result<MigrationPlanResult, CliStructuredError>> {
94
103
  const config = await loadConfig(options.config);
95
- const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
96
- options.config,
97
- config,
98
- );
104
+ const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
105
+ resolveMigrationPaths(options.config, config);
99
106
 
100
107
  const contractPathAbsolute = resolveContractPath(config);
101
108
  const contractPath = relative(process.cwd(), contractPathAbsolute);
@@ -104,7 +111,7 @@ async function executeMigrationPlanCommand(
104
111
  const details: Array<{ label: string; value: string }> = [
105
112
  { label: 'config', value: configPath },
106
113
  { label: 'contract', value: contractPath },
107
- { label: 'migrations', value: migrationsRelative },
114
+ { label: 'migrations', value: appMigrationsRelative },
108
115
  ];
109
116
  if (options.from) {
110
117
  details.push({ label: 'from', value: options.from });
@@ -170,7 +177,7 @@ async function executeMigrationPlanCommand(
170
177
  let fromContractSourceDir: string | null = null;
171
178
 
172
179
  try {
173
- const { bundles, graph } = await loadMigrationPackages(migrationsDir);
180
+ const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
174
181
 
175
182
  if (options.from) {
176
183
  const resolved = resolveBundleByPrefix(bundles, options.from);
@@ -179,11 +186,11 @@ async function executeMigrationPlanCommand(
179
186
  return notOk(
180
187
  f.reason === 'ambiguous'
181
188
  ? errorRuntime('Multiple matching migrations found', {
182
- why: `Prefix "${options.from}" matches ${f.count} migrations in ${migrationsRelative}`,
189
+ why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
183
190
  fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
184
191
  })
185
192
  : errorRuntime('Starting contract not found', {
186
- why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
193
+ why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
187
194
  fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
188
195
  }),
189
196
  );
@@ -220,6 +227,56 @@ async function executeMigrationPlanCommand(
220
227
  );
221
228
  }
222
229
 
230
+ // Per-space migrate pass: drift detection + on-disk artefact emission for
231
+ // every loaded extension that exposes a `contractSpace`. Runs *before*
232
+ // the app-space no-op check so that an extension bump alone (with no
233
+ // structural app-space change) still re-pins extension artefacts on
234
+ // disk. Drift warnings are non-fatal — the on-disk artefacts are refreshed
235
+ // and the user is notified that the bump is being captured.
236
+ const extensionInputs: readonly MigrateExtensionInput[] = (config.extensionPacks ?? []).map(
237
+ (pack) => {
238
+ const cs = (pack as { readonly contractSpace?: MigrateExtensionInput['contractSpace'] })
239
+ .contractSpace;
240
+ return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
241
+ },
242
+ );
243
+ const migratePass = await runContractSpaceMigratePass({
244
+ migrationsDir,
245
+ extensionPacks: extensionInputs,
246
+ });
247
+ if (!flags.json && !flags.quiet) {
248
+ for (const drift of migratePass.drifts) {
249
+ if (drift.kind === 'drift') {
250
+ ui.stderr(formatContractSpaceDriftWarning(drift));
251
+ }
252
+ }
253
+ }
254
+
255
+ // Materialise descriptor-shipped migration packages onto disk under
256
+ // `migrations/<spaceId>/<dirName>/` for any package not yet present.
257
+ // Idempotent (existing dirs are left untouched).
258
+ // Uses `planAllSpaces` for deterministic ordering + duplicate-spaceId
259
+ // detection.
260
+ const extensionMigrationsInputs: readonly ExtensionMigrationsExtensionInput[] = (
261
+ config.extensionPacks ?? []
262
+ ).map((pack) => {
263
+ const cs = (
264
+ pack as {
265
+ readonly contractSpace?: ExtensionMigrationsExtensionInput['contractSpace'];
266
+ }
267
+ ).contractSpace;
268
+ return cs !== undefined ? { id: pack.id, contractSpace: cs } : { id: pack.id };
269
+ });
270
+ const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
271
+ migrationsDir,
272
+ extensionPacks: extensionMigrationsInputs,
273
+ });
274
+ if (!flags.json && !flags.quiet) {
275
+ for (const entry of extensionMigrationsResult.emitted) {
276
+ ui.step(`Emitted ${entry.spaceId}/${entry.dirName}`);
277
+ }
278
+ }
279
+
223
280
  // Check for no-op (same hash means no changes)
224
281
  if (fromHash === toStorageHash) {
225
282
  const result: MigrationPlanResult = {
@@ -253,7 +310,7 @@ async function executeMigrationPlanCommand(
253
310
  const timestamp = new Date();
254
311
  const slug = options.name ?? 'migration';
255
312
  const dirName = formatMigrationDirName(timestamp, slug);
256
- const packageDir = join(migrationsDir, dirName);
313
+ const packageDir = join(appMigrationsDir, dirName);
257
314
 
258
315
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
259
316
  from: fromHash,
@@ -9,6 +9,7 @@ import {
9
9
  reconstructGraph,
10
10
  } from '@prisma-next/migration-tools/migration-graph';
11
11
  import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
12
+ import { APP_SPACE_ID, spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
12
13
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
13
14
  import { Command } from 'commander';
14
15
  import { relative, resolve } from 'pathe';
@@ -103,16 +104,17 @@ async function executeMigrationShowCommand(
103
104
  ? relative(process.cwd(), resolve(options.config))
104
105
  : 'prisma-next.config.ts';
105
106
 
106
- const migrationsDir = resolve(
107
+ const migrationsDirRoot = resolve(
107
108
  options.config ? resolve(options.config, '..') : process.cwd(),
108
109
  config.migrations?.dir ?? 'migrations',
109
110
  );
110
- const migrationsRelative = relative(process.cwd(), migrationsDir);
111
+ const appMigrationsDir = spaceMigrationDirectory(migrationsDirRoot, APP_SPACE_ID);
112
+ const appMigrationsRelative = relative(process.cwd(), appMigrationsDir);
111
113
 
112
114
  if (!flags.json && !flags.quiet) {
113
115
  const details: Array<{ label: string; value: string }> = [
114
116
  { label: 'config', value: configPath },
115
- { label: 'migrations', value: migrationsRelative },
117
+ { label: 'migrations', value: appMigrationsRelative },
116
118
  ];
117
119
  if (target) {
118
120
  details.push({ label: 'target', value: target });
@@ -132,11 +134,11 @@ async function executeMigrationShowCommand(
132
134
  if (target && looksLikePath(target)) {
133
135
  pkg = await readMigrationPackage(resolve(target));
134
136
  } else {
135
- const allPackages = await readMigrationsDir(migrationsDir);
137
+ const allPackages = await readMigrationsDir(appMigrationsDir);
136
138
  if (allPackages.length === 0) {
137
139
  return notOk(
138
140
  errorRuntime('No migrations found', {
139
- why: `No migration packages found in ${migrationsRelative}`,
141
+ why: `No migration packages found in ${appMigrationsRelative}`,
140
142
  fix: 'Run `prisma-next migration plan` to create a migration first.',
141
143
  }),
142
144
  );
@@ -369,7 +369,7 @@ async function executeMigrationStatusCommand(
369
369
  ui: TerminalUI,
370
370
  ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
371
371
  const config = await loadConfig(options.config);
372
- const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
372
+ const { configPath, appMigrationsDir, appMigrationsRelative, refsDir } = resolveMigrationPaths(
373
373
  options.config,
374
374
  config,
375
375
  );
@@ -414,7 +414,7 @@ async function executeMigrationStatusCommand(
414
414
  if (!flags.json && !flags.quiet) {
415
415
  const details: Array<{ label: string; value: string }> = [
416
416
  { label: 'config', value: configPath },
417
- { label: 'migrations', value: migrationsRelative },
417
+ { label: 'migrations', value: appMigrationsRelative },
418
418
  ];
419
419
  if (dbConnection && hasDriver) {
420
420
  details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
@@ -454,7 +454,7 @@ async function executeMigrationStatusCommand(
454
454
  let bundles: readonly OnDiskMigrationPackage[];
455
455
  let graph: MigrationGraph;
456
456
  try {
457
- ({ bundles, graph } = await loadMigrationPackages(migrationsDir));
457
+ ({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
458
458
  } catch (error) {
459
459
  if (MigrationToolsError.is(error)) {
460
460
  return notOk(mapMigrationToolsError(error));
@@ -28,7 +28,9 @@ import { enrichContract } from './contract-enrichment';
28
28
  import { ContractValidationError } from './errors';
29
29
  import { executeDbInit } from './operations/db-init';
30
30
  import { executeDbUpdate } from './operations/db-update';
31
+ import { type ExecuteDbVerifyResult, executeDbVerify } from './operations/db-verify';
31
32
  import { executeMigrationApply } from './operations/migration-apply';
33
+
32
34
  import type {
33
35
  ControlActionName,
34
36
  ControlClient,
@@ -37,6 +39,7 @@ import type {
37
39
  DbInitResult,
38
40
  DbUpdateOptions,
39
41
  DbUpdateResult,
42
+ DbVerifyOptions,
40
43
  EmitOptions,
41
44
  EmitResult,
42
45
  IntrospectOptions,
@@ -368,6 +371,9 @@ class ControlClientImpl implements ControlClient {
368
371
  mode: options.mode,
369
372
  migrations: this.options.target.migrations,
370
373
  frameworkComponents,
374
+ migrationsDir: options.migrationsDir,
375
+ targetId: this.options.target.targetId,
376
+ extensionPacks: this.options.extensionPacks ?? [],
371
377
  ...ifDefined('onProgress', onProgress),
372
378
  });
373
379
  }
@@ -396,11 +402,42 @@ class ControlClientImpl implements ControlClient {
396
402
  mode: options.mode,
397
403
  migrations: this.options.target.migrations,
398
404
  frameworkComponents,
405
+ migrationsDir: options.migrationsDir,
406
+ targetId: this.options.target.targetId,
407
+ extensionPacks: this.options.extensionPacks ?? [],
399
408
  ...ifDefined('acceptDataLoss', options.acceptDataLoss),
400
409
  ...ifDefined('onProgress', onProgress),
401
410
  });
402
411
  }
403
412
 
413
+ async dbVerify(options: DbVerifyOptions): Promise<ExecuteDbVerifyResult> {
414
+ const { onProgress } = options;
415
+ await this.connectWithProgress(options.connection, 'dbVerify', onProgress);
416
+ const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
417
+
418
+ let contract: Contract;
419
+ try {
420
+ contract = familyInstance.validateContract(options.contract);
421
+ } catch (error) {
422
+ const message = error instanceof Error ? error.message : String(error);
423
+ throw new ContractValidationError(message, error);
424
+ }
425
+
426
+ return executeDbVerify({
427
+ driver,
428
+ familyInstance,
429
+ contract,
430
+ migrationsDir: options.migrationsDir,
431
+ targetId: this.options.target.targetId,
432
+ extensionPacks: this.options.extensionPacks ?? [],
433
+ frameworkComponents,
434
+ mode: options.strict ? 'strict' : 'lenient',
435
+ skipSchema: options.skipSchema,
436
+ skipMarker: options.skipMarker,
437
+ ...ifDefined('onProgress', onProgress),
438
+ });
439
+ }
440
+
404
441
  async readMarker(): Promise<ContractMarkerRecord | null> {
405
442
  const { driver, familyInstance } = await this.ensureConnected();
406
443
  // The CLI client's readMarker reads the app's marker. Per-extension
@@ -410,6 +447,11 @@ class ControlClientImpl implements ControlClient {
410
447
  return familyInstance.readMarker({ driver, space: APP_SPACE_ID });
411
448
  }
412
449
 
450
+ async readAllMarkers(): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
451
+ const { driver, familyInstance } = await this.ensureConnected();
452
+ return familyInstance.readAllMarkers({ driver });
453
+ }
454
+
413
455
  async migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult> {
414
456
  const { onProgress } = options;
415
457
  await this.connectWithProgress(options.connection, 'migrationApply', onProgress);