@prisma-next/cli 0.3.0-dev.12 → 0.3.0-dev.122

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 +242 -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 +8 -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 +9 -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 +125 -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 +55 -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 +136 -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 +122 -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 +311 -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 +241 -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 +8 -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 +134 -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-B7OVZ-Ka.mjs +1576 -0
  88. package/dist/migration-status-B7OVZ-Ka.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 +85 -40
  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
@@ -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,187 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { relative, resolve } from 'node:path';
3
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
+ import type { Command } from 'commander';
5
+ import { loadConfig } from '../config-loader';
6
+ import { createControlClient } from '../control-api/client';
7
+ import type { ControlClient } from '../control-api/types';
8
+ import {
9
+ type CliStructuredError,
10
+ errorContractValidationFailed,
11
+ errorDatabaseConnectionRequired,
12
+ errorDriverRequired,
13
+ errorFileNotFound,
14
+ errorTargetMigrationNotSupported,
15
+ errorUnexpected,
16
+ } from './cli-errors';
17
+ import { addGlobalOptions, maskConnectionUrl, resolveContractPath } from './command-helpers';
18
+ import { formatStyledHeader } from './formatters/styled';
19
+ import type { GlobalFlags } from './global-flags';
20
+ import { createProgressAdapter } from './progress-adapter';
21
+ import type { TerminalUI } from './terminal-ui';
22
+
23
+ /**
24
+ * Resolved context for a migration command.
25
+ * Contains everything needed to invoke a control-api operation.
26
+ */
27
+ export interface MigrationContext {
28
+ readonly client: ControlClient;
29
+ readonly contractJson: Record<string, unknown>;
30
+ readonly dbConnection: unknown;
31
+ readonly onProgress: ReturnType<typeof createProgressAdapter>;
32
+ readonly configPath: string;
33
+ readonly contractPath: string;
34
+ readonly contractPathAbsolute: string;
35
+ readonly config: Awaited<ReturnType<typeof loadConfig>>;
36
+ }
37
+
38
+ /**
39
+ * Command-specific configuration for the shared scaffold.
40
+ */
41
+ export interface MigrationCommandDescriptor {
42
+ readonly commandName: string;
43
+ readonly description: string;
44
+ readonly url: string;
45
+ }
46
+
47
+ /**
48
+ * Prepares the shared context for migration commands (db init, db update).
49
+ *
50
+ * Handles: config loading, contract file reading, JSON parsing, connection resolution,
51
+ * driver/migration-support validation, client creation, and header output.
52
+ *
53
+ * Returns a Result with either the resolved context or a structured error.
54
+ */
55
+ export async function prepareMigrationContext(
56
+ options: { readonly db?: string; readonly config?: string; readonly dryRun?: boolean },
57
+ flags: GlobalFlags,
58
+ ui: TerminalUI,
59
+ descriptor: MigrationCommandDescriptor,
60
+ ): Promise<Result<MigrationContext, CliStructuredError>> {
61
+ // Load config
62
+ const config = await loadConfig(options.config);
63
+ const configPath = options.config
64
+ ? relative(process.cwd(), resolve(options.config))
65
+ : 'prisma-next.config.ts';
66
+ const contractPathAbsolute = resolveContractPath(config);
67
+ const contractPath = relative(process.cwd(), contractPathAbsolute);
68
+
69
+ // Output header to stderr (decoration)
70
+ if (!flags.json && !flags.quiet) {
71
+ const details: Array<{ label: string; value: string }> = [
72
+ { label: 'config', value: configPath },
73
+ { label: 'contract', value: contractPath },
74
+ ];
75
+ if (options.db) {
76
+ details.push({ label: 'database', value: maskConnectionUrl(options.db) });
77
+ }
78
+ if (options.dryRun) {
79
+ details.push({ label: 'mode', value: 'dry run' });
80
+ }
81
+ const header = formatStyledHeader({
82
+ command: descriptor.commandName,
83
+ description: descriptor.description,
84
+ url: descriptor.url,
85
+ details,
86
+ flags,
87
+ });
88
+ ui.stderr(header);
89
+ }
90
+
91
+ // Load contract file
92
+ let contractJsonContent: string;
93
+ try {
94
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
95
+ } catch (error) {
96
+ if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
97
+ return notOk(
98
+ errorFileNotFound(contractPathAbsolute, {
99
+ why: `Contract file not found at ${contractPathAbsolute}`,
100
+ fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
101
+ }),
102
+ );
103
+ }
104
+ return notOk(
105
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
106
+ why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
107
+ }),
108
+ );
109
+ }
110
+
111
+ // Parse contract JSON
112
+ let contractJson: Record<string, unknown>;
113
+ try {
114
+ contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
115
+ } catch (error) {
116
+ return notOk(
117
+ errorContractValidationFailed(
118
+ `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
119
+ { where: { path: contractPathAbsolute } },
120
+ ),
121
+ );
122
+ }
123
+
124
+ // Resolve database connection (--db flag or config.db.connection)
125
+ const dbConnection = options.db ?? config.db?.connection;
126
+ if (!dbConnection) {
127
+ return notOk(
128
+ errorDatabaseConnectionRequired({
129
+ why: `Database connection is required for ${descriptor.commandName} (set db.connection in ${configPath}, or pass --db <url>)`,
130
+ commandName: descriptor.commandName,
131
+ }),
132
+ );
133
+ }
134
+
135
+ // Check for driver
136
+ if (!config.driver) {
137
+ return notOk(
138
+ errorDriverRequired({ why: `Config.driver is required for ${descriptor.commandName}` }),
139
+ );
140
+ }
141
+
142
+ // Check target supports migrations via optional descriptor capability
143
+ const targetWithMigrations = config.target as typeof config.target & {
144
+ readonly migrations?: unknown;
145
+ };
146
+ if (!targetWithMigrations.migrations) {
147
+ return notOk(
148
+ errorTargetMigrationNotSupported({
149
+ why: `Target "${config.target.id}" does not support migrations`,
150
+ }),
151
+ );
152
+ }
153
+
154
+ // Create control client
155
+ const client = createControlClient({
156
+ family: config.family,
157
+ target: config.target,
158
+ adapter: config.adapter,
159
+ driver: config.driver,
160
+ extensionPacks: config.extensionPacks ?? [],
161
+ });
162
+
163
+ // Create progress adapter
164
+ const onProgress = createProgressAdapter({ ui, flags });
165
+
166
+ return ok({
167
+ client,
168
+ contractJson,
169
+ dbConnection,
170
+ onProgress,
171
+ configPath,
172
+ contractPath,
173
+ contractPathAbsolute,
174
+ config,
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Registers the shared CLI options for migration commands (db init, db update).
180
+ */
181
+ export function addMigrationCommandOptions(command: Command): Command {
182
+ addGlobalOptions(command);
183
+ return command
184
+ .option('--db <url>', 'Database connection string')
185
+ .option('--config <path>', 'Path to prisma-next.config.ts')
186
+ .option('--dry-run', 'Preview planned operations without applying', false);
187
+ }
@@ -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
+ }
@@ -0,0 +1,75 @@
1
+ import type { SpinnerResult } from '@clack/prompts';
2
+ import type { ControlProgressEvent, OnControlProgress } from '../control-api/types';
3
+ import type { GlobalFlags } from './global-flags';
4
+ import type { TerminalUI } from './terminal-ui';
5
+
6
+ /**
7
+ * Options for creating a progress adapter.
8
+ */
9
+ interface ProgressAdapterOptions {
10
+ readonly ui: TerminalUI;
11
+ readonly flags: GlobalFlags;
12
+ }
13
+
14
+ /**
15
+ * State for tracking active spans in the progress adapter.
16
+ */
17
+ interface SpanState {
18
+ readonly spinner: SpinnerResult;
19
+ readonly startTime: number;
20
+ readonly label: string;
21
+ }
22
+
23
+ /**
24
+ * Creates a progress adapter that converts control-api progress events
25
+ * into CLI spinner/progress output on stderr.
26
+ *
27
+ * The adapter:
28
+ * - Starts/succeeds spinners for top-level span boundaries
29
+ * - Prints per-operation lines for nested spans (e.g., migration operations under 'apply')
30
+ * - Respects quiet/json/non-TTY flags (no-op in those cases)
31
+ */
32
+ export function createProgressAdapter(options: ProgressAdapterOptions): OnControlProgress {
33
+ const { ui, flags } = options;
34
+
35
+ // Skip progress if quiet, JSON output, or non-interactive
36
+ if (flags.quiet || flags.json || !ui.isInteractive) {
37
+ return () => {};
38
+ }
39
+
40
+ // Track active spans by spanId
41
+ const activeSpans = new Map<string, SpanState>();
42
+
43
+ return (event: ControlProgressEvent) => {
44
+ if (event.kind === 'spanStart') {
45
+ // Nested spans (with parentSpanId) are printed as step lines
46
+ if (event.parentSpanId) {
47
+ ui.step(`${event.label}...`);
48
+ return;
49
+ }
50
+
51
+ // Top-level spans get a spinner
52
+ const spinner = ui.spinner();
53
+ spinner.start(event.label);
54
+
55
+ activeSpans.set(event.spanId, {
56
+ spinner,
57
+ startTime: Date.now(),
58
+ label: event.label,
59
+ });
60
+ } else if (event.kind === 'spanEnd') {
61
+ const spanState = activeSpans.get(event.spanId);
62
+ if (spanState) {
63
+ const elapsed = Date.now() - spanState.startTime;
64
+ if (event.outcome === 'error') {
65
+ spanState.spinner.error(`${spanState.label} (failed)`);
66
+ } else if (event.outcome === 'skipped') {
67
+ spanState.spinner.stop(`${spanState.label} (skipped)`);
68
+ } else {
69
+ spanState.spinner.stop(`${spanState.label} (${elapsed}ms)`);
70
+ }
71
+ activeSpans.delete(event.spanId);
72
+ }
73
+ }
74
+ };
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
+ }