postgresai 0.14.0-dev.69 → 0.14.0-dev.70

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.
@@ -13,6 +13,7 @@ import { startMcpServer } from "../lib/mcp-server";
13
13
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment } from "../lib/issues";
14
14
  import { resolveBaseUrls } from "../lib/util";
15
15
  import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
16
+ import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, type PgCompatibleError } from "../lib/supabase";
16
17
  import * as pkce from "../lib/pkce";
17
18
  import * as authServer from "../lib/auth-server";
18
19
  import { maskSecret } from "../lib/util";
@@ -568,6 +569,10 @@ program
568
569
  .option("--reset-password", "Reset monitoring role password only (no other changes)", false)
569
570
  .option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
570
571
  .option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
572
+ .option("--supabase", "Use Supabase Management API instead of direct PostgreSQL connection", false)
573
+ .option("--supabase-access-token <token>", "Supabase Management API access token (or SUPABASE_ACCESS_TOKEN env)")
574
+ .option("--supabase-project-ref <ref>", "Supabase project reference (or SUPABASE_PROJECT_REF env)")
575
+ .option("--json", "Output result as JSON (machine-readable)", false)
571
576
  .addHelpText(
572
577
  "after",
573
578
  [
@@ -607,6 +612,13 @@ program
607
612
  "",
608
613
  "Offline SQL plan (no DB connection):",
609
614
  " postgresai prepare-db --print-sql",
615
+ "",
616
+ "Supabase mode (use Management API instead of direct connection):",
617
+ " postgresai prepare-db --supabase --supabase-project-ref <ref>",
618
+ " SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
619
+ "",
620
+ " Generate a token at: https://supabase.com/dashboard/account/tokens",
621
+ " Find your project ref in: https://supabase.com/dashboard/project/<ref>",
610
622
  ].join("\n")
611
623
  )
