postgresai 0.14.0-dev.56 → 0.14.0-dev.58
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/bin/postgres-ai.ts +201 -8
- package/dist/bin/postgres-ai.js +693 -83
- package/dist/sql/05.helpers.sql +31 -7
- package/dist/sql/sql/05.helpers.sql +31 -7
- package/lib/config.ts +4 -4
- package/lib/issues.ts +318 -0
- package/lib/mcp-server.ts +207 -73
- package/lib/metrics-embedded.ts +1 -1
- package/package.json +1 -1
- package/sql/05.helpers.sql +31 -7
- package/test/init.integration.test.ts +98 -0
- package/test/init.test.ts +72 -0
- package/test/issues.cli.test.ts +314 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +988 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as os from "os";
|
|
|
10
10
|
import * as crypto from "node:crypto";
|
|
11
11
|
import { Client } from "pg";
|
|
12
12
|
import { startMcpServer } from "../lib/mcp-server";
|
|
13
|
-
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
13
|
+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment } from "../lib/issues";
|
|
14
14
|
import { resolveBaseUrls } from "../lib/util";
|
|
15
15
|
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
16
16
|
import * as pkce from "../lib/pkce";
|
|
@@ -42,7 +42,7 @@ async function execPromise(command: string): Promise<{ stdout: string; stderr: s
|
|
|
42
42
|
childProcess.exec(command, (error, stdout, stderr) => {
|
|
43
43
|
if (error) {
|
|
44
44
|
const err = error as Error & { code: number };
|
|
45
|
-
err.code = error.code
|
|
45
|
+
err.code = typeof error.code === "number" ? error.code : 1;
|
|
46
46
|
reject(err);
|
|
47
47
|
} else {
|
|
48
48
|
resolve({ stdout, stderr });
|
|
@@ -56,7 +56,7 @@ async function execFilePromise(file: string, args: string[]): Promise<{ stdout:
|
|
|
56
56
|
childProcess.execFile(file, args, (error, stdout, stderr) => {
|
|
57
57
|
if (error) {
|
|
58
58
|
const err = error as Error & { code: number };
|
|
59
|
-
err.code = error.code
|
|
59
|
+
err.code = typeof error.code === "number" ? error.code : 1;
|
|
60
60
|
reject(err);
|
|
61
61
|
} else {
|
|
62
62
|
resolve({ stdout, stderr });
|
|
@@ -1174,6 +1174,11 @@ mon
|
|
|
1174
1174
|
.option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)")
|
|
1175
1175
|
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
1176
1176
|
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; yes: boolean }) => {
|
|
1177
|
+
// Get apiKey from global program options (--api-key is defined globally)
|
|
1178
|
+
// This is needed because Commander.js routes --api-key to the global option, not the subcommand's option
|
|
1179
|
+
const globalOpts = program.opts<CliOptions>();
|
|
1180
|
+
const apiKey = opts.apiKey || globalOpts.apiKey;
|
|
1181
|
+
|
|
1177
1182
|
console.log("\n=================================");
|
|
1178
1183
|
console.log(" PostgresAI monitoring local install");
|
|
1179
1184
|
console.log("=================================\n");
|
|
@@ -1226,7 +1231,7 @@ mon
|
|
|
1226
1231
|
opts.dbUrl = undefined;
|
|
1227
1232
|
}
|
|
1228
1233
|
|
|
1229
|
-
if (opts.demo &&
|
|
1234
|
+
if (opts.demo && apiKey) {
|
|
1230
1235
|
console.error("✗ Cannot use --api-key with --demo mode");
|
|
1231
1236
|
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
1232
1237
|
console.error("\nUse demo mode without API key: postgres-ai mon local-install --demo");
|
|
@@ -1248,11 +1253,11 @@ mon
|
|
|
1248
1253
|
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
1249
1254
|
console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
|
|
1250
1255
|
|
|
1251
|
-
if (
|
|
1256
|
+
if (apiKey) {
|
|
1252
1257
|
console.log("Using API key provided via --api-key parameter");
|
|
1253
|
-
config.writeConfig({ apiKey
|
|
1258
|
+
config.writeConfig({ apiKey });
|
|
1254
1259
|
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
1255
|
-
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${
|
|
1260
|
+
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${apiKey}\n`, {
|
|
1256
1261
|
encoding: "utf8",
|
|
1257
1262
|
mode: 0o600
|
|
1258
1263
|
});
|
|
@@ -2508,7 +2513,7 @@ issues
|
|
|
2508
2513
|
});
|
|
2509
2514
|
|
|
2510
2515
|
issues
|
|
2511
|
-
.command("
|
|
2516
|
+
.command("post-comment <issueId> <content>")
|
|
2512
2517
|
.description("post a new comment to an issue")
|
|
2513
2518
|
.option("--parent <uuid>", "parent comment id")
|
|
2514
2519
|
.option("--debug", "enable debug output")
|
|
@@ -2553,6 +2558,194 @@ issues
|
|
|
2553
2558
|
}
|
|
2554
2559
|
});
|
|
2555
2560
|
|
|
2561
|
+
issues
|
|
2562
|
+
.command("create <title>")
|
|
2563
|
+
.description("create a new issue")
|
|
2564
|
+
.option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10))
|
|
2565
|
+
.option("--project-id <id>", "project id", (v) => parseInt(v, 10))
|
|
2566
|
+
.option("--description <text>", "issue description (supports \\\\n)")
|
|
2567
|
+
.option(
|
|
2568
|
+
"--label <label>",
|
|
2569
|
+
"issue label (repeatable)",
|
|
2570
|
+
(value: string, previous: string[]) => {
|
|
2571
|
+
previous.push(value);
|
|
2572
|
+
return previous;
|
|
2573
|
+
},
|
|
2574
|
+
[] as string[]
|
|
2575
|
+
)
|
|
2576
|
+
.option("--debug", "enable debug output")
|
|
2577
|
+
.option("--json", "output raw JSON")
|
|
2578
|
+
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
|
|
2579
|
+
try {
|
|
2580
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2581
|
+
const cfg = config.readConfig();
|
|
2582
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2583
|
+
if (!apiKey) {
|
|
2584
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2585
|
+
process.exitCode = 1;
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const title = interpretEscapes(String(rawTitle || "").trim());
|
|
2590
|
+
if (!title) {
|
|
2591
|
+
console.error("title is required");
|
|
2592
|
+
process.exitCode = 1;
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
2597
|
+
if (typeof orgId !== "number") {
|
|
2598
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
2599
|
+
process.exitCode = 1;
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
2604
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
2605
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
2606
|
+
|
|
2607
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2608
|
+
const result = await createIssue({
|
|
2609
|
+
apiKey,
|
|
2610
|
+
apiBaseUrl,
|
|
2611
|
+
title,
|
|
2612
|
+
orgId,
|
|
2613
|
+
description,
|
|
2614
|
+
projectId,
|
|
2615
|
+
labels,
|
|
2616
|
+
debug: !!opts.debug,
|
|
2617
|
+
});
|
|
2618
|
+
printResult(result, opts.json);
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2621
|
+
console.error(message);
|
|
2622
|
+
process.exitCode = 1;
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
issues
|
|
2627
|
+
.command("update <issueId>")
|
|
2628
|
+
.description("update an existing issue (title/description/status/labels)")
|
|
2629
|
+
.option("--title <text>", "new title (supports \\\\n)")
|
|
2630
|
+
.option("--description <text>", "new description (supports \\\\n)")
|
|
2631
|
+
.option("--status <value>", "status: open|closed|0|1")
|
|
2632
|
+
.option(
|
|
2633
|
+
"--label <label>",
|
|
2634
|
+
"set labels (repeatable). If provided, replaces existing labels.",
|
|
2635
|
+
(value: string, previous: string[]) => {
|
|
2636
|
+
previous.push(value);
|
|
2637
|
+
return previous;
|
|
2638
|
+
},
|
|
2639
|
+
[] as string[]
|
|
2640
|
+
)
|
|
2641
|
+
.option("--clear-labels", "set labels to an empty list")
|
|
2642
|
+
.option("--debug", "enable debug output")
|
|
2643
|
+
.option("--json", "output raw JSON")
|
|
2644
|
+
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
|
|
2645
|
+
try {
|
|
2646
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2647
|
+
const cfg = config.readConfig();
|
|
2648
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2649
|
+
if (!apiKey) {
|
|
2650
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2651
|
+
process.exitCode = 1;
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2656
|
+
|
|
2657
|
+
const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
|
|
2658
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
2659
|
+
|
|
2660
|
+
let status: number | undefined = undefined;
|
|
2661
|
+
if (opts.status !== undefined) {
|
|
2662
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
2663
|
+
if (raw === "open") status = 0;
|
|
2664
|
+
else if (raw === "closed") status = 1;
|
|
2665
|
+
else {
|
|
2666
|
+
const n = Number(raw);
|
|
2667
|
+
if (!Number.isFinite(n)) {
|
|
2668
|
+
console.error("status must be open|closed|0|1");
|
|
2669
|
+
process.exitCode = 1;
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
status = n;
|
|
2673
|
+
}
|
|
2674
|
+
if (status !== 0 && status !== 1) {
|
|
2675
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
2676
|
+
process.exitCode = 1;
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
let labels: string[] | undefined = undefined;
|
|
2682
|
+
if (opts.clearLabels) {
|
|
2683
|
+
labels = [];
|
|
2684
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
2685
|
+
labels = opts.label.map(String);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
const result = await updateIssue({
|
|
2689
|
+
apiKey,
|
|
2690
|
+
apiBaseUrl,
|
|
2691
|
+
issueId,
|
|
2692
|
+
title,
|
|
2693
|
+
description,
|
|
2694
|
+
status,
|
|
2695
|
+
labels,
|
|
2696
|
+
debug: !!opts.debug,
|
|
2697
|
+
});
|
|
2698
|
+
printResult(result, opts.json);
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2701
|
+
console.error(message);
|
|
2702
|
+
process.exitCode = 1;
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
issues
|
|
2707
|
+
.command("update-comment <commentId> <content>")
|
|
2708
|
+
.description("update an existing issue comment")
|
|
2709
|
+
.option("--debug", "enable debug output")
|
|
2710
|
+
.option("--json", "output raw JSON")
|
|
2711
|
+
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
2712
|
+
try {
|
|
2713
|
+
if (opts.debug) {
|
|
2714
|
+
// eslint-disable-next-line no-console
|
|
2715
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
2716
|
+
}
|
|
2717
|
+
content = interpretEscapes(content);
|
|
2718
|
+
if (opts.debug) {
|
|
2719
|
+
// eslint-disable-next-line no-console
|
|
2720
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2724
|
+
const cfg = config.readConfig();
|
|
2725
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2726
|
+
if (!apiKey) {
|
|
2727
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2728
|
+
process.exitCode = 1;
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2733
|
+
|
|
2734
|
+
const result = await updateIssueComment({
|
|
2735
|
+
apiKey,
|
|
2736
|
+
apiBaseUrl,
|
|
2737
|
+
commentId,
|
|
2738
|
+
content,
|
|
2739
|
+
debug: !!opts.debug,
|
|
2740
|
+
});
|
|
2741
|
+
printResult(result, opts.json);
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2744
|
+
console.error(message);
|
|
2745
|
+
process.exitCode = 1;
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2556
2749
|
// MCP server
|
|
2557
2750
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
2558
2751
|
|