@prisma-next/cli 0.12.0-dev.4 → 0.12.0-dev.40

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 (165) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-BNdG504y.mjs} +80 -56
  5. package/dist/client-BNdG504y.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-xvg9oq4T.mjs} +301 -23
  7. package/dist/command-helpers-xvg9oq4T.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +1 -1
  9. package/dist/commands/contract-infer.mjs +1 -1
  10. package/dist/commands/db-init.mjs +4 -5
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -3
  13. package/dist/commands/db-sign.mjs +4 -4
  14. package/dist/commands/db-update.d.mts.map +1 -1
  15. package/dist/commands/db-update.mjs +10 -7
  16. package/dist/commands/db-update.mjs.map +1 -1
  17. package/dist/commands/db-verify.mjs +1 -1
  18. package/dist/commands/migrate.d.mts +2 -2
  19. package/dist/commands/migrate.d.mts.map +1 -1
  20. package/dist/commands/migrate.mjs +6 -8
  21. package/dist/commands/migrate.mjs.map +1 -1
  22. package/dist/commands/migration-check.d.mts +55 -1
  23. package/dist/commands/migration-check.d.mts.map +1 -1
  24. package/dist/commands/migration-check.mjs +2 -2
  25. package/dist/commands/migration-graph.d.mts +25 -7
  26. package/dist/commands/migration-graph.d.mts.map +1 -1
  27. package/dist/commands/migration-graph.mjs +170 -2
  28. package/dist/commands/migration-graph.mjs.map +1 -0
  29. package/dist/commands/migration-list.d.mts +24 -26
  30. package/dist/commands/migration-list.d.mts.map +1 -1
  31. package/dist/commands/migration-list.mjs +2 -190
  32. package/dist/commands/migration-log.d.mts +20 -15
  33. package/dist/commands/migration-log.d.mts.map +1 -1
  34. package/dist/commands/migration-log.mjs +1 -137
  35. package/dist/commands/migration-new.mjs +3 -3
  36. package/dist/commands/migration-plan.d.mts +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -1
  38. package/dist/commands/migration-show.d.mts +1 -4
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +13 -25
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +41 -141
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +2 -759
  45. package/dist/commands/ref.d.mts +1 -1
  46. package/dist/commands/ref.mjs +3 -3
  47. package/dist/commands/telemetry/index.d.mts +7 -0
  48. package/dist/commands/telemetry/index.d.mts.map +1 -0
  49. package/dist/commands/telemetry/index.mjs +2 -0
  50. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-Wj3u4Xco.mjs} +2 -2
  51. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-Wj3u4Xco.mjs.map} +1 -1
  52. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-COg18szA.mjs} +3 -3
  53. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-COg18szA.mjs.map} +1 -1
  54. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-KyJNQK5-.mjs} +3 -3
  55. package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-KyJNQK5-.mjs.map} +1 -1
  56. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-IEp0227u.mjs} +3 -3
  57. package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-IEp0227u.mjs.map} +1 -1
  58. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-BdRPfM3Q.mjs} +63 -5
  59. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-BdRPfM3Q.mjs.map} +1 -1
  60. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C9k5KAyI.mjs} +4 -4
  61. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C9k5KAyI.mjs.map} +1 -1
  62. package/dist/exports/control-api.d.mts +2 -2
  63. package/dist/exports/control-api.d.mts.map +1 -1
  64. package/dist/exports/control-api.mjs +2 -2
  65. package/dist/exports/index.mjs +1 -1
  66. package/dist/exports/init-output.mjs +1 -1
  67. package/dist/{framework-components-fYXjz_in.mjs → framework-components-Be4inY3I.mjs} +2 -2
  68. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-Be4inY3I.mjs.map} +1 -1
  69. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
  70. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
  71. package/dist/{init-Cv9UzWL5.mjs → init-BIxw3l7t.mjs} +5 -58
  72. package/dist/init-BIxw3l7t.mjs.map +1 -0
  73. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DXUFGQDe.mjs} +3 -3
  74. package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-DXUFGQDe.mjs.map} +1 -1
  75. package/dist/{migration-check-BiBJoYYW.mjs → migration-check-CUavU7U9.mjs} +236 -88
  76. package/dist/migration-check-CUavU7U9.mjs.map +1 -0
  77. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-omgKpt3K.mjs} +3 -3
  78. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-omgKpt3K.mjs.map} +1 -1
  79. package/dist/migration-graph-space-render-ByJ83gxp.mjs +1966 -0
  80. package/dist/migration-graph-space-render-ByJ83gxp.mjs.map +1 -0
  81. package/dist/migration-list-jK6QeczE.mjs +228 -0
  82. package/dist/migration-list-jK6QeczE.mjs.map +1 -0
  83. package/dist/migration-list-types-DS63IdFd.d.mts +23 -0
  84. package/dist/migration-list-types-DS63IdFd.d.mts.map +1 -0
  85. package/dist/migration-log-CW0EjxSr.mjs +215 -0
  86. package/dist/migration-log-CW0EjxSr.mjs.map +1 -0
  87. package/dist/migration-path-target-DqcrbOis.mjs +24 -0
  88. package/dist/migration-path-target-DqcrbOis.mjs.map +1 -0
  89. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-NHdlUwPG.mjs} +5 -6
  90. package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-NHdlUwPG.mjs.map} +1 -1
  91. package/dist/migration-status-GZ6XfbWs.mjs +439 -0
  92. package/dist/migration-status-GZ6XfbWs.mjs.map +1 -0
  93. package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
  94. package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
  95. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-CJY9zOv7.mjs} +1 -1
  96. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-CJY9zOv7.mjs.map} +1 -1
  97. package/dist/telemetry-DQP0BvKv.mjs +122 -0
  98. package/dist/telemetry-DQP0BvKv.mjs.map +1 -0
  99. package/dist/{types-Dt_SfqFm.d.mts → types-Cculk5KV.d.mts} +44 -31
  100. package/dist/types-Cculk5KV.d.mts.map +1 -0
  101. package/dist/{verify-DCA9Sldu.mjs → verify-tvHRBBVP.mjs} +2 -2
  102. package/dist/{verify-DCA9Sldu.mjs.map → verify-tvHRBBVP.mjs.map} +1 -1
  103. package/package.json +22 -19
  104. package/src/cli.ts +5 -0
  105. package/src/commands/db-update.ts +7 -1
  106. package/src/commands/init/index.ts +6 -35
  107. package/src/commands/init/init.ts +1 -14
  108. package/src/commands/init/inputs.ts +0 -75
  109. package/src/commands/migrate.ts +6 -6
  110. package/src/commands/migration-check.ts +340 -117
  111. package/src/commands/migration-graph.ts +163 -90
  112. package/src/commands/migration-list.ts +55 -25
  113. package/src/commands/migration-log.ts +49 -98
  114. package/src/commands/migration-show.ts +10 -38
  115. package/src/commands/migration-status-overlay.ts +61 -0
  116. package/src/commands/migration-status.ts +440 -1056
  117. package/src/commands/telemetry/index.ts +107 -0
  118. package/src/commands/telemetry/status.ts +67 -0
  119. package/src/control-api/client.ts +17 -7
  120. package/src/control-api/operations/db-init.ts +3 -3
  121. package/src/control-api/operations/{db-apply.ts → db-run.ts} +37 -10
  122. package/src/control-api/operations/db-update.ts +4 -4
  123. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +32 -24
  124. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  125. package/src/control-api/types.ts +46 -29
  126. package/src/utils/cli-errors.ts +47 -2
  127. package/src/utils/formatters/errors.ts +11 -0
  128. package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
  129. package/src/utils/formatters/migration-graph-layout.ts +51 -7
  130. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  131. package/src/utils/formatters/migration-graph-space-render.ts +138 -0
  132. package/src/utils/formatters/migration-graph-tree-render.ts +405 -77
  133. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  134. package/src/utils/formatters/migration-list-graph-topology.ts +68 -90
  135. package/src/utils/formatters/migration-list-render.ts +122 -70
  136. package/src/utils/formatters/migration-list-styler.ts +48 -5
  137. package/src/utils/formatters/migration-log-table.ts +200 -0
  138. package/src/utils/formatters/migrations.ts +25 -1
  139. package/src/utils/global-flags.ts +35 -0
  140. package/src/utils/legend.ts +38 -0
  141. package/src/utils/migration-path-target.ts +39 -0
  142. package/src/utils/telemetry.ts +68 -32
  143. package/dist/client-KgJorIvG.mjs.map +0 -1
  144. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  145. package/dist/commands/migration-list.mjs.map +0 -1
  146. package/dist/commands/migration-log.mjs.map +0 -1
  147. package/dist/commands/migration-status.mjs.map +0 -1
  148. package/dist/extension-pack-inputs-IDvjRCi3.mjs +0 -62
  149. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +0 -1
  150. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  151. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  152. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  153. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  154. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  155. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  156. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  157. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  158. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  159. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  160. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  161. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  162. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  163. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  164. package/src/utils/formatters/graph-render.ts +0 -1323
  165. package/src/utils/formatters/graph-types.ts +0 -120