612
624
  .action(async (conn: string | undefined, opts: {
@@ -623,15 +635,46 @@ program
623
635
  resetPassword?: boolean;
624
636
  printSql?: boolean;
625
637
  printPassword?: boolean;
638
+ supabase?: boolean;
639
+ supabaseAccessToken?: string;
640
+ supabaseProjectRef?: string;
641
+ json?: boolean;
626
642
  }, cmd: Command) => {
627
- if (opts.verify && opts.resetPassword) {
628
- console.error("✗ Provide only one of --verify or --reset-password");
643
+ // JSON output helper
644
+ const jsonOutput = opts.json;
645
+ const outputJson = (data: Record<string, unknown>) => {
646
+ console.log(JSON.stringify(data, null, 2));
647
+ };
648
+ const outputError = (error: {
649
+ message: string;
650
+ step?: string;
651
+ code?: string;
652
+ detail?: string;
653
+ hint?: string;
654
+ httpStatus?: number;
655
+ }) => {
656
+ if (jsonOutput) {
657
+ outputJson({
658
+ success: false,
659
+ mode: opts.supabase ? "supabase" : "direct",
660
+ error,
661
+ });
662
+ } else {
663
+ console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error.message}`);
664
+ if (error.step) console.error(` Step: ${error.step}`);
665
+ if (error.code) console.error(` Code: ${error.code}`);
666
+ if (error.detail) console.error(` Detail: ${error.detail}`);
667
+ if (error.hint) console.error(` Hint: ${error.hint}`);
668
+ if (error.httpStatus) console.error(` HTTP Status: ${error.httpStatus}`);
669
+ }
629
670
  process.exitCode = 1;
671
+ };
672
+ if (opts.verify && opts.resetPassword) {
673
+ outputError({ message: "Provide only one of --verify or --reset-password" });
630
674
  return;
631
675
  }
632
676
  if (opts.verify && opts.printSql) {
633
- console.error("--verify cannot be combined with --print-sql");
634
- process.exitCode = 1;
677
+ outputError({ message: "--verify cannot be combined with --print-sql" });
635
678
  return;
636
679
  }
637
680
 
@@ -671,6 +714,296 @@ program
671
714
  }
672
715
  }
673
716
 
717
+ // Supabase mode: use Supabase Management API instead of direct PG connection
718
+ if (opts.supabase) {
719
+ let supabaseConfig;
720
+ try {
721
+ // Try to extract project ref from connection URL if provided
722
+ let projectRef = opts.supabaseProjectRef;
723
+ if (!projectRef && conn) {
724
+ projectRef = extractProjectRefFromUrl(conn);
725
+ }
726
+ supabaseConfig = resolveSupabaseConfig({
727
+ accessToken: opts.supabaseAccessToken,
728
+ projectRef,
729
+ });
730
+ } catch (e) {
731
+ const msg = e instanceof Error ? e.message : String(e);
732
+ outputError({ message: msg });
733
+ return;
734
+ }
735
+
736
+ const includeOptionalPermissions = !opts.skipOptionalPermissions;
737
+
738
+ if (!jsonOutput) {
739
+ console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
740
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
741
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
742
+ }
743
+
744
+ const supabaseClient = new SupabaseClient(supabaseConfig);
745
+
746
+ try {
747
+ // Get current database name
748
+ const database = await supabaseClient.getCurrentDatabase();
749
+ if (!database) {
750
+ throw new Error("Failed to resolve current database name");
751
+ }
752
+ if (!jsonOutput) {
753
+ console.log(`Database: ${database}`);
754
+ }
755
+
756
+ if (opts.verify) {
757
+ const v = await verifyInitSetupViaSupabase({
758
+ client: supabaseClient,
759
+ database,
760
+ monitoringUser: opts.monitoringUser,
761
+ includeOptionalPermissions,
762
+ });
763
+ if (v.ok) {
764
+ if (jsonOutput) {
765
+ outputJson({
766
+ success: true,
767
+ mode: "supabase",
768
+ action: "verify",
769
+ database,
770
+ monitoringUser: opts.monitoringUser,
771
+ verified: true,
772
+ missingOptional: v.missingOptional,
773
+ });
774
+ } else {
775
+ console.log("✓ prepare-db verify: OK");
776
+ if (v.missingOptional.length > 0) {
777
+ console.log("⚠ Optional items missing:");
778
+ for (const m of v.missingOptional) console.log(`- ${m}`);
779
+ }
780
+ }
781
+ return;
782
+ }
783
+ if (jsonOutput) {
784
+ outputJson({
785
+ success: false,
786
+ mode: "supabase",
787
+ action: "verify",
788
+ database,
789
+ monitoringUser: opts.monitoringUser,
790
+ verified: false,
791
+ missingRequired: v.missingRequired,
792
+ missingOptional: v.missingOptional,
793
+ });
794
+ } else {
795
+ console.error("✗ prepare-db verify failed: missing required items");
796
+ for (const m of v.missingRequired) console.error(`- ${m}`);
797
+ if (v.missingOptional.length > 0) {
798
+ console.error("Optional items missing:");
799
+ for (const m of v.missingOptional) console.error(`- ${m}`);
800
+ }
801
+ }
802
+ process.exitCode = 1;
803
+ return;
804
+ }
805
+
806
+ let monPassword: string;
807
+ let passwordGenerated = false;
808
+ try {
809
+ const resolved = await resolveMonitoringPassword({
810
+ passwordFlag: opts.password,
811
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
812
+ monitoringUser: opts.monitoringUser,
813
+ });
814
+ monPassword = resolved.password;
815
+ passwordGenerated = resolved.generated;
816
+ if (resolved.generated) {
817
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
818
+ if (canPrint) {
819
+ if (!jsonOutput) {
820
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
821
+ console.error("");
822
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
823
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
824
+ console.error("");
825
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
826
+ }
827
+ // For JSON mode, password will be included in the success output below
828
+ } else {
829
+ console.error(
830
+ [
831
+ `✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
832
+ "",
833
+ "Provide it explicitly:",
834
+ " --password <password> or PGAI_MON_PASSWORD=...",
835
+ "",
836
+ "Or (NOT recommended) print the generated password:",
837
+ " --print-password",
838
+ ].join("\n")
839
+ );
840
+ process.exitCode = 1;
841
+ return;
842
+ }
843
+ }
844
+ } catch (e) {
845
+ const msg = e instanceof Error ? e.message : String(e);
846
+ outputError({ message: msg });
847
+ return;
848
+ }
849
+
850
+ const plan = await buildInitPlan({
851
+ database,
852
+ monitoringUser: opts.monitoringUser,
853
+ monitoringPassword: monPassword,
854
+ includeOptionalPermissions,
855
+ });
856
+
857
+ // For Supabase mode, skip RDS and self-managed steps (they don't apply)
858
+ const supabaseApplicableSteps = plan.steps.filter(
859
+ (s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed"
860
+ );
861
+
862
+ const effectivePlan = opts.resetPassword
863
+ ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") }
864
+ : { ...plan, steps: supabaseApplicableSteps };
865
+
866
+ if (shouldPrintSql) {
867
+ console.log("\n--- SQL plan ---");
868
+ for (const step of effectivePlan.steps) {
869
+ console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
870
+ console.log(redactPasswords(step.sql));
871
+ }
872
+ console.log("\n--- end SQL plan ---\n");
873
+ console.log("Note: passwords are redacted in the printed SQL output.");
874
+ return;
875
+ }
876
+
877
+ const { applied, skippedOptional } = await applyInitPlanViaSupabase({
878
+ client: supabaseClient,
879
+ plan: effectivePlan,
880
+ });
881
+
882
+ if (jsonOutput) {
883
+ const result: Record<string, unknown> = {
884
+ success: true,
885
+ mode: "supabase",
886
+ action: opts.resetPassword ? "reset-password" : "apply",
887
+ database,
888
+ monitoringUser: opts.monitoringUser,
889
+ applied,
890
+ skippedOptional,
891
+ warnings: skippedOptional.length > 0
892
+ ? ["Some optional steps were skipped (not supported or insufficient privileges)"]
893
+ : [],
894
+ };
895
+ if (passwordGenerated) {
896
+ result.generatedPassword = monPassword;
897
+ }
898
+ outputJson(result);
899
+ } else {
900
+ console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
901
+ if (skippedOptional.length > 0) {
902
+ console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
903
+ for (const s of skippedOptional) console.log(`- ${s}`);
904
+ }
905
+ if (process.stdout.isTTY) {
906
+ console.log(`Applied ${applied.length} steps`);
907
+ }
908
+ }
909
+ } catch (error) {
910
+ const errAny = error as PgCompatibleError;
911
+ let message = "";
912
+ if (error instanceof Error && error.message) {
913
+ message = error.message;
914
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
915
+ message = errAny.message;
916
+ } else {
917
+ message = String(error);
918
+ }
919
+ if (!message || message === "[object Object]") {
920
+ message = "Unknown error";
921
+ }
922
+
923
+ // Surface step name if this was a plan step failure
924
+ const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
925
+ const failedStep = stepMatch?.[1];
926
+
927
+ // Build error object for JSON output
928
+ const errorObj: {
929
+ message: string;
930
+ step?: string;
931
+ code?: string;
932
+ detail?: string;
933
+ hint?: string;
934
+ httpStatus?: number;
935
+ } = { message };
936
+
937
+ if (failedStep) errorObj.step = failedStep;
938
+ if (errAny && typeof errAny === "object") {
939
+ if (typeof errAny.code === "string" && errAny.code) errorObj.code = errAny.code;
940
+ if (typeof errAny.detail === "string" && errAny.detail) errorObj.detail = errAny.detail;
941
+ if (typeof errAny.hint === "string" && errAny.hint) errorObj.hint = errAny.hint;
942
+ if (typeof errAny.httpStatus === "number") errorObj.httpStatus = errAny.httpStatus;
943
+ }
944
+
945
+ if (jsonOutput) {
946
+ outputJson({
947
+ success: false,
948
+ mode: "supabase",
949
+ error: errorObj,
950
+ });
951
+ process.exitCode = 1;
952
+ } else {
953
+ console.error(`Error: prepare-db (Supabase): ${message}`);
954
+
955
+ if (failedStep) {
956
+ console.error(` Step: ${failedStep}`);
957
+ }
958
+
959
+ // Surface PostgreSQL-compatible error details
960
+ if (errAny && typeof errAny === "object") {
961
+ if (typeof errAny.code === "string" && errAny.code) {
962
+ console.error(` Code: ${errAny.code}`);
963
+ }
964
+ if (typeof errAny.detail === "string" && errAny.detail) {
965
+ console.error(` Detail: ${errAny.detail}`);
966
+ }
967
+ if (typeof errAny.hint === "string" && errAny.hint) {
968
+ console.error(` Hint: ${errAny.hint}`);
969
+ }
970
+ if (typeof errAny.httpStatus === "number") {
971
+ console.error(` HTTP Status: ${errAny.httpStatus}`);
972
+ }
973
+ }
974
+
975
+ // Provide context hints for common errors
976
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
977
+ if (errAny.code === "42501") {
978
+ if (failedStep === "01.role") {
979
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
980
+ } else if (failedStep === "02.permissions") {
981
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
982
+ }
983
+ console.error(" Fix: ensure your Supabase access token has sufficient permissions");
984
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
985
+ }
986
+ }
987
+ if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
988
+ if (errAny.httpStatus === 401) {
989
+ console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
990
+ }
991
+ if (errAny.httpStatus === 403) {
992
+ console.error(" Hint: access denied; check your token permissions and project access");
993
+ }
994
+ if (errAny.httpStatus === 404) {
995
+ console.error(" Hint: project not found; verify the project reference is correct");
996
+ }
997
+ if (errAny.httpStatus === 429) {
998
+ console.error(" Hint: rate limited; wait a moment and try again");
999
+ }
1000
+ }
1001
+ process.exitCode = 1;
1002
+ }
1003
+ }
1004
+ return;
1005
+ }
1006
+
674
1007
  let adminConn;
