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 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
@@ -31,7 +33,70 @@ The CLI provides three command aliases:
31
33
  ```bash
32
34
  postgres-ai --help
33
35
  postgresai --help
34
- pgai --help # short alias
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
- pgai auth
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
- pgai mcp start # Start MCP stdio server exposing tools
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": "pgai",
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 `pgai issues list`.
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
- pgai issues list # List issues (shows: id, title, status, created_at)
153
- pgai issues view <issueId> # View issue details and comments
154
- pgai issues post_comment <issueId> <content> # Post a comment to an issue
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
- pgai issues list --json | jq '.[] | {id, title}'
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
- pgai issues view <issueId> > issue.json
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
- pgai auth --debug
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
- pgai auth --debug
311
+ postgresai auth --debug
247
312
  ```
248
313
 
249
314
  Via CLI options (overrides env):
250
315
 
251
316
  ```bash
252
- pgai auth --debug \
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
  ```
@@ -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
  */