postgresai 0.15.0-rc.2 → 0.15.0-rc.3

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.
@@ -26,6 +26,17 @@ import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checku
26
26
  import { getCheckupEntry } from "../lib/checkup-dictionary";
27
27
  import { createCheckupReport, uploadCheckupReportJson, convertCheckupReportJsonToMarkdown, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
28
28
  import { generateCheckSummary } from "../lib/checkup-summary";
29
+ import {
30
+ type Instance,
31
+ InstancesParseError,
32
+ loadInstances,
33
+ buildInstance,
34
+ addInstanceToFile,
35
+ removeInstanceFromFile,
36
+ buildClientConfig,
37
+ sslOptionFromConnString,
38
+ warnIfLaxSslmode,
39
+ } from "../lib/instances";
29
40
 
30
41
  // Node.js version check - require Node 18+
31
42
  // Node 14 reached EOL in April 2023, Node 16 in September 2023.
@@ -419,19 +430,6 @@ interface ConfigResult {
419
430
  apiKey: string;
420
431
  }
421
432
 
422
- /**
423
- * Instance configuration
424
- */
425
- interface Instance {
426
- name: string;
427
- conn_str?: string;
428
- preset_metrics?: string;
429
- custom_metrics?: any;
430
- is_enabled?: boolean;
431
- group?: string;
432
- custom_tags?: Record<string, any>;
433
- }
434
-
435
433
  /**
436
434
  * Path resolution result
437
435
  */
@@ -2302,6 +2300,18 @@ const mon = program.command("mon").description("monitoring services management")
2302
2300
  mon
2303
2301
  .command("local-install")
2304
2302
  .description("install local monitoring stack (generate config, start services)")
2303
+ .addHelpText(
2304
+ "after",
2305
+ [
2306
+ "",
2307
+ "Networking:",
2308
+ " Compose enables IPv6 on the project's default network so containers can",
2309
+ " reach IPv6-only databases (e.g. Supabase free-tier db.<ref>.supabase.co).",
2310
+ " Override on hosts whose Docker daemon cannot create an IPv6 network:",
2311
+ " PGAI_ENABLE_IPV6=false (accepted: true|false|yes|no, lowercase)",
2312
+ "",
2313
+ ].join("\n"),
2314
+ )
2305
2315
  .option("--demo", "demo mode with sample database", false)
2306
2316
  .option("--api-key <key>", "Postgres AI API key for automated report uploads")
2307
2317
  .option("--db-url <url>", "PostgreSQL connection URL to monitor")
@@ -2487,8 +2497,7 @@ mon
2487
2497
  const db = m[5];
2488
2498
  const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
2489
2499
 
2490
- 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`;
2491
- fs.appendFileSync(instancesPath, body, "utf8");
2500
+ addInstanceToFile(instancesPath, buildInstance(instanceName, connStr));
2492
2501
  console.log(`✓ Monitoring target '${instanceName}' added\n`);
2493
2502
 
2494
2503
  // Test connection
@@ -2496,7 +2505,8 @@ mon
2496
2505
  {
2497
2506
  let testClient: InstanceType<typeof Client> | null = null;
2498
2507
  try {
2499
- testClient = new Client({ connectionString: connStr, connectionTimeoutMillis: 10000 });
2508
+ warnIfLaxSslmode(connStr);
2509
+ testClient = new Client(buildClientConfig(connStr, { connectionTimeoutMillis: 10000 }));
2500
2510
  await testClient.connect();
2501
2511
  const result = await testClient.query("select version();");
2502
2512
  console.log("✓ Connection successful");
@@ -2535,8 +2545,7 @@ mon
2535
2545
  const db = m[5];
2536
2546
  const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
2537
2547
 
2538
- 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`;
2539
- fs.appendFileSync(instancesPath, body, "utf8");
2548
+ addInstanceToFile(instancesPath, buildInstance(instanceName, connStr));
2540
2549
  console.log(`✓ Monitoring target '${instanceName}' added\n`);
2541
2550
 
2542
2551
  // Test connection
@@ -2544,7 +2553,8 @@ mon
2544
2553
  {
2545
2554
  let testClient: InstanceType<typeof Client> | null = null;
2546
2555
  try {
2547
- testClient = new Client({ connectionString: connStr, connectionTimeoutMillis: 10000 });
2556
+ warnIfLaxSslmode(connStr);
2557
+ testClient = new Client(buildClientConfig(connStr, { connectionTimeoutMillis: 10000 }));
2548
2558
  await testClient.connect();
2549
2559
  const result = await testClient.query("select version();");
2550
2560
  console.log("✓ Connection successful");
@@ -3173,42 +3183,32 @@ targets
3173
3183
  return;
3174
3184
  }
3175
3185
 
3186
+ let instances: Instance[];
3176
3187
  try {
3177
- const content = fs.readFileSync(instancesPath, "utf8");
3178
- const instances = yaml.load(content) as Instance[] | null;
3179
-
3180
- if (!instances || !Array.isArray(instances) || instances.length === 0) {
3181
- console.log("No monitoring targets configured");
3182
- console.log("");
3183
- console.log("To add a monitoring target:");
3184
- console.log(" postgres-ai mon targets add <connection-string> <name>");
3185
- console.log("");
3186
- console.log("Example:");
3187
- console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
3188
- return;
3189
- }
3190
-
3191
- // Filter out disabled instances (e.g., demo placeholders)
3192
- const filtered = instances.filter((inst) => inst.name && inst.is_enabled !== false);
3193
-
3194
- if (filtered.length === 0) {
3195
- console.log("No monitoring targets configured");
3196
- console.log("");
3197
- console.log("To add a monitoring target:");
3198
- console.log(" postgres-ai mon targets add <connection-string> <name>");
3199
- console.log("");
3200
- console.log("Example:");
3201
- console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
3202
- return;
3203
- }
3204
-
3205
- for (const inst of filtered) {
3206
- console.log(`Target: ${inst.name}`);
3207
- }
3188
+ instances = loadInstances(instancesPath);
3208
3189
  } catch (err) {
3209
3190
  const message = err instanceof Error ? err.message : String(err);
3210
3191
  console.error(`Error parsing instances.yml: ${message}`);
3211
3192
  process.exitCode = 1;
3193
+ return;
3194
+ }
3195
+
3196
+ // Filter out disabled instances (e.g., demo placeholders)
3197
+ const filtered = instances.filter((inst) => inst.name && inst.is_enabled !== false);
3198
+
3199
+ if (filtered.length === 0) {
3200
+ console.log("No monitoring targets configured");
3201
+ console.log("");
3202
+ console.log("To add a monitoring target:");
3203
+ console.log(" postgres-ai mon targets add <connection-string> <name>");
3204
+ console.log("");
3205
+ console.log("Example:");
3206
+ console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
3207
+ return;
3208
+ }
3209
+
3210
+ for (const inst of filtered) {
3211
+ console.log(`Target: ${inst.name}`);
3212
3212
  }
3213
3213
  });
