postgresai 0.14.0-beta.3 → 0.14.0-dev.11

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,13 +10,11 @@ Command-line interface for PostgresAI monitoring and database management.
10
10
  npm install -g postgresai
11
11
  ```
12
12
 
13
- Or install the latest beta release explicitly:
13
+ Or install the latest alpha release explicitly:
14
14
  ```bash
15
- npm install -g postgresai@beta
15
+ npm install -g postgresai@alpha
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
-
20
18
  ### From Homebrew (macOS)
21
19
 
22
20
  ```bash
@@ -29,24 +27,11 @@ brew install postgresai
29
27
 
30
28
  ## Usage
31
29
 
32
- The `postgresai` package provides two command aliases (prefer `postgresai`):
30
+ The CLI provides three command aliases:
33
31
  ```bash
34
32
  postgres-ai --help
35
33
  postgresai --help
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
34
+ pgai --help # short alias
50
35
  ```
51
36
 
52
37
  ## init (create monitoring user in Postgres)
@@ -93,18 +78,10 @@ To see what SQL would be executed (passwords redacted by default):
93
78
  npx postgresai init postgresql://admin@host:5432/dbname --print-sql
94
79
  ```
95
80
 
96
- ### Verify and password reset
97
-
98
- Verify that everything is configured as expected (no changes):
99
-
100
- ```bash
101
- npx postgresai init postgresql://admin@host:5432/dbname --verify
102
- ```
103
-
104
- Reset monitoring user password only (no other changes):
81
+ To print SQL and exit without applying anything:
105
82
 
106
83
  ```bash
107
- npx postgresai init postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
84
+ npx postgresai init postgresql://admin@host:5432/dbname --dry-run
108
85
  ```
109
86
 
110
87
  ## Quick start
@@ -113,7 +90,7 @@ npx postgresai init postgresql://admin@host:5432/dbname --reset-password --passw
113
90
 
114
91
  Authenticate via browser to obtain API key:
115
92
  ```bash
116
- postgresai auth
93
+ pgai auth
117
94
  ```
118
95
 
119
96
  This will:
@@ -195,7 +172,7 @@ postgres-ai mon shell <service> # Open shell to monitoring servic
195
172
  ### MCP server (`mcp` group)
196
173
 
197
174
  ```bash
198
- postgresai mcp start # Start MCP stdio server exposing tools
175
+ pgai mcp start # Start MCP stdio server exposing tools
199
176
  ```
200
177
 
201
178
  Cursor configuration example (Settings → MCP):
