postgresai 0.12.0-beta.7 → 0.14.0-beta.1

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 CHANGED
@@ -10,9 +10,9 @@ Command-line interface for PostgresAI monitoring and database management.
10
10
  npm install -g postgresai
11
11
  ```
12
12
 
13
- Or install the latest alpha release explicitly:
13
+ Or install the latest beta release explicitly:
14
14
  ```bash
15
- npm install -g postgresai@alpha
15
+ npm install -g postgresai@beta
16
16
  ```
17
17
 
18
18
  ### From Homebrew (macOS)
@@ -34,6 +34,64 @@ postgresai --help
34
34
  pgai --help # short alias
35
35
  ```
36
36
 
37
+ ## init (create monitoring user in Postgres)
38
+
39
+ This command creates (or updates) the `postgres_ai_mon` user and grants the permissions described in the root `README.md` (it is idempotent).
40
+
41
+ Run without installing (positional connection string):
42
+
43
+ ```bash
44
+ npx postgresai init postgresql://admin@host:5432/dbname
45
+ ```
46
+
47
+ It also accepts libpq “conninfo” syntax:
48
+
49
+ ```bash
50
+ npx postgresai init "dbname=dbname host=host user=admin"
51
+ ```
52
+
53
+ And psql-like options:
54
+
55
+ ```bash
56
+ npx postgresai init -h host -p 5432 -U admin -d dbname
57
+ ```
58
+
59
+ Password input options (in priority order):
60
+ - `--password <password>`
61
+ - `PGAI_MON_PASSWORD` environment variable
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)
66
+
67
+ Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
68
+
69
+ ```bash
70
+ npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissions
71
+ ```
72
+
73
+ ### Print SQL / dry run
74
+
75
+ To see what SQL would be executed (passwords redacted by default):
76
+
77
+ ```bash
78
+ npx postgresai init postgresql://admin@host:5432/dbname --print-sql
79
+ ```
80
+
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):
90
+
91
+ ```bash
92
+ npx postgresai init postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
93
+ ```
94
+
37
95
  ## Quick start
38
96
 
39
97
  ### Authentication
@@ -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, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
18
20
 
19
21
  const execPromise = promisify(exec);
20
22
  const execFilePromise = promisify(execFile);
