Skip to content

Commit 96add1d

Browse files
committed
cli: dedupe init missing-connection output
1 parent 24ef4e3 commit 96add1d

File tree

6 files changed

+72
-132
lines changed

6 files changed

+72
-132
lines changed

README.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,35 +63,17 @@ Experience the full monitoring solution: **https://demo.postgres.ai** (login: `d
6363
- Supports Postgres versions 14-18
6464
- **pg_stat_statements extension must be created** for the DB used for connection
6565

66-
## ⚠️ Security Notice
67-
68-
**WARNING: Security is your responsibility!**
69-
70-
This monitoring solution exposes several ports that **MUST** be properly firewalled:
71-
- **Port 3000** (Grafana) - Contains sensitive database metrics and dashboards
72-
- **Port 58080** (PGWatch Postgres) - Database monitoring interface
73-
- **Port 58089** (PGWatch Prometheus) - Database monitoring interface
74-
- **Port 59090** (Victoria Metrics) - Metrics storage and queries
75-
- **Port 59091** (PGWatch Prometheus endpoint) - Metrics collection
76-
- **Port 55000** (Flask API) - Backend API service
77-
- **Port 55432** (Demo DB) - When using `--demo` option
78-
- **Port 55433** (Metrics DB) - Postgres metrics storage
79-
80-
**Configure your firewall to:**
81-
- Block public access to all monitoring ports
82-
- Allow access only from trusted networks/IPs
83-
- Use VPN or SSH tunnels for remote access
84-
85-
Failure to secure these ports may expose sensitive database information!
86-
8766
## 🚀 Quick start
8867

8968
Create a database user for monitoring (skip this if you want to just check out `postgres_ai` monitoring with a synthetic `demo` database).
9069

9170
Use the CLI to create/update the monitoring role and grant all required permissions (idempotent):
9271

9372
```bash
94-
# Connect as an admin/superuser and apply required permissions.
73+
# Connect as an admin/superuser and run the idempotent setup:
74+
# - create/update the monitoring role
75+
# - create required view(s)
76+
# - apply required grants (and optional extensions where supported)
9577
# Admin password comes from PGPASSWORD (libpq standard) unless you pass --admin-password.
9678
#
9779
# Monitoring password:
@@ -132,17 +114,14 @@ If you want to see what will be executed first, use `--print-sql` (prints the SQ
132114
npx postgresai init --print-sql
133115
```
134116

135-
Optionally, to render the plan for a specific database and/or show the password literal:
117+
Optionally, to render the plan for a specific database:
136118

137119
```bash
138120
# Pick database (default is PGDATABASE or "postgres"):
139121
npx postgresai init --print-sql -d dbname
140122

141-
# Provide an explicit monitoring password (still redacted unless you opt in):
123+
# Provide an explicit monitoring password (still redacted in output):
142124
npx postgresai init --print-sql -d dbname --password '...'
143-
144-
# Dangerous: print secrets in the SQL output:
145-
npx postgresai init --print-sql -d dbname --password '...' --show-secrets
146125
```
147126

