@prisma-next/cli 0.3.0-dev.10 → 0.3.0-dev.113

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 (215) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +470 -134
  3. package/dist/cli-errors-ByGuoqNj.mjs +3 -0
  4. package/dist/cli-errors-D6HxRn3A.d.mts +2 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.js +1 -2350
  7. package/dist/cli.mjs +235 -0
  8. package/dist/cli.mjs.map +1 -0
  9. package/dist/client-612RJJD_.mjs +1069 -0
  10. package/dist/client-612RJJD_.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts +7 -0
  12. package/dist/commands/contract-emit.d.mts.map +1 -0
  13. package/dist/commands/contract-emit.mjs +4 -0
  14. package/dist/commands/contract-infer.d.mts +7 -0
  15. package/dist/commands/contract-infer.d.mts.map +1 -0
  16. package/dist/commands/contract-infer.mjs +4 -0
  17. package/dist/commands/db-init.d.mts +7 -0
  18. package/dist/commands/db-init.d.mts.map +1 -0
  19. package/dist/commands/db-init.mjs +124 -0
  20. package/dist/commands/db-init.mjs.map +1 -0
  21. package/dist/commands/db-schema.d.mts +7 -0
  22. package/dist/commands/db-schema.d.mts.map +1 -0
  23. package/dist/commands/db-schema.mjs +52 -0
  24. package/dist/commands/db-schema.mjs.map +1 -0
  25. package/dist/commands/db-sign.d.mts +7 -0
  26. package/dist/commands/db-sign.d.mts.map +1 -0
  27. package/dist/commands/db-sign.mjs +135 -0
  28. package/dist/commands/db-sign.mjs.map +1 -0
  29. package/dist/commands/db-update.d.mts +7 -0
  30. package/dist/commands/db-update.d.mts.map +1 -0
  31. package/dist/commands/db-update.mjs +121 -0
  32. package/dist/commands/db-update.mjs.map +1 -0
  33. package/dist/commands/db-verify.d.mts +7 -0
  34. package/dist/commands/db-verify.d.mts.map +1 -0
  35. package/dist/commands/db-verify.mjs +310 -0
  36. package/dist/commands/db-verify.mjs.map +1 -0
  37. package/dist/commands/migration-apply.d.mts +36 -0
  38. package/dist/commands/migration-apply.d.mts.map +1 -0
  39. package/dist/commands/migration-apply.mjs +240 -0
  40. package/dist/commands/migration-apply.mjs.map +1 -0
  41. package/dist/commands/migration-plan.d.mts +47 -0
  42. package/dist/commands/migration-plan.d.mts.map +1 -0
  43. package/dist/commands/migration-plan.mjs +288 -0
  44. package/dist/commands/migration-plan.mjs.map +1 -0
  45. package/dist/commands/migration-ref.d.mts +43 -0
  46. package/dist/commands/migration-ref.d.mts.map +1 -0
  47. package/dist/commands/migration-ref.mjs +194 -0
  48. package/dist/commands/migration-ref.mjs.map +1 -0
  49. package/dist/commands/migration-show.d.mts +28 -0
  50. package/dist/commands/migration-show.d.mts.map +1 -0
  51. package/dist/commands/migration-show.mjs +139 -0
  52. package/dist/commands/migration-show.mjs.map +1 -0
  53. package/dist/commands/migration-status.d.mts +85 -0
  54. package/dist/commands/migration-status.d.mts.map +1 -0
  55. package/dist/commands/migration-status.mjs +4 -0
  56. package/dist/commands/migration-verify.d.mts +16 -0
  57. package/dist/commands/migration-verify.d.mts.map +1 -0
  58. package/dist/commands/migration-verify.mjs +87 -0
  59. package/dist/commands/migration-verify.mjs.map +1 -0
  60. package/dist/config-loader-d_KF19Tw.mjs +43 -0
  61. package/dist/config-loader-d_KF19Tw.mjs.map +1 -0
  62. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  63. package/dist/config-loader.d.mts.map +1 -0
  64. package/dist/config-loader.mjs +3 -0
  65. package/dist/contract-emit-CVv7dbQ9.mjs +187 -0
  66. package/dist/contract-emit-CVv7dbQ9.mjs.map +1 -0
  67. package/dist/contract-infer-Bvw8u8Eu.mjs +83 -0
  68. package/dist/contract-infer-Bvw8u8Eu.mjs.map +1 -0
  69. package/dist/exports/config-types.d.mts +2 -0
  70. package/dist/exports/config-types.mjs +3 -0
  71. package/dist/exports/control-api.d.mts +626 -0
  72. package/dist/exports/control-api.d.mts.map +1 -0
  73. package/dist/exports/control-api.mjs +107 -0
  74. package/dist/exports/control-api.mjs.map +1 -0
  75. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  76. package/dist/exports/index.d.mts.map +1 -0
  77. package/dist/exports/index.mjs +130 -0
  78. package/dist/exports/index.mjs.map +1 -0
  79. package/dist/extract-sql-ddl-Jf5blEO0.mjs +26 -0
  80. package/dist/extract-sql-ddl-Jf5blEO0.mjs.map +1 -0
  81. package/dist/framework-components-M2j-qPfr.mjs +59 -0
  82. package/dist/framework-components-M2j-qPfr.mjs.map +1 -0
  83. package/dist/inspect-live-schema-BQe5i4YE.mjs +90 -0
  84. package/dist/inspect-live-schema-BQe5i4YE.mjs.map +1 -0
  85. package/dist/migration-command-scaffold-SLrjcKXS.mjs +104 -0
  86. package/dist/migration-command-scaffold-SLrjcKXS.mjs.map +1 -0
  87. package/dist/migration-status-DAQKsmWW.mjs +1576 -0
  88. package/dist/migration-status-DAQKsmWW.mjs.map +1 -0
  89. package/dist/migrations-Db_ea9eE.mjs +173 -0
  90. package/dist/migrations-Db_ea9eE.mjs.map +1 -0
  91. package/dist/progress-adapter-DRNe2idZ.mjs +43 -0
  92. package/dist/progress-adapter-DRNe2idZ.mjs.map +1 -0
  93. package/dist/terminal-ui-DAcMBRKf.mjs +980 -0
  94. package/dist/terminal-ui-DAcMBRKf.mjs.map +1 -0
  95. package/dist/verify-DXKxBFvU.mjs +385 -0
  96. package/dist/verify-DXKxBFvU.mjs.map +1 -0
  97. package/package.json +88 -43
  98. package/src/cli.ts +109 -58
  99. package/src/commands/contract-emit.ts +236 -143
  100. package/src/commands/contract-infer-paths.ts +32 -0
  101. package/src/commands/contract-infer.ts +131 -0
  102. package/src/commands/db-init.ts +211 -425
  103. package/src/commands/db-schema.ts +77 -0
  104. package/src/commands/db-sign.ts +207 -228
  105. package/src/commands/db-update.ts +236 -0
  106. package/src/commands/db-verify.ts +484 -186
  107. package/src/commands/inspect-live-schema.ts +171 -0
  108. package/src/commands/migration-apply.ts +416 -0
  109. package/src/commands/migration-plan.ts +451 -0
  110. package/src/commands/migration-ref.ts +305 -0
  111. package/src/commands/migration-show.ts +246 -0
  112. package/src/commands/migration-status.ts +838 -0
  113. package/src/commands/migration-verify.ts +134 -0
  114. package/src/config-loader.ts +13 -3
  115. package/src/control-api/client.ts +614 -0
  116. package/src/control-api/contract-enrichment.ts +135 -0
  117. package/src/control-api/errors.ts +9 -0
  118. package/src/control-api/operations/contract-emit.ts +173 -0
  119. package/src/control-api/operations/db-init.ts +286 -0
  120. package/src/control-api/operations/db-update.ts +221 -0
  121. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  122. package/src/control-api/operations/migration-apply.ts +194 -0
  123. package/src/control-api/operations/migration-helpers.ts +49 -0
  124. package/src/control-api/types.ts +683 -0
  125. package/src/exports/config-types.ts +4 -3
  126. package/src/exports/control-api.ts +56 -0
  127. package/src/load-ts-contract.ts +16 -11
  128. package/src/utils/cli-errors.ts +5 -2
  129. package/src/utils/command-helpers.ts +293 -3
  130. package/src/utils/formatters/emit.ts +67 -0
  131. package/src/utils/formatters/errors.ts +82 -0
  132. package/src/utils/formatters/graph-migration-mapper.ts +220 -0
  133. package/src/utils/formatters/graph-render.ts +1317 -0
  134. package/src/utils/formatters/graph-types.ts +114 -0
  135. package/src/utils/formatters/help.ts +380 -0
  136. package/src/utils/formatters/helpers.ts +28 -0
  137. package/src/utils/formatters/migrations.ts +346 -0
  138. package/src/utils/formatters/styled.ts +212 -0
  139. package/src/utils/formatters/verify.ts +620 -0
  140. package/src/utils/global-flags.ts +41 -23
  141. package/src/utils/migration-command-scaffold.ts +187 -0
  142. package/src/utils/migration-types.ts +12 -0
  143. package/src/utils/progress-adapter.ts +75 -0
  144. package/src/utils/result-handler.ts +12 -13
  145. package/src/utils/shutdown.ts +92 -0
  146. package/src/utils/suggest-command.ts +31 -0
  147. package/src/utils/terminal-ui.ts +276 -0
  148. package/dist/chunk-BZMBKEEQ.js +0 -997
  149. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  150. package/dist/chunk-CVNWLFXO.js +0 -91
  151. package/dist/chunk-CVNWLFXO.js.map +0 -1
  152. package/dist/chunk-HWYQOCAJ.js +0 -47
  153. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  154. package/dist/chunk-QUPBU4KV.js +0 -131
  155. package/dist/chunk-QUPBU4KV.js.map +0 -1
  156. package/dist/cli.d.ts +0 -2
  157. package/dist/cli.d.ts.map +0 -1
  158. package/dist/cli.js.map +0 -1
  159. package/dist/commands/contract-emit.d.ts +0 -3
  160. package/dist/commands/contract-emit.d.ts.map +0 -1
  161. package/dist/commands/contract-emit.js +0 -9
  162. package/dist/commands/contract-emit.js.map +0 -1
  163. package/dist/commands/db-init.d.ts +0 -3
  164. package/dist/commands/db-init.d.ts.map +0 -1
  165. package/dist/commands/db-init.js +0 -337
  166. package/dist/commands/db-init.js.map +0 -1
  167. package/dist/commands/db-introspect.d.ts +0 -3
  168. package/dist/commands/db-introspect.d.ts.map +0 -1
  169. package/dist/commands/db-introspect.js +0 -186
  170. package/dist/commands/db-introspect.js.map +0 -1
  171. package/dist/commands/db-schema-verify.d.ts +0 -3
  172. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  173. package/dist/commands/db-schema-verify.js +0 -160
  174. package/dist/commands/db-schema-verify.js.map +0 -1
  175. package/dist/commands/db-sign.d.ts +0 -3
  176. package/dist/commands/db-sign.d.ts.map +0 -1
  177. package/dist/commands/db-sign.js +0 -195
  178. package/dist/commands/db-sign.js.map +0 -1
  179. package/dist/commands/db-verify.d.ts +0 -3
  180. package/dist/commands/db-verify.d.ts.map +0 -1
  181. package/dist/commands/db-verify.js +0 -169
  182. package/dist/commands/db-verify.js.map +0 -1
  183. package/dist/config-loader.d.ts.map +0 -1
  184. package/dist/config-loader.js +0 -7
  185. package/dist/config-loader.js.map +0 -1
  186. package/dist/exports/config-types.d.ts +0 -3
  187. package/dist/exports/config-types.d.ts.map +0 -1
  188. package/dist/exports/config-types.js +0 -6
  189. package/dist/exports/config-types.js.map +0 -1
  190. package/dist/exports/index.d.ts +0 -4
  191. package/dist/exports/index.d.ts.map +0 -1
  192. package/dist/exports/index.js +0 -175
  193. package/dist/exports/index.js.map +0 -1
  194. package/dist/load-ts-contract.d.ts.map +0 -1
  195. package/dist/utils/action.d.ts +0 -16
  196. package/dist/utils/action.d.ts.map +0 -1
  197. package/dist/utils/cli-errors.d.ts +0 -7
  198. package/dist/utils/cli-errors.d.ts.map +0 -1
  199. package/dist/utils/command-helpers.d.ts +0 -12
  200. package/dist/utils/command-helpers.d.ts.map +0 -1
  201. package/dist/utils/framework-components.d.ts +0 -70
  202. package/dist/utils/framework-components.d.ts.map +0 -1
  203. package/dist/utils/global-flags.d.ts +0 -25
  204. package/dist/utils/global-flags.d.ts.map +0 -1
  205. package/dist/utils/output.d.ts +0 -142
  206. package/dist/utils/output.d.ts.map +0 -1
  207. package/dist/utils/result-handler.d.ts +0 -15
  208. package/dist/utils/result-handler.d.ts.map +0 -1
  209. package/dist/utils/spinner.d.ts +0 -29
  210. package/dist/utils/spinner.d.ts.map +0 -1
  211. package/src/commands/db-introspect.ts +0 -256
  212. package/src/commands/db-schema-verify.ts +0 -232
  213. package/src/utils/action.ts +0 -43
  214. package/src/utils/output.ts +0 -1471
  215. package/src/utils/spinner.ts +0 -67
