postgresai 0.12.0-beta.7 → 0.14.0-dev.10

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
@@ -34,6 +34,53 @@ 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 and printed once
63
+
64
+ Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
65
+
66
+ ```bash
67
+ npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissions
68
+ ```
69
+
70
+ ### Print SQL / dry run
71
+
72
+ To see what SQL would be executed (passwords redacted by default):
73
+
74
+ ```bash
75
+ npx postgresai init postgresql://admin@host:5432/dbname --print-sql
76
+ ```
77
+
78
+ To print SQL and exit without applying anything:
79
+
80
+ ```bash
81
+ npx postgresai init postgresql://admin@host:5432/dbname --dry-run
82
+ ```
83
+
37
84
  ## Quick start
38
85
 
39
86
  ### Authentication
@@ -15,6 +15,7 @@ import { URL } from "url";
15
15
  import { startMcpServer } from "../lib/mcp-server";
16
16
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
17
17
  import { resolveBaseUrls } from "../lib/util";
18
+ import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword } from "../lib/init";
18
19
 
19
20
  const execPromise = promisify(exec);
20
21
  const execFilePromise = promisify(execFile);
@@ -116,6 +117,182 @@ program
116
117
  "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
117
118
  );
118
119
 
120
+ program
121
+ .command("init [conn]")
122
+ .description("Create a monitoring user and grant all required permissions (idempotent)")
123
+ .option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
124
+ .option("-h, --host <host>", "PostgreSQL host (psql-like)")
125
+ .option("-p, --port <port>", "PostgreSQL port (psql-like)")
126
+ .option("-U, --username <username>", "PostgreSQL user (psql-like)")
127
+ .option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
128
+ .option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
129
+ .option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
130
+ .option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
131
+ .option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
132
+ .option("--print-sql", "Print SQL steps before applying (passwords redacted by default)", false)
133
+ .option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
134
+ .option("--dry-run", "Print SQL steps and exit without applying changes", false)
135
+ .action(async (conn: string | undefined, opts: {
136
+ dbUrl?: string;
137
+ host?: string;
138
+ port?: string;
139
+ username?: string;
140
+ dbname?: string;
141
+ adminPassword?: string;
142
+ monitoringUser: string;
143
+ password?: string;
144
+ skipOptionalPermissions?: boolean;
145
+ printSql?: boolean;
146
+ showSecrets?: boolean;
147
+ dryRun?: boolean;
148
+ }) => {
149
+ let adminConn;
150
+ try {
151
+ adminConn = resolveAdminConnection({
152
+ conn,
153
+ dbUrlFlag: opts.dbUrl,
154
+ host: opts.host,
155
+ port: opts.port,
156
+ username: opts.username,
157
+ dbname: opts.dbname,
158
+ adminPassword: opts.adminPassword,
159
+ envPassword: process.env.PGPASSWORD,
160
+ });
161
+ } catch (e) {
162
+ const msg = e instanceof Error ? e.message : String(e);
163
+ console.error(`✗ ${msg}`);
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+
168
+ const includeOptionalPermissions = !opts.skipOptionalPermissions;
169
+
170
+ console.log(`Connecting to: ${adminConn.display}`);
171
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
172
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
173
+
174
+ const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
175
+
176
+ // Use native pg client instead of requiring psql to be installed
177
+ const { Client } = require("pg");
178
+ const client = new Client(adminConn.clientConfig);
179
+
180
+ try {
181
+ await client.connect();
182
+
183
+ const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
184
+ opts.monitoringUser,
185
+ ]);
186
+ const roleExists = roleRes.rowCount > 0;
187
+
188
+ const dbRes = await client.query("select current_database() as db");
189
+ const database = dbRes.rows?.[0]?.db;
190
+ if (typeof database !== "string" || !database) {
191
+ throw new Error("Failed to resolve current database name");
192
+ }
193
+
194
+ let monPassword: string;
195
+ try {
196
+ const resolved = await resolveMonitoringPassword({
197
+ passwordFlag: opts.password,
198
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
199
+ monitoringUser: opts.monitoringUser,
200
+ });
201
+ monPassword = resolved.password;
202
+ if (resolved.generated) {
203
+ console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
204
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
205
+ }
206
+ } catch (e) {
207
+ const msg = e instanceof Error ? e.message : String(e);
208
+ console.error(`✗ ${msg}`);
209
+ process.exitCode = 1;
210
+ return;
211
+ }
212
+
213
+ const plan = await buildInitPlan({
214
+ database,
215
+ monitoringUser: opts.monitoringUser,
216
+ monitoringPassword: monPassword,
217
+ includeOptionalPermissions,
218
+ roleExists,
219
+ });
220
+
221
+ if (shouldPrintSql) {
222
+ const redact = !opts.showSecrets;
223
+ const redactPasswords = (sql: string): string => {
224
+ if (!redact) return sql;
225
+ // Replace PASSWORD '<literal>' (handles doubled quotes inside).
226
+ return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
227
+ };
228
+
229
+ console.log("\n--- SQL plan ---");
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 (redact) {
236
+ console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
237
+ }
238
+ }
239
+
240
+ if (opts.dryRun) {
241
+ console.log("✓ dry-run completed (no changes were applied)");
242
+ return;
243
+ }
244
+
245
+ const { applied, skippedOptional } = await applyInitPlan({ client, plan });
246
+
247
+ console.log("✓ init completed");
248
+ if (skippedOptional.length > 0) {
249
+ console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
250
+ for (const s of skippedOptional) console.log(`- ${s}`);
251
+ }
252
+ // Keep output compact but still useful
253
+ if (process.stdout.isTTY) {
254
+ console.log(`Applied ${applied.length} steps`);
255
+ }
256
+ } catch (error) {
257
+ const errAny = error as any;
258
+ let message = "";
259
+ if (error instanceof Error && error.message) {
260
+ message = error.message;
261
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
262
+ message = errAny.message;
263
+ } else {
264
+ message = String(error);
265
+ }
266
+ if (!message || message === "[object Object]") {
267
+ message = "Unknown error";
268
+ }
269
+ console.error(`✗ init failed: ${message}`);
270
+ if (errAny && typeof errAny === "object") {
271
+ if (typeof errAny.code === "string" && errAny.code) {
272
+ console.error(`Error code: ${errAny.code}`);
273
+ }
274
+ if (typeof errAny.detail === "string" && errAny.detail) {
275
+ console.error(`Detail: ${errAny.detail}`);
276
+ }
277
+ if (typeof errAny.hint === "string" && errAny.hint) {
278
+ console.error(`Hint: ${errAny.hint}`);
279
+ }
280
+ }
281
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
282
+ if (errAny.code === "42501") {
283
+ console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
284
+ }
285
+ }
286
+ process.exitCode = 1;
287
+ } finally {
288
+ try {
289
+ await client.end();
290
+ } catch {
291
+ // ignore
292
+ }
293
+ }
294
+ });
295
+
119
296
  /**
120
297
  * Stub function for not implemented commands
121
298
  */