@@ -1,17 +1,36 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { createControlStack } from '@prisma-next/framework-components/control';
4
- import type { IntegrityViolation } from '@prisma-next/migration-tools/aggregate';
4
+ import type {
5
+ ContractSpaceAggregate,
6
+ IntegrityViolation,
7
+ } from '@prisma-next/migration-tools/aggregate';
5
8
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
9
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
6
10
  import { verifyMigrationHash } from '@prisma-next/migration-tools/hash';
7
11
  import { readMigrationsDir } from '@prisma-next/migration-tools/io';
8
12
  import { reconstructGraph } from '@prisma-next/migration-tools/migration-graph';
9
13
  import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
10
14
  import { parseMigrationRef } from '@prisma-next/migration-tools/ref-resolution';
15
+ import type { Refs } from '@prisma-next/migration-tools/refs';
11
16
  import { readRefs } from '@prisma-next/migration-tools/refs';
17
+ import {
18
+ isValidSpaceId,
19
+ listContractSpaceDirectories,
20
+ RESERVED_SPACE_SUBDIR_NAMES,
21
+ spaceMigrationDirectory,
22
+ spaceRefsDirectory,
23
+ } from '@prisma-next/migration-tools/spaces';
24
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
25
  import { Command } from 'commander';
13
26
  import { join, relative } from 'pathe';
