postgresai 0.14.0-dev.70 → 0.14.0-dev.72
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/bin/postgres-ai.ts +403 -95
- package/dist/bin/postgres-ai.js +1126 -158
- package/lib/init.ts +76 -19
- package/lib/issues.ts +453 -7
- package/lib/mcp-server.ts +180 -3
- package/lib/metrics-embedded.ts +1 -1
- package/lib/supabase.ts +52 -0
- package/package.json +1 -1
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +78 -70
- package/test/init.test.ts +155 -0
- package/test/issues.cli.test.ts +224 -0
- package/test/mcp-server.test.ts +551 -12
package/bin/postgres-ai.ts
CHANGED
|
@@ -10,10 +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";
|
|
16
|
-
import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, type PgCompatibleError } from "../lib/supabase";
|
|
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";
|
|
17
17
|
import * as pkce from "../lib/pkce";
|
|
18
18
|
import * as authServer from "../lib/auth-server";
|
|
19
19
|
import { maskSecret } from "../lib/util";
|
|
@@ -565,6 +565,7 @@ program
|
|
|
565
565
|
.option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER)
|
|
566
566
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
567
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.")
|
|
568
569
|
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
569
570
|
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
570
571
|
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
@@ -619,6 +620,10 @@ program
|
|
|
619
620
|
"",
|
|
620
621
|
" Generate a token at: https://supabase.com/dashboard/account/tokens",
|
|
621
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)",
|
|
622
627
|
].join("\n")
|
|
623
628
|
)
|
|
624
629
|
.action(async (conn: string | undefined, opts: {
|
|
@@ -631,6 +636,7 @@ program
|
|
|
631
636
|
monitoringUser: string;
|
|
632
637
|
password?: string;
|
|
633
638
|
skipOptionalPermissions?: boolean;
|
|
639
|
+
provider?: string;
|
|
634
640
|
verify?: boolean;
|
|
635
641
|
resetPassword?: boolean;
|
|
636
642
|
printSql?: boolean;
|
|
@@ -681,6 +687,12 @@ program
|
|
|
681
687
|
const shouldPrintSql = !!opts.printSql;
|
|
682
688
|
const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
|
|
683
689
|
|
|
690
|
+
// Validate provider and warn if unknown
|
|
691
|
+
const providerWarning = validateProvider(opts.provider);
|
|
692
|
+
if (providerWarning) {
|
|
693
|
+
console.warn(`⚠ ${providerWarning}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
684
696
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
685
697
|
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
|
|
686
698
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
@@ -698,11 +710,13 @@ program
|
|
|
698
710
|
monitoringUser: opts.monitoringUser,
|
|
699
711
|
monitoringPassword: monPassword,
|
|
700
712
|
includeOptionalPermissions,
|
|
713
|
+
provider: opts.provider,
|
|
701
714
|
});
|
|
702
715
|
|
|
703
716
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
704
717
|
console.log(`-- database: ${database}`);
|
|
705
718
|
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
719
|
+
console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
|
|
706
720
|
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
707
721
|
for (const step of plan.steps) {
|
|
708
722
|
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
@@ -743,6 +757,12 @@ program
|
|
|
743
757
|
|
|
744
758
|
const supabaseClient = new SupabaseClient(supabaseConfig);
|
|
745
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);
|
|
764
|
+
}
|
|
765
|
+
|
|
746
766
|
try {
|
|
747
767
|
// Get current database name
|
|
748
768
|
const database = await supabaseClient.getCurrentDatabase();
|
|
@@ -762,7 +782,7 @@ program
|
|
|
762
782
|
});
|
|
763
783
|
if (v.ok) {
|
|
764
784
|
if (jsonOutput) {
|
|
765
|
-
|
|
785
|
+
const result: Record<string, unknown> = {
|
|
766
786
|
success: true,
|
|
767
787
|
mode: "supabase",
|
|
768
788
|
action: "verify",
|
|
@@ -770,7 +790,11 @@ program
|
|
|
770
790
|
monitoringUser: opts.monitoringUser,
|
|
771
791
|
verified: true,
|
|
772
792
|
missingOptional: v.missingOptional,
|
|
773
|
-
}
|
|
793
|
+
};
|
|
794
|
+
if (databaseUrl) {
|
|
795
|
+
result.databaseUrl = databaseUrl;
|
|
796
|
+
}
|
|
797
|
+
outputJson(result);
|
|
774
798
|
} else {
|
|
775
799
|
console.log("✓ prepare-db verify: OK");
|
|
776
800
|
if (v.missingOptional.length > 0) {
|
|
@@ -781,7 +805,7 @@ program
|
|
|
781
805
|
return;
|
|
782
806
|
}
|
|
783
807
|
if (jsonOutput) {
|
|
784
|
-
|
|
808
|
+
const result: Record<string, unknown> = {
|
|
785
809
|
success: false,
|
|
786
810
|
mode: "supabase",
|
|
787
811
|
action: "verify",
|
|
@@ -790,7 +814,11 @@ program
|
|
|
790
814
|
verified: false,
|
|
791
815
|
missingRequired: v.missingRequired,
|
|
792
816
|
missingOptional: v.missingOptional,
|
|
793
|
-
}
|
|
817
|
+
};
|
|
818
|
+
if (databaseUrl) {
|
|
819
|
+
result.databaseUrl = databaseUrl;
|
|
820
|
+
}
|
|
821
|
+
outputJson(result);
|
|
794
822
|
} else {
|
|
795
823
|
console.error("✗ prepare-db verify failed: missing required items");
|
|
796
824
|
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
@@ -895,6 +923,9 @@ program
|
|
|
895
923
|
if (passwordGenerated) {
|
|
896
924
|
result.generatedPassword = monPassword;
|
|
897
925
|
}
|
|
926
|
+
if (databaseUrl) {
|
|
927
|
+
result.databaseUrl = databaseUrl;
|
|
928
|
+
}
|
|
898
929
|
outputJson(result);
|
|
899
930
|
} else {
|
|
900
931
|
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
@@ -1059,6 +1090,7 @@ program
|
|
|
1059
1090
|
database,
|
|
1060
1091
|
monitoringUser: opts.monitoringUser,
|
|
1061
1092
|
includeOptionalPermissions,
|
|
1093
|
+
provider: opts.provider,
|
|
1062
1094
|
});
|
|
1063
1095
|
if (v.ok) {
|
|
1064
1096
|
if (jsonOutput) {
|
|
@@ -1068,11 +1100,12 @@ program
|
|
|
1068
1100
|
action: "verify",
|
|
1069
1101
|
database,
|
|
1070
1102
|
monitoringUser: opts.monitoringUser,
|
|
1103
|
+
provider: opts.provider,
|
|
1071
1104
|
verified: true,
|
|
1072
1105
|
missingOptional: v.missingOptional,
|
|
1073
1106
|
});
|
|
1074
1107
|
} else {
|
|
1075
|
-
console.log(
|
|
1108
|
+
console.log(`✓ prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
1076
1109
|
if (v.missingOptional.length > 0) {
|
|
1077
1110
|
console.log("⚠ Optional items missing:");
|
|
1078
1111
|
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
@@ -1154,12 +1187,21 @@ program
|
|
|
1154
1187
|
monitoringUser: opts.monitoringUser,
|
|
1155
1188
|
monitoringPassword: monPassword,
|
|
1156
1189
|
includeOptionalPermissions,
|
|
1190
|
+
provider: opts.provider,
|
|
1157
1191
|
});
|
|
1158
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.
|
|
1159
1195
|
const effectivePlan = opts.resetPassword
|
|
1160
1196
|
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
1161
1197
|
: plan;
|
|
1162
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
|
+
|
|
1163
1205
|
if (shouldPrintSql) {
|
|
1164
1206
|
console.log("\n--- SQL plan ---");
|
|
1165
1207
|
for (const step of effectivePlan.steps) {
|
|
@@ -2974,22 +3016,44 @@ const issues = program.command("issues").description("issues management");
|
|
|
2974
3016
|
issues
|
|
2975
3017
|
.command("list")
|
|
2976
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)
|
|
2977
3022
|
.option("--debug", "enable debug output")
|
|
2978
3023
|
.option("--json", "output raw JSON")
|
|
2979
|
-
.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...");
|
|
2980
3026
|
try {
|
|
2981
3027
|
const rootOpts = program.opts<CliOptions>();
|
|
2982
3028
|
const cfg = config.readConfig();
|
|
2983
3029
|
const { apiKey } = getConfig(rootOpts);
|
|
2984
3030
|
if (!apiKey) {
|
|
3031
|
+
spinner.stop();
|
|
2985
3032
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2986
3033
|
process.exitCode = 1;
|
|
2987
3034
|
return;
|
|
2988
3035
|
}
|
|
3036
|
+
const orgId = cfg.orgId ?? undefined;
|
|
2989
3037
|
|
|
2990
3038
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2991
3039
|
|
|
2992
|
-
|
|
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();
|
|
2993
3057
|
const trimmed = Array.isArray(result)
|
|
2994
3058
|
? (result as any[]).map((r) => ({
|
|
2995
3059
|
id: (r as any).id,
|
|
@@ -3000,6 +3064,7 @@ issues
|
|
|
3000
3064
|
: result;
|
|
3001
3065
|
printResult(trimmed, opts.json);
|
|
3002
3066
|
} catch (err) {
|
|
3067
|
+
spinner.stop();
|
|
3003
3068
|
const message = err instanceof Error ? err.message : String(err);
|
|
3004
3069
|
console.error(message);
|
|
3005
3070
|
process.exitCode = 1;
|
|
@@ -3012,11 +3077,13 @@ issues
|
|
|
3012
3077
|
.option("--debug", "enable debug output")
|
|
3013
3078
|
.option("--json", "output raw JSON")
|
|
3014
3079
|
.action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3080
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
|
|
3015
3081
|
try {
|
|
3016
3082
|
const rootOpts = program.opts<CliOptions>();
|
|
3017
3083
|
const cfg = config.readConfig();
|
|
3018
3084
|
const { apiKey } = getConfig(rootOpts);
|
|
3019
3085
|
if (!apiKey) {
|
|
3086
|
+
spinner.stop();
|
|
3020
3087
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3021
3088
|
process.exitCode = 1;
|
|
3022
3089
|
return;
|
|
@@ -3026,15 +3093,19 @@ issues
|
|
|
3026
3093
|
|
|
3027
3094
|
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
3028
3095
|
if (!issue) {
|
|
3096
|
+
spinner.stop();
|
|
3029
3097
|
console.error("Issue not found");
|
|
3030
3098
|
process.exitCode = 1;
|
|
3031
3099
|
return;
|
|
3032
3100
|
}
|
|
3033
3101
|
|
|
3102
|
+
spinner.update("Fetching comments...");
|
|
3034
3103
|
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
3104
|
+
spinner.stop();
|
|
3035
3105
|
const combined = { issue, comments };
|
|
3036
3106
|
printResult(combined, opts.json);
|
|
3037
3107
|
} catch (err) {
|
|
3108
|
+
spinner.stop();
|
|
3038
3109
|
const message = err instanceof Error ? err.message : String(err);
|
|
3039
3110
|
console.error(message);
|
|
3040
3111
|
process.exitCode = 1;
|
|
@@ -3048,22 +3119,24 @@ issues
|
|
|
3048
3119
|
.option("--debug", "enable debug output")
|
|
3049
3120
|
.option("--json", "output raw JSON")
|
|
3050
3121
|
.action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
}
|
|
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
|
+
}
|
|
3062
3132
|
|
|
3133
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
3134
|
+
try {
|
|
3063
3135
|
const rootOpts = program.opts<CliOptions>();
|
|
3064
3136
|
const cfg = config.readConfig();
|
|
3065
3137
|
const { apiKey } = getConfig(rootOpts);
|
|
3066
3138
|
if (!apiKey) {
|
|
3139
|
+
spinner.stop();
|
|
3067
3140
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3068
3141
|
process.exitCode = 1;
|
|
3069
3142
|
return;
|
|
@@ -3079,8 +3152,10 @@ issues
|
|
|
3079
3152
|
parentCommentId: opts.parent,
|
|
3080
3153
|
debug: !!opts.debug,
|
|
3081
3154
|
});
|
|
3155
|
+
spinner.stop();
|
|
3082
3156
|
printResult(result, opts.json);
|
|
3083
3157
|
} catch (err) {
|
|
3158
|
+
spinner.stop();
|
|
3084
3159
|
const message = err instanceof Error ? err.message : String(err);
|
|
3085
3160
|
console.error(message);
|
|
3086
3161
|
process.exitCode = 1;
|
|
@@ -3092,7 +3167,7 @@ issues
|
|
|
3092
3167
|
.description("create a new issue")
|
|
3093
3168
|
.option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10))
|
|
3094
3169
|
.option("--project-id <id>", "project id", (v) => parseInt(v, 10))
|
|
3095
|
-
.option("--description <text>", "issue description (
|
|
3170
|
+
.option("--description <text>", "issue description (use \\n for newlines)")
|
|
3096
3171
|
.option(
|
|
3097
3172
|
"--label <label>",
|
|
3098
3173
|
"issue label (repeatable)",
|
|
@@ -3105,34 +3180,35 @@ issues
|
|
|
3105
3180
|
.option("--debug", "enable debug output")
|
|
3106
3181
|
.option("--json", "output raw JSON")
|
|
3107
3182
|
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
}
|
|
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
|
+
}
|
|
3117
3191
|
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3192
|
+
const title = interpretEscapes(String(rawTitle || "").trim());
|
|
3193
|
+
if (!title) {
|
|
3194
|
+
console.error("title is required");
|
|
3195
|
+
process.exitCode = 1;
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3124
3198
|
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
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
|
+
}
|
|
3131
3205
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
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;
|
|
3135
3209
|
|
|
3210
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
|
|
3211
|
+
try {
|
|
3136
3212
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3137
3213
|
const result = await createIssue({
|
|
3138
3214
|
apiKey,
|
|
@@ -3144,8 +3220,10 @@ issues
|
|
|
3144
3220
|
labels,
|
|
3145
3221
|
debug: !!opts.debug,
|
|
3146
3222
|
});
|
|
3223
|
+
spinner.stop();
|
|
3147
3224
|
printResult(result, opts.json);
|
|
3148
3225
|
} catch (err) {
|
|
3226
|
+
spinner.stop();
|
|
3149
3227
|
const message = err instanceof Error ? err.message : String(err);
|
|
3150
3228
|
console.error(message);
|
|
3151
3229
|
process.exitCode = 1;
|
|
@@ -3155,8 +3233,8 @@ issues
|
|
|
3155
3233
|
issues
|
|
3156
3234
|
.command("update <issueId>")
|
|
3157
3235
|
.description("update an existing issue (title/description/status/labels)")
|
|
3158
|
-
.option("--title <text>", "new title (
|
|
3159
|
-
.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)")
|
|
3160
3238
|
.option("--status <value>", "status: open|closed|0|1")
|
|
3161
3239
|
.option(
|
|
3162
3240
|
"--label <label>",
|
|
@@ -3171,49 +3249,50 @@ issues
|
|
|
3171
3249
|
.option("--debug", "enable debug output")
|
|
3172
3250
|
.option("--json", "output raw JSON")
|
|
3173
3251
|
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
}
|
|
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
|
+
}
|
|
3183
3260
|
|
|
3184
|
-
|
|
3261
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3185
3262
|
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
process.exitCode = 1;
|
|
3199
|
-
return;
|
|
3200
|
-
}
|
|
3201
|
-
status = n;
|
|
3202
|
-
}
|
|
3203
|
-
if (status !== 0 && status !== 1) {
|
|
3204
|
-
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");
|
|
3205
3275
|
process.exitCode = 1;
|
|
3206
3276
|
return;
|
|
3207
3277
|
}
|
|
3278
|
+
status = n;
|
|
3208
3279
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
3214
|
-
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;
|
|
3215
3284
|
}
|
|
3285
|
+
}
|
|
3216
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
|
+
}
|
|
3293
|
+
|
|
3294
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
|
|
3295
|
+
try {
|
|
3217
3296
|
const result = await updateIssue({
|
|
3218
3297
|
apiKey,
|
|
3219
3298
|
apiBaseUrl,
|
|
@@ -3224,8 +3303,10 @@ issues
|
|
|
3224
3303
|
labels,
|
|
3225
3304
|
debug: !!opts.debug,
|
|
3226
3305
|
});
|
|
3306
|
+
spinner.stop();
|
|
3227
3307
|
printResult(result, opts.json);
|
|
3228
3308
|
} catch (err) {
|
|
3309
|
+
spinner.stop();
|
|
3229
3310
|
const message = err instanceof Error ? err.message : String(err);
|
|
3230
3311
|
console.error(message);
|
|
3231
3312
|
process.exitCode = 1;
|
|
@@ -3238,21 +3319,91 @@ issues
|
|
|
3238
3319
|
.option("--debug", "enable debug output")
|
|
3239
3320
|
.option("--json", "output raw JSON")
|
|
3240
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...");
|
|
3241
3342
|
try {
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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;
|
|
3250
3379
|
}
|
|
3251
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 {
|
|
3252
3402
|
const rootOpts = program.opts<CliOptions>();
|
|
3253
3403
|
const cfg = config.readConfig();
|
|
3254
3404
|
const { apiKey } = getConfig(rootOpts);
|
|
3255
3405
|
if (!apiKey) {
|
|
3406
|
+
spinner.stop();
|
|
3256
3407
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
3257
3408
|
process.exitCode = 1;
|
|
3258
3409
|
return;
|
|
@@ -3260,15 +3411,172 @@ issues
|
|
|
3260
3411
|
|
|
3261
3412
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3262
3413
|
|
|
3263
|
-
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({
|
|
3264
3472
|
apiKey,
|
|
3265
3473
|
apiBaseUrl,
|
|
3266
|
-
|
|
3267
|
-
|
|
3474
|
+
issueId,
|
|
3475
|
+
title,
|
|
3476
|
+
description,
|
|
3477
|
+
sqlAction,
|
|
3478
|
+
configs,
|
|
3268
3479
|
debug: !!opts.debug,
|
|
3269
3480
|
});
|
|
3270
|
-
|
|
3481
|
+
spinner.stop();
|
|
3482
|
+
printResult({ id: result }, opts.json);
|
|
3271
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();
|
|
3272
3580
|
const message = err instanceof Error ? err.message : String(err);
|
|
3273
3581
|
console.error(message);
|
|
3274
3582
|
process.exitCode = 1;
|