675
1008
  try {
676
1009
  adminConn = resolveAdminConnection({
@@ -686,21 +1019,27 @@ program
686
1019
  });
687
1020
  } catch (e) {
688
1021
  const msg = e instanceof Error ? e.message : String(e);
689
- console.error(`Error: prepare-db: ${msg}`);
690
- // When connection details are missing, show full init help (options + examples).
691
- if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
692
- console.error("");
693
- cmd.outputHelp({ error: true });
1022
+ if (jsonOutput) {
1023
+ outputError({ message: msg });
1024
+ } else {
1025
+ console.error(`Error: prepare-db: ${msg}`);
1026
+ // When connection details are missing, show full init help (options + examples).
1027
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
1028
+ console.error("");
1029
+ cmd.outputHelp({ error: true });
1030
+ }
1031
+ process.exitCode = 1;
694
1032
  }
695
- process.exitCode = 1;
696
1033
  return;
697
1034
  }
698
1035
 
699
1036
  const includeOptionalPermissions = !opts.skipOptionalPermissions;
700
1037
 
701
- console.log(`Connecting to: ${adminConn.display}`);
702
- console.log(`Monitoring user: ${opts.monitoringUser}`);
703
- console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
1038
+ if (!jsonOutput) {
1039
+ console.log(`Connecting to: ${adminConn.display}`);
1040
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
1041
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
1042
+ }
704
1043
 
