@prisma-next/cli 0.10.0-dev.9 → 0.11.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 (145) hide show
  1. package/README.md +1 -1
  2. package/dist/{cli-errors-CF60g2cG.mjs → cli-errors-Djtz98Vm.mjs} +3 -3
  3. package/dist/cli-errors-Djtz98Vm.mjs.map +1 -0
  4. package/dist/cli.mjs +46 -51
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{client-Brv4qlfB.mjs → client-oXO2WCPD.mjs} +6 -5
  7. package/dist/client-oXO2WCPD.mjs.map +1 -0
  8. package/dist/{command-helpers-Dvgul7UA.mjs → command-helpers-BSb0tRC8.mjs} +103 -9
  9. package/dist/command-helpers-BSb0tRC8.mjs.map +1 -0
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +19 -20
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.mjs +6 -10
  16. package/dist/commands/db-schema.mjs.map +1 -1
  17. package/dist/commands/db-sign.mjs +7 -11
  18. package/dist/commands/db-sign.mjs.map +1 -1
  19. package/dist/commands/db-update.d.mts.map +1 -1
  20. package/dist/commands/db-update.mjs +16 -17
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.mjs +1 -1
  23. package/dist/commands/migrate.d.mts +1 -1
  24. package/dist/commands/migrate.mjs +7 -11
  25. package/dist/commands/migrate.mjs.map +1 -1
  26. package/dist/commands/migration-check.mjs +4 -7
  27. package/dist/commands/migration-check.mjs.map +1 -1
  28. package/dist/commands/migration-graph.d.mts +1 -1
  29. package/dist/commands/migration-graph.mjs +6 -10
  30. package/dist/commands/migration-graph.mjs.map +1 -1
  31. package/dist/commands/migration-list.mjs +5 -9
  32. package/dist/commands/migration-list.mjs.map +1 -1
  33. package/dist/commands/migration-log.d.mts.map +1 -1
  34. package/dist/commands/migration-log.mjs +7 -10
  35. package/dist/commands/migration-log.mjs.map +1 -1
  36. package/dist/commands/migration-new.mjs +6 -10
  37. package/dist/commands/migration-new.mjs.map +1 -1
  38. package/dist/commands/migration-plan.d.mts +1 -1
  39. package/dist/commands/migration-plan.mjs +1 -1
  40. package/dist/commands/migration-show.d.mts +1 -1
  41. package/dist/commands/migration-show.mjs +8 -12
  42. package/dist/commands/migration-show.mjs.map +1 -1
  43. package/dist/commands/migration-status.d.mts +1 -1
  44. package/dist/commands/migration-status.d.mts.map +1 -1
  45. package/dist/commands/migration-status.mjs +36 -14
  46. package/dist/commands/migration-status.mjs.map +1 -1
  47. package/dist/commands/ref.d.mts +1 -1
  48. package/dist/commands/ref.mjs +9 -19
  49. package/dist/commands/ref.mjs.map +1 -1
  50. package/dist/{contract-emit-iynA3BCA.mjs → contract-emit-bcrpT-wD.mjs} +3 -3
  51. package/dist/{contract-emit-iynA3BCA.mjs.map → contract-emit-bcrpT-wD.mjs.map} +1 -1
  52. package/dist/{contract-emit-BDBzHlaC.mjs → contract-emit-r4y8Zhf1.mjs} +7 -12
  53. package/dist/contract-emit-r4y8Zhf1.mjs.map +1 -0
  54. package/dist/{contract-infer-Dm8pBZMR.mjs → contract-infer-BmySmqVT.mjs} +8 -13
  55. package/dist/contract-infer-BmySmqVT.mjs.map +1 -0
  56. package/dist/{contract-space-aggregate-loader-pAc8CDfY.mjs → contract-space-aggregate-loader-BmNQwlws.mjs} +2 -2
  57. package/dist/{contract-space-aggregate-loader-pAc8CDfY.mjs.map → contract-space-aggregate-loader-BmNQwlws.mjs.map} +1 -1
  58. package/dist/{db-verify-CW8DR5Ei.mjs → db-verify-BClPs3ph.mjs} +9 -13
  59. package/dist/db-verify-BClPs3ph.mjs.map +1 -0
  60. package/dist/exports/control-api.d.mts +1 -1
  61. package/dist/exports/control-api.mjs +2 -2
  62. package/dist/exports/index.mjs +1 -1
  63. package/dist/exports/init-output.mjs +1 -1
  64. package/dist/{framework-components-xFLFpZUO.mjs → framework-components-65gOHkHB.mjs} +2 -2
  65. package/dist/{framework-components-xFLFpZUO.mjs.map → framework-components-65gOHkHB.mjs.map} +1 -1
  66. package/dist/{global-flags-DGmw6Kqg.d.mts → global-flags-CdE7M0d9.d.mts} +4 -1
  67. package/dist/global-flags-CdE7M0d9.d.mts.map +1 -0
  68. package/dist/{graph-render-eJDcLWny.mjs → graph-render-DJVv0_uf.mjs} +1 -1
  69. package/dist/{graph-render-eJDcLWny.mjs.map → graph-render-DJVv0_uf.mjs.map} +1 -1
  70. package/dist/{init-CxS9eqbQ.mjs → init-BCJZPWE1.mjs} +141 -55
  71. package/dist/init-BCJZPWE1.mjs.map +1 -0
  72. package/dist/{inspect-live-schema-iETRZ_59.mjs → inspect-live-schema-DSRbFoOL.mjs} +4 -4
  73. package/dist/{inspect-live-schema-iETRZ_59.mjs.map → inspect-live-schema-DSRbFoOL.mjs.map} +1 -1
  74. package/dist/{migration-command-scaffold-BlgVj_Pn.mjs → migration-command-scaffold-Bzd9La5c.mjs} +4 -4
  75. package/dist/{migration-command-scaffold-BlgVj_Pn.mjs.map → migration-command-scaffold-Bzd9La5c.mjs.map} +1 -1
  76. package/dist/{migration-plan-BSzcWsvm.mjs → migration-plan-CFwqw3Gk.mjs} +8 -12
  77. package/dist/migration-plan-CFwqw3Gk.mjs.map +1 -0
  78. package/dist/{migration-types-D2FW63pr.d.mts → migration-types-BXWvz12q.d.mts} +1 -1
  79. package/dist/{migration-types-D2FW63pr.d.mts.map → migration-types-BXWvz12q.d.mts.map} +1 -1
  80. package/dist/{migrations-CgANWI0w.mjs → migrations-CwZMa1Ck.mjs} +2 -2
  81. package/dist/{migrations-CgANWI0w.mjs.map → migrations-CwZMa1Ck.mjs.map} +1 -1
  82. package/dist/{output-B60Gw5fu.mjs → output-BlsrGMEF.mjs} +1 -1
  83. package/dist/{output-B60Gw5fu.mjs.map → output-BlsrGMEF.mjs.map} +1 -1
  84. package/dist/readme-mongo.md +35 -0
  85. package/dist/readme-postgres.md +34 -0
  86. package/dist/{terminal-ui-XtOQsqe9.mjs → terminal-ui-BiB_8KNo.mjs} +131 -24
  87. package/dist/terminal-ui-BiB_8KNo.mjs.map +1 -0
  88. package/dist/{types-0aS865QN.d.mts → types--CqjMdk0.d.mts} +2 -2
  89. package/dist/{types-0aS865QN.d.mts.map → types--CqjMdk0.d.mts.map} +1 -1
  90. package/dist/{verify-nlzO0uIY.mjs → verify-Bom75OYI.mjs} +2 -2
  91. package/dist/{verify-nlzO0uIY.mjs.map → verify-Bom75OYI.mjs.map} +1 -1
  92. package/package.json +18 -18
  93. package/src/cli.ts +36 -9
  94. package/src/commands/contract-emit.ts +4 -4
  95. package/src/commands/contract-infer.ts +6 -6
  96. package/src/commands/db-init.ts +13 -5
  97. package/src/commands/db-schema.ts +4 -4
  98. package/src/commands/db-sign.ts +4 -4
  99. package/src/commands/db-update.ts +13 -5
  100. package/src/commands/db-verify.ts +5 -5
  101. package/src/commands/init/detect-package-manager.ts +15 -0
  102. package/src/commands/init/errors.ts +31 -0
  103. package/src/commands/init/index.ts +2 -2
  104. package/src/commands/init/init.ts +33 -17
  105. package/src/commands/init/inputs.ts +6 -4
  106. package/src/commands/init/output.ts +1 -1
  107. package/src/commands/init/skill-install.ts +37 -26
  108. package/src/commands/init/templates/code-templates.ts +4 -12
  109. package/src/commands/init/templates/env.ts +8 -1
  110. package/src/commands/init/templates/readme-mongo.md +35 -0
  111. package/src/commands/init/templates/readme-postgres.md +34 -0
  112. package/src/commands/init/templates/readme.ts +62 -0
  113. package/src/commands/migrate.ts +4 -7
  114. package/src/commands/migration-check.ts +4 -4
  115. package/src/commands/migration-graph.ts +4 -4
  116. package/src/commands/migration-list.ts +4 -4
  117. package/src/commands/migration-log.ts +6 -5
  118. package/src/commands/migration-new.ts +4 -4
  119. package/src/commands/migration-plan.ts +4 -4
  120. package/src/commands/migration-show.ts +4 -4
  121. package/src/commands/migration-status.ts +49 -6
  122. package/src/commands/ref.ts +8 -8
  123. package/src/control-api/operations/apply-aggregate.ts +1 -0
  124. package/src/utils/cli-errors.ts +4 -0
  125. package/src/utils/command-helpers.ts +6 -2
  126. package/src/utils/global-flags.ts +102 -17
  127. package/src/utils/telemetry.ts +27 -52
  128. package/src/utils/terminal-ui.ts +44 -23
  129. package/dist/cli-errors-CF60g2cG.mjs.map +0 -1
  130. package/dist/client-Brv4qlfB.mjs.map +0 -1
  131. package/dist/command-helpers-Dvgul7UA.mjs.map +0 -1
  132. package/dist/contract-emit-BDBzHlaC.mjs.map +0 -1
  133. package/dist/contract-infer-Dm8pBZMR.mjs.map +0 -1
  134. package/dist/db-verify-CW8DR5Ei.mjs.map +0 -1
  135. package/dist/errors-BYAXmyRJ.mjs +0 -56
  136. package/dist/errors-BYAXmyRJ.mjs.map +0 -1
  137. package/dist/global-flags-DGmw6Kqg.d.mts.map +0 -1
  138. package/dist/init-CxS9eqbQ.mjs.map +0 -1
  139. package/dist/is-ci-YyvQBBke.mjs +0 -44
  140. package/dist/is-ci-YyvQBBke.mjs.map +0 -1
  141. package/dist/migration-plan-BSzcWsvm.mjs.map +0 -1
  142. package/dist/result-handler-CG3vVoKf.mjs +0 -25
  143. package/dist/result-handler-CG3vVoKf.mjs.map +0 -1
  144. package/dist/terminal-ui-XtOQsqe9.mjs.map +0 -1
  145. /package/dist/{cli-errors-DdcjVLJV.d.mts → cli-errors-Czmx92Zy.d.mts} +0 -0
