postgresai 0.14.0-beta.1 → 0.14.0-beta.3
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 +29 -14
- package/bin/postgres-ai.ts +44 -56
- package/dist/bin/postgres-ai.js +44 -54
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +4 -4
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/init.js +162 -163
- package/dist/lib/init.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/init.ts +178 -193
- package/package.json +1 -1
- package/sql/01.role.sql +8 -7
- package/test/init.integration.test.cjs +35 -21
- package/test/init.test.cjs +192 -23
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Or install the latest beta release explicitly:
|
|
|
15
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
|
|
@@ -27,11 +29,24 @@ brew install postgresai
|
|
|
27
29
|
|
|
28
30
|
## Usage
|
|
29
31
|
|
|
30
|
-
The
|
|
32
|
+
The `postgresai` package provides two command aliases (prefer `postgresai`):
|
|
31
33
|
```bash
|
|
32
34
|
postgres-ai --help
|
|
33
35
|
postgresai --help
|
|
34
|
-
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can also run it without installing via `npx`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx postgresai --help
|
|
42
|
+
```
|
|
43
|
+
|
|
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
|
|
35
50
|
```
|
|
36
51
|
|
|
37
52
|
## init (create monitoring user in Postgres)
|
|
@@ -98,7 +113,7 @@ npx postgresai init postgresql://admin@host:5432/dbname --reset-password --passw
|
|
|
98
113
|
|
|
99
114
|
Authenticate via browser to obtain API key:
|
|
100
115
|
```bash
|
|
101
|
-
|
|
116
|
+
postgresai auth
|
|
102
117
|
```
|
|
103
118
|
|
|
104
119
|
This will:
|
|
@@ -180,7 +195,7 @@ postgres-ai mon shell <service> # Open shell to monitoring servic
|
|
|
180
195
|
### MCP server (`mcp` group)
|
|
181
196
|
|
|
182
197
|
```bash
|
|
183
|
-
|
|
198
|
+
postgresai mcp start # Start MCP stdio server exposing tools
|
|
184
199
|
```
|
|
185
200
|
|
|
186
201
|
Cursor configuration example (Settings → MCP):
|
|
@@ -189,7 +204,7 @@ Cursor configuration example (Settings → MCP):
|
|
|
189
204
|
{
|
|
190
205
|
"mcpServers": {
|
|
191
206
|
"PostgresAI": {
|
|
192
|
-
"command": "
|
|
207
|
+
"command": "postgresai",
|
|
193
208
|
"args": ["mcp", "start"],
|
|
194
209
|
"env": {
|
|
195
210
|
"PGAI_API_BASE_URL": "https://postgres.ai/api/general/"
|
|
@@ -200,16 +215,16 @@ Cursor configuration example (Settings → MCP):
|
|
|
200
215
|
```
|
|
201
216
|
|
|
202
217
|
Tools exposed:
|
|
203
|
-
- list_issues: returns the same JSON as `
|
|
218
|
+
- list_issues: returns the same JSON as `postgresai issues list`.
|
|
204
219
|
- view_issue: view a single issue with its comments (args: { issue_id, debug? })
|
|
205
220
|
- post_issue_comment: post a comment (args: { issue_id, content, parent_comment_id?, debug? })
|
|
206
221
|
|
|
207
222
|
### Issues management (`issues` group)
|
|
208
223
|
|
|
209
224
|
```bash
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
225
|
+
postgresai issues list # List issues (shows: id, title, status, created_at)
|
|
226
|
+
postgresai issues view <issueId> # View issue details and comments
|
|
227
|
+
postgresai issues post_comment <issueId> <content> # Post a comment to an issue
|
|
213
228
|
# Options:
|
|
214
229
|
# --parent <uuid> Parent comment ID (for replies)
|
|
215
230
|
# --debug Enable debug output
|
|
@@ -223,13 +238,13 @@ By default, issues commands print human-friendly YAML when writing to a terminal
|
|
|
223
238
|
- Use `--json` to force JSON output:
|
|
224
239
|
|
|
225
240
|
```bash
|
|
226
|
-
|
|
241
|
+
postgresai issues list --json | jq '.[] | {id, title}'
|
|
227
242
|
```
|
|
228
243
|
|
|
229
244
|
- Rely on auto-detection: when stdout is not a TTY (e.g., piped or redirected), output is JSON automatically:
|
|
230
245
|
|
|
231
246
|
```bash
|
|
232
|
-
|
|
247
|
+
postgresai issues view <issueId> > issue.json
|
|
233
248
|
```
|
|
234
249
|
|
|
235
250
|
#### Grafana management
|
|
@@ -293,7 +308,7 @@ Linux/macOS (bash/zsh):
|
|
|
293
308
|
```bash
|
|
294
309
|
export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/
|
|
295
310
|
export PGAI_UI_BASE_URL=https://console-dev.postgres.ai
|
|
296
|
-
|
|
311
|
+
postgresai auth --debug
|
|
297
312
|
```
|
|
298
313
|
|
|
299
314
|
Windows PowerShell:
|
|
@@ -301,13 +316,13 @@ Windows PowerShell:
|
|
|
301
316
|
```powershell
|
|
302
317
|
$env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/"
|
|
303
318
|
$env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai"
|
|
304
|
-
|
|
319
|
+
postgresai auth --debug
|
|
305
320
|
```
|
|
306
321
|
|
|
307
322
|
Via CLI options (overrides env):
|
|
308
323
|
|
|
309
324
|
```bash
|
|
310
|
-
|
|
325
|
+
postgresai auth --debug \
|
|
311
326
|
--api-base-url https://v2.postgres.ai/api/general/ \
|
|
312
327
|
--ui-base-url https://console-dev.postgres.ai
|
|
313
328
|
```
|
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,13 +127,12 @@ 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)
|
|
134
134
|
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
135
135
|
.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)
|
|
137
136
|
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
138
137
|
.addHelpText(
|
|
139
138
|
"after",
|
|
@@ -152,6 +151,11 @@ program
|
|
|
152
151
|
" If auto-generated, it is printed only on TTY by default.",
|
|
153
152
|
" To print it in non-interactive mode: --print-password",
|
|
154
153
|
"",
|
|
154
|
+
"Environment variables (libpq standard):",
|
|
155
|
+
" PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
|
|
156
|
+
" PGPASSWORD — admin password",
|
|
157
|
+
" PGAI_MON_PASSWORD — monitoring password",
|
|
158
|
+
"",
|
|
155
159
|
"Inspect SQL without applying changes:",
|
|
156
160
|
" postgresai init <conn> --print-sql",
|
|
157
161
|
"",
|
|
@@ -162,7 +166,7 @@ program
|
|
|
162
166
|
" postgresai init <conn> --reset-password --password '...'",
|
|
163
167
|
"",
|
|
164
168
|
"Offline SQL plan (no DB connection):",
|
|
165
|
-
" postgresai init --print-sql
|
|
169
|
+
" postgresai init --print-sql",
|
|
166
170
|
].join("\n")
|
|
167
171
|
)
|
|
168
172
|
.action(async (conn: string | undefined, opts: {
|
|
@@ -178,7 +182,6 @@ program
|
|
|
178
182
|
verify?: boolean;
|
|
179
183
|
resetPassword?: boolean;
|
|
180
184
|
printSql?: boolean;
|
|
181
|
-
showSecrets?: boolean;
|
|
182
185
|
printPassword?: boolean;
|
|
183
186
|
}, cmd: Command) => {
|
|
184
187
|
if (opts.verify && opts.resetPassword) {
|
|
@@ -193,30 +196,25 @@ program
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
const shouldPrintSql = !!opts.printSql;
|
|
196
|
-
const
|
|
197
|
-
const redactPasswords = (sql: string): string => {
|
|
198
|
-
if (!shouldRedactSecrets) return sql;
|
|
199
|
-
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
200
|
-
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
201
|
-
};
|
|
199
|
+
const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
|
|
202
200
|
|
|
203
201
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
204
|
-
// Useful for audits/reviews; caller can provide -d/PGDATABASE
|
|
202
|
+
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
|
|
205
203
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
206
204
|
if (shouldPrintSql) {
|
|
207
205
|
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
208
206
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
209
207
|
|
|
210
|
-
// Use explicit password/env if provided; otherwise use a placeholder
|
|
208
|
+
// Use explicit password/env if provided; otherwise use a placeholder.
|
|
209
|
+
// Printed SQL always redacts secrets.
|
|
211
210
|
const monPassword =
|
|
212
|
-
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "
|
|
211
|
+
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "<redacted>").toString();
|
|
213
212
|
|
|
214
213
|
const plan = await buildInitPlan({
|
|
215
214
|
database,
|
|
216
215
|
monitoringUser: opts.monitoringUser,
|
|
217
216
|
monitoringPassword: monPassword,
|
|
218
217
|
includeOptionalPermissions,
|
|
219
|
-
roleExists: undefined,
|
|
220
218
|
});
|
|
221
219
|
|
|
222
220
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
@@ -228,9 +226,7 @@ program
|
|
|
228
226
|
console.log(redactPasswords(step.sql));
|
|
229
227
|
}
|
|
230
228
|
console.log("\n--- end SQL plan ---\n");
|
|
231
|
-
|
|
232
|
-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
233
|
-
}
|
|
229
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
234
230
|
return;
|
|
235
231
|
}
|
|
236
232
|
}
|
|
@@ -250,7 +246,7 @@ program
|
|
|
250
246
|
});
|
|
251
247
|
} catch (e) {
|
|
252
248
|
const msg = e instanceof Error ? e.message : String(e);
|
|
253
|
-
console.error(
|
|
249
|
+
console.error(`Error: init: ${msg}`);
|
|
254
250
|
// When connection details are missing, show full init help (options + examples).
|
|
255
251
|
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
256
252
|
console.error("");
|
|
@@ -267,16 +263,11 @@ program
|
|
|
267
263
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
268
264
|
|
|
269
265
|
// Use native pg client instead of requiring psql to be installed
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
let client: Client | undefined;
|
|
272
267
|
try {
|
|
268
|
+
client = new Client(adminConn.clientConfig);
|
|
273
269
|
await client.connect();
|
|
274
270
|
|
|
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
271
|
const dbRes = await client.query("select current_database() as db");
|
|
281
272
|
const database = dbRes.rows?.[0]?.db;
|
|
282
273
|
if (typeof database !== "string" || !database) {
|
|
@@ -319,10 +310,13 @@ program
|
|
|
319
310
|
if (resolved.generated) {
|
|
320
311
|
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
321
312
|
if (canPrint) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
console.
|
|
325
|
-
console.
|
|
313
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
314
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
315
|
+
console.error("");
|
|
316
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
317
|
+
// Quote for shell copy/paste safety.
|
|
318
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
319
|
+
console.error("");
|
|
326
320
|
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
327
321
|
} else {
|
|
328
322
|
console.error(
|
|
@@ -352,7 +346,6 @@ program
|
|
|
352
346
|
monitoringUser: opts.monitoringUser,
|
|
353
347
|
monitoringPassword: monPassword,
|
|
354
348
|
includeOptionalPermissions,
|
|
355
|
-
roleExists,
|
|
356
349
|
});
|
|
357
350
|
|
|
358
351
|
const effectivePlan = opts.resetPassword
|
|
@@ -366,9 +359,7 @@ program
|
|
|
366
359
|
console.log(redactPasswords(step.sql));
|
|
367
360
|
}
|
|
368
361
|
console.log("\n--- end SQL plan ---\n");
|
|
369
|
-
|
|
370
|
-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
371
|
-
}
|
|
362
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
372
363
|
return;
|
|
373
364
|
}
|
|
374
365
|
|
|
@@ -396,57 +387,54 @@ program
|
|
|
396
387
|
if (!message || message === "[object Object]") {
|
|
397
388
|
message = "Unknown error";
|
|
398
389
|
}
|
|
399
|
-
console.error(
|
|
390
|
+
console.error(`Error: init: ${message}`);
|
|
400
391
|
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
401
392
|
const stepMatch =
|
|
402
393
|
typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
403
394
|
const failedStep = stepMatch?.[1];
|
|
404
395
|
if (failedStep) {
|
|
405
|
-
console.error(`Step: ${failedStep}`);
|
|
396
|
+
console.error(` Step: ${failedStep}`);
|
|
406
397
|
}
|
|
407
398
|
if (errAny && typeof errAny === "object") {
|
|
408
399
|
if (typeof errAny.code === "string" && errAny.code) {
|
|
409
|
-
console.error(`
|
|
400
|
+
console.error(` Code: ${errAny.code}`);
|
|
410
401
|
}
|
|
411
402
|
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
412
|
-
console.error(`Detail: ${errAny.detail}`);
|
|
403
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
413
404
|
}
|
|
414
405
|
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
415
|
-
console.error(`Hint: ${errAny.hint}`);
|
|
406
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
416
407
|
}
|
|
417
408
|
}
|
|
418
409
|
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
419
410
|
if (errAny.code === "42501") {
|
|
420
|
-
console.error("");
|
|
421
|
-
console.error("Permission error: your admin connection is not allowed to complete the setup.");
|
|
422
411
|
if (failedStep === "01.role") {
|
|
423
|
-
console.error("
|
|
412
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
424
413
|
} else if (failedStep === "02.permissions") {
|
|
425
|
-
console.error("
|
|
414
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
426
415
|
}
|
|
427
|
-
console.error("
|
|
428
|
-
console.error("
|
|
429
|
-
console.error("
|
|
430
|
-
console.error("Tip: run with --print-sql to review the exact SQL plan.");
|
|
431
|
-
console.error("");
|
|
432
|
-
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
416
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
417
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
418
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
433
419
|
}
|
|
434
420
|
if (errAny.code === "ECONNREFUSED") {
|
|
435
|
-
console.error("Hint: check host/port and ensure Postgres is reachable from this machine
|
|
421
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
436
422
|
}
|
|
437
423
|
if (errAny.code === "ENOTFOUND") {
|
|
438
|
-
console.error("Hint: DNS resolution failed; double-check the host name
|
|
424
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
439
425
|
}
|
|
440
426
|
if (errAny.code === "ETIMEDOUT") {
|
|
441
|
-
console.error("Hint: connection timed out; check network/firewall rules
|
|
427
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
442
428
|
}
|
|
443
429
|
}
|
|
444
430
|
process.exitCode = 1;
|
|
445
431
|
} finally {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
432
|
+
if (client) {
|
|
433
|
+
try {
|
|
434
|
+
await client.end();
|
|
435
|
+
} catch {
|
|
436
|
+
// ignore
|
|
437
|
+
}
|
|
450
438
|
}
|
|
451
439
|
}
|
|
452
440
|
});
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -109,13 +109,12 @@ 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)
|
|
116
116
|
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
117
117
|
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
118
|
-
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
119
118
|
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
120
119
|
.addHelpText("after", [
|
|
121
120
|
"",
|
|
@@ -132,6 +131,11 @@ program
|
|
|
132
131
|
" If auto-generated, it is printed only on TTY by default.",
|
|
133
132
|
" To print it in non-interactive mode: --print-password",
|
|
134
133
|
"",
|
|
134
|
+
"Environment variables (libpq standard):",
|
|
135
|
+
" PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
|
|
136
|
+
" PGPASSWORD — admin password",
|
|
137
|
+
" PGAI_MON_PASSWORD — monitoring password",
|
|
138
|
+
"",
|
|
135
139
|
"Inspect SQL without applying changes:",
|
|
136
140
|
" postgresai init <conn> --print-sql",
|
|
137
141
|
"",
|
|
@@ -142,7 +146,7 @@ program
|
|
|
142
146
|
" postgresai init <conn> --reset-password --password '...'",
|
|
143
147
|
"",
|
|
144
148
|
"Offline SQL plan (no DB connection):",
|
|
145
|
-
" postgresai init --print-sql
|
|
149
|
+
" postgresai init --print-sql",
|
|
146
150
|
].join("\n"))
|
|
147
151
|
.action(async (conn, opts, cmd) => {
|
|
148
152
|
if (opts.verify && opts.resetPassword) {
|
|
@@ -156,27 +160,21 @@ program
|
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
const shouldPrintSql = !!opts.printSql;
|
|
159
|
-
const
|
|
160
|
-
const redactPasswords = (sql) => {
|
|
161
|
-
if (!shouldRedactSecrets)
|
|
162
|
-
return sql;
|
|
163
|
-
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
164
|
-
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
165
|
-
};
|
|
163
|
+
const redactPasswords = (sql) => (0, init_1.redactPasswordsInSql)(sql);
|
|
166
164
|
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
167
|
-
// Useful for audits/reviews; caller can provide -d/PGDATABASE
|
|
165
|
+
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
|
|
168
166
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
169
167
|
if (shouldPrintSql) {
|
|
170
168
|
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
171
169
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
172
|
-
// Use explicit password/env if provided; otherwise use a placeholder
|
|
173
|
-
|
|
170
|
+
// Use explicit password/env if provided; otherwise use a placeholder.
|
|
171
|
+
// Printed SQL always redacts secrets.
|
|
172
|
+
const monPassword = (opts.password ?? process.env.PGAI_MON_PASSWORD ?? "<redacted>").toString();
|
|
174
173
|
const plan = await (0, init_1.buildInitPlan)({
|
|
175
174
|
database,
|
|
176
175
|
monitoringUser: opts.monitoringUser,
|
|
177
176
|
monitoringPassword: monPassword,
|
|
178
177
|
includeOptionalPermissions,
|
|
179
|
-
roleExists: undefined,
|
|
180
178
|
});
|
|
181
179
|
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
182
180
|
console.log(`-- database: ${database}`);
|
|
@@ -187,9 +185,7 @@ program
|
|
|
187
185
|
console.log(redactPasswords(step.sql));
|
|
188
186
|
}
|
|
189
187
|
console.log("\n--- end SQL plan ---\n");
|
|
190
|
-
|
|
191
|
-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
192
|
-
}
|
|
188
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
193
189
|
return;
|
|
194
190
|
}
|
|
195
191
|
}
|
|
@@ -209,7 +205,7 @@ program
|
|
|
209
205
|
}
|
|
210
206
|
catch (e) {
|
|
211
207
|
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
-
console.error(
|
|
208
|
+
console.error(`Error: init: ${msg}`);
|
|
213
209
|
// When connection details are missing, show full init help (options + examples).
|
|
214
210
|
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
215
211
|
console.error("");
|
|
@@ -223,13 +219,10 @@ program
|
|
|
223
219
|
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
224
220
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
225
221
|
// Use native pg client instead of requiring psql to be installed
|
|
226
|
-
|
|
222
|
+
let client;
|
|
227
223
|
try {
|
|
224
|
+
client = new pg_1.Client(adminConn.clientConfig);
|
|
228
225
|
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
226
|
const dbRes = await client.query("select current_database() as db");
|
|
234
227
|
const database = dbRes.rows?.[0]?.db;
|
|
235
228
|
if (typeof database !== "string" || !database) {
|
|
@@ -273,10 +266,13 @@ program
|
|
|
273
266
|
if (resolved.generated) {
|
|
274
267
|
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
275
268
|
if (canPrint) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
console.
|
|
279
|
-
console.
|
|
269
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
270
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
271
|
+
console.error("");
|
|
272
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
273
|
+
// Quote for shell copy/paste safety.
|
|
274
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
275
|
+
console.error("");
|
|
280
276
|
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
281
277
|
}
|
|
282
278
|
else {
|
|
@@ -305,7 +301,6 @@ program
|
|
|
305
301
|
monitoringUser: opts.monitoringUser,
|
|
306
302
|
monitoringPassword: monPassword,
|
|
307
303
|
includeOptionalPermissions,
|
|
308
|
-
roleExists,
|
|
309
304
|
});
|
|
310
305
|
const effectivePlan = opts.resetPassword
|
|
311
306
|
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
@@ -317,9 +312,7 @@ program
|
|
|
317
312
|
console.log(redactPasswords(step.sql));
|
|
318
313
|
}
|
|
319
314
|
console.log("\n--- end SQL plan ---\n");
|
|
320
|
-
|
|
321
|
-
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
322
|
-
}
|
|
315
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
323
316
|
return;
|
|
324
317
|
}
|
|
325
318
|
const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan: effectivePlan });
|
|
@@ -349,59 +342,56 @@ program
|
|
|
349
342
|
if (!message || message === "[object Object]") {
|
|
350
343
|
message = "Unknown error";
|
|
351
344
|
}
|
|
352
|
-
console.error(
|
|
345
|
+
console.error(`Error: init: ${message}`);
|
|
353
346
|
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
354
347
|
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
355
348
|
const failedStep = stepMatch?.[1];
|
|
356
349
|
if (failedStep) {
|
|
357
|
-
console.error(`Step: ${failedStep}`);
|
|
350
|
+
console.error(` Step: ${failedStep}`);
|
|
358
351
|
}
|
|
359
352
|
if (errAny && typeof errAny === "object") {
|
|
360
353
|
if (typeof errAny.code === "string" && errAny.code) {
|
|
361
|
-
console.error(`
|
|
354
|
+
console.error(` Code: ${errAny.code}`);
|
|
362
355
|
}
|
|
363
356
|
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
364
|
-
console.error(`Detail: ${errAny.detail}`);
|
|
357
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
365
358
|
}
|
|
366
359
|
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
367
|
-
console.error(`Hint: ${errAny.hint}`);
|
|
360
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
368
361
|
}
|
|
369
362
|
}
|
|
370
363
|
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
371
364
|
if (errAny.code === "42501") {
|
|
372
|
-
console.error("");
|
|
373
|
-
console.error("Permission error: your admin connection is not allowed to complete the setup.");
|
|
374
365
|
if (failedStep === "01.role") {
|
|
375
|
-
console.error("
|
|
366
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
376
367
|
}
|
|
377
368
|
else if (failedStep === "02.permissions") {
|
|
378
|
-
console.error("
|
|
369
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
379
370
|
}
|
|
380
|
-
console.error("
|
|
381
|
-
console.error("
|
|
382
|
-
console.error("
|
|
383
|
-
console.error("Tip: run with --print-sql to review the exact SQL plan.");
|
|
384
|
-
console.error("");
|
|
385
|
-
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
371
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
372
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
373
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
386
374
|
}
|
|
387
375
|
if (errAny.code === "ECONNREFUSED") {
|
|
388
|
-
console.error("Hint: check host/port and ensure Postgres is reachable from this machine
|
|
376
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
389
377
|
}
|
|
390
378
|
if (errAny.code === "ENOTFOUND") {
|
|
391
|
-
console.error("Hint: DNS resolution failed; double-check the host name
|
|
379
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
392
380
|
}
|
|
393
381
|
if (errAny.code === "ETIMEDOUT") {
|
|
394
|
-
console.error("Hint: connection timed out; check network/firewall rules
|
|
382
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
395
383
|
}
|
|
396
384
|
}
|
|
397
385
|
process.exitCode = 1;
|
|
398
386
|
}
|
|
399
387
|
finally {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
388
|
+
if (client) {
|
|
389
|
+
try {
|
|
390
|
+
await client.end();
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// ignore
|
|
394
|
+
}
|
|
405
395
|
}
|
|
406
396
|
}
|
|
407
397
|
});
|