@@ -116,6 +118,339 @@ 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", "postgres_ai_mon")
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
+ "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
+ )
168
+ .action(async (conn: string | undefined, opts: {
169
+ dbUrl?: string;
170
+ host?: string;
171
+ port?: string;
172
+ username?: string;
173
+ dbname?: string;
174
+ adminPassword?: string;
175
+ monitoringUser: string;
176
+ password?: string;
177
+ skipOptionalPermissions?: boolean;
178
+ verify?: boolean;
179
+ resetPassword?: boolean;
180
+ printSql?: boolean;
181
+ showSecrets?: boolean;
182
+ printPassword?: boolean;
183
+ }, cmd: Command) => {
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
+
238
+ let adminConn;
239
+ try {
240
+ adminConn = resolveAdminConnection({
241
+ conn,
242
+ dbUrlFlag: opts.dbUrl,
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,
248
+ adminPassword: opts.adminPassword,
249
+ envPassword: process.env.PGPASSWORD,
250
+ });
251
+ } catch (e) {
252
+ const msg = e instanceof Error ? e.message : String(e);
253
+ console.error(`✗ ${msg}`);
254
+ // When connection details are missing, show full init help (options + examples).
255
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
256
+ console.error("");
257
+ cmd.outputHelp({ error: true });
258
+ }
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+
263
+ const includeOptionalPermissions = !opts.skipOptionalPermissions;
264
+
265
+ console.log(`Connecting to: ${adminConn.display}`);
266
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
267
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
268
+
269
+ // Use native pg client instead of requiring psql to be installed
270
+ const client = new Client(adminConn.clientConfig);
271
+
272
+ try {
273
+ await client.connect();
274
+
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
+ const dbRes = await client.query("select current_database() as db");
281
+ const database = dbRes.rows?.[0]?.db;
282
+ if (typeof database !== "string" || !database) {
283
+ throw new Error("Failed to resolve current database name");
284
+ }
285
+
286
+ if (opts.verify) {
287
+ const v = await verifyInitSetup({
288
+ client,
289
+ database,
290
+ monitoringUser: opts.monitoringUser,
291
+ includeOptionalPermissions,
292
+ });
293
+ if (v.ok) {
294
+ console.log("✓ init verify: OK");
295
+ if (v.missingOptional.length > 0) {
296
+ console.log("⚠ Optional items missing:");
297
+ for (const m of v.missingOptional) console.log(`- ${m}`);
298
+ }
299
+ return;
300
+ }
301
+ console.error("✗ init verify failed: missing required items");
302
+ for (const m of v.missingRequired) console.error(`- ${m}`);
303
+ if (v.missingOptional.length > 0) {
304
+ console.error("Optional items missing:");
305
+ for (const m of v.missingOptional) console.error(`- ${m}`);
306
+ }
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+
311
+ let monPassword: string;
312
+ try {
313
+ const resolved = await resolveMonitoringPassword({
314
+ passwordFlag: opts.password,
315
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
316
+ monitoringUser: opts.monitoringUser,
317
+ });
318
+ monPassword = resolved.password;
319
+ if (resolved.generated) {
320
+ const canPrint = process.stdout.isTTY || !!opts.printPassword;
321
+ if (canPrint) {
322
+ console.log("");
323
+ console.log(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
324
+ console.log(`PGAI_MON_PASSWORD=${monPassword}`);
325
+ console.log("");
326
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
327
+ } else {
328
+ console.error(
329
+ [
330
+ `✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
331
+ "",
332
+ "Provide it explicitly:",
333
+ " --password <password> or PGAI_MON_PASSWORD=...",
334
+ "",
335
+ "Or (NOT recommended) print the generated password:",
336
+ " --print-password",
337
+ ].join("\n")
338
+ );
339
+ process.exitCode = 1;
340
+ return;
341
+ }
342
+ }
343
+ } catch (e) {
344
+ const msg = e instanceof Error ? e.message : String(e);
345
+ console.error(`✗ ${msg}`);
346
+ process.exitCode = 1;
347
+ return;
348
+ }
349
+
350
+ const plan = await buildInitPlan({
351
+ database,
352
+ monitoringUser: opts.monitoringUser,
353
+ monitoringPassword: monPassword,
354
+ includeOptionalPermissions,
355
+ roleExists,
356
+ });
357
+
358
+ const effectivePlan = opts.resetPassword
359
+ ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
360
+ : plan;
361
+
362
+ if (shouldPrintSql) {
363
+ console.log("\n--- SQL plan ---");
364
+ for (const step of effectivePlan.steps) {
365
+ console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
366
+ console.log(redactPasswords(step.sql));
367
+ }
368
+ console.log("\n--- end SQL plan ---\n");
369
+ if (shouldRedactSecrets) {
370
+ console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
371
+ }
372
+ return;
373
+ }
374
+
375
+ const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
376
+
377
+ console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
378
+ if (skippedOptional.length > 0) {
379
+ console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
380
+ for (const s of skippedOptional) console.log(`- ${s}`);
381
+ }
382
+ // Keep output compact but still useful
383
+ if (process.stdout.isTTY) {
384
+ console.log(`Applied ${applied.length} steps`);
385
+ }
386
+ } catch (error) {
387
+ const errAny = error as any;
388
+ let message = "";
389
+ if (error instanceof Error && error.message) {
390
+ message = error.message;
391
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
392
+ message = errAny.message;
393
+ } else {
394
+ message = String(error);
395
+ }
396
+ if (!message || message === "[object Object]") {
397
+ message = "Unknown error";
398
+ }
399
+ console.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
+ }
407
+ if (errAny && typeof errAny === "object") {
408
+ if (typeof errAny.code === "string" && errAny.code) {
409
+ console.error(`Error code: ${errAny.code}`);
410
+ }
411
+ if (typeof errAny.detail === "string" && errAny.detail) {
412
+ console.error(`Detail: ${errAny.detail}`);
413
+ }
414
+ if (typeof errAny.hint === "string" && errAny.hint) {
415
+ console.error(`Hint: ${errAny.hint}`);
416
+ }
417
+ }
418
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
419
+ if (errAny.code === "42501") {
420
+ console.error("");
421
+ console.error("Permission error: your admin connection is not allowed to complete the setup.");
422
+ if (failedStep === "01.role") {
423
+ console.error("What failed: create/update the monitoring role (needs CREATEROLE or superuser).");
424
+ } else if (failedStep === "02.permissions") {
425
+ console.error("What failed: grant required permissions / create view / set role search_path.");
426
+ }
427
+ console.error("How to fix:");
428
+ console.error("- Connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
429
+ console.error("- On managed Postgres, use the provider's admin/master user.");
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).");
433
+ }
434
+ if (errAny.code === "ECONNREFUSED") {
435
+ console.error("Hint: check host/port and ensure Postgres is reachable from this machine.");
436
+ }
437
+ if (errAny.code === "ENOTFOUND") {
438
+ console.error("Hint: DNS resolution failed; double-check the host name.");
439
+ }
440
+ if (errAny.code === "ETIMEDOUT") {
441
+ console.error("Hint: connection timed out; check network/firewall rules.");
442
+ }
443
+ }
444
+ process.exitCode = 1;
445
+ } finally {
446
+ try {
447
+ await client.end();
448
+ } catch {
449
+ // ignore
450
+ }
451
+ }
452
+ });
453
+
119
454
  /**
120
455
  * Stub function for not implemented commands
121
456
  */