postgresai 0.11.0-alpha.8 → 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.
- package/README.md +81 -4
- package/bin/postgres-ai.ts +1003 -0
- package/dist/bin/postgres-ai.d.ts +3 -0
- package/dist/bin/postgres-ai.d.ts.map +1 -0
- package/dist/bin/postgres-ai.js +897 -0
- package/dist/bin/postgres-ai.js.map +1 -0
- package/dist/lib/auth-server.d.ts +31 -0
- package/dist/lib/auth-server.d.ts.map +1 -0
- package/dist/lib/auth-server.js +263 -0
- package/dist/lib/auth-server.js.map +1 -0
- package/dist/lib/config.d.ts +45 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +181 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/pkce.d.ts +32 -0
- package/dist/lib/pkce.d.ts.map +1 -0
- package/dist/lib/pkce.js +101 -0
- package/dist/lib/pkce.js.map +1 -0
- package/dist/package.json +42 -0
- package/lib/auth-server.ts +267 -0
- package/lib/config.ts +161 -0
- package/lib/pkce.ts +79 -0
- package/package.json +17 -8
- package/tsconfig.json +28 -0
- package/bin/postgres-ai.js +0 -703
package/bin/postgres-ai.js
DELETED
|
@@ -1,703 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const { Command } = require("commander");
|
|
5
|
-
const pkg = require("../package.json");
|
|
6
|
-
|
|
7
|
-
function getConfig(opts) {
|
|
8
|
-
const apiKey = opts.apiKey || process.env.PGAI_API_KEY || "";
|
|
9
|
-
const baseUrl =
|
|
10
|
-
opts.baseUrl || process.env.PGAI_BASE_URL || "https://postgres.ai/api/general/";
|
|
11
|
-
return { apiKey, baseUrl };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const program = new Command();
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.name("postgres-ai")
|
|
18
|
-
.description("PostgresAI CLI")
|
|
19
|
-
.version(pkg.version)
|
|
20
|
-
.option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
|
|
21
|
-
.option(
|
|
22
|
-
"--base-url <url>",
|
|
23
|
-
"API base URL (overrides PGAI_BASE_URL)",
|
|
24
|
-
"https://postgres.ai/api/general/"
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const stub = (name) => async () => {
|
|
28
|
-
// Temporary stubs until Node parity is implemented
|
|
29
|
-
console.error(`${name}: not implemented in Node CLI yet; use bash CLI for now`);
|
|
30
|
-
process.exitCode = 2;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function resolvePaths() {
|
|
34
|
-
const path = require("path");
|
|
35
|
-
const fs = require("fs");
|
|
36
|
-
const projectDir = process.cwd();
|
|
37
|
-
const composeFile = path.resolve(projectDir, "docker-compose.yml");
|
|
38
|
-
const instancesFile = path.resolve(projectDir, "instances.yml");
|
|
39
|
-
return { fs, path, projectDir, composeFile, instancesFile };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getComposeCmd() {
|
|
43
|
-
const { spawnSync } = require("child_process");
|
|
44
|
-
const tryCmd = (cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }).status === 0;
|
|
45
|
-
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
46
|
-
if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function runCompose(args) {
|
|
51
|
-
const { composeFile } = resolvePaths();
|
|
52
|
-
const cmd = getComposeCmd();
|
|
53
|
-
if (!cmd) {
|
|
54
|
-
console.error("docker compose not found (need docker-compose or docker compose)");
|
|
55
|
-
process.exitCode = 1;
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
const { spawn } = require("child_process");
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
const child = spawn(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], { stdio: "inherit" });
|
|
61
|
-
child.on("close", (code) => resolve(code));
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
program.command("help", { isDefault: true }).description("show help").action(() => {
|
|
66
|
-
program.outputHelp();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Service lifecycle
|
|
70
|
-
program
|
|
71
|
-
.command("quickstart")
|
|
72
|
-
.description("complete setup (generate config, start services)")
|
|
73
|
-
.option("--demo", "demo mode", false)
|
|
74
|
-
.action(async () => {
|
|
75
|
-
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
76
|
-
if (code1 !== 0) {
|
|
77
|
-
process.exitCode = code1;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const code2 = await runCompose(["up", "-d"]);
|
|
81
|
-
if (code2 !== 0) process.exitCode = code2;
|
|
82
|
-
});
|
|
83
|
-
program
|
|
84
|
-
.command("install")
|
|
85
|
-
.description("prepare project (no-op in repo checkout)")
|
|
86
|
-
.action(async () => {
|
|
87
|
-
console.log("Project files present; nothing to install.");
|
|
88
|
-
});
|
|
89
|
-
program
|
|
90
|
-
.command("start")
|
|
91
|
-
.description("start services")
|
|
92
|
-
.action(async () => {
|
|
93
|
-
const code = await runCompose(["up", "-d"]);
|
|
94
|
-
if (code !== 0) process.exitCode = code;
|
|
95
|
-
});
|
|
96
|
-
program
|
|
97
|
-
.command("stop")
|
|
98
|
-
.description("stop services")
|
|
99
|
-
.action(async () => {
|
|
100
|
-
const code = await runCompose(["down"]);
|
|
101
|
-
if (code !== 0) process.exitCode = code;
|
|
102
|
-
});
|
|
103
|
-
program
|
|
104
|
-
.command("restart")
|
|
105
|
-
.description("restart services")
|
|
106
|
-
.action(async () => {
|
|
107
|
-
const code = await runCompose(["restart"]);
|
|
108
|
-
if (code !== 0) process.exitCode = code;
|
|
109
|
-
});
|
|
110
|
-
program
|
|
111
|
-
.command("status")
|
|
112
|
-
.description("show service status")
|
|
113
|
-
.action(async () => {
|
|
114
|
-
const code = await runCompose(["ps"]);
|
|
115
|
-
if (code !== 0) process.exitCode = code;
|
|
116
|
-
});
|
|
117
|
-
program
|
|
118
|
-
.command("logs [service]")
|
|
119
|
-
.option("-f, --follow", "follow logs", false)
|
|
120
|
-
.description("show logs for all or specific service")
|
|
121
|
-
.action(async (service, opts) => {
|
|
122
|
-
const args = ["logs"]; if (opts.follow) args.push("-f"); if (service) args.push(service);
|
|
123
|
-
const code = await runCompose(args);
|
|
124
|
-
if (code !== 0) process.exitCode = code;
|
|
125
|
-
});
|
|
126
|
-
program
|
|
127
|
-
.command("health")
|
|
128
|
-
.description("health check")
|
|
129
|
-
.action(async () => {
|
|
130
|
-
const { exec } = require("child_process");
|
|
131
|
-
const util = require("util");
|
|
132
|
-
const execPromise = util.promisify(exec);
|
|
133
|
-
|
|
134
|
-
console.log("Checking service health...\n");
|
|
135
|
-
|
|
136
|
-
const services = [
|
|
137
|
-
{ name: "Grafana", url: "http://localhost:3000/api/health" },
|
|
138
|
-
{ name: "Prometheus", url: "http://localhost:59090/-/healthy" },
|
|
139
|
-
{ name: "PGWatch (Postgres)", url: "http://localhost:58080/health" },
|
|
140
|
-
{ name: "PGWatch (Prometheus)", url: "http://localhost:58089/health" },
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
let allHealthy = true;
|
|
144
|
-
|
|
145
|
-
for (const service of services) {
|
|
146
|
-
try {
|
|
147
|
-
const { stdout, stderr } = await execPromise(
|
|
148
|
-
`curl -sf -o /dev/null -w "%{http_code}" ${service.url}`,
|
|
149
|
-
{ timeout: 5000 }
|
|
150
|
-
);
|
|
151
|
-
const code = stdout.trim();
|
|
152
|
-
if (code === "200") {
|
|
153
|
-
console.log(`✓ ${service.name}: healthy`);
|
|
154
|
-
} else {
|
|
155
|
-
console.log(`✗ ${service.name}: unhealthy (HTTP ${code})`);
|
|
156
|
-
allHealthy = false;
|
|
157
|
-
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.log(`✗ ${service.name}: unreachable`);
|
|
160
|
-
allHealthy = false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
console.log("");
|
|
165
|
-
if (allHealthy) {
|
|
166
|
-
console.log("All services are healthy");
|
|
167
|
-
} else {
|
|
168
|
-
console.log("Some services are unhealthy");
|
|
169
|
-
process.exitCode = 1;
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
program
|
|
173
|
-
.command("config")
|
|
174
|
-
.description("show configuration")
|
|
175
|
-
.action(async () => {
|
|
176
|
-
const { fs, projectDir, composeFile, instancesFile } = resolvePaths();
|
|
177
|
-
console.log(`Project Directory: ${projectDir}`);
|
|
178
|
-
console.log(`Docker Compose File: ${composeFile}`);
|
|
179
|
-
console.log(`Instances File: ${instancesFile}`);
|
|
180
|
-
if (fs.existsSync(instancesFile)) {
|
|
181
|
-
console.log("\nInstances configuration:\n");
|
|
182
|
-
const text = fs.readFileSync(instancesFile, "utf8");
|
|
183
|
-
process.stdout.write(text);
|
|
184
|
-
if (!/\n$/.test(text)) console.log();
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
program
|
|
188
|
-
.command("update-config")
|
|
189
|
-
.description("apply configuration (generate sources)")
|
|
190
|
-
.action(async () => {
|
|
191
|
-
const code = await runCompose(["run", "--rm", "sources-generator"]);
|
|
192
|
-
if (code !== 0) process.exitCode = code;
|
|
193
|
-
});
|
|
194
|
-
program
|
|
195
|
-
.command("update")
|
|
196
|
-
.description("update project")
|
|
197
|
-
.action(async () => {
|
|
198
|
-
const { exec } = require("child_process");
|
|
199
|
-
const util = require("util");
|
|
200
|
-
const execPromise = util.promisify(exec);
|
|
201
|
-
const fs = require("fs");
|
|
202
|
-
const path = require("path");
|
|
203
|
-
|
|
204
|
-
console.log("Updating PostgresAI monitoring stack...\n");
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
// Check if we're in a git repo
|
|
208
|
-
const gitDir = path.resolve(process.cwd(), ".git");
|
|
209
|
-
if (!fs.existsSync(gitDir)) {
|
|
210
|
-
console.error("Not a git repository. Cannot update.");
|
|
211
|
-
process.exitCode = 1;
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Fetch latest changes
|
|
216
|
-
console.log("Fetching latest changes...");
|
|
217
|
-
await execPromise("git fetch origin");
|
|
218
|
-
|
|
219
|
-
// Check current branch
|
|
220
|
-
const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
|
|
221
|
-
const currentBranch = branch.trim();
|
|
222
|
-
console.log(`Current branch: ${currentBranch}`);
|
|
223
|
-
|
|
224
|
-
// Pull latest changes
|
|
225
|
-
console.log("Pulling latest changes...");
|
|
226
|
-
const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
|
|
227
|
-
console.log(pullOut);
|
|
228
|
-
|
|
229
|
-
// Update Docker images
|
|
230
|
-
console.log("\nUpdating Docker images...");
|
|
231
|
-
const code = await runCompose(["pull"]);
|
|
232
|
-
|
|
233
|
-
if (code === 0) {
|
|
234
|
-
console.log("\n✓ Update completed successfully");
|
|
235
|
-
console.log("\nTo apply updates, restart services:");
|
|
236
|
-
console.log(" postgres-ai restart");
|
|
237
|
-
} else {
|
|
238
|
-
console.error("\n✗ Docker image update failed");
|
|
239
|
-
process.exitCode = 1;
|
|
240
|
-
}
|
|
241
|
-
} catch (error) {
|
|
242
|
-
console.error(`Update failed: ${error.message}`);
|
|
243
|
-
process.exitCode = 1;
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
program
|
|
247
|
-
.command("reset [service]")
|
|
248
|
-
.description("reset all or specific service")
|
|
249
|
-
.action(async (service) => {
|
|
250
|
-
const readline = require("readline");
|
|
251
|
-
const rl = readline.createInterface({
|
|
252
|
-
input: process.stdin,
|
|
253
|
-
output: process.stdout,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
if (service) {
|
|
260
|
-
// Reset specific service
|
|
261
|
-
console.log(`\nThis will stop '${service}', remove its volume, and restart it.`);
|
|
262
|
-
console.log("All data for this service will be lost!\n");
|
|
263
|
-
|
|
264
|
-
const answer = await question("Continue? (y/N): ");
|
|
265
|
-
if (answer.toLowerCase() !== "y") {
|
|
266
|
-
console.log("Cancelled");
|
|
267
|
-
rl.close();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
console.log(`\nStopping ${service}...`);
|
|
272
|
-
await runCompose(["stop", service]);
|
|
273
|
-
|
|
274
|
-
console.log(`Removing volume for ${service}...`);
|
|
275
|
-
await runCompose(["rm", "-f", "-v", service]);
|
|
276
|
-
|
|
277
|
-
console.log(`Restarting ${service}...`);
|
|
278
|
-
const code = await runCompose(["up", "-d", service]);
|
|
279
|
-
|
|
280
|
-
if (code === 0) {
|
|
281
|
-
console.log(`\n✓ Service '${service}' has been reset`);
|
|
282
|
-
} else {
|
|
283
|
-
console.error(`\n✗ Failed to restart '${service}'`);
|
|
284
|
-
process.exitCode = 1;
|
|
285
|
-
}
|
|
286
|
-
} else {
|
|
287
|
-
// Reset all services
|
|
288
|
-
console.log("\nThis will stop all services and remove all data!");
|
|
289
|
-
console.log("Volumes, networks, and containers will be deleted.\n");
|
|
290
|
-
|
|
291
|
-
const answer = await question("Continue? (y/N): ");
|
|
292
|
-
if (answer.toLowerCase() !== "y") {
|
|
293
|
-
console.log("Cancelled");
|
|
294
|
-
rl.close();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
console.log("\nStopping services and removing data...");
|
|
299
|
-
const downCode = await runCompose(["down", "-v"]);
|
|
300
|
-
|
|
301
|
-
if (downCode === 0) {
|
|
302
|
-
console.log("✓ Environment reset completed - all containers and data removed");
|
|
303
|
-
} else {
|
|
304
|
-
console.error("✗ Reset failed");
|
|
305
|
-
process.exitCode = 1;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
rl.close();
|
|
310
|
-
} catch (error) {
|
|
311
|
-
rl.close();
|
|
312
|
-
console.error(`Reset failed: ${error.message}`);
|
|
313
|
-
process.exitCode = 1;
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
program
|
|
317
|
-
.command("clean")
|
|
318
|
-
.description("cleanup artifacts")
|
|
319
|
-
.action(async () => {
|
|
320
|
-
const { exec } = require("child_process");
|
|
321
|
-
const util = require("util");
|
|
322
|
-
const execPromise = util.promisify(exec);
|
|
323
|
-
|
|
324
|
-
console.log("Cleaning up Docker resources...\n");
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
// Remove stopped containers
|
|
328
|
-
const { stdout: containers } = await execPromise("docker ps -aq --filter 'status=exited'");
|
|
329
|
-
if (containers.trim()) {
|
|
330
|
-
await execPromise(`docker rm ${containers.trim().split('\n').join(' ')}`);
|
|
331
|
-
console.log("✓ Removed stopped containers");
|
|
332
|
-
} else {
|
|
333
|
-
console.log("✓ No stopped containers to remove");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Remove unused volumes
|
|
337
|
-
const { stdout: volumeOut } = await execPromise("docker volume prune -f");
|
|
338
|
-
console.log("✓ Removed unused volumes");
|
|
339
|
-
|
|
340
|
-
// Remove unused networks
|
|
341
|
-
const { stdout: networkOut } = await execPromise("docker network prune -f");
|
|
342
|
-
console.log("✓ Removed unused networks");
|
|
343
|
-
|
|
344
|
-
// Remove dangling images
|
|
345
|
-
const { stdout: imageOut } = await execPromise("docker image prune -f");
|
|
346
|
-
console.log("✓ Removed dangling images");
|
|
347
|
-
|
|
348
|
-
console.log("\nCleanup completed");
|
|
349
|
-
} catch (error) {
|
|
350
|
-
console.error(`Error during cleanup: ${error.message}`);
|
|
351
|
-
process.exitCode = 1;
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
program
|
|
355
|
-
.command("shell <service>")
|
|
356
|
-
.description("open service shell")
|
|
357
|
-
.action(async (service) => {
|
|
358
|
-
const code = await runCompose(["exec", "-T", service, "/bin/sh"]);
|
|
359
|
-
if (code !== 0) process.exitCode = code;
|
|
360
|
-
});
|
|
361
|
-
program
|
|
362
|
-
.command("check")
|
|
363
|
-
.description("system readiness check")
|
|
364
|
-
.action(async () => {
|
|
365
|
-
const code = await runCompose(["ps"]);
|
|
366
|
-
if (code !== 0) process.exitCode = code;
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Instance management
|
|
370
|
-
program
|
|
371
|
-
.command("list-instances")
|
|
372
|
-
.description("list instances")
|
|
373
|
-
.action(async () => {
|
|
374
|
-
const fs = require("fs");
|
|
375
|
-
const path = require("path");
|
|
376
|
-
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
377
|
-
if (!fs.existsSync(instancesPath)) {
|
|
378
|
-
console.error(`instances.yml not found in ${process.cwd()}`);
|
|
379
|
-
process.exitCode = 1;
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
const content = fs.readFileSync(instancesPath, "utf8");
|
|
383
|
-
const lines = content.split(/\r?\n/);
|
|
384
|
-
let currentName = "";
|
|
385
|
-
let printed = false;
|
|
386
|
-
const collected = [];
|
|
387
|
-
for (const line of lines) {
|
|
388
|
-
const m = line.match(/^-[\t ]*name:[\t ]*(.+)$/);
|
|
389
|
-
if (m) {
|
|
390
|
-
currentName = m[1].trim();
|
|
391
|
-
collected.push(currentName);
|
|
392
|
-
printed = true;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// Hide demo placeholder if that's the only entry
|
|
396
|
-
if (printed) {
|
|
397
|
-
const filtered = collected.filter((n) => n !== "target-database");
|
|
398
|
-
const list = filtered.length > 0 ? filtered : [];
|
|
399
|
-
if (list.length === 0) {
|
|
400
|
-
console.log("No instances configured");
|
|
401
|
-
console.log("");
|
|
402
|
-
console.log("To add an instance:");
|
|
403
|
-
console.log(" postgres-ai add-instance <connection-string> <name>");
|
|
404
|
-
console.log("");
|
|
405
|
-
console.log("Example:");
|
|
406
|
-
console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
for (const n of list) console.log(`Instance: ${n}`);
|
|
410
|
-
} else {
|
|
411
|
-
console.log("No instances found");
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
program
|
|
415
|
-
.command("add-instance [connStr] [name]")
|
|
416
|
-
.description("add instance")
|
|
417
|
-
.action(async (connStr, name) => {
|
|
418
|
-
const fs = require("fs");
|
|
419
|
-
const path = require("path");
|
|
420
|
-
const file = path.resolve(process.cwd(), "instances.yml");
|
|
421
|
-
if (!connStr) {
|
|
422
|
-
console.error("Connection string required: postgresql://user:pass@host:port/db");
|
|
423
|
-
process.exitCode = 1;
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
427
|
-
if (!m) {
|
|
428
|
-
console.error("Invalid connection string format");
|
|
429
|
-
process.exitCode = 1;
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const host = m[3];
|
|
433
|
-
const db = m[5];
|
|
434
|
-
const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
435
|
-
const lineStart = `- name: ${instanceName}`;
|
|
436
|
-
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`;
|
|
437
|
-
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
438
|
-
if (new RegExp(`^${lineStart}$`, "m").test(content)) {
|
|
439
|
-
console.error(`Instance '${instanceName}' already exists`);
|
|
440
|
-
process.exitCode = 1;
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
|
|
444
|
-
console.log(`Instance '${instanceName}' added`);
|
|
445
|
-
});
|
|
446
|
-
program
|
|
447
|
-
.command("remove-instance <name>")
|
|
448
|
-
.description("remove instance")
|
|
449
|
-
.action(async (name) => {
|
|
450
|
-
const fs = require("fs");
|
|
451
|
-
const path = require("path");
|
|
452
|
-
const file = path.resolve(process.cwd(), "instances.yml");
|
|
453
|
-
if (!fs.existsSync(file)) {
|
|
454
|
-
console.error("instances.yml not found");
|
|
455
|
-
process.exitCode = 1;
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
const text = fs.readFileSync(file, "utf8");
|
|
459
|
-
const lines = text.split(/\r?\n/);
|
|
460
|
-
const out = [];
|
|
461
|
-
let skip = false;
|
|
462
|
-
for (let i = 0; i < lines.length; i++) {
|
|
463
|
-
const line = lines[i];
|
|
464
|
-
const isStart = /^-[\t ]*name:[\t ]*(.+)$/.test(line);
|
|
465
|
-
if (isStart) {
|
|
466
|
-
const n = line.replace(/^-[\t ]*name:[\t ]*/, "").trim();
|
|
467
|
-
if (n === name) {
|
|
468
|
-
skip = true;
|
|
469
|
-
continue;
|
|
470
|
-
} else if (skip) {
|
|
471
|
-
skip = false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
if (!skip) out.push(line);
|
|
475
|
-
}
|
|
476
|
-
if (out.join("\n") === text) {
|
|
477
|
-
console.error(`Instance '${name}' not found`);
|
|
478
|
-
process.exitCode = 1;
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
fs.writeFileSync(file, out.join("\n"), "utf8");
|
|
482
|
-
console.log(`Instance '${name}' removed`);
|
|
483
|
-
});
|
|
484
|
-
program
|
|
485
|
-
.command("test-instance <name>")
|
|
486
|
-
.description("test instance connectivity")
|
|
487
|
-
.action(async (name) => {
|
|
488
|
-
const fs = require("fs");
|
|
489
|
-
const path = require("path");
|
|
490
|
-
const { exec } = require("child_process");
|
|
491
|
-
const util = require("util");
|
|
492
|
-
const execPromise = util.promisify(exec);
|
|
493
|
-
|
|
494
|
-
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
495
|
-
if (!fs.existsSync(instancesPath)) {
|
|
496
|
-
console.error("instances.yml not found");
|
|
497
|
-
process.exitCode = 1;
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const content = fs.readFileSync(instancesPath, "utf8");
|
|
502
|
-
const lines = content.split(/\r?\n/);
|
|
503
|
-
let connStr = "";
|
|
504
|
-
let foundInstance = false;
|
|
505
|
-
|
|
506
|
-
for (let i = 0; i < lines.length; i++) {
|
|
507
|
-
const nameLine = lines[i].match(/^-[\t ]*name:[\t ]*(.+)$/);
|
|
508
|
-
if (nameLine && nameLine[1].trim() === name) {
|
|
509
|
-
foundInstance = true;
|
|
510
|
-
// Look for conn_str in next lines
|
|
511
|
-
for (let j = i + 1; j < lines.length && j < i + 15; j++) {
|
|
512
|
-
const connLine = lines[j].match(/^[\t ]*conn_str:[\t ]*(.+)$/);
|
|
513
|
-
if (connLine) {
|
|
514
|
-
connStr = connLine[1].trim();
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
// Stop at next instance
|
|
518
|
-
if (lines[j].match(/^-[\t ]*name:/)) break;
|
|
519
|
-
}
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (!foundInstance) {
|
|
525
|
-
console.error(`Instance '${name}' not found`);
|
|
526
|
-
process.exitCode = 1;
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!connStr) {
|
|
531
|
-
console.error(`Connection string not found for instance '${name}'`);
|
|
532
|
-
process.exitCode = 1;
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
console.log(`Testing connection to '${name}'...`);
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
const { stdout, stderr } = await execPromise(
|
|
540
|
-
`psql "${connStr}" -c "SELECT version();" --no-psqlrc`,
|
|
541
|
-
{ timeout: 10000, env: { ...process.env, PAGER: 'cat' } }
|
|
542
|
-
);
|
|
543
|
-
console.log(`✓ Connection successful`);
|
|
544
|
-
console.log(stdout.trim());
|
|
545
|
-
} catch (error) {
|
|
546
|
-
console.error(`✗ Connection failed: ${error.message}`);
|
|
547
|
-
process.exitCode = 1;
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// API key and grafana
|
|
552
|
-
program
|
|
553
|
-
.command("add-key <apiKey>")
|
|
554
|
-
.description("store API key")
|
|
555
|
-
.action(async (apiKey) => {
|
|
556
|
-
const fs = require("fs");
|
|
557
|
-
const path = require("path");
|
|
558
|
-
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
559
|
-
const existing = fs.existsSync(cfgPath) ? fs.readFileSync(cfgPath, "utf8") : "";
|
|
560
|
-
const filtered = existing
|
|
561
|
-
.split(/\r?\n/)
|
|
562
|
-
.filter((l) => !/^api_key=/.test(l))
|
|
563
|
-
.join("\n")
|
|
564
|
-
.replace(/\n+$/g, "");
|
|
565
|
-
const next = filtered.length ? `${filtered}\napi_key=${apiKey}\n` : `api_key=${apiKey}\n`;
|
|
566
|
-
fs.writeFileSync(cfgPath, next, "utf8");
|
|
567
|
-
console.log("API key saved to .pgwatch-config");
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
program
|
|
571
|
-
.command("show-key")
|
|
572
|
-
.description("show API key (masked)")
|
|
573
|
-
.action(async () => {
|
|
574
|
-
const fs = require("fs");
|
|
575
|
-
const path = require("path");
|
|
576
|
-
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
577
|
-
if (!fs.existsSync(cfgPath)) {
|
|
578
|
-
console.log("No API key configured");
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const content = fs.readFileSync(cfgPath, "utf8");
|
|
582
|
-
const m = content.match(/^api_key=(.+)$/m);
|
|
583
|
-
if (!m) {
|
|
584
|
-
console.log("No API key configured");
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
const key = m[1].trim();
|
|
588
|
-
if (!key) {
|
|
589
|
-
console.log("No API key configured");
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const mask = (k) => (k.length <= 8 ? "****" : `${k.slice(0, 4)}${"*".repeat(k.length - 8)}${k.slice(-4)}`);
|
|
593
|
-
console.log(`Current API key: ${mask(key)}`);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
program
|
|
597
|
-
.command("remove-key")
|
|
598
|
-
.description("remove API key")
|
|
599
|
-
.action(async () => {
|
|
600
|
-
const fs = require("fs");
|
|
601
|
-
const path = require("path");
|
|
602
|
-
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
603
|
-
if (!fs.existsSync(cfgPath)) {
|
|
604
|
-
console.log("No API key configured");
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const content = fs.readFileSync(cfgPath, "utf8");
|
|
608
|
-
const filtered = content
|
|
609
|
-
.split(/\r?\n/)
|
|
610
|
-
.filter((l) => !/^api_key=/.test(l))
|
|
611
|
-
.join("\n")
|
|
612
|
-
.replace(/\n+$/g, "\n");
|
|
613
|
-
fs.writeFileSync(cfgPath, filtered, "utf8");
|
|
614
|
-
console.log("API key removed");
|
|
615
|
-
});
|
|
616
|
-
program
|
|
617
|
-
.command("generate-grafana-password")
|
|
618
|
-
.description("generate Grafana password")
|
|
619
|
-
.action(async () => {
|
|
620
|
-
const fs = require("fs");
|
|
621
|
-
const path = require("path");
|
|
622
|
-
const { exec } = require("child_process");
|
|
623
|
-
const util = require("util");
|
|
624
|
-
const execPromise = util.promisify(exec);
|
|
625
|
-
|
|
626
|
-
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
627
|
-
|
|
628
|
-
try {
|
|
629
|
-
// Generate secure password using openssl
|
|
630
|
-
const { stdout: password } = await execPromise(
|
|
631
|
-
"openssl rand -base64 12 | tr -d '\n'"
|
|
632
|
-
);
|
|
633
|
-
const newPassword = password.trim();
|
|
634
|
-
|
|
635
|
-
if (!newPassword) {
|
|
636
|
-
console.error("Failed to generate password");
|
|
637
|
-
process.exitCode = 1;
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Read existing config
|
|
642
|
-
let config = "";
|
|
643
|
-
if (fs.existsSync(cfgPath)) {
|
|
644
|
-
config = fs.readFileSync(cfgPath, "utf8");
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Update or add grafana_password
|
|
648
|
-
const lines = config.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
649
|
-
lines.push(`grafana_password=${newPassword}`);
|
|
650
|
-
|
|
651
|
-
// Write back
|
|
652
|
-
fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
|
|
653
|
-
|
|
654
|
-
console.log("✓ New Grafana password generated and saved");
|
|
655
|
-
console.log("\nNew credentials:");
|
|
656
|
-
console.log(" URL: http://localhost:3000");
|
|
657
|
-
console.log(" Username: monitor");
|
|
658
|
-
console.log(` Password: ${newPassword}`);
|
|
659
|
-
console.log("\nRestart Grafana to apply:");
|
|
660
|
-
console.log(" postgres-ai restart grafana");
|
|
661
|
-
} catch (error) {
|
|
662
|
-
console.error(`Failed to generate password: ${error.message}`);
|
|
663
|
-
console.error("\nNote: This command requires 'openssl' to be installed");
|
|
664
|
-
process.exitCode = 1;
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
program
|
|
668
|
-
.command("show-grafana-credentials")
|
|
669
|
-
.description("show Grafana credentials")
|
|
670
|
-
.action(async () => {
|
|
671
|
-
const fs = require("fs");
|
|
672
|
-
const path = require("path");
|
|
673
|
-
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
674
|
-
if (!fs.existsSync(cfgPath)) {
|
|
675
|
-
console.error("Configuration file not found. Run 'quickstart' first.");
|
|
676
|
-
process.exitCode = 1;
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const content = fs.readFileSync(cfgPath, "utf8");
|
|
680
|
-
const lines = content.split(/\r?\n/);
|
|
681
|
-
let password = "";
|
|
682
|
-
for (const line of lines) {
|
|
683
|
-
const m = line.match(/^grafana_password=(.+)$/);
|
|
684
|
-
if (m) {
|
|
685
|
-
password = m[1].trim();
|
|
686
|
-
break;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (!password) {
|
|
690
|
-
console.error("Grafana password not found in configuration");
|
|
691
|
-
process.exitCode = 1;
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
console.log("\nGrafana credentials:");
|
|
695
|
-
console.log(" URL: http://localhost:3000");
|
|
696
|
-
console.log(" Username: monitor");
|
|
697
|
-
console.log(` Password: ${password}`);
|
|
698
|
-
console.log("");
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
program.parseAsync(process.argv);
|
|
702
|
-
|
|
703
|
-
|