14
27
  import { loadConfig } from '../config-loader';
28
+ import {
29
+ type CliStructuredError,
30
+ errorInvalidSpaceId,
31
+ errorSpaceNotFound,
32
+ mapRefResolutionError,
33
+ } from '../utils/cli-errors';
15
34
  import {
16
35
  addGlobalOptions,
17
36
  resolveContractPath,
@@ -20,7 +39,9 @@ import {
20
39
  setCommandExamples,
21
40
  setCommandSeeAlso,
22
41
  } from '../utils/command-helpers';
42
+ import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
23
43
  import { toDeclaredExtensionsFromRaw } from '../utils/extension-pack-inputs';
44
+ import { formatErrorJson, formatErrorOutput } from '../utils/formatters/errors';
24
45
  import { formatStyledHeader } from '../utils/formatters/styled';
25
46
  import type { CommonCommandOptions } from '../utils/global-flags';
26
47
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
@@ -28,11 +49,17 @@ import {
28
49
  type CheckFailure,
29
50
  integrityViolationToCheckFailure,
30
51
  } from '../utils/integrity-violation-to-check-failure';
52
+ import {
53
+ findPackageByDirPath,
54
+ looksLikePath,
55
+ resolveAppTargetPath,
56
+ } from '../utils/migration-path-target';
31
57
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
32
58
  import { INTEGRITY_FAILED, OK, PRECONDITION } from './migration-check/exit-codes';
33
59
 
34
60
  interface MigrationCheckOptions extends CommonCommandOptions {
35
61
  readonly config?: string;
62
+ readonly space?: string;
36
63
  }
37
64
 
38
65
  export type { CheckFailure } from '../utils/integrity-violation-to-check-failure';
@@ -89,6 +116,173 @@ function checkSnapshotConsistency(pkg: OnDiskMigrationPackage): CheckFailure | n
89
116
  return null;
90
117
  }
91
118
 