705
1044
  // Use native pg client instead of requiring psql to be installed
706
1045
  let client: Client | undefined;
@@ -722,24 +1061,50 @@ program
722
1061
  includeOptionalPermissions,
723
1062
  });
724
1063
  if (v.ok) {
725
- console.log("✓ prepare-db verify: OK");
726
- if (v.missingOptional.length > 0) {
727
- console.log("⚠ Optional items missing:");
728
- for (const m of v.missingOptional) console.log(`- ${m}`);
1064
+ if (jsonOutput) {
1065
+ outputJson({
1066
+ success: true,
1067
+ mode: "direct",
1068
+ action: "verify",
1069
+ database,
1070
+ monitoringUser: opts.monitoringUser,
1071
+ verified: true,
1072
+ missingOptional: v.missingOptional,
1073
+ });
1074
+ } else {
1075
+ console.log("✓ prepare-db verify: OK");
1076
+ if (v.missingOptional.length > 0) {
1077
+ console.log("⚠ Optional items missing:");
1078
+ for (const m of v.missingOptional) console.log(`- ${m}`);
1079
+ }
729
1080
  }
730
1081
  return;
731
1082
  }
732
- console.error("✗ prepare-db verify failed: missing required items");
733
- for (const m of v.missingRequired) console.error(`- ${m}`);
734
- if (v.missingOptional.length > 0) {
735
- console.error("Optional items missing:");
736
- for (const m of v.missingOptional) console.error(`- ${m}`);
1083
+ if (jsonOutput) {
1084
+ outputJson({
1085
+ success: false,
1086
+ mode: "direct",
1087
+ action: "verify",
1088
+ database,
1089
+ monitoringUser: opts.monitoringUser,
1090
+ verified: false,
1091
+ missingRequired: v.missingRequired,
1092
+ missingOptional: v.missingOptional,
1093
+ });
1094
+ } else {
1095
+ console.error("✗ prepare-db verify failed: missing required items");
1096
+ for (const m of v.missingRequired) console.error(`- ${m}`);
1097
+ if (v.missingOptional.length > 0) {
1098
+ console.error("Optional items missing:");
1099
+ for (const m of v.missingOptional) console.error(`- ${m}`);
1100
+ }
737
1101
  }
738
1102
  process.exitCode = 1;
739
1103
  return;
740
1104
  }
741
1105
 
742
1106
  let monPassword: string;
1107
+ let passwordGenerated = false;
743
1108
  try {
744
1109
  const resolved = await resolveMonitoringPassword({
745
1110
  passwordFlag: opts.password,
@@ -747,17 +1112,21 @@ program
747
1112
  monitoringUser: opts.monitoringUser,
748
1113
  });
749
1114
  monPassword = resolved.password;
1115
+ passwordGenerated = resolved.generated;
750
1116
  if (resolved.generated) {
751
- const canPrint = process.stdout.isTTY || !!opts.printPassword;
1117
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
752
1118
  if (canPrint) {
753
- // Print secrets to stderr to reduce the chance they end up in piped stdout logs.
754
- const shellSafe = monPassword.replace(/'/g, "'\\''");
755
- console.error("");
756
- console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
757
- // Quote for shell copy/paste safety.
758
- console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
759
- console.error("");
760
- console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
1119
+ if (!jsonOutput) {
1120
+ // Print secrets to stderr to reduce the chance they end up in piped stdout logs.
1121
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
1122
+ console.error("");
1123
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
1124
+ // Quote for shell copy/paste safety.
1125
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
1126
+ console.error("");
1127
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
1128
+ }
1129
+ // For JSON mode, password will be included in the success output below
761
1130
  } else {
762
1131
  console.error(
763
1132
  [
@@ -776,8 +1145,7 @@ program
776
1145
  }
777
1146
  } catch (e) {
778
1147
  const msg = e instanceof Error ? e.message : String(e);
779
- console.error(`✗ ${msg}`);
780
- process.exitCode = 1;
1148
+ outputError({ message: msg });
781
1149
  return;
782
1150
  }
783
1151
 
@@ -805,14 +1173,33 @@ program
805
1173
 
806
1174
  const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
807
1175
 
808
- console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
809
- if (skippedOptional.length > 0) {
810
- console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
811
- for (const s of skippedOptional) console.log(`- ${s}`);
812
- }
813
- // Keep output compact but still useful
814
- if (process.stdout.isTTY) {
815
- console.log(`Applied ${applied.length} steps`);
1176
+ if (jsonOutput) {
1177
+ const result: Record<string, unknown> = {
1178
+ success: true,
1179
+ mode: "direct",
1180
+ action: opts.resetPassword ? "reset-password" : "apply",
1181
+ database,
1182
+ monitoringUser: opts.monitoringUser,
1183
+ applied,
1184
+ skippedOptional,
1185
+ warnings: skippedOptional.length > 0
1186
+ ? ["Some optional steps were skipped (not supported or insufficient privileges)"]
1187
+ : [],
1188
+ };
1189
+ if (passwordGenerated) {
1190
+ result.generatedPassword = monPassword;
1191
+ }
1192
+ outputJson(result);
1193
+ } else {
1194
+ console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
1195
+ if (skippedOptional.length > 0) {
1196
+ console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
1197
+ for (const s of skippedOptional) console.log(`- ${s}`);
1198
+ }
1199
+ // Keep output compact but still useful
1200
+ if (process.stdout.isTTY) {
1201
+ console.log(`Applied ${applied.length} steps`);
1202
+ }
816
1203
  }
817
1204
  } catch (error) {
818
1205
  const errAny = error as any;
@@ -827,47 +1214,74 @@ program
827
1214
  if (!message || message === "[object Object]") {
828
1215
  message = "Unknown error";
829
1216
  }
830
- console.error(`Error: prepare-db: ${message}`);
1217
+
831
1218
  // If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
832
1219
  const stepMatch =
833
1220
  typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
834
1221
  const failedStep = stepMatch?.[1];
835
- if (failedStep) {
836
- console.error(` Step: ${failedStep}`);
837
- }
1222
+
1223
+ // Build error object for JSON output
1224
+ const errorObj: {
1225
+ message: string;
1226
+ step?: string;
1227
+ code?: string;
1228
+ detail?: string;
1229
+ hint?: string;
1230
+ } = { message };
1231
+
1232
+ if (failedStep) errorObj.step = failedStep;
838
1233
  if (errAny && typeof errAny === "object") {
839
- if (typeof errAny.code === "string" && errAny.code) {
840
- console.error(` Code: ${errAny.code}`);
841
- }
842
- if (typeof errAny.detail === "string" && errAny.detail) {
843
- console.error(` Detail: ${errAny.detail}`);
844
- }
845
- if (typeof errAny.hint === "string" && errAny.hint) {
846
- console.error(` Hint: ${errAny.hint}`);
847
- }
1234
+ if (typeof errAny.code === "string" && errAny.code) errorObj.code = errAny.code;
1235
+ if (typeof errAny.detail === "string" && errAny.detail) errorObj.detail = errAny.detail;
1236
+ if (typeof errAny.hint === "string" && errAny.hint) errorObj.hint = errAny.hint;
848
1237
  }
849
- if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
850
- if (errAny.code === "42501") {
851
- if (failedStep === "01.role") {
852
- console.error(" Context: role creation/update requires CREATEROLE or superuser");
853
- } else if (failedStep === "02.permissions") {
854
- console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
855
- }
856
- console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
857
- console.error(" Fix: on managed Postgres, use the provider's admin/master user");
858
- console.error(" Tip: run with --print-sql to review the exact SQL plan");
859
- }
860
- if (errAny.code === "ECONNREFUSED") {
861
- console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
1238
+
1239
+ if (jsonOutput) {
1240
+ outputJson({
1241
+ success: false,
1242
+ mode: "direct",
1243
+ error: errorObj,
1244
+ });
1245
+ process.exitCode = 1;
1246
+ } else {
1247
+ console.error(`Error: prepare-db: ${message}`);
1248
+ if (failedStep) {
1249
+ console.error(` Step: ${failedStep}`);
862
1250
  }
863
- if (errAny.code === "ENOTFOUND") {
864
- console.error(" Hint: DNS resolution failed; double-check the host name");
1251
+ if (errAny && typeof errAny === "object") {
1252
+ if (typeof errAny.code === "string" && errAny.code) {
1253
+ console.error(` Code: ${errAny.code}`);
1254
+ }
1255
+ if (typeof errAny.detail === "string" && errAny.detail) {
1256
+ console.error(` Detail: ${errAny.detail}`);
1257
+ }
1258
+ if (typeof errAny.hint === "string" && errAny.hint) {
1259
+ console.error(` Hint: ${errAny.hint}`);
1260
+ }
865
1261
  }
866
- if (errAny.code === "ETIMEDOUT") {
867
- console.error(" Hint: connection timed out; check network/firewall rules");
1262
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
1263
+ if (errAny.code === "42501") {
1264
+ if (failedStep === "01.role") {
1265
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
1266
+ } else if (failedStep === "02.permissions") {
1267
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
1268
+ }
1269
+ console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
1270
+ console.error(" Fix: on managed Postgres, use the provider's admin/master user");
1271
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
1272
+ }
1273
+ if (errAny.code === "ECONNREFUSED") {
1274
+ console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
1275
+ }
1276
+ if (errAny.code === "ENOTFOUND") {
1277
+ console.error(" Hint: DNS resolution failed; double-check the host name");
1278
+ }
1279
+ if (errAny.code === "ETIMEDOUT") {
1280
+ console.error(" Hint: connection timed out; check network/firewall rules");
1281
+ }
868
1282
  }
1283
+ process.exitCode = 1;
869
1284
  }
870
- process.exitCode = 1;
871
1285
  } finally {
872
1286
  if (client) {
873
1287
  try {