@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
@@ -1,42 +1,49 @@
1
1
  export interface GlobalFlags {
2
- readonly json?: 'object' | 'ndjson';
2
+ readonly json?: boolean;
3
3
  readonly quiet?: boolean;
4
4
  readonly verbose?: number; // 0, 1, or 2
5
- readonly timestamps?: boolean;
6
5
  readonly color?: boolean;
6
+ readonly interactive?: boolean;
7
+ readonly yes?: boolean;
7
8
  }
8
9
 
9
- export interface CliOptions {
10
+ /**
11
+ * Common options parsed by Commander.js for every command.
12
+ * Extend this for command-specific options instead of duplicating these fields.
13
+ */
14
+ export interface CommonCommandOptions {
10
15
  readonly json?: string | boolean;
11
16
  readonly quiet?: boolean;
12
17
  readonly q?: boolean;
13
18
  readonly verbose?: boolean;
14
19
  readonly v?: boolean;
15
- readonly vv?: boolean;
16
20
  readonly trace?: boolean;
17
- readonly timestamps?: boolean;
18
21
  readonly color?: boolean;
19
22
  readonly 'no-color'?: boolean;
23
+ readonly interactive?: boolean;
24
+ readonly 'no-interactive'?: boolean;
25
+ readonly yes?: boolean;
26
+ readonly y?: boolean;
20
27
  }
21
28
 
22
29
  /**
23
30
  * Parses global flags from CLI options.
24
- * Handles verbosity flags (-v, -vv, --trace), JSON output, quiet mode, timestamps, and color.
31
+ * Handles verbosity flags (-v, --trace), JSON output, quiet mode, color,
32
+ * interactivity (--interactive/--no-interactive), and auto-accept (-y/--yes).
25
33
  */
26
- export function parseGlobalFlags(options: CliOptions): GlobalFlags {
34
+ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
27
35
  const flags: {
28
- json?: 'object' | 'ndjson';
36
+ json?: boolean;
29
37
  quiet?: boolean;
30
38
  verbose?: number;
31
- timestamps?: boolean;
32
39
  color?: boolean;
40
+ interactive?: boolean;
41
+ yes?: boolean;
33
42
  } = {};
34
43
 
35
- // JSON output
36
- if (options.json === true || options.json === 'object') {
37
- flags.json = 'object';
38
- } else if (options.json === 'ndjson') {
39
- flags.json = 'ndjson';
44
+ // JSON output: explicit --json flag or auto-detect piped stdout (Unix convention)
45
+ if (options.json || !process.stdout.isTTY) {
46
+ flags.json = true;
40
47
  }
41
48
 
42
49
  // Quiet mode
@@ -44,22 +51,18 @@ export function parseGlobalFlags(options: CliOptions): GlobalFlags {
44
51
  flags.quiet = true;
45
52
  }
46
53
 
47
- // Verbosity: -v = 1, -vv or --trace = 2
48
- if (options.vv || options.trace) {
54
+ // Verbosity: -v = 1, --trace = 2
55
+ // Env toggles: PRISMA_NEXT_TRACE=1 ≅ --trace, PRISMA_NEXT_DEBUG=1 ≅ -v
56
+ if (options.trace || process.env['PRISMA_NEXT_TRACE'] === '1') {
49
57
  flags.verbose = 2;
50
- } else if (options.verbose || options.v) {
58
+ } else if (options.verbose || options.v || process.env['PRISMA_NEXT_DEBUG'] === '1') {
51
59
  flags.verbose = 1;
52
60
  } else {
53
61
  flags.verbose = 0;
54
62
  }
55
63
 
56
- // Timestamps
57
- if (options.timestamps) {
58
- flags.timestamps = true;
59
- }
60
-
61
64
  // Color: respect NO_COLOR env var, --color/--no-color flags
62
- // When JSON output is enabled (any format), disable color to ensure clean JSON output
65
+ // When JSON output is enabled, disable color to ensure clean JSON output
63
66
  if (process.env['NO_COLOR'] || flags.json) {
64
67
  flags.color = false;
65
68
  } else if (options['no-color']) {
@@ -71,5 +74,20 @@ export function parseGlobalFlags(options: CliOptions): GlobalFlags {
71
74
  flags.color = process.stdout.isTTY && !process.env['CI'];
72
75
  }
73
76
 
77
+ // Interactivity: --interactive/--no-interactive
78
+ // Default: interactive when stdout is a TTY
79
+ if (options['no-interactive']) {
80
+ flags.interactive = false;
81
+ } else if (options.interactive !== undefined) {
82
+ flags.interactive = options.interactive;
83
+ } else {
84
+ flags.interactive = !!process.stdout.isTTY;
85
+ }
86
+
87
+ // Auto-accept prompts: -y/--yes
88
+ if (options.yes || options.y) {
89
+ flags.yes = true;
90
+ }
91
+
74
92
  return flags as GlobalFlags;
75
93
  }
@@ -0,0 +1,184 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { relative, resolve } from 'node:path';
3
+ import { hasMigrations } from '@prisma-next/framework-components/control';
4
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
+ import type { Command } from 'commander';
6
+ import { loadConfig } from '../config-loader';
7
+ import { createControlClient } from '../control-api/client';
8
+ import type { ControlClient } from '../control-api/types';
9
+ import {
10
+ type CliStructuredError,
11
+ errorContractValidationFailed,
12
+ errorDatabaseConnectionRequired,
13
+ errorDriverRequired,
14
+ errorFileNotFound,
15
+ errorTargetMigrationNotSupported,
16
+ errorUnexpected,
17
+ } from './cli-errors';
18
+ import { addGlobalOptions, maskConnectionUrl, resolveContractPath } from './command-helpers';
19
+ import { formatStyledHeader } from './formatters/styled';
20
+ import type { GlobalFlags } from './global-flags';
21
+ import { createProgressAdapter } from './progress-adapter';
22
+ import type { TerminalUI } from './terminal-ui';
23
+
24
+ /**
25
+ * Resolved context for a migration command.
26
+ * Contains everything needed to invoke a control-api operation.
27
+ */
28
+ export interface MigrationContext {
29
+ readonly client: ControlClient;
30
+ readonly contractJson: Record<string, unknown>;
31
+ readonly dbConnection: unknown;
32
+ readonly onProgress: ReturnType<typeof createProgressAdapter>;
33
+ readonly configPath: string;
34
+ readonly contractPath: string;
35
+ readonly contractPathAbsolute: string;
36
+ readonly config: Awaited<ReturnType<typeof loadConfig>>;
37
+ }
38
+
39
+ /**
40
+ * Command-specific configuration for the shared scaffold.
41
+ */
42
+ export interface MigrationCommandDescriptor {
43
+ readonly commandName: string;
44
+ readonly description: string;
45
+ readonly url: string;
46
+ }
47
+
48
+ /**
49
+ * Prepares the shared context for migration commands (db init, db update).
50
+ *
51
+ * Handles: config loading, contract file reading, JSON parsing, connection resolution,
52
+ * driver/migration-support validation, client creation, and header output.
53
+ *
54
+ * Returns a Result with either the resolved context or a structured error.
55
+ */
56
+ export async function prepareMigrationContext(
57
+ options: { readonly db?: string; readonly config?: string; readonly dryRun?: boolean },
58
+ flags: GlobalFlags,
59
+ ui: TerminalUI,
60
+ descriptor: MigrationCommandDescriptor,
61
+ ): Promise<Result<MigrationContext, CliStructuredError>> {
62
+ // Load config
63
+ const config = await loadConfig(options.config);
64
+ const configPath = options.config
65
+ ? relative(process.cwd(), resolve(options.config))
66
+ : 'prisma-next.config.ts';
67
+ const contractPathAbsolute = resolveContractPath(config);
68
+ const contractPath = relative(process.cwd(), contractPathAbsolute);
69
+
70
+ // Output header to stderr (decoration)
71
+ if (!flags.json && !flags.quiet) {
72
+ const details: Array<{ label: string; value: string }> = [
73
+ { label: 'config', value: configPath },
74
+ { label: 'contract', value: contractPath },
75
+ ];
76
+ if (options.db) {
77
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
78
+ }
79
+ if (options.dryRun) {
80
+ details.push({ label: 'mode', value: 'dry run' });
81
+ }
82
+ const header = formatStyledHeader({
83
+ command: descriptor.commandName,
84
+ description: descriptor.description,
85
+ url: descriptor.url,
86
+ details,
87
+ flags,
88
+ });
89
+ ui.stderr(header);
90
+ }
91
+
92
+ // Load contract file
93
+ let contractJsonContent: string;
94
+ try {
95
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
96
+ } catch (error) {
97
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
98
+ return notOk(
99
+ errorFileNotFound(contractPathAbsolute, {
100
+ why: `Contract file not found at ${contractPathAbsolute}`,
101
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
102
+ }),
103
+ );
104
+ }
105
+ return notOk(
106
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
107
+ why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
108
+ }),
109
+ );
110
+ }
111
+
112
+ // Parse contract JSON
113
+ let contractJson: Record<string, unknown>;
114
+ try {
115
+ contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
116
+ } catch (error) {
117
+ return notOk(
118
+ errorContractValidationFailed(
119
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
120
+ { where: { path: contractPathAbsolute } },
121
+ ),
122
+ );
123
+ }
124
+
125
+ // Resolve database connection (--db flag or config.db.connection)
126
+ const dbConnection = options.db ?? config.db?.connection;
127
+ if (!dbConnection) {
128
+ return notOk(
129
+ errorDatabaseConnectionRequired({
130
+ why: `Database connection is required for ${descriptor.commandName} (set db.connection in ${configPath}, or pass --db <url>)`,
131
+ commandName: descriptor.commandName,
132
+ }),
133
+ );
134
+ }
135
+
136
+ // Check for driver
137
+ if (!config.driver) {
138
+ return notOk(
139
+ errorDriverRequired({ why: `Config.driver is required for ${descriptor.commandName}` }),
140
+ );
141
+ }
142
+
143
+ if (!hasMigrations(config.target)) {
144
+ return notOk(
145
+ errorTargetMigrationNotSupported({
146
+ why: `Target "${config.target.id}" does not support migrations`,
147
+ }),
148
+ );
149
+ }
150
+
151
+ // Create control client
152
+ const client = createControlClient({
153
+ family: config.family,
154
+ target: config.target,
155
+ adapter: config.adapter,
156
+ driver: config.driver,
157
+ extensionPacks: config.extensionPacks ?? [],
158
+ });
159
+
160
+ // Create progress adapter
161
+ const onProgress = createProgressAdapter({ ui, flags });
162
+
163
+ return ok({
164
+ client,
165
+ contractJson,
166
+ dbConnection,
167
+ onProgress,
168
+ configPath,
169
+ contractPath,
170
+ contractPathAbsolute,
171
+ config,
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Registers the shared CLI options for migration commands (db init, db update).
177
+ */
178
+ export function addMigrationCommandOptions(command: Command): Command {
179
+ addGlobalOptions(command);
180
+ return command
181
+ .option('--db <url>', 'Database connection string')
182
+ .option('--config <path>', 'Path to prisma-next.config.ts')
183
+ .option('--dry-run', 'Preview planned operations without applying', false);
184
+ }
@@ -0,0 +1,12 @@
1
+ export interface StatusRef {
2
+ readonly name: string;
3
+ readonly hash: string;
4
+ readonly active: boolean;
5
+ }
6
+
7
+ export interface StatusDiagnostic {
8
+ readonly code: string;
9
+ readonly severity: 'warn' | 'info';
10
+ readonly message: string;
11
+ readonly hints: readonly string[];
12
+ }
@@ -1,14 +1,13 @@
1
- import ora from 'ora';
1
+ import type { SpinnerResult } from '@clack/prompts';
2
2
  import type { ControlProgressEvent, OnControlProgress } from '../control-api/types';
3
3
  import type { GlobalFlags } from './global-flags';
4
+ import type { TerminalUI } from './terminal-ui';
4
5
 
5
6
  /**
6
7
  * Options for creating a progress adapter.
7
8
  */
8
9
  interface ProgressAdapterOptions {
9
- /**
10
- * Global flags that control progress output behavior (quiet, json, color).
11
- */
10
+ readonly ui: TerminalUI;
12
11
  readonly flags: GlobalFlags;
13
12
  }
14
13
 
@@ -16,33 +15,26 @@ interface ProgressAdapterOptions {
16
15
  * State for tracking active spans in the progress adapter.
17
16
  */
18
17
  interface SpanState {
19
- readonly spinner: ReturnType<typeof ora>;
18
+ readonly spinner: SpinnerResult;
20
19
  readonly startTime: number;
20
+ readonly label: string;
21
21
  }
22
22
 
23
23
  /**
24
24
  * Creates a progress adapter that converts control-api progress events
25
- * into CLI spinner/progress output.
25
+ * into CLI spinner/progress output on stderr.
26
26
  *
27
27
  * The adapter:
28
28
  * - Starts/succeeds spinners for top-level span boundaries
29
29
  * - Prints per-operation lines for nested spans (e.g., migration operations under 'apply')
30
30
  * - Respects quiet/json/non-TTY flags (no-op in those cases)
31
- *
32
- * @param options - Progress adapter configuration
33
- * @returns An onProgress callback compatible with control-api operations
34
31
  */
35
32
  export function createProgressAdapter(options: ProgressAdapterOptions): OnControlProgress {
36
- const { flags } = options;
37
-
38
- // Skip progress if quiet, JSON output, or non-TTY
39
- const shouldShowProgress = !flags.quiet && flags.json !== 'object' && process.stdout.isTTY;
33
+ const { ui, flags } = options;
40
34
 
41
- if (!shouldShowProgress) {
42
- // Return a no-op callback
43
- return () => {
44
- // No-op
45
- };
35
+ // Skip progress if quiet, JSON output, or non-interactive
36
+ if (flags.quiet || flags.json || !ui.isInteractive) {
37
+ return () => {};
46
38
  }
47
39
 
48
40
  // Track active spans by spanId
@@ -50,37 +42,34 @@ export function createProgressAdapter(options: ProgressAdapterOptions): OnContro
50
42
 
51
43
  return (event: ControlProgressEvent) => {
52
44
  if (event.kind === 'spanStart') {
53
- // Nested spans (with parentSpanId) are printed as lines, not spinners
45
+ // Nested spans (with parentSpanId) are printed as step lines
54
46
  if (event.parentSpanId) {
55
- console.log(` → ${event.label}...`);
47
+ ui.step(`${event.label}...`);
56
48
  return;
57
49
  }
58
50
 
59
51
  // Top-level spans get a spinner
60
- const spinner = ora({
61
- text: event.label,
62
- color: flags.color !== false ? 'cyan' : false,
63
- }).start();
52
+ const spinner = ui.spinner();
53
+ spinner.start(event.label);
64
54
 
65
55
  activeSpans.set(event.spanId, {
66
56
  spinner,
67
57
  startTime: Date.now(),
58
+ label: event.label,
68
59
  });
69
60
  } else if (event.kind === 'spanEnd') {
70
- // Complete the spinner for this span (only top-level spans have spinners)
71
61
  const spanState = activeSpans.get(event.spanId);
72
62
  if (spanState) {
73
63
  const elapsed = Date.now() - spanState.startTime;
74
64
  if (event.outcome === 'error') {
75
- spanState.spinner.fail(`${spanState.spinner.text} (failed)`);
65
+ spanState.spinner.error(`${spanState.label} (failed)`);
76
66
  } else if (event.outcome === 'skipped') {
77
- spanState.spinner.info(`${spanState.spinner.text} (skipped)`);
67
+ spanState.spinner.stop(`${spanState.label} (skipped)`);
78
68
  } else {
79
- spanState.spinner.succeed(`${spanState.spinner.text} (${elapsed}ms)`);
69
+ spanState.spinner.stop(`${spanState.label} (${elapsed}ms)`);
80
70
  }
81
71
  activeSpans.delete(event.spanId);
82
72
  }
83
- // Nested span ends are no-ops (could log completion if needed)
84
73
  }
85
74
  };
86
75
  }
@@ -1,44 +1,43 @@
1
1
  import type { Result } from '@prisma-next/utils/result';
2
2
  import type { CliStructuredError } from './cli-errors';
3
+ import { formatErrorJson, formatErrorOutput } from './formatters/errors';
3
4
  import type { GlobalFlags } from './global-flags';
4
- import { formatErrorJson, formatErrorOutput } from './output';
5
+ import type { TerminalUI } from './terminal-ui';
5
6
 
6
7
  /**
7
8
  * Processes a CLI command result, handling both success and error cases.
8
9
  * Formats output appropriately and returns the exit code.
9
10
  * Never throws - returns exit code for commands to use with process.exit().
10
11
  *
11
- * @param result - The result from a CLI command
12
- * @param flags - Global flags for output formatting
13
- * @param onSuccess - Optional callback for successful results (for custom success output)
14
- * @returns The exit code that should be used (0 for success, non-zero for errors)
12
+ * Error output:
13
+ * - JSON mode: JSON error to stdout (piped) via ui.output(), human sees nothing on stderr.
14
+ * - Interactive: human-readable error to stderr.
15
15
  */
16
16
  export function handleResult<T>(
17
17
  result: Result<T, CliStructuredError>,
18
18
  flags: GlobalFlags,
19
+ ui: TerminalUI,
19
20
  onSuccess?: (value: T) => void,
20
21
  ): number {
21
22
  if (result.ok) {
22
- // Success case
23
23
  if (onSuccess) {
24
24
  onSuccess(result.value);
25
25
  }
26
26
  return 0;
27
27
  }
28
28
 
29
- // Error case - convert to CLI envelope
29
+ // Convert to CLI envelope
30
30
  const envelope = result.failure.toEnvelope();
31
31
 
32
- // Output error based on flags
33
32
  if (flags.json) {
34
- // JSON error to stderr
35
- console.error(formatErrorJson(envelope));
33
+ // JSON error stdout only
34
+ ui.output(formatErrorJson(envelope));
36
35
  } else {
37
- // Human-readable error to stderr
38
- console.error(formatErrorOutput(envelope, flags));
36
+ // Human-readable error stderr
37
+ ui.error(formatErrorOutput(envelope, flags));
39
38
  }
40
39
 
41
- // Infer exit code from error domain: CLI errors = 2, RTM errors = 1
40
+ // Infer exit code from error domain: CLI errors = 2, RUN errors = 1
42
41
  const exitCode = result.failure.domain === 'CLI' ? 2 : 1;
43
42
  return exitCode;
44
43
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Global shutdown controller for graceful SIGINT/SIGTERM handling.
3
+ *
4
+ * The CLI installs signal handlers once at startup. When a signal fires:
5
+ * 1. The AbortController is aborted — in-flight async work (DB queries, emit) can check `signal.aborted`.
6
+ * 2. A 3-second grace timer starts — gives `finally` blocks time to close connections.
7
+ * 3. If the process hasn't exited by then, force-exit with code 130 (128 + SIGINT).
8
+ * 4. A second signal during the grace period force-exits immediately.
9
+ */
10
+
11
+ /**
12
+ * Creates a shutdown handler with its own AbortController.
13
+ * Exposed for testing — production code uses the singleton below.
14
+ */
15
+ export interface ShutdownHandler {
16
+ readonly signal: AbortSignal;
17
+ readonly isShuttingDown: () => boolean;
18
+ readonly onSignal: () => void;
19
+ readonly clearGraceTimer: () => void;
20
+ }
21
+
22
+ export function createShutdownHandler(options?: {
23
+ readonly exit?: (code: number) => void;
24
+ readonly gracePeriodMs?: number;
25
+ }): ShutdownHandler {
26
+ const exit = options?.exit ?? ((code: number) => process.exit(code));
27
+ const gracePeriodMs = options?.gracePeriodMs ?? 3000;
28
+
29
+ const controller = new AbortController();
30
+ let shuttingDown = false;
31
+ let graceTimer: ReturnType<typeof setTimeout> | undefined;
32
+
33
+ const onSignal = () => {
34
+ if (shuttingDown) {
35
+ // Second signal — force exit
36
+ exit(130);
37
+ return;
38
+ }
39
+ shuttingDown = true;
40
+ controller.abort();
41
+
42
+ // Give finally blocks time to clean up, then force-exit
43
+ graceTimer = setTimeout(() => exit(130), gracePeriodMs);
44
+ graceTimer.unref();
45
+ };
46
+
47
+ return {
48
+ signal: controller.signal,
49
+ isShuttingDown: () => shuttingDown,
50
+ onSignal,
51
+ clearGraceTimer: () => {
52
+ if (graceTimer !== undefined) {
53
+ clearTimeout(graceTimer);
54
+ graceTimer = undefined;
55
+ }
56
+ },
57
+ };
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Singleton for production use
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const globalHandler = createShutdownHandler();
65
+
66
+ /**
67
+ * The global AbortSignal. Pass this to any async operation that should
68
+ * be cancellable on Ctrl+C (e.g., DB queries, long-running emit).
69
+ */
70
+ export const shutdownSignal: AbortSignal = globalHandler.signal;
71
+
72
+ /**
73
+ * Whether a shutdown has been initiated.
74
+ */
75
+ export function isShuttingDown(): boolean {
76
+ return globalHandler.isShuttingDown();
77
+ }
78
+
79
+ /**
80
+ * Installs SIGINT and SIGTERM handlers. Call once at CLI startup.
81
+ *
82
+ * - First signal: aborts the controller, starts a 3s grace timer.
83
+ * - Second signal: force-exits immediately.
84
+ */
85
+ let installed = false;
86
+
87
+ export function installShutdownHandlers(): void {
88
+ if (installed) return;
89
+ installed = true;
90
+ process.on('SIGINT', globalHandler.onSignal);
91
+ process.on('SIGTERM', globalHandler.onSignal);
92
+ }
@@ -0,0 +1,31 @@
1
+ import { distance } from 'closest-match';
2
+
3
+ /**
4
+ * Suggests similar command names for a mistyped input.
5
+ *
6
+ * Uses Levenshtein distance to find close matches. Only suggests commands
7
+ * within a reasonable distance threshold (40% of the input length, minimum 2).
8
+ * Returns up to 3 suggestions in case of ties.
9
+ *
10
+ * @returns Array of suggested command names (empty if nothing is close enough).
11
+ */
12
+ export function suggestCommands(input: string, candidates: readonly string[]): string[] {
13
+ if (candidates.length === 0) return [];
14
+
15
+ // Threshold: at most 40% of the input length (min 2) to avoid absurd suggestions
16
+ const maxDistance = Math.max(2, Math.ceil(input.length * 0.4));
17
+
18
+ const scored = candidates
19
+ .map((name) => ({ name, dist: distance(input, name) }))
20
+ .filter((entry) => entry.dist <= maxDistance)
21
+ .sort((a, b) => a.dist - b.dist);
22
+
23
+ if (scored.length === 0) return [];
24
+
25
+ // Take the best distance, then include ties (up to 3)
26
+ const bestDist = scored[0]!.dist;
27
+ return scored
28
+ .filter((entry) => entry.dist === bestDist)
29
+ .slice(0, 3)
30
+ .map((entry) => entry.name);
31
+ }