@prisma-next/cli 0.3.0-pr.99.5 → 0.3.0

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 (257) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +381 -128
  3. package/dist/agent-skill-mongo.md +106 -0
  4. package/dist/agent-skill-postgres.md +106 -0
  5. package/dist/cli-errors-BDCYR5ap.mjs +4 -0
  6. package/dist/cli-errors-DStABy9d.d.mts +3 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.js +1 -2910
  9. package/dist/cli.mjs +261 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/client-DiUkJAeN.mjs +987 -0
  12. package/dist/client-DiUkJAeN.mjs.map +1 -0
  13. package/dist/commands/contract-emit.d.mts +7 -0
  14. package/dist/commands/contract-emit.d.mts.map +1 -0
  15. package/dist/commands/contract-emit.mjs +9 -0
  16. package/dist/commands/contract-infer.d.mts +7 -0
  17. package/dist/commands/contract-infer.d.mts.map +1 -0
  18. package/dist/commands/contract-infer.mjs +10 -0
  19. package/dist/commands/db-init.d.mts +7 -0
  20. package/dist/commands/db-init.d.mts.map +1 -0
  21. package/dist/commands/db-init.mjs +126 -0
  22. package/dist/commands/db-init.mjs.map +1 -0
  23. package/dist/commands/db-schema.d.mts +7 -0
  24. package/dist/commands/db-schema.d.mts.map +1 -0
  25. package/dist/commands/db-schema.mjs +56 -0
  26. package/dist/commands/db-schema.mjs.map +1 -0
  27. package/dist/commands/db-sign.d.mts +7 -0
  28. package/dist/commands/db-sign.d.mts.map +1 -0
  29. package/dist/commands/db-sign.mjs +137 -0
  30. package/dist/commands/db-sign.mjs.map +1 -0
  31. package/dist/commands/db-update.d.mts +7 -0
  32. package/dist/commands/db-update.d.mts.map +1 -0
  33. package/dist/commands/db-update.mjs +123 -0
  34. package/dist/commands/db-update.mjs.map +1 -0
  35. package/dist/commands/db-verify.d.mts +7 -0
  36. package/dist/commands/db-verify.d.mts.map +1 -0
  37. package/dist/commands/db-verify.mjs +323 -0
  38. package/dist/commands/db-verify.mjs.map +1 -0
  39. package/dist/commands/migration-apply.d.mts +36 -0
  40. package/dist/commands/migration-apply.d.mts.map +1 -0
  41. package/dist/commands/migration-apply.mjs +245 -0
  42. package/dist/commands/migration-apply.mjs.map +1 -0
  43. package/dist/commands/migration-new.d.mts +8 -0
  44. package/dist/commands/migration-new.d.mts.map +1 -0
  45. package/dist/commands/migration-new.mjs +152 -0
  46. package/dist/commands/migration-new.mjs.map +1 -0
  47. package/dist/commands/migration-plan.d.mts +47 -0
  48. package/dist/commands/migration-plan.d.mts.map +1 -0
  49. package/dist/commands/migration-plan.mjs +313 -0
  50. package/dist/commands/migration-plan.mjs.map +1 -0
  51. package/dist/commands/migration-ref.d.mts +43 -0
  52. package/dist/commands/migration-ref.d.mts.map +1 -0
  53. package/dist/commands/migration-ref.mjs +195 -0
  54. package/dist/commands/migration-ref.mjs.map +1 -0
  55. package/dist/commands/migration-show.d.mts +28 -0
  56. package/dist/commands/migration-show.d.mts.map +1 -0
  57. package/dist/commands/migration-show.mjs +140 -0
  58. package/dist/commands/migration-show.mjs.map +1 -0
  59. package/dist/commands/migration-status.d.mts +86 -0
  60. package/dist/commands/migration-status.d.mts.map +1 -0
  61. package/dist/commands/migration-status.mjs +9 -0
  62. package/dist/commands/migration-verify.d.mts +16 -0
  63. package/dist/commands/migration-verify.d.mts.map +1 -0
  64. package/dist/commands/migration-verify.mjs +110 -0
  65. package/dist/commands/migration-verify.mjs.map +1 -0
  66. package/dist/config-loader-C4VXKl8f.mjs +43 -0
  67. package/dist/config-loader-C4VXKl8f.mjs.map +1 -0
  68. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  69. package/dist/config-loader.d.mts.map +1 -0
  70. package/dist/config-loader.mjs +3 -0
  71. package/dist/contract-emit-D2wDXfyo.mjs +191 -0
  72. package/dist/contract-emit-D2wDXfyo.mjs.map +1 -0
  73. package/dist/contract-emit-Zm_sd1wQ.mjs +112 -0
  74. package/dist/contract-emit-Zm_sd1wQ.mjs.map +1 -0
  75. package/dist/contract-emit-kN-IkKTE.mjs +6 -0
  76. package/dist/contract-enrichment-CGW6mm-E.mjs +79 -0
  77. package/dist/contract-enrichment-CGW6mm-E.mjs.map +1 -0
  78. package/dist/contract-infer-DozZT511.mjs +90 -0
  79. package/dist/contract-infer-DozZT511.mjs.map +1 -0
  80. package/dist/exports/config-types.d.mts +2 -0
  81. package/dist/exports/config-types.mjs +3 -0
  82. package/dist/exports/control-api.d.mts +624 -0
  83. package/dist/exports/control-api.d.mts.map +1 -0
  84. package/dist/exports/control-api.mjs +8 -0
  85. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +12 -7
  86. package/dist/exports/index.d.mts.map +1 -0
  87. package/dist/exports/index.mjs +142 -0
  88. package/dist/exports/index.mjs.map +1 -0
  89. package/dist/extract-operation-statements-DZUJNmL3.mjs +13 -0
  90. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +1 -0
  91. package/dist/extract-sql-ddl-DDMX-9mz.mjs +26 -0
  92. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +1 -0
  93. package/dist/framework-components-BAsliT4V.mjs +59 -0
  94. package/dist/framework-components-BAsliT4V.mjs.map +1 -0
  95. package/dist/init-6Pvm_esG.mjs +430 -0
  96. package/dist/init-6Pvm_esG.mjs.map +1 -0
  97. package/dist/inspect-live-schema-BYnhztxZ.mjs +91 -0
  98. package/dist/inspect-live-schema-BYnhztxZ.mjs.map +1 -0
  99. package/dist/migration-command-scaffold-CntCcntR.mjs +105 -0
  100. package/dist/migration-command-scaffold-CntCcntR.mjs.map +1 -0
  101. package/dist/migration-status-CJANY4yr.mjs +1583 -0
  102. package/dist/migration-status-CJANY4yr.mjs.map +1 -0
  103. package/dist/migrations-DTZBYXm1.mjs +173 -0
  104. package/dist/migrations-DTZBYXm1.mjs.map +1 -0
  105. package/dist/progress-adapter-B-YvmcDu.mjs +43 -0
  106. package/dist/progress-adapter-B-YvmcDu.mjs.map +1 -0
  107. package/dist/quick-reference-mongo.md +93 -0
  108. package/dist/quick-reference-postgres.md +91 -0
  109. package/dist/result-handler-oK_vA-Fn.mjs +697 -0
  110. package/dist/result-handler-oK_vA-Fn.mjs.map +1 -0
  111. package/dist/terminal-ui-C5k88MmW.mjs +274 -0
  112. package/dist/terminal-ui-C5k88MmW.mjs.map +1 -0
  113. package/dist/validate-contract-deps-esa-VQ0h.mjs +37 -0
  114. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +1 -0
  115. package/dist/verify-DlFQ2FOw.mjs +385 -0
  116. package/dist/verify-DlFQ2FOw.mjs.map +1 -0
  117. package/package.json +87 -40
  118. package/src/cli.ts +118 -58
  119. package/src/commands/contract-emit.ts +101 -78
  120. package/src/commands/contract-infer-paths.ts +32 -0
  121. package/src/commands/contract-infer.ts +143 -0
  122. package/src/commands/db-init.ts +97 -219
  123. package/src/commands/db-schema.ts +77 -0
  124. package/src/commands/db-sign.ts +46 -73
  125. package/src/commands/db-update.ts +236 -0
  126. package/src/commands/db-verify.ts +409 -119
  127. package/src/commands/init/detect-package-manager.ts +47 -0
  128. package/src/commands/init/index.ts +21 -0
  129. package/src/commands/init/init.ts +203 -0
  130. package/src/commands/init/templates/agent-skill-mongo.md +106 -0
  131. package/src/commands/init/templates/agent-skill-postgres.md +106 -0
  132. package/src/commands/init/templates/agent-skill.ts +19 -0
  133. package/src/commands/init/templates/code-templates.ts +168 -0
  134. package/src/commands/init/templates/quick-reference-mongo.md +93 -0
  135. package/src/commands/init/templates/quick-reference-postgres.md +91 -0
  136. package/src/commands/init/templates/quick-reference.ts +19 -0
  137. package/src/commands/init/templates/render.ts +20 -0
  138. package/src/commands/init/templates/tsconfig.ts +35 -0
  139. package/src/commands/inspect-live-schema.ts +170 -0
  140. package/src/commands/migration-apply.ts +427 -0
  141. package/src/commands/migration-new.ts +260 -0
  142. package/src/commands/migration-plan.ts +519 -0
  143. package/src/commands/migration-ref.ts +305 -0
  144. package/src/commands/migration-show.ts +246 -0
  145. package/src/commands/migration-status.ts +864 -0
  146. package/src/commands/migration-verify.ts +180 -0
  147. package/src/config-loader.ts +13 -3
  148. package/src/control-api/client.ts +205 -183
  149. package/src/control-api/contract-enrichment.ts +119 -0
  150. package/src/control-api/errors.ts +9 -0
  151. package/src/control-api/operations/contract-emit.ts +181 -0
  152. package/src/control-api/operations/db-init.ts +53 -49
  153. package/src/control-api/operations/db-update.ts +220 -0
  154. package/src/control-api/operations/extract-operation-statements.ts +14 -0
  155. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  156. package/src/control-api/operations/migration-apply.ts +191 -0
  157. package/src/control-api/operations/migration-helpers.ts +49 -0
  158. package/src/control-api/types.ts +274 -52
  159. package/src/exports/config-types.ts +4 -3
  160. package/src/exports/control-api.ts +15 -5
  161. package/src/load-ts-contract.ts +30 -19
  162. package/src/utils/cli-errors.ts +14 -8
  163. package/src/utils/command-helpers.ts +302 -3
  164. package/src/utils/formatters/emit.ts +67 -0
  165. package/src/utils/formatters/errors.ts +82 -0
  166. package/src/utils/formatters/graph-migration-mapper.ts +240 -0
  167. package/src/utils/formatters/graph-render.ts +1323 -0
  168. package/src/utils/formatters/graph-types.ts +120 -0
  169. package/src/utils/formatters/help.ts +380 -0
  170. package/src/utils/formatters/helpers.ts +28 -0
  171. package/src/utils/formatters/migrations.ts +346 -0
  172. package/src/utils/formatters/styled.ts +212 -0
  173. package/src/utils/formatters/verify.ts +621 -0
  174. package/src/utils/framework-components.ts +13 -10
  175. package/src/utils/global-flags.ts +41 -23
  176. package/src/utils/migration-command-scaffold.ts +184 -0
  177. package/src/utils/migration-types.ts +12 -0
  178. package/src/utils/progress-adapter.ts +18 -29
  179. package/src/utils/result-handler.ts +12 -13
  180. package/src/utils/shutdown.ts +92 -0
  181. package/src/utils/suggest-command.ts +31 -0
  182. package/src/utils/terminal-ui.ts +276 -0
  183. package/src/utils/validate-contract-deps.ts +49 -0
  184. package/dist/chunk-AGOTG4L3.js +0 -965
  185. package/dist/chunk-AGOTG4L3.js.map +0 -1
  186. package/dist/chunk-HLLI4YL7.js +0 -180
  187. package/dist/chunk-HLLI4YL7.js.map +0 -1
  188. package/dist/chunk-HWYQOCAJ.js +0 -47
  189. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  190. package/dist/chunk-VG2R7DGF.js +0 -735
  191. package/dist/chunk-VG2R7DGF.js.map +0 -1
  192. package/dist/cli.d.ts +0 -2
  193. package/dist/cli.d.ts.map +0 -1
  194. package/dist/cli.js.map +0 -1
  195. package/dist/commands/contract-emit.d.ts +0 -3
  196. package/dist/commands/contract-emit.d.ts.map +0 -1
  197. package/dist/commands/contract-emit.js +0 -10
  198. package/dist/commands/contract-emit.js.map +0 -1
  199. package/dist/commands/db-init.d.ts +0 -3
  200. package/dist/commands/db-init.d.ts.map +0 -1
  201. package/dist/commands/db-init.js +0 -257
  202. package/dist/commands/db-init.js.map +0 -1
  203. package/dist/commands/db-introspect.d.ts +0 -3
  204. package/dist/commands/db-introspect.d.ts.map +0 -1
  205. package/dist/commands/db-introspect.js +0 -155
  206. package/dist/commands/db-introspect.js.map +0 -1
  207. package/dist/commands/db-schema-verify.d.ts +0 -3
  208. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  209. package/dist/commands/db-schema-verify.js +0 -171
  210. package/dist/commands/db-schema-verify.js.map +0 -1
  211. package/dist/commands/db-sign.d.ts +0 -3
  212. package/dist/commands/db-sign.d.ts.map +0 -1
  213. package/dist/commands/db-sign.js +0 -195
  214. package/dist/commands/db-sign.js.map +0 -1
  215. package/dist/commands/db-verify.d.ts +0 -3
  216. package/dist/commands/db-verify.d.ts.map +0 -1
  217. package/dist/commands/db-verify.js +0 -193
  218. package/dist/commands/db-verify.js.map +0 -1
  219. package/dist/config-loader.d.ts.map +0 -1
  220. package/dist/config-loader.js +0 -7
  221. package/dist/config-loader.js.map +0 -1
  222. package/dist/control-api/client.d.ts +0 -13
  223. package/dist/control-api/client.d.ts.map +0 -1
  224. package/dist/control-api/operations/db-init.d.ts +0 -29
  225. package/dist/control-api/operations/db-init.d.ts.map +0 -1
  226. package/dist/control-api/types.d.ts +0 -387
  227. package/dist/control-api/types.d.ts.map +0 -1
  228. package/dist/exports/config-types.d.ts +0 -3
  229. package/dist/exports/config-types.d.ts.map +0 -1
  230. package/dist/exports/config-types.js +0 -6
  231. package/dist/exports/config-types.js.map +0 -1
  232. package/dist/exports/control-api.d.ts +0 -13
  233. package/dist/exports/control-api.d.ts.map +0 -1
  234. package/dist/exports/control-api.js +0 -7
  235. package/dist/exports/control-api.js.map +0 -1
  236. package/dist/exports/index.d.ts +0 -4
  237. package/dist/exports/index.d.ts.map +0 -1
  238. package/dist/exports/index.js +0 -176
  239. package/dist/exports/index.js.map +0 -1
  240. package/dist/load-ts-contract.d.ts.map +0 -1
  241. package/dist/utils/cli-errors.d.ts +0 -7
  242. package/dist/utils/cli-errors.d.ts.map +0 -1
  243. package/dist/utils/command-helpers.d.ts +0 -12
  244. package/dist/utils/command-helpers.d.ts.map +0 -1
  245. package/dist/utils/framework-components.d.ts +0 -70
  246. package/dist/utils/framework-components.d.ts.map +0 -1
  247. package/dist/utils/global-flags.d.ts +0 -25
  248. package/dist/utils/global-flags.d.ts.map +0 -1
  249. package/dist/utils/output.d.ts +0 -142
  250. package/dist/utils/output.d.ts.map +0 -1
  251. package/dist/utils/progress-adapter.d.ts +0 -26
  252. package/dist/utils/progress-adapter.d.ts.map +0 -1
  253. package/dist/utils/result-handler.d.ts +0 -15
  254. package/dist/utils/result-handler.d.ts.map +0 -1
  255. package/src/commands/db-introspect.ts +0 -227
  256. package/src/commands/db-schema-verify.ts +0 -238
  257. package/src/utils/output.ts +0 -1471
