postgresai 0.11.0-alpha.7 → 0.11.0-alpha.9

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.
@@ -0,0 +1,897 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const pkg = __importStar(require("../package.json"));
39
+ const config = __importStar(require("../lib/config"));
40
+ const yaml = __importStar(require("js-yaml"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const child_process_1 = require("child_process");
44
+ const util_1 = require("util");
45
+ const readline = __importStar(require("readline"));
46
+ const http = __importStar(require("https"));
47
+ const url_1 = require("url");
48
+ const execPromise = (0, util_1.promisify)(child_process_1.exec);
49
+ const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
50
+ /**
51
+ * Get configuration from various sources
52
+ * @param opts - Command line options
53
+ * @returns Configuration object
54
+ */
55
+ function getConfig(opts) {
56
+ // Priority order:
57
+ // 1. Command line option (--api-key)
58
+ // 2. Environment variable (PGAI_API_KEY)
59
+ // 3. User-level config file (~/.config/postgresai/config.json)
60
+ // 4. Legacy project-local config (.pgwatch-config)
61
+ let apiKey = opts.apiKey || process.env.PGAI_API_KEY || "";
62
+ // Try config file if not provided via CLI or env
63
+ if (!apiKey) {
64
+ const fileConfig = config.readConfig();
65
+ if (!apiKey)
66
+ apiKey = fileConfig.apiKey || "";
67
+ }
68
+ return { apiKey };
69
+ }
70
+ const program = new commander_1.Command();
71
+ program
72
+ .name("postgres-ai")
73
+ .description("PostgresAI CLI")
74
+ .version(pkg.version)
75
+ .option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
76
+ .option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)")
77
+ .option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)");
78
+ /**
79
+ * Stub function for not implemented commands
80
+ */
81
+ const stub = (name) => async () => {
82
+ // Temporary stubs until Node parity is implemented
83
+ console.error(`${name}: not implemented in Node CLI yet; use bash CLI for now`);
84
+ process.exitCode = 2;
85
+ };
86
+ /**
87
+ * Resolve project paths
88
+ */
89
+ function resolvePaths() {
90
+ const projectDir = process.cwd();
91
+ const composeFile = path.resolve(projectDir, "docker-compose.yml");
92
+ const instancesFile = path.resolve(projectDir, "instances.yml");
93
+ return { fs, path, projectDir, composeFile, instancesFile };
94
+ }
95
+ /**
96
+ * Get docker compose command
97
+ */
98
+ function getComposeCmd() {
99
+ const tryCmd = (cmd, args) => (0, child_process_1.spawnSync)(cmd, args, { stdio: "ignore" }).status === 0;
100
+ if (tryCmd("docker-compose", ["version"]))
101
+ return ["docker-compose"];
102
+ if (tryCmd("docker", ["compose", "version"]))
103
+ return ["docker", "compose"];
104
+ return null;
105
+ }
106
+ /**
107
+ * Run docker compose command
108
+ */
109
+ async function runCompose(args) {
110
+ const { composeFile } = resolvePaths();
111
+ const cmd = getComposeCmd();
112
+ if (!cmd) {
113
+ console.error("docker compose not found (need docker-compose or docker compose)");
114
+ process.exitCode = 1;
115
+ return 1;
116
+ }
117
+ return new Promise((resolve) => {
118
+ const child = (0, child_process_1.spawn)(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], { stdio: "inherit" });
119
+ child.on("close", (code) => resolve(code || 0));
120
+ });
121
+ }
122
+ program.command("help", { isDefault: true }).description("show help").action(() => {
123
+ program.outputHelp();
124
+ });
125
+ // Service lifecycle
126
+ program
127
+ .command("quickstart")
128
+ .description("complete setup (generate config, start services)")
129
+ .option("--demo", "demo mode", false)
130
+ .action(async () => {
131
+ const code1 = await runCompose(["run", "--rm", "sources-generator"]);
132
+ if (code1 !== 0) {
133
+ process.exitCode = code1;
134
+ return;
135
+ }
136
+ const code2 = await runCompose(["up", "-d"]);
137
+ if (code2 !== 0)
138
+ process.exitCode = code2;
139
+ });
140
+ program
141
+ .command("install")
142
+ .description("prepare project (no-op in repo checkout)")
143
+ .action(async () => {
144
+ console.log("Project files present; nothing to install.");
145
+ });
146
+ program
147
+ .command("start")
148
+ .description("start services")
149
+ .action(async () => {
150
+ const code = await runCompose(["up", "-d"]);
151
+ if (code !== 0)
152
+ process.exitCode = code;
153
+ });
154
+ program
155
+ .command("stop")
156
+ .description("stop services")
157
+ .action(async () => {
158
+ const code = await runCompose(["down"]);
159
+ if (code !== 0)
160
+ process.exitCode = code;
161
+ });
162
+ program
163
+ .command("restart [service]")
164
+ .description("restart all services or specific service")
165
+ .action(async (service) => {
166
+ const args = ["restart"];
167
+ if (service)
168
+ args.push(service);
169
+ const code = await runCompose(args);
170
+ if (code !== 0)
171
+ process.exitCode = code;
172
+ });
173
+ program
174
+ .command("status")
175
+ .description("show service status")
176
+ .action(async () => {
177
+ const code = await runCompose(["ps"]);
178
+ if (code !== 0)
179
+ process.exitCode = code;
180
+ });
181
+ program
182
+ .command("logs [service]")
183
+ .option("-f, --follow", "follow logs", false)
184
+ .option("--tail <lines>", "number of lines to show from the end of logs", "all")
185
+ .description("show logs for all or specific service")
186
+ .action(async (service, opts) => {
187
+ const args = ["logs"];
188
+ if (opts.follow)
189
+ args.push("-f");
190
+ if (opts.tail)
191
+ args.push("--tail", opts.tail);
192
+ if (service)
193
+ args.push(service);
194
+ const code = await runCompose(args);
195
+ if (code !== 0)
196
+ process.exitCode = code;
197
+ });
198
+ program
199
+ .command("health")
200
+ .description("health check")
201
+ .option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
202
+ .action(async (opts) => {
203
+ const services = [
204
+ { name: "Grafana", url: "http://localhost:3000/api/health" },
205
+ { name: "Prometheus", url: "http://localhost:59090/-/healthy" },
206
+ { name: "PGWatch (Postgres)", url: "http://localhost:58080/health" },
207
+ { name: "PGWatch (Prometheus)", url: "http://localhost:58089/health" },
208
+ ];
209
+ const waitTime = opts.wait || 0;
210
+ const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
211
+ console.log("Checking service health...\n");
212
+ let allHealthy = false;
213
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
214
+ if (attempt > 1) {
215
+ console.log(`Retrying (attempt ${attempt}/${maxAttempts})...\n`);
216
+ await new Promise(resolve => setTimeout(resolve, 5000));
217
+ }
218
+ allHealthy = true;
219
+ for (const service of services) {
220
+ try {
221
+ const { stdout } = await execPromise(`curl -sf -o /dev/null -w "%{http_code}" ${service.url}`, { timeout: 5000 });
222
+ const code = stdout.trim();
223
+ if (code === "200") {
224
+ console.log(`✓ ${service.name}: healthy`);
225
+ }
226
+ else {
227
+ console.log(`✗ ${service.name}: unhealthy (HTTP ${code})`);
228
+ allHealthy = false;
229
+ }
230
+ }
231
+ catch (error) {
232
+ console.log(`✗ ${service.name}: unreachable`);
233
+ allHealthy = false;
234
+ }
235
+ }
236
+ if (allHealthy) {
237
+ break;
238
+ }
239
+ }
240
+ console.log("");
241
+ if (allHealthy) {
242
+ console.log("All services are healthy");
243
+ }
244
+ else {
245
+ console.log("Some services are unhealthy");
246
+ process.exitCode = 1;
247
+ }
248
+ });
249
+ program
250
+ .command("config")
251
+ .description("show configuration")
252
+ .action(async () => {
253
+ const { fs, projectDir, composeFile, instancesFile } = resolvePaths();
254
+ console.log(`Project Directory: ${projectDir}`);
255
+ console.log(`Docker Compose File: ${composeFile}`);
256
+ console.log(`Instances File: ${instancesFile}`);
257
+ if (fs.existsSync(instancesFile)) {
258
+ console.log("\nInstances configuration:\n");
259
+ const text = fs.readFileSync(instancesFile, "utf8");
260
+ process.stdout.write(text);
261
+ if (!/\n$/.test(text))
262
+ console.log();
263
+ }
264
+ });
265
+ program
266
+ .command("update-config")
267
+ .description("apply configuration (generate sources)")
268
+ .action(async () => {
269
+ const code = await runCompose(["run", "--rm", "sources-generator"]);
270
+ if (code !== 0)
271
+ process.exitCode = code;
272
+ });
273
+ program
274
+ .command("update")
275
+ .description("update project")
276
+ .action(async () => {
277
+ console.log("Updating PostgresAI monitoring stack...\n");
278
+ try {
279
+ // Check if we're in a git repo
280
+ const gitDir = path.resolve(process.cwd(), ".git");
281
+ if (!fs.existsSync(gitDir)) {
282
+ console.error("Not a git repository. Cannot update.");
283
+ process.exitCode = 1;
284
+ return;
285
+ }
286
+ // Fetch latest changes
287
+ console.log("Fetching latest changes...");
288
+ await execPromise("git fetch origin");
289
+ // Check current branch
290
+ const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
291
+ const currentBranch = branch.trim();
292
+ console.log(`Current branch: ${currentBranch}`);
293
+ // Pull latest changes
294
+ console.log("Pulling latest changes...");
295
+ const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
296
+ console.log(pullOut);
297
+ // Update Docker images
298
+ console.log("\nUpdating Docker images...");
299
+ const code = await runCompose(["pull"]);
300
+ if (code === 0) {
301
+ console.log("\n✓ Update completed successfully");
302
+ console.log("\nTo apply updates, restart services:");
303
+ console.log(" postgres-ai restart");
304
+ }
305
+ else {
306
+ console.error("\n✗ Docker image update failed");
307
+ process.exitCode = 1;
308
+ }
309
+ }
310
+ catch (error) {
311
+ const message = error instanceof Error ? error.message : String(error);
312
+ console.error(`Update failed: ${message}`);
313
+ process.exitCode = 1;
314
+ }
315
+ });
316
+ program
317
+ .command("reset [service]")
318
+ .description("reset all or specific service")
319
+ .action(async (service) => {
320
+ const rl = readline.createInterface({
321
+ input: process.stdin,
322
+ output: process.stdout,
323
+ });
324
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
325
+ try {
326
+ if (service) {
327
+ // Reset specific service
328
+ console.log(`\nThis will stop '${service}', remove its volume, and restart it.`);
329
+ console.log("All data for this service will be lost!\n");
330
+ const answer = await question("Continue? (y/N): ");
331
+ if (answer.toLowerCase() !== "y") {
332
+ console.log("Cancelled");
333
+ rl.close();
334
+ return;
335
+ }
336
+ console.log(`\nStopping ${service}...`);
337
+ await runCompose(["stop", service]);
338
+ console.log(`Removing volume for ${service}...`);
339
+ await runCompose(["rm", "-f", "-v", service]);
340
+ console.log(`Restarting ${service}...`);
341
+ const code = await runCompose(["up", "-d", service]);
342
+ if (code === 0) {
343
+ console.log(`\n✓ Service '${service}' has been reset`);
344
+ }
345
+ else {
346
+ console.error(`\n✗ Failed to restart '${service}'`);
347
+ process.exitCode = 1;
348
+ }
349
+ }
350
+ else {
351
+ // Reset all services
352
+ console.log("\nThis will stop all services and remove all data!");
353
+ console.log("Volumes, networks, and containers will be deleted.\n");
354
+ const answer = await question("Continue? (y/N): ");
355
+ if (answer.toLowerCase() !== "y") {
356
+ console.log("Cancelled");
357
+ rl.close();
358
+ return;
359
+ }
360
+ console.log("\nStopping services and removing data...");
361
+ const downCode = await runCompose(["down", "-v"]);
362
+ if (downCode === 0) {
363
+ console.log("✓ Environment reset completed - all containers and data removed");
364
+ }
365
+ else {
366
+ console.error("✗ Reset failed");
367
+ process.exitCode = 1;
368
+ }
369
+ }
370
+ rl.close();
371
+ }
372
+ catch (error) {
373
+ rl.close();
374
+ const message = error instanceof Error ? error.message : String(error);
375
+ console.error(`Reset failed: ${message}`);
376
+ process.exitCode = 1;
377
+ }
378
+ });
379
+ program
380
+ .command("clean")
381
+ .description("cleanup artifacts")
382
+ .action(async () => {
383
+ console.log("Cleaning up Docker resources...\n");
384
+ try {
385
+ // Remove stopped containers
386
+ const { stdout: containers } = await execFilePromise("docker", ["ps", "-aq", "--filter", "status=exited"]);
387
+ if (containers.trim()) {
388
+ const containerIds = containers.trim().split('\n');
389
+ await execFilePromise("docker", ["rm", ...containerIds]);
390
+ console.log("✓ Removed stopped containers");
391
+ }
392
+ else {
393
+ console.log("✓ No stopped containers to remove");
394
+ }
395
+ // Remove unused volumes
396
+ await execFilePromise("docker", ["volume", "prune", "-f"]);
397
+ console.log("✓ Removed unused volumes");
398
+ // Remove unused networks
399
+ await execFilePromise("docker", ["network", "prune", "-f"]);
400
+ console.log("✓ Removed unused networks");
401
+ // Remove dangling images
402
+ await execFilePromise("docker", ["image", "prune", "-f"]);
403
+ console.log("✓ Removed dangling images");
404
+ console.log("\nCleanup completed");
405
+ }
406
+ catch (error) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ console.error(`Error during cleanup: ${message}`);
409
+ process.exitCode = 1;
410
+ }
411
+ });
412
+ program
413
+ .command("shell <service>")
414
+ .description("open service shell")
415
+ .action(async (service) => {
416
+ const code = await runCompose(["exec", service, "/bin/sh"]);
417
+ if (code !== 0)
418
+ process.exitCode = code;
419
+ });
420
+ program
421
+ .command("check")
422
+ .description("system readiness check")
423
+ .action(async () => {
424
+ const code = await runCompose(["ps"]);
425
+ if (code !== 0)
426
+ process.exitCode = code;
427
+ });
428
+ // Instance management
429
+ program
430
+ .command("list-instances")
431
+ .description("list instances")
432
+ .action(async () => {
433
+ const instancesPath = path.resolve(process.cwd(), "instances.yml");
434
+ if (!fs.existsSync(instancesPath)) {
435
+ console.error(`instances.yml not found in ${process.cwd()}`);
436
+ process.exitCode = 1;
437
+ return;
438
+ }
439
+ try {
440
+ const content = fs.readFileSync(instancesPath, "utf8");
441
+ const instances = yaml.load(content);
442
+ if (!instances || !Array.isArray(instances) || instances.length === 0) {
443
+ console.log("No instances configured");
444
+ console.log("");
445
+ console.log("To add an instance:");
446
+ console.log(" postgres-ai add-instance <connection-string> <name>");
447
+ console.log("");
448
+ console.log("Example:");
449
+ console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
450
+ return;
451
+ }
452
+ // Filter out demo placeholder
453
+ const filtered = instances.filter((inst) => inst.name && inst.name !== "target-database");
454
+ if (filtered.length === 0) {
455
+ console.log("No instances configured");
456
+ console.log("");
457
+ console.log("To add an instance:");
458
+ console.log(" postgres-ai add-instance <connection-string> <name>");
459
+ console.log("");
460
+ console.log("Example:");
461
+ console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
462
+ return;
463
+ }
464
+ for (const inst of filtered) {
465
+ console.log(`Instance: ${inst.name}`);
466
+ }
467
+ }
468
+ catch (err) {
469
+ const message = err instanceof Error ? err.message : String(err);
470
+ console.error(`Error parsing instances.yml: ${message}`);
471
+ process.exitCode = 1;
472
+ }
473
+ });
474
+ program
475
+ .command("add-instance [connStr] [name]")
476
+ .description("add instance")
477
+ .action(async (connStr, name) => {
478
+ const file = path.resolve(process.cwd(), "instances.yml");
479
+ if (!connStr) {
480
+ console.error("Connection string required: postgresql://user:pass@host:port/db");
481
+ process.exitCode = 1;
482
+ return;
483
+ }
484
+ const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
485
+ if (!m) {
486
+ console.error("Invalid connection string format");
487
+ process.exitCode = 1;
488
+ return;
489
+ }
490
+ const host = m[3];
491
+ const db = m[5];
492
+ const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
493
+ // Check if instance already exists
494
+ try {
495
+ if (fs.existsSync(file)) {
496
+ const content = fs.readFileSync(file, "utf8");
497
+ const instances = yaml.load(content) || [];
498
+ if (Array.isArray(instances)) {
499
+ const exists = instances.some((inst) => inst.name === instanceName);
500
+ if (exists) {
501
+ console.error(`Instance '${instanceName}' already exists`);
502
+ process.exitCode = 1;
503
+ return;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ catch (err) {
509
+ // If YAML parsing fails, fall back to simple check
510
+ const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
511
+ if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
512
+ console.error(`Instance '${instanceName}' already exists`);
513
+ process.exitCode = 1;
514
+ return;
515
+ }
516
+ }
517
+ // Add new instance
518
+ 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`;
519
+ const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
520
+ fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
521
+ console.log(`Instance '${instanceName}' added`);
522
+ });
523
+ program
524
+ .command("remove-instance <name>")
525
+ .description("remove instance")
526
+ .action(async (name) => {
527
+ const file = path.resolve(process.cwd(), "instances.yml");
528
+ if (!fs.existsSync(file)) {
529
+ console.error("instances.yml not found");
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+ try {
534
+ const content = fs.readFileSync(file, "utf8");
535
+ const instances = yaml.load(content);
536
+ if (!instances || !Array.isArray(instances)) {
537
+ console.error("Invalid instances.yml format");
538
+ process.exitCode = 1;
539
+ return;
540
+ }
541
+ const filtered = instances.filter((inst) => inst.name !== name);
542
+ if (filtered.length === instances.length) {
543
+ console.error(`Instance '${name}' not found`);
544
+ process.exitCode = 1;
545
+ return;
546
+ }
547
+ fs.writeFileSync(file, yaml.dump(filtered), "utf8");
548
+ console.log(`Instance '${name}' removed`);
549
+ }
550
+ catch (err) {
551
+ const message = err instanceof Error ? err.message : String(err);
552
+ console.error(`Error processing instances.yml: ${message}`);
553
+ process.exitCode = 1;
554
+ }
555
+ });
556
+ program
557
+ .command("test-instance <name>")
558
+ .description("test instance connectivity")
559
+ .action(async (name) => {
560
+ const instancesPath = path.resolve(process.cwd(), "instances.yml");
561
+ if (!fs.existsSync(instancesPath)) {
562
+ console.error("instances.yml not found");
563
+ process.exitCode = 1;
564
+ return;
565
+ }
566
+ try {
567
+ const content = fs.readFileSync(instancesPath, "utf8");
568
+ const instances = yaml.load(content);
569
+ if (!instances || !Array.isArray(instances)) {
570
+ console.error("Invalid instances.yml format");
571
+ process.exitCode = 1;
572
+ return;
573
+ }
574
+ const instance = instances.find((inst) => inst.name === name);
575
+ if (!instance) {
576
+ console.error(`Instance '${name}' not found`);
577
+ process.exitCode = 1;
578
+ return;
579
+ }
580
+ if (!instance.conn_str) {
581
+ console.error(`Connection string not found for instance '${name}'`);
582
+ process.exitCode = 1;
583
+ return;
584
+ }
585
+ console.log(`Testing connection to '${name}'...`);
586
+ const { stdout, stderr } = await execFilePromise("psql", [instance.conn_str, "-c", "SELECT version();", "--no-psqlrc"], { timeout: 10000, env: { ...process.env, PAGER: 'cat' } });
587
+ console.log(`✓ Connection successful`);
588
+ console.log(stdout.trim());
589
+ }
590
+ catch (error) {
591
+ const message = error instanceof Error ? error.message : String(error);
592
+ console.error(`✗ Connection failed: ${message}`);
593
+ process.exitCode = 1;
594
+ }
595
+ });
596
+ // Authentication and API key management
597
+ program
598
+ .command("auth")
599
+ .description("authenticate via browser and obtain API key")
600
+ .option("--port <port>", "local callback server port (default: random)", parseInt)
601
+ .option("--debug", "enable debug output")
602
+ .action(async (opts) => {
603
+ const pkce = require("../lib/pkce");
604
+ const authServer = require("../lib/auth-server");
605
+ console.log("Starting authentication flow...\n");
606
+ // Generate PKCE parameters
607
+ const params = pkce.generatePKCEParams();
608
+ const rootOpts = program.opts();
609
+ const apiBaseUrl = (rootOpts.apiBaseUrl || process.env.PGAI_API_BASE_URL || "https://postgres.ai/api/general/").replace(/\/$/, "");
610
+ const uiBaseUrl = (rootOpts.uiBaseUrl || process.env.PGAI_UI_BASE_URL || "https://console.postgres.ai").replace(/\/$/, "");
611
+ if (opts.debug) {
612
+ console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
613
+ console.log(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
614
+ }
615
+ try {
616
+ // Step 1: Start local callback server FIRST to get actual port
617
+ console.log("Starting local callback server...");
618
+ const requestedPort = opts.port || 0; // 0 = OS assigns available port
619
+ const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 300000);
620
+ // Wait a bit for server to start and get port
621
+ await new Promise(resolve => setTimeout(resolve, 100));
622
+ const actualPort = callbackServer.getPort();
623
+ const redirectUri = `http://localhost:${actualPort}/callback`;
624
+ console.log(`Callback server listening on port ${actualPort}`);
625
+ // Step 2: Initialize OAuth session on backend
626
+ console.log("Initializing authentication session...");
627
+ const initData = JSON.stringify({
628
+ client_type: "cli",
629
+ state: params.state,
630
+ code_challenge: params.codeChallenge,
631
+ code_challenge_method: params.codeChallengeMethod,
632
+ redirect_uri: redirectUri,
633
+ });
634
+ // Build init URL by appending to the API base path (keep /api/general)
635
+ const initUrl = new url_1.URL(`${apiBaseUrl}/rpc/oauth_init`);
636
+ if (opts.debug) {
637
+ console.log(`Debug: Trying to POST to: ${initUrl.toString()}`);
638
+ console.log(`Debug: Request data: ${initData}`);
639
+ }
640
+ const initReq = http.request(initUrl, {
641
+ method: "POST",
642
+ headers: {
643
+ "Content-Type": "application/json",
644
+ "Content-Length": Buffer.byteLength(initData),
645
+ },
646
+ }, (res) => {
647
+ let data = "";
648
+ res.on("data", (chunk) => (data += chunk));
649
+ res.on("end", async () => {
650
+ if (res.statusCode !== 200) {
651
+ console.error(`Failed to initialize auth session: ${res.statusCode}`);
652
+ console.error(data);
653
+ callbackServer.server.close();
654
+ process.exitCode = 1;
655
+ return;
656
+ }
657
+ // Step 3: Open browser
658
+ const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
659
+ if (opts.debug) {
660
+ console.log(`Debug: Auth URL: ${authUrl}`);
661
+ }
662
+ console.log(`\nOpening browser for authentication...`);
663
+ console.log(`If browser does not open automatically, visit:\n${authUrl}\n`);
664
+ // Open browser (cross-platform)
665
+ const openCommand = process.platform === "darwin" ? "open" :
666
+ process.platform === "win32" ? "start" :
667
+ "xdg-open";
668
+ (0, child_process_1.spawn)(openCommand, [authUrl], { detached: true, stdio: "ignore" }).unref();
669
+ // Step 4: Wait for callback
670
+ console.log("Waiting for authorization...");
671
+ try {
672
+ const { code } = await callbackServer.promise;
673
+ // Step 5: Exchange code for token
674
+ console.log("\nExchanging authorization code for API token...");
675
+ const exchangeData = JSON.stringify({
676
+ authorization_code: code,
677
+ code_verifier: params.codeVerifier,
678
+ state: params.state,
679
+ });
680
+ const exchangeUrl = new url_1.URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
681
+ const exchangeReq = http.request(exchangeUrl, {
682
+ method: "POST",
683
+ headers: {
684
+ "Content-Type": "application/json",
685
+ "Content-Length": Buffer.byteLength(exchangeData),
686
+ },
687
+ }, (exchangeRes) => {
688
+ let exchangeData = "";
689
+ exchangeRes.on("data", (chunk) => (exchangeData += chunk));
690
+ exchangeRes.on("end", () => {
691
+ if (exchangeRes.statusCode !== 200) {
692
+ console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
693
+ console.error(exchangeData);
694
+ process.exitCode = 1;
695
+ return;
696
+ }
697
+ try {
698
+ const result = JSON.parse(exchangeData);
699
+ const apiToken = result.api_token;
700
+ const orgId = result.org_id;
701
+ // Step 6: Save token to config
702
+ config.writeConfig({
703
+ apiKey: apiToken,
704
+ baseUrl: apiBaseUrl,
705
+ orgId: orgId,
706
+ });
707
+ console.log("\nAuthentication successful!");
708
+ console.log(`API key saved to: ${config.getConfigPath()}`);
709
+ console.log(`Organization ID: ${orgId}`);
710
+ console.log(`\nYou can now use the CLI without specifying an API key.`);
711
+ }
712
+ catch (err) {
713
+ const message = err instanceof Error ? err.message : String(err);
714
+ console.error(`Failed to parse response: ${message}`);
715
+ process.exitCode = 1;
716
+ }
717
+ });
718
+ });
719
+ exchangeReq.on("error", (err) => {
720
+ console.error(`Exchange request failed: ${err.message}`);
721
+ process.exitCode = 1;
722
+ });
723
+ exchangeReq.write(exchangeData);
724
+ exchangeReq.end();
725
+ }
726
+ catch (err) {
727
+ const message = err instanceof Error ? err.message : String(err);
728
+ console.error(`\nAuthentication failed: ${message}`);
729
+ process.exitCode = 1;
730
+ }
731
+ });
732
+ });
733
+ initReq.on("error", (err) => {
734
+ console.error(`Failed to connect to API: ${err.message}`);
735
+ callbackServer.server.close();
736
+ process.exitCode = 1;
737
+ });
738
+ initReq.write(initData);
739
+ initReq.end();
740
+ }
741
+ catch (err) {
742
+ const message = err instanceof Error ? err.message : String(err);
743
+ console.error(`Authentication error: ${message}`);
744
+ process.exitCode = 1;
745
+ }
746
+ });
747
+ program
748
+ .command("add-key <apiKey>")
749
+ .description("store API key")
750
+ .action(async (apiKey) => {
751
+ config.writeConfig({ apiKey });
752
+ console.log(`API key saved to ${config.getConfigPath()}`);
753
+ });
754
+ program
755
+ .command("show-key")
756
+ .description("show API key (masked)")
757
+ .action(async () => {
758
+ const cfg = config.readConfig();
759
+ if (!cfg.apiKey) {
760
+ console.log("No API key configured");
761
+ console.log(`\nTo authenticate, run: pgai auth`);
762
+ return;
763
+ }
764
+ const mask = (k) => {
765
+ if (k.length <= 8)
766
+ return "****";
767
+ if (k.length <= 16)
768
+ return `${k.slice(0, 4)}${"*".repeat(k.length - 8)}${k.slice(-4)}`;
769
+ // For longer keys, show more of the beginning to help identify them
770
+ return `${k.slice(0, Math.min(12, k.length - 8))}${"*".repeat(Math.max(4, k.length - 16))}${k.slice(-4)}`;
771
+ };
772
+ console.log(`Current API key: ${mask(cfg.apiKey)}`);
773
+ if (cfg.orgId) {
774
+ console.log(`Organization ID: ${cfg.orgId}`);
775
+ }
776
+ console.log(`Config location: ${config.getConfigPath()}`);
777
+ });
778
+ program
779
+ .command("remove-key")
780
+ .description("remove API key")
781
+ .action(async () => {
782
+ // Check both new config and legacy config
783
+ const newConfigPath = config.getConfigPath();
784
+ const hasNewConfig = fs.existsSync(newConfigPath);
785
+ const legacyPath = path.resolve(process.cwd(), ".pgwatch-config");
786
+ const hasLegacyConfig = fs.existsSync(legacyPath) && fs.statSync(legacyPath).isFile();
787
+ if (!hasNewConfig && !hasLegacyConfig) {
788
+ console.log("No API key configured");
789
+ return;
790
+ }
791
+ // Remove from new config
792
+ if (hasNewConfig) {
793
+ config.deleteConfigKeys(["apiKey", "orgId"]);
794
+ }
795
+ // Remove from legacy config
796
+ if (hasLegacyConfig) {
797
+ try {
798
+ const content = fs.readFileSync(legacyPath, "utf8");
799
+ const filtered = content
800
+ .split(/\r?\n/)
801
+ .filter((l) => !/^api_key=/.test(l))
802
+ .join("\n")
803
+ .replace(/\n+$/g, "\n");
804
+ fs.writeFileSync(legacyPath, filtered, "utf8");
805
+ }
806
+ catch (err) {
807
+ // If we can't read/write the legacy config, just skip it
808
+ console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
809
+ }
810
+ }
811
+ console.log("API key removed");
812
+ console.log(`\nTo authenticate again, run: pgai auth`);
813
+ });
814
+ program
815
+ .command("generate-grafana-password")
816
+ .description("generate Grafana password")
817
+ .action(async () => {
818
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
819
+ try {
820
+ // Generate secure password using openssl
821
+ const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
822
+ const newPassword = password.trim();
823
+ if (!newPassword) {
824
+ console.error("Failed to generate password");
825
+ process.exitCode = 1;
826
+ return;
827
+ }
828
+ // Read existing config
829
+ let configContent = "";
830
+ if (fs.existsSync(cfgPath)) {
831
+ const stats = fs.statSync(cfgPath);
832
+ if (stats.isDirectory()) {
833
+ console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
834
+ }
835
+ else {
836
+ configContent = fs.readFileSync(cfgPath, "utf8");
837
+ }
838
+ }
839
+ // Update or add grafana_password
840
+ const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
841
+ lines.push(`grafana_password=${newPassword}`);
842
+ // Write back
843
+ fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
844
+ console.log("✓ New Grafana password generated and saved");
845
+ console.log("\nNew credentials:");
846
+ console.log(" URL: http://localhost:3000");
847
+ console.log(" Username: monitor");
848
+ console.log(` Password: ${newPassword}`);
849
+ console.log("\nRestart Grafana to apply:");
850
+ console.log(" postgres-ai restart grafana");
851
+ }
852
+ catch (error) {
853
+ const message = error instanceof Error ? error.message : String(error);
854
+ console.error(`Failed to generate password: ${message}`);
855
+ console.error("\nNote: This command requires 'openssl' to be installed");
856
+ process.exitCode = 1;
857
+ }
858
+ });
859
+ program
860
+ .command("show-grafana-credentials")
861
+ .description("show Grafana credentials")
862
+ .action(async () => {
863
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
864
+ if (!fs.existsSync(cfgPath)) {
865
+ console.error("Configuration file not found. Run 'quickstart' first.");
866
+ process.exitCode = 1;
867
+ return;
868
+ }
869
+ const stats = fs.statSync(cfgPath);
870
+ if (stats.isDirectory()) {
871
+ console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
872
+ process.exitCode = 1;
873
+ return;
874
+ }
875
+ const content = fs.readFileSync(cfgPath, "utf8");
876
+ const lines = content.split(/\r?\n/);
877
+ let password = "";
878
+ for (const line of lines) {
879
+ const m = line.match(/^grafana_password=(.+)$/);
880
+ if (m) {
881
+ password = m[1].trim();
882
+ break;
883
+ }
884
+ }
885
+ if (!password) {
886
+ console.error("Grafana password not found in configuration");
887
+ process.exitCode = 1;
888
+ return;
889
+ }
890
+ console.log("\nGrafana credentials:");
891
+ console.log(" URL: http://localhost:3000");
892
+ console.log(" Username: monitor");
893
+ console.log(` Password: ${password}`);
894
+ console.log("");
895
+ });
896
+ program.parseAsync(process.argv);
897
+ //# sourceMappingURL=postgres-ai.js.map