@@ -49,6 +49,7 @@ const url_1 = require("url");
49
49
  const mcp_server_1 = require("../lib/mcp-server");
50
50
  const issues_1 = require("../lib/issues");
51
51
  const util_2 = require("../lib/util");
52
+ const init_1 = require("../lib/init");
52
53
  const execPromise = (0, util_1.promisify)(child_process_1.exec);
53
54
  const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
54
55
  /**
@@ -98,6 +99,163 @@ program
98
99
  .option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
99
100
  .option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)")
100
101
  .option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)");
102
+ program
103
+ .command("init [conn]")
104
+ .description("Create a monitoring user and grant all required permissions (idempotent)")
105
+ .option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
106
+ .option("-h, --host <host>", "PostgreSQL host (psql-like)")
107
+ .option("-p, --port <port>", "PostgreSQL port (psql-like)")
108
+ .option("-U, --username <username>", "PostgreSQL user (psql-like)")
109
+ .option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
110
+ .option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
111
+ .option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
112
+ .option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
113
+ .option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
114
+ .option("--print-sql", "Print SQL steps before applying (passwords redacted by default)", false)
115
+ .option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
116
+ .option("--dry-run", "Print SQL steps and exit without applying changes", false)
117
+ .action(async (conn, opts) => {
118
+ let adminConn;
119
+ try {
120
+ adminConn = (0, init_1.resolveAdminConnection)({
121
+ conn,
122
+ dbUrlFlag: opts.dbUrl,
123
+ host: opts.host,
124
+ port: opts.port,
125
+ username: opts.username,
126
+ dbname: opts.dbname,
127
+ adminPassword: opts.adminPassword,
128
+ envPassword: process.env.PGPASSWORD,
129
+ });
130
+ }
131
+ catch (e) {
132
+ const msg = e instanceof Error ? e.message : String(e);
133
+ console.error(`✗ ${msg}`);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ const includeOptionalPermissions = !opts.skipOptionalPermissions;
138
+ console.log(`Connecting to: ${adminConn.display}`);
139
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
140
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
141
+ const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
142
+ // Use native pg client instead of requiring psql to be installed
143
+ const { Client } = require("pg");
144
+ const client = new Client(adminConn.clientConfig);
145
+ try {
146
+ await client.connect();
147
+ const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
148
+ opts.monitoringUser,
149
+ ]);
150
+ const roleExists = roleRes.rowCount > 0;
151
+ const dbRes = await client.query("select current_database() as db");
152
+ const database = dbRes.rows?.[0]?.db;
153
+ if (typeof database !== "string" || !database) {
154
+ throw new Error("Failed to resolve current database name");
155
+ }
156
+ let monPassword;
157
+ try {
158
+ const resolved = await (0, init_1.resolveMonitoringPassword)({
159
+ passwordFlag: opts.password,
160
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
161
+ monitoringUser: opts.monitoringUser,
162
+ });
163
+ monPassword = resolved.password;
164
+ if (resolved.generated) {
165
+ console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
166
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
167
+ }
168
+ }
169
+ catch (e) {
170
+ const msg = e instanceof Error ? e.message : String(e);
171
+ console.error(`✗ ${msg}`);
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+ const plan = await (0, init_1.buildInitPlan)({
176
+ database,
177
+ monitoringUser: opts.monitoringUser,
178
+ monitoringPassword: monPassword,
179
+ includeOptionalPermissions,
180
+ roleExists,
181
+ });
182
+ if (shouldPrintSql) {
183
+ const redact = !opts.showSecrets;
184
+ const redactPasswords = (sql) => {
185
+ if (!redact)
186
+ return sql;
187
+ // Replace PASSWORD '<literal>' (handles doubled quotes inside).
188
+ return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
189
+ };
190
+ console.log("\n--- SQL plan ---");
191
+ for (const step of plan.steps) {
192
+ console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
193
+ console.log(redactPasswords(step.sql));
194
+ }
195
+ console.log("\n--- end SQL plan ---\n");
196
+ if (redact) {
197
+ console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
198
+ }
199
+ }
200
+ if (opts.dryRun) {
201
+ console.log("✓ dry-run completed (no changes were applied)");
202
+ return;
203
+ }
204
+ const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan });
205
+ console.log("✓ init completed");
206
+ if (skippedOptional.length > 0) {
207
+ console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
208
+ for (const s of skippedOptional)
209
+ console.log(`- ${s}`);
210
+ }
211
+ // Keep output compact but still useful
212
+ if (process.stdout.isTTY) {
213
+ console.log(`Applied ${applied.length} steps`);
214
+ }
215
+ }
216
+ catch (error) {
217
+ const errAny = error;
218
+ let message = "";
219
+ if (error instanceof Error && error.message) {
220
+ message = error.message;
221
+ }
222
+ else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
223
+ message = errAny.message;
224
+ }
225
+ else {
226
+ message = String(error);
227
+ }
228
+ if (!message || message === "[object Object]") {
229
+ message = "Unknown error";
230
+ }
231
+ console.error(`✗ init failed: ${message}`);
232
+ if (errAny && typeof errAny === "object") {
233
+ if (typeof errAny.code === "string" && errAny.code) {
234
+ console.error(`Error code: ${errAny.code}`);
235
+ }
236
+ if (typeof errAny.detail === "string" && errAny.detail) {
237
+ console.error(`Detail: ${errAny.detail}`);
238
+ }
239
+ if (typeof errAny.hint === "string" && errAny.hint) {
240
+ console.error(`Hint: ${errAny.hint}`);
241
+ }
242
+ }
243
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
244
+ if (errAny.code === "42501") {
245
+ console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
246
+ }
247
+ }
248
+ process.exitCode = 1;
249
+ }
250
+ finally {
251
+ try {
252
+ await client.end();
253
+ }
254
+ catch {
255
+ // ignore
256
+ }
257
+ }
258
+ });
101
259
  /**
102
260
  * Stub function for not implemented commands
103
261
  */