3214
3214
  targets
@@ -3231,40 +3231,17 @@ targets
3231
3231
  const db = m[5];
3232
3232
  const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
3233
3233
 
3234
- // Check if instance already exists
3235
3234
  try {
3236
- if (fs.existsSync(file) && !fs.lstatSync(file).isDirectory()) {
3237
- const content = fs.readFileSync(file, "utf8");
3238
- const instances = yaml.load(content) as Instance[] | null || [];
3239
- if (Array.isArray(instances)) {
3240
- const exists = instances.some((inst) => inst.name === instanceName);
3241
- if (exists) {
3242
- console.error(`Monitoring target '${instanceName}' already exists`);
3243
- process.exitCode = 1;
3244
- return;
3245
- }
3246
- }
3247
- }
3235
+ addInstanceToFile(file, buildInstance(instanceName, connStr));
3236
+ console.log(`Monitoring target '${instanceName}' added`);
3248
3237
  } catch (err) {
3249
- // If YAML parsing fails, fall back to simple check
3250
- const isFile = fs.existsSync(file) && !fs.lstatSync(file).isDirectory();
3251
- const content = isFile ? fs.readFileSync(file, "utf8") : "";
3252
- const escapedName = instanceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3253
- if (new RegExp(`^- name: ${escapedName}$`, "m").test(content)) {
3254
- console.error(`Monitoring target '${instanceName}' already exists`);
3255
- process.exitCode = 1;
3256
- return;
3257
- }
3258
- }
3259
-
3260
- // Add new instance — if instances.yml is a directory (Docker artifact), replace it with a file
3261
- if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
3262
- fs.rmSync(file, { recursive: true, force: true });
3238
+ // Surface InstancesParseError as-is so we don't silently overwrite a
3239
+ // corrupted file (which could discard several targets, including the
3240
+ // credentials in their conn_str values).
3241
+ const message = err instanceof Error ? err.message : String(err);
3242
+ console.error(message);
3243
+ process.exitCode = 1;
3263
3244
  }
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`;
3265
- const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
3266
- fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
3267
- console.log(`Monitoring target '${instanceName}' added`);
3268
3245
  });
3269
3246
  targets
3270
3247
  .command("remove <name>")
@@ -3278,24 +3255,12 @@ targets
3278
3255
  }
3279
3256
 
3280
3257
  try {
3281
- const content = fs.readFileSync(file, "utf8");
3282
- const instances = yaml.load(content) as Instance[] | null;
3283
-
3284
- if (!instances || !Array.isArray(instances)) {
3285
- console.error("Invalid instances.yml format");
3286
- process.exitCode = 1;
3287
- return;
3288
- }
3289
-
3290
- const filtered = instances.filter((inst) => inst.name !== name);
3291
-
3292
- if (filtered.length === instances.length) {
3258
+ const removed = removeInstanceFromFile(file, name);
3259
+ if (!removed) {
3293
3260
  console.error(`Monitoring target '${name}' not found`);
3294
3261
  process.exitCode = 1;
3295
3262
  return;
3296
3263
  }
3297
-
3298
- fs.writeFileSync(file, yaml.dump(filtered), "utf8");
3299
3264
  console.log(`Monitoring target '${name}' removed`);
3300
3265
  } catch (err) {
3301
3266
  const message = err instanceof Error ? err.message : String(err);
@@ -3314,35 +3279,35 @@ targets
3314
3279
  return;
3315
3280
  }
3316
3281
 
3282
+ let instances: Instance[];
3317
3283
  try {
3318
- const content = fs.readFileSync(instancesPath, "utf8");
3319
- const instances = yaml.load(content) as Instance[] | null;
3320
-
3321
- if (!instances || !Array.isArray(instances)) {
3322
- console.error("Invalid instances.yml format");
3323
- process.exitCode = 1;
3324
- return;
3325
- }
3326
-
3327
- const instance = instances.find((inst) => inst.name === name);
3284
+ instances = loadInstances(instancesPath);
3285
+ } catch (err) {
3286
+ const message = err instanceof Error ? err.message : String(err);
3287
+ console.error(`Error parsing instances.yml: ${message}`);
3288
+ process.exitCode = 1;
3289
+ return;
3290
+ }
3291
+ const instance = instances.find((inst) => inst.name === name);
3328
3292
 
3329
- if (!instance) {
3330
- console.error(`Monitoring target '${name}' not found`);
3331
- process.exitCode = 1;
3332
- return;
3333
- }
3293
+ if (!instance) {
3294
+ console.error(`Monitoring target '${name}' not found`);
3295
+ process.exitCode = 1;
3296
+ return;
3297
+ }
3334
3298
 
3335
- if (!instance.conn_str) {
3336
- console.error(`Connection string not found for monitoring target '${name}'`);
3337
- process.exitCode = 1;
3338
- return;
3339
- }
3299
+ if (!instance.conn_str) {
3300
+ console.error(`Connection string not found for monitoring target '${name}'`);
3301
+ process.exitCode = 1;
3302
+ return;
3303
+ }
3340
3304
 
3341
- console.log(`Testing connection to monitoring target '${name}'...`);
3305
+ console.log(`Testing connection to monitoring target '${name}'...`);
3342
3306
 
3343
- // Use native pg client instead of requiring psql to be installed
3344
- const client = new Client({ connectionString: instance.conn_str, connectionTimeoutMillis: 10000 });
3307
+ warnIfLaxSslmode(instance.conn_str);
3308
+ const client = new Client(buildClientConfig(instance.conn_str, { connectionTimeoutMillis: 10000 }));
3345
3309
 
3310
+ try {
3346
3311
  try {
3347
3312
  await client.connect();
3348
3313
  const result = await client.query('select version();');
@@ -3376,17 +3341,17 @@ auth
3376
3341
  process.exitCode = 1;
3377
3342
  return;
3378
3343
  }
3379
-
3344
+
3380
3345
  // Read existing config to check for defaultProject before updating
3381
3346
  const existingConfig = config.readConfig();
3382
3347
  const existingProject = existingConfig.defaultProject;
3383
-
3348
+
3384
3349
  config.writeConfig({ apiKey: trimmedKey });
3385
3350
  // When API key is set directly, only clear orgId (org selection may differ).
3386
3351
  // Preserve defaultProject to avoid orphaning historical reports.
3387
3352
  // If the new key lacks access to the project, upload will fail with a clear error.
3388
3353
  config.deleteConfigKeys(["orgId"]);
3389
-
3354
+
3390
3355
  console.log(`API key saved to ${config.getConfigPath()}`);
3391
3356
  if (existingProject) {
3392
3357
  console.log(`Note: Your default project "${existingProject}" has been preserved.`);
@@ -3570,13 +3535,13 @@ auth
3570
3535
  const existingOrgId = existingConfig.orgId;
3571
3536
  const existingProject = existingConfig.defaultProject;
3572
3537
  const orgChanged = existingOrgId && existingOrgId !== orgId;
3573
-
3538
+
3574
3539
  config.writeConfig({
3575
3540
  apiKey: apiToken,
3576
3541
  baseUrl: apiBaseUrl,
3577
3542
  orgId: orgId,
3578
3543
  });
3579
-
3544
+
3580
3545
  // Only clear defaultProject if org actually changed
3581
3546
  if (orgChanged && existingProject) {
3582
3547
  config.deleteConfigKeys(["defaultProject"]);
@@ -4971,4 +4936,3 @@ mcp
4971
4936
  program.parseAsync(process.argv).finally(() => {
4972
4937
  closeReadline();
4973
4938
  });
4974
-