@@ -204,7 +181,7 @@ Cursor configuration example (Settings → MCP):
204
181
  {
205
182
  "mcpServers": {
206
183
  "PostgresAI": {
207
- "command": "postgresai",
184
+ "command": "pgai",
208
185
  "args": ["mcp", "start"],
209
186
  "env": {
210
187
  "PGAI_API_BASE_URL": "https://postgres.ai/api/general/"
@@ -215,16 +192,16 @@ Cursor configuration example (Settings → MCP):
215
192
  ```
216
193
 
217
194
  Tools exposed:
218
- - list_issues: returns the same JSON as `postgresai issues list`.
195
+ - list_issues: returns the same JSON as `pgai issues list`.
219
196
  - view_issue: view a single issue with its comments (args: { issue_id, debug? })
220
197
  - post_issue_comment: post a comment (args: { issue_id, content, parent_comment_id?, debug? })
221
198
 
222
199
  ### Issues management (`issues` group)
223
200
 
224
201
  ```bash
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
202
+ pgai issues list # List issues (shows: id, title, status, created_at)
203
+ pgai issues view <issueId> # View issue details and comments
204
+ pgai issues post_comment <issueId> <content> # Post a comment to an issue
228
205
  # Options:
229
206
  # --parent <uuid> Parent comment ID (for replies)
230
207
  # --debug Enable debug output
@@ -238,13 +215,13 @@ By default, issues commands print human-friendly YAML when writing to a terminal
238
215
  - Use `--json` to force JSON output:
239
216
 
240
217
  ```bash
241
- postgresai issues list --json | jq '.[] | {id, title}'
218
+ pgai issues list --json | jq '.[] | {id, title}'
242
219
  ```
243
220
 
244
221
  - Rely on auto-detection: when stdout is not a TTY (e.g., piped or redirected), output is JSON automatically:
245
222
 
246
223
  ```bash
247
- postgresai issues view <issueId> > issue.json
224
+ pgai issues view <issueId> > issue.json
248
225
  ```
249
226
 
250
227
  #### Grafana management
@@ -308,7 +285,7 @@ Linux/macOS (bash/zsh):
308
285
  ```bash
309
286
  export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/
310
287
  export PGAI_UI_BASE_URL=https://console-dev.postgres.ai
311
- postgresai auth --debug
288
+ pgai auth --debug
312
289
  ```
313
290
 
314
291
  Windows PowerShell:
@@ -316,13 +293,13 @@ Windows PowerShell:
316
293
  ```powershell
317
294
  $env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/"
318
295
  $env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai"
319
- postgresai auth --debug
296
+ pgai auth --debug
320
297
  ```
321
298
 
322
299
  Via CLI options (overrides env):
323
300
 
324
301
  ```bash
325
- postgresai auth --debug \
302
+ pgai auth --debug \
326
303
  --api-base-url https://v2.postgres.ai/api/general/ \
327
304
  --ui-base-url https://console-dev.postgres.ai
328
305
  ```
@@ -12,11 +12,10 @@ 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";
16
15
  import { startMcpServer } from "../lib/mcp-server";
17
16
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
18
17
  import { resolveBaseUrls } from "../lib/util";
19
- import { applyInitPlan, buildInitPlan, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
18
+ import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword } from "../lib/init";
20
19
 
21
20
  const execPromise = promisify(exec);
22
21
  const execFilePromise = promisify(execFile);
@@ -127,13 +126,13 @@ program
127
126
  .option("-U, --username <username>", "PostgreSQL user (psql-like)")
128
127
  .option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
129
128
  .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)
129
+ .option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
131
130
  .option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
132
131
  .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)
132
+ .option("--print-sql", "Print SQL plan before applying (does not exit; use --dry-run to exit)", false)
133
+ .option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
136
134
  .option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
135
+ .option("--dry-run", "Print SQL steps and exit without applying changes", false)
137
136
  .addHelpText(
138
137
  "after",
139
138
  [
@@ -151,22 +150,8 @@ program
151
150
  " If auto-generated, it is printed only on TTY by default.",
152
151
  " To print it in non-interactive mode: --print-password",
153
152
  "",
154
- "Environment variables (libpq standard):",
155
- " PGHOST, PGPORT, PGUSER, PGDATABASE — connection defaults",
156
- " PGPASSWORD — admin password",
157
- " PGAI_MON_PASSWORD — monitoring password",
158
- "",
159
153
  "Inspect SQL without applying changes:",
160
- " postgresai init <conn> --print-sql",
161
- "",
162
- "Verify setup (no changes):",
163
- " postgresai init <conn> --verify",
164
- "",
165
- "Reset monitoring password only:",
166
- " postgresai init <conn> --reset-password --password '...'",
167
- "",
168
- "Offline SQL plan (no DB connection):",
169
- " postgresai init --print-sql",
154
+ " postgresai init <conn> --dry-run",
170
155
  ].join("\n")
171
156
  )
172
157
  .action(async (conn: string | undefined, opts: {
@@ -179,58 +164,11 @@ program
179
164
  monitoringUser: string;
180
165
  password?: string;
181
166
  skipOptionalPermissions?: boolean;
182
- verify?: boolean;
183
- resetPassword?: boolean;
184
167
  printSql?: boolean;
168
+ showSecrets?: boolean;
185
169
  printPassword?: boolean;
186
- }, cmd: Command) => {
187
- if (opts.verify && opts.resetPassword) {
188
- console.error("✗ Provide only one of --verify or --reset-password");
189
- process.exitCode = 1;
190
- return;
191
- }
192
- if (opts.verify && opts.printSql) {
193
- console.error("✗ --verify cannot be combined with --print-sql");
194
- process.exitCode = 1;
195
- return;
196
- }
197
-
198
- const shouldPrintSql = !!opts.printSql;
199
- const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
200
-
201
- // Offline mode: allow printing SQL without providing/using an admin connection.
202
- // Useful for audits/reviews; caller can provide -d/PGDATABASE.
203
- if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
204
- if (shouldPrintSql) {
205
- const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
206
- const includeOptionalPermissions = !opts.skipOptionalPermissions;
207
-
208
- // Use explicit password/env if provided; otherwise use a placeholder.
209
- // Printed SQL always redacts secrets.
210
- const monPassword =
211
- (opts.password ?? process.env.PGAI_MON_PASSWORD ?? "<redacted>").toString();
212
-
213
- const plan = await buildInitPlan({
214
- database,
215
- monitoringUser: opts.monitoringUser,
216
- monitoringPassword: monPassword,
217
- includeOptionalPermissions,
218
- });
219
-
220
- console.log("\n--- SQL plan (offline; not connected) ---");
221
- console.log(`-- database: ${database}`);
222
- console.log(`-- monitoring user: ${opts.monitoringUser}`);
223
- console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
224
- for (const step of plan.steps) {
225
- console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
226
- console.log(redactPasswords(step.sql));
227
- }
228
- console.log("\n--- end SQL plan ---\n");
229
- console.log("Note: passwords are redacted in the printed SQL output.");
230
- return;
231
- }
232
- }
233
-
170
+ dryRun?: boolean;
171
+ }) => {
234
172
  let adminConn;
235
173
  try {
236
174
  adminConn = resolveAdminConnection({
@@ -246,12 +184,7 @@ program
246
184
  });
247
185
  } catch (e) {
248
186
  const msg = e instanceof Error ? e.message : String(e);
249
- console.error(`Error: init: ${msg}`);
250
- // When connection details are missing, show full init help (options + examples).
251
- if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
252
- console.error("");
253
- cmd.outputHelp({ error: true });
254
- }
187
+ console.error(`✗ ${msg}`);
255
188
  process.exitCode = 1;
256
189
  return;
257
190
  }
@@ -262,43 +195,26 @@ program
262
195
  console.log(`Monitoring user: ${opts.monitoringUser}`);
263
196
  console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
264
197
 
198
+ const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
199
+
265
200
  // Use native pg client instead of requiring psql to be installed
266
- let client: Client | undefined;
201
+ const { Client } = require("pg");
202
+ const client = new Client(adminConn.clientConfig);
203
+
267
204
  try {
268
- client = new Client(adminConn.clientConfig);
269
205
  await client.connect();
270
206
 
207
+ const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
208
+ opts.monitoringUser,
209
+ ]);
210
+ const roleExists = roleRes.rowCount > 0;
211
+
271
212
  const dbRes = await client.query("select current_database() as db");