@@ -8,7 +8,7 @@ import { Command } from 'commander';
8
8
  import { loadConfig } from '../config-loader';
9
9
  import { createControlClient } from '../control-api/client';
10
10
  import {
11
- type CliStructuredError,
11
+ CliStructuredError,
12
12
  errorDatabaseConnectionRequired,
13
13
  errorDriverRequired,
14
14
  errorUnexpected,
@@ -26,9 +26,9 @@ import {
26
26
  } from '../utils/command-helpers';
27
27
  import { formatStyledHeader } from '../utils/formatters/styled';
28
28
  import type { CommonCommandOptions } from '../utils/global-flags';
29
- import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
29
+ import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
30
30
  import { handleResult } from '../utils/result-handler';
31
- import { TerminalUI } from '../utils/terminal-ui';
31
+ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
32
32
 
33
33
  interface MigrationLogOptions extends CommonCommandOptions {
34
34
  readonly db?: string;
@@ -159,6 +159,7 @@ async function executeMigrationLogCommand(
159
159
  summary: `${entries.length} migration(s) applied`,
160
160
  });
161
161
  } catch (error) {
162
+ if (CliStructuredError.is(error)) return notOk(error);
162
163
  if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
163
164
  return notOk(
164
165
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -192,8 +193,8 @@ export function createMigrationLogCommand(): Command {
192
193
  .option('--db <url>', 'Database connection string')
193
194
  .option('--config <path>', 'Path to prisma-next.config.ts')
194
195
  .action(async (options: MigrationLogOptions) => {
195
- const flags = parseGlobalFlags(options);
196
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
196
+ const flags = parseGlobalFlagsOrExit(options);
197
+ const ui = createTerminalUI(flags);
197
198
  const result = await executeMigrationLogCommand(options, flags, ui);
198
199
  const exitCode = handleResult(result, flags, ui, (logResult) => {
199
200
  if (flags.json) {
@@ -49,9 +49,9 @@ import {
49
49
  import { formatStyledHeader } from '../utils/formatters/styled';
50
50
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
51
51
  import type { CommonCommandOptions } from '../utils/global-flags';
52
- import { parseGlobalFlags } from '../utils/global-flags';
52
+ import { parseGlobalFlagsOrExit } from '../utils/global-flags';
53
53
  import { handleResult } from '../utils/result-handler';
54
- import { TerminalUI } from '../utils/terminal-ui';
54
+ import { createTerminalUI } from '../utils/terminal-ui';
55
55
 
56
56
  interface MigrationNewOptions extends CommonCommandOptions {
57
57
  readonly name?: string;
@@ -287,8 +287,8 @@ export function createMigrationNewCommand(): Command {
287
287
  .option('--from <hash>', 'Starting contract hash (default: latest migration target)')
288
288
  .option('--config <path>', 'Path to prisma-next.config.ts')
289
289
  .action(async (options: MigrationNewOptions) => {
290
- const flags = parseGlobalFlags(options);
291
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
290
+ const flags = parseGlobalFlagsOrExit(options);
291
+ const ui = createTerminalUI(flags);
292
292
 
293
293
  if (!flags.json && !flags.quiet) {
294
294
  const header = formatStyledHeader({
@@ -51,9 +51,9 @@ import { toExtensionInputs } from '../utils/extension-pack-inputs';
51
51
  import { formatStyledHeader } from '../utils/formatters/styled';
52
52
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
53
53
  import type { CommonCommandOptions } from '../utils/global-flags';
54
- import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
54
+ import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
55
55
  import { handleResult } from '../utils/result-handler';
56
- import { TerminalUI } from '../utils/terminal-ui';
56
+ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
57
57
 
58
58
  interface MigrationPlanOptions extends CommonCommandOptions {
59
59
  readonly config?: string;
@@ -551,10 +551,10 @@ export function createMigrationPlanCommand(): Command {
551
551
  'Starting contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
552
552
  )
553
553
  .action(async (options: MigrationPlanOptions) => {
554
- const flags = parseGlobalFlags(options);
554
+ const flags = parseGlobalFlagsOrExit(options);
555
555
  const startTime = Date.now();
556
556
 
557
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
557
+ const ui = createTerminalUI(flags);
558
558
  const result = await executeMigrationPlanCommand(options, flags, ui, startTime);
559
559
 
560
560
  const exitCode = handleResult(result, flags, ui, (planResult) => {
@@ -43,9 +43,9 @@ import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-l
43
43
  import { formatMigrationShowOutput } from '../utils/formatters/migrations';
44
44
  import { formatStyledHeader } from '../utils/formatters/styled';
45
45
  import type { CommonCommandOptions } from '../utils/global-flags';
46
- import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
46
+ import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
47
47
  import { handleResult } from '../utils/result-handler';
48
- import { TerminalUI } from '../utils/terminal-ui';
48
+ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
49
49
 
50
50
  interface MigrationShowOptions extends CommonCommandOptions {
51
51
  readonly config?: string;
@@ -495,9 +495,9 @@ export function createMigrationShowCommand(): Command {
495
495
  )
496
496
  .option('--config <path>', 'Path to prisma-next.config.ts')
497
497
  .action(async (target: string | undefined, options: MigrationShowOptions) => {
498
- const flags = parseGlobalFlags(options);
498
+ const flags = parseGlobalFlagsOrExit(options);
499
499
 
500
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
500
+ const ui = createTerminalUI(flags);
501
501
 
502
502
  const result = await executeMigrationShowCommand(target, options, flags, ui);
503
503
 
@@ -30,7 +30,7 @@ import { Command } from 'commander';
30
30
  import { loadConfig } from '../config-loader';
31
31
  import { createControlClient } from '../control-api/client';
32
32
  import {
33
- type CliStructuredError,
33
+ CliStructuredError,
34
34
  errorRuntime,
35
35
  errorUnexpected,
36
36
  mapMigrationToolsError,
@@ -65,10 +65,10 @@ import {
65
65
  } from '../utils/formatters/graph-render';
66
66
  import { formatStyledHeader } from '../utils/formatters/styled';
67
67
  import type { CommonCommandOptions } from '../utils/global-flags';
68
- import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
68
+ import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
69
69
  import type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
70
70
  import { handleResult } from '../utils/result-handler';
71
- import { TerminalUI } from '../utils/terminal-ui';
71
+ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
72
72
 
73
73
  interface MigrationStatusOptions extends CommonCommandOptions {
74
74
  readonly db?: string;
@@ -540,6 +540,40 @@ async function loadContractRawSafely(config: {
540
540
  }
541
541
  }
542
542
 
543
+ async function validateOnlineMarkerRead(
544
+ config: Awaited<ReturnType<typeof loadConfig>>,
545
+ dbConnection: unknown,
546
+ ): Promise<Result<void, CliStructuredError>> {
547
+ const driver = config.driver;
548
+ if (!driver) {
549
+ return ok(undefined);
550
+ }
551
+
552
+ const client = createControlClient({
553
+ family: config.family,
554
+ target: config.target,
555
+ adapter: config.adapter,
556
+ driver,
557
+ extensionPacks: config.extensionPacks ?? [],
558
+ });
559
+ try {
560
+ await client.connect(dbConnection);
561
+ await client.readMarker();
562
+ return ok(undefined);
563
+ } catch (error) {
564
+ if (CliStructuredError.is(error)) {
565
+ return notOk(error);
566
+ }
567
+ return notOk(
568
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
569
+ why: `Failed to read database marker: ${error instanceof Error ? error.message : String(error)}`,
570
+ }),
571
+ );
572
+ } finally {
573
+ await client.close();
574
+ }
575
+ }
576
+
543
577
  async function executeMigrationStatusCommand(
544
578
  options: MigrationStatusOptions,
545
579
  flags: GlobalFlags,
@@ -666,6 +700,12 @@ async function executeMigrationStatusCommand(
666
700
  }
667
701
 
668
702
  if (bundles.length === 0) {
703
+ if (dbConnection && hasDriver) {
704
+ const markerProbe = await validateOnlineMarkerRead(config, dbConnection);
705
+ if (!markerProbe.ok) {
706
+ return markerProbe;
707
+ }
708
+ }
669
709
  if (contractHash !== EMPTY_CONTRACT_HASH) {
670
710
  diagnostics.push({
671
711
  code: 'CONTRACT.AHEAD',
@@ -750,7 +790,10 @@ async function executeMigrationStatusCommand(
750
790
  // space has no marker", which is a different condition).
751
791
  allMarkers = null;
752
792
  }
753
- } catch {
793
+ } catch (error) {
794
+ if (CliStructuredError.is(error)) {
795
+ return notOk(error);
796
+ }
754
797
  if (!flags.json && !flags.quiet) {
755
798
  ui.warn('Could not connect to database — showing offline status');
756
799
  }
@@ -1097,8 +1140,8 @@ export function createMigrationStatusCommand(): Command {
1097
1140
  'Origin contract reference; same grammar as --to. Supplying --from switches to offline path computation.',
1098
1141
  )
1099
1142
  .action(async (options: MigrationStatusOptions) => {
1100
- const flags = parseGlobalFlags(options);
1101
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
1143
+ const flags = parseGlobalFlagsOrExit(options);
1144
+ const ui = createTerminalUI(flags);
1102
1145
 
1103
1146
  const result = await executeMigrationStatusCommand(options, flags, ui);
1104
1147
 
@@ -25,9 +25,9 @@ import {
25
25
  setCommandDescriptions,
26
26
  } from '../utils/command-helpers';
27
27
  import { formatCommandHelp } from '../utils/formatters/help';
28
- import { parseGlobalFlags } from '../utils/global-flags';
28
+ import { parseGlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
29
29
  import { handleResult } from '../utils/result-handler';
30
- import { TerminalUI } from '../utils/terminal-ui';
30
+ import { createTerminalUI } from '../utils/terminal-ui';
31
31
 
32
32
  interface RefSetResult {
33
33
  readonly ok: true;
@@ -145,8 +145,8 @@ function createRefSetCommand(): Command {
145
145
  hash: string,
146
146
  options: { config?: string; json?: string | boolean; quiet?: boolean },
147
147
  ) => {
148
- const flags = parseGlobalFlags(options);
149
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
148
+ const flags = parseGlobalFlagsOrExit(options);
149
+ const ui = createTerminalUI(flags);
150
150
  const result = await executeRefSetCommand(name, hash, options);
151
151
  const exitCode = handleResult(result, flags, ui, (value) => {
152
152
  if (flags.json) {
@@ -172,8 +172,8 @@ function createRefDeleteCommand(): Command {
172
172
  name: string,
173
173
  options: { config?: string; json?: string | boolean; quiet?: boolean },
174
174
  ) => {
175
- const flags = parseGlobalFlags(options);
176
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
175
+ const flags = parseGlobalFlagsOrExit(options);
176
+ const ui = createTerminalUI(flags);
177
177
  const result = await executeRefDeleteCommand(name, options);
178
178
  const exitCode = handleResult(result, flags, ui, (value) => {
179
179
  if (flags.json) {
@@ -194,8 +194,8 @@ function createRefListCommand(): Command {
194
194
  addGlobalOptions(command)
195
195
  .option('--config <path>', 'Path to prisma-next.config.ts')
196
196
  .action(async (options: { config?: string; json?: string | boolean; quiet?: boolean }) => {
197
- const flags = parseGlobalFlags(options);
198
- const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
197
+ const flags = parseGlobalFlagsOrExit(options);
198
+ const ui = createTerminalUI(flags);
199
199
  const result = await executeRefListCommand(options);
200
200
  const exitCode = handleResult(result, flags, ui, (value) => {
201
201
  if (flags.json) {
@@ -178,6 +178,7 @@ export async function applyAggregate<TFamilyId extends string, TTargetId extends
178
178
  meta: {
179
179
  ...(runnerResult.failure.meta ?? {}),
180
180
  failingSpace: runnerResult.failure.failingSpace,
181
+ runnerErrorCode: runnerResult.failure.code,
181
182
  },
182
183
  });
183
184
  }
@@ -16,9 +16,11 @@ import {
16
16
  errorDriverRequired,
17
17
  errorFamilyReadMarkerSqlRequired,
18
18
  errorFileNotFound,
19
+ errorInvalidOutputFormat,
19
20
  errorMigrationCliInvalidConfigArg,
20
21
  errorMigrationCliUnknownFlag,
21
22
  errorMigrationPlanningFailed,
23
+ errorOutputFormatMutex,
22
24
  errorQueryRunnerFactoryRequired,
23
25
  errorTargetMigrationNotSupported,
24
26
  errorUnexpected,
@@ -56,9 +58,11 @@ export {
56
58
  errorDriverRequired,
57
59
  errorFamilyReadMarkerSqlRequired,
58
60
  errorFileNotFound,
61
+ errorInvalidOutputFormat,
59
62
  errorMigrationCliInvalidConfigArg,
60
63
  errorMigrationCliUnknownFlag,
61
64
  errorMigrationPlanningFailed,
65
+ errorOutputFormatMutex,
62
66
  errorQueryRunnerFactoryRequired,
63
67
  errorTargetMigrationNotSupported,
64
68
  errorUnexpected,
@@ -365,7 +365,7 @@ export function sanitizeErrorMessage(message: string, connectionUrl?: string): s
365
365
 
366
366
  /**
367
367
  * Registers the global CLI options shared by every command:
368
- * --json, -q/--quiet, -v/--verbose, --trace, --color, --no-color,
368
+ * --format, --json, -q/--quiet, -v/--verbose, --trace, --color, --no-color,
369
369
  * --interactive, --no-interactive, -y/--yes.
370
370
  *
371
371
  * Also sets up the styled help formatter.
@@ -378,7 +378,11 @@ export function addGlobalOptions(command: Command): Command {
378
378
  return formatCommandHelp({ command: cmd, flags });
379
379
  },
380
380
  })
381
- .option('--json', 'Output as JSON')
381
+ .option(
382
+ '--format <pretty|json>',
383
+ 'Output format (default: pretty, or json when stdout is not a TTY)',
384
+ )
385
+ .option('--json', 'Output as JSON (alias for --format json)')
382
386
  .option('-q, --quiet', 'Quiet mode: errors only')
383
387
  .option('-v, --verbose', 'Verbose output: debug info, timings')
384
388
  .option('--trace', 'Trace output: deep internals, stack traces')
@@ -1,9 +1,17 @@
1
+ import { notOk } from '@prisma-next/utils/result';
2
+ import { CliStructuredError, errorInvalidOutputFormat, errorOutputFormatMutex } from './cli-errors';
1
3
  import { isCI } from './is-ci';
4
+ import { handleResult } from './result-handler';
5
+ import { createTerminalUI } from './terminal-ui';
6
+
7
+ export type OutputFormat = 'pretty' | 'json';
2
8
 
3
9
  export interface GlobalFlags {
10
+ readonly format: OutputFormat;
11
+ readonly explicitFormat: boolean;
4
12
  readonly json?: boolean;
5
13
  readonly quiet?: boolean;
6
- readonly verbose?: number; // 0, 1, or 2
14
+ readonly verbose?: number;
7
15
  readonly color?: boolean;
8
16
  readonly interactive?: boolean;
9
17
  readonly yes?: boolean;
@@ -14,6 +22,7 @@ export interface GlobalFlags {
14
22
  * Extend this for command-specific options instead of duplicating these fields.
15
23
  */
16
24
  export interface CommonCommandOptions {
25
+ readonly format?: string;
17
26
  readonly json?: string | boolean;
18
27
  readonly quiet?: boolean;
19
28
  readonly q?: boolean;
@@ -28,33 +37,117 @@ export interface CommonCommandOptions {
28
37
  readonly y?: boolean;
29
38
  }
30
39
 
40
+ function isJsonFlagSet(json: string | boolean | undefined): boolean {
41
+ return json === true;
42
+ }
43
+
44
+ interface ResolvedOutputFormat {
45
+ readonly format: OutputFormat;
46
+ readonly explicitFormat: boolean;
47
+ }
48
+
49
+ function resolveOutputFormat(options: CommonCommandOptions): ResolvedOutputFormat {
50
+ const formatOption = options.format;
51
+ const jsonFlag = isJsonFlagSet(options.json);
52
+
53
+ if (formatOption !== undefined) {
54
+ if (formatOption !== 'pretty' && formatOption !== 'json') {
55
+ throw errorInvalidOutputFormat(formatOption);
56
+ }
57
+ if (jsonFlag && formatOption === 'pretty') {
58
+ throw errorOutputFormatMutex();
59
+ }
60
+ return { format: formatOption, explicitFormat: true };
61
+ }
62
+
63
+ if (jsonFlag) {
64
+ return { format: 'json', explicitFormat: false };
65
+ }
66
+
67
+ if (!process.stdout.isTTY) {
68
+ return { format: 'json', explicitFormat: false };
69
+ }
70
+
71
+ return { format: 'pretty', explicitFormat: false };
72
+ }
73
+
74
+ function inferJsonModeForParseError(options: CommonCommandOptions): boolean {
75
+ if (options.format === 'json') {
76
+ return true;
77
+ }
78
+ if (isJsonFlagSet(options.json) && options.format !== 'pretty') {
79
+ return true;
80
+ }
81
+ if (options.format !== undefined) {
82
+ return false;
83
+ }
84
+ return !process.stdout.isTTY;
85
+ }
86
+
87
+ function emitGlobalFlagParseError(error: CliStructuredError, options: CommonCommandOptions): never {
88
+ const jsonMode = inferJsonModeForParseError(options);
89
+ const flags: GlobalFlags = {
90
+ format: jsonMode ? 'json' : 'pretty',
91
+ explicitFormat: false,
92
+ ...(jsonMode ? { json: true } : {}),
93
+ color: false,
94
+ verbose: 0,
95
+ interactive: false,
96
+ };
97
+ const ui = createTerminalUI(flags);
98
+ const exitCode = handleResult(notOk(error), flags, ui);
99
+ process.exit(exitCode);
100
+ }
101
+
102
+ /**
103
+ * Parses global flags from CLI options.
104
+ * Handles verbosity flags (-v, --trace), output format (--format, --json),
105
+ * quiet mode, color, interactivity (--interactive/--no-interactive), and
106
+ * auto-accept (-y/--yes).
107
+ *
108
+ * On invalid or conflicting format flags, prints a structured CLI error
109
+ * envelope and exits with code 2.
110
+ */
111
+ export function parseGlobalFlagsOrExit(options: CommonCommandOptions): GlobalFlags {
112
+ try {
113
+ return parseGlobalFlags(options);
114
+ } catch (error) {
115
+ if (CliStructuredError.is(error)) {
116
+ emitGlobalFlagParseError(error, options);
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
31
122
  /**
32
123
  * Parses global flags from CLI options.
33
- * Handles verbosity flags (-v, --trace), JSON output, quiet mode, color,
34
- * interactivity (--interactive/--no-interactive), and auto-accept (-y/--yes).
124
+ * Handles verbosity flags (-v, --trace), output format (--format, --json),
125
+ * quiet mode, color, interactivity (--interactive/--no-interactive), and
126
+ * auto-accept (-y/--yes).
127
+ *
128
+ * Throws {@link CliStructuredError} for invalid or conflicting format flags.
35
129
  */
36
130
  export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
131
+ const { format, explicitFormat } = resolveOutputFormat(options);
37
132
  const flags: {
133
+ format: OutputFormat;
134
+ explicitFormat: boolean;
38
135
  json?: boolean;
39
136
  quiet?: boolean;
40
137
  verbose?: number;
41
138
  color?: boolean;
42
139
  interactive?: boolean;
43
140
  yes?: boolean;
44
- } = {};
141
+ } = { format, explicitFormat };
45
142
 
46
- // JSON output: explicit --json flag or auto-detect piped stdout (Unix convention)
47
- if (options.json || !process.stdout.isTTY) {
143
+ if (format === 'json') {
48
144
  flags.json = true;
49
145
  }
50
146
 
51
- // Quiet mode
52
147
  if (options.quiet || options.q) {
53
148
  flags.quiet = true;
54
149
  }
55
150
 
56
- // Verbosity: -v = 1, --trace = 2
57
- // Env toggles: PRISMA_NEXT_TRACE=1 ≅ --trace, PRISMA_NEXT_DEBUG=1 ≅ -v
58
151
  if (options.trace || process.env['PRISMA_NEXT_TRACE'] === '1') {
59
152
  flags.verbose = 2;
60
153
  } else if (options.verbose || options.v || process.env['PRISMA_NEXT_DEBUG'] === '1') {
@@ -63,8 +156,6 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
63
156
  flags.verbose = 0;
64
157
  }
65
158
 
66
- // Color: respect NO_COLOR env var, --color/--no-color flags
67
- // When JSON output is enabled, disable color to ensure clean JSON output
68
159
  if (process.env['NO_COLOR'] || flags.json) {
69
160
  flags.color = false;
70
161
  } else if (options['no-color']) {
@@ -72,14 +163,9 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
72
163
  } else if (options.color !== undefined) {
73
164
  flags.color = options.color;
74
165
  } else {
75
- // Default: enable color if TTY and not in CI. Uses the consolidated
76
- // `isCI()` helper (`./is-ci.ts`) — the single source of truth shared
77
- // with telemetry-skip detection.
78
166
  flags.color = process.stdout.isTTY && !isCI();
79
167
  }
80
168
 
81
- // Interactivity: --interactive/--no-interactive
82
- // Default: interactive when stdout is a TTY
83
169
  if (options['no-interactive']) {
84
170
  flags.interactive = false;
85
171
  } else if (options.interactive !== undefined) {
@@ -88,7 +174,6 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
88
174
  flags.interactive = !!process.stdout.isTTY;
89
175
  }
90
176
 
91
- // Auto-accept prompts: -y/--yes
92
177
  if (options.yes || options.y) {
93
178
  flags.yes = true;
94
179
  }
@@ -8,16 +8,11 @@ import {
8
8
  type TelemetryRunOutcome,
9
9
  type UserConfig,
10
10
  } from '@prisma-next/cli-telemetry';
11
+ import { ifDefined } from '@prisma-next/utils/defined';
11
12
  import type { Command } from 'commander';
12
13
  import { version as CLI_VERSION } from '../../package.json' with { type: 'json' };
13
- import { loadConfig } from '../config-loader';
14
14
  import { isCI } from './is-ci';
15
15
 
16
- interface TelemetryFields {
17
- readonly databaseTarget: string | null;
18
- readonly extensions: readonly string[];
19
- }
20
-
21
16
  type TelemetryGate =
22
17
  | { readonly enabled: true; readonly userConfig: UserConfig }
23
18
  | { readonly enabled: false; readonly outcome: TelemetryRunOutcome };
@@ -73,32 +68,6 @@ function resolveTelemetryGate(): TelemetryGate {
73
68
  return { enabled: true, userConfig };
74
69
  }
75
70
 
76
- /**
77
- * Best-effort extraction of `databaseTarget` and `extensions` from the
78
- * project config. Loads via the same `c12`-backed loader the action
79
- * handlers use so we honour the same lookup rules. Every failure mode
80
- * (no config file, malformed config, async load reject) collapses to
81
- * `(null, [])` because telemetry is non-blocking and may fire for
82
- * commands that legitimately don't have a config (e.g. `init`).
83
- */
84
- async function loadConfigForTelemetry(): Promise<TelemetryFields> {
85
- try {
86
- const config = await loadConfig();
87
- const target = config.target as { readonly targetId?: unknown } | undefined;
88
- const databaseTarget =
89
- target !== undefined && typeof target.targetId === 'string' ? target.targetId : null;
90
- const extensionPacks = (config.extensionPacks ?? []) as ReadonlyArray<{
91
- readonly id?: unknown;
92
- }>;
93
- const extensions = extensionPacks
94
- .map((pack) => pack.id)
95
- .filter((id): id is string => typeof id === 'string');
96
- return { databaseTarget, extensions };
97
- } catch {
98
- return { databaseTarget: null, extensions: [] };
99
- }
100
- }
101
-
102
71
  /**
103
72
  * Path to the compiled sender script inside `@prisma-next/cli-telemetry`'s
104
73
  * `dist/`. Resolved off this module's `import.meta.url` via the package
@@ -109,40 +78,41 @@ function senderPath(): string {
109
78
  return fileURLToPath(new URL(import.meta.resolve('@prisma-next/cli-telemetry/sender')));
110
79
  }
111
80
 
112
- function fireTelemetryWithFields(
81
+ function fireTelemetry(
113
82
  actionCommand: Command,
114
- fields: TelemetryFields,
115
83
  userConfig: UserConfig,
84
+ overrides: { readonly databaseTarget?: string } = {},
116
85
  ): TelemetryRunOutcome {
117
86
  return runTelemetry({
118
87
  command: commanderSnapshotForTelemetry(actionCommand),
119
88
  version: CLI_VERSION,
120
- databaseTarget: fields.databaseTarget,
121
- extensions: fields.extensions,
122
89
  projectRoot: process.cwd(),
123
90
  senderPath: senderPath(),
124
91
  isCI: isCI(),
125
92
  env: process.env,
126
93
  userConfig,
94
+ ...ifDefined('databaseTarget', overrides.databaseTarget),
127
95
  });
128
96
  }
129
97
 
130
98
  /**
131
- * preAction-stage entry point: resolve env/CI/user-consent gates first,
132
- * then (only when enabled) load project config for database target and
133
- * extension metadata, then fork the detached sender. The early gate is
134
- * privacy- and UX-critical: config loading can execute user code and
135
- * must never happen before opt-out/default-off checks have resolved.
99
+ * preAction-stage entry point. Synchronous by construction: resolve
100
+ * env/CI/user-consent gates (cheap, all in-memory and a single tiny
101
+ * user-config read), then only when enabled `fork()` the detached
102
+ * sender script. The forked child loads `prisma-next.config.*` via
103
+ * c12 on its own (see `loadProjectConfig` in cli-telemetry); the
104
+ * parent does no project-config I/O on the command's hot path.
105
+ *
106
+ * Privacy invariant: gate resolution always happens before any project
107
+ * config touches disk. The child loading user TS code is acceptable
108
+ * only because it's gated behind the same resolved-enabled signal.
136
109
  */
137
- export async function fireTelemetryFromPreAction(
138
- actionCommand: Command,
139
- ): Promise<TelemetryRunOutcome> {
110
+ export function fireTelemetryFromPreAction(actionCommand: Command): TelemetryRunOutcome {
140
111
  const gate = resolveTelemetryGate();
141
112
  if (!gate.enabled) {
142
113
  return gate.outcome;
143
114
  }
144
- const fields = await loadConfigForTelemetry();
145
- return fireTelemetryWithFields(actionCommand, fields, gate.userConfig);
115
+ return fireTelemetry(actionCommand, gate.userConfig);
146
116
  }
147
117
 
148
118
  /**
@@ -152,15 +122,20 @@ export async function fireTelemetryFromPreAction(
152
122
  * default-off. After consent is persisted, `runInit` calls this helper
153
123
  * exactly for that first affirmative answer; subsequent init runs skip
154
124
  * it because the prompt is not shown again.
125
+ *
126
+ * The child's c12 load would return `databaseTarget: null` for this
127
+ * specific invocation because `prisma-next.config.*` is not yet on
128
+ * disk (init writes it later in the same run). To preserve the
129
+ * prompt-chosen target in the first-init telemetry event, this
130
+ * helper forwards the value as a parent-side IPC override on
131
+ * `ParentToSenderPayload.databaseTarget` — the child consults the
132
+ * override first and falls back to its c12 result when absent.
155
133
  */
156
134
  export function fireTelemetryAfterInitConsent(
157
135
  actionCommand: Command,
158
136
  inputs: { readonly databaseTarget: string },
159
137
  ): TelemetryRunOutcome {
160
- const userConfig = readUserConfig();
161
- return fireTelemetryWithFields(
162
- actionCommand,
163
- { databaseTarget: inputs.databaseTarget, extensions: [] },
164
- userConfig,
165
- );
138
+ return fireTelemetry(actionCommand, readUserConfig(), {
139
+ databaseTarget: inputs.databaseTarget,
140
+ });
166
141
  }