@@ -0,0 +1,519 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import type { OperationDescriptor } from '@prisma-next/framework-components/control';
4
+ import { attestMigration } from '@prisma-next/migration-tools/attestation';
5
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
6
+ import { findLatestMigration } from '@prisma-next/migration-tools/dag';
7
+ import {
8
+ formatMigrationDirName,
9
+ writeMigrationOps,
10
+ writeMigrationPackage,
11
+ } from '@prisma-next/migration-tools/io';
12
+ import {
13
+ evaluateMigrationTs,
14
+ scaffoldMigrationTs,
15
+ } from '@prisma-next/migration-tools/migration-ts';
16
+ import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
17
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
18
+ import { Command } from 'commander';
19
+ import { join, relative } from 'pathe';
20
+ import { loadConfig } from '../config-loader';
21
+ import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
22
+ import {
23
+ type CliErrorConflict,
24
+ CliStructuredError,
25
+ errorContractValidationFailed,
26
+ errorFileNotFound,
27
+ errorMigrationPlanningFailed,
28
+ errorRuntime,
29
+ errorTargetMigrationNotSupported,
30
+ errorUnexpected,
31
+ } from '../utils/cli-errors';
32
+ import {
33
+ addGlobalOptions,
34
+ getTargetMigrations,
35
+ loadAllBundles,
36
+ resolveContractPath,
37
+ resolveMigrationPaths,
38
+ setCommandDescriptions,
39
+ setCommandExamples,
40
+ } from '../utils/command-helpers';
41
+ import { formatStyledHeader } from '../utils/formatters/styled';
42
+ import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
43
+ import type { CommonCommandOptions } from '../utils/global-flags';
44
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
45
+ import { handleResult } from '../utils/result-handler';
46
+ import { TerminalUI } from '../utils/terminal-ui';
47
+
48
+ interface MigrationPlanOptions extends CommonCommandOptions {
49
+ readonly config?: string;
50
+ readonly name?: string;
51
+ readonly from?: string;
52
+ }
53
+
54
+ export interface MigrationPlanResult {
55
+ readonly ok: boolean;
56
+ readonly noOp: boolean;
57
+ readonly from: string;
58
+ readonly to: string;
59
+ readonly migrationId?: string;
60
+ readonly dir?: string;
61
+ readonly operations: readonly {
62
+ readonly id: string;
63
+ readonly label: string;
64
+ readonly operationClass: string;
65
+ }[];
66
+ readonly sql?: readonly string[];
67
+ readonly summary: string;
68
+ readonly timings: {
69
+ readonly total: number;
70
+ };
71
+ }
72
+
73
+ function mapMigrationToolsError(error: unknown): CliStructuredError {
74
+ if (CliStructuredError.is(error)) {
75
+ return error;
76
+ }
77
+ if (MigrationToolsError.is(error)) {
78
+ return errorRuntime(error.message, {
79
+ why: error.why,
80
+ fix: error.fix,
81
+ meta: { code: error.code, ...(error.details ?? {}) },
82
+ });
83
+ }
84
+ return errorUnexpected(error instanceof Error ? error.message : String(error), {
85
+ why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
86
+ });
87
+ }
88
+
89
+ async function executeMigrationPlanCommand(
90
+ options: MigrationPlanOptions,
91
+ flags: GlobalFlags,
92
+ ui: TerminalUI,
93
+ startTime: number,
94
+ ): Promise<Result<MigrationPlanResult, CliStructuredError>> {
95
+ const config = await loadConfig(options.config);
96
+ const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
97
+ options.config,
98
+ config,
99
+ );
100
+
101
+ const contractPathAbsolute = resolveContractPath(config);
102
+ const contractPath = relative(process.cwd(), contractPathAbsolute);
103
+
104
+ if (!flags.json && !flags.quiet) {
105
+ const details: Array<{ label: string; value: string }> = [
106
+ { label: 'config', value: configPath },
107
+ { label: 'contract', value: contractPath },
108
+ { label: 'migrations', value: migrationsRelative },
109
+ ];
110
+ if (options.from) {
111
+ details.push({ label: 'from', value: options.from });
112
+ }
113
+ if (options.name) {
114
+ details.push({ label: 'name', value: options.name });
115
+ }
116
+ const header = formatStyledHeader({
117
+ command: 'migration plan',
118
+ description: 'Plan a migration from contract changes',
119
+ url: 'https://pris.ly/migration-plan',
120
+ details,
121
+ flags,
122
+ });
123
+ ui.stderr(header);
124
+ }
125
+
126
+ // Load contract file (the "to" contract)
127
+ let contractJsonContent: string;
128
+ try {
129
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
130
+ } catch (error) {
131
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
132
+ return notOk(
133
+ errorFileNotFound(contractPathAbsolute, {
134
+ why: `Contract file not found at ${contractPathAbsolute}`,
135
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
136
+ }),
137
+ );
138
+ }
139
+ return notOk(
140
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
141
+ why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
142
+ }),
143
+ );
144
+ }
145
+
146
+ let toContractJson: Contract;
147
+ try {
148
+ toContractJson = JSON.parse(contractJsonContent) as Contract;
149
+ } catch (error) {
150
+ return notOk(
151
+ errorContractValidationFailed(
152
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
153
+ { where: { path: contractPathAbsolute } },
154
+ ),
155
+ );
156
+ }
157
+
158
+ const rawStorageHash = toContractJson.storage?.storageHash;
159
+ if (typeof rawStorageHash !== 'string') {
160
+ return notOk(
161
+ errorContractValidationFailed('Contract is missing storageHash', {
162
+ where: { path: contractPathAbsolute },
163
+ }),
164
+ );
165
+ }
166
+ const toStorageHash = rawStorageHash;
167
+
168
+ // Read existing migrations and determine "from" contract
169
+ let fromContract: Contract | null = null;
170
+ let fromHash: string = EMPTY_CONTRACT_HASH;
171
+
172
+ try {
173
+ const { attested: bundles, drafts, graph } = await loadAllBundles(migrationsDir);
174
+
175
+ // Check if a draft migration already targets this contract
176
+ const existingDraft = drafts.find((d) => d.manifest.to === toStorageHash);
177
+ if (existingDraft) {
178
+ return notOk(
179
+ errorRuntime('A draft migration to this contract already exists', {
180
+ why: `Draft migration at "${existingDraft.dirName}" already targets ${toStorageHash}`,
181
+ fix: `Run 'prisma-next migration verify --dir ${migrationsRelative}/${existingDraft.dirName}' to attest it, or delete it and re-plan.`,
182
+ }),
183
+ );
184
+ }
185
+
186
+ if (options.from) {
187
+ const resolved = resolveBundleByPrefix(bundles, options.from);
188
+ if (!resolved.ok) {
189
+ const f = resolved.failure;
190
+ return notOk(
191
+ f.reason === 'ambiguous'
192
+ ? errorRuntime('Multiple matching migrations found', {
193
+ why: `Prefix "${options.from}" matches ${f.count} migrations in ${migrationsRelative}`,
194
+ fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration target.',
195
+ })
196
+ : errorRuntime('Starting contract not found', {
197
+ why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
198
+ fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
199
+ }),
200
+ );
201
+ }
202
+ fromHash = resolved.value.manifest.to;
203
+ fromContract = resolved.value.manifest.toContract;
204
+ } else {
205
+ const latestMigration = findLatestMigration(graph);
206
+ if (latestMigration) {
207
+ fromHash = latestMigration.to;
208
+ const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
209
+ if (leafPkg) {
210
+ fromContract = leafPkg.manifest.toContract;
211
+ }
212
+ }
213
+ }
214
+ } catch (error) {
215
+ if (MigrationToolsError.is(error)) {
216
+ return notOk(mapMigrationToolsError(error));
217
+ }
218
+ throw error;
219
+ }
220
+
221
+ // Check for no-op (same hash means no changes)
222
+ if (fromHash === toStorageHash) {
223
+ const result: MigrationPlanResult = {
224
+ ok: true,
225
+ noOp: true,
226
+ from: fromHash,
227
+ to: toStorageHash,
228
+ operations: [],
229
+ summary: 'No changes detected between contracts',
230
+ timings: { total: Date.now() - startTime },
231
+ };
232
+ return ok(result);
233
+ }
234
+
235
+ // Check target supports migrations
236
+ const migrations = getTargetMigrations(config.target);
237
+ if (!migrations) {
238
+ return notOk(
239
+ errorTargetMigrationNotSupported({
240
+ why: `Target "${config.target.id}" does not support migrations`,
241
+ }),
242
+ );
243
+ }
244
+ const frameworkComponents = assertFrameworkComponentsCompatible(
245
+ config.family.familyId,
246
+ config.target.targetId,
247
+ [config.target, config.adapter, ...(config.extensionPacks ?? [])],
248
+ );
249
+
250
+ // Use descriptor-based planner if available, fall back to old planner
251
+ if (migrations.planWithDescriptors) {
252
+ const descriptorResult = migrations.planWithDescriptors({
253
+ fromContract,
254
+ toContract: toContractJson,
255
+ frameworkComponents,
256
+ });
257
+
258
+ if (!descriptorResult.ok) {
259
+ return notOk(
260
+ errorMigrationPlanningFailed({
261
+ conflicts: descriptorResult.conflicts as readonly CliErrorConflict[],
262
+ }),
263
+ );
264
+ }
265
+
266
+ if (descriptorResult.descriptors.length === 0) {
267
+ return notOk(
268
+ errorMigrationPlanningFailed({
269
+ conflicts: [
270
+ {
271
+ kind: 'unsupportedChange',
272
+ summary:
273
+ 'Contract changed but planner produced no operations. ' +
274
+ 'This indicates unsupported or ignored changes.',
275
+ },
276
+ ],
277
+ }),
278
+ );
279
+ }
280
+
281
+ // Build manifest and write migration package
282
+ const timestamp = new Date();
283
+ const slug = options.name ?? 'migration';
284
+ const dirName = formatMigrationDirName(timestamp, slug);
285
+ const packageDir = join(migrationsDir, dirName);
286
+
287
+ const manifest: MigrationManifest = {
288
+ from: fromHash,
289
+ to: toStorageHash,
290
+ migrationId: null,
291
+ kind: 'regular',
292
+ fromContract,
293
+ toContract: toContractJson,
294
+ hints: {
295
+ used: [],
296
+ applied: [],
297
+ plannerVersion: '2.0.0',
298
+ planningStrategy: 'descriptors',
299
+ },
300
+ labels: [],
301
+ createdAt: timestamp.toISOString(),
302
+ };
303
+
304
+ try {
305
+ // Always write migration.ts with the descriptors
306
+ // Write package with empty ops first (draft)
307
+ await writeMigrationPackage(packageDir, manifest, []);
308
+ await scaffoldMigrationTs(packageDir, {
309
+ descriptors: descriptorResult.descriptors,
310
+ contractJsonPath: contractPathAbsolute,
311
+ });
312
+
313
+ if (descriptorResult.needsDataMigration) {
314
+ // Draft — user must fill in dataTransform and run verify
315
+ const result: MigrationPlanResult = {
316
+ ok: true,
317
+ noOp: false,
318
+ from: fromHash,
319
+ to: toStorageHash,
320
+ dir: relative(process.cwd(), packageDir),
321
+ operations: descriptorResult.descriptors.map((d) => ({
322
+ id: (d as { kind: string }).kind,
323
+ label: (d as { kind: string }).kind,
324
+ operationClass: 'data' as const,
325
+ })),
326
+ sql: [],
327
+ summary: `Planned ${descriptorResult.descriptors.length} operation(s) — data migration required. Edit migration.ts and run \`migration verify --dir ${relative(process.cwd(), packageDir)}\` to attest.`,
328
+ timings: { total: Date.now() - startTime },
329
+ };
330
+ return ok(result);
331
+ }
332
+
333
+ // No data migration — evaluate, resolve, write ops, attest
334
+ const evaluatedDescriptors = await evaluateMigrationTs(packageDir);
335
+
336
+ if (!migrations.resolveDescriptors) {
337
+ throw errorTargetMigrationNotSupported({
338
+ why: `Target "${config.target.targetId}" does not implement resolveDescriptors; cannot finalize migration plan with migration.ts`,
339
+ });
340
+ }
341
+
342
+ const resolvedOps = migrations.resolveDescriptors(
343
+ evaluatedDescriptors as OperationDescriptor[],
344
+ {
345
+ fromContract,
346
+ toContract: toContractJson,
347
+ frameworkComponents,
348
+ },
349
+ );
350
+
351
+ await writeMigrationOps(packageDir, resolvedOps);
352
+ const migrationId = await attestMigration(packageDir);
353
+
354
+ const sql = extractSqlDdl(resolvedOps);
355
+ const result: MigrationPlanResult = {
356
+ ok: true,
357
+ noOp: false,
358
+ from: fromHash,
359
+ to: toStorageHash,
360
+ migrationId,
361
+ dir: relative(process.cwd(), packageDir),
362
+ operations: resolvedOps.map((op) => ({
363
+ id: op.id,
364
+ label: op.label,
365
+ operationClass: op.operationClass,
366
+ })),
367
+ sql,
368
+ summary: `Planned ${resolvedOps.length} operation(s)`,
369
+ timings: { total: Date.now() - startTime },
370
+ };
371
+ return ok(result);
372
+ } catch (error) {
373
+ return notOk(mapMigrationToolsError(error));
374
+ }
375
+ }
376
+
377
+ return notOk(
378
+ errorTargetMigrationNotSupported({
379
+ why: `Target "${config.target.id}" does not support planWithDescriptors`,
380
+ }),
381
+ );
382
+ }
383
+
384
+ export function createMigrationPlanCommand(): Command {
385
+ const command = new Command('plan');
386
+ setCommandDescriptions(
387
+ command,
388
+ 'Plan a migration from contract changes',
389
+ 'Compares the emitted contract against the latest on-disk migration state and\n' +
390
+ 'produces a new migration package with the required operations. No database\n' +
391
+ 'connection is needed — this is a fully offline operation.',
392
+ );
393
+ setCommandExamples(command, [
394
+ 'prisma-next migration plan',
395
+ 'prisma-next migration plan --name add-users-table',
396
+ ]);
397
+ addGlobalOptions(command)
398
+ .option('--config <path>', 'Path to prisma-next.config.ts')
399
+ .option('--name <slug>', 'Name slug for the migration directory', 'migration')
400
+ .option('--from <hash>', 'Explicit starting contract hash (overrides latest migration target)')
401
+ .action(async (options: MigrationPlanOptions) => {
402
+ const flags = parseGlobalFlags(options);
403
+ const startTime = Date.now();
404
+
405
+ const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
406
+ const result = await executeMigrationPlanCommand(options, flags, ui, startTime);
407
+
408
+ const exitCode = handleResult(result, flags, ui, (planResult) => {
409
+ if (flags.json) {
410
+ ui.output(JSON.stringify(planResult, null, 2));
411
+ } else if (!flags.quiet) {
412
+ ui.log(formatMigrationPlanOutput(planResult, flags));
413
+ }
414
+ });
415
+
416
+ process.exit(exitCode);
417
+ });
418
+
419
+ return command;
420
+ }
421
+
422
+ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
423
+ const lines: string[] = [];
424
+ const useColor = flags.color !== false;
425
+
426
+ const green_ = useColor ? (s: string) => `\x1b[32m${s}\x1b[0m` : (s: string) => s;
427
+ const yellow_ = useColor ? (s: string) => `\x1b[33m${s}\x1b[0m` : (s: string) => s;
428
+ const dim_ = useColor ? (s: string) => `\x1b[2m${s}\x1b[0m` : (s: string) => s;
429
+
430
+ if (result.noOp) {
431
+ lines.push(`${green_('✔')} No changes detected`);
432
+ lines.push(dim_(` from: ${result.from}`));
433
+ lines.push(dim_(` to: ${result.to}`));
434
+ return lines.join('\n');
435
+ }
436
+
437
+ lines.push(`${green_('✔')} ${result.summary}`);
438
+ lines.push('');
439
+
440
+ if (result.operations.length > 0) {
441
+ lines.push(dim_('│'));
442
+ for (let i = 0; i < result.operations.length; i++) {
443
+ const op = result.operations[i]!;
444
+ const isLast = i === result.operations.length - 1;
445
+ const treeChar = isLast ? '└' : '├';
446
+ const opClassLabel =
447
+ op.operationClass === 'destructive'
448
+ ? yellow_(`[${op.operationClass}]`)
449
+ : dim_(`[${op.operationClass}]`);
450
+ lines.push(`${dim_(treeChar)}─ ${op.label} ${opClassLabel}`);
451
+ }
452
+
453
+ const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
454
+ if (hasDestructive) {
455
+ lines.push('');
456
+ lines.push(
457
+ `${yellow_('⚠')} This migration contains destructive operations that may cause data loss.`,
458
+ );
459
+ }
460
+ lines.push('');
461
+ }
462
+
463
+ lines.push(dim_(`from: ${result.from}`));
464
+ lines.push(dim_(`to: ${result.to}`));
465
+ if (result.migrationId) {
466
+ lines.push(dim_(`migrationId: ${result.migrationId}`));
467
+ }
468
+ if (result.dir) {
469
+ lines.push(dim_(`dir: ${result.dir}`));
470
+ }
471
+
472
+ if (result.sql && result.sql.length > 0) {
473
+ lines.push('');
474
+ lines.push(dim_('DDL preview'));
475
+ lines.push('');
476
+ for (const statement of result.sql) {
477
+ const trimmed = statement.trim();
478
+ if (!trimmed) continue;
479
+ const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
480
+ lines.push(line);
481
+ }
482
+ }
483
+
484
+ if (flags.verbose && result.timings) {
485
+ lines.push('');
486
+ lines.push(dim_(`Total time: ${result.timings.total}ms`));
487
+ }
488
+
489
+ return lines.join('\n');
490
+ }
491
+
492
+ export type PrefixResolutionFailure =
493
+ | { reason: 'ambiguous'; count: number }
494
+ | { reason: 'not-found' };
495
+
496
+ /**
497
+ * Resolve a migration bundle by exact hash or prefix match.
498
+ *
499
+ * Tries exact match first, then prefix match (auto-prepending `sha256:` when
500
+ * the needle omits the scheme). Returns the matched bundle on success, or a
501
+ * discriminated failure indicating whether the prefix was ambiguous or simply
502
+ * not found.
503
+ *
504
+ * @internal Exported for testing only.
505
+ */
506
+ export function resolveBundleByPrefix<T extends { manifest: { to: string } }>(
507
+ bundles: readonly T[],
508
+ needle: string,
509
+ ): Result<T, PrefixResolutionFailure> {
510
+ const exact = bundles.find((p) => p.manifest.to === needle);
511
+ if (exact) return ok(exact);
512
+
513
+ const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
514
+ const candidates = bundles.filter((p) => p.manifest.to.startsWith(prefixWithScheme));
515
+
516
+ if (candidates.length === 1) return ok(candidates[0]!);
517
+ if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });
518
+ return notOk({ reason: 'not-found' });
519
+ }