postgresai 0.14.0-beta.12 → 0.14.0-beta.13
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 +928 -170
- package/dist/bin/postgres-ai.js +2095 -335
- package/lib/checkup.ts +69 -3
- package/lib/init.ts +76 -19
- package/lib/issues.ts +453 -7
- package/lib/mcp-server.ts +180 -3
- package/lib/metrics-embedded.ts +3 -3
- package/lib/supabase.ts +824 -0
- package/package.json +1 -1
- package/test/checkup.test.ts +240 -14
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +80 -71
- package/test/init.test.ts +266 -1
- package/test/issues.cli.test.ts +224 -0
- package/test/mcp-server.test.ts +551 -12
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +6 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -10,9 +10,10 @@ import * as os from "os";
|
|
|
10
10
|
import * as crypto from "node:crypto";
|
|
11
11
|
import { Client } from "pg";
|
|
12
12
|
import { startMcpServer } from "../lib/mcp-server";
|
|
13
|
-
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment } from "../lib/issues";
|
|
13
|
+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
|
|
14
14
|
import { resolveBaseUrls } from "../lib/util";
|
|
15
|
-
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
15
|
+
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
|
|
16
|
+
import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, 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";
|
|
@@ -564,10 +565,15 @@ program
|
|
|
564
565
|
.option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER)
|
|
565
566
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
566
567
|
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
568
|
+
.option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.")
|
|
567
569
|
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
568
570
|
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
569
571
|
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
570
572
|
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
573
|
+
.option("--supabase", "Use Supabase Management API instead of direct PostgreSQL connection", false)
|
|
574
|
+
.option("--supabase-access-token <token>", "Supabase Management API access token (or SUPABASE_ACCESS_TOKEN env)")
|
|
575
|
+
.option("--supabase-project-ref <ref>", "Supabase project reference (or SUPABASE_PROJECT_REF env)")
|
|
576
|
+
.option("--json", "Output result as JSON (machine-readable)", false)
|
|
571
577
|
.addHelpText(
|
|
572
578
|
"after",
|
|
573
579
|
[
|
|
@@ -607,6 +613,17 @@ program
|
|
|
607
613
|
"",
|
|
608
614
|
"Offline SQL plan (no DB connection):",
|
|
609
615
|
" postgresai prepare-db --print-sql",
|
|
616
|
+
"",
|
|
617
|
+
"Supabase mode (use Management API instead of direct connection):",
|
|
618
|
+
" postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
619
|
+
" SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
620
|
+
"",
|
|
621
|
+
" Generate a token at: https://supabase.com/dashboard/account/tokens",
|
|
622
|
+
" Find your project ref in: https://supabase.com/dashboard/project/<ref>",
|
|
623
|
+
"",
|
|
624
|
+
"Provider-specific behavior (for direct connections):",
|
|
625
|
+
" --provider supabase Skip role creation (create user in Supabase dashboard)",
|
|
626
|
+
" Skip ALTER USER (restricted by Supabase)",
|
|
610
627
|
].join("\n")
|
|
611
628
|
)
|
|
612
629
|
.action(async (conn: string | undefined, opts: {
|
|
@@ -619,25 +636,63 @@ program
|
|
|
619
636
|
monitoringUser: string;
|
|
620
637
|
password?: string;
|
|
621
638
|
skipOptionalPermissions?: boolean;
|
|
639
|
+
provider?: string;
|
|
622
640
|
verify?: boolean;
|
|
623
641
|
resetPassword?: boolean;
|
|
624
642
|
printSql?: boolean;
|
|
625
643
|
printPassword?: boolean;
|
|
644
|
+
supabase?: boolean;
|
|
645
|
+
supabaseAccessToken?: string;
|
|
646
|
+
supabaseProjectRef?: string;
|
|
647
|
+
json?: boolean;
|
|
626
648
|
}, cmd: Command) => {
|
|
627
|
-
|
|
628
|
-
|
|
649
|
+
// JSON output helper
|
|
650
|
+
const jsonOutput = opts.json;
|
|
651
|
+
const outputJson = (data: Record<string, unknown>) => {
|
|
652
|
+
console.log(JSON.stringify(data, null, 2));
|
|
653
|
+
};
|
|
654
|
+
const outputError = (error: {
|
|
655
|
+
message: string;
|
|
656
|
+
step?: string;
|
|
657
|
+
code?: string;
|
|
658
|
+
detail?: string;
|
|
659
|
+
hint?: string;
|
|
660
|
+
httpStatus?: number;
|
|
661
|
+
}) => {
|
|
662
|
+
if (jsonOutput) {
|
|
663
|
+
outputJson({
|
|
664
|
+
success: false,
|
|
665
|
+
mode: opts.supabase ? "supabase" : "direct",
|
|
666
|
+
error,
|
|
667
|
+
});
|
|
668
|
+
} else {
|
|
669
|
+
console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error.message}`);
|
|
670
|
+
if (error.step) console.error(` Step: ${error.step}`);
|
|
671
|
+
if (error.code) console.error(` Code: ${error.code}`);
|
|
672
|
+
if (error.detail) console.error(` Detail: ${error.detail}`);
|
|
673
|
+
if (error.hint) console.error(` Hint: ${error.hint}`);
|
|
674
|
+
if (error.httpStatus) console.error(` HTTP Status: ${error.httpStatus}`);
|
|
675
|
+
}
|
|
629
676
|
process.exitCode = 1;
|
|
677
|
+
};
|
|
678
|
+
if (opts.verify && opts.resetPassword) {
|
|
679
|
+
outputError({ message: "Provide only one of --verify or --reset-password" });
|
|
630
680
|
return;
|
|
631
681
|
}
|
|
632
682
|
if (opts.verify && opts.printSql) {
|
|
633
|
-
|
|
634
|
-
process.exitCode = 1;
|
|
683
|
+
outputError({ message: "--verify cannot be combined with --print-sql" });
|
|
635
684
|
return;
|
|
636
685
|
}
|
|
637
686
|
|
|
638
687
|
const shouldPrintSql = !!opts.printSql;
|
|
639
688
|
const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
|
|
640
689
|
|
|
690
|
+
// Validate provider and warn if unknown
|
|
691
|
+
const providerWarning = validateProvider(opts.provider);
|
|
692
|
+
if (providerWarning) {
|
|
693
|
+
console.warn(`⚠ ${providerWarning}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
641
696
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
642
697
|
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
|
|
643
698
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
@@ -655,11 +710,13 @@ program
|
|
|
655
710
|
monitoringUser: opts.monitoringUser,
|
|
656
711
|
monitoringPassword: monPassword,
|
|
657
712
|
includeOptionalPermissions,
|
|
713
|
+
provider: opts.provider,
|
|
658
714
|
});
|
|
659
715
|
|
|
660
716
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
661
717
|
console.log(`-- database: ${database}`);
|
|
662
718
|
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
719
|
+
console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
|
|
663
720
|
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
664
721
|
for (const step of plan.steps) {
|
|
665
722
|
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
@@ -671,6 +728,313 @@ program
|
|
|
671
728
|
}
|
|
672
729
|
}
|
|
673
730
|
|
|
731
|
+
// Supabase mode: use Supabase Management API instead of direct PG connection
|
|
732
|
+
if (opts.supabase) {
|
|
733
|
+
let supabaseConfig;
|
|
734
|
+
try {
|
|
735
|
+
// Try to extract project ref from connection URL if provided
|
|
736
|
+
let projectRef = opts.supabaseProjectRef;
|
|
737
|
+
if (!projectRef && conn) {
|
|
738
|
+
projectRef = extractProjectRefFromUrl(conn);
|
|
739
|
+
}
|
|
740
|
+
supabaseConfig = resolveSupabaseConfig({
|
|
741
|
+
accessToken: opts.supabaseAccessToken,
|
|
742
|
+
projectRef,
|
|
743
|
+
});
|
|
744
|
+
} catch (e) {
|
|
745
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
746
|
+
outputError({ message: msg });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
751
|
+
|
|
752
|
+
if (!jsonOutput) {
|
|
753
|
+
console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
|
|
754
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
755
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const supabaseClient = new SupabaseClient(supabaseConfig);
|
|
759
|
+
|
|
760
|
+
// Fetch database URL for JSON output (non-blocking, best-effort)
|
|
761
|
+
let databaseUrl: string | null = null;
|
|
762
|
+
if (jsonOutput) {
|
|
763
|
+
databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
// Get current database name
|
|
768
|
+
const database = await supabaseClient.getCurrentDatabase();
|
|
769
|
+
if (!database) {
|
|
770
|
+
throw new Error("Failed to resolve current database name");
|
|
771
|
+
}
|
|
772
|
+
if (!jsonOutput) {
|
|
773
|
+
console.log(`Database: ${database}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (opts.verify) {
|
|
777
|
+
const v = await verifyInitSetupViaSupabase({
|
|
778
|
+
client: supabaseClient,
|
|
779
|
+
database,
|
|
780
|
+
monitoringUser: opts.monitoringUser,
|
|
781
|
+
includeOptionalPermissions,
|
|
782
|
+
});
|
|
783
|
+
if (v.ok) {
|
|
784
|
+
if (jsonOutput) {
|
|
785
|
+
const result: Record<string, unknown> = {
|
|
786
|
+
success: true,
|
|
787
|
+
mode: "supabase",
|
|
788
|
+
action: "verify",
|
|
789
|
+
database,
|
|
790
|
+
monitoringUser: opts.monitoringUser,
|
|
791
|
+
verified: true,
|
|
792
|
+
missingOptional: v.missingOptional,
|
|
793
|
+
};
|
|
794
|
+
if (databaseUrl) {
|
|
795
|
+
result.databaseUrl = databaseUrl;
|
|
796
|
+
}
|
|
797
|
+
outputJson(result);
|
|
798
|
+
} else {
|
|
799
|
+
console.log("✓ prepare-db verify: OK");
|
|
800
|
+
if (v.missingOptional.length > 0) {
|
|
801
|
+
console.log("⚠ Optional items missing:");
|
|
802
|
+
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (jsonOutput) {
|
|
808
|
+
const result: Record<string, unknown> = {
|
|
809
|
+
success: false,
|
|
810
|
+
mode: "supabase",
|
|
811
|
+
action: "verify",
|
|
812
|
+
database,
|
|
813
|
+
monitoringUser: opts.monitoringUser,
|
|
814
|
+
verified: false,
|
|
815
|
+
missingRequired: v.missingRequired,
|
|
816
|
+
missingOptional: v.missingOptional,
|
|
817
|
+
};
|
|
818
|
+
if (databaseUrl) {
|
|
819
|
+
result.databaseUrl = databaseUrl;
|
|
820
|
+
}
|
|
821
|
+
outputJson(result);
|
|
822
|
+
} else {
|
|
823
|
+
console.error("✗ prepare-db verify failed: missing required items");
|
|
824
|
+
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
825
|
+
if (v.missingOptional.length > 0) {
|
|
826
|
+
console.error("Optional items missing:");
|
|
827
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
let monPassword: string;
|
|
835
|
+
let passwordGenerated = false;
|
|
836
|
+
try {
|
|
837
|
+
const resolved = await resolveMonitoringPassword({
|
|
838
|
+
passwordFlag: opts.password,
|
|
839
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
840
|
+
monitoringUser: opts.monitoringUser,
|
|
841
|
+
});
|
|
842
|
+
monPassword = resolved.password;
|
|
843
|
+
passwordGenerated = resolved.generated;
|
|
844
|
+
if (resolved.generated) {
|
|
845
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
846
|
+
if (canPrint) {
|
|
847
|
+
if (!jsonOutput) {
|
|
848
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
849
|
+
console.error("");
|
|
850
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
851
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
852
|
+
console.error("");
|
|
853
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
854
|
+
}
|
|
855
|
+
// For JSON mode, password will be included in the success output below
|
|
856
|
+
} else {
|
|
857
|
+
console.error(
|
|
858
|
+
[
|
|
859
|
+
`✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
860
|
+
"",
|
|
861
|
+
"Provide it explicitly:",
|
|
862
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
863
|
+
"",
|
|
864
|
+
"Or (NOT recommended) print the generated password:",
|
|
865
|
+
" --print-password",
|
|
866
|
+
].join("\n")
|
|
867
|
+
);
|
|
868
|
+
process.exitCode = 1;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
} catch (e) {
|
|
873
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
874
|
+
outputError({ message: msg });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const plan = await buildInitPlan({
|
|
879
|
+
database,
|
|
880
|
+
monitoringUser: opts.monitoringUser,
|
|
881
|
+
monitoringPassword: monPassword,
|
|
882
|
+
includeOptionalPermissions,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// For Supabase mode, skip RDS and self-managed steps (they don't apply)
|
|
886
|
+
const supabaseApplicableSteps = plan.steps.filter(
|
|
887
|
+
(s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed"
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const effectivePlan = opts.resetPassword
|
|
891
|
+
? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") }
|
|
892
|
+
: { ...plan, steps: supabaseApplicableSteps };
|
|
893
|
+
|
|
894
|
+
if (shouldPrintSql) {
|
|
895
|
+
console.log("\n--- SQL plan ---");
|
|
896
|
+
for (const step of effectivePlan.steps) {
|
|
897
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
898
|
+
console.log(redactPasswords(step.sql));
|
|
899
|
+
}
|
|
900
|
+
console.log("\n--- end SQL plan ---\n");
|
|
901
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const { applied, skippedOptional } = await applyInitPlanViaSupabase({
|
|
906
|
+
client: supabaseClient,
|
|
907
|
+
plan: effectivePlan,
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
if (jsonOutput) {
|
|
911
|
+
const result: Record<string, unknown> = {
|
|
912
|
+
success: true,
|
|
913
|
+
mode: "supabase",
|
|
914
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
915
|
+
database,
|
|
916
|
+
monitoringUser: opts.monitoringUser,
|
|
917
|
+
applied,
|
|
918
|
+
skippedOptional,
|
|
919
|
+
warnings: skippedOptional.length > 0
|
|
920
|
+
? ["Some optional steps were skipped (not supported or insufficient privileges)"]
|
|
921
|
+
: [],
|
|
922
|
+
};
|
|
923
|
+
if (passwordGenerated) {
|
|
924
|
+
result.generatedPassword = monPassword;
|
|
925
|
+
}
|
|
926
|
+
if (databaseUrl) {
|
|
927
|
+
result.databaseUrl = databaseUrl;
|
|
928
|
+
}
|
|
929
|
+
outputJson(result);
|
|
930
|
+
} else {
|
|
931
|
+
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
932
|
+
if (skippedOptional.length > 0) {
|
|
933
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
934
|
+
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
935
|
+
}
|
|
936
|
+
if (process.stdout.isTTY) {
|
|
937
|
+
console.log(`Applied ${applied.length} steps`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
} catch (error) {
|
|
941
|
+
const errAny = error as PgCompatibleError;
|
|
942
|
+
let message = "";
|
|
943
|
+
if (error instanceof Error && error.message) {
|
|
944
|
+
message = error.message;
|
|
945
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
946
|
+
message = errAny.message;
|
|
947
|
+
} else {
|
|
948
|
+
message = String(error);
|
|
949
|
+
}
|
|
950
|
+
if (!message || message === "[object Object]") {
|
|
951
|
+
message = "Unknown error";
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Surface step name if this was a plan step failure
|
|
955
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
956
|
+
const failedStep = stepMatch?.[1];
|
|
957
|
+
|
|
958
|
+
// Build error object for JSON output
|
|
959
|
+
const errorObj: {
|
|
960
|
+
message: string;
|
|
961
|
+
step?: string;
|
|
962
|
+
code?: string;
|
|
963
|
+
detail?: string;
|
|
964
|
+
hint?: string;
|
|
965
|
+
httpStatus?: number;
|
|
966
|
+
} = { message };
|
|
967
|
+
|
|
968
|
+
if (failedStep) errorObj.step = failedStep;
|
|
969
|
+
if (errAny && typeof errAny === "object") {
|
|
970
|
+
if (typeof errAny.code === "string" && errAny.code) errorObj.code = errAny.code;
|
|
971
|
+
if (typeof errAny.detail === "string" && errAny.detail) errorObj.detail = errAny.detail;
|
|
972
|
+
if (typeof errAny.hint === "string" && errAny.hint) errorObj.hint = errAny.hint;
|
|
973
|
+
if (typeof errAny.httpStatus === "number") errorObj.httpStatus = errAny.httpStatus;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (jsonOutput) {
|
|
977
|
+
outputJson({
|
|
978
|
+
success: false,
|
|
979
|
+
mode: "supabase",
|
|
980
|
+
error: errorObj,
|
|
981
|
+
});
|
|
982
|
+
process.exitCode = 1;
|
|
983
|
+
} else {
|
|
984
|
+
console.error(`Error: prepare-db (Supabase): ${message}`);
|
|
985
|
+
|
|
986
|
+
if (failedStep) {
|
|
987
|
+
console.error(` Step: ${failedStep}`);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Surface PostgreSQL-compatible error details
|
|
991
|
+
if (errAny && typeof errAny === "object") {
|
|
992
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
993
|
+
console.error(` Code: ${errAny.code}`);
|
|
994
|
+
}
|
|
995
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
996
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
997
|
+
}
|
|
998
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
999
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (typeof errAny.httpStatus === "number") {
|
|
1002
|
+
console.error(` HTTP Status: ${errAny.httpStatus}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Provide context hints for common errors
|
|
1007
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
1008
|
+
if (errAny.code === "42501") {
|
|
1009
|
+
if (failedStep === "01.role") {
|
|
1010
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
1011
|
+
} else if (failedStep === "02.permissions") {
|
|
1012
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
1013
|
+
}
|
|
1014
|
+
console.error(" Fix: ensure your Supabase access token has sufficient permissions");
|
|
1015
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
|
|
1019
|
+
if (errAny.httpStatus === 401) {
|
|
1020
|
+
console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
|
|
1021
|
+
}
|
|
1022
|
+
if (errAny.httpStatus === 403) {
|
|
1023
|
+
console.error(" Hint: access denied; check your token permissions and project access");
|
|
1024
|
+
}
|
|
1025
|
+
if (errAny.httpStatus === 404) {
|
|
1026
|
+
console.error(" Hint: project not found; verify the project reference is correct");
|
|
1027
|
+
}
|
|
1028
|
+
if (errAny.httpStatus === 429) {
|
|
1029
|
+
console.error(" Hint: rate limited; wait a moment and try again");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
process.exitCode = 1;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
674
1038
|
let adminConn;
|
|
675
1039
|
try {
|
|
676
1040
|
adminConn = resolveAdminConnection({
|
|
@@ -686,21 +1050,27 @@ program
|
|
|
686
1050
|
});
|
|
687
1051
|
} catch (e) {
|
|
688
1052
|
const msg = e instanceof Error ? e.message : String(e);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
console.error(
|
|
693
|
-
|
|
1053
|
+
if (jsonOutput) {
|
|
1054
|
+
outputError({ message: msg });
|
|
1055
|
+
} else {
|
|
1056
|
+
console.error(`Error: prepare-db: ${msg}`);
|
|
1057
|
+
// When connection details are missing, show full init help (options + examples).
|
|
1058
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
1059
|
+
console.error("");
|
|
1060
|
+
cmd.outputHelp({ error: true });
|
|
1061
|
+
}
|
|
1062
|
+
process.exitCode = 1;
|
|
694
1063
|
}
|
|
695
|
-
process.exitCode = 1;
|
|
696
1064
|
return;
|
|
697
1065
|
}
|
|
698
1066
|
|
|
699
1067
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
700
1068
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1069
|
+
if (!jsonOutput) {
|
|
1070
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
1071
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
1072
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
1073
|
+
}
|
|
704
1074
|
|
|
705
1075
|
// Use native pg client instead of requiring psql to be installed
|
|
706
1076
|
let client: Client | undefined;
|
|
@@ -720,26 +1090,54 @@ program
|
|
|
720
1090
|
database,
|
|
721
1091
|
monitoringUser: opts.monitoringUser,
|
|
722
1092
|
includeOptionalPermissions,
|
|
1093
|
+
provider: opts.provider,
|
|
723
1094
|
});
|
|
724
1095
|
if (v.ok) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1096
|
+
if (jsonOutput) {
|
|
1097
|
+
outputJson({
|
|
1098
|
+
success: true,
|
|
1099
|
+
mode: "direct",
|
|
1100
|
+
action: "verify",
|
|
1101
|
+
database,
|
|
1102
|
+
monitoringUser: opts.monitoringUser,
|
|
1103
|
+
provider: opts.provider,
|
|
1104
|
+
verified: true,
|
|
1105
|
+
missingOptional: v.missingOptional,
|
|
1106
|
+
});
|
|
1107
|
+
} else {
|
|
1108
|
+
console.log(`✓ prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
1109
|
+
if (v.missingOptional.length > 0) {
|
|
1110
|
+
console.log("⚠ Optional items missing:");
|
|
1111
|
+
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
1112
|
+
}
|
|
729
1113
|
}
|
|
730
1114
|
return;
|
|
731
1115
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1116
|
+
if (jsonOutput) {
|
|
1117
|
+
outputJson({
|
|
1118
|
+
success: false,
|
|
1119
|
+
mode: "direct",
|
|
1120
|
+
action: "verify",
|
|
1121
|
+
database,
|
|
1122
|
+
monitoringUser: opts.monitoringUser,
|
|
1123
|
+
verified: false,
|
|
1124
|
+
missingRequired: v.missingRequired,
|
|
1125
|
+
missingOptional: v.missingOptional,
|
|
1126
|
+
});
|
|
1127
|
+
} else {
|
|
1128
|
+
console.error("✗ prepare-db verify failed: missing required items");
|
|
1129
|
+
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
1130
|
+
if (v.missingOptional.length > 0) {
|
|
1131
|
+
console.error("Optional items missing:");
|
|
1132
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
1133
|
+
}
|
|
737
1134
|
}
|
|
738
1135
|
process.exitCode = 1;
|
|
739
1136
|
return;
|
|
740
1137
|
}
|
|
741
1138
|
|
|
742
1139
|
let monPassword: string;
|
|
1140
|
+
let passwordGenerated = false;
|
|
743
1141
|
try {
|
|
744
1142
|
const resolved = await resolveMonitoringPassword({
|
|
745
1143
|
passwordFlag: opts.password,
|
|
@@ -747,17 +1145,21 @@ program
|
|
|
747
1145
|
monitoringUser: opts.monitoringUser,
|
|
748
1146
|
});
|
|
749
1147
|
monPassword = resolved.password;
|
|
1148
|
+
passwordGenerated = resolved.generated;
|
|
750
1149
|
if (resolved.generated) {
|
|
751
|
-
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
1150
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
752
1151
|
if (canPrint) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1152
|
+
if (!jsonOutput) {
|
|
1153
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
1154
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
1155
|
+
console.error("");
|
|
1156
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
1157
|
+
// Quote for shell copy/paste safety.
|
|
1158
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
1159
|
+
console.error("");
|
|
1160
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
1161
|
+
}
|
|
1162
|
+
// For JSON mode, password will be included in the success output below
|
|
761
1163
|
} else {
|
|
762
1164
|
console.error(
|
|
763
1165
|
[
|
|
@@ -776,8 +1178,7 @@ program
|
|
|
776
1178
|
}
|
|
777
1179
|
} catch (e) {
|
|
778
1180
|
const msg = e instanceof Error ? e.message : String(e);
|
|
779
|
-
|
|
780
|
-
process.exitCode = 1;
|
|
1181
|
+
outputError({ message: msg });
|
|
781
1182
|
return;
|
|
782
1183
|
}
|
|
783
1184
|
|
|
@@ -786,12 +1187,21 @@ program
|
|
|
786
1187
|
monitoringUser: opts.monitoringUser,
|
|
787
1188
|
monitoringPassword: monPassword,
|
|
788
1189
|
includeOptionalPermissions,
|
|
1190
|
+
provider: opts.provider,
|
|
789
1191
|
});
|
|
790
1192
|
|
|
1193
|
+
// For reset-password, we only want the role step. But if provider skips role creation,
|
|
1194
|
+
// reset-password doesn't make sense - warn the user.
|
|
791
1195
|
const effectivePlan = opts.resetPassword
|
|
792
1196
|
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
793
1197
|
: plan;
|
|
794
1198
|
|
|
1199
|
+
if (opts.resetPassword && effectivePlan.steps.length === 0) {
|
|
1200
|
+
console.error(`✗ --reset-password not supported for provider "${opts.provider}" (role creation is skipped)`);
|
|
1201
|
+
process.exitCode = 1;
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
795
1205
|
if (shouldPrintSql) {
|
|
796
1206
|
console.log("\n--- SQL plan ---");
|
|
797
1207
|
for (const step of effectivePlan.steps) {
|
|
@@ -805,14 +1215,33 @@ program
|
|
|
805
1215
|
|
|
806
1216
|
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
807
1217
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1218
|
+
if (jsonOutput) {
|
|
1219
|
+
const result: Record<string, unknown> = {
|
|
1220
|
+
success: true,
|
|
1221
|
+
mode: "direct",
|
|
1222
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
1223
|
+
database,
|
|
1224
|
+
monitoringUser: opts.monitoringUser,
|
|
1225
|
+
applied,
|
|
1226
|
+
skippedOptional,
|
|
1227
|
+
warnings: skippedOptional.length > 0
|
|
1228
|
+
? ["Some optional steps were skipped (not supported or insufficient privileges)"]
|
|
1229
|
+
: [],
|
|
1230
|
+
};
|
|
1231
|
+
if (passwordGenerated) {
|
|
1232
|
+
result.generatedPassword = monPassword;
|
|
1233
|
+
}
|
|
1234
|
+
outputJson(result);
|
|
1235
|
+
} else {
|
|
1236
|
+
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
1237
|
+
if (skippedOptional.length > 0) {
|
|
1238
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
1239
|
+
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
1240
|
+
}
|
|
1241
|
+
// Keep output compact but still useful
|
|
1242
|
+
if (process.stdout.isTTY) {
|
|
1243
|
+
console.log(`Applied ${applied.length} steps`);
|
|
1244
|
+
}
|
|
816
1245
|
}
|
|
817
1246
|
} catch (error) {
|
|
818
1247
|
const errAny = error as any;
|
|
@@ -827,47 +1256,74 @@ program
|
|
|
827
1256
|
if (!message || message === "[object Object]") {
|
|
828
1257
|
message = "Unknown error";
|
|
829
1258
|
}
|
|
830
|
-
|
|
1259
|
+
|
|
831
1260
|
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
832
1261
|
const stepMatch =
|
|
833
1262
|
typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
834
1263
|
const failedStep = stepMatch?.[1];
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1264
|
+
|
|
1265
|
+
// Build error object for JSON output
|
|
1266
|
+
const errorObj: {
|
|
1267
|
+
message: string;
|
|
1268
|
+
step?: string;
|
|
1269
|
+
code?: string;
|
|
1270
|
+
detail?: string;
|
|
1271
|
+
hint?: string;
|
|
1272
|
+
} = { message };
|
|
1273
|
+
|
|
1274
|
+
if (failedStep) errorObj.step = failedStep;
|
|
838
1275
|
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
|
-
}
|
|
1276
|
+
if (typeof errAny.code === "string" && errAny.code) errorObj.code = errAny.code;
|
|
1277
|
+
if (typeof errAny.detail === "string" && errAny.detail) errorObj.detail = errAny.detail;
|
|
1278
|
+
if (typeof errAny.hint === "string" && errAny.hint) errorObj.hint = errAny.hint;
|
|
848
1279
|
}
|
|
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");
|
|
1280
|
+
|
|
1281
|
+
if (jsonOutput) {
|
|
1282
|
+
outputJson({
|
|
1283
|
+
success: false,
|
|
1284
|
+
mode: "direct",
|
|
1285
|
+
error: errorObj,
|
|
1286
|
+
});
|
|
1287
|
+
process.exitCode = 1;
|
|
1288
|
+
} else {
|
|
1289
|
+
console.error(`Error: prepare-db: ${message}`);
|
|
1290
|
+
if (failedStep) {
|
|
1291
|
+
console.error(` Step: ${failedStep}`);
|
|
862
1292
|
}
|
|
863
|
-
if (errAny
|
|
864
|
-
|
|
1293
|
+
if (errAny && typeof errAny === "object") {
|
|
1294
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
1295
|
+
console.error(` Code: ${errAny.code}`);
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
1298
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
1301
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
1302
|
+
}
|
|
865
1303
|
}
|
|
866
|
-
if (errAny.code === "
|
|
867
|
-
|
|
1304
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
1305
|
+
if (errAny.code === "42501") {
|
|
1306
|
+
if (failedStep === "01.role") {
|
|
1307
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
1308
|
+
} else if (failedStep === "02.permissions") {
|
|
1309
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
1310
|
+
}
|
|
1311
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
1312
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
1313
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
1314
|
+
}
|
|
1315
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
1316
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
1317
|
+
}
|
|
1318
|
+
if (errAny.code === "ENOTFOUND") {
|
|
1319
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
1320
|
+
}
|
|
1321
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
1322
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
1323
|
+
}
|
|
868
1324
|
}
|
|
1325
|
+
process.exitCode = 1;
|
|
869
1326
|
}
|
|
870
|
-
process.exitCode = 1;
|
|
871
1327
|
} finally {
|
|
872
1328
|
if (client) {
|
|
873
1329
|
try {
|
|
@@ -1204,25 +1660,25 @@ mon
|
|
|
1204
1660
|
// Update .env with custom tag if provided
|
|
1205
1661
|
const envFile = path.resolve(projectDir, ".env");
|
|
1206
1662
|
|
|
1207
|
-
// Build .env content, preserving important existing values
|
|
1208
|
-
//
|
|
1209
|
-
let existingTag: string | null = null;
|
|
1663
|
+
// Build .env content, preserving important existing values (registry, password)
|
|
1664
|
+
// Note: PGAI_TAG is intentionally NOT preserved - the CLI version should always match Docker images
|
|
1210
1665
|
let existingRegistry: string | null = null;
|
|
1211
1666
|
let existingPassword: string | null = null;
|
|
1212
1667
|
|
|
1213
1668
|
if (fs.existsSync(envFile)) {
|
|
1214
1669
|
const existingEnv = fs.readFileSync(envFile, "utf8");
|
|
1215
|
-
// Extract existing values
|
|
1216
|
-
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
1217
|
-
if (tagMatch) existingTag = tagMatch[1].trim();
|
|
1670
|
+
// Extract existing values (except tag - always use CLI version)
|
|
1218
1671
|
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
1219
1672
|
if (registryMatch) existingRegistry = registryMatch[1].trim();
|
|
1220
1673
|
const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
|
|
1221
1674
|
if (pwdMatch) existingPassword = pwdMatch[1].trim();
|
|
1222
1675
|
}
|
|
1223
1676
|
|
|
1224
|
-
// Priority: CLI --tag flag >
|
|
1225
|
-
|
|
1677
|
+
// Priority: CLI --tag flag > package version
|
|
1678
|
+
// Note: We intentionally do NOT use process.env.PGAI_TAG here because Bun auto-loads .env files,
|
|
1679
|
+
// which would cause stale .env values to override the CLI version. The CLI version should always
|
|
1680
|
+
// match the Docker images. Users can override with --tag if needed.
|
|
1681
|
+
const imageTag = opts.tag || pkg.version;
|
|
1226
1682
|
|
|
1227
1683
|
const envLines: string[] = [`PGAI_TAG=${imageTag}`];
|
|
1228
1684
|
if (existingRegistry) {
|
|
@@ -1550,6 +2006,16 @@ const MONITORING_CONTAINERS = [
|
|
|
1550
2006
|
"postgres-reports",
|
|
1551
2007
|
];
|
|
1552
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Network cleanup constants.
|
|
2011
|
+
* Docker Compose creates a default network named "{project}_default".
|
|
2012
|
+
* In CI environments, network cleanup can fail if containers are slow to disconnect.
|
|
2013
|
+
*/
|
|
2014
|
+
const COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
2015
|
+
const DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
2016
|
+
/** Delay before retrying network cleanup (allows container network disconnections to complete) */
|
|
2017
|
+
const NETWORK_CLEANUP_DELAY_MS = 2000;
|
|
2018
|
+
|
|
1553
2019
|
/** Remove orphaned containers that docker compose down might miss */
|
|
1554
2020
|
async function removeOrphanedContainers(): Promise<void> {
|
|
1555
2021
|
for (const container of MONITORING_CONTAINERS) {
|
|
@@ -1565,7 +2031,33 @@ mon
|
|
|
1565
2031
|
.command("stop")
|
|
1566
2032
|
.description("stop monitoring services")
|
|
1567
2033
|
.action(async () => {
|
|
1568
|
-
|
|
2034
|
+
// Multi-stage cleanup strategy for reliable shutdown in CI environments:
|
|
2035
|
+
// Stage 1: Standard compose down with orphan removal
|
|
2036
|
+
// Stage 2: Force remove any orphaned containers, then retry compose down
|
|
2037
|
+
// Stage 3: Force remove the Docker network directly
|
|
2038
|
+
// This handles edge cases where containers are slow to disconnect from networks.
|
|
2039
|
+
let code = await runCompose(["down", "--remove-orphans"]);
|
|
2040
|
+
|
|
2041
|
+
// Stage 2: If initial cleanup fails, try removing orphaned containers first
|
|
2042
|
+
if (code !== 0) {
|
|
2043
|
+
await removeOrphanedContainers();
|
|
2044
|
+
// Wait a moment for container network disconnections to complete
|
|
2045
|
+
await new Promise(resolve => setTimeout(resolve, NETWORK_CLEANUP_DELAY_MS));
|
|
2046
|
+
// Retry compose down
|
|
2047
|
+
code = await runCompose(["down", "--remove-orphans"]);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Final cleanup: force remove the network if it still exists
|
|
2051
|
+
if (code !== 0) {
|
|
2052
|
+
try {
|
|
2053
|
+
await execFilePromise("docker", ["network", "rm", DOCKER_NETWORK_NAME]);
|
|
2054
|
+
// Network removal succeeded - cleanup is complete
|
|
2055
|
+
code = 0;
|
|
2056
|
+
} catch {
|
|
2057
|
+
// Network doesn't exist or couldn't be removed, ignore
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1569
2061
|
if (code !== 0) process.exitCode = code;
|
|
1570
2062
|
});
|
|
1571
2063
|
|
|
@@ -2524,22 +3016,44 @@ const issues = program.command("issues").description("issues management");
|
|
|
2524
3016
|
issues
|
|
2525
3017
|
.command("list")
|
|
2526
3018
|
.description("list issues")
|
|
3019
|
+
.option("--status <status>", "filter by status: open, closed, or all (default: all)")
|
|
3020
|
+
.option("--limit <n>", "max number of issues to return (default: 20)", parseInt)
|
|
3021
|
+
.option("--offset <n>", "number of issues to skip (default: 0)", parseInt)
|
|
2527
3022
|
.option("--debug", "enable debug output")
|
|
2528
3023
|
.option("--json", "output raw JSON")
|
|
2529
|
-
.action(async (opts: { debug?: boolean; json?: boolean }) => {
|
|
3024
|
+
.action(async (opts: { status?: string; limit?: number; offset?: number; debug?: boolean; json?: boolean }) => {
|
|
3025
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
2530
3026
|
try {
|
|
2531
3027
|
const rootOpts = program.opts<CliOptions>();
|
|
2532
3028
|
const cfg = config.readConfig();
|
|
2533
3029
|
const { apiKey } = getConfig(rootOpts);
|
|
2534
3030
|
if (!apiKey) {
|
|
3031
|
+
spinner.stop();
|
|
2535
3032
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2536
3033
|
process.exitCode = 1;
|
|
2537
3034
|
return;
|
|
2538
3035
|
}
|
|
3036
|
+
const orgId = cfg.orgId ?? undefined;
|
|
2539
3037
|
|
|
2540
3038
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2541
3039
|
|
|
2542
|
-
|
|
3040
|
+
let statusFilter: "open" | "closed" | undefined;
|
|
3041
|
+
if (opts.status === "open") {
|
|
3042
|
+
statusFilter = "open";
|
|
3043
|
+
} else if (opts.status === "closed") {
|
|
3044
|
+
statusFilter = "closed";
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
const result = await fetchIssues({
|
|
3048
|
+
apiKey,
|
|
3049
|
+
apiBaseUrl,
|
|
3050
|
+
orgId,
|
|
3051
|
+
status: statusFilter,
|
|
3052
|
+
limit: opts.limit,
|
|
3053
|
+
offset: opts.offset,
|
|
3054
|
+
debug: !!opts.debug,
|
|
3055
|
+
});
|
|
3056
|
+
spinner.stop();
|
|
2543
3057
|
const trimmed = Array.isArray(result)
|
|
2544
3058
|
? (result as any[]).map((r) => ({
|
|
2545
3059
|
id: (r as any).id,
|
|
@@ -2550,6 +3064,7 @@ issues
|
|
|
2550
3064
|
: result;
|
|
2551
3065
|
printResult(trimmed, opts.json);
|
|
2552
3066
|
} catch (err) {
|
|
3067
|
+
spinner.stop();
|
|
2553
3068
|
const message = err instanceof Error ? err.message : String(err);
|
|
2554
3069
|
console.error(message);
|
|
2555
3070
|
process.exitCode = 1;
|
|
@@ -2562,11 +3077,13 @@ issues
|
|
|
2562
3077
|
.option("--debug", "enable debug output")
|
|
2563
3078
|
.option("--json", "output raw JSON")
|
|
2564
3079
|
.action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3080
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
|
|
2565
3081
|
try {
|
|
2566
3082
|
const rootOpts = program.opts<CliOptions>();
|
|
2567
3083
|
const cfg = config.readConfig();
|
|
2568
3084
|
const { apiKey } = getConfig(rootOpts);
|
|
2569
3085
|
if (!apiKey) {
|
|
3086
|
+
spinner.stop();
|
|
2570
3087
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2571
3088
|
process.exitCode = 1;
|
|
2572
3089
|
return;
|
|
@@ -2576,15 +3093,19 @@ issues
|
|
|
2576
3093
|
|
|
2577
3094
|
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
2578
3095
|
if (!issue) {
|
|
3096
|
+
spinner.stop();
|
|
2579
3097
|
console.error("Issue not found");
|
|
2580
3098
|
process.exitCode = 1;
|
|
2581
3099
|
return;
|
|
2582
3100
|
}
|
|
2583
3101
|
|
|
3102
|
+
spinner.update("Fetching comments...");
|
|
2584
3103
|
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
3104
|
+
spinner.stop();
|
|
2585
3105
|
const combined = { issue, comments };
|
|
2586
3106
|
printResult(combined, opts.json);
|
|
2587
3107
|
} catch (err) {
|
|
3108
|
+
spinner.stop();
|
|
2588
3109
|
const message = err instanceof Error ? err.message : String(err);
|
|
2589
3110
|
console.error(message);
|
|
2590
3111
|
process.exitCode = 1;
|
|
@@ -2598,22 +3119,24 @@ issues
|
|
|
2598
3119
|
.option("--debug", "enable debug output")
|
|
2599
3120
|
.option("--json", "output raw JSON")
|
|
2600
3121
|
.action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
}
|
|
3122
|
+
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
3123
|
+
if (opts.debug) {
|
|
3124
|
+
// eslint-disable-next-line no-console
|
|
3125
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3126
|
+
}
|
|
3127
|
+
content = interpretEscapes(content);
|
|
3128
|
+
if (opts.debug) {
|
|
3129
|
+
// eslint-disable-next-line no-console
|
|
3130
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3131
|
+
}
|
|
2612
3132
|
|
|
3133
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
3134
|
+
try {
|
|
2613
3135
|
const rootOpts = program.opts<CliOptions>();
|
|
2614
3136
|
const cfg = config.readConfig();
|
|
2615
3137
|
const { apiKey } = getConfig(rootOpts);
|
|
2616
3138
|
if (!apiKey) {
|
|
3139
|
+
spinner.stop();
|
|
2617
3140
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2618
3141
|
process.exitCode = 1;
|
|
2619
3142
|
return;
|
|
@@ -2629,8 +3152,10 @@ issues
|
|
|
2629
3152
|
parentCommentId: opts.parent,
|
|
2630
3153
|
debug: !!opts.debug,
|
|
2631
3154
|
});
|
|
3155
|
+
spinner.stop();
|
|
2632
3156
|
printResult(result, opts.json);
|
|
2633
3157
|
} catch (err) {
|
|
3158
|
+
spinner.stop();
|
|
2634
3159
|
const message = err instanceof Error ? err.message : String(err);
|
|
2635
3160
|
console.error(message);
|
|
2636
3161
|
process.exitCode = 1;
|
|
@@ -2642,7 +3167,7 @@ issues
|
|
|
2642
3167
|
.description("create a new issue")
|
|
2643
3168
|
.option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10))
|
|
2644
3169
|
.option("--project-id <id>", "project id", (v) => parseInt(v, 10))
|
|
2645
|
-
.option("--description <text>", "issue description (
|
|
3170
|
+
.option("--description <text>", "issue description (use \\n for newlines)")
|
|
2646
3171
|
.option(
|
|
2647
3172
|
"--label <label>",
|
|
2648
3173
|
"issue label (repeatable)",
|
|
@@ -2655,34 +3180,35 @@ issues
|
|
|
2655
3180
|
.option("--debug", "enable debug output")
|
|
2656
3181
|
.option("--json", "output raw JSON")
|
|
2657
3182
|
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
}
|
|
3183
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3184
|
+
const cfg = config.readConfig();
|
|
3185
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3186
|
+
if (!apiKey) {
|
|
3187
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3188
|
+
process.exitCode = 1;
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
2667
3191
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
3192
|
+
const title = interpretEscapes(String(rawTitle || "").trim());
|
|
3193
|
+
if (!title) {
|
|
3194
|
+
console.error("title is required");
|
|
3195
|
+
process.exitCode = 1;
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
2674
3198
|
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
3199
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
3200
|
+
if (typeof orgId !== "number") {
|
|
3201
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
3202
|
+
process.exitCode = 1;
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
2681
3205
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3206
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
3207
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
3208
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
2685
3209
|
|
|
3210
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
|
|
3211
|
+
try {
|
|
2686
3212
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2687
3213
|
const result = await createIssue({
|
|
2688
3214
|
apiKey,
|
|
@@ -2694,8 +3220,10 @@ issues
|
|
|
2694
3220
|
labels,
|
|
2695
3221
|
debug: !!opts.debug,
|
|
2696
3222
|
});
|
|
3223
|
+
spinner.stop();
|
|
2697
3224
|
printResult(result, opts.json);
|
|
2698
3225
|
} catch (err) {
|
|
3226
|
+
spinner.stop();
|
|
2699
3227
|
const message = err instanceof Error ? err.message : String(err);
|
|
2700
3228
|
console.error(message);
|
|
2701
3229
|
process.exitCode = 1;
|
|
@@ -2705,8 +3233,8 @@ issues
|
|
|
2705
3233
|
issues
|
|
2706
3234
|
.command("update <issueId>")
|
|
2707
3235
|
.description("update an existing issue (title/description/status/labels)")
|
|
2708
|
-
.option("--title <text>", "new title (
|
|
2709
|
-
.option("--description <text>", "new description (
|
|
3236
|
+
.option("--title <text>", "new title (use \\n for newlines)")
|
|
3237
|
+
.option("--description <text>", "new description (use \\n for newlines)")
|
|
2710
3238
|
.option("--status <value>", "status: open|closed|0|1")
|
|
2711
3239
|
.option(
|
|
2712
3240
|
"--label <label>",
|
|
@@ -2721,49 +3249,50 @@ issues
|
|
|
2721
3249
|
.option("--debug", "enable debug output")
|
|
2722
3250
|
.option("--json", "output raw JSON")
|
|
2723
3251
|
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
}
|
|
3252
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3253
|
+
const cfg = config.readConfig();
|
|
3254
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3255
|
+
if (!apiKey) {
|
|
3256
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3257
|
+
process.exitCode = 1;
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
2733
3260
|
|
|
2734
|
-
|
|
3261
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2735
3262
|
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
process.exitCode = 1;
|
|
2749
|
-
return;
|
|
2750
|
-
}
|
|
2751
|
-
status = n;
|
|
2752
|
-
}
|
|
2753
|
-
if (status !== 0 && status !== 1) {
|
|
2754
|
-
console.error("status must be 0 (open) or 1 (closed)");
|
|
3263
|
+
const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
|
|
3264
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
3265
|
+
|
|
3266
|
+
let status: number | undefined = undefined;
|
|
3267
|
+
if (opts.status !== undefined) {
|
|
3268
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
3269
|
+
if (raw === "open") status = 0;
|
|
3270
|
+
else if (raw === "closed") status = 1;
|
|
3271
|
+
else {
|
|
3272
|
+
const n = Number(raw);
|
|
3273
|
+
if (!Number.isFinite(n)) {
|
|
3274
|
+
console.error("status must be open|closed|0|1");
|
|
2755
3275
|
process.exitCode = 1;
|
|
2756
3276
|
return;
|
|
2757
3277
|
}
|
|
3278
|
+
status = n;
|
|
2758
3279
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
2764
|
-
labels = opts.label.map(String);
|
|
3280
|
+
if (status !== 0 && status !== 1) {
|
|
3281
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
3282
|
+
process.exitCode = 1;
|
|
3283
|
+
return;
|
|
2765
3284
|
}
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
let labels: string[] | undefined = undefined;
|
|
3288
|
+
if (opts.clearLabels) {
|
|
3289
|
+
labels = [];
|
|
3290
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
3291
|
+
labels = opts.label.map(String);
|
|
3292
|
+
}
|
|
2766
3293
|
|
|
3294
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
|
|
3295
|
+
try {
|
|
2767
3296
|
const result = await updateIssue({
|
|
2768
3297
|
apiKey,
|
|
2769
3298
|
apiBaseUrl,
|
|
@@ -2774,8 +3303,10 @@ issues
|
|
|
2774
3303
|
labels,
|
|
2775
3304
|
debug: !!opts.debug,
|
|
2776
3305
|
});
|
|
3306
|
+
spinner.stop();
|
|
2777
3307
|
printResult(result, opts.json);
|
|
2778
3308
|
} catch (err) {
|
|
3309
|
+
spinner.stop();
|
|
2779
3310
|
const message = err instanceof Error ? err.message : String(err);
|
|
2780
3311
|
console.error(message);
|
|
2781
3312
|
process.exitCode = 1;
|
|
@@ -2788,21 +3319,91 @@ issues
|
|
|
2788
3319
|
.option("--debug", "enable debug output")
|
|
2789
3320
|
.option("--json", "output raw JSON")
|
|
2790
3321
|
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3322
|
+
if (opts.debug) {
|
|
3323
|
+
// eslint-disable-next-line no-console
|
|
3324
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3325
|
+
}
|
|
3326
|
+
content = interpretEscapes(content);
|
|
3327
|
+
if (opts.debug) {
|
|
3328
|
+
// eslint-disable-next-line no-console
|
|
3329
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3333
|
+
const cfg = config.readConfig();
|
|
3334
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3335
|
+
if (!apiKey) {
|
|
3336
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3337
|
+
process.exitCode = 1;
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating comment...");
|
|
2791
3342
|
try {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3343
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3344
|
+
|
|
3345
|
+
const result = await updateIssueComment({
|
|
3346
|
+
apiKey,
|
|
3347
|
+
apiBaseUrl,
|
|
3348
|
+
commentId,
|
|
3349
|
+
content,
|
|
3350
|
+
debug: !!opts.debug,
|
|
3351
|
+
});
|
|
3352
|
+
spinner.stop();
|
|
3353
|
+
printResult(result, opts.json);
|
|
3354
|
+
} catch (err) {
|
|
3355
|
+
spinner.stop();
|
|
3356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3357
|
+
console.error(message);
|
|
3358
|
+
process.exitCode = 1;
|
|
3359
|
+
}
|
|
3360
|
+
});
|
|
3361
|
+
|
|
3362
|
+
// Action Items management (subcommands of issues)
|
|
3363
|
+
issues
|
|
3364
|
+
.command("action-items <issueId>")
|
|
3365
|
+
.description("list action items for an issue")
|
|
3366
|
+
.option("--debug", "enable debug output")
|
|
3367
|
+
.option("--json", "output raw JSON")
|
|
3368
|
+
.action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3369
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action items...");
|
|
3370
|
+
try {
|
|
3371
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3372
|
+
const cfg = config.readConfig();
|
|
3373
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3374
|
+
if (!apiKey) {
|
|
3375
|
+
spinner.stop();
|
|
3376
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3377
|
+
process.exitCode = 1;
|
|
3378
|
+
return;
|
|
2800
3379
|
}
|
|
2801
3380
|
|
|
3381
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3382
|
+
|
|
3383
|
+
const result = await fetchActionItems({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
3384
|
+
spinner.stop();
|
|
3385
|
+
printResult(result, opts.json);
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
spinner.stop();
|
|
3388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3389
|
+
console.error(message);
|
|
3390
|
+
process.exitCode = 1;
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
|
|
3394
|
+
issues
|
|
3395
|
+
.command("view-action-item <actionItemIds...>")
|
|
3396
|
+
.description("view action item(s) with all details (supports multiple IDs)")
|
|
3397
|
+
.option("--debug", "enable debug output")
|
|
3398
|
+
.option("--json", "output raw JSON")
|
|
3399
|
+
.action(async (actionItemIds: string[], opts: { debug?: boolean; json?: boolean }) => {
|
|
3400
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action item(s)...");
|
|
3401
|
+
try {
|
|
2802
3402
|
const rootOpts = program.opts<CliOptions>();
|
|
2803
3403
|
const cfg = config.readConfig();
|
|
2804
3404
|
const { apiKey } = getConfig(rootOpts);
|
|
2805
3405
|
if (!apiKey) {
|
|
3406
|
+
spinner.stop();
|
|
2806
3407
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2807
3408
|
process.exitCode = 1;
|
|
2808
3409
|
return;
|
|
@@ -2810,15 +3411,172 @@ issues
|
|
|
2810
3411
|
|
|
2811
3412
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2812
3413
|
|
|
2813
|
-
const result = await
|
|
3414
|
+
const result = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds, debug: !!opts.debug });
|
|
3415
|
+
if (result.length === 0) {
|
|
3416
|
+
spinner.stop();
|
|
3417
|
+
console.error("Action item(s) not found");
|
|
3418
|
+
process.exitCode = 1;
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
spinner.stop();
|
|
3422
|
+
printResult(result, opts.json);
|
|
3423
|
+
} catch (err) {
|
|
3424
|
+
spinner.stop();
|
|
3425
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3426
|
+
console.error(message);
|
|
3427
|
+
process.exitCode = 1;
|
|
3428
|
+
}
|
|
3429
|
+
});
|
|
3430
|
+
|
|
3431
|
+
issues
|
|
3432
|
+
.command("create-action-item <issueId> <title>")
|
|
3433
|
+
.description("create a new action item for an issue")
|
|
3434
|
+
.option("--description <text>", "detailed description (use \\n for newlines)")
|
|
3435
|
+
.option("--sql-action <sql>", "SQL command to execute")
|
|
3436
|
+
.option("--config <json>", "config change as JSON: {\"parameter\":\"...\",\"value\":\"...\"} (repeatable)", (value: string, previous: ConfigChange[]) => {
|
|
3437
|
+
try {
|
|
3438
|
+
previous.push(JSON.parse(value) as ConfigChange);
|
|
3439
|
+
} catch {
|
|
3440
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
3441
|
+
process.exit(1);
|
|
3442
|
+
}
|
|
3443
|
+
return previous;
|
|
3444
|
+
}, [] as ConfigChange[])
|
|
3445
|
+
.option("--debug", "enable debug output")
|
|
3446
|
+
.option("--json", "output raw JSON")
|
|
3447
|
+
.action(async (issueId: string, rawTitle: string, opts: { description?: string; sqlAction?: string; config?: ConfigChange[]; debug?: boolean; json?: boolean }) => {
|
|
3448
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3449
|
+
const cfg = config.readConfig();
|
|
3450
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3451
|
+
if (!apiKey) {
|
|
3452
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3453
|
+
process.exitCode = 1;
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
const title = interpretEscapes(String(rawTitle || "").trim());
|
|
3458
|
+
if (!title) {
|
|
3459
|
+
console.error("title is required");
|
|
3460
|
+
process.exitCode = 1;
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
3465
|
+
const sqlAction = opts.sqlAction;
|
|
3466
|
+
const configs = Array.isArray(opts.config) && opts.config.length > 0 ? opts.config : undefined;
|
|
3467
|
+
|
|
3468
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating action item...");
|
|
3469
|
+
try {
|
|
3470
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3471
|
+
const result = await createActionItem({
|
|
2814
3472
|
apiKey,
|
|
2815
3473
|
apiBaseUrl,
|
|
2816
|
-
|
|
2817
|
-
|
|
3474
|
+
issueId,
|
|
3475
|
+
title,
|
|
3476
|
+
description,
|
|
3477
|
+
sqlAction,
|
|
3478
|
+
configs,
|
|
2818
3479
|
debug: !!opts.debug,
|
|
2819
3480
|
});
|
|
2820
|
-
|
|
3481
|
+
spinner.stop();
|
|
3482
|
+
printResult({ id: result }, opts.json);
|
|
2821
3483
|
} catch (err) {
|
|
3484
|
+
spinner.stop();
|
|
3485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3486
|
+
console.error(message);
|
|
3487
|
+
process.exitCode = 1;
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
issues
|
|
3492
|
+
.command("update-action-item <actionItemId>")
|
|
3493
|
+
.description("update an action item (title, description, status, sql_action, configs)")
|
|
3494
|
+
.option("--title <text>", "new title (use \\n for newlines)")
|
|
3495
|
+
.option("--description <text>", "new description (use \\n for newlines)")
|
|
3496
|
+
.option("--done", "mark as done")
|
|
3497
|
+
.option("--not-done", "mark as not done")
|
|
3498
|
+
.option("--status <value>", "status: waiting_for_approval|approved|rejected")
|
|
3499
|
+
.option("--status-reason <text>", "reason for status change")
|
|
3500
|
+
.option("--sql-action <sql>", "SQL command (use empty string to clear)")
|
|
3501
|
+
.option("--config <json>", "config change as JSON (repeatable, replaces all configs)", (value: string, previous: ConfigChange[]) => {
|
|
3502
|
+
try {
|
|
3503
|
+
previous.push(JSON.parse(value) as ConfigChange);
|
|
3504
|
+
} catch {
|
|
3505
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}
|
|
3508
|
+
return previous;
|
|
3509
|
+
}, [] as ConfigChange[])
|
|
3510
|
+
.option("--clear-configs", "clear all config changes")
|
|
3511
|
+
.option("--debug", "enable debug output")
|
|
3512
|
+
.option("--json", "output raw JSON")
|
|
3513
|
+
.action(async (actionItemId: string, opts: { title?: string; description?: string; done?: boolean; notDone?: boolean; status?: string; statusReason?: string; sqlAction?: string; config?: ConfigChange[]; clearConfigs?: boolean; debug?: boolean; json?: boolean }) => {
|
|
3514
|
+
const rootOpts = program.opts<CliOptions>();
|
|
3515
|
+
const cfg = config.readConfig();
|
|
3516
|
+
const { apiKey } = getConfig(rootOpts);
|
|
3517
|
+
if (!apiKey) {
|
|
3518
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3519
|
+
process.exitCode = 1;
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
|
|
3524
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
3525
|
+
|
|
3526
|
+
let isDone: boolean | undefined = undefined;
|
|
3527
|
+
if (opts.done) isDone = true;
|
|
3528
|
+
else if (opts.notDone) isDone = false;
|
|
3529
|
+
|
|
3530
|
+
let status: string | undefined = undefined;
|
|
3531
|
+
if (opts.status !== undefined) {
|
|
3532
|
+
const validStatuses = ["waiting_for_approval", "approved", "rejected"];
|
|
3533
|
+
if (!validStatuses.includes(opts.status)) {
|
|
3534
|
+
console.error(`status must be one of: ${validStatuses.join(", ")}`);
|
|
3535
|
+
process.exitCode = 1;
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
status = opts.status;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
const statusReason = opts.statusReason;
|
|
3542
|
+
const sqlAction = opts.sqlAction;
|
|
3543
|
+
|
|
3544
|
+
let configs: ConfigChange[] | undefined = undefined;
|
|
3545
|
+
if (opts.clearConfigs) {
|
|
3546
|
+
configs = [];
|
|
3547
|
+
} else if (Array.isArray(opts.config) && opts.config.length > 0) {
|
|
3548
|
+
configs = opts.config;
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// Check that at least one update field is provided
|
|
3552
|
+
if (title === undefined && description === undefined &&
|
|
3553
|
+
isDone === undefined && status === undefined && statusReason === undefined &&
|
|
3554
|
+
sqlAction === undefined && configs === undefined) {
|
|
3555
|
+
console.error("At least one update option is required");
|
|
3556
|
+
process.exitCode = 1;
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating action item...");
|
|
3561
|
+
try {
|
|
3562
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3563
|
+
await updateActionItem({
|
|
3564
|
+
apiKey,
|
|
3565
|
+
apiBaseUrl,
|
|
3566
|
+
actionItemId,
|
|
3567
|
+
title,
|
|
3568
|
+
description,
|
|
3569
|
+
isDone,
|
|
3570
|
+
status,
|
|
3571
|
+
statusReason,
|
|
3572
|
+
sqlAction,
|
|
3573
|
+
configs,
|
|
3574
|
+
debug: !!opts.debug,
|
|
3575
|
+
});
|
|
3576
|
+
spinner.stop();
|
|
3577
|
+
printResult({ success: true }, opts.json);
|
|
3578
|
+
} catch (err) {
|
|
3579
|
+
spinner.stop();
|
|
2822
3580
|
const message = err instanceof Error ? err.message : String(err);
|
|
2823
3581
|
console.error(message);
|
|
2824
3582
|
process.exitCode = 1;
|