postgresai 0.14.0-dev.36 → 0.14.0-dev.38
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 +23 -23
- package/bin/postgres-ai.ts +357 -24
- package/dist/bin/postgres-ai.js +331 -23
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/checkup-api.d.ts +33 -0
- package/dist/lib/checkup-api.d.ts.map +1 -0
- package/dist/lib/checkup-api.js +187 -0
- package/dist/lib/checkup-api.js.map +1 -0
- package/dist/lib/checkup.d.ts +153 -0
- package/dist/lib/checkup.d.ts.map +1 -0
- package/dist/lib/checkup.js +536 -0
- package/dist/lib/checkup.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/checkup-api.ts +177 -0
- package/lib/checkup.ts +622 -0
- package/lib/config.ts +3 -0
- package/package.json +1 -1
- package/reports/A002.json +23 -0
- package/reports/A003.json +3343 -0
- package/reports/A004.json +134 -0
- package/reports/A007.json +683 -0
- package/reports/A013.json +23 -0
- package/test/checkup.test.cjs +645 -0
- package/test/init.integration.test.cjs +10 -10
- package/test/init.test.cjs +73 -4
package/README.md
CHANGED
|
@@ -50,26 +50,26 @@ If you want `npx pgai ...` as a shorthand for `npx postgresai ...`, install the
|
|
|
50
50
|
npx pgai --help
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## prepare-db (create monitoring user in Postgres)
|
|
54
54
|
|
|
55
55
|
This command creates (or updates) the `postgres_ai_mon` user, creates the required view(s), and grants the permissions described in the root `README.md` (it is idempotent). Where supported, it also enables observability extensions described there.
|
|
56
56
|
|
|
57
57
|
Run without installing (positional connection string):
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
npx postgresai
|
|
60
|
+
npx postgresai prepare-db postgresql://admin@host:5432/dbname
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
It also accepts libpq
|
|
63
|
+
It also accepts libpq "conninfo" syntax:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npx postgresai
|
|
66
|
+
npx postgresai prepare-db "dbname=dbname host=host user=admin"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
And psql-like options:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
npx postgresai
|
|
72
|
+
npx postgresai prepare-db -h host -p 5432 -U admin -d dbname
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
Password input options (in priority order):
|
|
@@ -83,7 +83,7 @@ By default, the generated password is printed **only in interactive (TTY) mode**
|
|
|
83
83
|
Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
npx postgresai
|
|
86
|
+
npx postgresai prepare-db postgresql://admin@host:5432/dbname --skip-optional-permissions
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
### Print SQL / dry run
|
|
@@ -91,7 +91,7 @@ npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissi
|
|
|
91
91
|
To see what SQL would be executed (passwords redacted by default):
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
npx postgresai
|
|
94
|
+
npx postgresai prepare-db postgresql://admin@host:5432/dbname --print-sql
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
### Verify and password reset
|
|
@@ -99,13 +99,13 @@ npx postgresai init postgresql://admin@host:5432/dbname --print-sql
|
|
|
99
99
|
Verify that everything is configured as expected (no changes):
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
npx postgresai
|
|
102
|
+
npx postgresai prepare-db postgresql://admin@host:5432/dbname --verify
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Reset monitoring user password only (no other changes):
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
npx postgresai
|
|
108
|
+
npx postgresai prepare-db postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
## Quick start
|
|
@@ -126,17 +126,17 @@ This will:
|
|
|
126
126
|
|
|
127
127
|
Start monitoring with demo database:
|
|
128
128
|
```bash
|
|
129
|
-
postgres-ai mon
|
|
129
|
+
postgres-ai mon local-install --demo
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
Start monitoring with your own database:
|
|
133
133
|
```bash
|
|
134
|
-
postgres-ai mon
|
|
134
|
+
postgres-ai mon local-install --db-url postgresql://user:pass@host:5432/db
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
Complete automated setup with API key and database:
|
|
138
138
|
```bash
|
|
139
|
-
postgres-ai mon
|
|
139
|
+
postgres-ai mon local-install --api-key your_key --db-url postgresql://user:pass@host:5432/db -y
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
This will:
|
|
@@ -153,12 +153,12 @@ This will:
|
|
|
153
153
|
#### Service lifecycle
|
|
154
154
|
```bash
|
|
155
155
|
# Complete setup with various options
|
|
156
|
-
postgres-ai mon
|
|
157
|
-
postgres-ai mon
|
|
158
|
-
postgres-ai mon
|
|
159
|
-
postgres-ai mon
|
|
160
|
-
postgres-ai mon
|
|
161
|
-
postgres-ai mon
|
|
156
|
+
postgres-ai mon local-install # Interactive setup for production
|
|
157
|
+
postgres-ai mon local-install --demo # Demo mode with sample database
|
|
158
|
+
postgres-ai mon local-install --api-key <key> # Setup with API key
|
|
159
|
+
postgres-ai mon local-install --db-url <url> # Setup with database URL
|
|
160
|
+
postgres-ai mon local-install --api-key <key> --db-url <url> # Complete automated setup
|
|
161
|
+
postgres-ai mon local-install -y # Auto-accept all defaults
|
|
162
162
|
|
|
163
163
|
# Service management
|
|
164
164
|
postgres-ai mon start # Start monitoring services
|
|
@@ -168,7 +168,7 @@ postgres-ai mon status # Show monitoring services status
|
|
|
168
168
|
postgres-ai mon health [--wait <sec>] # Check monitoring services health
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
#####
|
|
171
|
+
##### local-install options
|
|
172
172
|
- `--demo` - Demo mode with sample database (testing only, cannot use with --api-key)
|
|
173
173
|
- `--api-key <key>` - Postgres AI API key for automated report uploads
|
|
174
174
|
- `--db-url <url>` - PostgreSQL connection URL to monitor (format: `postgresql://user:pass@host:port/db`)
|
|
@@ -256,10 +256,10 @@ postgres-ai mon show-grafana-credentials # Show Grafana credentials
|
|
|
256
256
|
|
|
257
257
|
### Authentication and API key management
|
|
258
258
|
```bash
|
|
259
|
-
postgres-ai auth
|
|
260
|
-
postgres-ai
|
|
261
|
-
postgres-ai show-key
|
|
262
|
-
postgres-ai remove-key
|
|
259
|
+
postgres-ai auth # Authenticate via browser (OAuth)
|
|
260
|
+
postgres-ai auth --set-key <key> # Store API key directly
|
|
261
|
+
postgres-ai show-key # Show stored key (masked)
|
|
262
|
+
postgres-ai remove-key # Remove stored key
|
|
263
263
|
```
|
|
264
264
|
|
|
265
265
|
## Configuration
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -17,10 +17,68 @@ import { startMcpServer } from "../lib/mcp-server";
|
|
|
17
17
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
18
18
|
import { resolveBaseUrls } from "../lib/util";
|
|
19
19
|
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
20
|
+
import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
|
|
21
|
+
import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay } from "../lib/checkup-api";
|
|
20
22
|
|
|
21
23
|
const execPromise = promisify(exec);
|
|
22
24
|
const execFilePromise = promisify(execFile);
|
|
23
25
|
|
|
26
|
+
function expandHomePath(p: string): string {
|
|
27
|
+
const s = (p || "").trim();
|
|
28
|
+
if (!s) return s;
|
|
29
|
+
if (s === "~") return os.homedir();
|
|
30
|
+
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
31
|
+
return path.join(os.homedir(), s.slice(2));
|
|
32
|
+
}
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createTtySpinner(
|
|
37
|
+
enabled: boolean,
|
|
38
|
+
initialText: string
|
|
39
|
+
): { update: (text: string) => void; stop: (finalText?: string) => void } {
|
|
40
|
+
if (!enabled) {
|
|
41
|
+
return {
|
|
42
|
+
update: () => {},
|
|
43
|
+
stop: () => {},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const frames = ["|", "/", "-", "\\"];
|
|
48
|
+
const startTs = Date.now();
|
|
49
|
+
let text = initialText;
|
|
50
|
+
let frameIdx = 0;
|
|
51
|
+
let stopped = false;
|
|
52
|
+
|
|
53
|
+
const render = (): void => {
|
|
54
|
+
if (stopped) return;
|
|
55
|
+
const elapsedSec = ((Date.now() - startTs) / 1000).toFixed(1);
|
|
56
|
+
const frame = frames[frameIdx % frames.length]!;
|
|
57
|
+
frameIdx += 1;
|
|
58
|
+
process.stdout.write(`\r\x1b[2K${frame} ${text} (${elapsedSec}s)`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const timer = setInterval(render, 120);
|
|
62
|
+
render(); // immediate feedback
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
update: (t: string) => {
|
|
66
|
+
text = t;
|
|
67
|
+
render();
|
|
68
|
+
},
|
|
69
|
+
stop: (finalText?: string) => {
|
|
70
|
+
if (stopped) return;
|
|
71
|
+
stopped = true;
|
|
72
|
+
clearInterval(timer);
|
|
73
|
+
process.stdout.write("\r\x1b[2K");
|
|
74
|
+
if (finalText && finalText.trim()) {
|
|
75
|
+
process.stdout.write(finalText);
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write("\n");
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
24
82
|
/**
|
|
25
83
|
* CLI configuration options
|
|
26
84
|
*/
|
|
@@ -203,8 +261,22 @@ program
|
|
|
203
261
|
);
|
|
204
262
|
|
|
205
263
|
program
|
|
206
|
-
.command("
|
|
207
|
-
.description("
|
|
264
|
+
.command("set-default-project <project>")
|
|
265
|
+
.description("store default project for checkup uploads")
|
|
266
|
+
.action(async (project: string) => {
|
|
267
|
+
const value = (project || "").trim();
|
|
268
|
+
if (!value) {
|
|
269
|
+
console.error("Error: project is required");
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
config.writeConfig({ defaultProject: value });
|
|
274
|
+
console.log(`Default project saved: ${value}`);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
program
|
|
278
|
+
.command("prepare-db [conn]")
|
|
279
|
+
.description("Prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
|
|
208
280
|
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
|
|
209
281
|
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
|
|
210
282
|
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
|
|
@@ -223,9 +295,9 @@ program
|
|
|
223
295
|
[
|
|
224
296
|
"",
|
|
225
297
|
"Examples:",
|
|
226
|
-
" postgresai
|
|
227
|
-
" postgresai
|
|
228
|
-
" postgresai
|
|
298
|
+
" postgresai prepare-db postgresql://admin@host:5432/dbname",
|
|
299
|
+
" postgresai prepare-db \"dbname=dbname host=host user=admin\"",
|
|
300
|
+
" postgresai prepare-db -h host -p 5432 -U admin -d dbname",
|
|
229
301
|
"",
|
|
230
302
|
"Admin password:",
|
|
231
303
|
" --admin-password <password> or PGPASSWORD=... (libpq standard)",
|
|
@@ -247,16 +319,16 @@ program
|
|
|
247
319
|
" PGAI_MON_PASSWORD — monitoring password",
|
|
248
320
|
"",
|
|
249
321
|
"Inspect SQL without applying changes:",
|
|
250
|
-
" postgresai
|
|
322
|
+
" postgresai prepare-db <conn> --print-sql",
|
|
251
323
|
"",
|
|
252
324
|
"Verify setup (no changes):",
|
|
253
|
-
" postgresai
|
|
325
|
+
" postgresai prepare-db <conn> --verify",
|
|
254
326
|
"",
|
|
255
327
|
"Reset monitoring password only:",
|
|
256
|
-
" postgresai
|
|
328
|
+
" postgresai prepare-db <conn> --reset-password --password '...'",
|
|
257
329
|
"",
|
|
258
330
|
"Offline SQL plan (no DB connection):",
|
|
259
|
-
" postgresai
|
|
331
|
+
" postgresai prepare-db --print-sql",
|
|
260
332
|
].join("\n")
|
|
261
333
|
)
|
|
262
334
|
.action(async (conn: string | undefined, opts: {
|
|
@@ -336,7 +408,7 @@ program
|
|
|
336
408
|
});
|
|
337
409
|
} catch (e) {
|
|
338
410
|
const msg = e instanceof Error ? e.message : String(e);
|
|
339
|
-
console.error(`Error:
|
|
411
|
+
console.error(`Error: prepare-db: ${msg}`);
|
|
340
412
|
// When connection details are missing, show full init help (options + examples).
|
|
341
413
|
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
342
414
|
console.error("");
|
|
@@ -372,14 +444,14 @@ program
|
|
|
372
444
|
includeOptionalPermissions,
|
|
373
445
|
});
|
|
374
446
|
if (v.ok) {
|
|
375
|
-
console.log("✓
|
|
447
|
+
console.log("✓ prepare-db verify: OK");
|
|
376
448
|
if (v.missingOptional.length > 0) {
|
|
377
449
|
console.log("⚠ Optional items missing:");
|
|
378
450
|
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
379
451
|
}
|
|
380
452
|
return;
|
|
381
453
|
}
|
|
382
|
-
console.error("✗
|
|
454
|
+
console.error("✗ prepare-db verify failed: missing required items");
|
|
383
455
|
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
384
456
|
if (v.missingOptional.length > 0) {
|
|
385
457
|
console.error("Optional items missing:");
|
|
@@ -455,7 +527,7 @@ program
|
|
|
455
527
|
|
|
456
528
|
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
457
529
|
|
|
458
|
-
console.log(opts.resetPassword ? "✓
|
|
530
|
+
console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
|
|
459
531
|
if (skippedOptional.length > 0) {
|
|
460
532
|
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
461
533
|
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
@@ -477,7 +549,7 @@ program
|
|
|
477
549
|
if (!message || message === "[object Object]") {
|
|
478
550
|
message = "Unknown error";
|
|
479
551
|
}
|
|
480
|
-
console.error(`Error:
|
|
552
|
+
console.error(`Error: prepare-db: ${message}`);
|
|
481
553
|
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
482
554
|
const stepMatch =
|
|
483
555
|
typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
@@ -529,6 +601,250 @@ program
|
|
|
529
601
|
}
|
|
530
602
|
});
|
|
531
603
|
|
|
604
|
+
program
|
|
605
|
+
.command("checkup [conn]")
|
|
606
|
+
.description("generate health check reports directly from PostgreSQL (express mode)")
|
|
607
|
+
.option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
|
|
608
|
+
.option("--node-name <name>", "node name for reports", "node-01")
|
|
609
|
+
.option("--output <path>", "output directory for JSON files")
|
|
610
|
+
.option("--json", "output to stdout as JSON instead of files")
|
|
611
|
+
.option("--upload", "create a remote checkup report and upload JSON results (requires API key)", false)
|
|
612
|
+
.option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject)")
|
|
613
|
+
.addHelpText(
|
|
614
|
+
"after",
|
|
615
|
+
[
|
|
616
|
+
"",
|
|
617
|
+
"Available checks:",
|
|
618
|
+
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
|
|
619
|
+
"",
|
|
620
|
+
"Examples:",
|
|
621
|
+
" postgresai checkup postgresql://user:pass@host:5432/db",
|
|
622
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --check-id A003",
|
|
623
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --json",
|
|
624
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
625
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --upload --project my_project",
|
|
626
|
+
" postgresai set-default-project my_project",
|
|
627
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --upload",
|
|
628
|
+
].join("\n")
|
|
629
|
+
)
|
|
630
|
+
.action(async (conn: string | undefined, opts: {
|
|
631
|
+
checkId: string;
|
|
632
|
+
nodeName: string;
|
|
633
|
+
output?: string;
|
|
634
|
+
json?: boolean;
|
|
635
|
+
upload?: boolean;
|
|
636
|
+
project?: string;
|
|
637
|
+
}, cmd: Command) => {
|
|
638
|
+
if (!conn) {
|
|
639
|
+
// No args — show help like other commands do (instead of a bare error).
|
|
640
|
+
cmd.outputHelp();
|
|
641
|
+
process.exitCode = 1;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Preflight: validate/create output directory BEFORE connecting / running checks.
|
|
646
|
+
// This avoids waiting on network/DB work only to fail at the very end.
|
|
647
|
+
let outputPath: string | undefined;
|
|
648
|
+
if (opts.output && !opts.json) {
|
|
649
|
+
const outputDir = expandHomePath(opts.output);
|
|
650
|
+
outputPath = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
|
|
651
|
+
if (!fs.existsSync(outputPath)) {
|
|
652
|
+
try {
|
|
653
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
654
|
+
} catch (e) {
|
|
655
|
+
const errAny = e as any;
|
|
656
|
+
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
657
|
+
const msg = errAny instanceof Error ? errAny.message : String(errAny);
|
|
658
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") {
|
|
659
|
+
console.error(`Error: Failed to create output directory: ${outputPath}`);
|
|
660
|
+
console.error(`Reason: ${msg}`);
|
|
661
|
+
console.error("Tip: choose a writable path, e.g. --output ./reports or --output ~/reports");
|
|
662
|
+
process.exitCode = 1;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Preflight: validate upload flags/credentials BEFORE connecting / running checks.
|
|
671
|
+
// This allows "fast-fail" for missing API key / project name.
|
|
672
|
+
let uploadCfg:
|
|
673
|
+
| { apiKey: string; apiBaseUrl: string; project: string; epoch: number }
|
|
674
|
+
| undefined;
|
|
675
|
+
if (opts.upload) {
|
|
676
|
+
const rootOpts = program.opts() as CliOptions;
|
|
677
|
+
const { apiKey } = getConfig(rootOpts);
|
|
678
|
+
if (!apiKey) {
|
|
679
|
+
console.error("Error: API key is required for --upload");
|
|
680
|
+
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
681
|
+
process.exitCode = 1;
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const cfg = config.readConfig();
|
|
686
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
687
|
+
const project = ((opts.project || cfg.defaultProject) || "").trim();
|
|
688
|
+
if (!project) {
|
|
689
|
+
console.error("Error: --project is required (or set a default via 'postgresai set-default-project <project>')");
|
|
690
|
+
process.exitCode = 1;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const epoch = Math.floor(Date.now() / 1000);
|
|
694
|
+
uploadCfg = {
|
|
695
|
+
apiKey,
|
|
696
|
+
apiBaseUrl,
|
|
697
|
+
project,
|
|
698
|
+
epoch,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Use the same SSL behavior as prepare-db:
|
|
703
|
+
// - Default: sslmode=prefer (try SSL first, fallback to non-SSL)
|
|
704
|
+
// - Respect PGSSLMODE env and ?sslmode=... in connection URI
|
|
705
|
+
const adminConn = resolveAdminConnection({
|
|
706
|
+
conn,
|
|
707
|
+
envPassword: process.env.PGPASSWORD,
|
|
708
|
+
});
|
|
709
|
+
let client: Client | undefined;
|
|
710
|
+
const spinnerEnabled = !!process.stdout.isTTY && !opts.json;
|
|
711
|
+
const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
spinner.update("Connecting to Postgres");
|
|
715
|
+
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
716
|
+
client = connResult.client as Client;
|
|
717
|
+
|
|
718
|
+
let reports: Record<string, any>;
|
|
719
|
+
let uploadSummary:
|
|
720
|
+
| { project: string; reportId: number; uploaded: Array<{ checkId: string; filename: string; chunkId: number }> }
|
|
721
|
+
| undefined;
|
|
722
|
+
|
|
723
|
+
if (opts.checkId === "ALL") {
|
|
724
|
+
reports = await generateAllReports(client, opts.nodeName, (p) => {
|
|
725
|
+
spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
|
|
726
|
+
});
|
|
727
|
+
} else {
|
|
728
|
+
const checkId = opts.checkId.toUpperCase();
|
|
729
|
+
const generator = REPORT_GENERATORS[checkId];
|
|
730
|
+
if (!generator) {
|
|
731
|
+
spinner.stop();
|
|
732
|
+
console.error(`Unknown check ID: ${opts.checkId}`);
|
|
733
|
+
console.error(`Available: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
|
|
734
|
+
process.exitCode = 1;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
|
|
738
|
+
reports = { [checkId]: await generator(client, opts.nodeName) };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Optional: upload to PostgresAI API.
|
|
742
|
+
if (uploadCfg) {
|
|
743
|
+
spinner.update("Creating remote checkup report");
|
|
744
|
+
const created = await createCheckupReport({
|
|
745
|
+
apiKey: uploadCfg.apiKey,
|
|
746
|
+
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
747
|
+
project: uploadCfg.project,
|
|
748
|
+
epoch: uploadCfg.epoch,
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const reportId = created.reportId;
|
|
752
|
+
// Keep upload progress out of stdout when --json is used.
|
|
753
|
+
const logUpload = (msg: string): void => {
|
|
754
|
+
if (opts.json) console.error(msg);
|
|
755
|
+
};
|
|
756
|
+
logUpload(`Created remote checkup report: ${reportId}`);
|
|
757
|
+
|
|
758
|
+
const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
|
|
759
|
+
for (const [checkId, report] of Object.entries(reports)) {
|
|
760
|
+
spinner.update(`Uploading ${checkId}.json`);
|
|
761
|
+
const jsonText = JSON.stringify(report, null, 2);
|
|
762
|
+
const r = await uploadCheckupReportJson({
|
|
763
|
+
apiKey: uploadCfg.apiKey,
|
|
764
|
+
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
765
|
+
reportId,
|
|
766
|
+
filename: `${checkId}.json`,
|
|
767
|
+
checkId,
|
|
768
|
+
jsonText,
|
|
769
|
+
});
|
|
770
|
+
uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
|
|
771
|
+
}
|
|
772
|
+
logUpload("Upload completed");
|
|
773
|
+
uploadSummary = { project: uploadCfg.project, reportId, uploaded };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
spinner.stop();
|
|
777
|
+
// Output results
|
|
778
|
+
if (opts.json) {
|
|
779
|
+
console.log(JSON.stringify(reports, null, 2));
|
|
780
|
+
} else if (opts.output) {
|
|
781
|
+
// Write to files
|
|
782
|
+
// outputPath is preflight-validated above
|
|
783
|
+
const outDir = outputPath || path.resolve(process.cwd(), expandHomePath(opts.output));
|
|
784
|
+
for (const [checkId, report] of Object.entries(reports)) {
|
|
785
|
+
const filePath = path.join(outDir, `${checkId}.json`);
|
|
786
|
+
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
787
|
+
console.log(`✓ ${checkId}: ${filePath}`);
|
|
788
|
+
}
|
|
789
|
+
} else if (uploadSummary) {
|
|
790
|
+
// Default with --upload: show upload result instead of local-only summary.
|
|
791
|
+
console.log("\nCheckup report uploaded");
|
|
792
|
+
console.log("======================\n");
|
|
793
|
+
console.log(`Project: ${uploadSummary.project}`);
|
|
794
|
+
console.log(`Report ID: ${uploadSummary.reportId}`);
|
|
795
|
+
console.log("View in Console: console.postgres.ai → Support → checkup reports");
|
|
796
|
+
console.log("");
|
|
797
|
+
console.log("Files:");
|
|
798
|
+
for (const item of uploadSummary.uploaded) {
|
|
799
|
+
console.log(`- ${item.checkId}: ${item.filename}`);
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
// Default: print summary
|
|
803
|
+
console.log("\nHealth Check Reports Generated:");
|
|
804
|
+
console.log("================================\n");
|
|
805
|
+
for (const [checkId, report] of Object.entries(reports)) {
|
|
806
|
+
const r = report as any;
|
|
807
|
+
console.log(`${checkId}: ${r.checkTitle}`);
|
|
808
|
+
if (r.results && r.results[opts.nodeName]) {
|
|
809
|
+
const nodeData = r.results[opts.nodeName];
|
|
810
|
+
if (nodeData.postgres_version) {
|
|
811
|
+
console.log(` PostgreSQL: ${nodeData.postgres_version.version}`);
|
|
812
|
+
}
|
|
813
|
+
if (checkId === "A007" && nodeData.data) {
|
|
814
|
+
const count = Object.keys(nodeData.data).length;
|
|
815
|
+
console.log(` Altered settings: ${count}`);
|
|
816
|
+
}
|
|
817
|
+
if (checkId === "A004" && nodeData.data) {
|
|
818
|
+
if (nodeData.data.database_sizes) {
|
|
819
|
+
const dbCount = Object.keys(nodeData.data.database_sizes).length;
|
|
820
|
+
console.log(` Databases: ${dbCount}`);
|
|
821
|
+
}
|
|
822
|
+
if (nodeData.data.general_info?.cache_hit_ratio) {
|
|
823
|
+
console.log(` Cache hit ratio: ${nodeData.data.general_info.cache_hit_ratio.value}%`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
console.log("\nUse --json for full output or --output <dir> to save files");
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
spinner.stop();
|
|
832
|
+
if (error instanceof RpcError) {
|
|
833
|
+
for (const line of formatRpcErrorForDisplay(error)) {
|
|
834
|
+
console.error(line);
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
838
|
+
console.error(`Error: ${message}`);
|
|
839
|
+
}
|
|
840
|
+
process.exitCode = 1;
|
|
841
|
+
} finally {
|
|
842
|
+
if (client) {
|
|
843
|
+
await client.end();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
532
848
|
/**
|
|
533
849
|
* Stub function for not implemented commands
|
|
534
850
|
*/
|
|
@@ -679,8 +995,8 @@ program.command("help", { isDefault: true }).description("show help").action(()
|
|
|
679
995
|
const mon = program.command("mon").description("monitoring services management");
|
|
680
996
|
|
|
681
997
|
mon
|
|
682
|
-
.command("
|
|
683
|
-
.description("
|
|
998
|
+
.command("local-install")
|
|
999
|
+
.description("install local monitoring stack (generate config, start services)")
|
|
684
1000
|
.option("--demo", "demo mode with sample database", false)
|
|
685
1001
|
.option("--api-key <key>", "Postgres AI API key for automated report uploads")
|
|
686
1002
|
.option("--db-url <url>", "PostgreSQL connection URL to monitor")
|
|
@@ -688,7 +1004,7 @@ mon
|
|
|
688
1004
|
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
689
1005
|
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; yes: boolean }) => {
|
|
690
1006
|
console.log("\n=================================");
|
|
691
|
-
console.log(" PostgresAI
|
|
1007
|
+
console.log(" PostgresAI monitoring local install");
|
|
692
1008
|
console.log("=================================\n");
|
|
693
1009
|
console.log("This will install, configure, and start the monitoring system\n");
|
|
694
1010
|
|
|
@@ -726,8 +1042,8 @@ mon
|
|
|
726
1042
|
if (opts.demo && opts.apiKey) {
|
|
727
1043
|
console.error("✗ Cannot use --api-key with --demo mode");
|
|
728
1044
|
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
729
|
-
console.error("\nUse demo mode without API key: postgres-ai mon
|
|
730
|
-
console.error("Or use production mode with API key: postgres-ai mon
|
|
1045
|
+
console.error("\nUse demo mode without API key: postgres-ai mon local-install --demo");
|
|
1046
|
+
console.error("Or use production mode with API key: postgres-ai mon local-install --api-key=your_key");
|
|
731
1047
|
process.exitCode = 1;
|
|
732
1048
|
return;
|
|
733
1049
|
}
|
|
@@ -989,7 +1305,7 @@ mon
|
|
|
989
1305
|
|
|
990
1306
|
// Final summary
|
|
991
1307
|
console.log("=================================");
|
|
992
|
-
console.log("
|
|
1308
|
+
console.log(" Local install completed!");
|
|
993
1309
|
console.log("=================================\n");
|
|
994
1310
|
|
|
995
1311
|
console.log("What's running:");
|
|
@@ -1536,10 +1852,26 @@ targets
|
|
|
1536
1852
|
// Authentication and API key management
|
|
1537
1853
|
program
|
|
1538
1854
|
.command("auth")
|
|
1539
|
-
.description("authenticate via browser
|
|
1855
|
+
.description("authenticate via browser (OAuth) or store API key directly")
|
|
1856
|
+
.option("--set-key <key>", "store API key directly without OAuth flow")
|
|
1540
1857
|
.option("--port <port>", "local callback server port (default: random)", parseInt)
|
|
1541
1858
|
.option("--debug", "enable debug output")
|
|
1542
|
-
.action(async (opts: { port?: number; debug?: boolean }) => {
|
|
1859
|
+
.action(async (opts: { setKey?: string; port?: number; debug?: boolean }) => {
|
|
1860
|
+
// If --set-key is provided, store it directly without OAuth
|
|
1861
|
+
if (opts.setKey) {
|
|
1862
|
+
const trimmedKey = opts.setKey.trim();
|
|
1863
|
+
if (!trimmedKey) {
|
|
1864
|
+
console.error("Error: API key cannot be empty");
|
|
1865
|
+
process.exitCode = 1;
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
config.writeConfig({ apiKey: trimmedKey });
|
|
1870
|
+
console.log(`API key saved to ${config.getConfigPath()}`);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// Otherwise, proceed with OAuth flow
|
|
1543
1875
|
const pkce = require("../lib/pkce");
|
|
1544
1876
|
const authServer = require("../lib/auth-server");
|
|
1545
1877
|
|
|
@@ -1765,8 +2097,9 @@ program
|
|
|
1765
2097
|
|
|
1766
2098
|
program
|
|
1767
2099
|
.command("add-key <apiKey>")
|
|
1768
|
-
.description("store API key")
|
|
2100
|
+
.description("store API key (deprecated: use 'auth --set-key' instead)")
|
|
1769
2101
|
.action(async (apiKey: string) => {
|
|
2102
|
+
console.warn("Warning: 'add-key' is deprecated. Use 'auth --set-key <key>' instead.\n");
|
|
1770
2103
|
config.writeConfig({ apiKey });
|
|
1771
2104
|
console.log(`API key saved to ${config.getConfigPath()}`);
|
|
1772
2105
|
});
|
|
@@ -1893,7 +2226,7 @@ mon
|
|
|
1893
2226
|
const { projectDir } = await resolveOrInitPaths();
|
|
1894
2227
|
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
1895
2228
|
if (!fs.existsSync(cfgPath)) {
|
|
1896
|
-
console.error("Configuration file not found. Run 'postgres-ai mon
|
|
2229
|
+
console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
|
|
1897
2230
|
process.exitCode = 1;
|
|
1898
2231
|
return;
|
|
1899
2232
|
}
|