postgresai 0.15.0-dev.6 → 0.15.0-rc.0

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/README.md CHANGED
@@ -173,7 +173,7 @@ postgresai mon local-install --api-key your_key --db-url postgresql://user:pass@
173
173
  This will:
174
174
  - Configure API key for automated report uploads (if provided)
175
175
  - Add PostgreSQL instance to monitor (if provided)
176
- - Generate secure Grafana password
176
+ - Generate secure Grafana and replication passwords
177
177
  - Start all monitoring services
178
178
  - Open Grafana at http://localhost:3000
179
179
 
@@ -205,6 +205,8 @@ postgresai mon health [--wait <sec>] # Check monitoring services health
205
205
  - `--db-url <url>` - PostgreSQL connection URL to monitor (format: `postgresql://user:pass@host:port/db`)
206
206
  - `-y, --yes` - Accept all defaults and skip interactive prompts
207
207
 
208
+ `local-install` writes `.env` in the monitoring directory. It preserves existing `REPLICATOR_PASSWORD` and `VM_AUTH_*` values or generates new random ones when missing; `VM_AUTH_USERNAME` defaults to `vmauth` when absent. The replication password is used by the demo PostgreSQL standby replication user, and the VM auth credentials are required before Docker Compose can provision Grafana datasources. If you run `docker compose` directly or maintain `.env` yourself, set both VM auth values before upgrading. For rotation, run `VM_AUTH_PASSWORD="$(openssl rand -base64 18)" ./scripts/rotate-vm-auth.sh` from the monitoring directory so `.env`, `sink-prometheus`, and `grafana` update together.
209
+
208
210
  #### Monitoring target databases (`mon targets` subgroup)
