postgresai 0.12.0-beta.5 → 0.12.0-beta.7
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 +58 -2
- package/bin/postgres-ai.ts +619 -164
- package/dist/bin/postgres-ai.js +462 -33
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/issues.d.ts +69 -1
- package/dist/lib/issues.d.ts.map +1 -1
- package/dist/lib/issues.js +232 -1
- package/dist/lib/issues.js.map +1 -1
- package/dist/lib/mcp-server.d.ts.map +1 -1
- package/dist/lib/mcp-server.js +69 -15
- package/dist/lib/mcp-server.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/issues.ts +325 -3
- package/lib/mcp-server.ts +75 -17
- package/package.json +1 -1
package/bin/postgres-ai.ts
CHANGED
|
@@ -13,7 +13,7 @@ import * as readline from "readline";
|
|
|
13
13
|
import * as http from "https";
|
|
14
14
|
import { URL } from "url";
|
|
15
15
|
import { startMcpServer } from "../lib/mcp-server";
|
|
16
|
-
import { fetchIssues } from "../lib/issues";
|
|
16
|
+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
17
17
|
import { resolveBaseUrls } from "../lib/util";
|
|
18
18
|
|
|
19
19
|
const execPromise = promisify(exec);
|
|
@@ -59,14 +59,6 @@ interface PathResolution {
|
|
|
59
59
|
instancesFile: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Health check service
|
|
64
|
-
*/
|
|
65
|
-
interface HealthService {
|
|
66
|
-
name: string;
|
|
67
|
-
url: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
62
|
/**
|
|
71
63
|
* Get configuration from various sources
|
|
72
64
|
* @param opts - Command line options
|
|
@@ -90,6 +82,24 @@ function getConfig(opts: CliOptions): ConfigResult {
|
|
|
90
82
|
return { apiKey };
|
|
91
83
|
}
|
|
92
84
|
|
|
85
|
+
// Human-friendly output helper: YAML for TTY by default, JSON when --json or non-TTY
|
|
86
|
+
function printResult(result: unknown, json?: boolean): void {
|
|
87
|
+
if (typeof result === "string") {
|
|
88
|
+
process.stdout.write(result);
|
|
89
|
+
if (!/\n$/.test(result)) console.log();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (json || !process.stdout.isTTY) {
|
|
93
|
+
console.log(JSON.stringify(result, null, 2));
|
|
94
|
+
} else {
|
|
95
|
+
let text = yaml.dump(result as any);
|
|
96
|
+
if (Array.isArray(result)) {
|
|
97
|
+
text = text.replace(/\n- /g, "\n\n- ");
|
|
98
|
+
}
|
|
99
|
+
console.log(text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
const program = new Command();
|
|
94
104
|
|
|
95
105
|
program
|
|
@@ -172,7 +182,7 @@ function checkRunningContainers(): { running: boolean; containers: string[] } {
|
|
|
172
182
|
["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"],
|
|
173
183
|
{ stdio: "pipe", encoding: "utf8" }
|
|
174
184
|
);
|
|
175
|
-
|
|
185
|
+
|
|
176
186
|
if (result.status === 0 && result.stdout) {
|
|
177
187
|
const containers = result.stdout.trim().split("\n").filter(Boolean);
|
|
178
188
|
return { running: containers.length > 0, containers };
|
|
@@ -188,30 +198,53 @@ function checkRunningContainers(): { running: boolean; containers: string[] } {
|
|
|
188
198
|
*/
|
|
189
199
|
async function runCompose(args: string[]): Promise<number> {
|
|
190
200
|
let composeFile: string;
|
|
201
|
+
let projectDir: string;
|
|
191
202
|
try {
|
|
192
|
-
({ composeFile } = resolvePaths());
|
|
203
|
+
({ composeFile, projectDir } = resolvePaths());
|
|
193
204
|
} catch (error) {
|
|
194
205
|
const message = error instanceof Error ? error.message : String(error);
|
|
195
206
|
console.error(message);
|
|
196
207
|
process.exitCode = 1;
|
|
197
208
|
return 1;
|
|
198
209
|
}
|
|
199
|
-
|
|
210
|
+
|
|
200
211
|
// Check if Docker daemon is running
|
|
201
212
|
if (!isDockerRunning()) {
|
|
202
213
|
console.error("Docker is not running. Please start Docker and try again");
|
|
203
214
|
process.exitCode = 1;
|
|
204
215
|
return 1;
|
|
205
216
|
}
|
|
206
|
-
|
|
217
|
+
|
|
207
218
|
const cmd = getComposeCmd();
|
|
208
219
|
if (!cmd) {
|
|
209
220
|
console.error("docker compose not found (need docker-compose or docker compose)");
|
|
210
221
|
process.exitCode = 1;
|
|
211
222
|
return 1;
|
|
212
223
|
}
|
|
224
|
+
|
|
225
|
+
// Read Grafana password from .pgwatch-config and pass to Docker Compose
|
|
226
|
+
const env = { ...process.env };
|
|
227
|
+
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
228
|
+
if (fs.existsSync(cfgPath)) {
|
|
229
|
+
try {
|
|
230
|
+
const stats = fs.statSync(cfgPath);
|
|
231
|
+
if (!stats.isDirectory()) {
|
|
232
|
+
const content = fs.readFileSync(cfgPath, "utf8");
|
|
233
|
+
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
234
|
+
if (match) {
|
|
235
|
+
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// If we can't read the config, continue without setting the password
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
213
243
|
return new Promise<number>((resolve) => {
|
|
214
|
-
const child = spawn(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], {
|
|
244
|
+
const child = spawn(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], {
|
|
245
|
+
stdio: "inherit",
|
|
246
|
+
env: env
|
|
247
|
+
});
|
|
215
248
|
child.on("close", (code) => resolve(code || 0));
|
|
216
249
|
});
|
|
217
250
|
}
|
|
@@ -226,23 +259,308 @@ const mon = program.command("mon").description("monitoring services management")
|
|
|
226
259
|
mon
|
|
227
260
|
.command("quickstart")
|
|
228
261
|
.description("complete setup (generate config, start monitoring services)")
|
|
229
|
-
.option("--demo", "demo mode", false)
|
|
230
|
-
.
|
|
262
|
+
.option("--demo", "demo mode with sample database", false)
|
|
263
|
+
.option("--api-key <key>", "Postgres AI API key for automated report uploads")
|
|
264
|
+
.option("--db-url <url>", "PostgreSQL connection URL to monitor")
|
|
265
|
+
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
266
|
+
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; yes: boolean }) => {
|
|
267
|
+
console.log("\n=================================");
|
|
268
|
+
console.log(" PostgresAI Monitoring Quickstart");
|
|
269
|
+
console.log("=================================\n");
|
|
270
|
+
console.log("This will install, configure, and start the monitoring system\n");
|
|
271
|
+
|
|
272
|
+
// Validate conflicting options
|
|
273
|
+
if (opts.demo && opts.dbUrl) {
|
|
274
|
+
console.log("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
|
|
275
|
+
console.log("⚠ The --db-url will be ignored in demo mode.\n");
|
|
276
|
+
opts.dbUrl = undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (opts.demo && opts.apiKey) {
|
|
280
|
+
console.error("✗ Cannot use --api-key with --demo mode");
|
|
281
|
+
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
282
|
+
console.error("\nUse demo mode without API key: postgres-ai mon quickstart --demo");
|
|
283
|
+
console.error("Or use production mode with API key: postgres-ai mon quickstart --api-key=your_key");
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
231
288
|
// Check if containers are already running
|
|
232
289
|
const { running, containers } = checkRunningContainers();
|
|
233
290
|
if (running) {
|
|
234
|
-
console.log(
|
|
235
|
-
console.log("Use 'postgres-ai mon restart' to restart them");
|
|
291
|
+
console.log(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
|
|
292
|
+
console.log("Use 'postgres-ai mon restart' to restart them\n");
|
|
236
293
|
return;
|
|
237
294
|
}
|
|
238
|
-
|
|
295
|
+
|
|
296
|
+
// Step 1: API key configuration (only in production mode)
|
|
297
|
+
if (!opts.demo) {
|
|
298
|
+
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
299
|
+
console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
|
|
300
|
+
|
|
301
|
+
if (opts.apiKey) {
|
|
302
|
+
console.log("Using API key provided via --api-key parameter");
|
|
303
|
+
config.writeConfig({ apiKey: opts.apiKey });
|
|
304
|
+
console.log("✓ API key saved\n");
|
|
305
|
+
} else if (opts.yes) {
|
|
306
|
+
// Auto-yes mode without API key - skip API key setup
|
|
307
|
+
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
308
|
+
console.log("⚠ Reports will be generated locally only");
|
|
309
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
310
|
+
} else {
|
|
311
|
+
const rl = readline.createInterface({
|
|
312
|
+
input: process.stdin,
|
|
313
|
+
output: process.stdout
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const question = (prompt: string): Promise<string> =>
|
|
317
|
+
new Promise((resolve) => rl.question(prompt, resolve));
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
321
|
+
const proceedWithApiKey = !answer || answer.toLowerCase() === "y";
|
|
322
|
+
|
|
323
|
+
if (proceedWithApiKey) {
|
|
324
|
+
while (true) {
|
|
325
|
+
const inputApiKey = await question("Enter your Postgres AI API key: ");
|
|
326
|
+
const trimmedKey = inputApiKey.trim();
|
|
327
|
+
|
|
328
|
+
if (trimmedKey) {
|
|
329
|
+
config.writeConfig({ apiKey: trimmedKey });
|
|
330
|
+
console.log("✓ API key saved\n");
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log("⚠ API key cannot be empty");
|
|
335
|
+
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
336
|
+
if (retry.toLowerCase() === "n") {
|
|
337
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
338
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
344
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
rl.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
console.log("Step 1: Demo mode - API key configuration skipped");
|
|
352
|
+
console.log("Demo mode is for testing only and does not support API key integration\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Step 2: Add PostgreSQL instance (if not demo mode)
|
|
356
|
+
if (!opts.demo) {
|
|
357
|
+
console.log("Step 2: Add PostgreSQL Instance to Monitor\n");
|
|
358
|
+
|
|
359
|
+
// Clear instances.yml in production mode (start fresh)
|
|
360
|
+
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
361
|
+
const emptyInstancesContent = "# PostgreSQL instances to monitor\n# Add your instances using: postgres-ai mon targets add\n\n";
|
|
362
|
+
fs.writeFileSync(instancesPath, emptyInstancesContent, "utf8");
|
|
363
|
+
|
|
364
|
+
if (opts.dbUrl) {
|
|
365
|
+
console.log("Using database URL provided via --db-url parameter");
|
|
366
|
+
console.log(`Adding PostgreSQL instance from: ${opts.dbUrl}\n`);
|
|
367
|
+
|
|
368
|
+
const match = opts.dbUrl.match(/^postgresql:\/\/[^@]+@([^:/]+)/);
|
|
369
|
+
const autoInstanceName = match ? match[1] : "db-instance";
|
|
370
|
+
|
|
371
|
+
const connStr = opts.dbUrl;
|
|
372
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
373
|
+
|
|
374
|
+
if (!m) {
|
|
375
|
+
console.error("✗ Invalid connection string format");
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const host = m[3];
|
|
381
|
+
const db = m[5];
|
|
382
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
383
|
+
|
|
384
|
+
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`;
|
|
385
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
386
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
387
|
+
|
|
388
|
+
// Test connection
|
|
389
|
+
console.log("Testing connection to the added instance...");
|
|
390
|
+
try {
|
|
391
|
+
const { Client } = require("pg");
|
|
392
|
+
const client = new Client({ connectionString: connStr });
|
|
393
|
+
await client.connect();
|
|
394
|
+
const result = await client.query("select version();");
|
|
395
|
+
console.log("✓ Connection successful");
|
|
396
|
+
console.log(`${result.rows[0].version}\n`);
|
|
397
|
+
await client.end();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
401
|
+
}
|
|
402
|
+
} else if (opts.yes) {
|
|
403
|
+
// Auto-yes mode without database URL - skip database setup
|
|
404
|
+
console.log("Auto-yes mode: no database URL provided, skipping database setup");
|
|
405
|
+
console.log("⚠ No PostgreSQL instance added");
|
|
406
|
+
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
407
|
+
} else {
|
|
408
|
+
const rl = readline.createInterface({
|
|
409
|
+
input: process.stdin,
|
|
410
|
+
output: process.stdout
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const question = (prompt: string): Promise<string> =>
|
|
414
|
+
new Promise((resolve) => rl.question(prompt, resolve));
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
418
|
+
const answer = await question("Do you want to add a PostgreSQL instance now? (Y/n): ");
|
|
419
|
+
const proceedWithInstance = !answer || answer.toLowerCase() === "y";
|
|
420
|
+
|
|
421
|
+
if (proceedWithInstance) {
|
|
422
|
+
console.log("\nYou can provide either:");
|
|
423
|
+
console.log(" 1. A full connection string: postgresql://user:pass@host:port/database");
|
|
424
|
+
console.log(" 2. Press Enter to skip for now\n");
|
|
425
|
+
|
|
426
|
+
const connStr = await question("Enter connection string (or press Enter to skip): ");
|
|
427
|
+
|
|
428
|
+
if (connStr.trim()) {
|
|
429
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
430
|
+
if (!m) {
|
|
431
|
+
console.error("✗ Invalid connection string format");
|
|
432
|
+
console.log("⚠ Continuing without adding instance\n");
|
|
433
|
+
} else {
|
|
434
|
+
const host = m[3];
|
|
435
|
+
const db = m[5];
|
|
436
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
437
|
+
|
|
438
|
+
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`;
|
|
439
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
440
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
441
|
+
|
|
442
|
+
// Test connection
|
|
443
|
+
console.log("Testing connection to the added instance...");
|
|
444
|
+
try {
|
|
445
|
+
const { Client } = require("pg");
|
|
446
|
+
const client = new Client({ connectionString: connStr });
|
|
447
|
+
await client.connect();
|
|
448
|
+
const result = await client.query("select version();");
|
|
449
|
+
console.log("✓ Connection successful");
|
|
450
|
+
console.log(`${result.rows[0].version}\n`);
|
|
451
|
+
await client.end();
|
|
452
|
+
} catch (error) {
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
rl.close();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database\n");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Step 3: Update configuration
|
|
472
|
+
console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
|
|
239
473
|
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
240
474
|
if (code1 !== 0) {
|
|
241
475
|
process.exitCode = code1;
|
|
242
476
|
return;
|
|
243
477
|
}
|
|
244
|
-
|
|
245
|
-
|
|
478
|
+
console.log("✓ Configuration updated\n");
|
|
479
|
+
|
|
480
|
+
// Step 4: Ensure Grafana password is configured
|
|
481
|
+
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
482
|
+
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
483
|
+
let grafanaPassword = "";
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
if (fs.existsSync(cfgPath)) {
|
|
487
|
+
const stats = fs.statSync(cfgPath);
|
|
488
|
+
if (!stats.isDirectory()) {
|
|
489
|
+
const content = fs.readFileSync(cfgPath, "utf8");
|
|
490
|
+
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
491
|
+
if (match) {
|
|
492
|
+
grafanaPassword = match[1].trim();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!grafanaPassword) {
|
|
498
|
+
console.log("Generating secure Grafana password...");
|
|
499
|
+
const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
|
|
500
|
+
grafanaPassword = password.trim();
|
|
501
|
+
|
|
502
|
+
let configContent = "";
|
|
503
|
+
if (fs.existsSync(cfgPath)) {
|
|
504
|
+
const stats = fs.statSync(cfgPath);
|
|
505
|
+
if (!stats.isDirectory()) {
|
|
506
|
+
configContent = fs.readFileSync(cfgPath, "utf8");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
511
|
+
lines.push(`grafana_password=${grafanaPassword}`);
|
|
512
|
+
fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log("✓ Grafana password configured\n");
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.log("⚠ Could not generate Grafana password automatically");
|
|
518
|
+
console.log("Using default password: demo\n");
|
|
519
|
+
grafanaPassword = "demo";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Step 5: Start services
|
|
523
|
+
console.log(opts.demo ? "Step 5: Starting monitoring services..." : "Step 5: Starting monitoring services...");
|
|
524
|
+
const code2 = await runCompose(["up", "-d", "--force-recreate"]);
|
|
525
|
+
if (code2 !== 0) {
|
|
526
|
+
process.exitCode = code2;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
console.log("✓ Services started\n");
|
|
530
|
+
|
|
531
|
+
// Final summary
|
|
532
|
+
console.log("=================================");
|
|
533
|
+
console.log(" 🎉 Quickstart setup completed!");
|
|
534
|
+
console.log("=================================\n");
|
|
535
|
+
|
|
536
|
+
console.log("What's running:");
|
|
537
|
+
if (opts.demo) {
|
|
538
|
+
console.log(" ✅ Demo PostgreSQL database (monitoring target)");
|
|
539
|
+
}
|
|
540
|
+
console.log(" ✅ PostgreSQL monitoring infrastructure");
|
|
541
|
+
console.log(" ✅ Grafana dashboards (with secure password)");
|
|
542
|
+
console.log(" ✅ Prometheus metrics storage");
|
|
543
|
+
console.log(" ✅ Flask API backend");
|
|
544
|
+
console.log(" ✅ Automated report generation (every 24h)");
|
|
545
|
+
console.log(" ✅ Host stats monitoring (CPU, memory, disk, I/O)\n");
|
|
546
|
+
|
|
547
|
+
if (!opts.demo) {
|
|
548
|
+
console.log("Next steps:");
|
|
549
|
+
console.log(" • Add more PostgreSQL instances: postgres-ai mon targets add");
|
|
550
|
+
console.log(" • View configured instances: postgres-ai mon targets list");
|
|
551
|
+
console.log(" • Check service health: postgres-ai mon health\n");
|
|
552
|
+
} else {
|
|
553
|
+
console.log("Demo mode next steps:");
|
|
554
|
+
console.log(" • Explore Grafana dashboards at http://localhost:3000");
|
|
555
|
+
console.log(" • Connect to demo database: postgresql://postgres:postgres@localhost:55432/target_database");
|
|
556
|
+
console.log(" • Generate some load on the demo database to see metrics\n");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
560
|
+
console.log("🚀 MAIN ACCESS POINT - Start here:");
|
|
561
|
+
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
562
|
+
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
563
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
246
564
|
});
|
|
247
565
|
|
|
248
566
|
mon
|
|
@@ -256,7 +574,7 @@ mon
|
|
|
256
574
|
console.log("Use 'postgres-ai mon restart' to restart them");
|
|
257
575
|
return;
|
|
258
576
|
}
|
|
259
|
-
|
|
577
|
+
|
|
260
578
|
const code = await runCompose(["up", "-d"]);
|
|
261
579
|
if (code !== 0) process.exitCode = code;
|
|
262
580
|
});
|
|
@@ -305,42 +623,40 @@ mon
|
|
|
305
623
|
.description("health check for monitoring services")
|
|
306
624
|
.option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
|
|
307
625
|
.action(async (opts: { wait: number }) => {
|
|
308
|
-
const services
|
|
309
|
-
{ name: "Grafana",
|
|
310
|
-
{ name: "Prometheus",
|
|
311
|
-
{ name: "PGWatch (Postgres)",
|
|
312
|
-
{ name: "PGWatch (Prometheus)",
|
|
626
|
+
const services = [
|
|
627
|
+
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
628
|
+
{ name: "Prometheus", container: "sink-prometheus" },
|
|
629
|
+
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
630
|
+
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
631
|
+
{ name: "Target DB", container: "target-db" },
|
|
632
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
313
633
|
];
|
|
314
|
-
|
|
634
|
+
|
|
315
635
|
const waitTime = opts.wait || 0;
|
|
316
636
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
317
|
-
|
|
637
|
+
|
|
318
638
|
console.log("Checking service health...\n");
|
|
319
|
-
|
|
639
|
+
|
|
320
640
|
let allHealthy = false;
|
|
321
641
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
322
642
|
if (attempt > 1) {
|
|
323
643
|
console.log(`Retrying (attempt ${attempt}/${maxAttempts})...\n`);
|
|
324
644
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
325
645
|
}
|
|
326
|
-
|
|
646
|
+
|
|
327
647
|
allHealthy = true;
|
|
328
648
|
for (const service of services) {
|
|
329
649
|
try {
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
});
|
|
338
|
-
clearTimeout(timeoutId);
|
|
339
|
-
|
|
340
|
-
if (response.status === 200) {
|
|
650
|
+
const { execSync } = require("child_process");
|
|
651
|
+
const status = execSync(`docker inspect -f '{{.State.Status}}' ${service.container} 2>/dev/null`, {
|
|
652
|
+
encoding: 'utf8',
|
|
653
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
654
|
+
}).trim();
|
|
655
|
+
|
|
656
|
+
if (status === 'running') {
|
|
341
657
|
console.log(`✓ ${service.name}: healthy`);
|
|
342
658
|
} else {
|
|
343
|
-
console.log(`✗ ${service.name}: unhealthy (
|
|
659
|
+
console.log(`✗ ${service.name}: unhealthy (status: ${status})`);
|
|
344
660
|
allHealthy = false;
|
|
345
661
|
}
|
|
346
662
|
} catch (error) {
|
|
@@ -348,12 +664,12 @@ mon
|
|
|
348
664
|
allHealthy = false;
|
|
349
665
|
}
|
|
350
666
|
}
|
|
351
|
-
|
|
667
|
+
|
|
352
668
|
if (allHealthy) {
|
|
353
669
|
break;
|
|
354
670
|
}
|
|
355
671
|
}
|
|
356
|
-
|
|
672
|
+
|
|
357
673
|
console.log("");
|
|
358
674
|
if (allHealthy) {
|
|
359
675
|
console.log("All services are healthy");
|
|
@@ -399,7 +715,7 @@ mon
|
|
|
399
715
|
.description("update monitoring stack")
|
|
400
716
|
.action(async () => {
|
|
401
717
|
console.log("Updating PostgresAI monitoring stack...\n");
|
|
402
|
-
|
|
718
|
+
|
|
403
719
|
try {
|
|
404
720
|
// Check if we're in a git repo
|
|
405
721
|
const gitDir = path.resolve(process.cwd(), ".git");
|
|
@@ -408,25 +724,25 @@ mon
|
|
|
408
724
|
process.exitCode = 1;
|
|
409
725
|
return;
|
|
410
726
|
}
|
|
411
|
-
|
|
727
|
+
|
|
412
728
|
// Fetch latest changes
|
|
413
729
|
console.log("Fetching latest changes...");
|
|
414
730
|
await execPromise("git fetch origin");
|
|
415
|
-
|
|
731
|
+
|
|
416
732
|
// Check current branch
|
|
417
733
|
const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
|
|
418
734
|
const currentBranch = branch.trim();
|
|
419
735
|
console.log(`Current branch: ${currentBranch}`);
|
|
420
|
-
|
|
736
|
+
|
|
421
737
|
// Pull latest changes
|
|
422
738
|
console.log("Pulling latest changes...");
|
|
423
739
|
const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
|
|
424
740
|
console.log(pullOut);
|
|
425
|
-
|
|
741
|
+
|
|
426
742
|
// Update Docker images
|
|
427
743
|
console.log("\nUpdating Docker images...");
|
|
428
744
|
const code = await runCompose(["pull"]);
|
|
429
|
-
|
|
745
|
+
|
|
430
746
|
if (code === 0) {
|
|
431
747
|
console.log("\n✓ Update completed successfully");
|
|
432
748
|
console.log("\nTo apply updates, restart monitoring services:");
|
|
@@ -449,32 +765,32 @@ mon
|
|
|
449
765
|
input: process.stdin,
|
|
450
766
|
output: process.stdout,
|
|
451
767
|
});
|
|
452
|
-
|
|
768
|
+
|
|
453
769
|
const question = (prompt: string): Promise<string> =>
|
|
454
770
|
new Promise((resolve) => rl.question(prompt, resolve));
|
|
455
|
-
|
|
771
|
+
|
|
456
772
|
try {
|
|
457
773
|
if (service) {
|
|
458
774
|
// Reset specific service
|
|
459
775
|
console.log(`\nThis will stop '${service}', remove its volume, and restart it.`);
|
|
460
776
|
console.log("All data for this service will be lost!\n");
|
|
461
|
-
|
|
777
|
+
|
|
462
778
|
const answer = await question("Continue? (y/N): ");
|
|
463
779
|
if (answer.toLowerCase() !== "y") {
|
|
464
780
|
console.log("Cancelled");
|
|
465
781
|
rl.close();
|
|
466
782
|
return;
|
|
467
783
|
}
|
|
468
|
-
|
|
784
|
+
|
|
469
785
|
console.log(`\nStopping ${service}...`);
|
|
470
786
|
await runCompose(["stop", service]);
|
|
471
|
-
|
|
787
|
+
|
|
472
788
|
console.log(`Removing volume for ${service}...`);
|
|
473
789
|
await runCompose(["rm", "-f", "-v", service]);
|
|
474
|
-
|
|
790
|
+
|
|
475
791
|
console.log(`Restarting ${service}...`);
|
|
476
792
|
const code = await runCompose(["up", "-d", service]);
|
|
477
|
-
|
|
793
|
+
|
|
478
794
|
if (code === 0) {
|
|
479
795
|
console.log(`\n✓ Service '${service}' has been reset`);
|
|
480
796
|
} else {
|
|
@@ -485,17 +801,17 @@ mon
|
|
|
485
801
|
// Reset all services
|
|
486
802
|
console.log("\nThis will stop all services and remove all data!");
|
|
487
803
|
console.log("Volumes, networks, and containers will be deleted.\n");
|
|
488
|
-
|
|
804
|
+
|
|
489
805
|
const answer = await question("Continue? (y/N): ");
|
|
490
806
|
if (answer.toLowerCase() !== "y") {
|
|
491
807
|
console.log("Cancelled");
|
|
492
808
|
rl.close();
|
|
493
809
|
return;
|
|
494
810
|
}
|
|
495
|
-
|
|
811
|
+
|
|
496
812
|
console.log("\nStopping services and removing data...");
|
|
497
813
|
const downCode = await runCompose(["down", "-v"]);
|
|
498
|
-
|
|
814
|
+
|
|
499
815
|
if (downCode === 0) {
|
|
500
816
|
console.log("✓ Environment reset completed - all containers and data removed");
|
|
501
817
|
} else {
|
|
@@ -503,7 +819,7 @@ mon
|
|
|
503
819
|
process.exitCode = 1;
|
|
504
820
|
}
|
|
505
821
|
}
|
|
506
|
-
|
|
822
|
+
|
|
507
823
|
rl.close();
|
|
508
824
|
} catch (error) {
|
|
509
825
|
rl.close();
|
|
@@ -517,7 +833,7 @@ mon
|
|
|
517
833
|
.description("cleanup monitoring services artifacts")
|
|
518
834
|
.action(async () => {
|
|
519
835
|
console.log("Cleaning up Docker resources...\n");
|
|
520
|
-
|
|
836
|
+
|
|
521
837
|
try {
|
|
522
838
|
// Remove stopped containers
|
|
523
839
|
const { stdout: containers } = await execFilePromise("docker", ["ps", "-aq", "--filter", "status=exited"]);
|
|
@@ -528,19 +844,19 @@ mon
|
|
|
528
844
|
} else {
|
|
529
845
|
console.log("✓ No stopped containers to remove");
|
|
530
846
|
}
|
|
531
|
-
|
|
847
|
+
|
|
532
848
|
// Remove unused volumes
|
|
533
849
|
await execFilePromise("docker", ["volume", "prune", "-f"]);
|
|
534
850
|
console.log("✓ Removed unused volumes");
|
|
535
|
-
|
|
851
|
+
|
|
536
852
|
// Remove unused networks
|
|
537
853
|
await execFilePromise("docker", ["network", "prune", "-f"]);
|
|
538
854
|
console.log("✓ Removed unused networks");
|
|
539
|
-
|
|
855
|
+
|
|
540
856
|
// Remove dangling images
|
|
541
857
|
await execFilePromise("docker", ["image", "prune", "-f"]);
|
|
542
858
|
console.log("✓ Removed dangling images");
|
|
543
|
-
|
|
859
|
+
|
|
544
860
|
console.log("\nCleanup completed");
|
|
545
861
|
} catch (error) {
|
|
546
862
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -576,11 +892,11 @@ targets
|
|
|
576
892
|
process.exitCode = 1;
|
|
577
893
|
return;
|
|
578
894
|
}
|
|
579
|
-
|
|
895
|
+
|
|
580
896
|
try {
|
|
581
897
|
const content = fs.readFileSync(instancesPath, "utf8");
|
|
582
898
|
const instances = yaml.load(content) as Instance[] | null;
|
|
583
|
-
|
|
899
|
+
|
|
584
900
|
if (!instances || !Array.isArray(instances) || instances.length === 0) {
|
|
585
901
|
console.log("No monitoring targets configured");
|
|
586
902
|
console.log("");
|
|
@@ -591,10 +907,10 @@ targets
|
|
|
591
907
|
console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
|
|
592
908
|
return;
|
|
593
909
|
}
|
|
594
|
-
|
|
910
|
+
|
|
595
911
|
// Filter out disabled instances (e.g., demo placeholders)
|
|
596
912
|
const filtered = instances.filter((inst) => inst.name && inst.is_enabled !== false);
|
|
597
|
-
|
|
913
|
+
|
|
598
914
|
if (filtered.length === 0) {
|
|
599
915
|
console.log("No monitoring targets configured");
|
|
600
916
|
console.log("");
|
|
@@ -605,7 +921,7 @@ targets
|
|
|
605
921
|
console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
|
|
606
922
|
return;
|
|
607
923
|
}
|
|
608
|
-
|
|
924
|
+
|
|
609
925
|
for (const inst of filtered) {
|
|
610
926
|
console.log(`Target: ${inst.name}`);
|
|
611
927
|
}
|
|
@@ -634,7 +950,7 @@ targets
|
|
|
634
950
|
const host = m[3];
|
|
635
951
|
const db = m[5];
|
|
636
952
|
const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
637
|
-
|
|
953
|
+
|
|
638
954
|
// Check if instance already exists
|
|
639
955
|
try {
|
|
640
956
|
if (fs.existsSync(file)) {
|
|
@@ -658,7 +974,7 @@ targets
|
|
|
658
974
|
return;
|
|
659
975
|
}
|
|
660
976
|
}
|
|
661
|
-
|
|
977
|
+
|
|
662
978
|
// Add new instance
|
|
663
979
|
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`;
|
|
664
980
|
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
@@ -675,25 +991,25 @@ targets
|
|
|
675
991
|
process.exitCode = 1;
|
|
676
992
|
return;
|
|
677
993
|
}
|
|
678
|
-
|
|
994
|
+
|
|
679
995
|
try {
|
|
680
996
|
const content = fs.readFileSync(file, "utf8");
|
|
681
997
|
const instances = yaml.load(content) as Instance[] | null;
|
|
682
|
-
|
|
998
|
+
|
|
683
999
|
if (!instances || !Array.isArray(instances)) {
|
|
684
1000
|
console.error("Invalid instances.yml format");
|
|
685
1001
|
process.exitCode = 1;
|
|
686
1002
|
return;
|
|
687
1003
|
}
|
|
688
|
-
|
|
1004
|
+
|
|
689
1005
|
const filtered = instances.filter((inst) => inst.name !== name);
|
|
690
|
-
|
|
1006
|
+
|
|
691
1007
|
if (filtered.length === instances.length) {
|
|
692
1008
|
console.error(`Monitoring target '${name}' not found`);
|
|
693
1009
|
process.exitCode = 1;
|
|
694
1010
|
return;
|
|
695
1011
|
}
|
|
696
|
-
|
|
1012
|
+
|
|
697
1013
|
fs.writeFileSync(file, yaml.dump(filtered), "utf8");
|
|
698
1014
|
console.log(`Monitoring target '${name}' removed`);
|
|
699
1015
|
} catch (err) {
|
|
@@ -712,37 +1028,37 @@ targets
|
|
|
712
1028
|
process.exitCode = 1;
|
|
713
1029
|
return;
|
|
714
1030
|
}
|
|
715
|
-
|
|
1031
|
+
|
|
716
1032
|
try {
|
|
717
1033
|
const content = fs.readFileSync(instancesPath, "utf8");
|
|
718
1034
|
const instances = yaml.load(content) as Instance[] | null;
|
|
719
|
-
|
|
1035
|
+
|
|
720
1036
|
if (!instances || !Array.isArray(instances)) {
|
|
721
1037
|
console.error("Invalid instances.yml format");
|
|
722
1038
|
process.exitCode = 1;
|
|
723
1039
|
return;
|
|
724
1040
|
}
|
|
725
|
-
|
|
1041
|
+
|
|
726
1042
|
const instance = instances.find((inst) => inst.name === name);
|
|
727
|
-
|
|
1043
|
+
|
|
728
1044
|
if (!instance) {
|
|
729
1045
|
console.error(`Monitoring target '${name}' not found`);
|
|
730
1046
|
process.exitCode = 1;
|
|
731
1047
|
return;
|
|
732
1048
|
}
|
|
733
|
-
|
|
1049
|
+
|
|
734
1050
|
if (!instance.conn_str) {
|
|
735
1051
|
console.error(`Connection string not found for monitoring target '${name}'`);
|
|
736
1052
|
process.exitCode = 1;
|
|
737
1053
|
return;
|
|
738
1054
|
}
|
|
739
|
-
|
|
1055
|
+
|
|
740
1056
|
console.log(`Testing connection to monitoring target '${name}'...`);
|
|
741
|
-
|
|
1057
|
+
|
|
742
1058
|
// Use native pg client instead of requiring psql to be installed
|
|
743
1059
|
const { Client } = require('pg');
|
|
744
1060
|
const client = new Client({ connectionString: instance.conn_str });
|
|
745
|
-
|
|
1061
|
+
|
|
746
1062
|
try {
|
|
747
1063
|
await client.connect();
|
|
748
1064
|
const result = await client.query('select version();');
|
|
@@ -767,34 +1083,34 @@ program
|
|
|
767
1083
|
.action(async (opts: { port?: number; debug?: boolean }) => {
|
|
768
1084
|
const pkce = require("../lib/pkce");
|
|
769
1085
|
const authServer = require("../lib/auth-server");
|
|
770
|
-
|
|
1086
|
+
|
|
771
1087
|
console.log("Starting authentication flow...\n");
|
|
772
|
-
|
|
1088
|
+
|
|
773
1089
|
// Generate PKCE parameters
|
|
774
1090
|
const params = pkce.generatePKCEParams();
|
|
775
1091
|
|
|
776
1092
|
const rootOpts = program.opts<CliOptions>();
|
|
777
1093
|
const cfg = config.readConfig();
|
|
778
1094
|
const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
779
|
-
|
|
1095
|
+
|
|
780
1096
|
if (opts.debug) {
|
|
781
1097
|
console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
|
|
782
1098
|
console.log(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
|
|
783
1099
|
}
|
|
784
|
-
|
|
1100
|
+
|
|
785
1101
|
try {
|
|
786
1102
|
// Step 1: Start local callback server FIRST to get actual port
|
|
787
1103
|
console.log("Starting local callback server...");
|
|
788
1104
|
const requestedPort = opts.port || 0; // 0 = OS assigns available port
|
|
789
1105
|
const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
|
|
790
|
-
|
|
1106
|
+
|
|
791
1107
|
// Wait a bit for server to start and get port
|
|
792
1108
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
793
1109
|
const actualPort = callbackServer.getPort();
|
|
794
1110
|
const redirectUri = `http://localhost:${actualPort}/callback`;
|
|
795
|
-
|
|
1111
|
+
|
|
796
1112
|
console.log(`Callback server listening on port ${actualPort}`);
|
|
797
|
-
|
|
1113
|
+
|
|
798
1114
|
// Step 2: Initialize OAuth session on backend
|
|
799
1115
|
console.log("Initializing authentication session...");
|
|
800
1116
|
const initData = JSON.stringify({
|
|
@@ -804,15 +1120,15 @@ program
|
|
|
804
1120
|
code_challenge_method: params.codeChallengeMethod,
|
|
805
1121
|
redirect_uri: redirectUri,
|
|
806
1122
|
});
|
|
807
|
-
|
|
1123
|
+
|
|
808
1124
|
// Build init URL by appending to the API base path (keep /api/general)
|
|
809
1125
|
const initUrl = new URL(`${apiBaseUrl}/rpc/oauth_init`);
|
|
810
|
-
|
|
1126
|
+
|
|
811
1127
|
if (opts.debug) {
|
|
812
1128
|
console.log(`Debug: Trying to POST to: ${initUrl.toString()}`);
|
|
813
1129
|
console.log(`Debug: Request data: ${initData}`);
|
|
814
1130
|
}
|
|
815
|
-
|
|
1131
|
+
|
|
816
1132
|
const initReq = http.request(
|
|
817
1133
|
initUrl,
|
|
818
1134
|
{
|
|
@@ -828,7 +1144,7 @@ program
|
|
|
828
1144
|
res.on("end", async () => {
|
|
829
1145
|
if (res.statusCode !== 200) {
|
|
830
1146
|
console.error(`Failed to initialize auth session: ${res.statusCode}`);
|
|
831
|
-
|
|
1147
|
+
|
|
832
1148
|
// Check if response is HTML (common for 404 pages)
|
|
833
1149
|
if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
|
|
834
1150
|
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
@@ -839,31 +1155,31 @@ program
|
|
|
839
1155
|
} else {
|
|
840
1156
|
console.error(data);
|
|
841
1157
|
}
|
|
842
|
-
|
|
1158
|
+
|
|
843
1159
|
callbackServer.server.close();
|
|
844
1160
|
process.exit(1);
|
|
845
1161
|
}
|
|
846
|
-
|
|
1162
|
+
|
|
847
1163
|
// Step 3: Open browser
|
|
848
1164
|
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
849
|
-
|
|
1165
|
+
|
|
850
1166
|
if (opts.debug) {
|
|
851
1167
|
console.log(`Debug: Auth URL: ${authUrl}`);
|
|
852
1168
|
}
|
|
853
|
-
|
|
1169
|
+
|
|
854
1170
|
console.log(`\nOpening browser for authentication...`);
|
|
855
1171
|
console.log(`If browser does not open automatically, visit:\n${authUrl}\n`);
|
|
856
|
-
|
|
1172
|
+
|
|
857
1173
|
// Open browser (cross-platform)
|
|
858
1174
|
const openCommand = process.platform === "darwin" ? "open" :
|
|
859
1175
|
process.platform === "win32" ? "start" :
|
|
860
1176
|
"xdg-open";
|
|
861
1177
|
spawn(openCommand, [authUrl], { detached: true, stdio: "ignore" }).unref();
|
|
862
|
-
|
|
1178
|
+
|
|
863
1179
|
// Step 4: Wait for callback
|
|
864
1180
|
console.log("Waiting for authorization...");
|
|
865
1181
|
console.log("(Press Ctrl+C to cancel)\n");
|
|
866
|
-
|
|
1182
|
+
|
|
867
1183
|
// Handle Ctrl+C gracefully
|
|
868
1184
|
const cancelHandler = () => {
|
|
869
1185
|
console.log("\n\nAuthentication cancelled by user.");
|
|
@@ -871,13 +1187,13 @@ program
|
|
|
871
1187
|
process.exit(130); // Standard exit code for SIGINT
|
|
872
1188
|
};
|
|
873
1189
|
process.on("SIGINT", cancelHandler);
|
|
874
|
-
|
|
1190
|
+
|
|
875
1191
|
try {
|
|
876
1192
|
const { code } = await callbackServer.promise;
|
|
877
|
-
|
|
1193
|
+
|
|
878
1194
|
// Remove the cancel handler after successful auth
|
|
879
1195
|
process.off("SIGINT", cancelHandler);
|
|
880
|
-
|
|
1196
|
+
|
|
881
1197
|
// Step 5: Exchange code for token
|
|
882
1198
|
console.log("\nExchanging authorization code for API token...");
|
|
883
1199
|
const exchangeData = JSON.stringify({
|
|
@@ -901,7 +1217,7 @@ program
|
|
|
901
1217
|
exchangeRes.on("end", () => {
|
|
902
1218
|
if (exchangeRes.statusCode !== 200) {
|
|
903
1219
|
console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
|
|
904
|
-
|
|
1220
|
+
|
|
905
1221
|
// Check if response is HTML (common for 404 pages)
|
|
906
1222
|
if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
|
|
907
1223
|
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
@@ -912,23 +1228,23 @@ program
|
|
|
912
1228
|
} else {
|
|
913
1229
|
console.error(exchangeBody);
|
|
914
1230
|
}
|
|
915
|
-
|
|
1231
|
+
|
|
916
1232
|
process.exit(1);
|
|
917
1233
|
return;
|
|
918
1234
|
}
|
|
919
|
-
|
|
1235
|
+
|
|
920
1236
|
try {
|
|
921
1237
|
const result = JSON.parse(exchangeBody);
|
|
922
1238
|
const apiToken = result.api_token || result?.[0]?.result?.api_token; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
923
1239
|
const orgId = result.org_id || result?.[0]?.result?.org_id; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
924
|
-
|
|
1240
|
+
|
|
925
1241
|
// Step 6: Save token to config
|
|
926
1242
|
config.writeConfig({
|
|
927
1243
|
apiKey: apiToken,
|
|
928
1244
|
baseUrl: apiBaseUrl,
|
|
929
1245
|
orgId: orgId,
|
|
930
1246
|
});
|
|
931
|
-
|
|
1247
|
+
|
|
932
1248
|
console.log("\nAuthentication successful!");
|
|
933
1249
|
console.log(`API key saved to: ${config.getConfigPath()}`);
|
|
934
1250
|
console.log(`Organization ID: ${orgId}`);
|
|
@@ -942,21 +1258,21 @@ program
|
|
|
942
1258
|
});
|
|
943
1259
|
}
|
|
944
1260
|
);
|
|
945
|
-
|
|
1261
|
+
|
|
946
1262
|
exchangeReq.on("error", (err: Error) => {
|
|
947
1263
|
console.error(`Exchange request failed: ${err.message}`);
|
|
948
1264
|
process.exit(1);
|
|
949
1265
|
});
|
|
950
|
-
|
|
1266
|
+
|
|
951
1267
|
exchangeReq.write(exchangeData);
|
|
952
1268
|
exchangeReq.end();
|
|
953
|
-
|
|
1269
|
+
|
|
954
1270
|
} catch (err) {
|
|
955
1271
|
// Remove the cancel handler in error case too
|
|
956
1272
|
process.off("SIGINT", cancelHandler);
|
|
957
|
-
|
|
1273
|
+
|
|
958
1274
|
const message = err instanceof Error ? err.message : String(err);
|
|
959
|
-
|
|
1275
|
+
|
|
960
1276
|
// Provide more helpful error messages
|
|
961
1277
|
if (message.includes("timeout")) {
|
|
962
1278
|
console.error(`\nAuthentication timed out.`);
|
|
@@ -965,22 +1281,22 @@ program
|
|
|
965
1281
|
} else {
|
|
966
1282
|
console.error(`\nAuthentication failed: ${message}`);
|
|
967
1283
|
}
|
|
968
|
-
|
|
1284
|
+
|
|
969
1285
|
process.exit(1);
|
|
970
1286
|
}
|
|
971
1287
|
});
|
|
972
1288
|
}
|
|
973
1289
|
);
|
|
974
|
-
|
|
1290
|
+
|
|
975
1291
|
initReq.on("error", (err: Error) => {
|
|
976
1292
|
console.error(`Failed to connect to API: ${err.message}`);
|
|
977
1293
|
callbackServer.server.close();
|
|
978
1294
|
process.exit(1);
|
|
979
1295
|
});
|
|
980
|
-
|
|
1296
|
+
|
|
981
1297
|
initReq.write(initData);
|
|
982
1298
|
initReq.end();
|
|
983
|
-
|
|
1299
|
+
|
|
984
1300
|
} catch (err) {
|
|
985
1301
|
const message = err instanceof Error ? err.message : String(err);
|
|
986
1302
|
console.error(`Authentication error: ${message}`);
|
|
@@ -1023,17 +1339,17 @@ program
|
|
|
1023
1339
|
const hasNewConfig = fs.existsSync(newConfigPath);
|
|
1024
1340
|
const legacyPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
1025
1341
|
const hasLegacyConfig = fs.existsSync(legacyPath) && fs.statSync(legacyPath).isFile();
|
|
1026
|
-
|
|
1342
|
+
|
|
1027
1343
|
if (!hasNewConfig && !hasLegacyConfig) {
|
|
1028
1344
|
console.log("No API key configured");
|
|
1029
1345
|
return;
|
|
1030
1346
|
}
|
|
1031
|
-
|
|
1347
|
+
|
|
1032
1348
|
// Remove from new config
|
|
1033
1349
|
if (hasNewConfig) {
|
|
1034
1350
|
config.deleteConfigKeys(["apiKey", "orgId"]);
|
|
1035
1351
|
}
|
|
1036
|
-
|
|
1352
|
+
|
|
1037
1353
|
// Remove from legacy config
|
|
1038
1354
|
if (hasLegacyConfig) {
|
|
1039
1355
|
try {
|
|
@@ -1049,7 +1365,7 @@ program
|
|
|
1049
1365
|
console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1050
1366
|
}
|
|
1051
1367
|
}
|
|
1052
|
-
|
|
1368
|
+
|
|
1053
1369
|
console.log("API key removed");
|
|
1054
1370
|
console.log(`\nTo authenticate again, run: pgai auth`);
|
|
1055
1371
|
});
|
|
@@ -1058,20 +1374,20 @@ mon
|
|
|
1058
1374
|
.description("generate Grafana password for monitoring services")
|
|
1059
1375
|
.action(async () => {
|
|
1060
1376
|
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
1061
|
-
|
|
1377
|
+
|
|
1062
1378
|
try {
|
|
1063
1379
|
// Generate secure password using openssl
|
|
1064
1380
|
const { stdout: password } = await execPromise(
|
|
1065
1381
|
"openssl rand -base64 12 | tr -d '\n'"
|
|
1066
1382
|
);
|
|
1067
1383
|
const newPassword = password.trim();
|
|
1068
|
-
|
|
1384
|
+
|
|
1069
1385
|
if (!newPassword) {
|
|
1070
1386
|
console.error("Failed to generate password");
|
|
1071
1387
|
process.exitCode = 1;
|
|
1072
1388
|
return;
|
|
1073
1389
|
}
|
|
1074
|
-
|
|
1390
|
+
|
|
1075
1391
|
// Read existing config
|
|
1076
1392
|
let configContent = "";
|
|
1077
1393
|
if (fs.existsSync(cfgPath)) {
|
|
@@ -1082,14 +1398,14 @@ mon
|
|
|
1082
1398
|
configContent = fs.readFileSync(cfgPath, "utf8");
|
|
1083
1399
|
}
|
|
1084
1400
|
}
|
|
1085
|
-
|
|
1401
|
+
|
|
1086
1402
|
// Update or add grafana_password
|
|
1087
1403
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
1088
1404
|
lines.push(`grafana_password=${newPassword}`);
|
|
1089
|
-
|
|
1405
|
+
|
|
1090
1406
|
// Write back
|
|
1091
1407
|
fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
|
|
1092
|
-
|
|
1408
|
+
|
|
1093
1409
|
console.log("✓ New Grafana password generated and saved");
|
|
1094
1410
|
console.log("\nNew credentials:");
|
|
1095
1411
|
console.log(" URL: http://localhost:3000");
|
|
@@ -1114,14 +1430,14 @@ mon
|
|
|
1114
1430
|
process.exitCode = 1;
|
|
1115
1431
|
return;
|
|
1116
1432
|
}
|
|
1117
|
-
|
|
1433
|
+
|
|
1118
1434
|
const stats = fs.statSync(cfgPath);
|
|
1119
1435
|
if (stats.isDirectory()) {
|
|
1120
1436
|
console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
|
|
1121
1437
|
process.exitCode = 1;
|
|
1122
1438
|
return;
|
|
1123
1439
|
}
|
|
1124
|
-
|
|
1440
|
+
|
|
1125
1441
|
const content = fs.readFileSync(cfgPath, "utf8");
|
|
1126
1442
|
const lines = content.split(/\r?\n/);
|
|
1127
1443
|
let password = "";
|
|
@@ -1144,6 +1460,24 @@ mon
|
|
|
1144
1460
|
console.log("");
|
|
1145
1461
|
});
|
|
1146
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* Interpret escape sequences in a string (e.g., \n -> newline)
|
|
1465
|
+
* Note: In regex, to match literal backslash-n, we need \\n in the pattern
|
|
1466
|
+
* which requires \\\\n in the JavaScript string literal
|
|
1467
|
+
*/
|
|
1468
|
+
function interpretEscapes(str: string): string {
|
|
1469
|
+
// First handle double backslashes by temporarily replacing them
|
|
1470
|
+
// Then handle other escapes, then restore double backslashes as single
|
|
1471
|
+
return str
|
|
1472
|
+
.replace(/\\\\/g, '\x00') // Temporarily mark double backslashes
|
|
1473
|
+
.replace(/\\n/g, '\n') // Match literal backslash-n (\\\\n in JS string -> \\n in regex -> matches \n)
|
|
1474
|
+
.replace(/\\t/g, '\t')
|
|
1475
|
+
.replace(/\\r/g, '\r')
|
|
1476
|
+
.replace(/\\"/g, '"')
|
|
1477
|
+
.replace(/\\'/g, "'")
|
|
1478
|
+
.replace(/\x00/g, '\\'); // Restore double backslashes as single
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1147
1481
|
// Issues management
|
|
1148
1482
|
const issues = program.command("issues").description("issues management");
|
|
1149
1483
|
|
|
@@ -1151,7 +1485,8 @@ issues
|
|
|
1151
1485
|
.command("list")
|
|
1152
1486
|
.description("list issues")
|
|
1153
1487
|
.option("--debug", "enable debug output")
|
|
1154
|
-
.
|
|
1488
|
+
.option("--json", "output raw JSON")
|
|
1489
|
+
.action(async (opts: { debug?: boolean; json?: boolean }) => {
|
|
1155
1490
|
try {
|
|
1156
1491
|
const rootOpts = program.opts<CliOptions>();
|
|
1157
1492
|
const cfg = config.readConfig();
|
|
@@ -1165,12 +1500,96 @@ issues
|
|
|
1165
1500
|
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
1166
1501
|
|
|
1167
1502
|
const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1503
|
+
const trimmed = Array.isArray(result)
|
|
1504
|
+
? (result as any[]).map((r) => ({
|
|
1505
|
+
id: (r as any).id,
|
|
1506
|
+
title: (r as any).title,
|
|
1507
|
+
status: (r as any).status,
|
|
1508
|
+
created_at: (r as any).created_at,
|
|
1509
|
+
}))
|
|
1510
|
+
: result;
|
|
1511
|
+
printResult(trimmed, opts.json);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1514
|
+
console.error(message);
|
|
1515
|
+
process.exitCode = 1;
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
issues
|
|
1520
|
+
.command("view <issueId>")
|
|
1521
|
+
.description("view issue details and comments")
|
|
1522
|
+
.option("--debug", "enable debug output")
|
|
1523
|
+
.option("--json", "output raw JSON")
|
|
1524
|
+
.action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const rootOpts = program.opts<CliOptions>();
|
|
1527
|
+
const cfg = config.readConfig();
|
|
1528
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1529
|
+
if (!apiKey) {
|
|
1530
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1531
|
+
process.exitCode = 1;
|
|
1532
|
+
return;
|
|
1173
1533
|
}
|
|
1534
|
+
|
|
1535
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
1536
|
+
|
|
1537
|
+
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1538
|
+
if (!issue) {
|
|
1539
|
+
console.error("Issue not found");
|
|
1540
|
+
process.exitCode = 1;
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1545
|
+
const combined = { issue, comments };
|
|
1546
|
+
printResult(combined, opts.json);
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1549
|
+
console.error(message);
|
|
1550
|
+
process.exitCode = 1;
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
issues
|
|
1555
|
+
.command("post_comment <issueId> <content>")
|
|
1556
|
+
.description("post a new comment to an issue")
|
|
1557
|
+
.option("--parent <uuid>", "parent comment id")
|
|
1558
|
+
.option("--debug", "enable debug output")
|
|
1559
|
+
.option("--json", "output raw JSON")
|
|
1560
|
+
.action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
|
|
1561
|
+
try {
|
|
1562
|
+
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
1563
|
+
if (opts.debug) {
|
|
1564
|
+
// eslint-disable-next-line no-console
|
|
1565
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
1566
|
+
}
|
|
1567
|
+
content = interpretEscapes(content);
|
|
1568
|
+
if (opts.debug) {
|
|
1569
|
+
// eslint-disable-next-line no-console
|
|
1570
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const rootOpts = program.opts<CliOptions>();
|
|
1574
|
+
const cfg = config.readConfig();
|
|
1575
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1576
|
+
if (!apiKey) {
|
|
1577
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1578
|
+
process.exitCode = 1;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
1583
|
+
|
|
1584
|
+
const result = await createIssueComment({
|
|
1585
|
+
apiKey,
|
|
1586
|
+
apiBaseUrl,
|
|
1587
|
+
issueId,
|
|
1588
|
+
content,
|
|
1589
|
+
parentCommentId: opts.parent,
|
|
1590
|
+
debug: !!opts.debug,
|
|
1591
|
+
});
|
|
1592
|
+
printResult(result, opts.json);
|
|
1174
1593
|
} catch (err) {
|
|
1175
1594
|
const message = err instanceof Error ? err.message : String(err);
|
|
1176
1595
|
console.error(message);
|
|
@@ -1195,7 +1614,7 @@ mcp
|
|
|
1195
1614
|
.description("install MCP server configuration for AI coding tool")
|
|
1196
1615
|
.action(async (client?: string) => {
|
|
1197
1616
|
const supportedClients = ["cursor", "claude-code", "windsurf", "codex"];
|
|
1198
|
-
|
|
1617
|
+
|
|
1199
1618
|
// If no client specified, prompt user to choose
|
|
1200
1619
|
if (!client) {
|
|
1201
1620
|
console.log("Available AI coding tools:");
|
|
@@ -1204,24 +1623,24 @@ mcp
|
|
|
1204
1623
|
console.log(" 3. Windsurf");
|
|
1205
1624
|
console.log(" 4. Codex");
|
|
1206
1625
|
console.log("");
|
|
1207
|
-
|
|
1626
|
+
|
|
1208
1627
|
const rl = readline.createInterface({
|
|
1209
1628
|
input: process.stdin,
|
|
1210
1629
|
output: process.stdout
|
|
1211
1630
|
});
|
|
1212
|
-
|
|
1631
|
+
|
|
1213
1632
|
const answer = await new Promise<string>((resolve) => {
|
|
1214
1633
|
rl.question("Select your AI coding tool (1-4): ", resolve);
|
|
1215
1634
|
});
|
|
1216
1635
|
rl.close();
|
|
1217
|
-
|
|
1636
|
+
|
|
1218
1637
|
const choices: Record<string, string> = {
|
|
1219
1638
|
"1": "cursor",
|
|
1220
1639
|
"2": "claude-code",
|
|
1221
1640
|
"3": "windsurf",
|
|
1222
1641
|
"4": "codex"
|
|
1223
1642
|
};
|
|
1224
|
-
|
|
1643
|
+
|
|
1225
1644
|
client = choices[answer.trim()];
|
|
1226
1645
|
if (!client) {
|
|
1227
1646
|
console.error("Invalid selection");
|
|
@@ -1229,54 +1648,90 @@ mcp
|
|
|
1229
1648
|
return;
|
|
1230
1649
|
}
|
|
1231
1650
|
}
|
|
1232
|
-
|
|
1651
|
+
|
|
1233
1652
|
client = client.toLowerCase();
|
|
1234
|
-
|
|
1653
|
+
|
|
1235
1654
|
if (!supportedClients.includes(client)) {
|
|
1236
1655
|
console.error(`Unsupported client: ${client}`);
|
|
1237
1656
|
console.error(`Supported clients: ${supportedClients.join(", ")}`);
|
|
1238
1657
|
process.exitCode = 1;
|
|
1239
1658
|
return;
|
|
1240
1659
|
}
|
|
1241
|
-
|
|
1660
|
+
|
|
1242
1661
|
try {
|
|
1662
|
+
// Get the path to the current pgai executable
|
|
1663
|
+
let pgaiPath: string;
|
|
1664
|
+
try {
|
|
1665
|
+
const execPath = await execPromise("which pgai");
|
|
1666
|
+
pgaiPath = execPath.stdout.trim();
|
|
1667
|
+
} catch {
|
|
1668
|
+
// Fallback to just "pgai" if which fails
|
|
1669
|
+
pgaiPath = "pgai";
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Claude Code uses its own CLI to manage MCP servers
|
|
1673
|
+
if (client === "claude-code") {
|
|
1674
|
+
console.log("Installing PostgresAI MCP server for Claude Code...");
|
|
1675
|
+
|
|
1676
|
+
try {
|
|
1677
|
+
const { stdout, stderr } = await execPromise(
|
|
1678
|
+
`claude mcp add -s user postgresai ${pgaiPath} mcp start`
|
|
1679
|
+
);
|
|
1680
|
+
|
|
1681
|
+
if (stdout) console.log(stdout);
|
|
1682
|
+
if (stderr) console.error(stderr);
|
|
1683
|
+
|
|
1684
|
+
console.log("");
|
|
1685
|
+
console.log("Successfully installed PostgresAI MCP server for Claude Code");
|
|
1686
|
+
console.log("");
|
|
1687
|
+
console.log("Next steps:");
|
|
1688
|
+
console.log(" 1. Restart Claude Code to load the new configuration");
|
|
1689
|
+
console.log(" 2. The PostgresAI MCP server will be available as 'postgresai'");
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1692
|
+
console.error("Failed to install MCP server using Claude CLI");
|
|
1693
|
+
console.error(message);
|
|
1694
|
+
console.error("");
|
|
1695
|
+
console.error("Make sure the 'claude' CLI tool is installed and in your PATH");
|
|
1696
|
+
console.error("See: https://docs.anthropic.com/en/docs/build-with-claude/mcp");
|
|
1697
|
+
process.exitCode = 1;
|
|
1698
|
+
}
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// For other clients (Cursor, Windsurf, Codex), use JSON config editing
|
|
1243
1703
|
const homeDir = os.homedir();
|
|
1244
1704
|
let configPath: string;
|
|
1245
1705
|
let configDir: string;
|
|
1246
|
-
|
|
1706
|
+
|
|
1247
1707
|
// Determine config file location based on client
|
|
1248
1708
|
switch (client) {
|
|
1249
1709
|
case "cursor":
|
|
1250
1710
|
configPath = path.join(homeDir, ".cursor", "mcp.json");
|
|
1251
1711
|
configDir = path.dirname(configPath);
|
|
1252
1712
|
break;
|
|
1253
|
-
|
|
1254
|
-
case "claude-code":
|
|
1255
|
-
configPath = path.join(homeDir, ".claude-code", "mcp.json");
|
|
1256
|
-
configDir = path.dirname(configPath);
|
|
1257
|
-
break;
|
|
1258
|
-
|
|
1713
|
+
|
|
1259
1714
|
case "windsurf":
|
|
1260
1715
|
configPath = path.join(homeDir, ".windsurf", "mcp.json");
|
|
1261
1716
|
configDir = path.dirname(configPath);
|
|
1262
1717
|
break;
|
|
1263
|
-
|
|
1718
|
+
|
|
1264
1719
|
case "codex":
|
|
1265
1720
|
configPath = path.join(homeDir, ".codex", "mcp.json");
|
|
1266
1721
|
configDir = path.dirname(configPath);
|
|
1267
1722
|
break;
|
|
1268
|
-
|
|
1723
|
+
|
|
1269
1724
|
default:
|
|
1270
1725
|
console.error(`Configuration not implemented for: ${client}`);
|
|
1271
1726
|
process.exitCode = 1;
|
|
1272
1727
|
return;
|
|
1273
1728
|
}
|
|
1274
|
-
|
|
1729
|
+
|
|
1275
1730
|
// Ensure config directory exists
|
|
1276
1731
|
if (!fs.existsSync(configDir)) {
|
|
1277
1732
|
fs.mkdirSync(configDir, { recursive: true });
|
|
1278
1733
|
}
|
|
1279
|
-
|
|
1734
|
+
|
|
1280
1735
|
// Read existing config or create new one
|
|
1281
1736
|
let config: any = { mcpServers: {} };
|
|
1282
1737
|
if (fs.existsSync(configPath)) {
|
|
@@ -1290,21 +1745,21 @@ mcp
|
|
|
1290
1745
|
console.error(`Warning: Could not parse existing config, creating new one`);
|
|
1291
1746
|
}
|
|
1292
1747
|
}
|
|
1293
|
-
|
|
1748
|
+
|
|
1294
1749
|
// Add or update PostgresAI MCP server configuration
|
|
1295
1750
|
config.mcpServers.postgresai = {
|
|
1296
|
-
command:
|
|
1751
|
+
command: pgaiPath,
|
|
1297
1752
|
args: ["mcp", "start"]
|
|
1298
1753
|
};
|
|
1299
|
-
|
|
1754
|
+
|
|
1300
1755
|
// Write updated config
|
|
1301
1756
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
1302
|
-
|
|
1757
|
+
|
|
1303
1758
|
console.log(`✓ PostgresAI MCP server configured for ${client}`);
|
|
1304
1759
|
console.log(` Config file: ${configPath}`);
|
|
1305
1760
|
console.log("");
|
|
1306
1761
|
console.log("Please restart your AI coding tool to activate the MCP server");
|
|
1307
|
-
|
|
1762
|
+
|
|
1308
1763
|
} catch (error) {
|
|
1309
1764
|
const message = error instanceof Error ? error.message : String(error);
|
|
1310
1765
|
console.error(`Failed to install MCP server: ${message}`);
|