postgresai 0.15.0-dev.3 → 0.15.0-dev.5
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 +275 -50
- package/dist/bin/postgres-ai.js +833 -394
- 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/compose-cmd.test.ts +120 -0
- 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
|
/**
|
|
@@ -475,14 +477,14 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
475
477
|
fs.mkdirSync(projectDir, { recursive: true, mode: 0o700 });
|
|
476
478
|
}
|
|
477
479
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
].filter((v): v is string => Boolean(v && v.trim()));
|
|
480
|
+
const refs = [
|
|
481
|
+
process.env.PGAI_PROJECT_REF,
|
|
482
|
+
pkg.version,
|
|
483
|
+
`v${pkg.version}`,
|
|
484
|
+
"main",
|
|
485
|
+
].filter((v): v is string => Boolean(v && v.trim()));
|
|
485
486
|
|
|
487
|
+
if (!fs.existsSync(composeFile)) {
|
|
486
488
|
let lastErr: unknown;
|
|
487
489
|
for (const ref of refs) {
|
|
488
490
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -501,6 +503,21 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
501
503
|
}
|
|
502
504
|
}
|
|
503
505
|
|
|
506
|
+
// Download instances.demo.yml (demo target template) if not present
|
|
507
|
+
const demoFile = path.resolve(projectDir, "instances.demo.yml");
|
|
508
|
+
if (!fs.existsSync(demoFile)) {
|
|
509
|
+
for (const ref of refs) {
|
|
510
|
+
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/instances.demo.yml`;
|
|
511
|
+
try {
|
|
512
|
+
const text = await downloadText(url);
|
|
513
|
+
fs.writeFileSync(demoFile, text, { encoding: "utf8", mode: 0o600 });
|
|
514
|
+
break;
|
|
515
|
+
} catch {
|
|
516
|
+
// non-fatal — demo file is optional
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
504
521
|
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory)
|
|
505
522
|
if (!fs.existsSync(instancesFile)) {
|
|
506
523
|
const header =
|
|
@@ -580,6 +597,10 @@ program
|
|
|
580
597
|
.option(
|
|
581
598
|
"--ui-base-url <url>",
|
|
582
599
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
600
|
+
)
|
|
601
|
+
.option(
|
|
602
|
+
"--storage-base-url <url>",
|
|
603
|
+
"Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)"
|
|
583
604
|
);
|
|
584
605
|
|
|
585
606
|
program
|
|
@@ -596,6 +617,27 @@ program
|
|
|
596
617
|
console.log(`Default project saved: ${value}`);
|
|
597
618
|
});
|
|
598
619
|
|
|
620
|
+
program
|
|
621
|
+
.command("set-storage-url <url>")
|
|
622
|
+
.description("store storage base URL for file uploads")
|
|
623
|
+
.action(async (url: string) => {
|
|
624
|
+
const value = (url || "").trim();
|
|
625
|
+
if (!value) {
|
|
626
|
+
console.error("Error: url is required");
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const { normalizeBaseUrl } = await import("../lib/util");
|
|
632
|
+
const normalized = normalizeBaseUrl(value);
|
|
633
|
+
config.writeConfig({ storageBaseUrl: normalized });
|
|
634
|
+
console.log(`Storage URL saved: ${normalized}`);
|
|
635
|
+
} catch {
|
|
636
|
+
console.error(`Error: invalid URL: ${value}`);
|
|
637
|
+
process.exitCode = 1;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
599
641
|
program
|
|
600
642
|
.command("prepare-db [conn]")
|
|
601
643
|
.description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
|
|
@@ -841,8 +883,8 @@ program
|
|
|
841
883
|
} else {
|
|
842
884
|
console.log("✓ prepare-db verify: OK");
|
|
843
885
|
if (v.missingOptional.length > 0) {
|
|
844
|
-
console.
|
|
845
|
-
for (const m of v.missingOptional) console.
|
|
886
|
+
console.error("⚠ Optional items missing:");
|
|
887
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
846
888
|
}
|
|
847
889
|
}
|
|
848
890
|
return;
|
|
@@ -973,8 +1015,8 @@ program
|
|
|
973
1015
|
} else {
|
|
974
1016
|
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
975
1017
|
if (skippedOptional.length > 0) {
|
|
976
|
-
console.
|
|
977
|
-
for (const s of skippedOptional) console.
|
|
1018
|
+
console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
1019
|
+
for (const s of skippedOptional) console.error(`- ${s}`);
|
|
978
1020
|
}
|
|
979
1021
|
if (process.stdout.isTTY) {
|
|
980
1022
|
console.log(`Applied ${applied.length} steps`);
|
|
@@ -1155,8 +1197,8 @@ program
|
|
|
1155
1197
|
} else {
|
|
1156
1198
|
console.log(`✓ prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
1157
1199
|
if (v.missingOptional.length > 0) {
|
|
1158
|
-
console.
|
|
1159
|
-
for (const m of v.missingOptional) console.
|
|
1200
|
+
console.error("⚠ Optional items missing:");
|
|
1201
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
1160
1202
|
}
|
|
1161
1203
|
}
|
|
1162
1204
|
return;
|
|
@@ -1283,8 +1325,8 @@ program
|
|
|
1283
1325
|
} else {
|
|
1284
1326
|
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
1285
1327
|
if (skippedOptional.length > 0) {
|
|
1286
|
-
console.
|
|
1287
|
-
for (const s of skippedOptional) console.
|
|
1328
|
+
console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
1329
|
+
for (const s of skippedOptional) console.error(`- ${s}`);
|
|
1288
1330
|
}
|
|
1289
1331
|
// Keep output compact but still useful
|
|
1290
1332
|
if (process.stdout.isTTY) {
|
|
@@ -1600,11 +1642,11 @@ program
|
|
|
1600
1642
|
console.log("✓ unprepare-db completed");
|
|
1601
1643
|
console.log(`Applied ${applied.length} steps`);
|
|
1602
1644
|
} else {
|
|
1603
|
-
console.
|
|
1645
|
+
console.error("⚠ unprepare-db completed with errors");
|
|
1604
1646
|
console.log(`Applied ${applied.length} steps`);
|
|
1605
|
-
console.
|
|
1647
|
+
console.error("Errors:");
|
|
1606
1648
|
for (const err of errors) {
|
|
1607
|
-
console.
|
|
1649
|
+
console.error(` - ${err}`);
|
|
1608
1650
|
}
|
|
1609
1651
|
process.exitCode = 1;
|
|
1610
1652
|
}
|
|
@@ -2044,13 +2086,16 @@ function isDockerRunning(): boolean {
|
|
|
2044
2086
|
}
|
|
2045
2087
|
|
|
2046
2088
|
/**
|
|
2047
|
-
* Get docker compose command
|
|
2089
|
+
* Get docker compose command.
|
|
2090
|
+
* Prefer "docker compose" (V2 plugin) over legacy "docker-compose" (V1 standalone)
|
|
2091
|
+
* because docker-compose V1 (<=1.29) is incompatible with modern Docker engines
|
|
2092
|
+
* (KeyError: 'ContainerConfig' on container recreation).
|
|
2048
2093
|
*/
|
|
2049
2094
|
function getComposeCmd(): string[] | null {
|
|
2050
2095
|
const tryCmd = (cmd: string, args: string[]): boolean =>
|
|
2051
2096
|
spawnSync(cmd, args, { stdio: "ignore", timeout: 5000 } as Parameters<typeof spawnSync>[2]).status === 0;
|
|
2052
|
-
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
2053
2097
|
if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
|
|
2098
|
+
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
2054
2099
|
return null;
|
|
2055
2100
|
}
|
|
2056
2101
|
|
|
@@ -2089,9 +2134,9 @@ function registerMonitoringInstance(
|
|
|
2089
2134
|
const debug = opts?.debug;
|
|
2090
2135
|
|
|
2091
2136
|
if (debug) {
|
|
2092
|
-
console.
|
|
2093
|
-
console.
|
|
2094
|
-
console.
|
|
2137
|
+
console.error(`\nDebug: Registering monitoring instance...`);
|
|
2138
|
+
console.error(`Debug: POST ${url}`);
|
|
2139
|
+
console.error(`Debug: project_name=${projectName}`);
|
|
2095
2140
|
}
|
|
2096
2141
|
|
|
2097
2142
|
// Fire and forget - don't block the main flow
|
|
@@ -2109,18 +2154,18 @@ function registerMonitoringInstance(
|
|
|
2109
2154
|
const body = await res.text().catch(() => "");
|
|
2110
2155
|
if (!res.ok) {
|
|
2111
2156
|
if (debug) {
|
|
2112
|
-
console.
|
|
2113
|
-
console.
|
|
2157
|
+
console.error(`Debug: Monitoring registration failed: HTTP ${res.status}`);
|
|
2158
|
+
console.error(`Debug: Response: ${body}`);
|
|
2114
2159
|
}
|
|
2115
2160
|
return;
|
|
2116
2161
|
}
|
|
2117
2162
|
if (debug) {
|
|
2118
|
-
console.
|
|
2163
|
+
console.error(`Debug: Monitoring registration response: ${body}`);
|
|
2119
2164
|
}
|
|
2120
2165
|
})
|
|
2121
2166
|
.catch((err) => {
|
|
2122
2167
|
if (debug) {
|
|
2123
|
-
console.
|
|
2168
|
+
console.error(`Debug: Monitoring registration error: ${err.message}`);
|
|
2124
2169
|
}
|
|
2125
2170
|
});
|
|
2126
2171
|
}
|
|
@@ -2208,6 +2253,26 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
|
|
|
2208
2253
|
}
|
|
2209
2254
|
}
|
|
2210
2255
|
|
|
2256
|
+
// Load VM auth credentials from .env if not already set
|
|
2257
|
+
const envFilePath = path.resolve(projectDir, ".env");
|
|
2258
|
+
if (fs.existsSync(envFilePath)) {
|
|
2259
|
+
try {
|
|
2260
|
+
const envContent = fs.readFileSync(envFilePath, "utf8");
|
|
2261
|
+
if (!env.VM_AUTH_USERNAME) {
|
|
2262
|
+
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
2263
|
+
if (m) env.VM_AUTH_USERNAME = m[1].trim().replace(/^["']|["']$/g, '');
|
|
2264
|
+
}
|
|
2265
|
+
if (!env.VM_AUTH_PASSWORD) {
|
|
2266
|
+
const m = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
2267
|
+
if (m) env.VM_AUTH_PASSWORD = m[1].trim().replace(/^["']|["']$/g, '');
|
|
2268
|
+
}
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
if (process.env.DEBUG) {
|
|
2271
|
+
console.warn(`Warning: Could not read VM auth from .env: ${err instanceof Error ? err.message : String(err)}`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2211
2276
|
// On macOS, self-node-exporter can't mount host root filesystem - skip it
|
|
2212
2277
|
const finalArgs = [...args];
|
|
2213
2278
|
if (process.platform === "darwin" && args.includes("up")) {
|
|
@@ -2300,8 +2365,8 @@ mon
|
|
|
2300
2365
|
|
|
2301
2366
|
// Validate conflicting options
|
|
2302
2367
|
if (opts.demo && opts.dbUrl) {
|
|
2303
|
-
console.
|
|
2304
|
-
console.
|
|
2368
|
+
console.error("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
|
|
2369
|
+
console.error("⚠ The --db-url will be ignored in demo mode.\n");
|
|
2305
2370
|
opts.dbUrl = undefined;
|
|
2306
2371
|
}
|
|
2307
2372
|
|
|
@@ -2317,7 +2382,7 @@ mon
|
|
|
2317
2382
|
// Check if containers are already running
|
|
2318
2383
|
const { running, containers } = checkRunningContainers();
|
|
2319
2384
|
if (running) {
|
|
2320
|
-
console.
|
|
2385
|
+
console.error(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
|
|
2321
2386
|
console.log("Use 'postgres-ai mon restart' to restart them\n");
|
|
2322
2387
|
return;
|
|
2323
2388
|
}
|
|
@@ -2336,7 +2401,7 @@ mon
|
|
|
2336
2401
|
} else if (opts.yes) {
|
|
2337
2402
|
// Auto-yes mode without API key - skip API key setup
|
|
2338
2403
|
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
2339
|
-
console.
|
|
2404
|
+
console.error("⚠ Reports will be generated locally only");
|
|
2340
2405
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2341
2406
|
} else {
|
|
2342
2407
|
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
@@ -2356,16 +2421,16 @@ mon
|
|
|
2356
2421
|
break;
|
|
2357
2422
|
}
|
|
2358
2423
|
|
|
2359
|
-
console.
|
|
2424
|
+
console.error("⚠ API key cannot be empty");
|
|
2360
2425
|
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
2361
2426
|
if (retry.toLowerCase() === "n") {
|
|
2362
|
-
console.
|
|
2427
|
+
console.error("⚠ Skipping API key setup - reports will be generated locally only");
|
|
2363
2428
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2364
2429
|
break;
|
|
2365
2430
|
}
|
|
2366
2431
|
}
|
|
2367
2432
|
} else {
|
|
2368
|
-
console.
|
|
2433
|
+
console.error("⚠ Skipping API key setup - reports will be generated locally only");
|
|
2369
2434
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
2370
2435
|
}
|
|
2371
2436
|
}
|
|
@@ -2425,7 +2490,7 @@ mon
|
|
|
2425
2490
|
} else if (opts.yes) {
|
|
2426
2491
|
// Auto-yes mode without database URL - skip database setup
|
|
2427
2492
|
console.log("Auto-yes mode: no database URL provided, skipping database setup");
|
|
2428
|
-
console.
|
|
2493
|
+
console.error("⚠ No PostgreSQL instance added");
|
|
2429
2494
|
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
2430
2495
|
} else {
|
|
2431
2496
|
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
@@ -2443,7 +2508,7 @@ mon
|
|
|
2443
2508
|
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
2444
2509
|
if (!m) {
|
|
2445
2510
|
console.error("✗ Invalid connection string format");
|
|
2446
|
-
console.
|
|
2511
|
+
console.error("⚠ Continuing without adding instance\n");
|
|
2447
2512
|
} else {
|
|
2448
2513
|
const host = m[3];
|
|
2449
2514
|
const db = m[5];
|
|
@@ -2468,14 +2533,23 @@ mon
|
|
|
2468
2533
|
}
|
|
2469
2534
|
}
|
|
2470
2535
|
} else {
|
|
2471
|
-
console.
|
|
2536
|
+
console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
2472
2537
|
}
|
|
2473
2538
|
} else {
|
|
2474
|
-
console.
|
|
2539
|
+
console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
2475
2540
|
}
|
|
2476
2541
|
}
|
|
2477
2542
|
} else {
|
|
2478
|
-
|
|
2543
|
+
// Demo mode: copy instances.demo.yml → instances.yml so the demo target is active
|
|
2544
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
2545
|
+
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
2546
|
+
const demoSrc = path.resolve(projectDir, "instances.demo.yml");
|
|
2547
|
+
if (fs.existsSync(demoSrc)) {
|
|
2548
|
+
fs.copyFileSync(demoSrc, instancesPath);
|
|
2549
|
+
console.log("✓ Demo monitoring target configured\n");
|
|
2550
|
+
} else {
|
|
2551
|
+
console.error("⚠ instances.demo.yml not found — demo target not configured\n");
|
|
2552
|
+
}
|
|
2479
2553
|
}
|
|
2480
2554
|
|
|
2481
2555
|
// Step 3: Update configuration
|
|
@@ -2491,6 +2565,8 @@ mon
|
|
|
2491
2565
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
2492
2566
|
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
2493
2567
|
let grafanaPassword = "";
|
|
2568
|
+
let vmAuthUsername = "";
|
|
2569
|
+
let vmAuthPassword = "";
|
|
2494
2570
|
|
|
2495
2571
|
try {
|
|
2496
2572
|
if (fs.existsSync(cfgPath)) {
|
|
@@ -2524,12 +2600,58 @@ mon
|
|
|
2524
2600
|
|
|
2525
2601
|
console.log("✓ Grafana password configured\n");
|
|
2526
2602
|
} catch (error) {
|
|
2527
|
-
console.
|
|
2603
|
+
console.error("⚠ Could not generate Grafana password automatically");
|
|
2528
2604
|
console.log("Using default password: demo\n");
|
|
2529
2605
|
grafanaPassword = "demo";
|
|
2530
2606
|
}
|
|
2531
2607
|
|
|
2608
|
+
// Generate VictoriaMetrics auth credentials
|
|
2609
|
+
try {
|
|
2610
|
+
const envFile = path.resolve(projectDir, ".env");
|
|
2611
|
+
|
|
2612
|
+
// Read existing VM auth from .env if present
|
|
2613
|
+
if (fs.existsSync(envFile)) {
|
|
2614
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
2615
|
+
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
2616
|
+
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
2617
|
+
if (userMatch) vmAuthUsername = userMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
2618
|
+
if (passMatch) vmAuthPassword = passMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
if (!vmAuthUsername || !vmAuthPassword) {
|
|
2622
|
+
console.log("Generating VictoriaMetrics auth credentials...");
|
|
2623
|
+
vmAuthUsername = vmAuthUsername || "vmauth";
|
|
2624
|
+
if (!vmAuthPassword) {
|
|
2625
|
+
const { stdout: vmPass } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
|
|
2626
|
+
vmAuthPassword = vmPass.trim();
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// Update .env file with VM auth credentials
|
|
2630
|
+
let envContent = "";
|
|
2631
|
+
if (fs.existsSync(envFile)) {
|
|
2632
|
+
envContent = fs.readFileSync(envFile, "utf8");
|
|
2633
|
+
}
|
|
2634
|
+
const envLines = envContent.split(/\r?\n/)
|
|
2635
|
+
.filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l))
|
|
2636
|
+
.filter((l, i, arr) => !(i === arr.length - 1 && l === ''));
|
|
2637
|
+
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
2638
|
+
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
2639
|
+
fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
console.log("✓ VictoriaMetrics auth configured\n");
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
console.error("⚠ Could not generate VictoriaMetrics auth credentials automatically");
|
|
2645
|
+
if (process.env.DEBUG) {
|
|
2646
|
+
console.warn(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2532
2650
|
// Step 5: Start services
|
|
2651
|
+
// Remove stopped containers left by "run --rm" dependencies (e.g. config-init)
|
|
2652
|
+
// to avoid docker-compose v1 'ContainerConfig' error on recreation.
|
|
2653
|
+
// Best-effort: ignore exit code — container may not exist, failure here is non-fatal.
|
|
2654
|
+
await runCompose(["rm", "-f", "-s", "config-init"]);
|
|
2533
2655
|
console.log("Step 5: Starting monitoring services...");
|
|
2534
2656
|
const code2 = await runCompose(["up", "-d", "--force-recreate"], grafanaPassword);
|
|
2535
2657
|
if (code2 !== 0) {
|
|
@@ -2579,6 +2701,9 @@ mon
|
|
|
2579
2701
|
console.log("🚀 MAIN ACCESS POINT - Start here:");
|
|
2580
2702
|
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
2581
2703
|
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
2704
|
+
if (vmAuthUsername && vmAuthPassword) {
|
|
2705
|
+
console.log(` VictoriaMetrics Auth: ${vmAuthUsername} / ${vmAuthPassword}`);
|
|
2706
|
+
}
|
|
2582
2707
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
2583
2708
|
});
|
|
2584
2709
|
|
|
@@ -2916,7 +3041,7 @@ mon
|
|
|
2916
3041
|
if (downCode === 0) {
|
|
2917
3042
|
console.log("✓ Monitoring services stopped and removed");
|
|
2918
3043
|
} else {
|
|
2919
|
-
console.
|
|
3044
|
+
console.error("⚠ Could not stop services (may not be running)");
|
|
2920
3045
|
}
|
|
2921
3046
|
|
|
2922
3047
|
// Remove any orphaned containers that docker compose down missed
|
|
@@ -3229,8 +3354,8 @@ auth
|
|
|
3229
3354
|
const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3230
3355
|
|
|
3231
3356
|
if (opts.debug) {
|
|
3232
|
-
console.
|
|
3233
|
-
console.
|
|
3357
|
+
console.error(`Debug: Resolved API base URL: ${apiBaseUrl}`);
|
|
3358
|
+
console.error(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
|
|
3234
3359
|
}
|
|
3235
3360
|
|
|
3236
3361
|
try {
|
|
@@ -3260,8 +3385,8 @@ auth
|
|
|
3260
3385
|
const initUrl = new URL(`${apiBaseUrl}/rpc/oauth_init`);
|
|
3261
3386
|
|
|
3262
3387
|
if (opts.debug) {
|
|
3263
|
-
console.
|
|
3264
|
-
console.
|
|
3388
|
+
console.error(`Debug: Trying to POST to: ${initUrl.toString()}`);
|
|
3389
|
+
console.error(`Debug: Request data: ${initData}`);
|
|
3265
3390
|
}
|
|
3266
3391
|
|
|
3267
3392
|
// Step 2: Initialize OAuth session on backend using fetch
|
|
@@ -3307,7 +3432,7 @@ auth
|
|
|
3307
3432
|
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
3433
|
|
|
3309
3434
|
if (opts.debug) {
|
|
3310
|
-
console.
|
|
3435
|
+
console.error(`Debug: Auth URL: ${authUrl}`);
|
|
3311
3436
|
}
|
|
3312
3437
|
|
|
3313
3438
|
console.log(`\nOpening browser for authentication...`);
|
|
@@ -3598,6 +3723,19 @@ mon
|
|
|
3598
3723
|
console.log(" URL: http://localhost:3000");
|
|
3599
3724
|
console.log(" Username: monitor");
|
|
3600
3725
|
console.log(` Password: ${password}`);
|
|
3726
|
+
|
|
3727
|
+
// Show VM auth credentials from .env
|
|
3728
|
+
const envFile = path.resolve(projectDir, ".env");
|
|
3729
|
+
if (fs.existsSync(envFile)) {
|
|
3730
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
3731
|
+
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
3732
|
+
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
3733
|
+
if (vmUser && vmPass) {
|
|
3734
|
+
console.log("\nVictoriaMetrics credentials:");
|
|
3735
|
+
console.log(` Username: ${vmUser[1].trim().replace(/^["']|["']$/g, '')}`);
|
|
3736
|
+
console.log(` Password: ${vmPass[1].trim().replace(/^["']|["']$/g, '')}`);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3601
3739
|
console.log("");
|
|
3602
3740
|
});
|
|
3603
3741
|
|
|
@@ -3731,12 +3869,12 @@ issues
|
|
|
3731
3869
|
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
3732
3870
|
if (opts.debug) {
|
|
3733
3871
|
// eslint-disable-next-line no-console
|
|
3734
|
-
console.
|
|
3872
|
+
console.error(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3735
3873
|
}
|
|
3736
3874
|
content = interpretEscapes(content);
|
|
3737
3875
|
if (opts.debug) {
|
|
3738
3876
|
// eslint-disable-next-line no-console
|
|
3739
|
-
console.
|
|
3877
|
+
console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3740
3878
|
}
|
|
3741
3879
|
|
|
3742
3880
|
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
@@ -3930,12 +4068,12 @@ issues
|
|
|
3930
4068
|
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
3931
4069
|
if (opts.debug) {
|
|
3932
4070
|
// eslint-disable-next-line no-console
|
|
3933
|
-
console.
|
|
4071
|
+
console.error(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
3934
4072
|
}
|
|
3935
4073
|
content = interpretEscapes(content);
|
|
3936
4074
|
if (opts.debug) {
|
|
3937
4075
|
// eslint-disable-next-line no-console
|
|
3938
|
-
console.
|
|
4076
|
+
console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3939
4077
|
}
|
|
3940
4078
|
|
|
3941
4079
|
const rootOpts = program.opts<CliOptions>();
|
|
@@ -3968,6 +4106,93 @@ issues
|
|
|
3968
4106
|
}
|
|
3969
4107
|
});
|
|
3970
4108
|
|
|
4109
|
+
// File upload/download (subcommands of issues)
|
|
4110
|
+
const issueFiles = issues.command("files").description("upload and download files for issues");
|
|
4111
|
+
|
|
4112
|
+
issueFiles
|
|
4113
|
+
.command("upload <path>")
|
|
4114
|
+
.description("upload a file to storage and get a markdown link")
|
|
4115
|
+
.option("--debug", "enable debug output")
|
|
4116
|
+
.option("--json", "output raw JSON")
|
|
4117
|
+
.action(async (filePath: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
4118
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Uploading file...");
|
|
4119
|
+
try {
|
|
4120
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4121
|
+
const cfg = config.readConfig();
|
|
4122
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4123
|
+
if (!apiKey) {
|
|
4124
|
+
spinner.stop();
|
|
4125
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4126
|
+
process.exitCode = 1;
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4131
|
+
|
|
4132
|
+
const result = await uploadFile({
|
|
4133
|
+
apiKey,
|
|
4134
|
+
storageBaseUrl,
|
|
4135
|
+
filePath,
|
|
4136
|
+
debug: !!opts.debug,
|
|
4137
|
+
});
|
|
4138
|
+
spinner.stop();
|
|
4139
|
+
|
|
4140
|
+
if (opts.json) {
|
|
4141
|
+
printResult(result, true);
|
|
4142
|
+
} else {
|
|
4143
|
+
const md = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
|
|
4144
|
+
const displayUrl = result.url.startsWith("/") ? `${storageBaseUrl}${result.url}` : `${storageBaseUrl}/${result.url}`;
|
|
4145
|
+
console.log(`URL: ${displayUrl}`);
|
|
4146
|
+
console.log(`File: ${result.metadata.originalName}`);
|
|
4147
|
+
console.log(`Size: ${result.metadata.size} bytes`);
|
|
4148
|
+
console.log(`Type: ${result.metadata.mimeType}`);
|
|
4149
|
+
console.log(`Markdown: ${md}`);
|
|
4150
|
+
}
|
|
4151
|
+
} catch (err) {
|
|
4152
|
+
spinner.stop();
|
|
4153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4154
|
+
console.error(message);
|
|
4155
|
+
process.exitCode = 1;
|
|
4156
|
+
}
|
|
4157
|
+
});
|
|
4158
|
+
|
|
4159
|
+
issueFiles
|
|
4160
|
+
.command("download <url>")
|
|
4161
|
+
.description("download a file from storage")
|
|
4162
|
+
.option("-o, --output <path>", "output file path (default: derive from URL)")
|
|
4163
|
+
.option("--debug", "enable debug output")
|
|
4164
|
+
.action(async (fileUrl: string, opts: { output?: string; debug?: boolean }) => {
|
|
4165
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Downloading file...");
|
|
4166
|
+
try {
|
|
4167
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4168
|
+
const cfg = config.readConfig();
|
|
4169
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4170
|
+
if (!apiKey) {
|
|
4171
|
+
spinner.stop();
|
|
4172
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4173
|
+
process.exitCode = 1;
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4178
|
+
|
|
4179
|
+
const result = await downloadFile({
|
|
4180
|
+
apiKey,
|
|
4181
|
+
storageBaseUrl,
|
|
4182
|
+
fileUrl,
|
|
4183
|
+
outputPath: opts.output,
|
|
4184
|
+
debug: !!opts.debug,
|
|
4185
|
+
});
|
|
4186
|
+
spinner.stop();
|
|
4187
|
+
console.log(`Saved: ${result.savedTo}`);
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
spinner.stop();
|
|
4190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4191
|
+
console.error(message);
|
|
4192
|
+
process.exitCode = 1;
|
|
4193
|
+
}
|
|
4194
|
+
});
|
|
4195
|
+
|
|
3971
4196
|
// Action Items management (subcommands of issues)
|
|
3972
4197
|
issues
|
|
3973
4198
|
.command("action-items <issueId>")
|