209
211
  ```bash
210
212
  postgresai mon targets list # List databases to monitor
@@ -15,7 +15,7 @@ import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, create
15
15
  import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
16
16
  import { resolveBaseUrls } from "../lib/util";
17
17
  import { uploadFile, downloadFile, buildMarkdownLink } from "../lib/storage";
18
- import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
18
+ import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, checkCurrentUserPermissions, connectWithSslFallback, DEFAULT_MONITORING_USER, formatPermissionCheckMessages, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
19
19
  import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
20
20
  import * as pkce from "../lib/pkce";
21
21
  import * as authServer from "../lib/auth-server";
@@ -54,21 +54,16 @@ function closeReadline() {
54
54
  }
55
55
  }
56
56
 
57
- // Helper functions for spawning processes - use Node.js child_process for compatibility
58
- async function execPromise(command: string): Promise<{ stdout: string; stderr: string }> {
59
- return new Promise((resolve, reject) => {
60
- childProcess.exec(command, (error, stdout, stderr) => {
61
- if (error) {
62
- const err = error as Error & { code: number };
63
- err.code = typeof error.code === "number" ? error.code : 1;
64
- reject(err);
65
- } else {
66
- resolve({ stdout, stderr });
67
- }
68
- });
69
- });
57
+ function stripMatchingQuotes(value: string): string {
58
+ const trimmed = value.trim();
59
+ const quote = trimmed[0];
60
+ if (trimmed.length >= 2 && (quote === '"' || quote === "'") && trimmed.endsWith(quote)) {
61
+ return trimmed.slice(1, -1);
62
+ }
63
+ return trimmed;
70
64
  }
71
65
 
66
+ // Helper functions for spawning processes - use Node.js child_process for compatibility
72
67
  async function execFilePromise(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
73
68
  return new Promise((resolve, reject) => {
74
69
  childProcess.execFile(file, args, (error, stdout, stderr) => {
@@ -506,7 +501,7 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
506
501
 
507
502
  // Ensure instances.yml exists as a FILE (avoid Docker creating a directory).
508
503
  // Docker bind-mounts create missing paths as directories; replace if so.
509
- if (fs.existsSync(instancesFile) && fs.statSync(instancesFile).isDirectory()) {
504
+ if (fs.existsSync(instancesFile) && fs.lstatSync(instancesFile).isDirectory()) {
510
505
  fs.rmSync(instancesFile, { recursive: true, force: true });
511
506
  }
512
507
  if (!fs.existsSync(instancesFile)) {
@@ -1831,6 +1826,24 @@ program
1831
1826
  const connResult = await connectWithSslFallback(Client, adminConn);
1832
1827
  client = connResult.client as Client;
1833
1828
 
1829
+ // Preflight: verify the connected user has sufficient permissions
1830
+ spinner.update("Checking database permissions");
1831
+ const permCheck = await checkCurrentUserPermissions(client);
1832
+ const permMessages = formatPermissionCheckMessages(permCheck);
1833
+
1834
+ for (const w of permMessages.warnings) {
1835
+ console.error(w);
1836
+ }
1837
+
1838
+ if (permMessages.failed) {
1839
+ spinner.stop();
1840
+ for (const e of permMessages.errors) {
1841
+ console.error(e);
1842
+ }
1843
+ process.exitCode = 1;
1844
+ return;
1845
+ }
1846
+
1834
1847
  // Generate reports
1835
1848
  let reports: Record<string, any>;
1836
1849
  if (checkId === "ALL") {
@@ -2250,11 +2263,11 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
2250
2263
  const envContent = fs.readFileSync(envFilePath, "utf8");
2251
2264
  if (!env.VM_AUTH_USERNAME) {
2252
2265
  const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
2253
- if (m) env.VM_AUTH_USERNAME = m[1].trim().replace(/^["']|["']$/g, '');
2266
+ if (m) env.VM_AUTH_USERNAME = stripMatchingQuotes(m[1]);
2254
2267
  }
2255
2268
  if (!env.VM_AUTH_PASSWORD) {
2256
2269
  const m = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
2257
- if (m) env.VM_AUTH_PASSWORD = m[1].trim().replace(/^["']|["']$/g, '');
2270
+ if (m) env.VM_AUTH_PASSWORD = stripMatchingQuotes(m[1]);
2258
2271
  }
2259
2272
  } catch (err) {
2260
2273
  if (process.env.DEBUG) {
@@ -2320,10 +2333,13 @@ mon
2320
2333
  // Update .env with custom tag if provided
2321
2334
  const envFile = path.resolve(projectDir, ".env");
2322
2335
 
2323
- // Build .env content, preserving important existing values (registry, password)
2324
- // Note: PGAI_TAG is intentionally NOT preserved - the CLI version should always match Docker images
2336
+ // Build .env content, preserving important existing values.
2337
+ // Note: PGAI_TAG is intentionally NOT preserved - the CLI version should always match Docker images.
2325
2338
  let existingRegistry: string | null = null;
2326
2339
  let existingPassword: string | null = null;
2340
+ let existingReplicatorPassword: string | null = null;
2341
+ let existingVmAuthUsername: string | null = null;
2342
+ let existingVmAuthPassword: string | null = null;
2327
2343
 
2328
2344
  if (fs.existsSync(envFile)) {
2329
2345
  const existingEnv = fs.readFileSync(envFile, "utf8");
@@ -2332,6 +2348,12 @@ mon
2332
2348
  if (registryMatch) existingRegistry = registryMatch[1].trim();
2333
2349
  const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
2334
2350
  if (pwdMatch) existingPassword = pwdMatch[1].trim();
2351
+ const replicatorPwdMatch = existingEnv.match(/^REPLICATOR_PASSWORD=(.+)$/m);
2352
+ if (replicatorPwdMatch) existingReplicatorPassword = replicatorPwdMatch[1].trim();
2353
+ const vmAuthUserMatch = existingEnv.match(/^VM_AUTH_USERNAME=(.+)$/m);
2354
+ if (vmAuthUserMatch) existingVmAuthUsername = stripMatchingQuotes(vmAuthUserMatch[1]);
2355
+ const vmAuthPasswordMatch = existingEnv.match(/^VM_AUTH_PASSWORD=(.+)$/m);
2356
+ if (vmAuthPasswordMatch) existingVmAuthPassword = stripMatchingQuotes(vmAuthPasswordMatch[1]);
2335
2357
  }
2336
2358
 
2337
2359
  // Priority: CLI --tag flag > package version
@@ -2347,6 +2369,11 @@ mon
2347
2369
  if (existingPassword) {
2348
2370
  envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
2349
2371
  }
2372
+ envLines.push(
2373
+ `REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto.randomBytes(32).toString("hex")}`,
2374
+ );
2375
+ envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
2376
+ envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto.randomBytes(18).toString("base64")}`);
2350
2377
  fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
