@prisma-next/cli 0.4.1 → 0.4.3

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 (170) hide show
  1. package/README.md +56 -26
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/cli-errors-DDeVsP2Y.d.mts +5 -0
  7. package/dist/cli.mjs +131 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-DGKrciLM.mjs → client-1JqqkiC7.mjs} +45 -20
  10. package/dist/client-1JqqkiC7.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +7 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +8 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +11 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +8 -5
  19. package/dist/commands/db-schema.mjs.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -9
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.mjs +10 -9
  25. package/dist/commands/db-verify.mjs.map +1 -1
  26. package/dist/commands/migration-apply.d.mts +5 -2
  27. package/dist/commands/migration-apply.d.mts.map +1 -1
  28. package/dist/commands/migration-apply.mjs +57 -57
  29. package/dist/commands/migration-apply.mjs.map +1 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +26 -32
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts +14 -5
  34. package/dist/commands/migration-plan.d.mts.map +1 -1
  35. package/dist/commands/migration-plan.mjs +45 -48
  36. package/dist/commands/migration-plan.mjs.map +1 -1
  37. package/dist/commands/migration-ref.d.mts +6 -4
  38. package/dist/commands/migration-ref.d.mts.map +1 -1
  39. package/dist/commands/migration-ref.mjs +31 -40
  40. package/dist/commands/migration-ref.mjs.map +1 -1
  41. package/dist/commands/migration-show.d.mts +13 -7
  42. package/dist/commands/migration-show.d.mts.map +1 -1
  43. package/dist/commands/migration-show.mjs +28 -29
  44. package/dist/commands/migration-show.mjs.map +1 -1
  45. package/dist/commands/migration-status.d.mts +23 -5
  46. package/dist/commands/migration-status.d.mts.map +1 -1
  47. package/dist/commands/migration-status.mjs +8 -3
  48. package/dist/{config-loader-_xQZsw0i.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  49. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  50. package/dist/config-loader.mjs +1 -1
  51. package/dist/contract-emit-DS5NzZh2.mjs +6 -0
  52. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  53. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  54. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  55. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  56. package/dist/{contract-enrichment-BV4KpbNW.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  57. package/dist/{contract-enrichment-BV4KpbNW.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  58. package/dist/{contract-infer-CUbiWGX0.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  59. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  60. package/dist/exports/control-api.d.mts +86 -21
  61. package/dist/exports/control-api.d.mts.map +1 -1
  62. package/dist/exports/control-api.mjs +7 -5
  63. package/dist/exports/index.mjs +8 -3
  64. package/dist/exports/index.mjs.map +1 -1
  65. package/dist/exports/init-output.d.mts +39 -0
  66. package/dist/exports/init-output.d.mts.map +1 -0
  67. package/dist/exports/init-output.mjs +3 -0
  68. package/dist/{framework-components-B__p--vT.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  69. package/dist/{framework-components-B__p--vT.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  70. package/dist/init-DAbQMxIR.mjs +2062 -0
  71. package/dist/init-DAbQMxIR.mjs.map +1 -0
  72. package/dist/{inspect-live-schema-wIYBTdL3.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  73. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  74. package/dist/migration-cli.d.mts +80 -0
  75. package/dist/migration-cli.d.mts.map +1 -0
  76. package/dist/migration-cli.mjs +408 -0
  77. package/dist/migration-cli.mjs.map +1 -0
  78. package/dist/{migration-command-scaffold-BC73xQSo.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  79. package/dist/{migration-command-scaffold-BC73xQSo.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  80. package/dist/{migration-status-CXBbScH5.mjs → migration-status-DoPrFIOQ.mjs} +119 -64
  81. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  82. package/dist/{migrations-DYRAjiVh.mjs → migrations-MEoKMiV5.mjs} +42 -21
  83. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  84. package/dist/output-BpcQrnnq.mjs +103 -0
  85. package/dist/output-BpcQrnnq.mjs.map +1 -0
  86. package/dist/{progress-adapter-Bwouy73-.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  87. package/dist/{progress-adapter-Bwouy73-.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  88. package/dist/quick-reference-mongo.md +34 -13
  89. package/dist/quick-reference-postgres.md +11 -9
  90. package/dist/{result-handler-CGohaH1o.mjs → result-handler-Ch6hVnOo.mjs} +36 -94
  91. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  92. package/dist/{terminal-ui-BuPXVRFY.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  93. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  94. package/dist/{verify-Cm2UFuZA.mjs → verify-BT9tgCOH.mjs} +2 -2
  95. package/dist/{verify-Cm2UFuZA.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  96. package/package.json +27 -17
  97. package/src/cli.ts +32 -6
  98. package/src/commands/contract-emit.ts +67 -163
  99. package/src/commands/contract-infer.ts +7 -20
  100. package/src/commands/db-init.ts +1 -0
  101. package/src/commands/db-update.ts +1 -1
  102. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  103. package/src/commands/init/errors.ts +254 -0
  104. package/src/commands/init/exit-codes.ts +62 -0
  105. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  106. package/src/commands/init/hygiene-gitignore.ts +48 -0
  107. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  108. package/src/commands/init/index.ts +112 -7
  109. package/src/commands/init/init.ts +766 -144
  110. package/src/commands/init/inputs.ts +421 -0
  111. package/src/commands/init/output.ts +147 -0
  112. package/src/commands/init/probe-db.ts +308 -0
  113. package/src/commands/init/reinit-cleanup.ts +83 -0
  114. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  115. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  116. package/src/commands/init/templates/agent-skill.ts +25 -3
  117. package/src/commands/init/templates/code-templates.ts +125 -32
  118. package/src/commands/init/templates/env.ts +80 -0
  119. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  120. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  121. package/src/commands/init/templates/quick-reference.ts +42 -3
  122. package/src/commands/init/templates/tsconfig.ts +167 -5
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +86 -65
  125. package/src/commands/migration-new.ts +28 -34
  126. package/src/commands/migration-plan.ts +80 -56
  127. package/src/commands/migration-ref.ts +40 -54
  128. package/src/commands/migration-show.ts +53 -36
  129. package/src/commands/migration-status.ts +202 -71
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +21 -0
  132. package/src/control-api/operations/contract-emit.ts +198 -115
  133. package/src/control-api/operations/db-init.ts +10 -6
  134. package/src/control-api/operations/db-update.ts +10 -6
  135. package/src/control-api/operations/migration-apply.ts +30 -9
  136. package/src/control-api/types.ts +69 -7
  137. package/src/exports/control-api.ts +2 -1
  138. package/src/exports/init-output.ts +10 -0
  139. package/src/migration-cli.ts +577 -0
  140. package/src/utils/cli-errors.ts +50 -2
  141. package/src/utils/command-helpers.ts +48 -26
  142. package/src/utils/emit-queue.ts +26 -0
  143. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  144. package/src/utils/formatters/migrations.ts +62 -26
  145. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  146. package/dist/cli-errors-CznZA5-d.mjs +0 -5
  147. package/dist/cli-errors-z37sV3eR.d.mts +0 -4
  148. package/dist/client-DGKrciLM.mjs.map +0 -1
  149. package/dist/config-loader-_xQZsw0i.mjs.map +0 -1
  150. package/dist/contract-emit-304WZtZJ.mjs +0 -4
  151. package/dist/contract-emit-DgeWdonT.mjs +0 -122
  152. package/dist/contract-emit-DgeWdonT.mjs.map +0 -1
  153. package/dist/contract-emit-mU1_B_m9.mjs +0 -195
  154. package/dist/contract-emit-mU1_B_m9.mjs.map +0 -1
  155. package/dist/contract-infer-CUbiWGX0.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DWWFz1PK.mjs +0 -13
  157. package/dist/extract-operation-statements-DWWFz1PK.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-7zn_AFS8.mjs +0 -26
  159. package/dist/extract-sql-ddl-7zn_AFS8.mjs.map +0 -1
  160. package/dist/init-DRquYpPa.mjs +0 -430
  161. package/dist/init-DRquYpPa.mjs.map +0 -1
  162. package/dist/inspect-live-schema-wIYBTdL3.mjs.map +0 -1
  163. package/dist/migration-status-CXBbScH5.mjs.map +0 -1
  164. package/dist/migrations-DYRAjiVh.mjs.map +0 -1
  165. package/dist/result-handler-CGohaH1o.mjs.map +0 -1
  166. package/dist/terminal-ui-BuPXVRFY.mjs.map +0 -1
  167. package/dist/validate-contract-deps-DZqv9m7H.mjs +0 -37
  168. package/dist/validate-contract-deps-DZqv9m7H.mjs.map +0 -1
  169. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  170. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -0,0 +1,577 @@
1
+ /**
2
+ * The migration-file CLI interface: the actor invoked when the author runs
3
+ * `node migration.ts` directly.
4
+ *
5
+ * Naming: this is *not* a "migration runner" in the apply-time sense. The
6
+ * apply-time runner is the thing `prisma-next migration apply` uses to
7
+ * execute migration JSON ops against a database. `MigrationCLI` is the
8
+ * tiny CLI surface owned by an authored `migration.ts` file: parse the
9
+ * file's argv, load the project's `prisma-next.config.ts`, assemble a
10
+ * `ControlStack`, instantiate the migration class, and serialize.
11
+ *
12
+ * The user authors a migration class, then calls
13
+ * `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope
14
+ * after the class definition. When the file is invoked as a node
15
+ * entrypoint (`node migration.ts`), the CLI:
16
+ *
17
+ * 1. Detects whether the file is the direct entrypoint (no-op when imported).
18
+ * 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`) via
19
+ * [clipanion](https://github.com/arcanis/clipanion).
20
+ * 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`
21
+ * the CLI commands use, walking up from the migration file's directory.
22
+ * 4. Probe-instantiates the migration class without a stack so it can read
23
+ * `targetId` and verify it matches `config.target.targetId`
24
+ * (`PN-MIG-2006` on mismatch) before any stack-driven adapter
25
+ * construction runs.
26
+ * 5. Assembles a `ControlStack` from the loaded config descriptors and
27
+ * constructs the migration with that stack.
28
+ * 6. Reads any previously-scaffolded `migration.json`, then calls
29
+ * `buildMigrationArtifacts` from `@prisma-next/migration-tools` to
30
+ * produce in-memory `ops.json` + `migration.json` content. Persists
31
+ * the result to disk (or prints in dry-run mode).
32
+ *
33
+ * File I/O lives here, in `@prisma-next/cli`: this is the only place
34
+ * that legitimately combines config loading, stack assembly, and
35
+ * on-disk persistence. `@prisma-next/migration-tools` owns the pure
36
+ * conversion from a `Migration` instance to artifact strings; `Migration`
37
+ * stays a pure abstract class.
38
+ *
39
+ * Parser library: clipanion (chosen over Commander/citty/`node:util.parseArgs`
40
+ * for its in-process testability and runtime-agnostic execution surface; see
41
+ * `docs/architecture docs/research/commander-friction-points.md` for the
42
+ * evaluation rubric and the durable rationale that drove the choice).
43
+ */
44
+
45
+ import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
46
+ import type { Writable } from 'node:stream';
47
+ import { fileURLToPath } from 'node:url';
48
+ import {
49
+ CliStructuredError,
50
+ errorMigrationCliInvalidConfigArg,
51
+ errorMigrationCliUnknownFlag,
52
+ } from '@prisma-next/errors/control';
53
+ import { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';
54
+ import { createControlStack } from '@prisma-next/framework-components/control';
55
+ import { errorInvalidJson, MigrationToolsError } from '@prisma-next/migration-tools/errors';
56
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
57
+ import { buildMigrationArtifacts, type Migration } from '@prisma-next/migration-tools/migration';
58
+ import { Cli, Command, Option, UsageError } from 'clipanion';
59
+ import { dirname, join } from 'pathe';
60
+ import { loadConfig } from './config-loader';
61
+
62
+ /**
63
+ * Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses
64
+ * accept an optional `ControlStack` in their constructor (each subclass
65
+ * narrows the stack to its own family/target generics); the CLI always
66
+ * passes one assembled from the loaded config. We use a rest-args `any[]`
67
+ * constructor signature so that subclass constructors with narrower
68
+ * parameter types remain assignable - constructor type compatibility in
69
+ * TS is contravariant in the parameter, and a wider `unknown` parameter
70
+ * on the alias side would reject any narrower subclass signature.
71
+ *
72
+ * The CLI only ever passes one argument (`new MigrationClass(stack)`);
73
+ * the rest-arity is purely a type-compatibility concession for subclass
74
+ * constructors that declare narrower parameter types, not an extension
75
+ * point for additional construction arguments.
76
+ */
77
+ // biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures
78
+ export type MigrationConstructor = new (...args: any[]) => Migration;
79
+
80
+ /**
81
+ * Stream surface accepted by `MigrationCLI.run`'s `options.stdout` /
82
+ * `options.stderr`. Aliases node's `Writable` because clipanion's
83
+ * `BaseContext.stdout`/`stderr` are typed as `Writable`, and the CLI
84
+ * forwards the injected streams into clipanion's context.
85
+ *
86
+ * `process.stdout` and `process.stderr` are `Writable`-shaped, so the
87
+ * default-fallback path remains a no-op for existing two-argument
88
+ * callers like `MigrationCLI.run(import.meta.url, MyMigration)`.
89
+ *
90
+ * Tests inject a `Writable` subclass that captures chunks for
91
+ * assertions.
92
+ */
93
+ export type MigrationCliWritable = Writable;
94
+
95
+ /**
96
+ * Flags exposed by the migration-file CLI.
97
+ *
98
+ * Must stay in sync with the `Option` declarations on
99
+ * `MigrationFileCommand` below. This list is rendered in the
100
+ * `errorMigrationCliUnknownFlag` envelope's `fix` text and `meta`,
101
+ * so order matters for user-visible output (declaration order is the
102
+ * order users see when they run `--help`).
103
+ */
104
+ const KNOWN_FLAGS: readonly string[] = ['--help', '--dry-run', '--config'];
105
+
106
+ /**
107
+ * The clipanion command that owns the migration-file CLI's option
108
+ * declarations. The class is internal — `MigrationCLI.run` is the
109
+ * stable public surface. Adding a flag here automatically updates
110
+ * `--help` rendering and the `KNOWN_FLAGS` list (the latter must be
111
+ * updated in tandem).
112
+ */
113
+ class MigrationFileCommand extends Command {
114
+ static override paths = [Command.Default];
115
+
116
+ static override usage = Command.Usage({
117
+ description: 'Self-emit ops.json and migration.json from a class-flow migration',
118
+ details: `
119
+ Loads the project's prisma-next.config.ts, assembles a ControlStack
120
+ from the configured target/adapter/extensions, and serializes the
121
+ migration's operations + metadata next to this file.
122
+ `,
123
+ examples: [
124
+ ['Self-emit ops.json + migration.json next to migration.ts', '$0'],
125
+ ['Preview without writing files', '$0 --dry-run'],
126
+ ['Use a non-default config path', '$0 --config ./custom.config.ts'],
127
+ ],
128
+ });
129
+
130
+ dryRun = Option.Boolean('--dry-run', false, {
131
+ description: 'Print operations to stdout without writing files',
132
+ });
133
+
134
+ config = Option.String('--config', {
135
+ description: 'Path to prisma-next.config.ts',
136
+ });
137
+
138
+ /**
139
+ * Unused: orchestration runs inside `MigrationCLI.run` so error
140
+ * routing stays under our control (clipanion's `cli.run` writes
141
+ * error output to `context.stdout`, but our contract requires
142
+ * structured errors on stderr). `cli.process` is used to parse
143
+ * argv into a populated `MigrationFileCommand` instance whose
144
+ * fields drive the orchestration directly.
145
+ */
146
+ override async execute(): Promise<number> {
147
+ return 0;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * The CLI surface invoked by an authored `migration.ts` file. Exposed as
153
+ * a class with a static `run` method (rather than a free function) to
154
+ * give the concept a stable identity in the ubiquitous language: this is
155
+ * the "migration-file CLI", distinct from the apply-time runner that
156
+ * executes migration JSON ops.
157
+ *
158
+ * Currently a single static method. Future surface (e.g. a programmatic
159
+ * `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can
160
+ * land here without changing the import shape used by every authored
161
+ * migration.
162
+ */
163
+ // biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.
164
+ export class MigrationCLI {
165
+ /**
166
+ * Orchestrates a class-flow `migration.ts` script run.
167
+ *
168
+ * The third argument is the in-process testability surface: callers
169
+ * (and tests) may inject `argv`, `stdout`, and `stderr` instead of
170
+ * relying on `process.argv` / `process.stdout` / `process.stderr`.
171
+ * Each option defaults to its `process` global when omitted, so
172
+ * existing two-argument call sites
173
+ * (`MigrationCLI.run(import.meta.url, MyMigration)`) continue to
174
+ * compile and behave identically.
175
+ *
176
+ * Returns the exit code so the caller can branch on it. Also writes
177
+ * the same code to `process.exitCode` so script-style callers that
178
+ * don't await the return value still surface a non-zero exit when
179
+ * something fails.
180
+ *
181
+ * Exit codes:
182
+ * - 0 — success, or `--help`, or imported-not-entrypoint no-op.
183
+ * - 1 — runtime/orchestration error (config not found, target
184
+ * mismatch, etc.).
185
+ * - 2 — usage error (unknown flag, malformed `--config`). Aligns
186
+ * with `docs/CLI Style Guide.md` § Exit Codes.
187
+ */
188
+ static async run(
189
+ importMetaUrl: string,
190
+ MigrationClass: MigrationConstructor,
191
+ options: {
192
+ readonly argv?: readonly string[];
193
+ readonly stdout?: MigrationCliWritable;
194
+ readonly stderr?: MigrationCliWritable;
195
+ } = {},
196
+ ): Promise<number> {
197
+ if (!importMetaUrl) {
198
+ return 0;
199
+ }
200
+
201
+ const argv = options.argv ?? process.argv;
202
+ const stdout = options.stdout ?? process.stdout;
203
+ const stderr = options.stderr ?? process.stderr;
204
+
205
+ if (!isDirectEntrypoint(importMetaUrl, argv)) {
206
+ return 0;
207
+ }
208
+
209
+ const exitCode = await orchestrate(importMetaUrl, MigrationClass, {
210
+ argv,
211
+ stdout,
212
+ stderr,
213
+ });
214
+ // Preserve any pre-existing non-zero `process.exitCode` set by code
215
+ // running alongside `MigrationCLI.run` (an unhandled rejection
216
+ // upstream, an explicit `process.exitCode = N` from another
217
+ // module). Overwriting it with our success would mask the upstream
218
+ // failure for script-style callers that don't await the return
219
+ // value. Failures we return here are still surfaced — non-zero
220
+ // codes always win over the prior status — but successes never
221
+ // clear it.
222
+ if (exitCode !== 0 || !process.exitCode) {
223
+ process.exitCode = exitCode;
224
+ }
225
+ return exitCode;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Argv-aware variant of the entrypoint guard. The shared
231
+ * `@prisma-next/migration-tools` helper of the same name reads
232
+ * `process.argv[1]` directly, which doesn't compose with the new
233
+ * in-process testability surface (tests inject `argv` without mutating
234
+ * the process global). Inlined here so the migration-file CLI's check
235
+ * follows the injected `argv[1]` consistently.
236
+ */
237
+ function isDirectEntrypoint(importMetaUrl: string, argv: readonly string[]): boolean {
238
+ const argv1 = argv[1];
239
+ if (!argv1) {
240
+ return false;
241
+ }
242
+ try {
243
+ return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Argv-and-stream-driven orchestration body. Pulled out of the static
251
+ * method so the entrypoint guard / process-default plumbing stays
252
+ * separate from the parse + load + serialize steps.
253
+ */
254
+ async function orchestrate(
255
+ importMetaUrl: string,
256
+ MigrationClass: MigrationConstructor,
257
+ ctx: {
258
+ readonly argv: readonly string[];
259
+ readonly stdout: MigrationCliWritable;
260
+ readonly stderr: MigrationCliWritable;
261
+ },
262
+ ): Promise<number> {
263
+ const cli = Cli.from([MigrationFileCommand], {
264
+ binaryName: 'migration.ts',
265
+ binaryLabel: 'Migration file CLI',
266
+ });
267
+
268
+ const input = ctx.argv.slice(2);
269
+
270
+ // Pre-scan for malformed `--config` (no value, or value-shaped-as-flag)
271
+ // before delegating to clipanion. The legacy parser surfaced both as
272
+ // `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`); pre-scanning
273
+ // here keeps that contract independent of how clipanion classifies
274
+ // the error internally (it variably throws `UnknownSyntaxError` or
275
+ // accepts the flag-shaped token as the value depending on what other
276
+ // options are registered).
277
+ const configError = detectInvalidConfig(input);
278
+ if (configError) {
279
+ writeStructuredError(ctx.stderr, configError);
280
+ return 2;
281
+ }
282
+
283
+ let parsed: MigrationFileCommand;
284
+ try {
285
+ const command = cli.process({
286
+ input: [...input],
287
+ context: { stdout: ctx.stdout, stderr: ctx.stderr },
288
+ });
289
+ if (!(command instanceof MigrationFileCommand)) {
290
+ // The only registered command class is `MigrationFileCommand`;
291
+ // any other concrete type indicates clipanion emitted its
292
+ // built-in `HelpCommand`. Render usage directly so we don't
293
+ // depend on calling `cli.run` (which routes errors to stdout —
294
+ // wrong stream for our contract).
295
+ ctx.stdout.write(cli.usage(MigrationFileCommand, { detailed: true }));
296
+ return 0;
297
+ }
298
+ parsed = command;
299
+ } catch (err) {
300
+ return renderParseError(err, input, ctx.stderr);
301
+ }
302
+
303
+ if (parsed.help) {
304
+ ctx.stdout.write(cli.usage(MigrationFileCommand, { detailed: true }));
305
+ return 0;
306
+ }
307
+
308
+ try {
309
+ await runMigration(importMetaUrl, MigrationClass, parsed, ctx);
310
+ return 0;
311
+ } catch (err) {
312
+ if (CliStructuredError.is(err)) {
313
+ writeStructuredError(ctx.stderr, err);
314
+ } else if (MigrationToolsError.is(err)) {
315
+ // Migration-tools errors (e.g. `errorInvalidJson` thrown by
316
+ // `readExistingMetadata` when migration.json is malformed) carry
317
+ // their own `code`/`why`/`fix` shape. Render them with the same
318
+ // visual structure as `CliStructuredError` so consumers grepping
319
+ // for `MIGRATION.<CODE>` see consistent output across surfaces.
320
+ const fix = err.fix ? `\n${err.fix}` : '';
321
+ ctx.stderr.write(`${err.code}: ${err.message}\n${err.why}${fix}\n`);
322
+ } else {
323
+ ctx.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
324
+ }
325
+ return 1;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Returns an `errorMigrationCliInvalidConfigArg` envelope when `input`
331
+ * contains a malformed `--config`:
332
+ *
333
+ * - `--config` as the last token (no value follows).
334
+ * - `--config <flag>` where `<flag>` starts with `-` (silently
335
+ * consuming the next flag would either drop the flag or serialize
336
+ * against the wrong project).
337
+ * - `--config <empty>` where the value is the empty string. Shells
338
+ * expand `--config ""` (or `--config "$UNSET_VAR"`) into a real
339
+ * empty argv token; treating that as a usage error here surfaces
340
+ * `PN-CLI-4012` instead of a less actionable loader error on an
341
+ * empty path.
342
+ * - `--config=` (the equals form with an empty value). Same shape as
343
+ * the empty-string case above; the user expressed intent to override
344
+ * the config path but the override is empty.
345
+ *
346
+ * `--config=<value>` and `--config <value>` with a non-empty value are
347
+ * both valid (and the equals form's value is allowed to start with
348
+ * `-` — the `=` makes the binding explicit).
349
+ */
350
+ function detectInvalidConfig(input: readonly string[]): CliStructuredError | null {
351
+ for (let i = 0; i < input.length; i++) {
352
+ const token = input[i];
353
+ if (token === '--config') {
354
+ const next = input[i + 1];
355
+ if (next === undefined || next === '') {
356
+ return errorMigrationCliInvalidConfigArg();
357
+ }
358
+ if (next.startsWith('-')) {
359
+ return errorMigrationCliInvalidConfigArg({ nextToken: next });
360
+ }
361
+ continue;
362
+ }
363
+ if (token === '--config=') {
364
+ return errorMigrationCliInvalidConfigArg();
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+
370
+ /**
371
+ * Translate clipanion's parse-time errors into the project's structured
372
+ * error envelopes.
373
+ *
374
+ * - `UnknownSyntaxError` covers both unknown flags (`--frobnicate`) and
375
+ * the bare-trailing `--config` case (where arity-1 needs a value but
376
+ * none was supplied). Distinguished by inspecting the input array.
377
+ * - `UsageError` covers schema/validator failures from typanion. None
378
+ * of the migration-file CLI's options have validators today, but we
379
+ * still translate it as a usage error (exit 2) for forward-compat.
380
+ * - Anything else re-throws — caller's outer catch will surface it as
381
+ * exit 1 (runtime error).
382
+ */
383
+ function renderParseError(
384
+ err: unknown,
385
+ input: readonly string[],
386
+ stderr: MigrationCliWritable,
387
+ ): number {
388
+ if (isUnknownSyntaxError(err)) {
389
+ const flag = findOffendingFlag(input);
390
+ writeStructuredError(stderr, errorMigrationCliUnknownFlag({ flag, knownFlags: KNOWN_FLAGS }));
391
+ return 2;
392
+ }
393
+ if (err instanceof UsageError) {
394
+ // typanion validator failures and similar usage errors. None of
395
+ // the migration-file CLI's options have validators today, so this
396
+ // branch is forward-compat scaffolding — kept so that a future
397
+ // option declaration with a validator routes through the same PN
398
+ // envelope path rather than escaping as exit 1.
399
+ writeStructuredError(stderr, errorMigrationCliInvalidConfigArg({ nextToken: err.message }));
400
+ return 2;
401
+ }
402
+ throw err;
403
+ }
404
+
405
+ /**
406
+ * Duck-type check for clipanion's `UnknownSyntaxError`: the class is
407
+ * thrown by the parser but is not re-exported from the package's main
408
+ * entry (only `UsageError` is — see clipanion's `advanced/index.d.ts`).
409
+ * Identified by `name === 'UnknownSyntaxError'` and the
410
+ * `clipanion.type === 'none'` discriminator that clipanion's
411
+ * `ErrorWithMeta` interface guarantees.
412
+ */
413
+ function isUnknownSyntaxError(err: unknown): err is Error {
414
+ if (!(err instanceof Error) || err.name !== 'UnknownSyntaxError') {
415
+ return false;
416
+ }
417
+ // clipanion's `ErrorWithMeta` interface guarantees a `clipanion` field with
418
+ // a `type` discriminator on every error it throws. Read it via a structural
419
+ // shape rather than importing the class (it's not re-exported from the
420
+ // package main).
421
+ const meta = (err as { clipanion?: { type?: string } }).clipanion;
422
+ return typeof meta === 'object' && meta !== null && meta.type === 'none';
423
+ }
424
+
425
+ /**
426
+ * Best-effort: pull the first input token that doesn't match a known
427
+ * flag. Falls back to the first token when we can't pinpoint it. The
428
+ * returned name is rendered into the user-visible PN-CLI-4013 envelope
429
+ * (`Unknown flag \`<name>\``) and round-tripped via `meta.flag` so
430
+ * agent consumers can render their own "did you mean" suggestions.
431
+ */
432
+ function findOffendingFlag(input: readonly string[]): string {
433
+ for (const token of input) {
434
+ if (!token.startsWith('-')) {
435
+ continue;
436
+ }
437
+ const head = token.split('=', 1)[0] ?? token;
438
+ if (!KNOWN_FLAGS.includes(head) && head !== '-h') {
439
+ return head;
440
+ }
441
+ }
442
+ return input[0] ?? '';
443
+ }
444
+
445
+ /**
446
+ * Write a `CliStructuredError` envelope to the given stream. Format
447
+ * matches the legacy hand-rolled writer (`message: why`) so the rest of
448
+ * the project's error rendering stays consistent across surfaces. The
449
+ * full PN code (`PN-<domain>-<code>`) is included so consumers can
450
+ * grep for stable identifiers.
451
+ */
452
+ function writeStructuredError(stream: MigrationCliWritable, err: CliStructuredError): void {
453
+ const envelope = err.toEnvelope();
454
+ const why = envelope.why ?? envelope.summary;
455
+ const fix = envelope.fix ? `\n${envelope.fix}` : '';
456
+ stream.write(`${envelope.code}: ${envelope.summary}\n${why}${fix}\n`);
457
+ }
458
+
459
+ /**
460
+ * Read a previously-scaffolded `migration.json` from disk, returning
461
+ * `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`
462
+ * when the file is present but cannot be parsed as JSON. The CLI feeds
463
+ * this into `buildMigrationArtifacts` so the pure builder can preserve
464
+ * fields owned by `migration plan` (contract bookends, hints, labels,
465
+ * `createdAt`) across re-emits.
466
+ *
467
+ * Author-time path: this loader still does not verify the manifest hash
468
+ * or schema — that is the apply-time loader's job. Hash mismatch is the
469
+ * *expected* outcome of a re-author (the developer's source changes
470
+ * invalidate the prior hash by construction), and verification here
471
+ * would block legitimate regenerations. Syntactic JSON-parse failure,
472
+ * however, is now surfaced rather than swallowed: a malformed
473
+ * `migration.json` indicates either a hand-edit gone wrong or partial
474
+ * write, and silently rebuilding from `describe()` would discard the
475
+ * user's on-disk content (preserved bookends, hints, labels,
476
+ * `createdAt`) without any indication something was wrong on disk.
477
+ * Apply-time consumers always route through the verifying
478
+ * `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.
479
+ */
480
+ function readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {
481
+ let raw: string;
482
+ try {
483
+ raw = readFileSync(metadataPath, 'utf-8');
484
+ } catch {
485
+ return null;
486
+ }
487
+ try {
488
+ return JSON.parse(raw) as Partial<MigrationMetadata>;
489
+ } catch (e) {
490
+ throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Persist a migration instance's artifacts to `migrationDir`. In
496
+ * `dryRun` mode the artifacts are printed to stdout (with the same
497
+ * `--- migration.json --- / --- ops.json ---` framing the legacy
498
+ * `serializeMigration` helper used) and no files are written. Otherwise
499
+ * `ops.json` and `migration.json` are written next to `migration.ts` and
500
+ * a confirmation line is printed.
501
+ *
502
+ * File I/O lives in the CLI rather than `@prisma-next/migration-tools`
503
+ * so the migration-tools package stays focused on the pure
504
+ * `Migration` → in-memory artifact conversion. The CLI is the only
505
+ * legitimate site for combining config loading, stack assembly, and
506
+ * filesystem persistence.
507
+ */
508
+ function serializeMigrationToDisk(
509
+ instance: Migration,
510
+ migrationDir: string,
511
+ dryRun: boolean,
512
+ stdout: MigrationCliWritable,
513
+ ): void {
514
+ const metadataPath = join(migrationDir, 'migration.json');
515
+ const existing = readExistingMetadata(metadataPath);
516
+ const { opsJson, metadataJson } = buildMigrationArtifacts(instance, existing);
517
+
518
+ if (dryRun) {
519
+ stdout.write(`--- migration.json ---\n${metadataJson}\n`);
520
+ stdout.write('--- ops.json ---\n');
521
+ stdout.write(`${opsJson}\n`);
522
+ return;
523
+ }
524
+
525
+ writeFileSync(join(migrationDir, 'ops.json'), opsJson);
526
+ writeFileSync(metadataPath, metadataJson);
527
+
528
+ stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
529
+ }
530
+
531
+ /**
532
+ * Inner orchestration: load config, probe-construct the migration,
533
+ * verify target, assemble the stack, construct with the stack, persist.
534
+ *
535
+ * Throws `CliStructuredError` for known failure modes (config not
536
+ * found, target mismatch); the outer `orchestrate` translates those to
537
+ * exit 1.
538
+ */
539
+ async function runMigration(
540
+ importMetaUrl: string,
541
+ MigrationClass: MigrationConstructor,
542
+ parsed: MigrationFileCommand,
543
+ ctx: {
544
+ readonly stdout: MigrationCliWritable;
545
+ readonly stderr: MigrationCliWritable;
546
+ },
547
+ ): Promise<void> {
548
+ const migrationFile = fileURLToPath(importMetaUrl);
549
+ const migrationDir = dirname(migrationFile);
550
+
551
+ const config = await loadConfig(parsed.config);
552
+
553
+ // Probe-instantiate without a stack so we can read `targetId` before
554
+ // any target-specific constructor side effects (e.g.
555
+ // `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete
556
+ // subclasses are required to accept the no-arg form; the abstract
557
+ // `Migration` constructor declares `stack?` and target subclasses
558
+ // (Postgres, Mongo) propagate that optionality. This makes the
559
+ // target-mismatch guard fail fast with `PN-MIG-2006` before any
560
+ // stack-driven adapter construction begins, even if the wrong-target
561
+ // adapter's `create` would otherwise succeed and silently misshapen
562
+ // the stored adapter cast.
563
+ const probe = new MigrationClass();
564
+
565
+ if (probe.targetId !== config.target.targetId) {
566
+ throw errorMigrationTargetMismatch({
567
+ migrationTargetId: probe.targetId,
568
+ configTargetId: config.target.targetId,
569
+ });
570
+ }
571
+
572
+ const stack = createControlStack(config);
573
+ const instance = new MigrationClass(stack);
574
+
575
+ serializeMigrationToDisk(instance, migrationDir, parsed.dryRun, ctx.stdout);
576
+ void ctx.stderr;
577
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Re-export all domain error factories from @prisma-next/errors for convenience.
3
- * CLI-specific errors (e.g., Commander.js argument validation) can be added here if needed.
3
+ * CLI-specific errors (e.g., Commander argument validation in the main CLI, or
4
+ * clipanion parse errors in the migration-file CLI) can be added here if needed.
4
5
  */
5
6
  export type { CliErrorConflict, CliErrorEnvelope } from '@prisma-next/errors/control';
6
- export {
7
+
8
+ import {
7
9
  CliStructuredError,
8
10
  errorConfigFileNotFound,
9
11
  errorConfigValidation,
@@ -14,11 +16,34 @@ export {
14
16
  errorDriverRequired,
15
17
  errorFamilyReadMarkerSqlRequired,
16
18
  errorFileNotFound,
19
+ errorMigrationCliInvalidConfigArg,
20
+ errorMigrationCliUnknownFlag,
17
21
  errorMigrationPlanningFailed,
18
22
  errorQueryRunnerFactoryRequired,
19
23
  errorTargetMigrationNotSupported,
20
24
  errorUnexpected,
21
25
  } from '@prisma-next/errors/control';
26
+ import { errorRuntime } from '@prisma-next/errors/execution';
27
+ import type { MigrationToolsError } from '@prisma-next/migration-tools/errors';
28
+
29
+ export {
30
+ CliStructuredError,
31
+ errorConfigFileNotFound,
32
+ errorConfigValidation,
33
+ errorContractConfigMissing,
34
+ errorContractMissingExtensionPacks,
35
+ errorContractValidationFailed,
36
+ errorDatabaseConnectionRequired,
37
+ errorDriverRequired,
38
+ errorFamilyReadMarkerSqlRequired,
39
+ errorFileNotFound,
40
+ errorMigrationCliInvalidConfigArg,
41
+ errorMigrationCliUnknownFlag,
42
+ errorMigrationPlanningFailed,
43
+ errorQueryRunnerFactoryRequired,
44
+ errorTargetMigrationNotSupported,
45
+ errorUnexpected,
46
+ };
22
47
  export {
23
48
  ERROR_CODE_DESTRUCTIVE_CHANGES,
24
49
  errorDestructiveChanges,
@@ -37,3 +62,26 @@ export {
37
62
  errorUnfilledPlaceholder,
38
63
  placeholder,
39
64
  } from '@prisma-next/errors/migration';
65
+
66
+ /**
67
+ * Maps a `MigrationToolsError` raised by the migration-tools loader/graph
68
+ * surface (`readMigrationPackage`, `readMigrationsDir`, `readRefs`,
69
+ * `resolveRef`, `reconstructGraph`, ...) into a CLI `errorRuntime` envelope.
70
+ *
71
+ * The full `error.details` payload is forwarded into `meta` so machine
72
+ * consumers (`--json`) see structural fields like `dir`, `storedHash`,
73
+ * `computedHash` (for `MIGRATION.HASH_MISMATCH`) alongside the stable
74
+ * `code`. The user-visible `summary`/`why`/`fix` text is unchanged.
75
+ *
76
+ * Callers are expected to gate on `MigrationToolsError.is(error)` first
77
+ * (mirroring the original inline pattern); non-`MigrationToolsError`
78
+ * values are caller-classified (rethrow, wrap with command-specific
79
+ * `errorUnexpected`, etc.).
80
+ */
81
+ export function mapMigrationToolsError(error: MigrationToolsError): CliStructuredError {
82
+ return errorRuntime(error.message, {
83
+ why: error.why,
84
+ fix: error.fix,
85
+ meta: { code: error.code, ...(error.details ?? {}) },
86
+ });
87
+ }