postgresai 0.14.0-dev.13 → 0.14.0-dev.15

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