2351
2378
 
2352
2379
  if (opts.tag) {
@@ -2466,16 +2493,20 @@ mon
2466
2493
 
2467
2494
  // Test connection
2468
2495
  console.log("Testing connection to the added instance...");
2469
- try {
2470
- const client = new Client({ connectionString: connStr });
2471
- await client.connect();
2472
- const result = await client.query("select version();");
2473
- console.log("✓ Connection successful");
2474
- console.log(`${result.rows[0].version}\n`);
2475
- await client.end();
2476
- } catch (error) {
2477
- const message = error instanceof Error ? error.message : String(error);
2478
- console.error(`✗ Connection failed: ${message}\n`);
2496
+ {
2497
+ let testClient: InstanceType<typeof Client> | null = null;
2498
+ try {
2499
+ testClient = new Client({ connectionString: connStr, connectionTimeoutMillis: 10000 });
2500
+ await testClient.connect();
2501
+ const result = await testClient.query("select version();");
2502
+ console.log("✓ Connection successful");
2503
+ console.log(`${result.rows[0].version}\n`);
2504
+ } catch (error) {
2505
+ const message = error instanceof Error ? error.message : String(error);
2506
+ console.error(`✗ Connection failed: ${message}\n`);
2507
+ } finally {
2508
+ if (testClient) await testClient.end();
2509
+ }
2479
2510
  }
2480
2511
  } else if (opts.yes) {
2481
2512
  // Auto-yes mode without database URL - skip database setup
@@ -2510,16 +2541,20 @@ mon
2510
2541
 
2511
2542
  // Test connection
2512
2543
  console.log("Testing connection to the added instance...");
2513
- try {
2514
- const client = new Client({ connectionString: connStr });
2515
- await client.connect();
2516
- const result = await client.query("select version();");
2517
- console.log("✓ Connection successful");
2518
- console.log(`${result.rows[0].version}\n`);
2519
- await client.end();
2520
- } catch (error) {
2521
- const message = error instanceof Error ? error.message : String(error);
2522
- console.error(`✗ Connection failed: ${message}\n`);
2544
+ {
2545
+ let testClient: InstanceType<typeof Client> | null = null;
2546
+ try {
2547
+ testClient = new Client({ connectionString: connStr, connectionTimeoutMillis: 10000 });
2548
+ await testClient.connect();
2549
+ const result = await testClient.query("select version();");
2550
+ console.log("✓ Connection successful");
2551
+ console.log(`${result.rows[0].version}\n`);
2552
+ } catch (error) {
2553
+ const message = error instanceof Error ? error.message : String(error);
2554
+ console.error(`✗ Connection failed: ${message}\n`);
2555
+ } finally {
2556
+ if (testClient) await testClient.end();
2557
+ }
2523
2558
  }
2524
2559
  }
2525
2560
  } else {
@@ -2530,10 +2565,21 @@ mon
2530
2565
  }
2531
2566
  }
