postgresai 0.14.0-dev.71 → 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
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, type PgCompatibleError } from "../lib/supabase";
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";
@@ -757,6 +757,12 @@ program
757
757
 
758
758
  const supabaseClient = new SupabaseClient(supabaseConfig);
759
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
+
760
766
  try {
761
767
  // Get current database name
762
768
  const database = await supabaseClient.getCurrentDatabase();
@@ -776,7 +782,7 @@ program
776
782
  });
777
783
  if (v.ok) {
778
784
  if (jsonOutput) {
779
- outputJson({
785
+ const result: Record<string, unknown> = {
780
786
  success: true,
781
787
  mode: "supabase",
782
788
  action: "verify",
@@ -784,7 +790,11 @@ program
784
790
  monitoringUser: opts.monitoringUser,
785
791
  verified: true,
786
792
  missingOptional: v.missingOptional,
787
- });
793
+ };
794
+ if (databaseUrl) {
795
+ result.databaseUrl = databaseUrl;
796
+ }
797
+ outputJson(result);
788
798
  } else {
789
799
  console.log("✓ prepare-db verify: OK");
790
800
  if (v.missingOptional.length > 0) {
@@ -795,7 +805,7 @@ program
795
805
  return;
796
806
  }
797
807
  if (jsonOutput) {
798
- outputJson({
808
+ const result: Record<string, unknown> = {
799
809
  success: false,
800
810
  mode: "supabase",
801
811
  action: "verify",
@@ -804,7 +814,11 @@ program
804
814
  verified: false,
805
815
  missingRequired: v.missingRequired,
806
816
  missingOptional: v.missingOptional,
807
- });
817
+ };
818
+ if (databaseUrl) {
819
+ result.databaseUrl = databaseUrl;
820
+ }
821
+ outputJson(result);
808
822
  } else {
809
823
  console.error("✗ prepare-db verify failed: missing required items");
810
824
  for (const m of v.missingRequired) console.error(`- ${m}`);
@@ -909,6 +923,9 @@ program
909
923
  if (passwordGenerated) {
910
924
  result.generatedPassword = monPassword;
911
925
  }
926
+ if (databaseUrl) {
927
+ result.databaseUrl = databaseUrl;
928
+ }
912
929
  outputJson(result);
913
930
  } else {
914
931
  console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
@@ -2999,22 +3016,44 @@ const issues = program.command("issues").description("issues management");
2999
3016
  issues
3000
3017
  .command("list")
3001
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)
3002
3022
  .option("--debug", "enable debug output")
3003
3023
  .option("--json", "output raw JSON")
3004
- .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...");
3005
3026
  try {
3006
3027
  const rootOpts = program.opts<CliOptions>();
3007
3028
  const cfg = config.readConfig();
3008
3029
  const { apiKey } = getConfig(rootOpts);
3009
3030
  if (!apiKey) {
3031
+ spinner.stop();
3010
3032
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3011
3033
  process.exitCode = 1;
3012
3034
  return;
3013
3035
  }
3036
+ const orgId = cfg.orgId ?? undefined;
3014
3037
 
3015
3038
  const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3016
3039
 
3017
- 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();
3018
3057
  const trimmed = Array.isArray(result)
3019
3058
  ? (result as any[]).map((r) => ({
3020
3059
  id: (r as any).id,
@@ -3025,6 +3064,7 @@ issues
3025
3064
  : result;
3026
3065
  printResult(trimmed, opts.json);
3027
3066
  } catch (err) {
3067
+ spinner.stop();
3028
3068
  const message = err instanceof Error ? err.message : String(err);
3029
3069
  console.error(message);
3030
3070
  process.exitCode = 1;
@@ -3037,11 +3077,13 @@ issues
3037
3077
  .option("--debug", "enable debug output")
3038
3078
  .option("--json", "output raw JSON")
3039
3079
  .action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
3080
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
3040
3081
  try {
3041
3082
  const rootOpts = program.opts<CliOptions>();
3042
3083
  const cfg = config.readConfig();
3043
3084
  const { apiKey } = getConfig(rootOpts);
3044
3085
  if (!apiKey) {
3086
+ spinner.stop();
3045
3087
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3046
3088
  process.exitCode = 1;
3047
3089
  return;
@@ -3051,15 +3093,19 @@ issues
3051
3093
 
3052
3094
  const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
3053
3095
  if (!issue) {
3096
+ spinner.stop();
3054
3097
  console.error("Issue not found");
3055
3098
  process.exitCode = 1;
3056
3099
  return;
3057
3100
  }
3058
3101
 
3102
+ spinner.update("Fetching comments...");
3059
3103
  const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
3104
+ spinner.stop();
3060
3105
  const combined = { issue, comments };
3061
3106
  printResult(combined, opts.json);
3062
3107
  } catch (err) {
3108
+ spinner.stop();
3063
3109
  const message = err instanceof Error ? err.message : String(err);
3064
3110
  console.error(message);
3065
3111
  process.exitCode = 1;
@@ -3073,22 +3119,24 @@ issues
3073
3119
  .option("--debug", "enable debug output")
3074
3120
  .option("--json", "output raw JSON")
3075
3121
  .action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
3076
- try {
3077
- // Interpret escape sequences in content (e.g., \n -> newline)
3078
- if (opts.debug) {
3079
- // eslint-disable-next-line no-console
3080
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3081
- }
3082
- content = interpretEscapes(content);
3083
- if (opts.debug) {
3084
- // eslint-disable-next-line no-console
3085
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
3086
- }
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
+ }
3087
3132
 
3133
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
3134
+ try {
3088
3135
  const rootOpts = program.opts<CliOptions>();
3089
3136
  const cfg = config.readConfig();
3090
3137
  const { apiKey } = getConfig(rootOpts);
3091
3138
  if (!apiKey) {
3139
+ spinner.stop();
3092
3140
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3093
3141
  process.exitCode = 1;
3094
3142
  return;
@@ -3104,8 +3152,10 @@ issues
3104
3152
  parentCommentId: opts.parent,
3105
3153
  debug: !!opts.debug,
3106
3154
  });
3155
+ spinner.stop();
3107
3156
  printResult(result, opts.json);
3108
3157
  } catch (err) {
3158
+ spinner.stop();
3109
3159
  const message = err instanceof Error ? err.message : String(err);
3110
3160
  console.error(message);
3111
3161
  process.exitCode = 1;
@@ -3117,7 +3167,7 @@ issues
3117
3167
  .description("create a new issue")
3118
3168
  .option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10))
3119
3169
  .option("--project-id <id>", "project id", (v) => parseInt(v, 10))
3120
- .option("--description <text>", "issue description (supports \\\\n)")
3170
+ .option("--description <text>", "issue description (use \\n for newlines)")
3121
3171
  .option(
3122
3172
  "--label <label>",
3123
3173
  "issue label (repeatable)",
@@ -3130,34 +3180,35 @@ issues
3130
3180
  .option("--debug", "enable debug output")
3131
3181
  .option("--json", "output raw JSON")
3132
3182
  .action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
3133
- try {
3134
- const rootOpts = program.opts<CliOptions>();
3135
- const cfg = config.readConfig();
3136
- const { apiKey } = getConfig(rootOpts);
3137
- if (!apiKey) {
3138
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3139
- process.exitCode = 1;
3140
- return;
3141
- }
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
+ }
3142
3191
 
3143
- const title = interpretEscapes(String(rawTitle || "").trim());
3144
- if (!title) {
3145
- console.error("title is required");
3146
- process.exitCode = 1;
3147
- return;
3148
- }
3192
+ const title = interpretEscapes(String(rawTitle || "").trim());
3193
+ if (!title) {
3194
+ console.error("title is required");
3195
+ process.exitCode = 1;
3196
+ return;
3197
+ }
3149
3198
 
3150
- const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
3151
- if (typeof orgId !== "number") {
3152
- console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
3153
- process.exitCode = 1;
3154
- return;
3155
- }
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
+ }
3156
3205
 
3157
- const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
3158
- const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
3159
- 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;
3160
3209
 
3210
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
3211
+ try {
3161
3212
  const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3162
3213
  const result = await createIssue({
3163
3214
  apiKey,
@@ -3169,8 +3220,10 @@ issues
3169
3220
  labels,
3170
3221
  debug: !!opts.debug,
3171
3222
  });
3223
+ spinner.stop();
3172
3224
  printResult(result, opts.json);
3173
3225
  } catch (err) {
3226
+ spinner.stop();
3174
3227
  const message = err instanceof Error ? err.message : String(err);
3175
3228
  console.error(message);
3176
3229
  process.exitCode = 1;
@@ -3180,8 +3233,8 @@ issues
3180
3233
  issues
3181
3234
  .command("update <issueId>")
3182
3235
  .description("update an existing issue (title/description/status/labels)")
3183
- .option("--title <text>", "new title (supports \\\\n)")
3184
- .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)")
3185
3238
  .option("--status <value>", "status: open|closed|0|1")
3186
3239
  .option(
3187
3240
  "--label <label>",
@@ -3196,49 +3249,50 @@ issues
3196
3249
  .option("--debug", "enable debug output")
3197
3250
  .option("--json", "output raw JSON")
3198
3251
  .action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
3199
- try {
3200
- const rootOpts = program.opts<CliOptions>();
3201
- const cfg = config.readConfig();
3202
- const { apiKey } = getConfig(rootOpts);
3203
- if (!apiKey) {
3204
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3205
- process.exitCode = 1;
3206
- return;
3207
- }
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
+ }
3208
3260
 
3209
- const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3261
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3210
3262
 
3211
- const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
3212
- const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
3213
-
3214
- let status: number | undefined = undefined;
3215
- if (opts.status !== undefined) {
3216
- const raw = String(opts.status).trim().toLowerCase();
3217
- if (raw === "open") status = 0;
3218
- else if (raw === "closed") status = 1;
3219
- else {
3220
- const n = Number(raw);
3221
- if (!Number.isFinite(n)) {
3222
- console.error("status must be open|closed|0|1");
3223
- process.exitCode = 1;
3224
- return;
3225
- }
3226
- status = n;
3227
- }
3228
- if (status !== 0 && status !== 1) {
3229
- 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");
3230
3275
  process.exitCode = 1;
3231
3276
  return;
3232
3277
  }
3278
+ status = n;
3233
3279
  }
3234
-
3235
- let labels: string[] | undefined = undefined;
3236
- if (opts.clearLabels) {
3237
- labels = [];
3238
- } else if (Array.isArray(opts.label) && opts.label.length > 0) {
3239
- 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;
3240
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
+ }
3241
3293
 
3294
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
3295
+ try {
3242
3296
  const result = await updateIssue({
3243
3297
  apiKey,
3244
3298
  apiBaseUrl,
@@ -3249,8 +3303,10 @@ issues
3249
3303
  labels,
3250
3304
  debug: !!opts.debug,
3251
3305
  });
3306
+ spinner.stop();
3252
3307
  printResult(result, opts.json);
3253
3308
  } catch (err) {
3309
+ spinner.stop();
3254
3310
  const message = err instanceof Error ? err.message : String(err);
3255
3311
  console.error(message);
3256
3312
  process.exitCode = 1;
@@ -3263,21 +3319,91 @@ issues
3263
3319
  .option("--debug", "enable debug output")
3264
3320
  .option("--json", "output raw JSON")
3265
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...");
3266
3342
  try {
3267
- if (opts.debug) {
3268
- // eslint-disable-next-line no-console
3269
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3270
- }
3271
- content = interpretEscapes(content);
3272
- if (opts.debug) {
3273
- // eslint-disable-next-line no-console
3274
- 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;
3275
3379
  }
3276
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 {
3277
3402
  const rootOpts = program.opts<CliOptions>();
3278
3403
  const cfg = config.readConfig();
3279
3404
  const { apiKey } = getConfig(rootOpts);
3280
3405
  if (!apiKey) {
3406
+ spinner.stop();
3281
3407
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
3282
3408
  process.exitCode = 1;
3283
3409
  return;
@@ -3285,15 +3411,172 @@ issues
3285
3411
 
3286
3412
  const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3287
3413
 
3288
- 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({
3289
3472
  apiKey,
3290
3473
  apiBaseUrl,
3291
- commentId,
3292
- content,
3474
+ issueId,
3475
+ title,
3476
+ description,
3477
+ sqlAction,
3478
+ configs,
3293
3479
  debug: !!opts.debug,
3294
3480
  });
3295
- printResult(result, opts.json);
3481
+ spinner.stop();
3482
+ printResult({ id: result }, opts.json);
3296
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();
3297
3580
  const message = err instanceof Error ? err.message : String(err);
3298
3581
  console.error(message);
3299
3582
  process.exitCode = 1;