148127
### Troubleshooting
@@ -161,7 +140,7 @@ If you see errors like `permission denied` / `insufficient_privilege` / code `42
161140
- **Review SQL before running** (audit-friendly):
162141

163142
```bash
164-
npx postgresai init --print-sql -d mydb --password '...' --show-secrets
143+
npx postgresai init --print-sql -d mydb
165144
```
166145

167146
**One command setup:**
@@ -191,6 +170,27 @@ Or if you want to just check out how it works:
191170

192171
That's it! Everything is installed, configured, and running.
193172
173+
## ⚠️ Security Notice
174+
175+
**WARNING: Security is your responsibility!**
176+
177+
This monitoring solution exposes several ports that **MUST** be properly firewalled:
178+
- **Port 3000** (Grafana) - Contains sensitive database metrics and dashboards
179+
- **Port 58080** (PGWatch Postgres) - Database monitoring interface
180+
- **Port 58089** (PGWatch Prometheus) - Database monitoring interface
181+
- **Port 59090** (Victoria Metrics) - Metrics storage and queries
182+
- **Port 59091** (PGWatch Prometheus endpoint) - Metrics collection
183+
- **Port 55000** (Flask API) - Backend API service
184+
- **Port 55432** (Demo DB) - When using `--demo` option
185+
- **Port 55433** (Metrics DB) - Postgres metrics storage
186+
187+
**Configure your firewall to:**
188+
- Block public access to all monitoring ports
189+
- Allow access only from trusted networks/IPs
190+
- Use VPN or SSH tunnels for remote access
191+
192+
Failure to secure these ports may expose sensitive database information!
193+
194194
## 📊 What you get
195195
196196
- **Grafana Dashboards** - Visual monitoring at http://localhost:3000

cli/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ brew install postgresai
2929

3030
## Usage
3131

32-
The CLI provides three command aliases:
32+
The `postgresai` package provides two command aliases (prefer `postgresai`):
3333
```bash
3434
postgres-ai --help
3535
postgresai --help
@@ -41,9 +41,17 @@ You can also run it without installing via `npx`:
4141
npx postgresai --help
4242
```
4343

44+
### Optional shorthand: `pgai`
45+
46+
If you want `npx pgai ...` as a shorthand for `npx postgresai ...`, install the separate `pgai` wrapper package:
47+
48+
```bash
49+
npx pgai --help
50+
```
51+
4452
## init (create monitoring user in Postgres)
4553

46-
This command creates (or updates) the `postgres_ai_mon` user and grants the permissions described in the root `README.md` (it is idempotent).
54+
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.
4755

4856
Run without installing (positional connection string):
4957

cli/bin/postgres-ai.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ program
120120

121121
program
122122
.command("init [conn]")
123-
.description("Create a monitoring user and grant all required permissions (idempotent)")
123+
.description("Create a monitoring user, required view(s), and grant required permissions (idempotent)")
124124
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
125125
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
126126
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
@@ -133,7 +133,6 @@ program
133133
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
134134
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
135135
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
136-
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
137136
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
138137
.addHelpText(
139138
"after",
@@ -167,7 +166,7 @@ program
167166
" postgresai init <conn> --reset-password --password '...'",
168167
"",
169168
"Offline SQL plan (no DB connection):",
170-
" postgresai init --print-sql -d dbname --password '...' --show-secrets",
169+
" postgresai init --print-sql",
171170
].join("\n")
172171
)
173172
.action(async (conn: string | undefined, opts: {
@@ -183,7 +182,6 @@ program
183182
verify?: boolean;
184183
resetPassword?: boolean;
185184
printSql?: boolean;
186-
showSecrets?: boolean;
187185
printPassword?: boolean;
188186
}, cmd: Command) => {
189187
if (opts.verify && opts.resetPassword) {
@@ -198,23 +196,19 @@ program
198196
}
199197

200198
const shouldPrintSql = !!opts.printSql;
201-
const shouldRedactSecrets = !opts.showSecrets;
202-
const redactPasswords = (sql: string): string => {
203-
if (!shouldRedactSecrets) return sql;
204-
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
205-
return redactPasswordsInSql(sql);
206-
};
199+
const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
207200

208201
// Offline mode: allow printing SQL without providing/using an admin connection.
209-
// Useful for audits/reviews; caller can provide -d/PGDATABASE and an explicit monitoring password.
202+
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
210203
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
211204
if (shouldPrintSql) {
212205
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
213206
const includeOptionalPermissions = !opts.skipOptionalPermissions;
214207

215-
// Use explicit password/env if provided; otherwise use a placeholder (will be redacted unless --show-secrets).
208+
// Use explicit password/env if provided; otherwise use a placeholder.
209+
// Printed SQL always redacts secrets.
216210
const monPassword =
217-
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "CHANGE_ME").toString();
211+
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "<redacted>").toString();
218212

219213
const plan = await buildInitPlan({
220214
database,
@@ -232,9 +226,7 @@ program
232226
console.log(redactPasswords(step.sql));
233227
}
234228
console.log("\n--- end SQL plan ---\n");
235-
if (shouldRedactSecrets) {
236-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
237-
}
229+
console.log("Note: passwords are redacted in the printed SQL output.");
238230
return;
239231
}
240232
}
@@ -367,9 +359,7 @@ program
367359
console.log(redactPasswords(step.sql));
368360
}
369361
console.log("\n--- end SQL plan ---\n");
370-
if (shouldRedactSecrets) {
371-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
372-
}
362+
console.log("Note: passwords are redacted in the printed SQL output.");
373363
return;
374364
}
375365

cli/lib/init.ts

Lines changed: 2 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as readline from "readline";
21
import { randomBytes } from "crypto";
32
import { URL } from "url";
43
import type { ConnectionOptions as TlsConnectionOptions } from "tls";
@@ -223,19 +222,8 @@ export function resolveAdminConnection(opts: {
223222
}
224223

225224
if (!hasConnDetails) {
226-
throw new Error(
227-
[
228-
"Connection is required.",
229-
"",
230-
"Examples:",
231-
" postgresai init postgresql://admin@host:5432/dbname",
232-
" postgresai init \"dbname=dbname host=host user=admin\"",
233-
" postgresai init -h host -p 5432 -U admin -d dbname",
234-
"",
235-
"Admin password:",
236-
" --admin-password <password> (or set PGPASSWORD)",
237-
].join("\n")
238-
);
225+
// Keep this message short: the CLI prints full help (including examples) on this error.
226+
throw new Error("Connection is required.");
239227
}
240228

241229
const cfg: PgClientConfig = {};
@@ -254,72 +242,6 @@ export function resolveAdminConnection(opts: {
254242
return { clientConfig: cfg, display: describePgConfig(cfg) };
255243
}
256244

257-
export async function promptHidden(prompt: string): Promise<string> {
258-
// Implement our own hidden input reader so:
259-
// - prompt text is visible
260-
// - only user input is masked
261-
// - we don't rely on non-public readline internals
262-
if (!process.stdin.isTTY) {
263-
throw new Error("Cannot prompt for password in non-interactive mode");
264-
}
265-
266-
const stdin = process.stdin;
267-
const stdout = process.stdout as NodeJS.WriteStream;
268-
269-
stdout.write(prompt);
270-
271-
return await new Promise<string>((resolve, reject) => {
272-
let value = "";
273-
274-
const cleanup = () => {
275-
try {
276-
stdin.setRawMode(false);
277-
} catch {
278-
// ignore
279-
}
280-
stdin.removeListener("keypress", onKeypress);
281-
};
282-
283-
const onKeypress = (str: string, key: any) => {
284-
if (key?.ctrl && key?.name === "c") {
285-
stdout.write("\n");
286-
cleanup();
287-
reject(new Error("Cancelled"));
288-
return;
289-
}
290-
291-
if (key?.name === "return" || key?.name === "enter") {
292-
stdout.write("\n");
293-
cleanup();
294-
resolve(value);
295-
return;
296-
}
297-
298-
if (key?.name === "backspace") {
299-
if (value.length > 0) {
300-
value = value.slice(0, -1);
301-
// Erase one mask char.
302-
stdout.write("\b \b");
303-
}
304-
return;
305-
}
306-
307-
// Ignore other control keys.
308-
if (key?.ctrl || key?.meta) return;
309-
310-
if (typeof str === "string" && str.length > 0) {
311-
value += str;
312-
stdout.write("*");
313-
}
314-
};
315-
316-
readline.emitKeypressEvents(stdin);
317-
stdin.setRawMode(true);
318-
stdin.on("keypress", onKeypress);
319-
stdin.resume();
320-
});
321-
}
322-
323245
function generateMonitoringPassword(): string {
324246
// URL-safe and easy to copy/paste; 24 bytes => 32 base64url chars (no padding).
325247
// Note: randomBytes() throws on failure; we add a tiny sanity check for unexpected output.
@@ -333,7 +255,6 @@ function generateMonitoringPassword(): string {
333255
export async function resolveMonitoringPassword(opts: {
334256
passwordFlag?: string;
335257
passwordEnv?: string;
336-
prompt?: (prompt: string) => Promise<string>;
337258
monitoringUser: string;
338259
}): Promise<{ password: string; generated: boolean }> {
339260
const fromFlag = (opts.passwordFlag || "").trim();

cli/test/init.test.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ test("resolveAdminConnection rejects when only PGPASSWORD is provided (no connec
173173
assert.throws(() => init.resolveAdminConnection({ envPassword: "pw" }), /Connection is required/);
174174
});
175175

176-
test("resolveAdminConnection error message includes examples", () => {
177-
assert.throws(() => init.resolveAdminConnection({}), /Examples:/);
176+
test("resolveAdminConnection rejects when connection is missing", () => {
177+
assert.throws(() => init.resolveAdminConnection({}), /Connection is required/);
178178
});
179179

180180
test("cli: init with missing connection prints init help/options", () => {

pgai/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# pgai
2+
3+
`pgai` is a thin wrapper around the [`postgresai`](../cli/README.md) CLI, intended to provide a short command name.
4+
5+
## Usage
6+
7+
Run without installing:
8+
9+
```bash
10+
npx pgai --help
11+
npx pgai init postgresql://admin@host:5432/dbname
12+
```
13+
14+
This is equivalent to:
15+
16+
```bash
17+
npx postgresai --help
18+
npx postgresai init postgresql://admin@host:5432/dbname
19+
```
20+
21+

0 commit comments

Comments
 (0)