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.
@@ -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
- outputJson({
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
- outputJson({
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("✓ prepare-db verify: OK");
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
- const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
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
- try {
3052
- // Interpret escape sequences in content (e.g., \n -> newline)
3053
- if (opts.debug) {
3054
- // eslint-disable-next-line no-console
3055
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3056
- }
3057
- content = interpretEscapes(content);
3058
- if (opts.debug) {
3059
- // eslint-disable-next-line no-console
3060
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
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 (supports \\\\n)")
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
- try {
3109
- const rootOpts = program.opts<CliOptions>();
3110
- const cfg = config.readConfig();
3111
- const { apiKey } = getConfig(rootOpts);
3112
- if (!apiKey) {
3113
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3114
- process.exitCode = 1;
3115
- return;
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
- const title = interpretEscapes(String(rawTitle || "").trim());
3119
- if (!title) {
3120
- console.error("title is required");
3121
- process.exitCode = 1;
3122
- return;
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
- const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
3126
- if (typeof orgId !== "number") {
3127
- console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
3128
- process.exitCode = 1;
3129
- return;
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
- const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
3133
- const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
3134
- const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
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 (supports \\\\n)")
3159
- .option("--description <text>", "new description (supports \\\\n)")
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
- try {
3175
- const rootOpts = program.opts<CliOptions>();
3176
- const cfg = config.readConfig();
3177
- const { apiKey } = getConfig(rootOpts);
3178
- if (!apiKey) {
3179
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3180
- process.exitCode = 1;
3181
- return;
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
- const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3261
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3185
3262
 
3186
- const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
3187
- const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
3188
-
3189
- let status: number | undefined = undefined;
3190
- if (opts.status !== undefined) {
3191
- const raw = String(opts.status).trim().toLowerCase();
3192
- if (raw === "open") status = 0;
3193
- else if (raw === "closed") status = 1;
3194
- else {
3195
- const n = Number(raw);
3196
- if (!Number.isFinite(n)) {
3197
- console.error("status must be open|closed|0|1");
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
- let labels: string[] | undefined = undefined;
3211
- if (opts.clearLabels) {
3212
- labels = [];
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
- if (opts.debug) {
3243
- // eslint-disable-next-line no-console
3244
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3245
- }
3246
- content = interpretEscapes(content);
3247
- if (opts.debug) {
3248
- // eslint-disable-next-line no-console
3249
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
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 updateIssueComment({
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
- commentId,
3267
- content,
3474
+ issueId,
3475
+ title,
3476
+ description,
3477
+ sqlAction,
3478
+ configs,
3268
3479
  debug: !!opts.debug,
3269
3480
  });
3270
- printResult(result, opts.json);
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;