postgresai 0.12.0-beta.7 → 0.14.0-beta.2
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 +80 -15
- package/bin/postgres-ai.ts +333 -0
- package/dist/bin/postgres-ai.js +307 -0
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +77 -0
- package/dist/lib/init.d.ts.map +1 -0
- package/dist/lib/init.js +550 -0
- package/dist/lib/init.js.map +1 -0
- package/dist/package.json +3 -2
- package/lib/init.ts +629 -0
- package/package.json +3 -2
- package/sql/01.role.sql +16 -0
- package/sql/02.permissions.sql +33 -0
- package/sql/03.optional_rds.sql +6 -0
- package/sql/04.optional_self_managed.sql +8 -0
- package/test/init.integration.test.cjs +382 -0
- package/test/init.test.cjs +323 -0
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
|
|
@@ -31,7 +33,70 @@ The CLI provides three command aliases:
|
|
|
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
|
+
## init (create monitoring user in Postgres)
|
|
45
|
+
|
|
46
|
+
This command creates (or updates) the `postgres_ai_mon` user and grants the permissions described in the root `README.md` (it is idempotent).
|
|
47
|
+
|
|
48
|
+
Run without installing (positional connection string):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx postgresai init postgresql://admin@host:5432/dbname
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It also accepts libpq “conninfo” syntax:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx postgresai init "dbname=dbname host=host user=admin"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
And psql-like options:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx postgresai init -h host -p 5432 -U admin -d dbname
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Password input options (in priority order):
|
|
67
|
+
- `--password <password>`
|
|
68
|
+
- `PGAI_MON_PASSWORD` environment variable
|
|
69
|
+
- if not provided: a strong password is generated automatically
|
|
70
|
+
|
|
71
|
+
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:
|
|
72
|
+
- `--print-password` (dangerous in CI logs)
|
|
73
|
+
|
|
74
|
+
Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissions
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Print SQL / dry run
|
|
81
|
+
|
|
82
|
+
To see what SQL would be executed (passwords redacted by default):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx postgresai init postgresql://admin@host:5432/dbname --print-sql
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Verify and password reset
|
|
89
|
+
|
|
90
|
+
Verify that everything is configured as expected (no changes):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx postgresai init postgresql://admin@host:5432/dbname --verify
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Reset monitoring user password only (no other changes):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx postgresai init postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
|
|
35
100
|
```
|
|
36
101
|
|
|
37
102
|
## Quick start
|
|
@@ -40,7 +105,7 @@ pgai --help # short alias
|
|
|
40
105
|
|
|
41
106
|
Authenticate via browser to obtain API key:
|
|
42
107
|
```bash
|
|
43
|
-
|
|
108
|
+
postgresai auth
|
|
44
109
|
```
|
|
45
110
|
|
|
46
111
|
This will:
|
|
@@ -122,7 +187,7 @@ postgres-ai mon shell <service> # Open shell to monitoring servic
|
|
|
122
187
|
### MCP server (`mcp` group)
|
|
123
188
|
|
|
124
189
|
```bash
|
|
125
|
-
|
|
190
|
+
postgresai mcp start # Start MCP stdio server exposing tools
|
|
126
191
|
```
|
|
127
192
|
|
|
128
193
|
Cursor configuration example (Settings → MCP):
|
|
@@ -131,7 +196,7 @@ Cursor configuration example (Settings → MCP):
|
|
|
131
196
|
{
|
|
132
197
|
"mcpServers": {
|
|
133
198
|
"PostgresAI": {
|
|
134
|
-
"command": "
|
|
199
|
+
"command": "postgresai",
|
|
135
200
|
"args": ["mcp", "start"],
|
|
136
201
|
"env": {
|
|
137
202
|
"PGAI_API_BASE_URL": "https://postgres.ai/api/general/"
|
|
@@ -142,16 +207,16 @@ Cursor configuration example (Settings → MCP):
|
|
|
142
207
|
```
|
|
143
208
|
|
|
144
209
|
Tools exposed:
|
|
145
|
-
- list_issues: returns the same JSON as `
|
|
210
|
+
- list_issues: returns the same JSON as `postgresai issues list`.
|
|
146
211
|
- view_issue: view a single issue with its comments (args: { issue_id, debug? })
|
|
147
212
|
- post_issue_comment: post a comment (args: { issue_id, content, parent_comment_id?, debug? })
|
|
148
213
|
|
|
149
214
|
### Issues management (`issues` group)
|
|
150
215
|
|
|
151
216
|
```bash
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
217
|
+
postgresai issues list # List issues (shows: id, title, status, created_at)
|
|
218
|
+
postgresai issues view <issueId> # View issue details and comments
|
|
219
|
+
postgresai issues post_comment <issueId> <content> # Post a comment to an issue
|
|
155
220
|
# Options:
|
|
156
221
|
# --parent <uuid> Parent comment ID (for replies)
|
|
157
222
|
# --debug Enable debug output
|
|
@@ -165,13 +230,13 @@ By default, issues commands print human-friendly YAML when writing to a terminal
|
|
|
165
230
|
- Use `--json` to force JSON output:
|
|
166
231
|
|
|
167
232
|
```bash
|
|
168
|
-
|
|
233
|
+
postgresai issues list --json | jq '.[] | {id, title}'
|
|
169
234
|
```
|
|
170
235
|
|
|
171
236
|
- Rely on auto-detection: when stdout is not a TTY (e.g., piped or redirected), output is JSON automatically:
|
|
172
237
|
|
|
173
238
|
```bash
|
|
174
|
-
|
|
239
|
+
postgresai issues view <issueId> > issue.json
|
|
175
240
|
```
|
|
176
241
|
|
|
177
242
|
#### Grafana management
|
|
@@ -235,7 +300,7 @@ Linux/macOS (bash/zsh):
|
|
|
235
300
|
```bash
|
|
236
301
|
export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/
|
|
237
302
|
export PGAI_UI_BASE_URL=https://console-dev.postgres.ai
|
|
238
|
-
|
|
303
|
+
postgresai auth --debug
|
|
239
304
|
```
|
|
240
305
|
|
|
241
306
|
Windows PowerShell:
|
|
@@ -243,13 +308,13 @@ Windows PowerShell:
|
|
|
243
308
|
```powershell
|
|
244
309
|
$env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/"
|
|
245
310
|
$env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai"
|
|
246
|
-
|
|
311
|
+
postgresai auth --debug
|
|
247
312
|
```
|
|
248
313
|
|
|
249
314
|
Via CLI options (overrides env):
|
|
250
315
|
|
|
251
316
|
```bash
|
|
252
|
-
|
|
317
|
+
postgresai auth --debug \
|
|
253
318
|
--api-base-url https://v2.postgres.ai/api/general/ \
|
|
254
319
|
--ui-base-url https://console-dev.postgres.ai
|
|
255
320
|
```
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -12,9 +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";
|
|
19
|
+
import { applyInitPlan, buildInitPlan, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
18
20
|
|
|
19
21
|
const execPromise = promisify(exec);
|
|
20
22
|
const execFilePromise = promisify(execFile);
|
|
@@ -116,6 +118,337 @@ program
|
|
|
116
118
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
117
119
|
);
|
|
118
120
|
|
|
121
|
+
program
|
|
122
|
+
.command("init [conn]")
|
|
123
|
+
.description("Create a monitoring user and grant all required permissions (idempotent)")
|
|
124
|
+
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
|
|
125
|
+
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
|
|
126
|
+
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
|
|
127
|
+
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
128
|
+
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
129
|
+
.option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
|
|
130
|
+
.option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER)
|
|
131
|
+
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
132
|
+
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
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)
|
|
136
|
+
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
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
|
+
"Environment variables (libpq standard):",
|
|
156
|
+
" PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
|
|
157
|
+
" PGPASSWORD — admin password",
|
|
158
|
+
" PGAI_MON_PASSWORD — monitoring password",
|
|
159
|
+
"",
|
|
160
|
+
"Inspect SQL without applying changes:",
|
|
161
|
+
" postgresai init <conn> --print-sql",
|
|
162
|
+
"",
|
|
163
|
+
"Verify setup (no changes):",
|
|
164
|
+
" postgresai init <conn> --verify",
|
|
165
|
+
"",
|
|
166
|
+
"Reset monitoring password only:",
|
|
167
|
+
" postgresai init <conn> --reset-password --password '...'",
|
|
168
|
+
"",
|
|
169
|
+
"Offline SQL plan (no DB connection):",
|
|
170
|
+
" postgresai init --print-sql -d dbname --password '...' --show-secrets",
|
|
171
|
+
].join("\n")
|
|
172
|
+
)
|
|
173
|
+
.action(async (conn: string | undefined, opts: {
|
|
174
|
+
dbUrl?: string;
|
|
175
|
+
host?: string;
|
|
176
|
+
port?: string;
|
|
177
|
+
username?: string;
|
|
178
|
+
dbname?: string;
|
|
179
|
+
adminPassword?: string;
|
|
180
|
+
monitoringUser: string;
|
|
181
|
+
password?: string;
|
|
182
|
+
skipOptionalPermissions?: boolean;
|
|
183
|
+
verify?: boolean;
|
|
184
|
+
resetPassword?: boolean;
|
|
185
|
+
printSql?: boolean;
|
|
186
|
+
showSecrets?: boolean;
|
|
187
|
+
printPassword?: boolean;
|
|
188
|
+
}, cmd: Command) => {
|
|
189
|
+
if (opts.verify && opts.resetPassword) {
|
|
190
|
+
console.error("✗ Provide only one of --verify or --reset-password");
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (opts.verify && opts.printSql) {
|
|
195
|
+
console.error("✗ --verify cannot be combined with --print-sql");
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
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
|
+
};
|
|
207
|
+
|
|
208
|
+
// 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.
|
|
210
|
+
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
211
|
+
if (shouldPrintSql) {
|
|
212
|
+
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
213
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
214
|
+
|
|
215
|
+
// Use explicit password/env if provided; otherwise use a placeholder (will be redacted unless --show-secrets).
|
|
216
|
+
const monPassword =
|
|
217
|
+
(opts.password ?? process.env.PGAI_MON_PASSWORD ?? "CHANGE_ME").toString();
|
|
218
|
+
|
|
219
|
+
const plan = await buildInitPlan({
|
|
220
|
+
database,
|
|
221
|
+
monitoringUser: opts.monitoringUser,
|
|
222
|
+
monitoringPassword: monPassword,
|
|
223
|
+
includeOptionalPermissions,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
227
|
+
console.log(`-- database: ${database}`);
|
|
228
|
+
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
229
|
+
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
230
|
+
for (const step of plan.steps) {
|
|
231
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
232
|
+
console.log(redactPasswords(step.sql));
|
|
233
|
+
}
|
|
234
|
+
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
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let adminConn;
|
|
243
|
+
try {
|
|
244
|
+
adminConn = resolveAdminConnection({
|
|
245
|
+
conn,
|
|
246
|
+
dbUrlFlag: opts.dbUrl,
|
|
247
|
+
// Allow libpq standard env vars as implicit defaults (common UX).
|
|
248
|
+
host: opts.host ?? process.env.PGHOST,
|
|
249
|
+
port: opts.port ?? process.env.PGPORT,
|
|
250
|
+
username: opts.username ?? process.env.PGUSER,
|
|
251
|
+
dbname: opts.dbname ?? process.env.PGDATABASE,
|
|
252
|
+
adminPassword: opts.adminPassword,
|
|
253
|
+
envPassword: process.env.PGPASSWORD,
|
|
254
|
+
});
|
|
255
|
+
} catch (e) {
|
|
256
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
257
|
+
console.error(`Error: init: ${msg}`);
|
|
258
|
+
// When connection details are missing, show full init help (options + examples).
|
|
259
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
260
|
+
console.error("");
|
|
261
|
+
cmd.outputHelp({ error: true });
|
|
262
|
+
}
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
268
|
+
|
|
269
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
270
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
271
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
272
|
+
|
|
273
|
+
// Use native pg client instead of requiring psql to be installed
|
|
274
|
+
let client: Client | undefined;
|
|
275
|
+
try {
|
|
276
|
+
client = new Client(adminConn.clientConfig);
|
|
277
|
+
await client.connect();
|
|
278
|
+
|
|
279
|
+
const dbRes = await client.query("select current_database() as db");
|
|
280
|
+
const database = dbRes.rows?.[0]?.db;
|
|
281
|
+
if (typeof database !== "string" || !database) {
|
|
282
|
+
throw new Error("Failed to resolve current database name");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (opts.verify) {
|
|
286
|
+
const v = await verifyInitSetup({
|
|
287
|
+
client,
|
|
288
|
+
database,
|
|
289
|
+
monitoringUser: opts.monitoringUser,
|
|
290
|
+
includeOptionalPermissions,
|
|
291
|
+
});
|
|
292
|
+
if (v.ok) {
|
|
293
|
+
console.log("✓ init verify: OK");
|
|
294
|
+
if (v.missingOptional.length > 0) {
|
|
295
|
+
console.log("⚠ Optional items missing:");
|
|
296
|
+
for (const m of v.missingOptional) console.log(`- ${m}`);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.error("✗ init verify failed: missing required items");
|
|
301
|
+
for (const m of v.missingRequired) console.error(`- ${m}`);
|
|
302
|
+
if (v.missingOptional.length > 0) {
|
|
303
|
+
console.error("Optional items missing:");
|
|
304
|
+
for (const m of v.missingOptional) console.error(`- ${m}`);
|
|
305
|
+
}
|
|
306
|
+
process.exitCode = 1;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let monPassword: string;
|
|
311
|
+
try {
|
|
312
|
+
const resolved = await resolveMonitoringPassword({
|
|
313
|
+
passwordFlag: opts.password,
|
|
314
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
315
|
+
monitoringUser: opts.monitoringUser,
|
|
316
|
+
});
|
|
317
|
+
monPassword = resolved.password;
|
|
318
|
+
if (resolved.generated) {
|
|
319
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
320
|
+
if (canPrint) {
|
|
321
|
+
// Print secrets to stderr to reduce the chance they end up in piped stdout logs.
|
|
322
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
323
|
+
console.error("");
|
|
324
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
325
|
+
// Quote for shell copy/paste safety.
|
|
326
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
327
|
+
console.error("");
|
|
328
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
329
|
+
} else {
|
|
330
|
+
console.error(
|
|
331
|
+
[
|
|
332
|
+
`✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
333
|
+
"",
|
|
334
|
+
"Provide it explicitly:",
|
|
335
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
336
|
+
"",
|
|
337
|
+
"Or (NOT recommended) print the generated password:",
|
|
338
|
+
" --print-password",
|
|
339
|
+
].join("\n")
|
|
340
|
+
);
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (e) {
|
|
346
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
347
|
+
console.error(`✗ ${msg}`);
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const plan = await buildInitPlan({
|
|
353
|
+
database,
|
|
354
|
+
monitoringUser: opts.monitoringUser,
|
|
355
|
+
monitoringPassword: monPassword,
|
|
356
|
+
includeOptionalPermissions,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const effectivePlan = opts.resetPassword
|
|
360
|
+
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
361
|
+
: plan;
|
|
362
|
+
|
|
363
|
+
if (shouldPrintSql) {
|
|
364
|
+
console.log("\n--- SQL plan ---");
|
|
365
|
+
for (const step of effectivePlan.steps) {
|
|
366
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
367
|
+
console.log(redactPasswords(step.sql));
|
|
368
|
+
}
|
|
369
|
+
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
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
377
|
+
|
|
378
|
+
console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
|
|
379
|
+
if (skippedOptional.length > 0) {
|
|
380
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
381
|
+
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
382
|
+
}
|
|
383
|
+
// Keep output compact but still useful
|
|
384
|
+
if (process.stdout.isTTY) {
|
|
385
|
+
console.log(`Applied ${applied.length} steps`);
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
const errAny = error as any;
|
|
389
|
+
let message = "";
|
|
390
|
+
if (error instanceof Error && error.message) {
|
|
391
|
+
message = error.message;
|
|
392
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
393
|
+
message = errAny.message;
|
|
394
|
+
} else {
|
|
395
|
+
message = String(error);
|
|
396
|
+
}
|
|
397
|
+
if (!message || message === "[object Object]") {
|
|
398
|
+
message = "Unknown error";
|
|
399
|
+
}
|
|
400
|
+
console.error(`Error: init: ${message}`);
|
|
401
|
+
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
402
|
+
const stepMatch =
|
|
403
|
+
typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
404
|
+
const failedStep = stepMatch?.[1];
|
|
405
|
+
if (failedStep) {
|
|
406
|
+
console.error(` Step: ${failedStep}`);
|
|
407
|
+
}
|
|
408
|
+
if (errAny && typeof errAny === "object") {
|
|
409
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
410
|
+
console.error(` Code: ${errAny.code}`);
|
|
411
|
+
}
|
|
412
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
413
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
414
|
+
}
|
|
415
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
416
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
420
|
+
if (errAny.code === "42501") {
|
|
421
|
+
if (failedStep === "01.role") {
|
|
422
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
423
|
+
} else if (failedStep === "02.permissions") {
|
|
424
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
425
|
+
}
|
|
426
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
427
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
428
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
429
|
+
}
|
|
430
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
431
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
432
|
+
}
|
|
433
|
+
if (errAny.code === "ENOTFOUND") {
|
|
434
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
435
|
+
}
|
|
436
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
437
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
} finally {
|
|
442
|
+
if (client) {
|
|
443
|
+
try {
|
|
444
|
+
await client.end();
|
|
445
|
+
} catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
119
452
|
/**
|
|
120
453
|
* Stub function for not implemented commands
|
|
121
454
|
*/
|