postgresai 0.14.0-dev.10 → 0.14.0-dev.12
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 +14 -3
- package/bin/postgres-ai.ts +160 -29
- package/dist/bin/postgres-ai.js +148 -26
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +11 -0
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/init.js +106 -12
- package/dist/lib/init.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/init.ts +142 -12
- package/package.json +1 -1
- package/sql/01.role.sql +11 -0
- package/test/init.integration.test.cjs +86 -0
- package/test/init.test.cjs +30 -0
package/README.md
CHANGED
|
@@ -59,7 +59,10 @@ npx postgresai init -h host -p 5432 -U admin -d dbname
|
|
|
59
59
|
Password input options (in priority order):
|
|
60
60
|
- `--password <password>`
|
|
61
61
|
- `PGAI_MON_PASSWORD` environment variable
|
|
62
|
-
- if not provided: a strong password is generated automatically
|
|
62
|
+
- if not provided: a strong password is generated automatically
|
|
63
|
+
|
|
64
|
+
By default, the generated password is printed **only in interactive (TTY) mode**. In non-interactive mode, you must either provide the password explicitly, or opt-in to printing it:
|
|
65
|
+
- `--print-password` (dangerous in CI logs)
|
|
63
66
|
|
|
64
67
|
Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
|
|
65
68
|
|
|
@@ -75,10 +78,18 @@ To see what SQL would be executed (passwords redacted by default):
|
|
|
75
78
|
npx postgresai init postgresql://admin@host:5432/dbname --print-sql
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
### Verify and password reset
|
|
82
|
+
|
|
83
|
+
Verify that everything is configured as expected (no changes):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx postgresai init postgresql://admin@host:5432/dbname --verify
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Reset monitoring user password only (no other changes):
|
|
79
90
|
|
|
80
91
|
```bash
|
|
81
|
-
npx postgresai init postgresql://admin@host:5432/dbname --
|
|
92
|
+
npx postgresai init postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
|
|
82
93
|
```
|
|
83
94
|
|
|
84
95
|
## Quick start
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { promisify } from "util";
|
|
|
12
12
|
import * as readline from "readline";
|
|
13
13
|
import * as http from "https";
|
|
14
14
|
import { URL } from "url";
|
|
15
|
+
import { Client } from "pg";
|
|
15
16
|
import { startMcpServer } from "../lib/mcp-server";
|
|
16
17
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
17
18
|
import { resolveBaseUrls } from "../lib/util";
|
|
18
|
-
import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword } from "../lib/init";
|
|
19
|
+
import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
19
20
|
|
|
20
21
|
const execPromise = promisify(exec);
|
|
21
22
|
const execFilePromise = promisify(execFile);
|
|
@@ -129,9 +130,41 @@ program
|
|
|
129
130
|
.option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
|
|
130
131
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
131
132
|
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
132
|
-
.option("--
|
|
133
|
+
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
134
|
+
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
135
|
+
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
133
136
|
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
134
|
-
.option("--
|
|
137
|
+
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
138
|
+
.addHelpText(
|
|
139
|
+
"after",
|
|
140
|
+
[
|
|
141
|
+
"",
|
|
142
|
+
"Examples:",
|
|
143
|
+
" postgresai init postgresql://admin@host:5432/dbname",
|
|
144
|
+
" postgresai init \"dbname=dbname host=host user=admin\"",
|
|
145
|
+
" postgresai init -h host -p 5432 -U admin -d dbname",
|
|
146
|
+
"",
|
|
147
|
+
"Admin password:",
|
|
148
|
+
" --admin-password <password> or PGPASSWORD=... (libpq standard)",
|
|
149
|
+
"",
|
|
150
|
+
"Monitoring password:",
|
|
151
|
+
" --password <password> or PGAI_MON_PASSWORD=... (otherwise auto-generated)",
|
|
152
|
+
" If auto-generated, it is printed only on TTY by default.",
|
|
153
|
+
" To print it in non-interactive mode: --print-password",
|
|
154
|
+
"",
|
|
155
|
+
"Inspect SQL without applying changes:",
|
|
156
|
+
" postgresai init <conn> --print-sql",
|
|
157
|
+
"",
|
|
158
|
+
"Verify setup (no changes):",
|
|
159
|
+
" postgresai init <conn> --verify",
|
|
160
|
+
"",
|
|
161
|
+
"Reset monitoring password only:",
|
|
162
|
+
" postgresai init <conn> --reset-password --password '...'",
|
|
163
|
+
"",
|
|
164
|
+
"Offline SQL plan (no DB connection):",
|
|
165
|
+
" postgresai init --print-sql -d dbname --password '...' --show-secrets",
|
|
166
|
+
].join("\n")
|
|
167
|
+
)
|
|
135
168
|
.action(async (conn: string | undefined, opts: {
|
|
136
169
|
dbUrl?: string;
|
|
137
170
|
host?: string;
|
|
@@ -142,19 +175,76 @@ program
|
|
|
142
175
|
monitoringUser: string;
|
|
143
176
|
password?: string;
|
|
144
177
|
skipOptionalPermissions?: boolean;
|
|
178
|
+
verify?: boolean;
|
|
179
|
+
resetPassword?: boolean;
|
|
145
180
|
printSql?: boolean;
|
|
146
181
|
showSecrets?: boolean;
|
|
147
|
-
|
|
182
|
+
printPassword?: boolean;
|
|
148
183
|
}) => {
|
|
184
|
+
if (opts.verify && opts.resetPassword) {
|
|
185
|
+
console.error("✗ Provide only one of --verify or --reset-password");
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (opts.verify && opts.printSql) {
|
|
190
|
+
console.error("✗ --verify cannot be combined with --print-sql");
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const shouldPrintSql = !!opts.printSql;
|
|
196
|
+
const shouldRedactSecrets = !opts.showSecrets;
|
|
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
|
+
};
|
|
202
|
+
|
|
203
|
+
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
204
|
+
// Useful for audits/reviews; caller can provide -d/PGDATABASE and an explicit monitoring password.
|
|
205
|
+
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
206
|
+
if (shouldPrintSql) {
|
|
207
|
+
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
208
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
209
|
+
|
|
210
|
+
// Use explicit password/env if provided; otherwise use a placeholder (will be redacted unless --show-secrets).
|
|
211
|
+
const monPassword =
|
|
212
|
+
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "CHANGE_ME").toString();
|
|
213
|
+
|
|
214
|
+
const plan = await buildInitPlan({
|
|
215
|
+
database,
|
|
216
|
+
monitoringUser: opts.monitoringUser,
|
|
217
|
+
monitoringPassword: monPassword,
|
|
218
|
+
includeOptionalPermissions,
|
|
219
|
+
roleExists: undefined,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
223
|
+
console.log(`-- database: ${database}`);
|
|
224
|
+
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
225
|
+
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
226
|
+
for (const step of plan.steps) {
|
|
227
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
228
|
+
console.log(redactPasswords(step.sql));
|
|
229
|
+
}
|
|
230
|
+
console.log("\n--- end SQL plan ---\n");
|
|
231
|
+
if (shouldRedactSecrets) {
|
|
232
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
149
238
|
let adminConn;
|
|
150
239
|
try {
|
|
151
240
|
adminConn = resolveAdminConnection({
|
|
152
241
|
conn,
|
|
153
242
|
dbUrlFlag: opts.dbUrl,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
243
|
+
// Allow libpq standard env vars as implicit defaults (common UX).
|
|
244
|
+
host: opts.host ?? process.env.PGHOST,
|
|
245
|
+
port: opts.port ?? process.env.PGPORT,
|
|
246
|
+
username: opts.username ?? process.env.PGUSER,
|
|
247
|
+
dbname: opts.dbname ?? process.env.PGDATABASE,
|
|
158
248
|
adminPassword: opts.adminPassword,
|
|
159
249
|
envPassword: process.env.PGPASSWORD,
|
|
160
250
|
});
|
|
@@ -171,10 +261,7 @@ program
|
|
|
171
261
|
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
172
262
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
173
263
|
|
|
174
|
-
const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
|
|
175
|
-
|
|
176
264
|
// Use native pg client instead of requiring psql to be installed
|
|
177
|
-
const { Client } = require("pg");
|
|
178
265
|
const client = new Client(adminConn.clientConfig);
|
|
179
266
|
|
|
180
267
|
try {
|
|
@@ -183,7 +270,7 @@ program
|
|
|
183
270
|
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
184
271
|
opts.monitoringUser,
|
|
185
272
|
]);
|
|
186
|
-
const roleExists = roleRes.rowCount > 0;
|
|
273
|
+
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
187
274
|
|
|
188
275
|
const dbRes = await client.query("select current_database() as db");
|
|
189
276
|
const database = dbRes.rows?.[0]?.db;
|
|
@@ -191,6 +278,31 @@ program
|
|
|
191
278
|
throw new Error("Failed to resolve current database name");
|
|
192
279
|
}
|
|
193
280
|
|
|
281
|
+
if (opts.verify) {
|
|
282
|
+
const v = await verifyInitSetup({
|
|
283
|
+
client,
|
|
284
|
+
database,
|
|
285
|
+
monitoringUser: opts.monitoringUser,
|
|
286
|
+
includeOptionalPermissions,
|
|
287
|
+
});
|
|
288
|
+
if (v.ok) {
|
|
289
|
+
console.log("✓ init verify: OK");
|
|
290
|
+
if (v.missingOptional.length > 0) {
|
|
291
|
+
console.log("⚠ Optional items missing:");
|
|
292
|
+
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.error("✗ init verify failed: missing required items");
|
|
297
|
+
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
298
|
+
if (v.missingOptional.length > 0) {
|
|
299
|
+
console.error("Optional items missing:");
|
|
300
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
301
|
+
}
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
194
306
|
let monPassword: string;
|
|
195
307
|
try {
|
|
196
308
|
const resolved = await resolveMonitoringPassword({
|
|
@@ -200,8 +312,25 @@ program
|
|
|
200
312
|
});
|
|
201
313
|
monPassword = resolved.password;
|
|
202
314
|
if (resolved.generated) {
|
|
203
|
-
|
|
204
|
-
|
|
315
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
316
|
+
if (canPrint) {
|
|
317
|
+
console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
|
|
318
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
319
|
+
} else {
|
|
320
|
+
console.error(
|
|
321
|
+
[
|
|
322
|
+
`✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
323
|
+
"",
|
|
324
|
+
"Provide it explicitly:",
|
|
325
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
326
|
+
"",
|
|
327
|
+
"Or (NOT recommended) print the generated password:",
|
|
328
|
+
" --print-password",
|
|
329
|
+
].join("\n")
|
|
330
|
+
);
|
|
331
|
+
process.exitCode = 1;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
205
334
|
}
|
|
206
335
|
} catch (e) {
|
|
207
336
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -218,33 +347,26 @@ program
|
|
|
218
347
|
roleExists,
|
|
219
348
|
});
|
|
220
349
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (!redact) return sql;
|
|
225
|
-
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
226
|
-
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
227
|
-
};
|
|
350
|
+
const effectivePlan = opts.resetPassword
|
|
351
|
+
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
352
|
+
: plan;
|
|
228
353
|
|
|
354
|
+
if (shouldPrintSql) {
|
|
229
355
|
console.log("\n--- SQL plan ---");
|
|
230
|
-
for (const step of
|
|
356
|
+
for (const step of effectivePlan.steps) {
|
|
231
357
|
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
232
358
|
console.log(redactPasswords(step.sql));
|
|
233
359
|
}
|
|
234
360
|
console.log("\n--- end SQL plan ---\n");
|
|
235
|
-
if (
|
|
361
|
+
if (shouldRedactSecrets) {
|
|
236
362
|
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
237
363
|
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (opts.dryRun) {
|
|
241
|
-
console.log("✓ dry-run completed (no changes were applied)");
|
|
242
364
|
return;
|
|
243
365
|
}
|
|
244
366
|
|
|
245
|
-
const { applied, skippedOptional } = await applyInitPlan({ client, plan });
|
|
367
|
+
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
246
368
|
|
|
247
|
-
console.log("✓ init completed");
|
|
369
|
+
console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
|
|
248
370
|
if (skippedOptional.length > 0) {
|
|
249
371
|
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
250
372
|
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
@@ -282,6 +404,15 @@ program
|
|
|
282
404
|
if (errAny.code === "42501") {
|
|
283
405
|
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
284
406
|
}
|
|
407
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
408
|
+
console.error("Hint: check host/port and ensure Postgres is reachable from this machine.");
|
|
409
|
+
}
|
|
410
|
+
if (errAny.code === "ENOTFOUND") {
|
|
411
|
+
console.error("Hint: DNS resolution failed; double-check the host name.");
|
|
412
|
+
}
|
|
413
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
414
|
+
console.error("Hint: connection timed out; check network/firewall rules.");
|
|
415
|
+
}
|
|
285
416
|
}
|
|
286
417
|
process.exitCode = 1;
|
|
287
418
|
} finally {
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -46,6 +46,7 @@ const util_1 = require("util");
|
|
|
46
46
|
const readline = __importStar(require("readline"));
|
|
47
47
|
const http = __importStar(require("https"));
|
|
48
48
|
const url_1 = require("url");
|
|
49
|
+
const pg_1 = require("pg");
|
|
49
50
|
const mcp_server_1 = require("../lib/mcp-server");
|
|
50
51
|
const issues_1 = require("../lib/issues");
|
|
51
52
|
const util_2 = require("../lib/util");
|
|
@@ -111,19 +112,97 @@ program
|
|
|
111
112
|
.option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
|
|
112
113
|
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
113
114
|
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
114
|
-
.option("--
|
|
115
|
+
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
116
|
+
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
117
|
+
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
115
118
|
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
116
|
-
.option("--
|
|
119
|
+
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
120
|
+
.addHelpText("after", [
|
|
121
|
+
"",
|
|
122
|
+
"Examples:",
|
|
123
|
+
" postgresai init postgresql://admin@host:5432/dbname",
|
|
124
|
+
" postgresai init \"dbname=dbname host=host user=admin\"",
|
|
125
|
+
" postgresai init -h host -p 5432 -U admin -d dbname",
|
|
126
|
+
"",
|
|
127
|
+
"Admin password:",
|
|
128
|
+
" --admin-password <password> or PGPASSWORD=... (libpq standard)",
|
|
129
|
+
"",
|
|
130
|
+
"Monitoring password:",
|
|
131
|
+
" --password <password> or PGAI_MON_PASSWORD=... (otherwise auto-generated)",
|
|
132
|
+
" If auto-generated, it is printed only on TTY by default.",
|
|
133
|
+
" To print it in non-interactive mode: --print-password",
|
|
134
|
+
"",
|
|
135
|
+
"Inspect SQL without applying changes:",
|
|
136
|
+
" postgresai init <conn> --print-sql",
|
|
137
|
+
"",
|
|
138
|
+
"Verify setup (no changes):",
|
|
139
|
+
" postgresai init <conn> --verify",
|
|
140
|
+
"",
|
|
141
|
+
"Reset monitoring password only:",
|
|
142
|
+
" postgresai init <conn> --reset-password --password '...'",
|
|
143
|
+
"",
|
|
144
|
+
"Offline SQL plan (no DB connection):",
|
|
145
|
+
" postgresai init --print-sql -d dbname --password '...' --show-secrets",
|
|
146
|
+
].join("\n"))
|
|
117
147
|
.action(async (conn, opts) => {
|
|
148
|
+
if (opts.verify && opts.resetPassword) {
|
|
149
|
+
console.error("✗ Provide only one of --verify or --reset-password");
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (opts.verify && opts.printSql) {
|
|
154
|
+
console.error("✗ --verify cannot be combined with --print-sql");
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const shouldPrintSql = !!opts.printSql;
|
|
159
|
+
const shouldRedactSecrets = !opts.showSecrets;
|
|
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
|
+
};
|
|
166
|
+
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
167
|
+
// Useful for audits/reviews; caller can provide -d/PGDATABASE and an explicit monitoring password.
|
|
168
|
+
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
169
|
+
if (shouldPrintSql) {
|
|
170
|
+
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
171
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
172
|
+
// Use explicit password/env if provided; otherwise use a placeholder (will be redacted unless --show-secrets).
|
|
173
|
+
const monPassword = (opts.password ?? process.env.PGAI_MON_PASSWORD ?? "CHANGE_ME").toString();
|
|
174
|
+
const plan = await (0, init_1.buildInitPlan)({
|
|
175
|
+
database,
|
|
176
|
+
monitoringUser: opts.monitoringUser,
|
|
177
|
+
monitoringPassword: monPassword,
|
|
178
|
+
includeOptionalPermissions,
|
|
179
|
+
roleExists: undefined,
|
|
180
|
+
});
|
|
181
|
+
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
182
|
+
console.log(`-- database: ${database}`);
|
|
183
|
+
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
184
|
+
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
185
|
+
for (const step of plan.steps) {
|
|
186
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
187
|
+
console.log(redactPasswords(step.sql));
|
|
188
|
+
}
|
|
189
|
+
console.log("\n--- end SQL plan ---\n");
|
|
190
|
+
if (shouldRedactSecrets) {
|
|
191
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
118
196
|
let adminConn;
|
|
119
197
|
try {
|
|
120
198
|
adminConn = (0, init_1.resolveAdminConnection)({
|
|
121
199
|
conn,
|
|
122
200
|
dbUrlFlag: opts.dbUrl,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
201
|
+
// Allow libpq standard env vars as implicit defaults (common UX).
|
|
202
|
+
host: opts.host ?? process.env.PGHOST,
|
|
203
|
+
port: opts.port ?? process.env.PGPORT,
|
|
204
|
+
username: opts.username ?? process.env.PGUSER,
|
|
205
|
+
dbname: opts.dbname ?? process.env.PGDATABASE,
|
|
127
206
|
adminPassword: opts.adminPassword,
|
|
128
207
|
envPassword: process.env.PGPASSWORD,
|
|
129
208
|
});
|
|
@@ -138,21 +217,46 @@ program
|
|
|
138
217
|
console.log(`Connecting to: ${adminConn.display}`);
|
|
139
218
|
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
140
219
|
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
141
|
-
const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
|
|
142
220
|
// Use native pg client instead of requiring psql to be installed
|
|
143
|
-
const
|
|
144
|
-
const client = new Client(adminConn.clientConfig);
|
|
221
|
+
const client = new pg_1.Client(adminConn.clientConfig);
|
|
145
222
|
try {
|
|
146
223
|
await client.connect();
|
|
147
224
|
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
148
225
|
opts.monitoringUser,
|
|
149
226
|
]);
|
|
150
|
-
const roleExists = roleRes.rowCount > 0;
|
|
227
|
+
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
151
228
|
const dbRes = await client.query("select current_database() as db");
|
|
152
229
|
const database = dbRes.rows?.[0]?.db;
|
|
153
230
|
if (typeof database !== "string" || !database) {
|
|
154
231
|
throw new Error("Failed to resolve current database name");
|
|
155
232
|
}
|
|
233
|
+
if (opts.verify) {
|
|
234
|
+
const v = await (0, init_1.verifyInitSetup)({
|
|
235
|
+
client,
|
|
236
|
+
database,
|
|
237
|
+
monitoringUser: opts.monitoringUser,
|
|
238
|
+
includeOptionalPermissions,
|
|
239
|
+
});
|
|
240
|
+
if (v.ok) {
|
|
241
|
+
console.log("✓ init verify: OK");
|
|
242
|
+
if (v.missingOptional.length > 0) {
|
|
243
|
+
console.log("⚠ Optional items missing:");
|
|
244
|
+
for (const m of v.missingOptional)
|
|
245
|
+
console.log(`- ${m}`);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.error("✗ init verify failed: missing required items");
|
|
250
|
+
for (const m of v.missingRequired)
|
|
251
|
+
console.error(`- ${m}`);
|
|
252
|
+
if (v.missingOptional.length > 0) {
|
|
253
|
+
console.error("Optional items missing:");
|
|
254
|
+
for (const m of v.missingOptional)
|
|
255
|
+
console.error(`- ${m}`);
|
|
256
|
+
}
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
156
260
|
let monPassword;
|
|
157
261
|
try {
|
|
158
262
|
const resolved = await (0, init_1.resolveMonitoringPassword)({
|
|
@@ -162,8 +266,24 @@ program
|
|
|
162
266
|
});
|
|
163
267
|
monPassword = resolved.password;
|
|
164
268
|
if (resolved.generated) {
|
|
165
|
-
|
|
166
|
-
|
|
269
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
270
|
+
if (canPrint) {
|
|
271
|
+
console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
|
|
272
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.error([
|
|
276
|
+
`✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
277
|
+
"",
|
|
278
|
+
"Provide it explicitly:",
|
|
279
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
280
|
+
"",
|
|
281
|
+
"Or (NOT recommended) print the generated password:",
|
|
282
|
+
" --print-password",
|
|
283
|
+
].join("\n"));
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
167
287
|
}
|
|
168
288
|
}
|
|
169
289
|
catch (e) {
|
|
@@ -179,30 +299,23 @@ program
|
|
|
179
299
|
includeOptionalPermissions,
|
|
180
300
|
roleExists,
|
|
181
301
|
});
|
|
302
|
+
const effectivePlan = opts.resetPassword
|
|
303
|
+
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
304
|
+
: plan;
|
|
182
305
|
if (shouldPrintSql) {
|
|
183
|
-
const redact = !opts.showSecrets;
|
|
184
|
-
const redactPasswords = (sql) => {
|
|
185
|
-
if (!redact)
|
|
186
|
-
return sql;
|
|
187
|
-
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
188
|
-
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
189
|
-
};
|
|
190
306
|
console.log("\n--- SQL plan ---");
|
|
191
|
-
for (const step of
|
|
307
|
+
for (const step of effectivePlan.steps) {
|
|
192
308
|
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
193
309
|
console.log(redactPasswords(step.sql));
|
|
194
310
|
}
|
|
195
311
|
console.log("\n--- end SQL plan ---\n");
|
|
196
|
-
if (
|
|
312
|
+
if (shouldRedactSecrets) {
|
|
197
313
|
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
198
314
|
}
|
|
199
|
-
}
|
|
200
|
-
if (opts.dryRun) {
|
|
201
|
-
console.log("✓ dry-run completed (no changes were applied)");
|
|
202
315
|
return;
|
|
203
316
|
}
|
|
204
|
-
const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan });
|
|
205
|
-
console.log("✓ init completed");
|
|
317
|
+
const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan: effectivePlan });
|
|
318
|
+
console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
|
|
206
319
|
if (skippedOptional.length > 0) {
|
|
207
320
|
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
208
321
|
for (const s of skippedOptional)
|
|
@@ -244,6 +357,15 @@ program
|
|
|
244
357
|
if (errAny.code === "42501") {
|
|
245
358
|
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
246
359
|
}
|
|
360
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
361
|
+
console.error("Hint: check host/port and ensure Postgres is reachable from this machine.");
|
|
362
|
+
}
|
|
363
|
+
if (errAny.code === "ENOTFOUND") {
|
|
364
|
+
console.error("Hint: DNS resolution failed; double-check the host name.");
|
|
365
|
+
}
|
|
366
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
367
|
+
console.error("Hint: connection timed out; check network/firewall rules.");
|
|
368
|
+
}
|
|
247
369
|
}
|
|
248
370
|
process.exitCode = 1;
|
|
249
371
|
}
|