@prisma-next/cli 0.3.0-dev.6 → 0.3.0-dev.63

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 (180) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +314 -80
  3. package/dist/cli-errors-JlPTsazx.mjs +3 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.js +1 -2376
  6. package/dist/cli.mjs +198 -0
  7. package/dist/cli.mjs.map +1 -0
  8. package/dist/client-PimzSD1f.mjs +981 -0
  9. package/dist/client-PimzSD1f.mjs.map +1 -0
  10. package/dist/commands/contract-emit.d.mts +7 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -0
  12. package/dist/commands/contract-emit.mjs +150 -0
  13. package/dist/commands/contract-emit.mjs.map +1 -0
  14. package/dist/commands/db-init.d.mts +7 -0
  15. package/dist/commands/db-init.d.mts.map +1 -0
  16. package/dist/commands/db-init.mjs +132 -0
  17. package/dist/commands/db-init.mjs.map +1 -0
  18. package/dist/commands/db-introspect.d.mts +7 -0
  19. package/dist/commands/db-introspect.d.mts.map +1 -0
  20. package/dist/commands/db-introspect.mjs +117 -0
  21. package/dist/commands/db-introspect.mjs.map +1 -0
  22. package/dist/commands/db-schema-verify.d.mts +7 -0
  23. package/dist/commands/db-schema-verify.d.mts.map +1 -0
  24. package/dist/commands/db-schema-verify.mjs +119 -0
  25. package/dist/commands/db-schema-verify.mjs.map +1 -0
  26. package/dist/commands/db-sign.d.mts +7 -0
  27. package/dist/commands/db-sign.d.mts.map +1 -0
  28. package/dist/commands/db-sign.mjs +141 -0
  29. package/dist/commands/db-sign.mjs.map +1 -0
  30. package/dist/commands/db-update.d.mts +7 -0
  31. package/dist/commands/db-update.d.mts.map +1 -0
  32. package/dist/commands/db-update.mjs +121 -0
  33. package/dist/commands/db-update.mjs.map +1 -0
  34. package/dist/commands/db-verify.d.mts +7 -0
  35. package/dist/commands/db-verify.d.mts.map +1 -0
  36. package/dist/commands/db-verify.mjs +132 -0
  37. package/dist/commands/db-verify.mjs.map +1 -0
  38. package/dist/commands/migration-apply.d.mts +23 -0
  39. package/dist/commands/migration-apply.d.mts.map +1 -0
  40. package/dist/commands/migration-apply.mjs +249 -0
  41. package/dist/commands/migration-apply.mjs.map +1 -0
  42. package/dist/commands/migration-plan.d.mts +25 -0
  43. package/dist/commands/migration-plan.d.mts.map +1 -0
  44. package/dist/commands/migration-plan.mjs +266 -0
  45. package/dist/commands/migration-plan.mjs.map +1 -0
  46. package/dist/commands/migration-show.d.mts +28 -0
  47. package/dist/commands/migration-show.d.mts.map +1 -0
  48. package/dist/commands/migration-show.mjs +138 -0
  49. package/dist/commands/migration-show.mjs.map +1 -0
  50. package/dist/commands/migration-status.d.mts +35 -0
  51. package/dist/commands/migration-status.d.mts.map +1 -0
  52. package/dist/commands/migration-status.mjs +259 -0
  53. package/dist/commands/migration-status.mjs.map +1 -0
  54. package/dist/commands/migration-verify.d.mts +16 -0
  55. package/dist/commands/migration-verify.d.mts.map +1 -0
  56. package/dist/commands/migration-verify.mjs +86 -0
  57. package/dist/commands/migration-verify.mjs.map +1 -0
  58. package/dist/config-loader-PPf4CtDj.mjs +43 -0
  59. package/dist/config-loader-PPf4CtDj.mjs.map +1 -0
  60. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  61. package/dist/config-loader.d.mts.map +1 -0
  62. package/dist/config-loader.mjs +3 -0
  63. package/dist/exports/config-types.d.mts +2 -0
  64. package/dist/exports/config-types.mjs +3 -0
  65. package/dist/exports/control-api.d.mts +621 -0
  66. package/dist/exports/control-api.d.mts.map +1 -0
  67. package/dist/exports/control-api.mjs +97 -0
  68. package/dist/exports/control-api.mjs.map +1 -0
  69. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  70. package/dist/exports/index.d.mts.map +1 -0
  71. package/dist/exports/index.mjs +130 -0
  72. package/dist/exports/index.mjs.map +1 -0
  73. package/dist/extract-sql-ddl-BmlKvk4o.mjs +26 -0
  74. package/dist/extract-sql-ddl-BmlKvk4o.mjs.map +1 -0
  75. package/dist/framework-components-CjV_jD8f.mjs +59 -0
  76. package/dist/framework-components-CjV_jD8f.mjs.map +1 -0
  77. package/dist/migration-command-scaffold-DfY_F3ev.mjs +97 -0
  78. package/dist/migration-command-scaffold-DfY_F3ev.mjs.map +1 -0
  79. package/dist/progress-adapter-DENrzF6I.mjs +49 -0
  80. package/dist/progress-adapter-DENrzF6I.mjs.map +1 -0
  81. package/dist/result-handler-iA9JtUC7.mjs +1186 -0
  82. package/dist/result-handler-iA9JtUC7.mjs.map +1 -0
  83. package/package.json +75 -38
  84. package/src/cli.ts +43 -0
  85. package/src/commands/contract-emit.ts +221 -111
  86. package/src/commands/db-init.ts +217 -426
  87. package/src/commands/db-introspect.ts +148 -185
  88. package/src/commands/db-schema-verify.ts +162 -149
  89. package/src/commands/db-sign.ts +215 -202
  90. package/src/commands/db-update.ts +220 -0
  91. package/src/commands/db-verify.ts +193 -156
  92. package/src/commands/migration-apply.ts +431 -0
  93. package/src/commands/migration-plan.ts +446 -0
  94. package/src/commands/migration-show.ts +255 -0
  95. package/src/commands/migration-status.ts +436 -0
  96. package/src/commands/migration-verify.ts +151 -0
  97. package/src/config-loader.ts +13 -3
  98. package/src/control-api/client.ts +605 -0
  99. package/src/control-api/errors.ts +9 -0
  100. package/src/control-api/operations/contract-emit.ts +161 -0
  101. package/src/control-api/operations/db-init.ts +286 -0
  102. package/src/control-api/operations/db-update.ts +221 -0
  103. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  104. package/src/control-api/operations/migration-apply.ts +195 -0
  105. package/src/control-api/operations/migration-helpers.ts +49 -0
  106. package/src/control-api/types.ts +687 -0
  107. package/src/exports/config-types.ts +3 -3
  108. package/src/exports/control-api.ts +53 -0
  109. package/src/load-ts-contract.ts +16 -11
  110. package/src/utils/cli-errors.ts +3 -1
  111. package/src/utils/command-helpers.ts +92 -3
  112. package/src/utils/framework-components.ts +11 -30
  113. package/src/utils/migration-command-scaffold.ts +190 -0
  114. package/src/utils/output.ts +363 -25
  115. package/src/utils/progress-adapter.ts +86 -0
  116. package/dist/chunk-464LNZCE.js +0 -134
  117. package/dist/chunk-464LNZCE.js.map +0 -1
  118. package/dist/chunk-BZMBKEEQ.js +0 -997
  119. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  120. package/dist/chunk-HWYQOCAJ.js +0 -47
  121. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  122. package/dist/chunk-ZKYEJROM.js +0 -94
  123. package/dist/chunk-ZKYEJROM.js.map +0 -1
  124. package/dist/cli.d.ts +0 -2
  125. package/dist/cli.d.ts.map +0 -1
  126. package/dist/cli.js.map +0 -1
  127. package/dist/commands/contract-emit.d.ts +0 -3
  128. package/dist/commands/contract-emit.d.ts.map +0 -1
  129. package/dist/commands/contract-emit.js +0 -9
  130. package/dist/commands/contract-emit.js.map +0 -1
  131. package/dist/commands/db-init.d.ts +0 -3
  132. package/dist/commands/db-init.d.ts.map +0 -1
  133. package/dist/commands/db-init.js +0 -341
  134. package/dist/commands/db-init.js.map +0 -1
  135. package/dist/commands/db-introspect.d.ts +0 -3
  136. package/dist/commands/db-introspect.d.ts.map +0 -1
  137. package/dist/commands/db-introspect.js +0 -190
  138. package/dist/commands/db-introspect.js.map +0 -1
  139. package/dist/commands/db-schema-verify.d.ts +0 -3
  140. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  141. package/dist/commands/db-schema-verify.js +0 -164
  142. package/dist/commands/db-schema-verify.js.map +0 -1
  143. package/dist/commands/db-sign.d.ts +0 -3
  144. package/dist/commands/db-sign.d.ts.map +0 -1
  145. package/dist/commands/db-sign.js +0 -199
  146. package/dist/commands/db-sign.js.map +0 -1
  147. package/dist/commands/db-verify.d.ts +0 -3
  148. package/dist/commands/db-verify.d.ts.map +0 -1
  149. package/dist/commands/db-verify.js +0 -173
  150. package/dist/commands/db-verify.js.map +0 -1
  151. package/dist/config-loader.d.ts.map +0 -1
  152. package/dist/config-loader.js +0 -7
  153. package/dist/config-loader.js.map +0 -1
  154. package/dist/exports/config-types.d.ts +0 -3
  155. package/dist/exports/config-types.d.ts.map +0 -1
  156. package/dist/exports/config-types.js +0 -6
  157. package/dist/exports/config-types.js.map +0 -1
  158. package/dist/exports/index.d.ts +0 -4
  159. package/dist/exports/index.d.ts.map +0 -1
  160. package/dist/exports/index.js +0 -175
  161. package/dist/exports/index.js.map +0 -1
  162. package/dist/load-ts-contract.d.ts.map +0 -1
  163. package/dist/utils/action.d.ts +0 -16
  164. package/dist/utils/action.d.ts.map +0 -1
  165. package/dist/utils/cli-errors.d.ts +0 -7
  166. package/dist/utils/cli-errors.d.ts.map +0 -1
  167. package/dist/utils/command-helpers.d.ts +0 -12
  168. package/dist/utils/command-helpers.d.ts.map +0 -1
  169. package/dist/utils/framework-components.d.ts +0 -81
  170. package/dist/utils/framework-components.d.ts.map +0 -1
  171. package/dist/utils/global-flags.d.ts +0 -25
  172. package/dist/utils/global-flags.d.ts.map +0 -1
  173. package/dist/utils/output.d.ts +0 -142
  174. package/dist/utils/output.d.ts.map +0 -1
  175. package/dist/utils/result-handler.d.ts +0 -15
  176. package/dist/utils/result-handler.d.ts.map +0 -1
  177. package/dist/utils/spinner.d.ts +0 -29
  178. package/dist/utils/spinner.d.ts.map +0 -1
  179. package/src/utils/action.ts +0 -43
  180. package/src/utils/spinner.ts +0 -67