2532
2567
  } else {
2533
- // Demo mode: copy bundled instances.demo.yml instances.yml so the demo target is active
2568
+ // Demo mode: configure instances.yml from the bundled demo template.
2569
+ //
2570
+ // Side effects:
2571
+ // - Writes instancesPath (instances.yml next to docker-compose.yml)
2572
+ // - If Docker previously bind-mounted instances.yml as a directory, removes it first.
2573
+ //
2574
+ // Failure modes:
2575
+ // - Exits with code 1 if instances.demo.yml is not found in any candidate path.
2576
+ // This is fatal because starting without a target produces empty dashboards that
2577
+ // look like a bug rather than a misconfiguration.
2578
+ //
2579
+ // Template search order (import.meta.url is resolved at runtime, not baked in at build):
2580
+ // 1. npm layout: dist/bin/../../instances.demo.yml → package-root/instances.demo.yml
2581
+ // 2. dev layout: cli/bin/../../../instances.demo.yml → repo-root/instances.demo.yml
2534
2582
  console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
2535
- // Use import.meta.url instead of __dirname — bundlers bake in __dirname at build time.
2536
- // Check multiple candidate paths (npm package vs repo dev layout).
2537
2583
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
2538
2584
  const demoCandidates = [
2539
2585
  path.resolve(currentDir, "..", "..", "instances.demo.yml"), // npm: dist/bin -> package root
@@ -2541,14 +2587,15 @@ mon
2541
2587
  ];
2542
2588
  const demoSrc = demoCandidates.find(p => fs.existsSync(p));
2543
2589
  if (demoSrc) {
2544
- // Remove directory artifact left by Docker bind-mounts
2545
- if (fs.existsSync(instancesPath) && fs.statSync(instancesPath).isDirectory()) {
2590
+ // Remove directory artifact left by Docker bind-mounts before copying
2591
+ if (fs.existsSync(instancesPath) && fs.lstatSync(instancesPath).isDirectory()) {
2546
2592
  fs.rmSync(instancesPath, { recursive: true, force: true });
2547
2593
  }
2548
2594
  fs.copyFileSync(demoSrc, instancesPath);
2549
2595
  console.log("✓ Demo monitoring target configured\n");
2550
2596
  } else {
2551
- console.error(`⚠ instances.demo.yml not found — demo target not configured (searched: ${demoCandidates.join(", ")})\n`);
2597
+ console.error(`Error: instances.demo.yml not found — cannot configure demo target.\nSearched: ${demoCandidates.join(", ")}\n`);
2598
+ process.exit(1);
2552
2599
  }
2553
2600
  }
2554
2601
 
@@ -2582,8 +2629,8 @@ mon
2582
2629
 
2583
2630
  if (!grafanaPassword) {
2584
2631
  console.log("Generating secure Grafana password...");
2585
- const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
2586
- grafanaPassword = password.trim();
2632
+ const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
2633
+ grafanaPassword = password.trim().replace(/\n/g, "");
2587
2634
 
2588
2635
  let configContent = "";
2589
2636
  if (fs.existsSync(cfgPath)) {
@@ -2614,16 +2661,16 @@ mon
2614
2661
  const envContent = fs.readFileSync(envFile, "utf8");
2615
2662
  const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
2616
2663
  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, '');
2664
+ if (userMatch) vmAuthUsername = stripMatchingQuotes(userMatch[1]);
2665
+ if (passMatch) vmAuthPassword = stripMatchingQuotes(passMatch[1]);
2619
2666
  }
2620
2667
 
2621
2668
  if (!vmAuthUsername || !vmAuthPassword) {
2622
2669
  console.log("Generating VictoriaMetrics auth credentials...");
2623
2670
  vmAuthUsername = vmAuthUsername || "vmauth";
2624
2671
  if (!vmAuthPassword) {
2625
- const { stdout: vmPass } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
2626
- vmAuthPassword = vmPass.trim();
2672
+ const { stdout: vmPass } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
2673
+ vmAuthPassword = vmPass.trim().replace(/\n/g, "");
2627
2674
  }
2628
2675
 
2629
2676
  // Update .env file with VM auth credentials
@@ -2904,7 +2951,7 @@ mon
2904
2951
  console.log(`Project Directory: ${projectDir}`);
2905
2952
  console.log(`Docker Compose File: ${composeFile}`);
2906
2953
  console.log(`Instances File: ${instancesFile}`);
2907
- if (fs.existsSync(instancesFile) && !fs.statSync(instancesFile).isDirectory()) {
2954
+ if (fs.existsSync(instancesFile) && !fs.lstatSync(instancesFile).isDirectory()) {
2908
2955
  console.log("\nInstances configuration:\n");
2909
2956
  const text = fs.readFileSync(instancesFile, "utf8");
2910
2957
  process.stdout.write(text);
@@ -2935,16 +2982,16 @@ mon
2935
2982
 
2936
2983
  // Fetch latest changes
2937
2984
  console.log("Fetching latest changes...");
2938
- await execPromise("git fetch origin");
2985
+ await execFilePromise("git", ["fetch", "origin"]);
2939
2986
 
2940
2987
  // Check current branch
2941
- const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
2988
+ const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
2942
2989
  const currentBranch = branch.trim();
2943
2990
  console.log(`Current branch: ${currentBranch}`);
2944
2991
 
2945
2992
  // Pull latest changes
2946
2993
  console.log("Pulling latest changes...");
2947
- const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
2994
+ const { stdout: pullOut } = await execFilePromise("git", ["pull", "origin", currentBranch]);
2948
2995
  console.log(pullOut);
2949
2996
 
2950
2997
  // Update Docker images
@@ -3120,7 +3167,7 @@ targets
3120
3167
  .description("list monitoring target databases")
3121
3168
  .action(async () => {
3122
3169
  const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
3123
- if (!fs.existsSync(instancesPath) || fs.statSync(instancesPath).isDirectory()) {
3170
+ if (!fs.existsSync(instancesPath) || fs.lstatSync(instancesPath).isDirectory()) {
3124
3171
  console.error(`instances.yml not found in ${projectDir}`);
3125
3172
  process.exitCode = 1;
3126
3173
  return;
@@ -3186,7 +3233,7 @@ targets
3186
3233
 
3187
3234
  // Check if instance already exists
3188
3235
  try {
3189
- if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) {
3236
+ if (fs.existsSync(file) && !fs.lstatSync(file).isDirectory()) {
3190
3237
  const content = fs.readFileSync(file, "utf8");
3191
3238
  const instances = yaml.load(content) as Instance[] | null || [];
3192
3239
  if (Array.isArray(instances)) {
@@ -3200,9 +3247,10 @@ targets
3200
3247
  }
3201
3248
  } catch (err) {
3202
3249
  // If YAML parsing fails, fall back to simple check
3203
- const isFile = fs.existsSync(file) && !fs.statSync(file).isDirectory();
3250
+ const isFile = fs.existsSync(file) && !fs.lstatSync(file).isDirectory();
3204
3251
  const content = isFile ? fs.readFileSync(file, "utf8") : "";
3205
- if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
3252
+ const escapedName = instanceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3253
+ if (new RegExp(`^- name: ${escapedName}$`, "m").test(content)) {
3206
3254
  console.error(`Monitoring target '${instanceName}' already exists`);
3207
3255
  process.exitCode = 1;
3208
3256
  return;
@@ -3210,7 +3258,7 @@ targets
3210
3258
  }
3211
3259
 
3212
3260
  // Add new instance — if instances.yml is a directory (Docker artifact), replace it with a file
3213
- if (fs.existsSync(file) && fs.statSync(file).isDirectory()) {
3261
+ if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
3214
3262
  fs.rmSync(file, { recursive: true, force: true });
3215
3263
  }
3216
3264
  const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
@@ -3223,7 +3271,7 @@ targets
3223
3271
  .description("remove monitoring target database")
3224
3272
  .action(async (name: string) => {
3225
3273
  const { instancesFile: file } = await resolveOrInitPaths();
3226
- if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
3274
+ if (!fs.existsSync(file) || fs.lstatSync(file).isDirectory()) {
3227
3275
  console.error("instances.yml not found");
3228
3276
  process.exitCode = 1;
3229
3277
  return;
@@ -3260,7 +3308,7 @@ targets
3260
3308
  .description("test monitoring target database connectivity")
3261
3309
  .action(async (name: string) => {
3262
3310
  const { instancesFile: instancesPath } = await resolveOrInitPaths();
3263
- if (!fs.existsSync(instancesPath) || fs.statSync(instancesPath).isDirectory()) {
3311
+ if (!fs.existsSync(instancesPath) || fs.lstatSync(instancesPath).isDirectory()) {
3264
3312
  console.error("instances.yml not found");
3265
3313
  process.exitCode = 1;
3266
3314
  return;
@@ -3293,7 +3341,7 @@ targets
3293
3341
  console.log(`Testing connection to monitoring target '${name}'...`);
3294
3342
 
3295
3343
  // Use native pg client instead of requiring psql to be installed
3296
- const client = new Client({ connectionString: instance.conn_str });
3344
+ const client = new Client({ connectionString: instance.conn_str, connectionTimeoutMillis: 10000 });
3297
3345
 
3298
3346
  try {
3299
3347
  await client.connect();
@@ -3646,10 +3694,10 @@ mon
3646
3694
 
3647
3695
  try {
3648
3696
  // Generate secure password using openssl
3649
- const { stdout: password } = await execPromise(
3650
- "openssl rand -base64 12 | tr -d '\n'"
3697
+ const { stdout: password } = await execFilePromise(
3698
+ "openssl", ["rand", "-base64", "12"]
3651
3699
  );
3652
- const newPassword = password.trim();
3700
+ const newPassword = password.trim().replace(/\n/g, "");
3653
3701
 
3654
3702
  if (!newPassword) {
3655
3703
  console.error("Failed to generate password");
@@ -3736,8 +3784,8 @@ mon
3736
3784
  const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
3737
3785
  if (vmUser && vmPass) {
3738
3786
  console.log("\nVictoriaMetrics credentials:");
3739
- console.log(` Username: ${vmUser[1].trim().replace(/^["']|["']$/g, '')}`);
3740
- console.log(` Password: ${vmPass[1].trim().replace(/^["']|["']$/g, '')}`);
3787
+ console.log(` Username: ${stripMatchingQuotes(vmUser[1])}`);
3788
+ console.log(` Password: ${stripMatchingQuotes(vmPass[1])}`);
3741
3789
  }
3742
3790
  }
3743
3791
  console.log("");
@@ -4700,7 +4748,7 @@ mcp
4700
4748
  // Get the path to the current pgai executable
4701
4749
  let pgaiPath: string;
4702
4750
  try {
4703
- const execPath = await execPromise("which pgai");
4751
+ const execPath = await execFilePromise("which", ["pgai"]);
4704
4752
  pgaiPath = execPath.stdout.trim();
4705
4753
  } catch {
4706
4754
  // Fallback to just "pgai" if which fails
@@ -4712,8 +4760,8 @@ mcp
4712
4760
  console.log("Installing PostgresAI MCP server for Claude Code...");
4713
4761
 
4714
4762
  try {
4715
- const { stdout, stderr } = await execPromise(
4716
- `claude mcp add -s user postgresai ${pgaiPath} mcp start`
4763
+ const { stdout, stderr } = await execFilePromise(
4764
+ "claude", ["mcp", "add", "-s", "user", "postgresai", pgaiPath, "mcp", "start"]
4717
4765
  );
4718
4766
 
4719
4767
  if (stdout) console.log(stdout);
package/bun.lock CHANGED
@@ -6,7 +6,7 @@
6
6
  "name": "postgresai",
7
7
  "dependencies": {
8
8
  "@modelcontextprotocol/sdk": "^1.20.2",
9
- "commander": "^12.1.0",
9
+ "commander": "^14.0.3",
10
10
  "js-yaml": "^4.1.0",
11
11
  "pg": "^8.16.3",
12
12
  },
@@ -16,7 +16,7 @@
16
16
  "@types/pg": "^8.15.6",
17
17
  "ajv": "^8.17.1",
18
18
  "ajv-formats": "^3.0.1",
19
- "typescript": "^5.9.3",
19
+ "typescript": "^6.0.2",
20
20
  },
21
21
  },
22
22
  },
@@ -51,7 +51,7 @@
51
51
 
52
52
  "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
53
53
 
54
- "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
54
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
55
55
 
56
56
  "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
57
57
 
@@ -233,7 +233,7 @@
233
233
 
234
234
  "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
235
235
 
236
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
236
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
237
237
 
238
238
  "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
239
239