postgresai 0.12.0-beta.6 → 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 +118 -4
- package/bin/postgres-ai.ts +768 -37
- package/dist/bin/postgres-ai.js +706 -26
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +75 -0
- package/dist/lib/init.d.ts.map +1 -0
- package/dist/lib/init.js +483 -0
- package/dist/lib/init.js.map +1 -0
- package/dist/lib/issues.d.ts +69 -1
- package/dist/lib/issues.d.ts.map +1 -1
- package/dist/lib/issues.js +232 -1
- package/dist/lib/issues.js.map +1 -1
- package/dist/lib/mcp-server.d.ts.map +1 -1
- package/dist/lib/mcp-server.js +69 -15
- package/dist/lib/mcp-server.js.map +1 -1
- package/dist/package.json +3 -2
- package/lib/init.ts +565 -0
- package/lib/issues.ts +325 -3
- package/lib/mcp-server.ts +75 -17
- package/package.json +3 -2
- package/sql/01.role.sql +15 -0
- package/sql/02.permissions.sql +33 -0
- package/sql/03.optional_rds.sql +6 -0
- package/sql/04.optional_self_managed.sql +8 -0
- package/test/init.integration.test.cjs +368 -0
- package/test/init.test.cjs +154 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -46,9 +46,11 @@ const util_1 = require("util");
|
|
|
46
46
|
const readline = __importStar(require("readline"));
|
|
47
47
|
const http = __importStar(require("https"));
|
|
48
48
|
const url_1 = require("url");
|
|
49
|
+
const pg_1 = require("pg");
|
|
49
50
|
const mcp_server_1 = require("../lib/mcp-server");
|
|
50
51
|
const issues_1 = require("../lib/issues");
|
|
51
52
|
const util_2 = require("../lib/util");
|
|
53
|
+
const init_1 = require("../lib/init");
|
|
52
54
|
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
53
55
|
const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
|
|
54
56
|
/**
|
|
@@ -71,6 +73,25 @@ function getConfig(opts) {
|
|
|
71
73
|
}
|
|
72
74
|
return { apiKey };
|
|
73
75
|
}
|
|
76
|
+
// Human-friendly output helper: YAML for TTY by default, JSON when --json or non-TTY
|
|
77
|
+
function printResult(result, json) {
|
|
78
|
+
if (typeof result === "string") {
|
|
79
|
+
process.stdout.write(result);
|
|
80
|
+
if (!/\n$/.test(result))
|
|
81
|
+
console.log();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (json || !process.stdout.isTTY) {
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
let text = yaml.dump(result);
|
|
89
|
+
if (Array.isArray(result)) {
|
|
90
|
+
text = text.replace(/\n- /g, "\n\n- ");
|
|
91
|
+
}
|
|
92
|
+
console.log(text);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
const program = new commander_1.Command();
|
|
75
96
|
program
|
|
76
97
|
.name("postgres-ai")
|
|
@@ -79,6 +100,311 @@ program
|
|
|
79
100
|
.option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
|
|
80
101
|
.option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)")
|
|
81
102
|
.option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)");
|
|
103
|
+
program
|
|
104
|
+
.command("init [conn]")
|
|
105
|
+
.description("Create a monitoring user and grant all required permissions (idempotent)")
|
|
106
|
+
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
|
|
107
|
+
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
|
|
108
|
+
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
|
|
109
|
+
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
110
|
+
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
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")
|
|
113
|
+
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
114
|
+
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
115
|
+
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
|
|
116
|
+
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
|
|
117
|
+
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
|
|
118
|
+
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
119
|
+
.option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false)
|
|
120
|
+
.addHelpText("after", [
|
|
121
|
+
"",
|
|
122
|
+
"Examples:",
|
|
123
|
+
" postgresai init postgresql://admin@host:5432/dbname",
|
|
124
|
+
" postgresai init \"dbname=dbname host=host user=admin\"",
|
|
125
|
+
" postgresai init -h host -p 5432 -U admin -d dbname",
|
|
126
|
+
"",
|
|
127
|
+
"Admin password:",
|
|
128
|
+
" --admin-password <password> or PGPASSWORD=... (libpq standard)",
|
|
129
|
+
"",
|
|
130
|
+
"Monitoring password:",
|
|
131
|
+
" --password <password> or PGAI_MON_PASSWORD=... (otherwise auto-generated)",
|
|
132
|
+
" If auto-generated, it is printed only on TTY by default.",
|
|
133
|
+
" To print it in non-interactive mode: --print-password",
|
|
134
|
+
"",
|
|
135
|
+
"Inspect SQL without applying changes:",
|
|
136
|
+
" postgresai init <conn> --print-sql",
|
|
137
|
+
"",
|
|
138
|
+
"Verify setup (no changes):",
|
|
139
|
+
" postgresai init <conn> --verify",
|
|
140
|
+
"",
|
|
141
|
+
"Reset monitoring password only:",
|
|
142
|
+
" postgresai init <conn> --reset-password --password '...'",
|
|
143
|
+
"",
|
|
144
|
+
"Offline SQL plan (no DB connection):",
|
|
145
|
+
" postgresai init --print-sql -d dbname --password '...' --show-secrets",
|
|
146
|
+
].join("\n"))
|
|
147
|
+
.action(async (conn, opts, cmd) => {
|
|
148
|
+
if (opts.verify && opts.resetPassword) {
|
|
149
|
+
console.error("✗ Provide only one of --verify or --reset-password");
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (opts.verify && opts.printSql) {
|
|
154
|
+
console.error("✗ --verify cannot be combined with --print-sql");
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const shouldPrintSql = !!opts.printSql;
|
|
159
|
+
const shouldRedactSecrets = !opts.showSecrets;
|
|
160
|
+
const redactPasswords = (sql) => {
|
|
161
|
+
if (!shouldRedactSecrets)
|
|
162
|
+
return sql;
|
|
163
|
+
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
164
|
+
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
165
|
+
};
|
|
166
|
+
// Offline mode: allow printing SQL without providing/using an admin connection.
|
|
167
|
+
// Useful for audits/reviews; caller can provide -d/PGDATABASE and an explicit monitoring password.
|
|
168
|
+
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
169
|
+
if (shouldPrintSql) {
|
|
170
|
+
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
171
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
172
|
+
// Use explicit password/env if provided; otherwise use a placeholder (will be redacted unless --show-secrets).
|
|
173
|
+
const monPassword = (opts.password ?? process.env.PGAI_MON_PASSWORD ?? "CHANGE_ME").toString();
|
|
174
|
+
const plan = await (0, init_1.buildInitPlan)({
|
|
175
|
+
database,
|
|
176
|
+
monitoringUser: opts.monitoringUser,
|
|
177
|
+
monitoringPassword: monPassword,
|
|
178
|
+
includeOptionalPermissions,
|
|
179
|
+
roleExists: undefined,
|
|
180
|
+
});
|
|
181
|
+
console.log("\n--- SQL plan (offline; not connected) ---");
|
|
182
|
+
console.log(`-- database: ${database}`);
|
|
183
|
+
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
184
|
+
console.log(`-- optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
185
|
+
for (const step of plan.steps) {
|
|
186
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
187
|
+
console.log(redactPasswords(step.sql));
|
|
188
|
+
}
|
|
189
|
+
console.log("\n--- end SQL plan ---\n");
|
|
190
|
+
if (shouldRedactSecrets) {
|
|
191
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let adminConn;
|
|
197
|
+
try {
|
|
198
|
+
adminConn = (0, init_1.resolveAdminConnection)({
|
|
199
|
+
conn,
|
|
200
|
+
dbUrlFlag: opts.dbUrl,
|
|
201
|
+
// Allow libpq standard env vars as implicit defaults (common UX).
|
|
202
|
+
host: opts.host ?? process.env.PGHOST,
|
|
203
|
+
port: opts.port ?? process.env.PGPORT,
|
|
204
|
+
username: opts.username ?? process.env.PGUSER,
|
|
205
|
+
dbname: opts.dbname ?? process.env.PGDATABASE,
|
|
206
|
+
adminPassword: opts.adminPassword,
|
|
207
|
+
envPassword: process.env.PGPASSWORD,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
+
console.error(`✗ ${msg}`);
|
|
213
|
+
// When connection details are missing, show full init help (options + examples).
|
|
214
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
215
|
+
console.error("");
|
|
216
|
+
cmd.outputHelp({ error: true });
|
|
217
|
+
}
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
222
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
223
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
224
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
225
|
+
// Use native pg client instead of requiring psql to be installed
|
|
226
|
+
const client = new pg_1.Client(adminConn.clientConfig);
|
|
227
|
+
try {
|
|
228
|
+
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
|
+
const dbRes = await client.query("select current_database() as db");
|
|
234
|
+
const database = dbRes.rows?.[0]?.db;
|
|
235
|
+
if (typeof database !== "string" || !database) {
|
|
236
|
+
throw new Error("Failed to resolve current database name");
|
|
237
|
+
}
|
|
238
|
+
if (opts.verify) {
|
|
239
|
+
const v = await (0, init_1.verifyInitSetup)({
|
|
240
|
+
client,
|
|
241
|
+
database,
|
|
242
|
+
monitoringUser: opts.monitoringUser,
|
|
243
|
+
includeOptionalPermissions,
|
|
244
|
+
});
|
|
245
|
+
if (v.ok) {
|
|
246
|
+
console.log("✓ init verify: OK");
|
|
247
|
+
if (v.missingOptional.length > 0) {
|
|
248
|
+
console.log("⚠ Optional items missing:");
|
|
249
|
+
for (const m of v.missingOptional)
|
|
250
|
+
console.log(`- ${m}`);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
console.error("✗ init verify failed: missing required items");
|
|
255
|
+
for (const m of v.missingRequired)
|
|
256
|
+
console.error(`- ${m}`);
|
|
257
|
+
if (v.missingOptional.length > 0) {
|
|
258
|
+
console.error("Optional items missing:");
|
|
259
|
+
for (const m of v.missingOptional)
|
|
260
|
+
console.error(`- ${m}`);
|
|
261
|
+
}
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
let monPassword;
|
|
266
|
+
try {
|
|
267
|
+
const resolved = await (0, init_1.resolveMonitoringPassword)({
|
|
268
|
+
passwordFlag: opts.password,
|
|
269
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
270
|
+
monitoringUser: opts.monitoringUser,
|
|
271
|
+
});
|
|
272
|
+
monPassword = resolved.password;
|
|
273
|
+
if (resolved.generated) {
|
|
274
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
275
|
+
if (canPrint) {
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
278
|
+
console.log(`PGAI_MON_PASSWORD=${monPassword}`);
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error([
|
|
284
|
+
`✗ Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
285
|
+
"",
|
|
286
|
+
"Provide it explicitly:",
|
|
287
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
288
|
+
"",
|
|
289
|
+
"Or (NOT recommended) print the generated password:",
|
|
290
|
+
" --print-password",
|
|
291
|
+
].join("\n"));
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
299
|
+
console.error(`✗ ${msg}`);
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const plan = await (0, init_1.buildInitPlan)({
|
|
304
|
+
database,
|
|
305
|
+
monitoringUser: opts.monitoringUser,
|
|
306
|
+
monitoringPassword: monPassword,
|
|
307
|
+
includeOptionalPermissions,
|
|
308
|
+
roleExists,
|
|
309
|
+
});
|
|
310
|
+
const effectivePlan = opts.resetPassword
|
|
311
|
+
? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") }
|
|
312
|
+
: plan;
|
|
313
|
+
if (shouldPrintSql) {
|
|
314
|
+
console.log("\n--- SQL plan ---");
|
|
315
|
+
for (const step of effectivePlan.steps) {
|
|
316
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
317
|
+
console.log(redactPasswords(step.sql));
|
|
318
|
+
}
|
|
319
|
+
console.log("\n--- end SQL plan ---\n");
|
|
320
|
+
if (shouldRedactSecrets) {
|
|
321
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan: effectivePlan });
|
|
326
|
+
console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
|
|
327
|
+
if (skippedOptional.length > 0) {
|
|
328
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
329
|
+
for (const s of skippedOptional)
|
|
330
|
+
console.log(`- ${s}`);
|
|
331
|
+
}
|
|
332
|
+
// Keep output compact but still useful
|
|
333
|
+
if (process.stdout.isTTY) {
|
|
334
|
+
console.log(`Applied ${applied.length} steps`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
const errAny = error;
|
|
339
|
+
let message = "";
|
|
340
|
+
if (error instanceof Error && error.message) {
|
|
341
|
+
message = error.message;
|
|
342
|
+
}
|
|
343
|
+
else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
344
|
+
message = errAny.message;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
message = String(error);
|
|
348
|
+
}
|
|
349
|
+
if (!message || message === "[object Object]") {
|
|
350
|
+
message = "Unknown error";
|
|
351
|
+
}
|
|
352
|
+
console.error(`✗ init failed: ${message}`);
|
|
353
|
+
// If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
|
|
354
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
355
|
+
const failedStep = stepMatch?.[1];
|
|
356
|
+
if (failedStep) {
|
|
357
|
+
console.error(`Step: ${failedStep}`);
|
|
358
|
+
}
|
|
359
|
+
if (errAny && typeof errAny === "object") {
|
|
360
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
361
|
+
console.error(`Error code: ${errAny.code}`);
|
|
362
|
+
}
|
|
363
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
364
|
+
console.error(`Detail: ${errAny.detail}`);
|
|
365
|
+
}
|
|
366
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
367
|
+
console.error(`Hint: ${errAny.hint}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
371
|
+
if (errAny.code === "42501") {
|
|
372
|
+
console.error("");
|
|
373
|
+
console.error("Permission error: your admin connection is not allowed to complete the setup.");
|
|
374
|
+
if (failedStep === "01.role") {
|
|
375
|
+
console.error("What failed: create/update the monitoring role (needs CREATEROLE or superuser).");
|
|
376
|
+
}
|
|
377
|
+
else if (failedStep === "02.permissions") {
|
|
378
|
+
console.error("What failed: grant required permissions / create view / set role search_path.");
|
|
379
|
+
}
|
|
380
|
+
console.error("How to fix:");
|
|
381
|
+
console.error("- Connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
382
|
+
console.error("- On managed Postgres, use the provider's admin/master user.");
|
|
383
|
+
console.error("Tip: run with --print-sql to review the exact SQL plan.");
|
|
384
|
+
console.error("");
|
|
385
|
+
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
386
|
+
}
|
|
387
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
388
|
+
console.error("Hint: check host/port and ensure Postgres is reachable from this machine.");
|
|
389
|
+
}
|
|
390
|
+
if (errAny.code === "ENOTFOUND") {
|
|
391
|
+
console.error("Hint: DNS resolution failed; double-check the host name.");
|
|
392
|
+
}
|
|
393
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
394
|
+
console.error("Hint: connection timed out; check network/firewall rules.");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
try {
|
|
401
|
+
await client.end();
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// ignore
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
82
408
|
/**
|
|
83
409
|
* Stub function for not implemented commands
|
|
84
410
|
*/
|
|
@@ -206,23 +532,284 @@ const mon = program.command("mon").description("monitoring services management")
|
|
|
206
532
|
mon
|
|
207
533
|
.command("quickstart")
|
|
208
534
|
.description("complete setup (generate config, start monitoring services)")
|
|
209
|
-
.option("--demo", "demo mode", false)
|
|
210
|
-
.
|
|
535
|
+
.option("--demo", "demo mode with sample database", false)
|
|
536
|
+
.option("--api-key <key>", "Postgres AI API key for automated report uploads")
|
|
537
|
+
.option("--db-url <url>", "PostgreSQL connection URL to monitor")
|
|
538
|
+
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
539
|
+
.action(async (opts) => {
|
|
540
|
+
console.log("\n=================================");
|
|
541
|
+
console.log(" PostgresAI Monitoring Quickstart");
|
|
542
|
+
console.log("=================================\n");
|
|
543
|
+
console.log("This will install, configure, and start the monitoring system\n");
|
|
544
|
+
// Validate conflicting options
|
|
545
|
+
if (opts.demo && opts.dbUrl) {
|
|
546
|
+
console.log("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
|
|
547
|
+
console.log("⚠ The --db-url will be ignored in demo mode.\n");
|
|
548
|
+
opts.dbUrl = undefined;
|
|
549
|
+
}
|
|
550
|
+
if (opts.demo && opts.apiKey) {
|
|
551
|
+
console.error("✗ Cannot use --api-key with --demo mode");
|
|
552
|
+
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
553
|
+
console.error("\nUse demo mode without API key: postgres-ai mon quickstart --demo");
|
|
554
|
+
console.error("Or use production mode with API key: postgres-ai mon quickstart --api-key=your_key");
|
|
555
|
+
process.exitCode = 1;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
211
558
|
// Check if containers are already running
|
|
212
559
|
const { running, containers } = checkRunningContainers();
|
|
213
560
|
if (running) {
|
|
214
|
-
console.log(
|
|
215
|
-
console.log("Use 'postgres-ai mon restart' to restart them");
|
|
561
|
+
console.log(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
|
|
562
|
+
console.log("Use 'postgres-ai mon restart' to restart them\n");
|
|
216
563
|
return;
|
|
217
564
|
}
|
|
565
|
+
// Step 1: API key configuration (only in production mode)
|
|
566
|
+
if (!opts.demo) {
|
|
567
|
+
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
568
|
+
console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
|
|
569
|
+
if (opts.apiKey) {
|
|
570
|
+
console.log("Using API key provided via --api-key parameter");
|
|
571
|
+
config.writeConfig({ apiKey: opts.apiKey });
|
|
572
|
+
console.log("✓ API key saved\n");
|
|
573
|
+
}
|
|
574
|
+
else if (opts.yes) {
|
|
575
|
+
// Auto-yes mode without API key - skip API key setup
|
|
576
|
+
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
577
|
+
console.log("⚠ Reports will be generated locally only");
|
|
578
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
const rl = readline.createInterface({
|
|
582
|
+
input: process.stdin,
|
|
583
|
+
output: process.stdout
|
|
584
|
+
});
|
|
585
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
586
|
+
try {
|
|
587
|
+
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
588
|
+
const proceedWithApiKey = !answer || answer.toLowerCase() === "y";
|
|
589
|
+
if (proceedWithApiKey) {
|
|
590
|
+
while (true) {
|
|
591
|
+
const inputApiKey = await question("Enter your Postgres AI API key: ");
|
|
592
|
+
const trimmedKey = inputApiKey.trim();
|
|
593
|
+
if (trimmedKey) {
|
|
594
|
+
config.writeConfig({ apiKey: trimmedKey });
|
|
595
|
+
console.log("✓ API key saved\n");
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
console.log("⚠ API key cannot be empty");
|
|
599
|
+
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
600
|
+
if (retry.toLowerCase() === "n") {
|
|
601
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
602
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
609
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
rl.close();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
console.log("Step 1: Demo mode - API key configuration skipped");
|
|
619
|
+
console.log("Demo mode is for testing only and does not support API key integration\n");
|
|
620
|
+
}
|
|
621
|
+
// Step 2: Add PostgreSQL instance (if not demo mode)
|
|
622
|
+
if (!opts.demo) {
|
|
623
|
+
console.log("Step 2: Add PostgreSQL Instance to Monitor\n");
|
|
624
|
+
// Clear instances.yml in production mode (start fresh)
|
|
625
|
+
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
626
|
+
const emptyInstancesContent = "# PostgreSQL instances to monitor\n# Add your instances using: postgres-ai mon targets add\n\n";
|
|
627
|
+
fs.writeFileSync(instancesPath, emptyInstancesContent, "utf8");
|
|
628
|
+
if (opts.dbUrl) {
|
|
629
|
+
console.log("Using database URL provided via --db-url parameter");
|
|
630
|
+
console.log(`Adding PostgreSQL instance from: ${opts.dbUrl}\n`);
|
|
631
|
+
const match = opts.dbUrl.match(/^postgresql:\/\/[^@]+@([^:/]+)/);
|
|
632
|
+
const autoInstanceName = match ? match[1] : "db-instance";
|
|
633
|
+
const connStr = opts.dbUrl;
|
|
634
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
635
|
+
if (!m) {
|
|
636
|
+
console.error("✗ Invalid connection string format");
|
|
637
|
+
process.exitCode = 1;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const host = m[3];
|
|
641
|
+
const db = m[5];
|
|
642
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
643
|
+
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
644
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
645
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
646
|
+
// Test connection
|
|
647
|
+
console.log("Testing connection to the added instance...");
|
|
648
|
+
try {
|
|
649
|
+
const { Client } = require("pg");
|
|
650
|
+
const client = new Client({ connectionString: connStr });
|
|
651
|
+
await client.connect();
|
|
652
|
+
const result = await client.query("select version();");
|
|
653
|
+
console.log("✓ Connection successful");
|
|
654
|
+
console.log(`${result.rows[0].version}\n`);
|
|
655
|
+
await client.end();
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
659
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else if (opts.yes) {
|
|
663
|
+
// Auto-yes mode without database URL - skip database setup
|
|
664
|
+
console.log("Auto-yes mode: no database URL provided, skipping database setup");
|
|
665
|
+
console.log("⚠ No PostgreSQL instance added");
|
|
666
|
+
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
const rl = readline.createInterface({
|
|
670
|
+
input: process.stdin,
|
|
671
|
+
output: process.stdout
|
|
672
|
+
});
|
|
673
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
674
|
+
try {
|
|
675
|
+
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
676
|
+
const answer = await question("Do you want to add a PostgreSQL instance now? (Y/n): ");
|
|
677
|
+
const proceedWithInstance = !answer || answer.toLowerCase() === "y";
|
|
678
|
+
if (proceedWithInstance) {
|
|
679
|
+
console.log("\nYou can provide either:");
|
|
680
|
+
console.log(" 1. A full connection string: postgresql://user:pass@host:port/database");
|
|
681
|
+
console.log(" 2. Press Enter to skip for now\n");
|
|
682
|
+
const connStr = await question("Enter connection string (or press Enter to skip): ");
|
|
683
|
+
if (connStr.trim()) {
|
|
684
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
685
|
+
if (!m) {
|
|
686
|
+
console.error("✗ Invalid connection string format");
|
|
687
|
+
console.log("⚠ Continuing without adding instance\n");
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
const host = m[3];
|
|
691
|
+
const db = m[5];
|
|
692
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
693
|
+
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
694
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
695
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
696
|
+
// Test connection
|
|
697
|
+
console.log("Testing connection to the added instance...");
|
|
698
|
+
try {
|
|
699
|
+
const { Client } = require("pg");
|
|
700
|
+
const client = new Client({ connectionString: connStr });
|
|
701
|
+
await client.connect();
|
|
702
|
+
const result = await client.query("select version();");
|
|
703
|
+
console.log("✓ Connection successful");
|
|
704
|
+
console.log(`${result.rows[0].version}\n`);
|
|
705
|
+
await client.end();
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
709
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
rl.close();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database\n");
|
|
728
|
+
}
|
|
729
|
+
// Step 3: Update configuration
|
|
730
|
+
console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
|
|
218
731
|
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
219
732
|
if (code1 !== 0) {
|
|
220
733
|
process.exitCode = code1;
|
|
221
734
|
return;
|
|
222
735
|
}
|
|
223
|
-
|
|
224
|
-
|
|
736
|
+
console.log("✓ Configuration updated\n");
|
|
737
|
+
// Step 4: Ensure Grafana password is configured
|
|
738
|
+
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
739
|
+
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
740
|
+
let grafanaPassword = "";
|
|
741
|
+
try {
|
|
742
|
+
if (fs.existsSync(cfgPath)) {
|
|
743
|
+
const stats = fs.statSync(cfgPath);
|
|
744
|
+
if (!stats.isDirectory()) {
|
|
745
|
+
const content = fs.readFileSync(cfgPath, "utf8");
|
|
746
|
+
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
747
|
+
if (match) {
|
|
748
|
+
grafanaPassword = match[1].trim();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (!grafanaPassword) {
|
|
753
|
+
console.log("Generating secure Grafana password...");
|
|
754
|
+
const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
|
|
755
|
+
grafanaPassword = password.trim();
|
|
756
|
+
let configContent = "";
|
|
757
|
+
if (fs.existsSync(cfgPath)) {
|
|
758
|
+
const stats = fs.statSync(cfgPath);
|
|
759
|
+
if (!stats.isDirectory()) {
|
|
760
|
+
configContent = fs.readFileSync(cfgPath, "utf8");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
764
|
+
lines.push(`grafana_password=${grafanaPassword}`);
|
|
765
|
+
fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
|
|
766
|
+
}
|
|
767
|
+
console.log("✓ Grafana password configured\n");
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
console.log("⚠ Could not generate Grafana password automatically");
|
|
771
|
+
console.log("Using default password: demo\n");
|
|
772
|
+
grafanaPassword = "demo";
|
|
773
|
+
}
|
|
774
|
+
// Step 5: Start services
|
|
775
|
+
console.log(opts.demo ? "Step 5: Starting monitoring services..." : "Step 5: Starting monitoring services...");
|
|
776
|
+
const code2 = await runCompose(["up", "-d", "--force-recreate"]);
|
|
777
|
+
if (code2 !== 0) {
|
|
225
778
|
process.exitCode = code2;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
console.log("✓ Services started\n");
|
|
782
|
+
// Final summary
|
|
783
|
+
console.log("=================================");
|
|
784
|
+
console.log(" 🎉 Quickstart setup completed!");
|
|
785
|
+
console.log("=================================\n");
|
|
786
|
+
console.log("What's running:");
|
|
787
|
+
if (opts.demo) {
|
|
788
|
+
console.log(" ✅ Demo PostgreSQL database (monitoring target)");
|
|
789
|
+
}
|
|
790
|
+
console.log(" ✅ PostgreSQL monitoring infrastructure");
|
|
791
|
+
console.log(" ✅ Grafana dashboards (with secure password)");
|
|
792
|
+
console.log(" ✅ Prometheus metrics storage");
|
|
793
|
+
console.log(" ✅ Flask API backend");
|
|
794
|
+
console.log(" ✅ Automated report generation (every 24h)");
|
|
795
|
+
console.log(" ✅ Host stats monitoring (CPU, memory, disk, I/O)\n");
|
|
796
|
+
if (!opts.demo) {
|
|
797
|
+
console.log("Next steps:");
|
|
798
|
+
console.log(" • Add more PostgreSQL instances: postgres-ai mon targets add");
|
|
799
|
+
console.log(" • View configured instances: postgres-ai mon targets list");
|
|
800
|
+
console.log(" • Check service health: postgres-ai mon health\n");
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
console.log("Demo mode next steps:");
|
|
804
|
+
console.log(" • Explore Grafana dashboards at http://localhost:3000");
|
|
805
|
+
console.log(" • Connect to demo database: postgresql://postgres:postgres@localhost:55432/target_database");
|
|
806
|
+
console.log(" • Generate some load on the demo database to see metrics\n");
|
|
807
|
+
}
|
|
808
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
809
|
+
console.log("🚀 MAIN ACCESS POINT - Start here:");
|
|
810
|
+
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
811
|
+
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
812
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
226
813
|
});
|
|
227
814
|
mon
|
|
228
815
|
.command("start")
|
|
@@ -289,10 +876,12 @@ mon
|
|
|
289
876
|
.option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
|
|
290
877
|
.action(async (opts) => {
|
|
291
878
|
const services = [
|
|
292
|
-
{ name: "Grafana",
|
|
293
|
-
{ name: "Prometheus",
|
|
294
|
-
{ name: "PGWatch (Postgres)",
|
|
295
|
-
{ name: "PGWatch (Prometheus)",
|
|
879
|
+
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
880
|
+
{ name: "Prometheus", container: "sink-prometheus" },
|
|
881
|
+
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
882
|
+
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
883
|
+
{ name: "Target DB", container: "target-db" },
|
|
884
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
296
885
|
];
|
|
297
886
|
const waitTime = opts.wait || 0;
|
|
298
887
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
@@ -306,19 +895,16 @@ mon
|
|
|
306
895
|
allHealthy = true;
|
|
307
896
|
for (const service of services) {
|
|
308
897
|
try {
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
});
|
|
316
|
-
clearTimeout(timeoutId);
|
|
317
|
-
if (response.status === 200) {
|
|
898
|
+
const { execSync } = require("child_process");
|
|
899
|
+
const status = execSync(`docker inspect -f '{{.State.Status}}' ${service.container} 2>/dev/null`, {
|
|
900
|
+
encoding: 'utf8',
|
|
901
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
902
|
+
}).trim();
|
|
903
|
+
if (status === 'running') {
|
|
318
904
|
console.log(`✓ ${service.name}: healthy`);
|
|
319
905
|
}
|
|
320
906
|
else {
|
|
321
|
-
console.log(`✗ ${service.name}: unhealthy (
|
|
907
|
+
console.log(`✗ ${service.name}: unhealthy (status: ${status})`);
|
|
322
908
|
allHealthy = false;
|
|
323
909
|
}
|
|
324
910
|
}
|
|
@@ -1041,12 +1627,30 @@ mon
|
|
|
1041
1627
|
console.log(` Password: ${password}`);
|
|
1042
1628
|
console.log("");
|
|
1043
1629
|
});
|
|
1630
|
+
/**
|
|
1631
|
+
* Interpret escape sequences in a string (e.g., \n -> newline)
|
|
1632
|
+
* Note: In regex, to match literal backslash-n, we need \\n in the pattern
|
|
1633
|
+
* which requires \\\\n in the JavaScript string literal
|
|
1634
|
+
*/
|
|
1635
|
+
function interpretEscapes(str) {
|
|
1636
|
+
// First handle double backslashes by temporarily replacing them
|
|
1637
|
+
// Then handle other escapes, then restore double backslashes as single
|
|
1638
|
+
return str
|
|
1639
|
+
.replace(/\\\\/g, '\x00') // Temporarily mark double backslashes
|
|
1640
|
+
.replace(/\\n/g, '\n') // Match literal backslash-n (\\\\n in JS string -> \\n in regex -> matches \n)
|
|
1641
|
+
.replace(/\\t/g, '\t')
|
|
1642
|
+
.replace(/\\r/g, '\r')
|
|
1643
|
+
.replace(/\\"/g, '"')
|
|
1644
|
+
.replace(/\\'/g, "'")
|
|
1645
|
+
.replace(/\x00/g, '\\'); // Restore double backslashes as single
|
|
1646
|
+
}
|
|
1044
1647
|
// Issues management
|
|
1045
1648
|
const issues = program.command("issues").description("issues management");
|
|
1046
1649
|
issues
|
|
1047
1650
|
.command("list")
|
|
1048
1651
|
.description("list issues")
|
|
1049
1652
|
.option("--debug", "enable debug output")
|
|
1653
|
+
.option("--json", "output raw JSON")
|
|
1050
1654
|
.action(async (opts) => {
|
|
1051
1655
|
try {
|
|
1052
1656
|
const rootOpts = program.opts();
|
|
@@ -1059,14 +1663,90 @@ issues
|
|
|
1059
1663
|
}
|
|
1060
1664
|
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1061
1665
|
const result = await (0, issues_1.fetchIssues)({ apiKey, apiBaseUrl, debug: !!opts.debug });
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1666
|
+
const trimmed = Array.isArray(result)
|
|
1667
|
+
? result.map((r) => ({
|
|
1668
|
+
id: r.id,
|
|
1669
|
+
title: r.title,
|
|
1670
|
+
status: r.status,
|
|
1671
|
+
created_at: r.created_at,
|
|
1672
|
+
}))
|
|
1673
|
+
: result;
|
|
1674
|
+
printResult(trimmed, opts.json);
|
|
1675
|
+
}
|
|
1676
|
+
catch (err) {
|
|
1677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1678
|
+
console.error(message);
|
|
1679
|
+
process.exitCode = 1;
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
issues
|
|
1683
|
+
.command("view <issueId>")
|
|
1684
|
+
.description("view issue details and comments")
|
|
1685
|
+
.option("--debug", "enable debug output")
|
|
1686
|
+
.option("--json", "output raw JSON")
|
|
1687
|
+
.action(async (issueId, opts) => {
|
|
1688
|
+
try {
|
|
1689
|
+
const rootOpts = program.opts();
|
|
1690
|
+
const cfg = config.readConfig();
|
|
1691
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1692
|
+
if (!apiKey) {
|
|
1693
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1694
|
+
process.exitCode = 1;
|
|
1695
|
+
return;
|
|
1066
1696
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1697
|
+
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1698
|
+
const issue = await (0, issues_1.fetchIssue)({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1699
|
+
if (!issue) {
|
|
1700
|
+
console.error("Issue not found");
|
|
1701
|
+
process.exitCode = 1;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const comments = await (0, issues_1.fetchIssueComments)({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1705
|
+
const combined = { issue, comments };
|
|
1706
|
+
printResult(combined, opts.json);
|
|
1707
|
+
}
|
|
1708
|
+
catch (err) {
|
|
1709
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1710
|
+
console.error(message);
|
|
1711
|
+
process.exitCode = 1;
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
issues
|
|
1715
|
+
.command("post_comment <issueId> <content>")
|
|
1716
|
+
.description("post a new comment to an issue")
|
|
1717
|
+
.option("--parent <uuid>", "parent comment id")
|
|
1718
|
+
.option("--debug", "enable debug output")
|
|
1719
|
+
.option("--json", "output raw JSON")
|
|
1720
|
+
.action(async (issueId, content, opts) => {
|
|
1721
|
+
try {
|
|
1722
|
+
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
1723
|
+
if (opts.debug) {
|
|
1724
|
+
// eslint-disable-next-line no-console
|
|
1725
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
1726
|
+
}
|
|
1727
|
+
content = interpretEscapes(content);
|
|
1728
|
+
if (opts.debug) {
|
|
1729
|
+
// eslint-disable-next-line no-console
|
|
1730
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
1069
1731
|
}
|
|
1732
|
+
const rootOpts = program.opts();
|
|
1733
|
+
const cfg = config.readConfig();
|
|
1734
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1735
|
+
if (!apiKey) {
|
|
1736
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1737
|
+
process.exitCode = 1;
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1741
|
+
const result = await (0, issues_1.createIssueComment)({
|
|
1742
|
+
apiKey,
|
|
1743
|
+
apiBaseUrl,
|
|
1744
|
+
issueId,
|
|
1745
|
+
content,
|
|
1746
|
+
parentCommentId: opts.parent,
|
|
1747
|
+
debug: !!opts.debug,
|
|
1748
|
+
});
|
|
1749
|
+
printResult(result, opts.json);
|
|
1070
1750
|
}
|
|
1071
1751
|
catch (err) {
|
|
1072
1752
|
const message = err instanceof Error ? err.message : String(err);
|