272
213
  const database = dbRes.rows?.[0]?.db;
273
214
  if (typeof database !== "string" || !database) {
274
215
  throw new Error("Failed to resolve current database name");
275
216
  }
276
217
 
277
- if (opts.verify) {
278
- const v = await verifyInitSetup({
279
- client,
280
- database,
281
- monitoringUser: opts.monitoringUser,
282
- includeOptionalPermissions,
283
- });
284
- if (v.ok) {
285
- console.log("✓ init verify: OK");
286
- if (v.missingOptional.length > 0) {
287
- console.log("⚠ Optional items missing:");
288
- for (const m of v.missingOptional) console.log(`- ${m}`);
289
- }
290
- return;
291
- }
292
- console.error("✗ init verify failed: missing required items");
293
- for (const m of v.missingRequired) console.error(`- ${m}`);
294
- if (v.missingOptional.length > 0) {
295
- console.error("Optional items missing:");
296
- for (const m of v.missingOptional) console.error(`- ${m}`);
297
- }
298
- process.exitCode = 1;
299
- return;
300
- }
301
-
302
218
  let monPassword: string;
303
219
  try {
304
220
  const resolved = await resolveMonitoringPassword({
@@ -310,13 +226,7 @@ program
310
226
  if (resolved.generated) {
311
227
  const canPrint = process.stdout.isTTY || !!opts.printPassword;
312
228
  if (canPrint) {
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("");
229
+ console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
320
230
  console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
321
231
  } else {
322
232
  console.error(
@@ -346,26 +256,36 @@ program
346
256
  monitoringUser: opts.monitoringUser,
347
257
  monitoringPassword: monPassword,
348
258
  includeOptionalPermissions,
259
+ roleExists,
349
260
  });
350
261
 
351
- const effectivePlan = opts.resetPassword
352
- ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
353
- : plan;
354
-
355
262
  if (shouldPrintSql) {
263
+ const redact = !opts.showSecrets;
264
+ const redactPasswords = (sql: string): string => {
265
+ if (!redact) return sql;
266
+ // Replace PASSWORD '<literal>' (handles doubled quotes inside).
267
+ return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
268
+ };
269
+
356
270
  console.log("\n--- SQL plan ---");
357
- for (const step of effectivePlan.steps) {
271
+ for (const step of plan.steps) {
358
272
  console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
359
273
  console.log(redactPasswords(step.sql));
360
274
  }
361
275
  console.log("\n--- end SQL plan ---\n");
362
- console.log("Note: passwords are redacted in the printed SQL output.");
276
+ if (redact) {
277
+ console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
278
+ }
279
+ }
280
+
281
+ if (opts.dryRun) {
282
+ console.log("✓ dry-run completed (no changes were applied)");
363
283
  return;
364
284
  }
365
285
 
366
- const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
286
+ const { applied, skippedOptional } = await applyInitPlan({ client, plan });
367
287
 
368
- console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
288
+ console.log("✓ init completed");
369
289
  if (skippedOptional.length > 0) {
370
290
  console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
371
291
  for (const s of skippedOptional) console.log(`- ${s}`);
@@ -387,54 +307,38 @@ program
387
307
  if (!message || message === "[object Object]") {
388
308
  message = "Unknown error";
389
309
  }
390
- console.error(`Error: init: ${message}`);
391
- // If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
392
- const stepMatch =
393
- typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
394
- const failedStep = stepMatch?.[1];
395
- if (failedStep) {
396
- console.error(` Step: ${failedStep}`);
397
- }
310
+ console.error(`✗ init failed: ${message}`);
398
311
  if (errAny && typeof errAny === "object") {
399
312
  if (typeof errAny.code === "string" && errAny.code) {
400
- console.error(` Code: ${errAny.code}`);
313
+ console.error(`Error code: ${errAny.code}`);
401
314
  }
402
315
  if (typeof errAny.detail === "string" && errAny.detail) {
403
- console.error(` Detail: ${errAny.detail}`);
316
+ console.error(`Detail: ${errAny.detail}`);
404
317
  }
405
318
  if (typeof errAny.hint === "string" && errAny.hint) {
406
- console.error(` Hint: ${errAny.hint}`);
319
+ console.error(`Hint: ${errAny.hint}`);
407
320
  }
408
321
  }
409
322
  if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
410
323
  if (errAny.code === "42501") {
411
- if (failedStep === "01.role") {
412
- console.error(" Context: role creation/update requires CREATEROLE or superuser");
413
- } else if (failedStep === "02.permissions") {
414
- console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
415
- }
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");
324
+ console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
419
325
  }
420
326
  if (errAny.code === "ECONNREFUSED") {
421
- console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
327
+ console.error("Hint: check host/port and ensure Postgres is reachable from this machine.");
422
328
  }
423
329
  if (errAny.code === "ENOTFOUND") {
424
- console.error(" Hint: DNS resolution failed; double-check the host name");
330
+ console.error("Hint: DNS resolution failed; double-check the host name.");
425
331
  }
426
332
  if (errAny.code === "ETIMEDOUT") {
427
- console.error(" Hint: connection timed out; check network/firewall rules");
333
+ console.error("Hint: connection timed out; check network/firewall rules.");
428
334
  }
429
335
  }
430
336
  process.exitCode = 1;
431
337
  } finally {
432
- if (client) {
433
- try {
434
- await client.end();
435
- } catch {
436
- // ignore
437
- }
338
+ try {
339
+ await client.end();
340
+ } catch {
341
+ // ignore
438
342
  }
439
343
  }
440
344
  });