postgresai 0.11.0-alpha.9 → 0.12.0-alpha.14
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 +76 -18
- package/bin/postgres-ai.ts +258 -124
- package/dist/bin/postgres-ai.js +237 -117
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/auth-server.js +8 -8
- package/dist/lib/issues.d.ts +7 -0
- package/dist/lib/issues.d.ts.map +1 -0
- package/dist/lib/issues.js +105 -0
- package/dist/lib/issues.js.map +1 -0
- package/dist/lib/mcp-server.d.ts +9 -0
- package/dist/lib/mcp-server.d.ts.map +1 -0
- package/dist/lib/mcp-server.js +114 -0
- package/dist/lib/mcp-server.js.map +1 -0
- package/dist/lib/util.d.ts +27 -0
- package/dist/lib/util.d.ts.map +1 -0
- package/dist/lib/util.js +46 -0
- package/dist/lib/util.js.map +1 -0
- package/dist/package.json +5 -2
- package/lib/auth-server.ts +8 -8
- package/lib/issues.ts +83 -0
- package/lib/mcp-server.ts +98 -0
- package/lib/util.ts +60 -0
- package/package.json +5 -4
- package/tsconfig.json +2 -2
package/bin/postgres-ai.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { promisify } from "util";
|
|
|
12
12
|
import * as readline from "readline";
|
|
13
13
|
import * as http from "https";
|
|
14
14
|
import { URL } from "url";
|
|
15
|
+
import { startMcpServer } from "../lib/mcp-server";
|
|
16
|
+
import { fetchIssues } from "../lib/issues";
|
|
17
|
+
import { resolveBaseUrls } from "../lib/util";
|
|
15
18
|
|
|
16
19
|
const execPromise = promisify(exec);
|
|
17
20
|
const execFilePromise = promisify(execFile);
|
|
@@ -116,10 +119,24 @@ const stub = (name: string) => async (): Promise<void> => {
|
|
|
116
119
|
* Resolve project paths
|
|
117
120
|
*/
|
|
118
121
|
function resolvePaths(): PathResolution {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const startDir = process.cwd();
|
|
123
|
+
let currentDir = startDir;
|
|
124
|
+
|
|
125
|
+
while (true) {
|
|
126
|
+
const composeFile = path.resolve(currentDir, "docker-compose.yml");
|
|
127
|
+
if (fs.existsSync(composeFile)) {
|
|
128
|
+
const instancesFile = path.resolve(currentDir, "instances.yml");
|
|
129
|
+
return { fs, path, projectDir: currentDir, composeFile, instancesFile };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parentDir = path.dirname(currentDir);
|
|
133
|
+
if (parentDir === currentDir) break;
|
|
134
|
+
currentDir = parentDir;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(
|
|
138
|
+
`docker-compose.yml not found. Run monitoring commands from the PostgresAI project directory or one of its subdirectories (starting search from ${startDir}).`
|
|
139
|
+
);
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
/**
|
|
@@ -137,7 +154,15 @@ function getComposeCmd(): string[] | null {
|
|
|
137
154
|
* Run docker compose command
|
|
138
155
|
*/
|
|
139
156
|
async function runCompose(args: string[]): Promise<number> {
|
|
140
|
-
|
|
157
|
+
let composeFile: string;
|
|
158
|
+
try {
|
|
159
|
+
({ composeFile } = resolvePaths());
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
console.error(message);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
141
166
|
const cmd = getComposeCmd();
|
|
142
167
|
if (!cmd) {
|
|
143
168
|
console.error("docker compose not found (need docker-compose or docker compose)");
|
|
@@ -154,10 +179,12 @@ program.command("help", { isDefault: true }).description("show help").action(()
|
|
|
154
179
|
program.outputHelp();
|
|
155
180
|
});
|
|
156
181
|
|
|
157
|
-
//
|
|
158
|
-
program
|
|
182
|
+
// Monitoring services management
|
|
183
|
+
const mon = program.command("mon").description("monitoring services management");
|
|
184
|
+
|
|
185
|
+
mon
|
|
159
186
|
.command("quickstart")
|
|
160
|
-
.description("complete setup (generate config, start services)")
|
|
187
|
+
.description("complete setup (generate config, start monitoring services)")
|
|
161
188
|
.option("--demo", "demo mode", false)
|
|
162
189
|
.action(async () => {
|
|
163
190
|
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
@@ -168,47 +195,46 @@ program
|
|
|
168
195
|
const code2 = await runCompose(["up", "-d"]);
|
|
169
196
|
if (code2 !== 0) process.exitCode = code2;
|
|
170
197
|
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.description("prepare project (no-op in repo checkout)")
|
|
174
|
-
.action(async () => {
|
|
175
|
-
console.log("Project files present; nothing to install.");
|
|
176
|
-
});
|
|
177
|
-
program
|
|
198
|
+
|
|
199
|
+
mon
|
|
178
200
|
.command("start")
|
|
179
|
-
.description("start services")
|
|
201
|
+
.description("start monitoring services")
|
|
180
202
|
.action(async () => {
|
|
181
203
|
const code = await runCompose(["up", "-d"]);
|
|
182
204
|
if (code !== 0) process.exitCode = code;
|
|
183
205
|
});
|
|
184
|
-
|
|
206
|
+
|
|
207
|
+
mon
|
|
185
208
|
.command("stop")
|
|
186
|
-
.description("stop services")
|
|
209
|
+
.description("stop monitoring services")
|
|
187
210
|
.action(async () => {
|
|
188
211
|
const code = await runCompose(["down"]);
|
|
189
212
|
if (code !== 0) process.exitCode = code;
|
|
190
213
|
});
|
|
191
|
-
|
|
214
|
+
|
|
215
|
+
mon
|
|
192
216
|
.command("restart [service]")
|
|
193
|
-
.description("restart all services or specific service")
|
|
217
|
+
.description("restart all monitoring services or specific service")
|
|
194
218
|
.action(async (service?: string) => {
|
|
195
219
|
const args = ["restart"];
|
|
196
220
|
if (service) args.push(service);
|
|
197
221
|
const code = await runCompose(args);
|
|
198
222
|
if (code !== 0) process.exitCode = code;
|
|
199
223
|
});
|
|
200
|
-
|
|
224
|
+
|
|
225
|
+
mon
|
|
201
226
|
.command("status")
|
|
202
|
-
.description("show
|
|
227
|
+
.description("show monitoring services status")
|
|
203
228
|
.action(async () => {
|
|
204
229
|
const code = await runCompose(["ps"]);
|
|
205
230
|
if (code !== 0) process.exitCode = code;
|
|
206
231
|
});
|
|
207
|
-
|
|
232
|
+
|
|
233
|
+
mon
|
|
208
234
|
.command("logs [service]")
|
|
209
235
|
.option("-f, --follow", "follow logs", false)
|
|
210
236
|
.option("--tail <lines>", "number of lines to show from the end of logs", "all")
|
|
211
|
-
.description("show logs for all or specific service")
|
|
237
|
+
.description("show logs for all or specific monitoring service")
|
|
212
238
|
.action(async (service: string | undefined, opts: { follow: boolean; tail: string }) => {
|
|
213
239
|
const args: string[] = ["logs"];
|
|
214
240
|
if (opts.follow) args.push("-f");
|
|
@@ -217,9 +243,9 @@ program
|
|
|
217
243
|
const code = await runCompose(args);
|
|
218
244
|
if (code !== 0) process.exitCode = code;
|
|
219
245
|
});
|
|
220
|
-
|
|
246
|
+
mon
|
|
221
247
|
.command("health")
|
|
222
|
-
.description("health check")
|
|
248
|
+
.description("health check for monitoring services")
|
|
223
249
|
.option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
|
|
224
250
|
.action(async (opts: { wait: number }) => {
|
|
225
251
|
const services: HealthService[] = [
|
|
@@ -244,15 +270,20 @@ program
|
|
|
244
270
|
allHealthy = true;
|
|
245
271
|
for (const service of services) {
|
|
246
272
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
273
|
+
// Use native fetch instead of requiring curl to be installed
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
276
|
+
|
|
277
|
+
const response = await fetch(service.url, {
|
|
278
|
+
signal: controller.signal,
|
|
279
|
+
method: 'GET',
|
|
280
|
+
});
|
|
281
|
+
clearTimeout(timeoutId);
|
|
282
|
+
|
|
283
|
+
if (response.status === 200) {
|
|
253
284
|
console.log(`✓ ${service.name}: healthy`);
|
|
254
285
|
} else {
|
|
255
|
-
console.log(`✗ ${service.name}: unhealthy (HTTP ${
|
|
286
|
+
console.log(`✗ ${service.name}: unhealthy (HTTP ${response.status})`);
|
|
256
287
|
allHealthy = false;
|
|
257
288
|
}
|
|
258
289
|
} catch (error) {
|
|
@@ -274,11 +305,21 @@ program
|
|
|
274
305
|
process.exitCode = 1;
|
|
275
306
|
}
|
|
276
307
|
});
|
|
277
|
-
|
|
308
|
+
mon
|
|
278
309
|
.command("config")
|
|
279
|
-
.description("show configuration")
|
|
310
|
+
.description("show monitoring services configuration")
|
|
280
311
|
.action(async () => {
|
|
281
|
-
|
|
312
|
+
let projectDir: string;
|
|
313
|
+
let composeFile: string;
|
|
314
|
+
let instancesFile: string;
|
|
315
|
+
try {
|
|
316
|
+
({ projectDir, composeFile, instancesFile } = resolvePaths());
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
319
|
+
console.error(message);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
282
323
|
console.log(`Project Directory: ${projectDir}`);
|
|
283
324
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
284
325
|
console.log(`Instances File: ${instancesFile}`);
|
|
@@ -289,16 +330,16 @@ program
|
|
|
289
330
|
if (!/\n$/.test(text)) console.log();
|
|
290
331
|
}
|
|
291
332
|
});
|
|
292
|
-
|
|
333
|
+
mon
|
|
293
334
|
.command("update-config")
|
|
294
|
-
.description("apply configuration (generate sources)")
|
|
335
|
+
.description("apply monitoring services configuration (generate sources)")
|
|
295
336
|
.action(async () => {
|
|
296
337
|
const code = await runCompose(["run", "--rm", "sources-generator"]);
|
|
297
338
|
if (code !== 0) process.exitCode = code;
|
|
298
339
|
});
|
|
299
|
-
|
|
340
|
+
mon
|
|
300
341
|
.command("update")
|
|
301
|
-
.description("update
|
|
342
|
+
.description("update monitoring stack")
|
|
302
343
|
.action(async () => {
|
|
303
344
|
console.log("Updating PostgresAI monitoring stack...\n");
|
|
304
345
|
|
|
@@ -331,8 +372,8 @@ program
|
|
|
331
372
|
|
|
332
373
|
if (code === 0) {
|
|
333
374
|
console.log("\n✓ Update completed successfully");
|
|
334
|
-
console.log("\nTo apply updates, restart services:");
|
|
335
|
-
console.log(" postgres-ai restart");
|
|
375
|
+
console.log("\nTo apply updates, restart monitoring services:");
|
|
376
|
+
console.log(" postgres-ai mon restart");
|
|
336
377
|
} else {
|
|
337
378
|
console.error("\n✗ Docker image update failed");
|
|
338
379
|
process.exitCode = 1;
|
|
@@ -343,9 +384,9 @@ program
|
|
|
343
384
|
process.exitCode = 1;
|
|
344
385
|
}
|
|
345
386
|
});
|
|
346
|
-
|
|
387
|
+
mon
|
|
347
388
|
.command("reset [service]")
|
|
348
|
-
.description("reset all or specific service")
|
|
389
|
+
.description("reset all or specific monitoring service")
|
|
349
390
|
.action(async (service?: string) => {
|
|
350
391
|
const rl = readline.createInterface({
|
|
351
392
|
input: process.stdin,
|
|
@@ -414,9 +455,9 @@ program
|
|
|
414
455
|
process.exitCode = 1;
|
|
415
456
|
}
|
|
416
457
|
});
|
|
417
|
-
|
|
458
|
+
mon
|
|
418
459
|
.command("clean")
|
|
419
|
-
.description("cleanup artifacts")
|
|
460
|
+
.description("cleanup monitoring services artifacts")
|
|
420
461
|
.action(async () => {
|
|
421
462
|
console.log("Cleaning up Docker resources...\n");
|
|
422
463
|
|
|
@@ -450,25 +491,27 @@ program
|
|
|
450
491
|
process.exitCode = 1;
|
|
451
492
|
}
|
|
452
493
|
});
|
|
453
|
-
|
|
494
|
+
mon
|
|
454
495
|
.command("shell <service>")
|
|
455
|
-
.description("open service
|
|
496
|
+
.description("open shell to monitoring service")
|
|
456
497
|
.action(async (service: string) => {
|
|
457
498
|
const code = await runCompose(["exec", service, "/bin/sh"]);
|
|
458
499
|
if (code !== 0) process.exitCode = code;
|
|
459
500
|
});
|
|
460
|
-
|
|
501
|
+
mon
|
|
461
502
|
.command("check")
|
|
462
|
-
.description("system readiness check")
|
|
503
|
+
.description("monitoring services system readiness check")
|
|
463
504
|
.action(async () => {
|
|
464
505
|
const code = await runCompose(["ps"]);
|
|
465
506
|
if (code !== 0) process.exitCode = code;
|
|
466
507
|
});
|
|
467
508
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
509
|
+
// Monitoring targets (databases to monitor)
|
|
510
|
+
const targets = mon.command("targets").description("manage databases to monitor");
|
|
511
|
+
|
|
512
|
+
targets
|
|
513
|
+
.command("list")
|
|
514
|
+
.description("list monitoring target databases")
|
|
472
515
|
.action(async () => {
|
|
473
516
|
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
474
517
|
if (!fs.existsSync(instancesPath)) {
|
|
@@ -482,32 +525,32 @@ program
|
|
|
482
525
|
const instances = yaml.load(content) as Instance[] | null;
|
|
483
526
|
|
|
484
527
|
if (!instances || !Array.isArray(instances) || instances.length === 0) {
|
|
485
|
-
console.log("No
|
|
528
|
+
console.log("No monitoring targets configured");
|
|
486
529
|
console.log("");
|
|
487
|
-
console.log("To add
|
|
488
|
-
console.log(" postgres-ai add
|
|
530
|
+
console.log("To add a monitoring target:");
|
|
531
|
+
console.log(" postgres-ai mon targets add <connection-string> <name>");
|
|
489
532
|
console.log("");
|
|
490
533
|
console.log("Example:");
|
|
491
|
-
console.log(" postgres-ai add
|
|
534
|
+
console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
|
|
492
535
|
return;
|
|
493
536
|
}
|
|
494
537
|
|
|
495
|
-
// Filter out demo
|
|
496
|
-
const filtered = instances.filter((inst) => inst.name && inst.
|
|
538
|
+
// Filter out disabled instances (e.g., demo placeholders)
|
|
539
|
+
const filtered = instances.filter((inst) => inst.name && inst.is_enabled !== false);
|
|
497
540
|
|
|
498
541
|
if (filtered.length === 0) {
|
|
499
|
-
console.log("No
|
|
542
|
+
console.log("No monitoring targets configured");
|
|
500
543
|
console.log("");
|
|
501
|
-
console.log("To add
|
|
502
|
-
console.log(" postgres-ai add
|
|
544
|
+
console.log("To add a monitoring target:");
|
|
545
|
+
console.log(" postgres-ai mon targets add <connection-string> <name>");
|
|
503
546
|
console.log("");
|
|
504
547
|
console.log("Example:");
|
|
505
|
-
console.log(" postgres-ai add
|
|
548
|
+
console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
|
|
506
549
|
return;
|
|
507
550
|
}
|
|
508
551
|
|
|
509
552
|
for (const inst of filtered) {
|
|
510
|
-
console.log(`
|
|
553
|
+
console.log(`Target: ${inst.name}`);
|
|
511
554
|
}
|
|
512
555
|
} catch (err) {
|
|
513
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -515,9 +558,9 @@ program
|
|
|
515
558
|
process.exitCode = 1;
|
|
516
559
|
}
|
|
517
560
|
});
|
|
518
|
-
|
|
519
|
-
.command("add
|
|
520
|
-
.description("add
|
|
561
|
+
targets
|
|
562
|
+
.command("add [connStr] [name]")
|
|
563
|
+
.description("add monitoring target database")
|
|
521
564
|
.action(async (connStr?: string, name?: string) => {
|
|
522
565
|
const file = path.resolve(process.cwd(), "instances.yml");
|
|
523
566
|
if (!connStr) {
|
|
@@ -543,7 +586,7 @@ program
|
|
|
543
586
|
if (Array.isArray(instances)) {
|
|
544
587
|
const exists = instances.some((inst) => inst.name === instanceName);
|
|
545
588
|
if (exists) {
|
|
546
|
-
console.error(`
|
|
589
|
+
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
547
590
|
process.exitCode = 1;
|
|
548
591
|
return;
|
|
549
592
|
}
|
|
@@ -553,7 +596,7 @@ program
|
|
|
553
596
|
// If YAML parsing fails, fall back to simple check
|
|
554
597
|
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
555
598
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
|
|
556
|
-
console.error(`
|
|
599
|
+
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
557
600
|
process.exitCode = 1;
|
|
558
601
|
return;
|
|
559
602
|
}
|
|
@@ -563,11 +606,11 @@ program
|
|
|
563
606
|
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`;
|
|
564
607
|
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
565
608
|
fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
|
|
566
|
-
console.log(`
|
|
609
|
+
console.log(`Monitoring target '${instanceName}' added`);
|
|
567
610
|
});
|
|
568
|
-
|
|
569
|
-
.command("remove
|
|
570
|
-
.description("remove
|
|
611
|
+
targets
|
|
612
|
+
.command("remove <name>")
|
|
613
|
+
.description("remove monitoring target database")
|
|
571
614
|
.action(async (name: string) => {
|
|
572
615
|
const file = path.resolve(process.cwd(), "instances.yml");
|
|
573
616
|
if (!fs.existsSync(file)) {
|
|
@@ -589,22 +632,22 @@ program
|
|
|
589
632
|
const filtered = instances.filter((inst) => inst.name !== name);
|
|
590
633
|
|
|
591
634
|
if (filtered.length === instances.length) {
|
|
592
|
-
console.error(`
|
|
635
|
+
console.error(`Monitoring target '${name}' not found`);
|
|
593
636
|
process.exitCode = 1;
|
|
594
637
|
return;
|
|
595
638
|
}
|
|
596
639
|
|
|
597
640
|
fs.writeFileSync(file, yaml.dump(filtered), "utf8");
|
|
598
|
-
console.log(`
|
|
641
|
+
console.log(`Monitoring target '${name}' removed`);
|
|
599
642
|
} catch (err) {
|
|
600
643
|
const message = err instanceof Error ? err.message : String(err);
|
|
601
644
|
console.error(`Error processing instances.yml: ${message}`);
|
|
602
645
|
process.exitCode = 1;
|
|
603
646
|
}
|
|
604
647
|
});
|
|
605
|
-
|
|
606
|
-
.command("test
|
|
607
|
-
.description("test
|
|
648
|
+
targets
|
|
649
|
+
.command("test <name>")
|
|
650
|
+
.description("test monitoring target database connectivity")
|
|
608
651
|
.action(async (name: string) => {
|
|
609
652
|
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
610
653
|
if (!fs.existsSync(instancesPath)) {
|
|
@@ -626,26 +669,31 @@ program
|
|
|
626
669
|
const instance = instances.find((inst) => inst.name === name);
|
|
627
670
|
|
|
628
671
|
if (!instance) {
|
|
629
|
-
console.error(`
|
|
672
|
+
console.error(`Monitoring target '${name}' not found`);
|
|
630
673
|
process.exitCode = 1;
|
|
631
674
|
return;
|
|
632
675
|
}
|
|
633
676
|
|
|
634
677
|
if (!instance.conn_str) {
|
|
635
|
-
console.error(`Connection string not found for
|
|
678
|
+
console.error(`Connection string not found for monitoring target '${name}'`);
|
|
636
679
|
process.exitCode = 1;
|
|
637
680
|
return;
|
|
638
681
|
}
|
|
639
682
|
|
|
640
|
-
console.log(`Testing connection to '${name}'...`);
|
|
683
|
+
console.log(`Testing connection to monitoring target '${name}'...`);
|
|
641
684
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
685
|
+
// Use native pg client instead of requiring psql to be installed
|
|
686
|
+
const { Client } = require('pg');
|
|
687
|
+
const client = new Client({ connectionString: instance.conn_str });
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
await client.connect();
|
|
691
|
+
const result = await client.query('select version();');
|
|
692
|
+
console.log(`✓ Connection successful`);
|
|
693
|
+
console.log(result.rows[0].version);
|
|
694
|
+
} finally {
|
|
695
|
+
await client.end();
|
|
696
|
+
}
|
|
649
697
|
} catch (error) {
|
|
650
698
|
const message = error instanceof Error ? error.message : String(error);
|
|
651
699
|
console.error(`✗ Connection failed: ${message}`);
|
|
@@ -669,9 +717,8 @@ program
|
|
|
669
717
|
const params = pkce.generatePKCEParams();
|
|
670
718
|
|
|
671
719
|
const rootOpts = program.opts<CliOptions>();
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
const uiBaseUrl = (rootOpts.uiBaseUrl || process.env.PGAI_UI_BASE_URL || "https://console.postgres.ai").replace(/\/$/, "");
|
|
720
|
+
const cfg = config.readConfig();
|
|
721
|
+
const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
675
722
|
|
|
676
723
|
if (opts.debug) {
|
|
677
724
|
console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
|
|
@@ -682,7 +729,7 @@ program
|
|
|
682
729
|
// Step 1: Start local callback server FIRST to get actual port
|
|
683
730
|
console.log("Starting local callback server...");
|
|
684
731
|
const requestedPort = opts.port || 0; // 0 = OS assigns available port
|
|
685
|
-
const callbackServer = authServer.createCallbackServer(requestedPort, params.state,
|
|
732
|
+
const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
|
|
686
733
|
|
|
687
734
|
// Wait a bit for server to start and get port
|
|
688
735
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -724,10 +771,20 @@ program
|
|
|
724
771
|
res.on("end", async () => {
|
|
725
772
|
if (res.statusCode !== 200) {
|
|
726
773
|
console.error(`Failed to initialize auth session: ${res.statusCode}`);
|
|
727
|
-
|
|
774
|
+
|
|
775
|
+
// Check if response is HTML (common for 404 pages)
|
|
776
|
+
if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
|
|
777
|
+
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
778
|
+
console.error(" 1. The API endpoint URL is incorrect");
|
|
779
|
+
console.error(" 2. The endpoint does not exist (404)");
|
|
780
|
+
console.error(`\nAPI URL attempted: ${initUrl.toString()}`);
|
|
781
|
+
console.error("\nPlease verify the --api-base-url parameter.");
|
|
782
|
+
} else {
|
|
783
|
+
console.error(data);
|
|
784
|
+
}
|
|
785
|
+
|
|
728
786
|
callbackServer.server.close();
|
|
729
|
-
process.
|
|
730
|
-
return;
|
|
787
|
+
process.exit(1);
|
|
731
788
|
}
|
|
732
789
|
|
|
733
790
|
// Step 3: Open browser
|
|
@@ -748,9 +805,22 @@ program
|
|
|
748
805
|
|
|
749
806
|
// Step 4: Wait for callback
|
|
750
807
|
console.log("Waiting for authorization...");
|
|
808
|
+
console.log("(Press Ctrl+C to cancel)\n");
|
|
809
|
+
|
|
810
|
+
// Handle Ctrl+C gracefully
|
|
811
|
+
const cancelHandler = () => {
|
|
812
|
+
console.log("\n\nAuthentication cancelled by user.");
|
|
813
|
+
callbackServer.server.close();
|
|
814
|
+
process.exit(130); // Standard exit code for SIGINT
|
|
815
|
+
};
|
|
816
|
+
process.on("SIGINT", cancelHandler);
|
|
817
|
+
|
|
751
818
|
try {
|
|
752
819
|
const { code } = await callbackServer.promise;
|
|
753
820
|
|
|
821
|
+
// Remove the cancel handler after successful auth
|
|
822
|
+
process.off("SIGINT", cancelHandler);
|
|
823
|
+
|
|
754
824
|
// Step 5: Exchange code for token
|
|
755
825
|
console.log("\nExchanging authorization code for API token...");
|
|
756
826
|
const exchangeData = JSON.stringify({
|
|
@@ -758,7 +828,6 @@ program
|
|
|
758
828
|
code_verifier: params.codeVerifier,
|
|
759
829
|
state: params.state,
|
|
760
830
|
});
|
|
761
|
-
|
|
762
831
|
const exchangeUrl = new URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
|
|
763
832
|
const exchangeReq = http.request(
|
|
764
833
|
exchangeUrl,
|
|
@@ -770,20 +839,31 @@ program
|
|
|
770
839
|
},
|
|
771
840
|
},
|
|
772
841
|
(exchangeRes) => {
|
|
773
|
-
let
|
|
774
|
-
exchangeRes.on("data", (chunk) => (
|
|
842
|
+
let exchangeBody = "";
|
|
843
|
+
exchangeRes.on("data", (chunk) => (exchangeBody += chunk));
|
|
775
844
|
exchangeRes.on("end", () => {
|
|
776
845
|
if (exchangeRes.statusCode !== 200) {
|
|
777
846
|
console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
|
|
778
|
-
|
|
779
|
-
|
|
847
|
+
|
|
848
|
+
// Check if response is HTML (common for 404 pages)
|
|
849
|
+
if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
|
|
850
|
+
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
851
|
+
console.error(" 1. The API endpoint URL is incorrect");
|
|
852
|
+
console.error(" 2. The endpoint does not exist (404)");
|
|
853
|
+
console.error(`\nAPI URL attempted: ${exchangeUrl.toString()}`);
|
|
854
|
+
console.error("\nPlease verify the --api-base-url parameter.");
|
|
855
|
+
} else {
|
|
856
|
+
console.error(exchangeBody);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
process.exit(1);
|
|
780
860
|
return;
|
|
781
861
|
}
|
|
782
862
|
|
|
783
863
|
try {
|
|
784
|
-
const result = JSON.parse(
|
|
785
|
-
const apiToken = result.api_token;
|
|
786
|
-
const orgId = result.org_id;
|
|
864
|
+
const result = JSON.parse(exchangeBody);
|
|
865
|
+
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.
|
|
866
|
+
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.
|
|
787
867
|
|
|
788
868
|
// Step 6: Save token to config
|
|
789
869
|
config.writeConfig({
|
|
@@ -796,10 +876,11 @@ program
|
|
|
796
876
|
console.log(`API key saved to: ${config.getConfigPath()}`);
|
|
797
877
|
console.log(`Organization ID: ${orgId}`);
|
|
798
878
|
console.log(`\nYou can now use the CLI without specifying an API key.`);
|
|
879
|
+
process.exit(0);
|
|
799
880
|
} catch (err) {
|
|
800
881
|
const message = err instanceof Error ? err.message : String(err);
|
|
801
882
|
console.error(`Failed to parse response: ${message}`);
|
|
802
|
-
process.
|
|
883
|
+
process.exit(1);
|
|
803
884
|
}
|
|
804
885
|
});
|
|
805
886
|
}
|
|
@@ -807,16 +888,28 @@ program
|
|
|
807
888
|
|
|
808
889
|
exchangeReq.on("error", (err: Error) => {
|
|
809
890
|
console.error(`Exchange request failed: ${err.message}`);
|
|
810
|
-
process.
|
|
891
|
+
process.exit(1);
|
|
811
892
|
});
|
|
812
893
|
|
|
813
894
|
exchangeReq.write(exchangeData);
|
|
814
895
|
exchangeReq.end();
|
|
815
896
|
|
|
816
897
|
} catch (err) {
|
|
898
|
+
// Remove the cancel handler in error case too
|
|
899
|
+
process.off("SIGINT", cancelHandler);
|
|
900
|
+
|
|
817
901
|
const message = err instanceof Error ? err.message : String(err);
|
|
818
|
-
|
|
819
|
-
|
|
902
|
+
|
|
903
|
+
// Provide more helpful error messages
|
|
904
|
+
if (message.includes("timeout")) {
|
|
905
|
+
console.error(`\nAuthentication timed out.`);
|
|
906
|
+
console.error(`This usually means you closed the browser window without completing authentication.`);
|
|
907
|
+
console.error(`Please try again and complete the authentication flow.`);
|
|
908
|
+
} else {
|
|
909
|
+
console.error(`\nAuthentication failed: ${message}`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
process.exit(1);
|
|
820
913
|
}
|
|
821
914
|
});
|
|
822
915
|
}
|
|
@@ -825,7 +918,7 @@ program
|
|
|
825
918
|
initReq.on("error", (err: Error) => {
|
|
826
919
|
console.error(`Failed to connect to API: ${err.message}`);
|
|
827
920
|
callbackServer.server.close();
|
|
828
|
-
process.
|
|
921
|
+
process.exit(1);
|
|
829
922
|
});
|
|
830
923
|
|
|
831
924
|
initReq.write(initData);
|
|
@@ -834,7 +927,7 @@ program
|
|
|
834
927
|
} catch (err) {
|
|
835
928
|
const message = err instanceof Error ? err.message : String(err);
|
|
836
929
|
console.error(`Authentication error: ${message}`);
|
|
837
|
-
process.
|
|
930
|
+
process.exit(1);
|
|
838
931
|
}
|
|
839
932
|
});
|
|
840
933
|
|
|
@@ -856,13 +949,8 @@ program
|
|
|
856
949
|
console.log(`\nTo authenticate, run: pgai auth`);
|
|
857
950
|
return;
|
|
858
951
|
}
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
if (k.length <= 16) return `${k.slice(0, 4)}${"*".repeat(k.length - 8)}${k.slice(-4)}`;
|
|
862
|
-
// For longer keys, show more of the beginning to help identify them
|
|
863
|
-
return `${k.slice(0, Math.min(12, k.length - 8))}${"*".repeat(Math.max(4, k.length - 16))}${k.slice(-4)}`;
|
|
864
|
-
};
|
|
865
|
-
console.log(`Current API key: ${mask(cfg.apiKey)}`);
|
|
952
|
+
const { maskSecret } = require("../lib/util");
|
|
953
|
+
console.log(`Current API key: ${maskSecret(cfg.apiKey)}`);
|
|
866
954
|
if (cfg.orgId) {
|
|
867
955
|
console.log(`Organization ID: ${cfg.orgId}`);
|
|
868
956
|
}
|
|
@@ -908,9 +996,9 @@ program
|
|
|
908
996
|
console.log("API key removed");
|
|
909
997
|
console.log(`\nTo authenticate again, run: pgai auth`);
|
|
910
998
|
});
|
|
911
|
-
|
|
999
|
+
mon
|
|
912
1000
|
.command("generate-grafana-password")
|
|
913
|
-
.description("generate Grafana password")
|
|
1001
|
+
.description("generate Grafana password for monitoring services")
|
|
914
1002
|
.action(async () => {
|
|
915
1003
|
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
916
1004
|
|
|
@@ -950,8 +1038,8 @@ program
|
|
|
950
1038
|
console.log(" URL: http://localhost:3000");
|
|
951
1039
|
console.log(" Username: monitor");
|
|
952
1040
|
console.log(` Password: ${newPassword}`);
|
|
953
|
-
console.log("\
|
|
954
|
-
console.log(" postgres-ai
|
|
1041
|
+
console.log("\nReset Grafana to apply new password:");
|
|
1042
|
+
console.log(" postgres-ai mon reset grafana");
|
|
955
1043
|
} catch (error) {
|
|
956
1044
|
const message = error instanceof Error ? error.message : String(error);
|
|
957
1045
|
console.error(`Failed to generate password: ${message}`);
|
|
@@ -959,13 +1047,13 @@ program
|
|
|
959
1047
|
process.exitCode = 1;
|
|
960
1048
|
}
|
|
961
1049
|
});
|
|
962
|
-
|
|
1050
|
+
mon
|
|
963
1051
|
.command("show-grafana-credentials")
|
|
964
|
-
.description("show Grafana credentials")
|
|
1052
|
+
.description("show Grafana credentials for monitoring services")
|
|
965
1053
|
.action(async () => {
|
|
966
1054
|
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
967
1055
|
if (!fs.existsSync(cfgPath)) {
|
|
968
|
-
console.error("Configuration file not found. Run 'quickstart' first.");
|
|
1056
|
+
console.error("Configuration file not found. Run 'postgres-ai mon quickstart' first.");
|
|
969
1057
|
process.exitCode = 1;
|
|
970
1058
|
return;
|
|
971
1059
|
}
|
|
@@ -999,5 +1087,51 @@ program
|
|
|
999
1087
|
console.log("");
|
|
1000
1088
|
});
|
|
1001
1089
|
|
|
1090
|
+
// Issues management
|
|
1091
|
+
const issues = program.command("issues").description("issues management");
|
|
1092
|
+
|
|
1093
|
+
issues
|
|
1094
|
+
.command("list")
|
|
1095
|
+
.description("list issues")
|
|
1096
|
+
.option("--debug", "enable debug output")
|
|
1097
|
+
.action(async (opts: { debug?: boolean }) => {
|
|
1098
|
+
try {
|
|
1099
|
+
const rootOpts = program.opts<CliOptions>();
|
|
1100
|
+
const cfg = config.readConfig();
|
|
1101
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1102
|
+
if (!apiKey) {
|
|
1103
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1104
|
+
process.exitCode = 1;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
1109
|
+
|
|
1110
|
+
const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
|
|
1111
|
+
if (typeof result === "string") {
|
|
1112
|
+
process.stdout.write(result);
|
|
1113
|
+
if (!/\n$/.test(result)) console.log();
|
|
1114
|
+
} else {
|
|
1115
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1116
|
+
}
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1119
|
+
console.error(message);
|
|
1120
|
+
process.exitCode = 1;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// MCP server
|
|
1125
|
+
const mcp = program.command("mcp").description("MCP server integration");
|
|
1126
|
+
|
|
1127
|
+
mcp
|
|
1128
|
+
.command("start")
|
|
1129
|
+
.description("start MCP stdio server")
|
|
1130
|
+
.option("--debug", "enable debug output")
|
|
1131
|
+
.action(async (opts: { debug?: boolean }) => {
|
|
1132
|
+
const rootOpts = program.opts<CliOptions>();
|
|
1133
|
+
await startMcpServer(rootOpts, { debug: !!opts.debug });
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1002
1136
|
program.parseAsync(process.argv);
|
|
1003
1137
|
|