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.
- package/README.md +32 -0
- package/bin/postgres-ai.ts +486 -72
- package/dist/bin/postgres-ai.js +828 -75
- package/lib/metrics-embedded.ts +1 -1
- package/lib/supabase.ts +769 -0
- package/package.json +1 -1
- package/test/supabase.test.ts +568 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
console.error(
|
|
693
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
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
|
-
|
|
836
|
-
|
|
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
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
|
864
|
-
|
|
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 === "
|
|
867
|
-
|
|
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 {
|