119
+ /**
120
+ * One contract space's on-disk state, resolved for the explicit graph
121
+ * checks `runMigrationCheck` runs per space: the space's migration
122
+ * packages, its user-authored refs, its induced graph, and the absolute
123
+ * `migrations/<space>/` + `migrations/<space>/refs/` directories the
124
+ * file-existence and dangling-ref `where` paths are derived from.
125
+ */
126
+ export interface CheckSpace {
127
+ readonly spaceId: string;
128
+ readonly packages: readonly OnDiskMigrationPackage[];
129
+ readonly refs: Refs;
130
+ readonly graph: MigrationGraph;
131
+ readonly migrationsDir: string;
132
+ readonly refsDir: string;
133
+ }
134
+
135
+ /**
136
+ * Project the loaded {@link ContractSpaceAggregate} into the
137
+ * {@link CheckSpace} rows the multi-space check iterates — one per on-disk
138
+ * contract-space directory, in the aggregate's `app`-first ordering. Mirrors
139
+ * `migration list`'s `migrationSpaceListEntriesFromAggregate`: space
140
+ * membership matches the on-disk directories, package / ref / graph data come
141
+ * from `aggregate.space(id)`.
142
+ */
143
+ export async function enumerateCheckSpaces(
144
+ aggregate: ContractSpaceAggregate,
145
+ projectMigrationsDir: string,
146
+ ): Promise<readonly CheckSpace[]> {
147
+ const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
148
+ const onDiskSpaceIds = new Set(
149
+ candidateDirs.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId),
150
+ );
151
+ const spaces: CheckSpace[] = [];
152
+ for (const member of aggregate.spaces()) {
153
+ const spaceId = member.spaceId;
154
+ if (!isValidSpaceId(spaceId)) continue;
155
+ if (!onDiskSpaceIds.has(spaceId)) continue;
156
+ const migrationsDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);
157
+ spaces.push({
158
+ spaceId,
159
+ packages: member.packages,
160
+ refs: member.refs,
161
+ graph: member.graph(),
162
+ migrationsDir,
163
+ refsDir: spaceRefsDirectory(migrationsDir),
164
+ });
165
+ }
166
+ return spaces;
167
+ }
168
+
169
+ function checkManifestFilesPresent(space: CheckSpace): readonly CheckFailure[] {
170
+ if (!existsSync(space.migrationsDir)) return [];
171
+ const loadedDirNames = new Set(space.packages.map((p) => p.dirName));
172
+ const failures: CheckFailure[] = [];
173
+ let entries: string[];
174
+ try {
175
+ entries = readdirSync(space.migrationsDir);
176
+ } catch {
177
+ return failures;
178
+ }
179
+ for (const entry of entries) {
180
+ if (entry.startsWith('.') || entry.startsWith('_') || entry === 'refs') continue;
181
+ const entryPath = join(space.migrationsDir, entry);
182
+ try {
183
+ if (!statSync(entryPath).isDirectory()) continue;
184
+ } catch {
185
+ continue;
186
+ }
187
+ if (!loadedDirNames.has(entry)) {
188
+ for (const f of ['migration.json', 'ops.json']) {
189
+ const fail = checkFileExists(entryPath, entry, f);
190
+ if (fail) failures.push(fail);
191
+ }
192
+ }
193
+ }
194
+ return failures;
195
+ }
196
+
197
+ function checkReachability(space: CheckSpace): readonly CheckFailure[] {
198
+ const allToHashes = new Set(space.packages.map((p) => p.metadata.to));
199
+ const failures: CheckFailure[] = [];
200
+ for (const pkg of space.packages) {
201
+ const isReachable =
202
+ pkg.metadata.from === null ||
203
+ allToHashes.has(pkg.metadata.from) ||
204
+ pkg.metadata.from === 'sha256:empty';
205
+ if (!isReachable) {
206
+ failures.push({
207
+ pnCode: 'PN-MIG-CHECK-003',
208
+ where: migrationPathRelative(pkg.dirPath),
209
+ why: `Migration "${pkg.dirName}" starts from ${pkg.metadata.from} which no other migration produces`,
210
+ fix: 'This migration is unreachable in the graph. Delete it or re-emit a connecting migration.',
211
+ });
212
+ }
213
+ }
214
+ return failures;
215
+ }
216
+
217
+ function checkDanglingRefs(space: CheckSpace): readonly CheckFailure[] {
218
+ const failures: CheckFailure[] = [];
219
+ for (const [name, entry] of Object.entries(space.refs)) {
220
+ if (!space.graph.nodes.has(entry.hash)) {
221
+ failures.push({
222
+ pnCode: 'PN-MIG-CHECK-004',
223
+ where: relative(process.cwd(), join(space.refsDir, `${name}.json`)),
224
+ why: `Ref "${name}" points at ${entry.hash} which does not exist in the migration graph`,
225
+ fix: `Update the ref with \`prisma-next ref set ${name} <valid-hash>\` or delete it.`,
226
+ });
227
+ }
228
+ }
229
+ return failures;
230
+ }
231
+
232
+ function checkSpace(space: CheckSpace): readonly CheckFailure[] {
233
+ return [
234
+ ...checkManifestFilesPresent(space),
235
+ ...space.packages.map(checkSnapshotConsistency).filter((f): f is CheckFailure => f !== null),
236
+ ...checkReachability(space),
237
+ ...checkDanglingRefs(space),
238
+ ];
239
+ }
240
+
241
+ /**
242
+ * Inputs for {@link runMigrationCheck} — the multi-space policy core of
243
+ * the holistic (no-arg) `migration check`. Enumeration is supplied by the
244
+ * caller (the CLI shell builds it from {@link enumerateCheckSpaces}); the
245
+ * core does not touch config, flags, or streams.
246
+ */
247
+ export interface RunMigrationCheckInputs {
248
+ readonly spaces: readonly CheckSpace[];
249
+ readonly spaceFilter?: string;
250
+ }
251
+
252
+ /**
253
+ * Policy core of the holistic `migration check`: validates `--space`,
254
+ * narrows the pre-enumerated spaces, and runs the per-space explicit graph
255
+ * checks (file-existence, snapshot consistency, reachability, dangling
256
+ * refs), aggregating every failure into one {@link MigrationCheckResult}.
257
+ *
258
+ * `--space` validation mirrors `migration list`: an invalid id →
259
+ * {@link errorInvalidSpaceId}; an id with no on-disk space →
260
+ * {@link errorSpaceNotFound}. Both map to exit `PRECONDITION` at the shell.
261
+ * Aggregate-integrity violations (which already span every space) are folded
262
+ * in by the caller, not here.
263
+ */
264
+ export function runMigrationCheck(
265
+ inputs: RunMigrationCheckInputs,
266
+ ): Result<MigrationCheckResult, CliStructuredError> {
267
+ const { spaces, spaceFilter } = inputs;
268
+
269
+ if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
270
+ return notOk(errorInvalidSpaceId(spaceFilter));
271
+ }
272
+ if (spaceFilter !== undefined && !spaces.some((s) => s.spaceId === spaceFilter)) {
273
+ return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()));
274
+ }
275
+
276
+ const scopedSpaces =
277
+ spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
278
+
279
+ const failures = scopedSpaces.flatMap(checkSpace);
280
+ if (failures.length === 0) {
281
+ return ok({ ok: true, failures: [], summary: 'All checks passed' });
282
+ }
283
+ return ok({ ok: false, failures, summary: `${failures.length} integrity failure(s)` });
284
+ }
285
+
92
286
  async function loadAggregateIntegrityViolations(
93
287
  config: Awaited<ReturnType<typeof loadConfig>>,
94
288
  migrationsDir: string,
@@ -110,12 +304,18 @@ async function loadAggregateIntegrityViolations(
110
304
  }
111
305
  }
