postgresai 0.14.0-dev.13 → 0.14.0-dev.15
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 +12 -2
- package/bin/postgres-ai.ts +45 -26
- package/dist/bin/postgres-ai.js +45 -24
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +4 -2
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/init.js +159 -93
- package/dist/lib/init.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/init.ts +174 -111
- package/package.json +1 -1
- package/test/init.integration.test.cjs +35 -18
- package/test/init.test.cjs +173 -21
package/README.md
CHANGED
|
@@ -10,11 +10,13 @@ Command-line interface for PostgresAI monitoring and database management.
|
|
|
10
10
|
npm install -g postgresai
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Or install the latest
|
|
13
|
+
Or install the latest beta release explicitly:
|
|
14
14
|
```bash
|
|
15
|
-
npm install -g postgresai@
|
|
15
|
+
npm install -g postgresai@beta
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Note: in this repository, `cli/package.json` uses a placeholder version (`0.0.0-dev.0`). The real published version is set by the git tag in CI when publishing to npm.
|
|
19
|
+
|
|
18
20
|
### From Homebrew (macOS)
|
|
19
21
|
|
|
20
22
|
```bash
|
|
@@ -34,6 +36,13 @@ postgresai --help
|
|
|
34
36
|
pgai --help # short alias
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
You can also run it without installing via `npx`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx postgresai --help
|
|
43
|
+
npx pgai --help
|
|
44
|
+
```
|
|
45
|
+
|
|
37
46
|
## init (create monitoring user in Postgres)
|
|
38
47
|
|
|
39
48
|
This command creates (or updates) the `postgres_ai_mon` user and grants the permissions described in the root `README.md` (it is idempotent).
|
|
@@ -42,6 +51,7 @@ Run without installing (positional connection string):
|
|
|
42
51
|
|
|
43
52
|
```bash
|
|
44
53
|
npx postgresai init postgresql://admin@host:5432/dbname
|
|
54
|
+
npx pgai init postgresql://admin@host:5432/dbname
|
|
45
55
|
```
|
|
46
56
|
|
|
47
57
|
It also accepts libpq “conninfo” syntax:
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Client } from "pg";
|
|
|
16
16
|
import { startMcpServer } from "../lib/mcp-server";
|
|
17
17
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
18
18
|
import { resolveBaseUrls } from "../lib/util";
|
|
19
|
-
import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
19
|
+
import { applyInitPlan, buildInitPlan, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
20
20
|
|
|
21
21
|
const execPromise = promisify(exec);
|
|
22
22
|
const execFilePromise = promisify(execFile);
|
|
@@ -127,7 +127,7 @@ program
|
|
|
127
127
|
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
128
128
|
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
129
129
|
.option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
|
|
130
|
-
.option("--monitoring-user <name>", "Monitoring role name to create/update",
|
|
130
|
+
.option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER)
|
|
131
131
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
132
132
|
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
133
133
|
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
@@ -152,6 +152,11 @@ program
|
|
|
152
152
|
" If auto-generated, it is printed only on TTY by default.",
|
|
153
153
|
" To print it in non-interactive mode: --print-password",
|
|
154
154
|
"",
|
|
155
|
+
"Environment variables (libpq standard):",
|
|
156
|
+
" PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
|
|
157
|
+
" PGPASSWORD — admin password",
|
|
158
|
+
" PGAI_MON_PASSWORD — monitoring password",
|
|
159
|
+
"",
|
|
155
160
|
"Inspect SQL without applying changes:",
|
|
156
161
|
" postgresai init <conn> --print-sql",
|
|
157
162
|
"",
|
|
@@ -197,7 +202,7 @@ program
|
|
|
197
202
|
const redactPasswords = (sql: string): string => {
|
|
198
203
|
if (!shouldRedactSecrets) return sql;
|
|
199
204
|
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
200
|
-
return sql
|
|
205
|
+
return redactPasswordsInSql(sql);
|
|
201
206
|
};
|
|
202
207
|
|
|
203
208
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
@@ -216,7 +221,6 @@ program
|
|
|
216
221
|
monitoringUser: opts.monitoringUser,
|
|
217
222
|
monitoringPassword: monPassword,
|
|
218
223
|
includeOptionalPermissions,
|
|
219
|
-
roleExists: undefined,
|
|
220
224
|
});
|
|
221
225
|
|
|
222
226
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
@@ -250,7 +254,7 @@ program
|
|
|
250
254
|
});
|
|
251
255
|
} catch (e) {
|
|
252
256
|
const msg = e instanceof Error ? e.message : String(e);
|
|
253
|
-
console.error(
|
|
257
|
+
console.error(`Error: ${msg}`);
|
|
254
258
|
// When connection details are missing, show full init help (options + examples).
|
|
255
259
|
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
256
260
|
console.error("");
|
|
@@ -267,16 +271,11 @@ program
|
|
|
267
271
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
268
272
|
|
|
269
273
|
// Use native pg client instead of requiring psql to be installed
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
let client: Client | undefined;
|
|
272
275
|
try {
|
|
276
|
+
client = new Client(adminConn.clientConfig);
|
|
273
277
|
await client.connect();
|
|
274
278
|
|
|
275
|
-
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
276
|
-
opts.monitoringUser,
|
|
277
|
-
]);
|
|
278
|
-
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
279
|
-
|
|
280
279
|
const dbRes = await client.query("select current_database() as db");
|
|
281
280
|
const database = dbRes.rows?.[0]?.db;
|
|
282
281
|
if (typeof database !== "string" || !database) {
|
|
@@ -319,7 +318,12 @@ program
|
|
|
319
318
|
if (resolved.generated) {
|
|
320
319
|
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
321
320
|
if (canPrint) {
|
|
322
|
-
|
|
321
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
322
|
+
console.error("");
|
|
323
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
324
|
+
// Quote for shell copy/paste safety.
|
|
325
|
+
console.error(`PGAI_MON_PASSWORD='${monPassword}'`);
|
|
326
|
+
console.error("");
|
|
323
327
|
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
324
328
|
} else {
|
|
325
329
|
console.error(
|
|
@@ -349,7 +353,6 @@ program
|
|
|
349
353
|
monitoringUser: opts.monitoringUser,
|
|
350
354
|
monitoringPassword: monPassword,
|
|
351
355
|
includeOptionalPermissions,
|
|
352
|
-
roleExists,
|
|
353
356
|
});
|
|
354
357
|
|
|
355
358
|
const effectivePlan = opts.resetPassword
|
|
@@ -393,38 +396,54 @@ program
|
|
|
393
396
|
if (!message || message === "[object Object]") {
|
|
394
397
|
message = "Unknown error";
|
|
395
398
|
}
|
|
396
|
-
console.error(
|
|
399
|
+
console.error(`Error: init failed: ${message}`);
|
|
400
|
+
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
401
|
+
const stepMatch =
|
|
402
|
+
typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
403
|
+
const failedStep = stepMatch?.[1];
|
|
404
|
+
if (failedStep) {
|
|
405
|
+
console.error(` Step: ${failedStep}`);
|
|
406
|
+
}
|
|
397
407
|
if (errAny && typeof errAny === "object") {
|
|
398
408
|
if (typeof errAny.code === "string" && errAny.code) {
|
|
399
|
-
console.error(`
|
|
409
|
+
console.error(` Code: ${errAny.code}`);
|
|
400
410
|
}
|
|
401
411
|
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
402
|
-
console.error(`Detail: ${errAny.detail}`);
|
|
412
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
403
413
|
}
|
|
404
414
|
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
405
|
-
console.error(`Hint: ${errAny.hint}`);
|
|
415
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
406
416
|
}
|
|
407
417
|
}
|
|
408
418
|
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
409
419
|
if (errAny.code === "42501") {
|
|
410
|
-
|
|
420
|
+
if (failedStep === "01.role") {
|
|
421
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
422
|
+
} else if (failedStep === "02.permissions") {
|
|
423
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
424
|
+
}
|
|
425
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
426
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
427
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
411
428
|
}
|
|
412
429
|
if (errAny.code === "ECONNREFUSED") {
|
|
413
|
-
console.error("Hint: check host/port and ensure Postgres is reachable from this machine
|
|
430
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
414
431
|
}
|
|
415
432
|
if (errAny.code === "ENOTFOUND") {
|
|
416
|
-
console.error("Hint: DNS resolution failed; double-check the host name
|
|
433
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
417
434
|
}
|
|
418
435
|
if (errAny.code === "ETIMEDOUT") {
|
|
419
|
-
console.error("Hint: connection timed out; check network/firewall rules
|
|
436
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
420
437
|
}
|
|
421
438
|
}
|
|
422
439
|
process.exitCode = 1;
|
|
423
440
|
} finally {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
if (client) {
|
|
442
|
+
try {
|
|
443
|
+
await client.end();
|
|
444
|
+
} catch {
|
|
445
|
+
// ignore
|
|
446
|
+
}
|
|
428
447
|
}
|
|
429
448
|
}
|
|
430
449
|
});
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -109,7 +109,7 @@ program
|
|
|
109
109
|
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
110
110
|
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
111
111
|
.option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
|
|
112
|
-
.option("--monitoring-user <name>", "Monitoring role name to create/update",
|
|
112
|
+
.option("--monitoring-user <name>", "Monitoring role name to create/update", init_1.DEFAULT_MONITORING_USER)
|
|
113
113
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
114
114
|
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
115
115
|
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
@@ -132,6 +132,11 @@ program
|
|
|
132
132
|
" If auto-generated, it is printed only on TTY by default.",
|
|
133
133
|
" To print it in non-interactive mode: --print-password",
|
|
134
134
|
"",
|
|
135
|
+
"Environment variables (libpq standard):",
|
|
136
|
+
" PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
|
|
137
|
+
" PGPASSWORD — admin password",
|
|
138
|
+
" PGAI_MON_PASSWORD — monitoring password",
|
|
139
|
+
"",
|
|
135
140
|
"Inspect SQL without applying changes:",
|
|
136
141
|
" postgresai init <conn> --print-sql",
|
|
137
142
|
"",
|
|
@@ -161,7 +166,7 @@ program
|
|
|
161
166
|
if (!shouldRedactSecrets)
|
|
162
167
|
return sql;
|
|
163
168
|
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
164
|
-
return
|
|
169
|
+
return (0, init_1.redactPasswordsInSql)(sql);
|
|
165
170
|
};
|
|
166
171
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
167
172
|
// Useful for audits/reviews; caller can provide -d/PGDATABASE and an explicit monitoring password.
|
|
@@ -176,7 +181,6 @@ program
|
|
|
176
181
|
monitoringUser: opts.monitoringUser,
|
|
177
182
|
monitoringPassword: monPassword,
|
|
178
183
|
includeOptionalPermissions,
|
|
179
|
-
roleExists: undefined,
|
|
180
184
|
});
|
|
181
185
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
182
186
|
console.log(`-- database: ${database}`);
|
|
@@ -209,7 +213,7 @@ program
|
|
|
209
213
|
}
|
|
210
214
|
catch (e) {
|
|
211
215
|
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
-
console.error(
|
|
216
|
+
console.error(`Error: ${msg}`);
|
|
213
217
|
// When connection details are missing, show full init help (options + examples).
|
|
214
218
|
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
215
219
|
console.error("");
|
|
@@ -223,13 +227,10 @@ program
|
|
|
223
227
|
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
224
228
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
225
229
|
// Use native pg client instead of requiring psql to be installed
|
|
226
|
-
|
|
230
|
+
let client;
|
|
227
231
|
try {
|
|
232
|
+
client = new pg_1.Client(adminConn.clientConfig);
|
|
228
233
|
await client.connect();
|
|
229
|
-
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
230
|
-
opts.monitoringUser,
|
|
231
|
-
]);
|
|
232
|
-
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
233
234
|
const dbRes = await client.query("select current_database() as db");
|
|
234
235
|
const database = dbRes.rows?.[0]?.db;
|
|
235
236
|
if (typeof database !== "string" || !database) {
|
|
@@ -273,7 +274,12 @@ program
|
|
|
273
274
|
if (resolved.generated) {
|
|
274
275
|
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
275
276
|
if (canPrint) {
|
|
276
|
-
|
|
277
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
278
|
+
console.error("");
|
|
279
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
280
|
+
// Quote for shell copy/paste safety.
|
|
281
|
+
console.error(`PGAI_MON_PASSWORD='${monPassword}'`);
|
|
282
|
+
console.error("");
|
|
277
283
|
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
278
284
|
}
|
|
279
285
|
else {
|
|
@@ -302,7 +308,6 @@ program
|
|
|
302
308
|
monitoringUser: opts.monitoringUser,
|
|
303
309
|
monitoringPassword: monPassword,
|
|
304
310
|
includeOptionalPermissions,
|
|
305
|
-
roleExists,
|
|
306
311
|
});
|
|
307
312
|
const effectivePlan = opts.resetPassword
|
|
308
313
|
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
@@ -346,40 +351,56 @@ program
|
|
|
346
351
|
if (!message || message === "[object Object]") {
|
|
347
352
|
message = "Unknown error";
|
|
348
353
|
}
|
|
349
|
-
console.error(
|
|
354
|
+
console.error(`Error: init failed: ${message}`);
|
|
355
|
+
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
356
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
357
|
+
const failedStep = stepMatch?.[1];
|
|
358
|
+
if (failedStep) {
|
|
359
|
+
console.error(` Step: ${failedStep}`);
|
|
360
|
+
}
|
|
350
361
|
if (errAny && typeof errAny === "object") {
|
|
351
362
|
if (typeof errAny.code === "string" && errAny.code) {
|
|
352
|
-
console.error(`
|
|
363
|
+
console.error(` Code: ${errAny.code}`);
|
|
353
364
|
}
|
|
354
365
|
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
355
|
-
console.error(`Detail: ${errAny.detail}`);
|
|
366
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
356
367
|
}
|
|
357
368
|
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
358
|
-
console.error(`Hint: ${errAny.hint}`);
|
|
369
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
359
370
|
}
|
|
360
371
|
}
|
|
361
372
|
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
362
373
|
if (errAny.code === "42501") {
|
|
363
|
-
|
|
374
|
+
if (failedStep === "01.role") {
|
|
375
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
376
|
+
}
|
|
377
|
+
else if (failedStep === "02.permissions") {
|
|
378
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
379
|
+
}
|
|
380
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
381
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
382
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
364
383
|
}
|
|
365
384
|
if (errAny.code === "ECONNREFUSED") {
|
|
366
|
-
console.error("Hint: check host/port and ensure Postgres is reachable from this machine
|
|
385
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
367
386
|
}
|
|
368
387
|
if (errAny.code === "ENOTFOUND") {
|
|
369
|
-
console.error("Hint: DNS resolution failed; double-check the host name
|
|
388
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
370
389
|
}
|
|
371
390
|
if (errAny.code === "ETIMEDOUT") {
|
|
372
|
-
console.error("Hint: connection timed out; check network/firewall rules
|
|
391
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
373
392
|
}
|
|
374
393
|
}
|
|
375
394
|
process.exitCode = 1;
|
|
376
395
|
}
|
|
377
396
|
finally {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
if (client) {
|
|
398
|
+
try {
|
|
399
|
+
await client.end();
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// ignore
|
|
403
|
+
}
|
|
383
404
|
}
|
|
384
405
|
}
|
|
385
406
|
});
|