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.
- package/bin/postgres-ai.ts +88 -124
- package/dist/bin/postgres-ai.js +3260 -540
- package/lib/instances.ts +245 -0
- package/package.json +1 -1
- package/test/monitoring.test.ts +277 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3237
|
-
|
|
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
|
-
//
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
const
|
|
3253
|
-
|
|
3254
|
-
|
|
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
|
|
3282
|
-
|
|
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
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
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
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3293
|
+
if (!instance) {
|
|
3294
|
+
console.error(`Monitoring target '${name}' not found`);
|
|
3295
|
+
process.exitCode = 1;
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3334
3298
|
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
3305
|
+
console.log(`Testing connection to monitoring target '${name}'...`);
|
|
3342
3306
|
|
|
3343
|
-
|
|
3344
|
-
|
|
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
|
-
|