112
306
 
307
+ interface MigrationCheckOutcome {
308
+ readonly result?: MigrationCheckResult;
309
+ readonly error?: CliStructuredError;
310
+ readonly exitCode: number;
311
+ }
312
+
113
313
  async function executeMigrationCheckCommand(
114
314
  target: string | undefined,
115
315
  options: MigrationCheckOptions,
116
316
  flags: GlobalFlags,
117
317
  ui: TerminalUI,
118
- ): Promise<{ result: MigrationCheckResult; exitCode: number }> {
318
+ ): Promise<MigrationCheckOutcome> {
119
319
  const config = await loadConfig(options.config);
120
320
  const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
121
321
  resolveMigrationPaths(options.config, config);
@@ -137,133 +337,132 @@ async function executeMigrationCheckCommand(
137
337
  ui.stderr(header);
138
338
  }
139
339
 
140
- const failures: CheckFailure[] = [];
340
+ if (target) {
341
+ return await checkSingleTarget(target, {
342
+ appMigrationsDir,
343
+ appMigrationsRelative,
344
+ refsDir,
345
+ });
346
+ }
141
347
 
142
- const loaded = await readMigrationsDir(appMigrationsDir);
143
- const bundles: readonly OnDiskMigrationPackage[] = loaded.packages;
144
- const graph = reconstructGraph(bundles);
348
+ const loadedAggregate = await buildReadAggregate(config, { migrationsDir });
349
+ if (!loadedAggregate.ok) {
350
+ return { error: loadedAggregate.failure, exitCode: PRECONDITION };
351
+ }
145
352
 
146
- if (existsSync(appMigrationsDir)) {
147
- const loadedDirNames = new Set(bundles.map((p) => p.dirName));
148
- try {
149
- const entries = readdirSync(appMigrationsDir);
150
- for (const entry of entries) {
151
- if (entry.startsWith('.') || entry.startsWith('_') || entry === 'refs') continue;
152
- const entryPath = join(appMigrationsDir, entry);
153
- try {
154
- if (!statSync(entryPath).isDirectory()) continue;
155
- } catch {
156
- continue;
157
- }
158
- if (!loadedDirNames.has(entry)) {
159
- for (const f of ['migration.json', 'ops.json']) {
160
- const fail = checkFileExists(entryPath, entry, f);
161
- if (fail) failures.push(fail);
162
- }
163
- }
164
- }
165
- } catch {
166
- // migrations dir unreadable — skip
167
- }
353
+ const spaces = await enumerateCheckSpaces(loadedAggregate.value.aggregate, migrationsDir);
354
+ const checkResult = runMigrationCheck({
355
+ spaces,
356
+ ...(options.space !== undefined ? { spaceFilter: options.space } : {}),
357
+ });
358
+ if (!checkResult.ok) {
359
+ return { error: checkResult.failure, exitCode: PRECONDITION };
168
360
  }
169
361
 
170
- if (target) {
171
- const refs = await readRefs(refsDir);
172
- const migResult = parseMigrationRef(target, { graph, refs });
173
- if (!migResult.ok) {
174
- const msg =
175
- migResult.failure.kind === 'not-found'
176
- ? `Migration "${target}" does not exist`
177
- : migResult.failure.kind === 'wrong-grammar'
178
- ? migResult.failure.message
179
- : `Invalid migration reference: "${target}"`;
180
- return {
181
- result: { ok: false, failures: [], summary: msg },
182
- exitCode: PRECONDITION,
183
- };
184
- }
362
+ const failures: CheckFailure[] = [...checkResult.value.failures];
363
+ const allViolations = await loadAggregateIntegrityViolations(config, migrationsDir);
364
+ const scopedViolations =
365
+ options.space === undefined
366
+ ? allViolations
367
+ : allViolations.filter((v) => v.kind !== 'disjointness' && v.spaceId === options.space);
368
+ for (const violation of scopedViolations) {
369
+ failures.push(integrityViolationToCheckFailure(violation, migrationsDir));
370
+ }
185
371
 
186
- const matchedPkg = bundles.find(
187
- (p) => p.metadata.migrationHash === migResult.value.migrationHash,
188
- );
189
- if (!matchedPkg) {
190
- return {
191
- result: {
192
- ok: false,
193
- failures: [],
194
- summary: `Migration package for "${target}" not found on disk`,
195
- },
196
- exitCode: PRECONDITION,
197
- };
198
- }
372
+ if (failures.length === 0) {
373
+ return {
374
+ result: { ok: true, failures: [], summary: 'All checks passed' },
375
+ exitCode: OK,
376
+ };
377
+ }
199
378
 
200
- for (const f of ['migration.json', 'ops.json']) {
201
- const fail = checkFileExists(matchedPkg.dirPath, matchedPkg.dirName, f);
202
- if (fail) failures.push(fail);
203
- }
379
+ return {
380
+ result: { ok: false, failures, summary: `${failures.length} integrity failure(s)` },
381
+ exitCode: INTEGRITY_FAILED,
382
+ };
383
+ }
204
384
 
205
- const verification = verifyMigrationHash(matchedPkg);
206
- if (!verification.ok) {
207
- failures.push({
208
- pnCode: 'PN-MIG-CHECK-001',
209
- where: migrationFileRelative(matchedPkg.dirPath, 'migration.json'),
210
- why: `Stored hash ${verification.storedHash} does not match recomputed hash ${verification.computedHash}`,
211
- fix: 'Re-emit the migration package or restore from version control.',
212
- });
213
- }
385
+ interface SingleTargetPaths {
386
+ readonly appMigrationsDir: string;
387
+ readonly appMigrationsRelative: string;
388
+ readonly refsDir: string;
389
+ }
390
+
391
+ /**
392
+ * Single-target (`check <ref/path>`) mode — app-space only by design (the
393
+ * migration's space is pinned by the reference; multi-space single-target
394
+ * resolution is a deliberate follow-up, see the slice spec § Out of scope).
395
+ * Resolves the one referenced package and verifies its hash / manifest /
396
+ * snapshot, plus the app-space orphan-manifest check the prior behaviour ran.
397
+ */
398
+ async function checkSingleTarget(
399
+ target: string,
400
+ paths: SingleTargetPaths,
401
+ ): Promise<MigrationCheckOutcome> {
402
+ const { appMigrationsDir, appMigrationsRelative, refsDir } = paths;
403
+ const loaded = await readMigrationsDir(appMigrationsDir);
404
+ const bundles: readonly OnDiskMigrationPackage[] = loaded.packages;
405
+ const appSpace: CheckSpace = {
406
+ spaceId: 'app',
407
+ packages: bundles,
408
+ refs: await readRefs(refsDir),
409
+ graph: reconstructGraph(bundles),
410
+ migrationsDir: appMigrationsDir,
411
+ refsDir,
412
+ };
413
+
414
+ const failures: CheckFailure[] = [...checkManifestFilesPresent(appSpace)];
214
415
 
215
- const snapshotFailure = checkSnapshotConsistency(matchedPkg);
216
- if (snapshotFailure) failures.push(snapshotFailure);
416
+ let matchedPkg: OnDiskMigrationPackage | undefined;
417
+ if (looksLikePath(target)) {
418
+ const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
419
+ if (!resolved.ok) {
420
+ return { error: resolved.failure, exitCode: PRECONDITION };
421
+ }
422
+ matchedPkg = findPackageByDirPath(bundles, resolved.value);
217
423
  } else {
218
- for (const pkg of bundles) {
219
- const snapshotFailure = checkSnapshotConsistency(pkg);
220
- if (snapshotFailure) failures.push(snapshotFailure);
424
+ const migResult = parseMigrationRef(target, { graph: appSpace.graph, refs: appSpace.refs });
425
+ if (!migResult.ok) {
426
+ return { error: mapRefResolutionError(migResult.failure), exitCode: PRECONDITION };
221
427
  }
428
+ matchedPkg = bundles.find((p) => p.metadata.migrationHash === migResult.value.migrationHash);
429
+ }
222
430
 
223
- const allToHashes = new Set(bundles.map((p) => p.metadata.to));
224
- for (const pkg of bundles) {
225
- const isReachable =
226
- pkg.metadata.from === null ||
227
- allToHashes.has(pkg.metadata.from) ||
228
- pkg.metadata.from === 'sha256:empty';
229
- if (!isReachable) {
230
- failures.push({
231
- pnCode: 'PN-MIG-CHECK-003',
232
- where: migrationPathRelative(pkg.dirPath),
233
- why: `Migration "${pkg.dirName}" starts from ${pkg.metadata.from} which no other migration produces`,
234
- fix: 'This migration is unreachable in the graph. Delete it or re-emit a connecting migration.',
235
- });
236
- }
237
- }
431
+ if (!matchedPkg) {
432
+ return {
433
+ result: {
434
+ ok: false,
435
+ failures: [],
436
+ summary: `Migration package for "${target}" not found on disk`,
437
+ },
438
+ exitCode: PRECONDITION,
439
+ };
440
+ }
238
441
 
239
- try {
240
- const refs = await readRefs(refsDir);
241
- for (const [name, entry] of Object.entries(refs)) {
242
- if (!graph.nodes.has(entry.hash)) {
243
- failures.push({
244
- pnCode: 'PN-MIG-CHECK-004',
245
- where: relative(process.cwd(), join(refsDir, `${name}.json`)),
246
- why: `Ref "${name}" points at ${entry.hash} which does not exist in the migration graph`,
247
- fix: `Update the ref with \`prisma-next ref set ${name} <valid-hash>\` or delete it.`,
248
- });
249
- }
250
- }
251
- } catch {
252
- // Refs unreadable — skip ref checks
253
- }
442
+ for (const f of ['migration.json', 'ops.json']) {
443
+ const fail = checkFileExists(matchedPkg.dirPath, matchedPkg.dirName, f);
444
+ if (fail) failures.push(fail);
445
+ }
254
446
 
255
- for (const violation of await loadAggregateIntegrityViolations(config, migrationsDir)) {
256
- failures.push(integrityViolationToCheckFailure(violation, migrationsDir));
257
- }
447
+ const verification = verifyMigrationHash(matchedPkg);
448
+ if (!verification.ok) {
449
+ failures.push({
450
+ pnCode: 'PN-MIG-CHECK-001',
451
+ where: migrationFileRelative(matchedPkg.dirPath, 'migration.json'),
452
+ why: `Stored hash ${verification.storedHash} does not match recomputed hash ${verification.computedHash}`,
453
+ fix: 'Re-emit the migration package or restore from version control.',
454
+ });
258
455
  }
259
456
 
457
+ const snapshotFailure = checkSnapshotConsistency(matchedPkg);
458
+ if (snapshotFailure) failures.push(snapshotFailure);
459
+
260
460
  if (failures.length === 0) {
261
461
  return {
262
462
  result: { ok: true, failures: [], summary: 'All checks passed' },
263
463
  exitCode: OK,
264
464
  };
265
465
  }
266
-
267
466
  return {
268
467
  result: { ok: false, failures, summary: `${failures.length} integrity failure(s)` },
269
468
  exitCode: INTEGRITY_FAILED,
@@ -277,11 +476,16 @@ export function createMigrationCheckCommand(): Command {
277
476
  'Verify artifact and graph integrity',
278
477
  'Validates that on-disk migration packages are internally consistent\n' +
279
478
  '(hashes match, manifests are complete) and that the graph is well-formed\n' +
280
- '(edges connect, refs point at valid nodes). Offline does not consult\n' +
281
- 'the database.',
479
+ '(edges connect, refs point at valid nodes). The whole-graph check spans\n' +
480
+ 'every contract space by default; pass --space <id> to narrow to one, or\n' +
481
+ 'a migration reference to check a single app-space package.\n' +
482
+ 'Offline — does not consult the database.\n' +
483
+ 'Exit codes: 0 = all checks passed, 2 = precondition failed\n' +
484
+ '(unresolved target or unknown --space), 4 = integrity failure(s) found.',
282
485
  );
283
486
  setCommandExamples(command, [
284
487
  'prisma-next migration check',
488
+ 'prisma-next migration check --space app',
285
489
  'prisma-next migration check 20260101-add-users',
286
490
  'prisma-next migration check --json',
287
491
  ]);
@@ -289,25 +493,44 @@ export function createMigrationCheckCommand(): Command {
289
493
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
290
494
  { verb: 'migration list', oneLiner: 'List on-disk migrations' },
291
495
  { verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
496
+ { verb: 'migration show', oneLiner: 'Display migration package contents' },
292
497
  ]);
293
498
  command.exitOverride();
294
499
  addGlobalOptions(command)
295
- .argument('[migration]', 'Migration reference (directory name or hash) to check')
500
+ .argument('[target]', 'Migration reference: directory name, hash/prefix, ref, or path')
296
501
  .option('--config <path>', 'Path to prisma-next.config.ts')
502
+ .option('--space <id>', 'Narrow output to a single contract space')
297
503
  .action(async (target: string | undefined, options: MigrationCheckOptions) => {
298
504
  const flags = parseGlobalFlagsOrExit(options);
299
505
  const ui = createTerminalUI(flags);
300
506
 
301
- let result: MigrationCheckResult;
302
- let exitCode: number;
507
+ let outcome: MigrationCheckOutcome;
303
508
  try {
304
- ({ result, exitCode } = await executeMigrationCheckCommand(target, options, flags, ui));
509
+ outcome = await executeMigrationCheckCommand(target, options, flags, ui);
305
510
  } catch (error) {
306
511
  const msg = error instanceof Error ? error.message : String(error);
307
- result = { ok: false, failures: [], summary: msg };
308
- exitCode = PRECONDITION;
512
+ outcome = {
513
+ result: { ok: false, failures: [], summary: msg },
514
+ exitCode: PRECONDITION,
515
+ };
309
516
  }
310
517
 
518
+ if (outcome.error) {
519
+ const envelope = outcome.error.toEnvelope();
520
+ if (flags.json) {
521
+ ui.output(formatErrorJson(envelope));
522
+ } else if (!flags.quiet) {
523
+ ui.error(formatErrorOutput(envelope, flags));
524
+ }
525
+ process.exit(outcome.exitCode);
526
+ }
527
+
528
+ const result = outcome.result ?? {
529
+ ok: false,
530
+ failures: [],
531
+ summary: 'No check result produced',
532
+ };
533
+
311
534
  if (flags.json) {
312
535
  ui.output(JSON.stringify(result, null, 2));
313
536
  } else if (!flags.quiet) {
@@ -322,7 +545,7 @@ export function createMigrationCheckCommand(): Command {
322
545
  }
323
546
  }
324
547
 
325
- process.exit(exitCode);
548
+ process.exit(outcome.exitCode);
326
549
  });
327
550
 
328
551
  return command;