@@ -0,0 +1,451 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { ContractIR } from '@prisma-next/contract/ir';
3
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/core-control-plane/constants';
4
+ import { createControlPlaneStack } from '@prisma-next/core-control-plane/stack';
5
+ import { attestMigration } from '@prisma-next/migration-tools/attestation';
6
+ import { findLatestMigration } from '@prisma-next/migration-tools/dag';
7
+ import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
8
+ import { type MigrationManifest, MigrationToolsError } from '@prisma-next/migration-tools/types';
9
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
10
+ import { Command } from 'commander';
11
+ import { join, relative } from 'pathe';
12
+ import { loadConfig } from '../config-loader';
13
+ import { extractSqlDdl } from '../control-api/operations/extract-sql-ddl';
14
+ import {
15
+ type CliErrorConflict,
16
+ type CliStructuredError,
17
+ errorContractValidationFailed,
18
+ errorFileNotFound,
19
+ errorMigrationPlanningFailed,
20
+ errorRuntime,
21
+ errorTargetMigrationNotSupported,
22
+ errorUnexpected,
23
+ } from '../utils/cli-errors';
24
+ import {
25
+ addGlobalOptions,
26
+ getTargetMigrations,
27
+ loadMigrationBundles,
28
+ resolveContractPath,
29
+ resolveMigrationPaths,
30
+ setCommandDescriptions,
31
+ setCommandExamples,
32
+ } from '../utils/command-helpers';
33
+ import { formatStyledHeader } from '../utils/formatters/styled';
34
+ import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
35
+ import type { CommonCommandOptions } from '../utils/global-flags';
36
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
37
+ import { handleResult } from '../utils/result-handler';
38
+ import { TerminalUI } from '../utils/terminal-ui';
39
+
40
+ interface MigrationPlanOptions extends CommonCommandOptions {
41
+ readonly config?: string;
42
+ readonly name?: string;
43
+ readonly from?: string;
44
+ }
45
+
46
+ export interface MigrationPlanResult {
47
+ readonly ok: boolean;
48
+ readonly noOp: boolean;
49
+ readonly from: string;
50
+ readonly to: string;
51
+ readonly migrationId?: string;
52
+ readonly dir?: string;
53
+ readonly operations: readonly {
54
+ readonly id: string;
55
+ readonly label: string;
56
+ readonly operationClass: string;
57
+ }[];
58
+ readonly sql?: readonly string[];
59
+ readonly summary: string;
60
+ readonly timings: {
61
+ readonly total: number;
62
+ };
63
+ }
64
+
65
+ function mapMigrationToolsError(error: unknown): CliStructuredError {
66
+ if (MigrationToolsError.is(error)) {
67
+ return errorRuntime(error.message, {
68
+ why: error.why,
69
+ fix: error.fix,
70
+ meta: { code: error.code, ...(error.details ?? {}) },
71
+ });
72
+ }
73
+ return errorUnexpected(error instanceof Error ? error.message : String(error), {
74
+ why: `Unexpected error during migration plan: ${error instanceof Error ? error.message : String(error)}`,
75
+ });
76
+ }
77
+
78
+ async function executeMigrationPlanCommand(
79
+ options: MigrationPlanOptions,
80
+ flags: GlobalFlags,
81
+ ui: TerminalUI,
82
+ startTime: number,
83
+ ): Promise<Result<MigrationPlanResult, CliStructuredError>> {
84
+ const config = await loadConfig(options.config);
85
+ const { configPath, migrationsDir, migrationsRelative } = resolveMigrationPaths(
86
+ options.config,
87
+ config,
88
+ );
89
+
90
+ const contractPathAbsolute = resolveContractPath(config);
91
+ const contractPath = relative(process.cwd(), contractPathAbsolute);
92
+
93
+ if (!flags.json && !flags.quiet) {
94
+ const details: Array<{ label: string; value: string }> = [
95
+ { label: 'config', value: configPath },
96
+ { label: 'contract', value: contractPath },
97
+ { label: 'migrations', value: migrationsRelative },
98
+ ];
99
+ if (options.from) {
100
+ details.push({ label: 'from', value: options.from });
101
+ }
102
+ if (options.name) {
103
+ details.push({ label: 'name', value: options.name });
104
+ }
105
+ const header = formatStyledHeader({
106
+ command: 'migration plan',
107
+ description: 'Plan a migration from contract changes',
108
+ url: 'https://pris.ly/migration-plan',
109
+ details,
110
+ flags,
111
+ });
112
+ ui.stderr(header);
113
+ }
114
+
115
+ // Load contract file (the "to" contract)
116
+ let contractJsonContent: string;
117
+ try {
118
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
119
+ } catch (error) {
120
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
121
+ return notOk(
122
+ errorFileNotFound(contractPathAbsolute, {
123
+ why: `Contract file not found at ${contractPathAbsolute}`,
124
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
125
+ }),
126
+ );
127
+ }
128
+ return notOk(
129
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
130
+ why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
131
+ }),
132
+ );
133
+ }
134
+
135
+ let toContractJson: ContractIR;
136
+ try {
137
+ toContractJson = JSON.parse(contractJsonContent) as ContractIR;
138
+ } catch (error) {
139
+ return notOk(
140
+ errorContractValidationFailed(
141
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
142
+ { where: { path: contractPathAbsolute } },
143
+ ),
144
+ );
145
+ }
146
+
147
+ const toStorageHash = (toContractJson as unknown as Record<string, unknown>)['storageHash'] as
148
+ | string
149
+ | undefined;
150
+ if (!toStorageHash) {
151
+ return notOk(
152
+ errorContractValidationFailed('Contract is missing storageHash', {
153
+ where: { path: contractPathAbsolute },
154
+ }),
155
+ );
156
+ }
157
+
158
+ // Read existing migrations and determine "from" contract
159
+ let fromContract: ContractIR | null = null;
160
+ let fromHash: string = EMPTY_CONTRACT_HASH;
161
+
162
+ try {
163
+ const { bundles, graph } = await loadMigrationBundles(migrationsDir);
164
+
165
+ if (options.from) {
166
+ const resolved = resolveBundleByPrefix(bundles, options.from);
167
+ if (!resolved.ok) {
168
+ const f = resolved.failure;
169
+ return notOk(
170
+ f.reason === 'ambiguous'
171
+ ? errorRuntime('Multiple matching migrations found', {
172
+ why: `Prefix "${options.from}" matches ${f.count} migrations in ${migrationsRelative}`,
173
+ fix: 'Provide a longer prefix to disambiguate, or omit --from to use the latest migration leaf.',
174
+ })
175
+ : errorRuntime('Starting contract not found', {
176
+ why: `No migration with to hash matching "${options.from}" exists in ${migrationsRelative}`,
177
+ fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration leaf.',
178
+ }),
179
+ );
180
+ }
181
+ fromHash = resolved.value.manifest.to;
182
+ fromContract = resolved.value.manifest.toContract;
183
+ } else {
184
+ const latestMigration = findLatestMigration(graph);
185
+ if (latestMigration) {
186
+ fromHash = latestMigration.to;
187
+ const leafPkg = bundles.find((p) => p.manifest.migrationId === latestMigration.migrationId);
188
+ if (leafPkg) {
189
+ fromContract = leafPkg.manifest.toContract;
190
+ }
191
+ }
192
+ }
193
+ } catch (error) {
194
+ if (MigrationToolsError.is(error)) {
195
+ return notOk(mapMigrationToolsError(error));
196
+ }
197
+ throw error;
198
+ }
199
+
200
+ // Check for no-op (same hash means no changes)
201
+ if (fromHash === toStorageHash) {
202
+ const result: MigrationPlanResult = {
203
+ ok: true,
204
+ noOp: true,
205
+ from: fromHash,
206
+ to: toStorageHash,
207
+ operations: [],
208
+ summary: 'No changes detected between contracts',
209
+ timings: { total: Date.now() - startTime },
210
+ };
211
+ return ok(result);
212
+ }
213
+
214
+ // Check target supports migrations
215
+ const migrations = getTargetMigrations(config.target);
216
+ if (!migrations) {
217
+ return notOk(
218
+ errorTargetMigrationNotSupported({
219
+ why: `Target "${config.target.id}" does not support migrations`,
220
+ }),
221
+ );
222
+ }
223
+ const stack = createControlPlaneStack({
224
+ target: config.target,
225
+ adapter: config.adapter,
226
+ extensionPacks: config.extensionPacks ?? [],
227
+ });
228
+ const familyInstance = config.family.create(stack);
229
+ const frameworkComponents = assertFrameworkComponentsCompatible(
230
+ config.family.familyId,
231
+ config.target.targetId,
232
+ [config.target, config.adapter, ...(config.extensionPacks ?? [])],
233
+ );
234
+ const planner = migrations.createPlanner(familyInstance);
235
+ const fromSchemaIR = migrations.contractToSchema(fromContract, frameworkComponents);
236
+ const plannerResult = planner.plan({
237
+ contract: toContractJson,
238
+ schema: fromSchemaIR,
239
+ policy: { allowedOperationClasses: ['additive', 'widening', 'destructive'] },
240
+ frameworkComponents,
241
+ });
242
+
243
+ if (plannerResult.kind === 'failure') {
244
+ return notOk(
245
+ errorMigrationPlanningFailed({
246
+ conflicts: plannerResult.conflicts as readonly CliErrorConflict[],
247
+ }),
248
+ );
249
+ }
250
+
251
+ if (plannerResult.plan.operations.length === 0) {
252
+ return notOk(
253
+ errorMigrationPlanningFailed({
254
+ conflicts: [
255
+ {
256
+ kind: 'unsupportedChange',
257
+ summary:
258
+ 'Contract changed but planner produced no operations. ' +
259
+ 'This indicates unsupported or ignored changes (e.g. removals, type changes, or a planner/contract mismatch).',
260
+ },
261
+ ],
262
+ }),
263
+ );
264
+ }
265
+
266
+ // Build manifest and write migration package
267
+ const timestamp = new Date();
268
+ const slug = options.name ?? 'migration';
269
+ const dirName = formatMigrationDirName(timestamp, slug);
270
+ const packageDir = join(migrationsDir, dirName);
271
+
272
+ const manifest: MigrationManifest = {
273
+ from: fromHash,
274
+ to: toStorageHash,
275
+ migrationId: null,
276
+ kind: 'regular',
277
+ fromContract,
278
+ toContract: toContractJson,
279
+ hints: {
280
+ used: [],
281
+ applied: [],
282
+ plannerVersion: '1.0.0',
283
+ planningStrategy: 'diff',
284
+ },
285
+ labels: [],
286
+ createdAt: timestamp.toISOString(),
287
+ };
288
+
289
+ try {
290
+ await writeMigrationPackage(packageDir, manifest, plannerResult.plan.operations);
291
+ const migrationId = await attestMigration(packageDir);
292
+
293
+ const sql = extractSqlDdl(plannerResult.plan.operations);
294
+ const result: MigrationPlanResult = {
295
+ ok: true,
296
+ noOp: false,
297
+ from: fromHash,
298
+ to: toStorageHash,
299
+ migrationId,
300
+ dir: relative(process.cwd(), packageDir),
301
+ operations: plannerResult.plan.operations.map((op) => ({
302
+ id: op.id,
303
+ label: op.label,
304
+ operationClass: op.operationClass,
305
+ })),
306
+ sql,
307
+ summary: `Planned ${plannerResult.plan.operations.length} operation(s)`,
308
+ timings: { total: Date.now() - startTime },
309
+ };
310
+ return ok(result);
311
+ } catch (error) {
312
+ return notOk(mapMigrationToolsError(error));
313
+ }
314
+ }
315
+
316
+ export function createMigrationPlanCommand(): Command {
317
+ const command = new Command('plan');
318
+ setCommandDescriptions(
319
+ command,
320
+ 'Plan a migration from contract changes',
321
+ 'Compares the emitted contract against the latest on-disk migration state and\n' +
322
+ 'produces a new migration package with the required operations. No database\n' +
323
+ 'connection is needed — this is a fully offline operation.',
324
+ );
325
+ setCommandExamples(command, [
326
+ 'prisma-next migration plan',
327
+ 'prisma-next migration plan --name add-users-table',
328
+ ]);
329
+ addGlobalOptions(command)
330
+ .option('--config <path>', 'Path to prisma-next.config.ts')
331
+ .option('--name <slug>', 'Name slug for the migration directory', 'migration')
332
+ .option('--from <hash>', 'Explicit starting contract hash (overrides migration chain leaf)')
333
+ .action(async (options: MigrationPlanOptions) => {
334
+ const flags = parseGlobalFlags(options);
335
+ const startTime = Date.now();
336
+
337
+ const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
338
+ const result = await executeMigrationPlanCommand(options, flags, ui, startTime);
339
+
340
+ const exitCode = handleResult(result, flags, ui, (planResult) => {
341
+ if (flags.json) {
342
+ ui.output(JSON.stringify(planResult, null, 2));
343
+ } else if (!flags.quiet) {
344
+ ui.log(formatMigrationPlanOutput(planResult, flags));
345
+ }
346
+ });
347
+
348
+ process.exit(exitCode);
349
+ });
350
+
351
+ return command;
352
+ }
353
+
354
+ function formatMigrationPlanOutput(result: MigrationPlanResult, flags: GlobalFlags): string {
355
+ const lines: string[] = [];
356
+ const useColor = flags.color !== false;
357
+
358
+ const green_ = useColor ? (s: string) => `\x1b[32m${s}\x1b[0m` : (s: string) => s;
359
+ const yellow_ = useColor ? (s: string) => `\x1b[33m${s}\x1b[0m` : (s: string) => s;
360
+ const dim_ = useColor ? (s: string) => `\x1b[2m${s}\x1b[0m` : (s: string) => s;
361
+
362
+ if (result.noOp) {
363
+ lines.push(`${green_('✔')} No changes detected`);
364
+ lines.push(dim_(` from: ${result.from}`));
365
+ lines.push(dim_(` to: ${result.to}`));
366
+ return lines.join('\n');
367
+ }
368
+
369
+ lines.push(`${green_('✔')} ${result.summary}`);
370
+ lines.push('');
371
+
372
+ if (result.operations.length > 0) {
373
+ lines.push(dim_('│'));
374
+ for (let i = 0; i < result.operations.length; i++) {
375
+ const op = result.operations[i]!;
376
+ const isLast = i === result.operations.length - 1;
377
+ const treeChar = isLast ? '└' : '├';
378
+ const opClassLabel =
379
+ op.operationClass === 'destructive'
380
+ ? yellow_(`[${op.operationClass}]`)
381
+ : dim_(`[${op.operationClass}]`);
382
+ lines.push(`${dim_(treeChar)}─ ${op.label} ${opClassLabel}`);
383
+ }
384
+
385
+ const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
386
+ if (hasDestructive) {
387
+ lines.push('');
388
+ lines.push(
389
+ `${yellow_('⚠')} This migration contains destructive operations that may cause data loss.`,
390
+ );
391
+ }
392
+ lines.push('');
393
+ }
394
+
395
+ lines.push(dim_(`from: ${result.from}`));
396
+ lines.push(dim_(`to: ${result.to}`));
397
+ if (result.migrationId) {
398
+ lines.push(dim_(`migrationId: ${result.migrationId}`));
399
+ }
400
+ if (result.dir) {
401
+ lines.push(dim_(`dir: ${result.dir}`));
402
+ }
403
+
404
+ if (result.sql && result.sql.length > 0) {
405
+ lines.push('');
406
+ lines.push(dim_('DDL preview'));
407
+ lines.push('');
408
+ for (const statement of result.sql) {
409
+ const trimmed = statement.trim();
410
+ if (!trimmed) continue;
411
+ const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
412
+ lines.push(line);
413
+ }
414
+ }
415
+
416
+ if (flags.verbose && result.timings) {
417
+ lines.push('');
418
+ lines.push(dim_(`Total time: ${result.timings.total}ms`));
419
+ }
420
+
421
+ return lines.join('\n');
422
+ }
423
+
424
+ export type PrefixResolutionFailure =
425
+ | { reason: 'ambiguous'; count: number }
426
+ | { reason: 'not-found' };
427
+
428
+ /**
429
+ * Resolve a migration bundle by exact hash or prefix match.
430
+ *
431
+ * Tries exact match first, then prefix match (auto-prepending `sha256:` when
432
+ * the needle omits the scheme). Returns the matched bundle on success, or a
433
+ * discriminated failure indicating whether the prefix was ambiguous or simply
434
+ * not found.
435
+ *
436
+ * @internal Exported for testing only.
437
+ */
438
+ export function resolveBundleByPrefix<T extends { manifest: { to: string } }>(
439
+ bundles: readonly T[],
440
+ needle: string,
441
+ ): Result<T, PrefixResolutionFailure> {
442
+ const exact = bundles.find((p) => p.manifest.to === needle);
443
+ if (exact) return ok(exact);
444
+
445
+ const prefixWithScheme = needle.startsWith('sha256:') ? needle : `sha256:${needle}`;
446
+ const candidates = bundles.filter((p) => p.manifest.to.startsWith(prefixWithScheme));
447
+
448
+ if (candidates.length === 1) return ok(candidates[0]!);
449
+ if (candidates.length > 1) return notOk({ reason: 'ambiguous', count: candidates.length });
450
+ return notOk({ reason: 'not-found' });
451
+ }