@@ -1,12 +1,14 @@
1
- import { relative } from 'node:path';
1
+ import { ifDefined } from '@prisma-next/utils/defined';
2
2
  import { bgGreen, blue, bold, cyan, dim, green, magenta, red, yellow } from 'colorette';
3
3
  import type { Command } from 'commander';
4
+ import { relative } from 'pathe';
4
5
  import stringWidth from 'string-width';
5
6
  import stripAnsi from 'strip-ansi';
6
7
  import wrapAnsi from 'wrap-ansi';
7
8
  // EmitContractResult type for CLI output formatting (includes file paths)
8
9
  export interface EmitContractResult {
9
- readonly coreHash: string;
10
+ readonly storageHash: string;
11
+ readonly executionHash?: string;
10
12
  readonly profileHash: string;
11
13
  readonly outDir: string;
12
14
  readonly files: {
@@ -94,7 +96,10 @@ export function formatEmitOutput(result: EmitContractResult, flags: GlobalFlags)
94
96
 
95
97
  lines.push(`${prefix}✔ Emitted contract.json → ${jsonPath}`);
96
98
  lines.push(`${prefix}✔ Emitted contract.d.ts → ${dtsPath}`);
97
- lines.push(`${prefix} coreHash: ${result.coreHash}`);
99
+ lines.push(`${prefix} storageHash: ${result.storageHash}`);
100
+ if (result.executionHash) {
101
+ lines.push(`${prefix} executionHash: ${result.executionHash}`);
102
+ }
98
103
  if (result.profileHash) {
99
104
  lines.push(`${prefix} profileHash: ${result.profileHash}`);
100
105
  }
@@ -111,7 +116,8 @@ export function formatEmitOutput(result: EmitContractResult, flags: GlobalFlags)
111
116
  export function formatEmitJson(result: EmitContractResult): string {
112
117
  const output = {
113
118
  ok: true,
114
- coreHash: result.coreHash,
119
+ storageHash: result.storageHash,
120
+ ...ifDefined('executionHash', result.executionHash),
115
121
  ...(result.profileHash ? { profileHash: result.profileHash } : {}),
116
122
  outDir: result.outDir,
117
123
  files: result.files,
@@ -223,7 +229,7 @@ export function formatVerifyOutput(result: VerifyDatabaseResult, flags: GlobalFl
223
229
 
224
230
  if (result.ok) {
225
231
  lines.push(`${prefix}${formatGreen('✔')} ${result.summary}`);
226
- lines.push(`${prefix}${formatDimText(` coreHash: ${result.contract.coreHash}`)}`);
232
+ lines.push(`${prefix}${formatDimText(` storageHash: ${result.contract.storageHash}`)}`);
227
233
  if (result.contract.profileHash) {
228
234
  lines.push(`${prefix}${formatDimText(` profileHash: ${result.contract.profileHash}`)}`);
229
235
  }
@@ -772,8 +778,8 @@ export function formatSignOutput(result: SignDatabaseResult, flags: GlobalFlags)
772
778
  lines.push(`${prefix}${formatGreen('✔')} Database signed`);
773
779
 
774
780
  // Show from -> to hashes with clear labels
775
- const previousHash = result.marker.previous?.coreHash ?? 'none';
776
- const currentHash = result.contract.coreHash;
781
+ const previousHash = result.marker.previous?.storageHash ?? 'none';
782
+ const currentHash = result.contract.storageHash;
777
783
 
778
784
  lines.push(`${prefix}${formatDimText(` from: ${previousHash}`)}`);
779
785
  lines.push(`${prefix}${formatDimText(` to: ${currentHash}`)}`);
@@ -802,19 +808,19 @@ export function formatSignJson(result: SignDatabaseResult): string {
802
808
  }
803
809
 
804
810
  // ============================================================================
805
- // DB Init Output Formatters
811
+ // Migration Command Output Formatters (shared by db init and db update)
806
812
  // ============================================================================
807
813
 
808
814
  /**
809
- * Result type for db init command.
815
+ * Shared CLI output type for migration commands (db init, db update).
810
816
  */
811
- export interface DbInitResult {
812
- readonly ok: boolean;
817
+ export interface MigrationCommandResult {
818
+ readonly ok: true;
813
819
  readonly mode: 'plan' | 'apply';
814
- readonly plan?: {
820
+ readonly plan: {
815
821
  readonly targetId: string;
816
822
  readonly destination: {
817
- readonly coreHash: string;
823
+ readonly storageHash: string;
818
824
  readonly profileHash?: string;
819
825
  };
820
826
  readonly operations: readonly {
@@ -822,13 +828,14 @@ export interface DbInitResult {
822
828
  readonly label: string;
823
829
  readonly operationClass: string;
824
830
  }[];
831
+ readonly sql?: readonly string[];
825
832
  };
826
833
  readonly execution?: {
827
834
  readonly operationsPlanned: number;
828
835
  readonly operationsExecuted: number;
829
836
  };
830
837
  readonly marker?: {
831
- readonly coreHash: string;
838
+ readonly storageHash: string;
832
839
  readonly profileHash?: string;
833
840
  };
834
841
  readonly summary: string;
@@ -838,9 +845,12 @@ export interface DbInitResult {
838
845
  }
839
846
 
840
847
  /**
841
- * Formats human-readable output for db init plan mode.
848
+ * Formats human-readable output for migration commands (db init, db update) in plan mode.
842
849
  */
843
- export function formatDbInitPlanOutput(result: DbInitResult, flags: GlobalFlags): string {
850
+ export function formatMigrationPlanOutput(
851
+ result: MigrationCommandResult,
852
+ flags: GlobalFlags,
853
+ ): string {
844
854
  if (flags.quiet) {
845
855
  return '';
846
856
  }
@@ -857,14 +867,26 @@ export function formatDbInitPlanOutput(result: DbInitResult, flags: GlobalFlags)
857
867
 
858
868
  // Show operations tree
859
869
  if (result.plan?.operations && result.plan.operations.length > 0) {
870
+ const formatYellow = createColorFormatter(useColor, yellow);
860
871
  lines.push(`${prefix}${formatDimText('│')}`);
861
872
  for (let i = 0; i < result.plan.operations.length; i++) {
862
873
  const op = result.plan.operations[i];
863
874
  if (!op) continue;
864
875
  const isLast = i === result.plan.operations.length - 1;
865
876
  const treeChar = isLast ? '└' : '├';
866
- const opClass = formatDimText(`[${op.operationClass}]`);
867
- lines.push(`${prefix}${formatDimText(treeChar)}─ ${op.label} ${opClass}`);
877
+ const opClassLabel =
878
+ op.operationClass === 'destructive'
879
+ ? formatYellow(`[${op.operationClass}]`)
880
+ : formatDimText(`[${op.operationClass}]`);
881
+ lines.push(`${prefix}${formatDimText(treeChar)}─ ${op.label} ${opClassLabel}`);
882
+ }
883
+
884
+ const hasDestructive = result.plan.operations.some((op) => op.operationClass === 'destructive');
885
+ if (hasDestructive) {
886
+ lines.push(`${prefix}`);
887
+ lines.push(
888
+ `${prefix}${formatYellow('⚠')} This migration contains destructive operations that may cause data loss.`,
889
+ );
868
890
  }
869
891
  }
870
892
 
@@ -872,10 +894,28 @@ export function formatDbInitPlanOutput(result: DbInitResult, flags: GlobalFlags)
872
894
  if (result.plan?.destination) {
873
895
  lines.push(`${prefix}`);
874
896
  lines.push(
875
- `${prefix}${formatDimText(`Destination hash: ${result.plan.destination.coreHash}`)}`,
897
+ `${prefix}${formatDimText(`Destination hash: ${result.plan.destination.storageHash}`)}`,
876
898
  );
877
899
  }
878
900
 
901
+ // SQL DDL preview (SQL family only)
902
+ const planSql = result.plan?.sql;
903
+ if (planSql) {
904
+ lines.push(`${prefix}`);
905
+ lines.push(`${prefix}${formatDimText('DDL preview')}`);
906
+ if (planSql.length === 0) {
907
+ lines.push(`${prefix}${formatDimText('No DDL operations.')}`);
908
+ } else {
909
+ lines.push(`${prefix}`);
910
+ for (const statement of planSql) {
911
+ const trimmed = statement.trim();
912
+ if (!trimmed) continue;
913
+ const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
914
+ lines.push(`${prefix}${line}`);
915
+ }
916
+ }
917
+ }
918
+
879
919
  // Timings in verbose mode
880
920
  if (isVerbose(flags, 1)) {
881
921
  lines.push(`${prefix}${formatDimText(`Total time: ${result.timings.total}ms`)}`);
@@ -889,10 +929,303 @@ export function formatDbInitPlanOutput(result: DbInitResult, flags: GlobalFlags)
889
929
  return lines.join('\n');
890
930
  }
891
931
 
932
+ export interface MigrationApplyCommandOutputResult {
933
+ readonly migrationsApplied: number;
934
+ readonly markerHash: string;
935
+ readonly applied: readonly {
936
+ readonly dirName: string;
937
+ readonly operationsExecuted: number;
938
+ }[];
939
+ readonly summary: string;
940
+ readonly timings?: {
941
+ readonly total: number;
942
+ };
943
+ }
944
+
945
+ export interface MigrationVerifyCommandOutputResult {
946
+ readonly status: 'verified' | 'attested';
947
+ readonly migrationId?: string;
948
+ }
949
+
950
+ export function formatMigrationApplyCommandOutput(
951
+ result: MigrationApplyCommandOutputResult,
952
+ flags: GlobalFlags,
953
+ ): string {
954
+ if (flags.quiet) {
955
+ return '';
956
+ }
957
+
958
+ const lines: string[] = [];
959
+ const useColor = flags.color !== false;
960
+ const formatGreen = createColorFormatter(useColor, green);
961
+ const formatDimText = (text: string) => formatDim(useColor, text);
962
+
963
+ if (result.migrationsApplied === 0) {
964
+ lines.push(`${formatGreen('✔')} ${result.summary}`);
965
+ lines.push(formatDimText(` marker: ${result.markerHash}`));
966
+ return lines.join('\n');
967
+ }
968
+
969
+ lines.push(`${formatGreen('✔')} ${result.summary}`);
970
+ lines.push('');
971
+
972
+ for (let i = 0; i < result.applied.length; i++) {
973
+ const migration = result.applied[i]!;
974
+ const isLast = i === result.applied.length - 1;
975
+ const treeChar = isLast ? '└' : '├';
976
+ lines.push(
977
+ `${formatDimText(treeChar)}─ ${migration.dirName} ${formatDimText(`[${migration.operationsExecuted} op(s)]`)}`,
978
+ );
979
+ }
980
+
981
+ lines.push('');
982
+ lines.push(formatDimText(`marker: ${result.markerHash}`));
983
+
984
+ if (isVerbose(flags, 1) && result.timings) {
985
+ lines.push('');
986
+ lines.push(formatDimText(`Total time: ${result.timings.total}ms`));
987
+ }
988
+
989
+ return lines.join('\n');
990
+ }
991
+
992
+ export function formatMigrationVerifyCommandOutput(
993
+ result: MigrationVerifyCommandOutputResult,
994
+ flags: GlobalFlags,
995
+ ): string {
996
+ if (flags.quiet) {
997
+ return '';
998
+ }
999
+
1000
+ const lines: string[] = [];
1001
+ const useColor = flags.color !== false;
1002
+ const formatGreen = createColorFormatter(useColor, green);
1003
+ const formatYellow = createColorFormatter(useColor, yellow);
1004
+ const formatDimText = (text: string) => formatDim(useColor, text);
1005
+
1006
+ switch (result.status) {
1007
+ case 'verified':
1008
+ lines.push(`${formatGreen('✔')} Migration verified`);
1009
+ lines.push(formatDimText(` migrationId: ${result.migrationId}`));
1010
+ break;
1011
+ case 'attested':
1012
+ lines.push(`${formatYellow('◉')} Draft migration attested`);
1013
+ lines.push(formatDimText(` migrationId: ${result.migrationId}`));
1014
+ break;
1015
+ }
1016
+
1017
+ return lines.join('\n');
1018
+ }
1019
+
1020
+ interface MigrationShowResult {
1021
+ readonly dirName: string;
1022
+ readonly dirPath: string;
1023
+ readonly from: string;
1024
+ readonly to: string;
1025
+ readonly migrationId: string | null;
1026
+ readonly kind: string;
1027
+ readonly createdAt: string;
1028
+ readonly operations: readonly {
1029
+ readonly id: string;
1030
+ readonly label: string;
1031
+ readonly operationClass: string;
1032
+ }[];
1033
+ readonly sql: readonly string[];
1034
+ readonly summary: string;
1035
+ }
1036
+
1037
+ export function formatMigrationShowOutput(result: MigrationShowResult, flags: GlobalFlags): string {
1038
+ if (flags.quiet) {
1039
+ return '';
1040
+ }
1041
+
1042
+ const lines: string[] = [];
1043
+ const prefix = createPrefix(flags);
1044
+ const useColor = flags.color !== false;
1045
+ const formatGreen = createColorFormatter(useColor, green);
1046
+ const formatYellow = createColorFormatter(useColor, yellow);
1047
+ const formatDimText = (text: string) => formatDim(useColor, text);
1048
+
1049
+ lines.push(`${prefix}${formatGreen('✔')} ${result.dirName}`);
1050
+ lines.push(`${prefix}${formatDimText(` kind: ${result.kind}`)}`);
1051
+ lines.push(`${prefix}${formatDimText(` from: ${result.from}`)}`);
1052
+ lines.push(`${prefix}${formatDimText(` to: ${result.to}`)}`);
1053
+ if (result.migrationId) {
1054
+ lines.push(`${prefix}${formatDimText(` migrationId: ${result.migrationId}`)}`);
1055
+ } else {
1056
+ lines.push(`${prefix}${formatYellow(' migrationId: (draft — not yet attested)')}`);
1057
+ }
1058
+ lines.push(`${prefix}${formatDimText(` created: ${result.createdAt}`)}`);
1059
+
1060
+ lines.push(`${prefix}`);
1061
+ lines.push(`${prefix}${result.operations.length} operation(s)`);
1062
+
1063
+ if (result.operations.length > 0) {
1064
+ lines.push(`${prefix}${formatDimText('│')}`);
1065
+ for (let i = 0; i < result.operations.length; i++) {
1066
+ const op = result.operations[i]!;
1067
+ const isLast = i === result.operations.length - 1;
1068
+ const treeChar = isLast ? '└' : '├';
1069
+ const opClassLabel =
1070
+ op.operationClass === 'destructive'
1071
+ ? formatYellow(`[${op.operationClass}]`)
1072
+ : formatDimText(`[${op.operationClass}]`);
1073
+ lines.push(`${prefix}${formatDimText(treeChar)}─ ${op.label} ${opClassLabel}`);
1074
+ }
1075
+
1076
+ const hasDestructive = result.operations.some((op) => op.operationClass === 'destructive');
1077
+ if (hasDestructive) {
1078
+ lines.push(`${prefix}`);
1079
+ lines.push(
1080
+ `${prefix}${formatYellow('⚠')} This migration contains destructive operations that may cause data loss.`,
1081
+ );
1082
+ }
1083
+ }
1084
+
1085
+ if (result.sql.length > 0) {
1086
+ lines.push(`${prefix}`);
1087
+ lines.push(`${prefix}${formatDimText('DDL preview')}`);
1088
+ lines.push(`${prefix}`);
1089
+ for (const statement of result.sql) {
1090
+ const trimmed = statement.trim();
1091
+ if (!trimmed) continue;
1092
+ const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
1093
+ lines.push(`${prefix}${line}`);
1094
+ }
1095
+ }
1096
+
1097
+ return lines.join('\n');
1098
+ }
1099
+
1100
+ interface MigrationStatusEntry {
1101
+ readonly dirName: string;
1102
+ readonly to: string;
1103
+ readonly migrationId: string | null;
1104
+ readonly operationSummary: string;
1105
+ readonly hasDestructive: boolean;
1106
+ readonly status: 'applied' | 'pending' | 'unknown';
1107
+ }
1108
+
1109
+ interface StatusDiagnostic {
1110
+ readonly code: string;
1111
+ readonly severity: 'warn' | 'info';
1112
+ readonly message: string;
1113
+ readonly hints: readonly string[];
1114
+ }
1115
+
1116
+ interface MigrationStatusResult {
1117
+ readonly mode: 'online' | 'offline';
1118
+ readonly migrations: readonly MigrationStatusEntry[];
1119
+ readonly markerHash?: string;
1120
+ readonly leafHash: string;
1121
+ readonly contractHash: string;
1122
+ readonly summary: string;
1123
+ readonly diagnostics?: readonly StatusDiagnostic[];
1124
+ }
1125
+
1126
+ export function formatMigrationStatusOutput(
1127
+ result: MigrationStatusResult,
1128
+ flags: GlobalFlags,
1129
+ ): string {
1130
+ if (flags.quiet) {
1131
+ return '';
1132
+ }
1133
+
1134
+ const lines: string[] = [];
1135
+ const prefix = createPrefix(flags);
1136
+ const useColor = flags.color !== false;
1137
+ const formatGreen = createColorFormatter(useColor, green);
1138
+ const formatYellow = createColorFormatter(useColor, yellow);
1139
+ const formatDimText = (text: string) => formatDim(useColor, text);
1140
+ const formatCyan = createColorFormatter(useColor, cyan);
1141
+
1142
+ if (result.migrations.length === 0) {
1143
+ lines.push(`${prefix}${formatDimText('No migrations found')}`);
1144
+ } else {
1145
+ lines.push(`${prefix}${formatDimText('∅ (empty)')}`);
1146
+ lines.push(`${prefix}${formatDimText('│')}`);
1147
+
1148
+ for (let i = 0; i < result.migrations.length; i++) {
1149
+ const entry = result.migrations[i]!;
1150
+ const isLast = i === result.migrations.length - 1;
1151
+ const treeChar = isLast ? '└' : '├';
1152
+ const continueLine = isLast ? ' ' : '│';
1153
+
1154
+ let statusBadge = '';
1155
+ if (entry.status === 'applied') {
1156
+ statusBadge = formatGreen(' ✓ Applied');
1157
+ } else if (entry.status === 'pending') {
1158
+ statusBadge = formatYellow(' ⧗ Pending');
1159
+ }
1160
+
1161
+ let marker = '';
1162
+ if (result.mode === 'online') {
1163
+ const isLastApplied =
1164
+ entry.status === 'applied' &&
1165
+ (i === result.migrations.length - 1 || result.migrations[i + 1]?.status !== 'applied');
1166
+ if (isLastApplied) {
1167
+ marker = formatCyan(' ◄ DB');
1168
+ }
1169
+ }
1170
+ if (isLast && entry.to === result.contractHash) {
1171
+ marker += ` ${formatCyan('◄ Contract')}`;
1172
+ } else if (isLast && result.contractHash !== entry.to) {
1173
+ marker += ` ${formatYellow('◄ Contract is ahead — run migration plan')}`;
1174
+ }
1175
+
1176
+ lines.push(`${prefix}${formatDimText(treeChar)}─ ${entry.dirName}${statusBadge}${marker}`);
1177
+
1178
+ const opsSummary = entry.hasDestructive
1179
+ ? formatYellow(entry.operationSummary)
1180
+ : formatDimText(entry.operationSummary);
1181
+ lines.push(`${prefix}${formatDimText(continueLine)} ${opsSummary}`);
1182
+
1183
+ const hashDisplay = entry.to.length > 20 ? `${entry.to.slice(0, 20)}...` : entry.to;
1184
+ lines.push(`${prefix}${formatDimText(continueLine)} ${formatDimText(`→ ${hashDisplay}`)}`);
1185
+
1186
+ if (!isLast) {
1187
+ lines.push(`${prefix}${formatDimText('│')}`);
1188
+ }
1189
+ }
1190
+
1191
+ lines.push(`${prefix}`);
1192
+
1193
+ if (result.mode === 'online') {
1194
+ const hasUnknown = result.migrations.some((e) => e.status === 'unknown');
1195
+ const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
1196
+ if (hasUnknown) {
1197
+ lines.push(`${prefix}${formatYellow('⚠')} ${result.summary}`);
1198
+ } else if (pendingCount === 0) {
1199
+ lines.push(`${prefix}${formatGreen('✔')} ${result.summary}`);
1200
+ } else {
1201
+ lines.push(`${prefix}${formatYellow('⧗')} ${result.summary}`);
1202
+ }
1203
+ } else {
1204
+ lines.push(`${prefix}${result.summary}`);
1205
+ }
1206
+ }
1207
+
1208
+ const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
1209
+ if (warnings.length > 0) {
1210
+ lines.push('');
1211
+ for (const diag of warnings) {
1212
+ lines.push(`${prefix}${formatYellow('⚠')} ${diag.message}`);
1213
+ for (const hint of diag.hints) {
1214
+ lines.push(`${prefix} ${formatDimText(hint)}`);
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ return lines.join('\n');
1220
+ }
1221
+
892
1222
  /**
893
- * Formats human-readable output for db init apply mode.
1223
+ * Formats human-readable output for migration commands (db init, db update) in apply mode.
894
1224
  */
895
- export function formatDbInitApplyOutput(result: DbInitResult, flags: GlobalFlags): string {
1225
+ export function formatMigrationApplyOutput(
1226
+ result: MigrationCommandResult,
1227
+ flags: GlobalFlags,
1228
+ ): string {
896
1229
  if (flags.quiet) {
897
1230
  return '';
898
1231
  }
@@ -906,11 +1239,15 @@ export function formatDbInitApplyOutput(result: DbInitResult, flags: GlobalFlags
906
1239
  if (result.ok) {
907
1240
  // Success summary
908
1241
  const executed = result.execution?.operationsExecuted ?? 0;
909
- lines.push(`${prefix}${formatGreen('✔')} Applied ${executed} operation(s)`);
1242
+ if (executed === 0) {
1243
+ lines.push(`${prefix}${formatGreen('✔')} Database already matches contract`);
1244
+ } else {
1245
+ lines.push(`${prefix}${formatGreen('✔')} Applied ${executed} operation(s)`);
1246
+ }
910
1247
 
911
1248
  // Marker info
912
1249
  if (result.marker) {
913
- lines.push(`${prefix}${formatDimText(` Marker written: ${result.marker.coreHash}`)}`);
1250
+ lines.push(`${prefix}${formatDimText(` Signature: ${result.marker.storageHash}`)}`);
914
1251
  if (result.marker.profileHash) {
915
1252
  lines.push(`${prefix}${formatDimText(` Profile hash: ${result.marker.profileHash}`)}`);
916
1253
  }
@@ -926,9 +1263,9 @@ export function formatDbInitApplyOutput(result: DbInitResult, flags: GlobalFlags
926
1263
  }
927
1264
 
928
1265
  /**
929
- * Formats JSON output for db init command.
1266
+ * Formats JSON output for migration commands (db init, db update).
930
1267
  */
931
- export function formatDbInitJson(result: DbInitResult): string {
1268
+ export function formatMigrationJson(result: MigrationCommandResult): string {
932
1269
  return JSON.stringify(result, null, 2);
933
1270
  }
934
1271
 
@@ -1241,6 +1578,7 @@ function getCommandDocsUrl(commandPath: string): string | undefined {
1241
1578
  const docsMap: Record<string, string> = {
1242
1579
  'contract emit': 'https://pris.ly/contract-emit',
1243
1580
  'db verify': 'https://pris.ly/db-verify',
1581
+ 'db update': 'https://pris.ly/db-update',
1244
1582
  };
1245
1583
  return docsMap[commandPath];
1246
1584
  }
@@ -0,0 +1,86 @@
1
+ import ora from 'ora';
2
+ import type { ControlProgressEvent, OnControlProgress } from '../control-api/types';
3
+ import type { GlobalFlags } from './global-flags';
4
+
5
+ /**
6
+ * Options for creating a progress adapter.
7
+ */
8
+ interface ProgressAdapterOptions {
9
+ /**
10
+ * Global flags that control progress output behavior (quiet, json, color).
11
+ */
12
+ readonly flags: GlobalFlags;
13
+ }
14
+
15
+ /**
16
+ * State for tracking active spans in the progress adapter.
17
+ */
18
+ interface SpanState {
19
+ readonly spinner: ReturnType<typeof ora>;
20
+ readonly startTime: number;
21
+ }
22
+
23
+ /**
24
+ * Creates a progress adapter that converts control-api progress events
25
+ * into CLI spinner/progress output.
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
+ * @param options - Progress adapter configuration
33
+ * @returns An onProgress callback compatible with control-api operations
34
+ */
35
+ 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;
40
+
41
+ if (!shouldShowProgress) {
42
+ // Return a no-op callback
43
+ return () => {
44
+ // No-op
45
+ };
46
+ }
47
+
48
+ // Track active spans by spanId
49
+ const activeSpans = new Map<string, SpanState>();
50
+
51
+ return (event: ControlProgressEvent) => {
52
+ if (event.kind === 'spanStart') {
53
+ // Nested spans (with parentSpanId) are printed as lines, not spinners
54
+ if (event.parentSpanId) {
55
+ console.log(` → ${event.label}...`);
56
+ return;
57
+ }
58
+
59
+ // Top-level spans get a spinner
60
+ const spinner = ora({
61
+ text: event.label,
62
+ color: flags.color !== false ? 'cyan' : false,
63
+ }).start();
64
+
65
+ activeSpans.set(event.spanId, {
66
+ spinner,
67
+ startTime: Date.now(),
68
+ });
69
+ } else if (event.kind === 'spanEnd') {
70
+ // Complete the spinner for this span (only top-level spans have spinners)
71
+ const spanState = activeSpans.get(event.spanId);
72
+ if (spanState) {
73
+ const elapsed = Date.now() - spanState.startTime;
74
+ if (event.outcome === 'error') {
75
+ spanState.spinner.fail(`${spanState.spinner.text} (failed)`);
76
+ } else if (event.outcome === 'skipped') {
77
+ spanState.spinner.info(`${spanState.spinner.text} (skipped)`);
78
+ } else {
79
+ spanState.spinner.succeed(`${spanState.spinner.text} (${elapsed}ms)`);
80
+ }
81
+ activeSpans.delete(event.spanId);
82
+ }
83
+ // Nested span ends are no-ops (could log completion if needed)
84
+ }
85
+ };
86
+ }