postgresai 0.15.0-dev.3 → 0.15.0-dev.4
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 +154 -40
- package/dist/bin/postgres-ai.js +727 -384
- package/lib/checkup.ts +16 -10
- package/lib/config.ts +3 -0
- package/lib/init.ts +1 -1
- package/lib/issues.ts +72 -72
- package/lib/reports.ts +12 -12
- package/lib/storage.ts +291 -0
- package/lib/util.ts +7 -1
- package/package.json +1 -1
- package/test/init.test.ts +1 -1
- package/test/issues.cli.test.ts +230 -1
- package/test/mcp-server.test.ts +69 -0
- package/test/reports.test.ts +3 -3
- package/test/storage.test.ts +761 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { startMcpServer } from "../lib/mcp-server";
|
|
|
13
13
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
|
|
14
14
|
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
|
|
15
15
|
import { resolveBaseUrls } from "../lib/util";
|
|
16
|
+
import { uploadFile, downloadFile, buildMarkdownLink } from "../lib/storage";
|
|
16
17
|
import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
|
|
17
18
|
import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
|
|
18
19
|
import * as pkce from "../lib/pkce";
|
|
@@ -412,6 +413,7 @@ interface CliOptions {
|
|
|
412
413
|
apiKey?: string;
|
|
413
414
|
apiBaseUrl?: string;
|
|
414
415
|
uiBaseUrl?: string;
|
|
416
|
+
storageBaseUrl?: string;
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
/**
|
|
@@ -580,6 +582,10 @@ program
|
|
|
580
582
|
.option(
|
|
581
583
|
"--ui-base-url <url>",
|
|
582
584
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
585
|
+
)
|
|
586
|
+
.option(
|
|
587
|
+
"--storage-base-url <url>",
|
|
588
|
+
"Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)"
|
|
583
589
|
);
|
|
584
590
|
|
|
585
591
|
program
|
|
@@ -596,6 +602,27 @@ program
|
|
|
596
602
|
console.log(`Default project saved: ${value}`);
|
|
597
603
|
});
|
|
598
604
|
|
|
605
|
+
program
|
|
606
|
+
.command("set-storage-url <url>")
|
|
607
|
+
.description("store storage base URL for file uploads")
|
|
608
|
+
.action(async (url: string) => {
|
|
609
|
+
const value = (url || "").trim();
|
|
610
|
+
if (!value) {
|
|
611
|
+
console.error("Error: url is required");
|
|
612
|
+
process.exitCode = 1;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const { normalizeBaseUrl } = await import("../lib/util");
|
|
617
|
+
const normalized = normalizeBaseUrl(value);
|
|
618
|
+
config.writeConfig({ storageBaseUrl: normalized });
|
|
619
|
+
console.log(`Storage URL saved: ${normalized}`);
|
|
620
|
+
} catch {
|
|
621
|
+
console.error(`Error: invalid URL: ${value}`);
|
|
622
|
+
process.exitCode = 1;
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
599
626
|
program
|
|
600
627
|
.command("prepare-db [conn]")
|
|
601
628
|
.description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
|
|
@@ -841,8 +868,8 @@ program
|
|
|
841
868
|
} else {
|
|
842
869
|
console.log("✓ prepare-db verify: OK");
|
|
843
870
|
if (v.missingOptional.length > 0) {
|
|
844
|
-
console.
|
|
845
|
-
for (const m of v.missingOptional) console.
|
|
871
|
+
console.error("⚠ Optional items missing:");
|
|
872
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
846
873
|
}
|
|
847
874
|
}
|
|
848
875
|
return;
|
|
@@ -973,8 +1000,8 @@ program
|
|
|
973
1000
|
} else {
|
|
974
1001
|
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
975
1002
|
if (skippedOptional.length > 0) {
|
|
976
|
-
console.
|
|
977
|
-
for (const s of skippedOptional) console.
|
|
1003
|
+
console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
1004
|
+
for (const s of skippedOptional) console.error(`- ${s}`);
|
|
978
1005
|
}
|
|
979
1006
|
if (process.stdout.isTTY) {
|
|
980
1007
|
console.log(`Applied ${applied.length} steps`);
|
|
@@ -1155,8 +1182,8 @@ program
|
|
|
1155
1182
|
} else {
|
|
1156
1183
|
console.log(`✓ prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
1157
1184
|
if (v.missingOptional.length > 0) {
|
|
1158
|
-
console.
|
|
1159
|
-
for (const m of v.missingOptional) console.
|
|
1185
|
+
console.error("⚠ Optional items missing:");
|
|
1186
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
1160
1187
|
}
|
|
1161
1188
|
}
|
|
1162
1189
|
return;
|
|
@@ -1283,8 +1310,8 @@ program
|
|
|
1283
1310
|
} else {
|
|
1284
1311
|
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
1285
1312
|
if (skippedOptional.length > 0) {
|
|
1286
|
-
console.
|
|
1287
|
-
for (const s of skippedOptional) console.
|
|
1313
|
+
console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
1314
|
+
for (const s of skippedOptional) console.error(`- ${s}`);
|
|
1288
1315
|
}
|
|
1289
1316
|
// Keep output compact but still useful
|
|
1290
1317
|
if (process.stdout.isTTY) {
|
|
@@ -1600,11 +1627,11 @@ program
|
|
|
1600
1627
|
console.log("✓ unprepare-db completed");
|
|
1601
1628
|
console.log(`Applied ${applied.length} steps`);
|
|
1602
1629
|
} else {
|
|
1603
|
-
console.
|
|
1630
|
+
console.error("⚠ unprepare-db completed with errors");
|
|
1604
1631
|
console.log(`Applied ${applied.length} steps`);
|
|
1605
|
-
console.
|
|
1632
|
+
console.error("Errors:");
|
|
1606
1633
|
for (const err of errors) {
|
|
1607
|
-
console.
|
|
1634
|
+
console.error(` - ${err}`);
|
|
1608
1635
|
}
|
|
1609
1636
|
process.exitCode = 1;
|
|
1610
1637
|
}
|
|
@@ -2089,9 +2116,9 @@ function registerMonitoringInstance(
|
|
|
2089
2116
|
const debug = opts?.debug;
|
|
2090
2117
|
|
|
2091
2118
|
if (debug) {
|
|
2092
|
-
console.
|
|
2093
|
-
console.
|
|
2094
|
-
console.
|
|
2119
|
+
console.error(`\nDebug: Registering monitoring instance...`);
|
|
2120
|
+
console.error(`Debug: POST ${url}`);
|
|
2121
|
+
console.error(`Debug: project_name=${projectName}`);
|
|
2095
2122
|
}
|
|
2096
2123
|
|
|
2097
2124
|
// Fire and forget - don't block the main flow
|
|
@@ -2109,18 +2136,18 @@ function registerMonitoringInstance(
|
|
|
2109
2136
|
const body = await res.text().catch(() => "");
|
|
2110
2137
|
if (!res.ok) {
|
|
2111
2138
|
if (debug) {
|
|
2112
|
-
console.
|
|
2113
|
-
console.
|
|
2139
|
+
console.error(`Debug: Monitoring registration failed: HTTP ${res.status}`);
|
|
2140
|
+
console.error(`Debug: Response: ${body}`);
|
|
2114
2141
|
}
|
|
2115
2142
|
return;
|
|
2116
2143
|
}
|
|
2117
2144
|
if (debug) {
|
|
2118
|
-
console.
|
|
2145
|
+
console.error(`Debug: Monitoring registration response: ${body}`);
|
|
2119
2146
|
}
|
|
2120
2147
|
})
|
|
2121
2148
|
.catch((err) => {
|
|
2122
2149
|
if (debug) {
|
|
2123
|
-
console.
|
|
2150
|
+
console.error(`Debug: Monitoring registration error: ${err.message}`);
|
|
2124
2151
|
}
|
|
2125
2152
|
});
|
|
2126
2153
|
}
|
|
@@ -2300,8 +2327,8 @@ mon
|
|
|
2300
2327
|
|
|
2301
2328
|
// Validate conflicting options
|
|
2302
2329
|
if (opts.demo && opts.dbUrl) {
|
|
2303
|
-
console.
|
|
2304
|
-
console.
|
|
2330
|
+
console.error("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
|
|
2331
|
+
console.error("⚠ The --db-url will be ignored in demo mode.\n");
|
|
2305
2332
|
opts.dbUrl = undefined;
|
|
2306
2333
|
}
|
|
2307
2334
|
|
|
@@ -2317,7 +2344,7 @@ mon
|
|
|
2317
2344
|
// Check if containers are already running
|
|
2318
2345
|
const { running, containers } = checkRunningContainers();
|
|
2319
2346
|
if (running) {
|
|
2320
|
-
console.
|
|
2347
|
+
console.error(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
|
|
2321
2348
|
console.log("Use 'postgres-ai mon restart' to restart them\n");
|
|
2322
2349
|
return;
|
|
2323
2350
|
}
|
|
@@ -2336,7 +2363,7 @@ mon
|
|
|
2336
2363
|
} else if (opts.yes) {
|
|
2337
2364
|
// Auto-yes mode without API key - skip API key setup
|
|
2338
2365
|
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
2339
|
-
console.
|
|
2366
|
+
console.error("⚠ Reports will be generated locally only");
|
|
2340
2367
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2341
2368
|
} else {
|
|
2342
2369
|
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
@@ -2356,16 +2383,16 @@ mon
|
|
|
2356
2383
|
break;
|
|
2357
2384
|
}
|
|
2358
2385
|
|
|
2359
|
-
console.
|
|
2386
|
+
console.error("⚠ API key cannot be empty");
|
|
2360
2387
|
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
2361
2388
|
if (retry.toLowerCase() === "n") {
|
|
2362
|
-
console.
|
|
2389
|
+
console.error("⚠ Skipping API key setup - reports will be generated locally only");
|
|
2363
2390
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2364
2391
|
break;
|
|
2365
2392
|
}
|
|
2366
2393
|
}
|
|
2367
2394
|
} else {
|
|
2368
|
-
console.
|
|
2395
|
+
console.error("⚠ Skipping API key setup - reports will be generated locally only");
|
|
2369
2396
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2370
2397
|
}
|
|
2371
2398
|
}
|
|
@@ -2425,7 +2452,7 @@ mon
|
|
|
2425
2452
|
} else if (opts.yes) {
|
|
2426
2453
|
// Auto-yes mode without database URL - skip database setup
|
|
2427
2454
|
console.log("Auto-yes mode: no database URL provided, skipping database setup");
|
|
2428
|
-
console.
|
|
2455
|
+
console.error("⚠ No PostgreSQL instance added");
|
|
2429
2456
|
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
2430
2457
|
} else {
|
|
2431
2458
|
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
@@ -2443,7 +2470,7 @@ mon
|
|
|
2443
2470
|
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
2444
2471
|
if (!m) {
|
|
2445
2472
|
console.error("✗ Invalid connection string format");
|
|
2446
|
-
console.
|
|
2473
|
+
console.error("⚠ Continuing without adding instance\n");
|
|
2447
2474
|
} else {
|
|
2448
2475
|
const host = m[3];
|
|
2449
2476
|
const db = m[5];
|
|
@@ -2468,10 +2495,10 @@ mon
|
|
|
2468
2495
|
}
|
|
2469
2496
|
}
|
|
2470
2497
|
} else {
|
|
2471
|
-
console.
|
|
2498
|
+
console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
2472
2499
|
}
|
|
2473
2500
|
} else {
|
|
2474
|
-
console.
|
|
2501
|
+
console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
2475
2502
|
}
|
|
2476
2503
|
}
|
|
2477
2504
|
} else {
|
|
@@ -2524,7 +2551,7 @@ mon
|
|
|
2524
2551
|
|
|
2525
2552
|
console.log("✓ Grafana password configured\n");
|
|
2526
2553
|
} catch (error) {
|
|
2527
|
-
console.
|
|
2554
|
+
console.error("⚠ Could not generate Grafana password automatically");
|
|
2528
2555
|
console.log("Using default password: demo\n");
|
|
2529
2556
|
grafanaPassword = "demo";
|
|
2530
2557
|
}
|
|
@@ -2916,7 +2943,7 @@ mon
|
|
|
2916
2943
|
if (downCode === 0) {
|
|
2917
2944
|
console.log("✓ Monitoring services stopped and removed");
|
|
2918
2945
|
} else {
|
|
2919
|
-
console.
|
|
2946
|
+
console.error("⚠ Could not stop services (may not be running)");
|
|
2920
2947
|
}
|
|
2921
2948
|
|
|
2922
2949
|
// Remove any orphaned containers that docker compose down missed
|
|
@@ -3229,8 +3256,8 @@ auth
|
|
|
3229
3256
|
const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3230
3257
|
|
|
3231
3258
|
if (opts.debug) {
|
|
3232
|
-
console.
|
|
3233
|
-
console.
|
|
3259
|
+
console.error(`Debug: Resolved API base URL: ${apiBaseUrl}`);
|
|
3260
|
+
console.error(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
|
|
3234
3261
|
}
|
|
3235
3262
|
|
|
3236
3263
|
try {
|
|
@@ -3260,8 +3287,8 @@ auth
|
|
|
3260
3287
|
const initUrl = new URL(`${apiBaseUrl}/rpc/oauth_init`);
|
|
3261
3288
|
|
|
3262
3289
|
if (opts.debug) {
|
|
3263
|
-
console.
|
|
3264
|
-
console.
|
|
3290
|
+
console.error(`Debug: Trying to POST to: ${initUrl.toString()}`);
|
|
3291
|
+
console.error(`Debug: Request data: ${initData}`);
|
|
3265
3292
|
}
|
|
3266
3293
|
|
|
3267
3294
|
// Step 2: Initialize OAuth session on backend using fetch
|
|
@@ -3307,7 +3334,7 @@ auth
|
|
|
3307
3334
|
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
|
|
3308
3335
|
|
|
3309
3336
|
if (opts.debug) {
|
|
3310
|
-
console.
|
|
3337
|
+
console.error(`Debug: Auth URL: ${authUrl}`);
|
|
3311
3338
|
}
|
|
3312
3339
|
|
|
3313
3340
|
console.log(`\nOpening browser for authentication...`);
|
|
@@ -3731,12 +3758,12 @@ issues
|
|
|
3731
3758
|
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
3732
3759
|
if (opts.debug) {
|
|
3733
3760
|
// eslint-disable-next-line no-console
|
|
3734
|
-
console.
|
|
3761
|
+
console.error(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3735
3762
|
}
|
|
3736
3763
|
content = interpretEscapes(content);
|
|
3737
3764
|
if (opts.debug) {
|
|
3738
3765
|
// eslint-disable-next-line no-console
|
|
3739
|
-
console.
|
|
3766
|
+
console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3740
3767
|
}
|
|
3741
3768
|
|
|
3742
3769
|
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
@@ -3930,12 +3957,12 @@ issues
|
|
|
3930
3957
|
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3931
3958
|
if (opts.debug) {
|
|
3932
3959
|
// eslint-disable-next-line no-console
|
|
3933
|
-
console.
|
|
3960
|
+
console.error(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3934
3961
|
}
|
|
3935
3962
|
content = interpretEscapes(content);
|
|
3936
3963
|
if (opts.debug) {
|
|
3937
3964
|
// eslint-disable-next-line no-console
|
|
3938
|
-
console.
|
|
3965
|
+
console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3939
3966
|
}
|
|
3940
3967
|
|
|
3941
3968
|
const rootOpts = program.opts<CliOptions>();
|
|
@@ -3968,6 +3995,93 @@ issues
|
|
|
3968
3995
|
}
|
|
3969
3996
|
});
|
|
3970
3997
|
|
|
3998
|
+
// File upload/download (subcommands of issues)
|
|
3999
|
+
const issueFiles = issues.command("files").description("upload and download files for issues");
|
|
4000
|
+
|
|
4001
|
+
issueFiles
|
|
4002
|
+
.command("upload <path>")
|
|
4003
|
+
.description("upload a file to storage and get a markdown link")
|
|
4004
|
+
.option("--debug", "enable debug output")
|
|
4005
|
+
.option("--json", "output raw JSON")
|
|
4006
|
+
.action(async (filePath: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
4007
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Uploading file...");
|
|
4008
|
+
try {
|
|
4009
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4010
|
+
const cfg = config.readConfig();
|
|
4011
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4012
|
+
if (!apiKey) {
|
|
4013
|
+
spinner.stop();
|
|
4014
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4015
|
+
process.exitCode = 1;
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4020
|
+
|
|
4021
|
+
const result = await uploadFile({
|
|
4022
|
+
apiKey,
|
|
4023
|
+
storageBaseUrl,
|
|
4024
|
+
filePath,
|
|
4025
|
+
debug: !!opts.debug,
|
|
4026
|
+
});
|
|
4027
|
+
spinner.stop();
|
|
4028
|
+
|
|
4029
|
+
if (opts.json) {
|
|
4030
|
+
printResult(result, true);
|
|
4031
|
+
} else {
|
|
4032
|
+
const md = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
|
|
4033
|
+
const displayUrl = result.url.startsWith("/") ? `${storageBaseUrl}${result.url}` : `${storageBaseUrl}/${result.url}`;
|
|
4034
|
+
console.log(`URL: ${displayUrl}`);
|
|
4035
|
+
console.log(`File: ${result.metadata.originalName}`);
|
|
4036
|
+
console.log(`Size: ${result.metadata.size} bytes`);
|
|
4037
|
+
console.log(`Type: ${result.metadata.mimeType}`);
|
|
4038
|
+
console.log(`Markdown: ${md}`);
|
|
4039
|
+
}
|
|
4040
|
+
} catch (err) {
|
|
4041
|
+
spinner.stop();
|
|
4042
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4043
|
+
console.error(message);
|
|
4044
|
+
process.exitCode = 1;
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
issueFiles
|
|
4049
|
+
.command("download <url>")
|
|
4050
|
+
.description("download a file from storage")
|
|
4051
|
+
.option("-o, --output <path>", "output file path (default: derive from URL)")
|
|
4052
|
+
.option("--debug", "enable debug output")
|
|
4053
|
+
.action(async (fileUrl: string, opts: { output?: string; debug?: boolean }) => {
|
|
4054
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Downloading file...");
|
|
4055
|
+
try {
|
|
4056
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4057
|
+
const cfg = config.readConfig();
|
|
4058
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4059
|
+
if (!apiKey) {
|
|
4060
|
+
spinner.stop();
|
|
4061
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4062
|
+
process.exitCode = 1;
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4067
|
+
|
|
4068
|
+
const result = await downloadFile({
|
|
4069
|
+
apiKey,
|
|
4070
|
+
storageBaseUrl,
|
|
4071
|
+
fileUrl,
|
|
4072
|
+
outputPath: opts.output,
|
|
4073
|
+
debug: !!opts.debug,
|
|
4074
|
+
});
|
|
4075
|
+
spinner.stop();
|
|
4076
|
+
console.log(`Saved: ${result.savedTo}`);
|
|
4077
|
+
} catch (err) {
|
|
4078
|
+
spinner.stop();
|
|
4079
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4080
|
+
console.error(message);
|
|
4081
|
+
process.exitCode = 1;
|
|
4082
|
+
}
|
|
4083
|
+
});
|
|
4084
|
+
|
|
3971
4085
|
// Action Items management (subcommands of issues)
|
|
3972
4086
|
issues
|
|
3973
4087
|
.command("action-items <issueId>")
|