postgresai 0.14.0-beta.12 → 0.14.0-beta.13
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 +32 -0
- package/bin/postgres-ai.ts +928 -170
- package/dist/bin/postgres-ai.js +2095 -335
- package/lib/checkup.ts +69 -3
- package/lib/init.ts +76 -19
- package/lib/issues.ts +453 -7
- package/lib/mcp-server.ts +180 -3
- package/lib/metrics-embedded.ts +3 -3
- package/lib/supabase.ts +824 -0
- package/package.json +1 -1
- package/test/checkup.test.ts +240 -14
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +80 -71
- package/test/init.test.ts +266 -1
- package/test/issues.cli.test.ts +224 -0
- package/test/mcp-server.test.ts +551 -12
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +6 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-beta.
|
|
13067
|
+
version: "0.14.0-beta.13",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -15887,7 +15887,7 @@ var Result = import_lib.default.Result;
|
|
|
15887
15887
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15888
15888
|
var defaults = import_lib.default.defaults;
|
|
15889
15889
|
// package.json
|
|
15890
|
-
var version = "0.14.0-beta.
|
|
15890
|
+
var version = "0.14.0-beta.13";
|
|
15891
15891
|
var package_default2 = {
|
|
15892
15892
|
name: "postgresai",
|
|
15893
15893
|
version,
|
|
@@ -16079,13 +16079,24 @@ function resolveBaseUrls(opts, cfg, defaults2 = {}) {
|
|
|
16079
16079
|
|
|
16080
16080
|
// lib/issues.ts
|
|
16081
16081
|
async function fetchIssues(params) {
|
|
16082
|
-
const { apiKey, apiBaseUrl, debug } = params;
|
|
16082
|
+
const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
|
|
16083
16083
|
if (!apiKey) {
|
|
16084
16084
|
throw new Error("API key is required");
|
|
16085
16085
|
}
|
|
16086
16086
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16087
16087
|
const url = new URL(`${base}/issues`);
|
|
16088
16088
|
url.searchParams.set("select", "id,title,status,created_at");
|
|
16089
|
+
url.searchParams.set("order", "id.desc");
|
|
16090
|
+
url.searchParams.set("limit", String(limit));
|
|
16091
|
+
url.searchParams.set("offset", String(offset));
|
|
16092
|
+
if (typeof orgId === "number") {
|
|
16093
|
+
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
16094
|
+
}
|
|
16095
|
+
if (status === "open") {
|
|
16096
|
+
url.searchParams.set("status", "eq.0");
|
|
16097
|
+
} else if (status === "closed") {
|
|
16098
|
+
url.searchParams.set("status", "eq.1");
|
|
16099
|
+
}
|
|
16089
16100
|
const headers = {
|
|
16090
16101
|
"access-token": apiKey,
|
|
16091
16102
|
Prefer: "return=representation",
|
|
@@ -16170,7 +16181,7 @@ async function fetchIssue(params) {
|
|
|
16170
16181
|
}
|
|
16171
16182
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16172
16183
|
const url = new URL(`${base}/issues`);
|
|
16173
|
-
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
|
|
16184
|
+
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
|
|
16174
16185
|
url.searchParams.set("id", `eq.${issueId}`);
|
|
16175
16186
|
url.searchParams.set("limit", "1");
|
|
16176
16187
|
const headers = {
|
|
@@ -16198,11 +16209,20 @@ async function fetchIssue(params) {
|
|
|
16198
16209
|
if (response.ok) {
|
|
16199
16210
|
try {
|
|
16200
16211
|
const parsed = JSON.parse(data);
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
return parsed;
|
|
16212
|
+
const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
16213
|
+
if (!rawIssue) {
|
|
16214
|
+
return null;
|
|
16205
16215
|
}
|
|
16216
|
+
const actionItemsSummary = Array.isArray(rawIssue.action_items) ? rawIssue.action_items.map((item) => ({ id: item.id, title: item.title })) : [];
|
|
16217
|
+
return {
|
|
16218
|
+
id: rawIssue.id,
|
|
16219
|
+
title: rawIssue.title,
|
|
16220
|
+
description: rawIssue.description,
|
|
16221
|
+
status: rawIssue.status,
|
|
16222
|
+
created_at: rawIssue.created_at,
|
|
16223
|
+
author_display_name: rawIssue.author_display_name,
|
|
16224
|
+
action_items: actionItemsSummary
|
|
16225
|
+
};
|
|
16206
16226
|
} catch {
|
|
16207
16227
|
throw new Error(`Failed to parse issue response: ${data}`);
|
|
16208
16228
|
}
|
|
@@ -16441,6 +16461,243 @@ async function updateIssueComment(params) {
|
|
|
16441
16461
|
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
16442
16462
|
}
|
|
16443
16463
|
}
|
|
16464
|
+
async function fetchActionItem(params) {
|
|
16465
|
+
const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
|
|
16466
|
+
if (!apiKey) {
|
|
16467
|
+
throw new Error("API key is required");
|
|
16468
|
+
}
|
|
16469
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16470
|
+
const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
|
|
16471
|
+
const validIds = rawIds.filter((id) => id != null && typeof id === "string").map((id) => id.trim()).filter((id) => id.length > 0 && uuidPattern.test(id));
|
|
16472
|
+
if (validIds.length === 0) {
|
|
16473
|
+
throw new Error("actionItemId is required and must be a valid UUID");
|
|
16474
|
+
}
|
|
16475
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16476
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
16477
|
+
if (validIds.length === 1) {
|
|
16478
|
+
url.searchParams.set("id", `eq.${validIds[0]}`);
|
|
16479
|
+
} else {
|
|
16480
|
+
url.searchParams.set("id", `in.(${validIds.join(",")})`);
|
|
16481
|
+
}
|
|
16482
|
+
const headers = {
|
|
16483
|
+
"access-token": apiKey,
|
|
16484
|
+
Prefer: "return=representation",
|
|
16485
|
+
"Content-Type": "application/json",
|
|
16486
|
+
Connection: "close"
|
|
16487
|
+
};
|
|
16488
|
+
if (debug) {
|
|
16489
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16490
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16491
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
16492
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16493
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16494
|
+
}
|
|
16495
|
+
const response = await fetch(url.toString(), {
|
|
16496
|
+
method: "GET",
|
|
16497
|
+
headers
|
|
16498
|
+
});
|
|
16499
|
+
if (debug) {
|
|
16500
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16501
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16502
|
+
}
|
|
16503
|
+
const data = await response.text();
|
|
16504
|
+
if (response.ok) {
|
|
16505
|
+
try {
|
|
16506
|
+
const parsed = JSON.parse(data);
|
|
16507
|
+
if (Array.isArray(parsed)) {
|
|
16508
|
+
return parsed;
|
|
16509
|
+
}
|
|
16510
|
+
return parsed ? [parsed] : [];
|
|
16511
|
+
} catch {
|
|
16512
|
+
throw new Error(`Failed to parse action item response: ${data}`);
|
|
16513
|
+
}
|
|
16514
|
+
} else {
|
|
16515
|
+
throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
|
|
16516
|
+
}
|
|
16517
|
+
}
|
|
16518
|
+
async function fetchActionItems(params) {
|
|
16519
|
+
const { apiKey, apiBaseUrl, issueId, debug } = params;
|
|
16520
|
+
if (!apiKey) {
|
|
16521
|
+
throw new Error("API key is required");
|
|
16522
|
+
}
|
|
16523
|
+
if (!issueId) {
|
|
16524
|
+
throw new Error("issueId is required");
|
|
16525
|
+
}
|
|
16526
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16527
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
16528
|
+
throw new Error("issueId must be a valid UUID");
|
|
16529
|
+
}
|
|
16530
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16531
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
16532
|
+
url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
|
|
16533
|
+
const headers = {
|
|
16534
|
+
"access-token": apiKey,
|
|
16535
|
+
Prefer: "return=representation",
|
|
16536
|
+
"Content-Type": "application/json",
|
|
16537
|
+
Connection: "close"
|
|
16538
|
+
};
|
|
16539
|
+
if (debug) {
|
|
16540
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16541
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16542
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
16543
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16544
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16545
|
+
}
|
|
16546
|
+
const response = await fetch(url.toString(), {
|
|
16547
|
+
method: "GET",
|
|
16548
|
+
headers
|
|
16549
|
+
});
|
|
16550
|
+
if (debug) {
|
|
16551
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16552
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16553
|
+
}
|
|
16554
|
+
const data = await response.text();
|
|
16555
|
+
if (response.ok) {
|
|
16556
|
+
try {
|
|
16557
|
+
return JSON.parse(data);
|
|
16558
|
+
} catch {
|
|
16559
|
+
throw new Error(`Failed to parse action items response: ${data}`);
|
|
16560
|
+
}
|
|
16561
|
+
} else {
|
|
16562
|
+
throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
|
|
16563
|
+
}
|
|
16564
|
+
}
|
|
16565
|
+
async function createActionItem(params) {
|
|
16566
|
+
const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
|
|
16567
|
+
if (!apiKey) {
|
|
16568
|
+
throw new Error("API key is required");
|
|
16569
|
+
}
|
|
16570
|
+
if (!issueId) {
|
|
16571
|
+
throw new Error("issueId is required");
|
|
16572
|
+
}
|
|
16573
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16574
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
16575
|
+
throw new Error("issueId must be a valid UUID");
|
|
16576
|
+
}
|
|
16577
|
+
if (!title) {
|
|
16578
|
+
throw new Error("title is required");
|
|
16579
|
+
}
|
|
16580
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16581
|
+
const url = new URL(`${base}/rpc/issue_action_item_create`);
|
|
16582
|
+
const bodyObj = {
|
|
16583
|
+
issue_id: issueId,
|
|
16584
|
+
title
|
|
16585
|
+
};
|
|
16586
|
+
if (description !== undefined) {
|
|
16587
|
+
bodyObj.description = description;
|
|
16588
|
+
}
|
|
16589
|
+
if (sqlAction !== undefined) {
|
|
16590
|
+
bodyObj.sql_action = sqlAction;
|
|
16591
|
+
}
|
|
16592
|
+
if (configs !== undefined) {
|
|
16593
|
+
bodyObj.configs = configs;
|
|
16594
|
+
}
|
|
16595
|
+
const body = JSON.stringify(bodyObj);
|
|
16596
|
+
const headers = {
|
|
16597
|
+
"access-token": apiKey,
|
|
16598
|
+
Prefer: "return=representation",
|
|
16599
|
+
"Content-Type": "application/json",
|
|
16600
|
+
Connection: "close"
|
|
16601
|
+
};
|
|
16602
|
+
if (debug) {
|
|
16603
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16604
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16605
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
16606
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16607
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16608
|
+
console.log(`Debug: Request body: ${body}`);
|
|
16609
|
+
}
|
|
16610
|
+
const response = await fetch(url.toString(), {
|
|
16611
|
+
method: "POST",
|
|
16612
|
+
headers,
|
|
16613
|
+
body
|
|
16614
|
+
});
|
|
16615
|
+
if (debug) {
|
|
16616
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16617
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16618
|
+
}
|
|
16619
|
+
const data = await response.text();
|
|
16620
|
+
if (response.ok) {
|
|
16621
|
+
try {
|
|
16622
|
+
return JSON.parse(data);
|
|
16623
|
+
} catch {
|
|
16624
|
+
throw new Error(`Failed to parse create action item response: ${data}`);
|
|
16625
|
+
}
|
|
16626
|
+
} else {
|
|
16627
|
+
throw new Error(formatHttpError("Failed to create action item", response.status, data));
|
|
16628
|
+
}
|
|
16629
|
+
}
|
|
16630
|
+
async function updateActionItem(params) {
|
|
16631
|
+
const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
|
|
16632
|
+
if (!apiKey) {
|
|
16633
|
+
throw new Error("API key is required");
|
|
16634
|
+
}
|
|
16635
|
+
if (!actionItemId) {
|
|
16636
|
+
throw new Error("actionItemId is required");
|
|
16637
|
+
}
|
|
16638
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16639
|
+
if (!uuidPattern.test(actionItemId.trim())) {
|
|
16640
|
+
throw new Error("actionItemId must be a valid UUID");
|
|
16641
|
+
}
|
|
16642
|
+
const hasUpdateField = title !== undefined || description !== undefined || isDone !== undefined || status !== undefined || statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
|
|
16643
|
+
if (!hasUpdateField) {
|
|
16644
|
+
throw new Error("At least one field to update is required");
|
|
16645
|
+
}
|
|
16646
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16647
|
+
const url = new URL(`${base}/rpc/issue_action_item_update`);
|
|
16648
|
+
const bodyObj = {
|
|
16649
|
+
action_item_id: actionItemId
|
|
16650
|
+
};
|
|
16651
|
+
if (title !== undefined) {
|
|
16652
|
+
bodyObj.title = title;
|
|
16653
|
+
}
|
|
16654
|
+
if (description !== undefined) {
|
|
16655
|
+
bodyObj.description = description;
|
|
16656
|
+
}
|
|
16657
|
+
if (isDone !== undefined) {
|
|
16658
|
+
bodyObj.is_done = isDone;
|
|
16659
|
+
}
|
|
16660
|
+
if (status !== undefined) {
|
|
16661
|
+
bodyObj.status = status;
|
|
16662
|
+
}
|
|
16663
|
+
if (statusReason !== undefined) {
|
|
16664
|
+
bodyObj.status_reason = statusReason;
|
|
16665
|
+
}
|
|
16666
|
+
if (sqlAction !== undefined) {
|
|
16667
|
+
bodyObj.sql_action = sqlAction;
|
|
16668
|
+
}
|
|
16669
|
+
if (configs !== undefined) {
|
|
16670
|
+
bodyObj.configs = configs;
|
|
16671
|
+
}
|
|
16672
|
+
const body = JSON.stringify(bodyObj);
|
|
16673
|
+
const headers = {
|
|
16674
|
+
"access-token": apiKey,
|
|
16675
|
+
Prefer: "return=representation",
|
|
16676
|
+
"Content-Type": "application/json",
|
|
16677
|
+
Connection: "close"
|
|
16678
|
+
};
|
|
16679
|
+
if (debug) {
|
|
16680
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16681
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16682
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
16683
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16684
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16685
|
+
console.log(`Debug: Request body: ${body}`);
|
|
16686
|
+
}
|
|
16687
|
+
const response = await fetch(url.toString(), {
|
|
16688
|
+
method: "POST",
|
|
16689
|
+
headers,
|
|
16690
|
+
body
|
|
16691
|
+
});
|
|
16692
|
+
if (debug) {
|
|
16693
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16694
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16695
|
+
}
|
|
16696
|
+
if (!response.ok) {
|
|
16697
|
+
const data = await response.text();
|
|
16698
|
+
throw new Error(formatHttpError("Failed to update action item", response.status, data));
|
|
16699
|
+
}
|
|
16700
|
+
}
|
|
16444
16701
|
|
|
16445
16702
|
// node_modules/zod/v4/core/core.js
|
|
16446
16703
|
var NEVER = Object.freeze({
|
|
@@ -23350,7 +23607,16 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
23350
23607
|
}
|
|
23351
23608
|
try {
|
|
23352
23609
|
if (toolName === "list_issues") {
|
|
23353
|
-
const
|
|
23610
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
23611
|
+
const statusArg = args.status ? String(args.status) : undefined;
|
|
23612
|
+
let status;
|
|
23613
|
+
if (statusArg === "open")
|
|
23614
|
+
status = "open";
|
|
23615
|
+
else if (statusArg === "closed")
|
|
23616
|
+
status = "closed";
|
|
23617
|
+
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
|
|
23618
|
+
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
23619
|
+
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
|
|
23354
23620
|
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
23355
23621
|
}
|
|
23356
23622
|
if (toolName === "view_issue") {
|
|
@@ -23430,6 +23696,70 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
23430
23696
|
const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
|
|
23431
23697
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23432
23698
|
}
|
|
23699
|
+
if (toolName === "view_action_item") {
|
|
23700
|
+
let actionItemIds;
|
|
23701
|
+
if (Array.isArray(args.action_item_ids)) {
|
|
23702
|
+
actionItemIds = args.action_item_ids.map((id) => String(id).trim()).filter((id) => id);
|
|
23703
|
+
} else if (args.action_item_id) {
|
|
23704
|
+
actionItemIds = [String(args.action_item_id).trim()];
|
|
23705
|
+
} else {
|
|
23706
|
+
actionItemIds = [];
|
|
23707
|
+
}
|
|
23708
|
+
if (actionItemIds.length === 0) {
|
|
23709
|
+
return { content: [{ type: "text", text: "action_item_id or action_item_ids is required" }], isError: true };
|
|
23710
|
+
}
|
|
23711
|
+
const actionItems = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds, debug });
|
|
23712
|
+
if (actionItems.length === 0) {
|
|
23713
|
+
return { content: [{ type: "text", text: "Action item(s) not found" }], isError: true };
|
|
23714
|
+
}
|
|
23715
|
+
return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
|
|
23716
|
+
}
|
|
23717
|
+
if (toolName === "list_action_items") {
|
|
23718
|
+
const issueId = String(args.issue_id || "").trim();
|
|
23719
|
+
if (!issueId) {
|
|
23720
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23721
|
+
}
|
|
23722
|
+
const actionItems = await fetchActionItems({ apiKey, apiBaseUrl, issueId, debug });
|
|
23723
|
+
return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
|
|
23724
|
+
}
|
|
23725
|
+
if (toolName === "create_action_item") {
|
|
23726
|
+
const issueId = String(args.issue_id || "").trim();
|
|
23727
|
+
const rawTitle = String(args.title || "").trim();
|
|
23728
|
+
if (!issueId) {
|
|
23729
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23730
|
+
}
|
|
23731
|
+
if (!rawTitle) {
|
|
23732
|
+
return { content: [{ type: "text", text: "title is required" }], isError: true };
|
|
23733
|
+
}
|
|
23734
|
+
const title = interpretEscapes(rawTitle);
|
|
23735
|
+
const rawDescription = args.description ? String(args.description) : undefined;
|
|
23736
|
+
const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
23737
|
+
const sqlAction = args.sql_action !== undefined ? String(args.sql_action) : undefined;
|
|
23738
|
+
const configs = Array.isArray(args.configs) ? args.configs : undefined;
|
|
23739
|
+
const result = await createActionItem({ apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug });
|
|
23740
|
+
return { content: [{ type: "text", text: JSON.stringify({ id: result }, null, 2) }] };
|
|
23741
|
+
}
|
|
23742
|
+
if (toolName === "update_action_item") {
|
|
23743
|
+
const actionItemId = String(args.action_item_id || "").trim();
|
|
23744
|
+
if (!actionItemId) {
|
|
23745
|
+
return { content: [{ type: "text", text: "action_item_id is required" }], isError: true };
|
|
23746
|
+
}
|
|
23747
|
+
const rawTitle = args.title !== undefined ? String(args.title) : undefined;
|
|
23748
|
+
const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
|
|
23749
|
+
const rawDescription = args.description !== undefined ? String(args.description) : undefined;
|
|
23750
|
+
const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
23751
|
+
const isDone = args.is_done !== undefined ? Boolean(args.is_done) : undefined;
|
|
23752
|
+
const status = args.status !== undefined ? String(args.status) : undefined;
|
|
23753
|
+
const statusReason = args.status_reason !== undefined ? String(args.status_reason) : undefined;
|
|
23754
|
+
if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined) {
|
|
23755
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, is_done, status, or status_reason)" }], isError: true };
|
|
23756
|
+
}
|
|
23757
|
+
if (status !== undefined && !["waiting_for_approval", "approved", "rejected"].includes(status)) {
|
|
23758
|
+
return { content: [{ type: "text", text: "status must be 'waiting_for_approval', 'approved', or 'rejected'" }], isError: true };
|
|
23759
|
+
}
|
|
23760
|
+
await updateActionItem({ apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, debug });
|
|
23761
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }] };
|
|
23762
|
+
}
|
|
23433
23763
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
23434
23764
|
} catch (err) {
|
|
23435
23765
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23437,7 +23767,11 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
23437
23767
|
}
|
|
23438
23768
|
}
|
|
23439
23769
|
async function startMcpServer(rootOpts, extra) {
|
|
23440
|
-
const server = new Server({
|
|
23770
|
+
const server = new Server({
|
|
23771
|
+
name: "postgresai-mcp",
|
|
23772
|
+
version: package_default2.version,
|
|
23773
|
+
title: "PostgresAI MCP Server"
|
|
23774
|
+
}, { capabilities: { tools: {} } });
|
|
23441
23775
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
23442
23776
|
return {
|
|
23443
23777
|
tools: [
|
|
@@ -23447,6 +23781,10 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
23447
23781
|
inputSchema: {
|
|
23448
23782
|
type: "object",
|
|
23449
23783
|
properties: {
|
|
23784
|
+
org_id: { type: "number", description: "Organization ID (optional, falls back to config)" },
|
|
23785
|
+
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
|
|
23786
|
+
limit: { type: "number", description: "Max number of issues to return (default: 20)" },
|
|
23787
|
+
offset: { type: "number", description: "Number of issues to skip (default: 0)" },
|
|
23450
23788
|
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23451
23789
|
},
|
|
23452
23790
|
additionalProperties: false
|
|
@@ -23535,47 +23873,130 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
23535
23873
|
required: ["comment_id", "content"],
|
|
23536
23874
|
additionalProperties: false
|
|
23537
23875
|
}
|
|
23538
|
-
}
|
|
23539
|
-
|
|
23540
|
-
|
|
23541
|
-
|
|
23542
|
-
|
|
23543
|
-
|
|
23544
|
-
|
|
23545
|
-
|
|
23546
|
-
|
|
23547
|
-
}
|
|
23548
|
-
|
|
23549
|
-
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
23577
|
-
|
|
23578
|
-
|
|
23876
|
+
},
|
|
23877
|
+
{
|
|
23878
|
+
name: "view_action_item",
|
|
23879
|
+
description: "View action item(s) with all details. Supports single ID or multiple IDs.",
|
|
23880
|
+
inputSchema: {
|
|
23881
|
+
type: "object",
|
|
23882
|
+
properties: {
|
|
23883
|
+
action_item_id: { type: "string", description: "Single action item ID (UUID)" },
|
|
23884
|
+
action_item_ids: { type: "array", items: { type: "string" }, description: "Multiple action item IDs (UUIDs)" },
|
|
23885
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23886
|
+
},
|
|
23887
|
+
additionalProperties: false
|
|
23888
|
+
}
|
|
23889
|
+
},
|
|
23890
|
+
{
|
|
23891
|
+
name: "list_action_items",
|
|
23892
|
+
description: "List action items for an issue",
|
|
23893
|
+
inputSchema: {
|
|
23894
|
+
type: "object",
|
|
23895
|
+
properties: {
|
|
23896
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
23897
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23898
|
+
},
|
|
23899
|
+
required: ["issue_id"],
|
|
23900
|
+
additionalProperties: false
|
|
23901
|
+
}
|
|
23902
|
+
},
|
|
23903
|
+
{
|
|
23904
|
+
name: "create_action_item",
|
|
23905
|
+
description: "Create a new action item for an issue",
|
|
23906
|
+
inputSchema: {
|
|
23907
|
+
type: "object",
|
|
23908
|
+
properties: {
|
|
23909
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
23910
|
+
title: { type: "string", description: "Action item title" },
|
|
23911
|
+
description: { type: "string", description: "Detailed description" },
|
|
23912
|
+
sql_action: { type: "string", description: "SQL command to execute, e.g. 'DROP INDEX CONCURRENTLY idx_unused;'" },
|
|
23913
|
+
configs: {
|
|
23914
|
+
type: "array",
|
|
23915
|
+
items: {
|
|
23916
|
+
type: "object",
|
|
23917
|
+
properties: {
|
|
23918
|
+
parameter: { type: "string" },
|
|
23919
|
+
value: { type: "string" }
|
|
23920
|
+
},
|
|
23921
|
+
required: ["parameter", "value"]
|
|
23922
|
+
},
|
|
23923
|
+
description: "Configuration parameter changes"
|
|
23924
|
+
},
|
|
23925
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23926
|
+
},
|
|
23927
|
+
required: ["issue_id", "title"],
|
|
23928
|
+
additionalProperties: false
|
|
23929
|
+
}
|
|
23930
|
+
},
|
|
23931
|
+
{
|
|
23932
|
+
name: "update_action_item",
|
|
23933
|
+
description: "Update an action item: mark as done/not done, approve/reject, or edit title/description",
|
|
23934
|
+
inputSchema: {
|
|
23935
|
+
type: "object",
|
|
23936
|
+
properties: {
|
|
23937
|
+
action_item_id: { type: "string", description: "Action item ID (UUID)" },
|
|
23938
|
+
title: { type: "string", description: "New title" },
|
|
23939
|
+
description: { type: "string", description: "New description" },
|
|
23940
|
+
is_done: { type: "boolean", description: "Mark as done (true) or not done (false)" },
|
|
23941
|
+
status: { type: "string", description: "Approval status: 'waiting_for_approval', 'approved', or 'rejected'" },
|
|
23942
|
+
status_reason: { type: "string", description: "Reason for approval/rejection" },
|
|
23943
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23944
|
+
},
|
|
23945
|
+
required: ["action_item_id"],
|
|
23946
|
+
additionalProperties: false
|
|
23947
|
+
}
|
|
23948
|
+
}
|
|
23949
|
+
]
|
|
23950
|
+
};
|
|
23951
|
+
});
|
|
23952
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
23953
|
+
return handleToolCall(req, rootOpts, extra);
|
|
23954
|
+
});
|
|
23955
|
+
const transport = new StdioServerTransport;
|
|
23956
|
+
await server.connect(transport);
|
|
23957
|
+
}
|
|
23958
|
+
|
|
23959
|
+
// lib/issues.ts
|
|
23960
|
+
async function fetchIssues2(params) {
|
|
23961
|
+
const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
|
|
23962
|
+
if (!apiKey) {
|
|
23963
|
+
throw new Error("API key is required");
|
|
23964
|
+
}
|
|
23965
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
23966
|
+
const url = new URL(`${base}/issues`);
|
|
23967
|
+
url.searchParams.set("select", "id,title,status,created_at");
|
|
23968
|
+
url.searchParams.set("order", "id.desc");
|
|
23969
|
+
url.searchParams.set("limit", String(limit));
|
|
23970
|
+
url.searchParams.set("offset", String(offset));
|
|
23971
|
+
if (typeof orgId === "number") {
|
|
23972
|
+
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
23973
|
+
}
|
|
23974
|
+
if (status === "open") {
|
|
23975
|
+
url.searchParams.set("status", "eq.0");
|
|
23976
|
+
} else if (status === "closed") {
|
|
23977
|
+
url.searchParams.set("status", "eq.1");
|
|
23978
|
+
}
|
|
23979
|
+
const headers = {
|
|
23980
|
+
"access-token": apiKey,
|
|
23981
|
+
Prefer: "return=representation",
|
|
23982
|
+
"Content-Type": "application/json",
|
|
23983
|
+
Connection: "close"
|
|
23984
|
+
};
|
|
23985
|
+
if (debug) {
|
|
23986
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
23987
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
23988
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
23989
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
23990
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
23991
|
+
}
|
|
23992
|
+
const response = await fetch(url.toString(), {
|
|
23993
|
+
method: "GET",
|
|
23994
|
+
headers
|
|
23995
|
+
});
|
|
23996
|
+
if (debug) {
|
|
23997
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
23998
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
23999
|
+
}
|
|
23579
24000
|
const data = await response.text();
|
|
23580
24001
|
if (response.ok) {
|
|
23581
24002
|
try {
|
|
@@ -23639,7 +24060,7 @@ async function fetchIssue2(params) {
|
|
|
23639
24060
|
}
|
|
23640
24061
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
23641
24062
|
const url = new URL(`${base}/issues`);
|
|
23642
|
-
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
|
|
24063
|
+
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
|
|
23643
24064
|
url.searchParams.set("id", `eq.${issueId}`);
|
|
23644
24065
|
url.searchParams.set("limit", "1");
|
|
23645
24066
|
const headers = {
|
|
@@ -23667,11 +24088,20 @@ async function fetchIssue2(params) {
|
|
|
23667
24088
|
if (response.ok) {
|
|
23668
24089
|
try {
|
|
23669
24090
|
const parsed = JSON.parse(data);
|
|
23670
|
-
|
|
23671
|
-
|
|
23672
|
-
|
|
23673
|
-
return parsed;
|
|
24091
|
+
const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
24092
|
+
if (!rawIssue) {
|
|
24093
|
+
return null;
|
|
23674
24094
|
}
|
|
24095
|
+
const actionItemsSummary = Array.isArray(rawIssue.action_items) ? rawIssue.action_items.map((item) => ({ id: item.id, title: item.title })) : [];
|
|
24096
|
+
return {
|
|
24097
|
+
id: rawIssue.id,
|
|
24098
|
+
title: rawIssue.title,
|
|
24099
|
+
description: rawIssue.description,
|
|
24100
|
+
status: rawIssue.status,
|
|
24101
|
+
created_at: rawIssue.created_at,
|
|
24102
|
+
author_display_name: rawIssue.author_display_name,
|
|
24103
|
+
action_items: actionItemsSummary
|
|
24104
|
+
};
|
|
23675
24105
|
} catch {
|
|
23676
24106
|
throw new Error(`Failed to parse issue response: ${data}`);
|
|
23677
24107
|
}
|
|
@@ -23910,6 +24340,243 @@ async function updateIssueComment2(params) {
|
|
|
23910
24340
|
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
23911
24341
|
}
|
|
23912
24342
|
}
|
|
24343
|
+
async function fetchActionItem2(params) {
|
|
24344
|
+
const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
|
|
24345
|
+
if (!apiKey) {
|
|
24346
|
+
throw new Error("API key is required");
|
|
24347
|
+
}
|
|
24348
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24349
|
+
const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
|
|
24350
|
+
const validIds = rawIds.filter((id) => id != null && typeof id === "string").map((id) => id.trim()).filter((id) => id.length > 0 && uuidPattern.test(id));
|
|
24351
|
+
if (validIds.length === 0) {
|
|
24352
|
+
throw new Error("actionItemId is required and must be a valid UUID");
|
|
24353
|
+
}
|
|
24354
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
24355
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
24356
|
+
if (validIds.length === 1) {
|
|
24357
|
+
url.searchParams.set("id", `eq.${validIds[0]}`);
|
|
24358
|
+
} else {
|
|
24359
|
+
url.searchParams.set("id", `in.(${validIds.join(",")})`);
|
|
24360
|
+
}
|
|
24361
|
+
const headers = {
|
|
24362
|
+
"access-token": apiKey,
|
|
24363
|
+
Prefer: "return=representation",
|
|
24364
|
+
"Content-Type": "application/json",
|
|
24365
|
+
Connection: "close"
|
|
24366
|
+
};
|
|
24367
|
+
if (debug) {
|
|
24368
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
24369
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
24370
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
24371
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
24372
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
24373
|
+
}
|
|
24374
|
+
const response = await fetch(url.toString(), {
|
|
24375
|
+
method: "GET",
|
|
24376
|
+
headers
|
|
24377
|
+
});
|
|
24378
|
+
if (debug) {
|
|
24379
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
24380
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
24381
|
+
}
|
|
24382
|
+
const data = await response.text();
|
|
24383
|
+
if (response.ok) {
|
|
24384
|
+
try {
|
|
24385
|
+
const parsed = JSON.parse(data);
|
|
24386
|
+
if (Array.isArray(parsed)) {
|
|
24387
|
+
return parsed;
|
|
24388
|
+
}
|
|
24389
|
+
return parsed ? [parsed] : [];
|
|
24390
|
+
} catch {
|
|
24391
|
+
throw new Error(`Failed to parse action item response: ${data}`);
|
|
24392
|
+
}
|
|
24393
|
+
} else {
|
|
24394
|
+
throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
|
|
24395
|
+
}
|
|
24396
|
+
}
|
|
24397
|
+
async function fetchActionItems2(params) {
|
|
24398
|
+
const { apiKey, apiBaseUrl, issueId, debug } = params;
|
|
24399
|
+
if (!apiKey) {
|
|
24400
|
+
throw new Error("API key is required");
|
|
24401
|
+
}
|
|
24402
|
+
if (!issueId) {
|
|
24403
|
+
throw new Error("issueId is required");
|
|
24404
|
+
}
|
|
24405
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24406
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
24407
|
+
throw new Error("issueId must be a valid UUID");
|
|
24408
|
+
}
|
|
24409
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
24410
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
24411
|
+
url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
|
|
24412
|
+
const headers = {
|
|
24413
|
+
"access-token": apiKey,
|
|
24414
|
+
Prefer: "return=representation",
|
|
24415
|
+
"Content-Type": "application/json",
|
|
24416
|
+
Connection: "close"
|
|
24417
|
+
};
|
|
24418
|
+
if (debug) {
|
|
24419
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
24420
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
24421
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
24422
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
24423
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
24424
|
+
}
|
|
24425
|
+
const response = await fetch(url.toString(), {
|
|
24426
|
+
method: "GET",
|
|
24427
|
+
headers
|
|
24428
|
+
});
|
|
24429
|
+
if (debug) {
|
|
24430
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
24431
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
24432
|
+
}
|
|
24433
|
+
const data = await response.text();
|
|
24434
|
+
if (response.ok) {
|
|
24435
|
+
try {
|
|
24436
|
+
return JSON.parse(data);
|
|
24437
|
+
} catch {
|
|
24438
|
+
throw new Error(`Failed to parse action items response: ${data}`);
|
|
24439
|
+
}
|
|
24440
|
+
} else {
|
|
24441
|
+
throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
|
|
24442
|
+
}
|
|
24443
|
+
}
|
|
24444
|
+
async function createActionItem2(params) {
|
|
24445
|
+
const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
|
|
24446
|
+
if (!apiKey) {
|
|
24447
|
+
throw new Error("API key is required");
|
|
24448
|
+
}
|
|
24449
|
+
if (!issueId) {
|
|
24450
|
+
throw new Error("issueId is required");
|
|
24451
|
+
}
|
|
24452
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24453
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
24454
|
+
throw new Error("issueId must be a valid UUID");
|
|
24455
|
+
}
|
|
24456
|
+
if (!title) {
|
|
24457
|
+
throw new Error("title is required");
|
|
24458
|
+
}
|
|
24459
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
24460
|
+
const url = new URL(`${base}/rpc/issue_action_item_create`);
|
|
24461
|
+
const bodyObj = {
|
|
24462
|
+
issue_id: issueId,
|
|
24463
|
+
title
|
|
24464
|
+
};
|
|
24465
|
+
if (description !== undefined) {
|
|
24466
|
+
bodyObj.description = description;
|
|
24467
|
+
}
|
|
24468
|
+
if (sqlAction !== undefined) {
|
|
24469
|
+
bodyObj.sql_action = sqlAction;
|
|
24470
|
+
}
|
|
24471
|
+
if (configs !== undefined) {
|
|
24472
|
+
bodyObj.configs = configs;
|
|
24473
|
+
}
|
|
24474
|
+
const body = JSON.stringify(bodyObj);
|
|
24475
|
+
const headers = {
|
|
24476
|
+
"access-token": apiKey,
|
|
24477
|
+
Prefer: "return=representation",
|
|
24478
|
+
"Content-Type": "application/json",
|
|
24479
|
+
Connection: "close"
|
|
24480
|
+
};
|
|
24481
|
+
if (debug) {
|
|
24482
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
24483
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
24484
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
24485
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
24486
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
24487
|
+
console.log(`Debug: Request body: ${body}`);
|
|
24488
|
+
}
|
|
24489
|
+
const response = await fetch(url.toString(), {
|
|
24490
|
+
method: "POST",
|
|
24491
|
+
headers,
|
|
24492
|
+
body
|
|
24493
|
+
});
|
|
24494
|
+
if (debug) {
|
|
24495
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
24496
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
24497
|
+
}
|
|
24498
|
+
const data = await response.text();
|
|
24499
|
+
if (response.ok) {
|
|
24500
|
+
try {
|
|
24501
|
+
return JSON.parse(data);
|
|
24502
|
+
} catch {
|
|
24503
|
+
throw new Error(`Failed to parse create action item response: ${data}`);
|
|
24504
|
+
}
|
|
24505
|
+
} else {
|
|
24506
|
+
throw new Error(formatHttpError("Failed to create action item", response.status, data));
|
|
24507
|
+
}
|
|
24508
|
+
}
|
|
24509
|
+
async function updateActionItem2(params) {
|
|
24510
|
+
const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
|
|
24511
|
+
if (!apiKey) {
|
|
24512
|
+
throw new Error("API key is required");
|
|
24513
|
+
}
|
|
24514
|
+
if (!actionItemId) {
|
|
24515
|
+
throw new Error("actionItemId is required");
|
|
24516
|
+
}
|
|
24517
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24518
|
+
if (!uuidPattern.test(actionItemId.trim())) {
|
|
24519
|
+
throw new Error("actionItemId must be a valid UUID");
|
|
24520
|
+
}
|
|
24521
|
+
const hasUpdateField = title !== undefined || description !== undefined || isDone !== undefined || status !== undefined || statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
|
|
24522
|
+
if (!hasUpdateField) {
|
|
24523
|
+
throw new Error("At least one field to update is required");
|
|
24524
|
+
}
|
|
24525
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
24526
|
+
const url = new URL(`${base}/rpc/issue_action_item_update`);
|
|
24527
|
+
const bodyObj = {
|
|
24528
|
+
action_item_id: actionItemId
|
|
24529
|
+
};
|
|
24530
|
+
if (title !== undefined) {
|
|
24531
|
+
bodyObj.title = title;
|
|
24532
|
+
}
|
|
24533
|
+
if (description !== undefined) {
|
|
24534
|
+
bodyObj.description = description;
|
|
24535
|
+
}
|
|
24536
|
+
if (isDone !== undefined) {
|
|
24537
|
+
bodyObj.is_done = isDone;
|
|
24538
|
+
}
|
|
24539
|
+
if (status !== undefined) {
|
|
24540
|
+
bodyObj.status = status;
|
|
24541
|
+
}
|
|
24542
|
+
if (statusReason !== undefined) {
|
|
24543
|
+
bodyObj.status_reason = statusReason;
|
|
24544
|
+
}
|
|
24545
|
+
if (sqlAction !== undefined) {
|
|
24546
|
+
bodyObj.sql_action = sqlAction;
|
|
24547
|
+
}
|
|
24548
|
+
if (configs !== undefined) {
|
|
24549
|
+
bodyObj.configs = configs;
|
|
24550
|
+
}
|
|
24551
|
+
const body = JSON.stringify(bodyObj);
|
|
24552
|
+
const headers = {
|
|
24553
|
+
"access-token": apiKey,
|
|
24554
|
+
Prefer: "return=representation",
|
|
24555
|
+
"Content-Type": "application/json",
|
|
24556
|
+
Connection: "close"
|
|
24557
|
+
};
|
|
24558
|
+
if (debug) {
|
|
24559
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
24560
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
24561
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
24562
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
24563
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
24564
|
+
console.log(`Debug: Request body: ${body}`);
|
|
24565
|
+
}
|
|
24566
|
+
const response = await fetch(url.toString(), {
|
|
24567
|
+
method: "POST",
|
|
24568
|
+
headers,
|
|
24569
|
+
body
|
|
24570
|
+
});
|
|
24571
|
+
if (debug) {
|
|
24572
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
24573
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
24574
|
+
}
|
|
24575
|
+
if (!response.ok) {
|
|
24576
|
+
const data = await response.text();
|
|
24577
|
+
throw new Error(formatHttpError("Failed to update action item", response.status, data));
|
|
24578
|
+
}
|
|
24579
|
+
}
|
|
23913
24580
|
|
|
23914
24581
|
// lib/util.ts
|
|
23915
24582
|
function maskSecret2(secret) {
|
|
@@ -23947,6 +24614,15 @@ import { URL as URL2, fileURLToPath } from "url";
|
|
|
23947
24614
|
import * as fs3 from "fs";
|
|
23948
24615
|
import * as path3 from "path";
|
|
23949
24616
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
24617
|
+
var KNOWN_PROVIDERS = ["self-managed", "supabase"];
|
|
24618
|
+
var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
|
|
24619
|
+
var SKIP_ALTER_USER_PROVIDERS = ["supabase"];
|
|
24620
|
+
var SKIP_SEARCH_PATH_CHECK_PROVIDERS = ["supabase"];
|
|
24621
|
+
function validateProvider(provider) {
|
|
24622
|
+
if (!provider || KNOWN_PROVIDERS.includes(provider))
|
|
24623
|
+
return null;
|
|
24624
|
+
return `Unknown provider "${provider}". Known providers: ${KNOWN_PROVIDERS.join(", ")}. Treating as self-managed.`;
|
|
24625
|
+
}
|
|
23950
24626
|
function sslModeToConfig(mode) {
|
|
23951
24627
|
if (mode.toLowerCase() === "disable")
|
|
23952
24628
|
return false;
|
|
@@ -24261,6 +24937,7 @@ async function resolveMonitoringPassword(opts) {
|
|
|
24261
24937
|
async function buildInitPlan(params) {
|
|
24262
24938
|
const monitoringUser = params.monitoringUser || DEFAULT_MONITORING_USER;
|
|
24263
24939
|
const database = params.database;
|
|
24940
|
+
const provider = params.provider ?? "self-managed";
|
|
24264
24941
|
const qRole = quoteIdent(monitoringUser);
|
|
24265
24942
|
const qDb = quoteIdent(database);
|
|
24266
24943
|
const qPw = quoteLiteral(params.monitoringPassword);
|
|
@@ -24270,7 +24947,8 @@ async function buildInitPlan(params) {
|
|
|
24270
24947
|
ROLE_IDENT: qRole,
|
|
24271
24948
|
DB_IDENT: qDb
|
|
24272
24949
|
};
|
|
24273
|
-
|
|
24950
|
+
if (!SKIP_ROLE_CREATION_PROVIDERS.includes(provider)) {
|
|
24951
|
+
const roleStmt = `do $$ begin
|
|
24274
24952
|
if not exists (select 1 from pg_catalog.pg_roles where rolname = ${qRoleNameLit}) then
|
|
24275
24953
|
begin
|
|
24276
24954
|
create user ${qRole} with password ${qPw};
|
|
@@ -24280,11 +24958,23 @@ async function buildInitPlan(params) {
|
|
|
24280
24958
|
end if;
|
|
24281
24959
|
alter user ${qRole} with password ${qPw};
|
|
24282
24960
|
end $$;`;
|
|
24283
|
-
|
|
24284
|
-
|
|
24961
|
+
const roleSql = applyTemplate(loadSqlTemplate("01.role.sql"), { ...vars, ROLE_STMT: roleStmt });
|
|
24962
|
+
steps.push({ name: "01.role", sql: roleSql });
|
|
24963
|
+
}
|
|
24964
|
+
let permissionsSql = applyTemplate(loadSqlTemplate("02.permissions.sql"), vars);
|
|
24965
|
+
if (SKIP_ALTER_USER_PROVIDERS.includes(provider)) {
|
|
24966
|
+
permissionsSql = permissionsSql.split(`
|
|
24967
|
+
`).filter((line) => {
|
|
24968
|
+
const trimmed = line.trim();
|
|
24969
|
+
if (trimmed.startsWith("--") || trimmed === "")
|
|
24970
|
+
return true;
|
|
24971
|
+
return !/^\s*alter\s+user\s+/i.test(line);
|
|
24972
|
+
}).join(`
|
|
24973
|
+
`);
|
|
24974
|
+
}
|
|
24285
24975
|
steps.push({
|
|
24286
24976
|
name: "02.permissions",
|
|
24287
|
-
sql:
|
|
24977
|
+
sql: permissionsSql
|
|
24288
24978
|
});
|
|
24289
24979
|
steps.push({
|
|
24290
24980
|
name: "05.helpers",
|
|
@@ -24301,25 +24991,413 @@ end $$;`;
|
|
|
24301
24991
|
optional: true
|
|
24302
24992
|
});
|
|
24303
24993
|
}
|
|
24304
|
-
return { monitoringUser, database, steps };
|
|
24994
|
+
return { monitoringUser, database, steps };
|
|
24995
|
+
}
|
|
24996
|
+
async function applyInitPlan(params) {
|
|
24997
|
+
const applied = [];
|
|
24998
|
+
const skippedOptional = [];
|
|
24999
|
+
const executeStep = async (step) => {
|
|
25000
|
+
await params.client.query("begin;");
|
|
25001
|
+
try {
|
|
25002
|
+
await params.client.query(step.sql, step.params);
|
|
25003
|
+
await params.client.query("commit;");
|
|
25004
|
+
} catch (e) {
|
|
25005
|
+
try {
|
|
25006
|
+
await params.client.query("rollback;");
|
|
25007
|
+
} catch {}
|
|
25008
|
+
throw e;
|
|
25009
|
+
}
|
|
25010
|
+
};
|
|
25011
|
+
for (const step of params.plan.steps.filter((s) => !s.optional)) {
|
|
25012
|
+
try {
|
|
25013
|
+
await executeStep(step);
|
|
25014
|
+
applied.push(step.name);
|
|
25015
|
+
} catch (e) {
|
|
25016
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
25017
|
+
const errAny = e;
|
|
25018
|
+
const wrapped = new Error(`Failed at step "${step.name}": ${msg}`);
|
|
25019
|
+
const pgErrorFields = [
|
|
25020
|
+
"code",
|
|
25021
|
+
"detail",
|
|
25022
|
+
"hint",
|
|
25023
|
+
"position",
|
|
25024
|
+
"internalPosition",
|
|
25025
|
+
"internalQuery",
|
|
25026
|
+
"where",
|
|
25027
|
+
"schema",
|
|
25028
|
+
"table",
|
|
25029
|
+
"column",
|
|
25030
|
+
"dataType",
|
|
25031
|
+
"constraint",
|
|
25032
|
+
"file",
|
|
25033
|
+
"line",
|
|
25034
|
+
"routine"
|
|
25035
|
+
];
|
|
25036
|
+
if (errAny && typeof errAny === "object") {
|
|
25037
|
+
for (const field of pgErrorFields) {
|
|
25038
|
+
if (errAny[field] !== undefined)
|
|
25039
|
+
wrapped[field] = errAny[field];
|
|
25040
|
+
}
|
|
25041
|
+
}
|
|
25042
|
+
if (e instanceof Error && e.stack) {
|
|
25043
|
+
wrapped.stack = e.stack;
|
|
25044
|
+
}
|
|
25045
|
+
throw wrapped;
|
|
25046
|
+
}
|
|
25047
|
+
}
|
|
25048
|
+
for (const step of params.plan.steps.filter((s) => s.optional)) {
|
|
25049
|
+
try {
|
|
25050
|
+
await executeStep(step);
|
|
25051
|
+
applied.push(step.name);
|
|
25052
|
+
} catch {
|
|
25053
|
+
skippedOptional.push(step.name);
|
|
25054
|
+
}
|
|
25055
|
+
}
|
|
25056
|
+
return { applied, skippedOptional };
|
|
25057
|
+
}
|
|
25058
|
+
async function verifyInitSetup(params) {
|
|
25059
|
+
await params.client.query("begin isolation level repeatable read;");
|
|
25060
|
+
try {
|
|
25061
|
+
const missingRequired = [];
|
|
25062
|
+
const missingOptional = [];
|
|
25063
|
+
const role = params.monitoringUser;
|
|
25064
|
+
const db = params.database;
|
|
25065
|
+
const provider = params.provider ?? "self-managed";
|
|
25066
|
+
const roleRes = await params.client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [role]);
|
|
25067
|
+
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
25068
|
+
if (!roleExists) {
|
|
25069
|
+
missingRequired.push(`role "${role}" does not exist`);
|
|
25070
|
+
return { ok: false, missingRequired, missingOptional };
|
|
25071
|
+
}
|
|
25072
|
+
const connectRes = await params.client.query("select has_database_privilege($1, $2, 'CONNECT') as ok", [role, db]);
|
|
25073
|
+
if (!connectRes.rows?.[0]?.ok) {
|
|
25074
|
+
missingRequired.push(`CONNECT on database "${db}"`);
|
|
25075
|
+
}
|
|
25076
|
+
const pgMonitorRes = await params.client.query("select pg_has_role($1, 'pg_monitor', 'member') as ok", [role]);
|
|
25077
|
+
if (!pgMonitorRes.rows?.[0]?.ok) {
|
|
25078
|
+
missingRequired.push("membership in role pg_monitor");
|
|
25079
|
+
}
|
|
25080
|
+
const pgIndexRes = await params.client.query("select has_table_privilege($1, 'pg_catalog.pg_index', 'SELECT') as ok", [role]);
|
|
25081
|
+
if (!pgIndexRes.rows?.[0]?.ok) {
|
|
25082
|
+
missingRequired.push("SELECT on pg_catalog.pg_index");
|
|
25083
|
+
}
|
|
25084
|
+
const schemaExistsRes = await params.client.query("select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok", [role]);
|
|
25085
|
+
if (!schemaExistsRes.rows?.[0]?.ok) {
|
|
25086
|
+
missingRequired.push("USAGE on schema postgres_ai");
|
|
25087
|
+
}
|
|
25088
|
+
const viewExistsRes = await params.client.query("select to_regclass('postgres_ai.pg_statistic') is not null as ok");
|
|
25089
|
+
if (!viewExistsRes.rows?.[0]?.ok) {
|
|
25090
|
+
missingRequired.push("view postgres_ai.pg_statistic exists");
|
|
25091
|
+
} else {
|
|
25092
|
+
const viewPrivRes = await params.client.query("select has_table_privilege($1, 'postgres_ai.pg_statistic', 'SELECT') as ok", [role]);
|
|
25093
|
+
if (!viewPrivRes.rows?.[0]?.ok) {
|
|
25094
|
+
missingRequired.push("SELECT on view postgres_ai.pg_statistic");
|
|
25095
|
+
}
|
|
25096
|
+
}
|
|
25097
|
+
const schemaUsageRes = await params.client.query("select has_schema_privilege($1, 'public', 'USAGE') as ok", [role]);
|
|
25098
|
+
if (!schemaUsageRes.rows?.[0]?.ok) {
|
|
25099
|
+
missingRequired.push("USAGE on schema public");
|
|
25100
|
+
}
|
|
25101
|
+
if (!SKIP_SEARCH_PATH_CHECK_PROVIDERS.includes(provider)) {
|
|
25102
|
+
const rolcfgRes = await params.client.query("select rolconfig from pg_catalog.pg_roles where rolname = $1", [role]);
|
|
25103
|
+
const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
|
|
25104
|
+
const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
|
|
25105
|
+
if (typeof spLine !== "string" || !spLine) {
|
|
25106
|
+
missingRequired.push("role search_path is set");
|
|
25107
|
+
} else {
|
|
25108
|
+
const sp = spLine.toLowerCase();
|
|
25109
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
25110
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
25111
|
+
}
|
|
25112
|
+
}
|
|
25113
|
+
}
|
|
25114
|
+
const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
|
|
25115
|
+
if (!explainFnRes.rows?.[0]?.ok) {
|
|
25116
|
+
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
25117
|
+
}
|
|
25118
|
+
const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
|
|
25119
|
+
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
25120
|
+
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
25121
|
+
}
|
|
25122
|
+
if (params.includeOptionalPermissions) {
|
|
25123
|
+
{
|
|
25124
|
+
const extRes = await params.client.query("select 1 from pg_extension where extname = 'rds_tools'");
|
|
25125
|
+
if ((extRes.rowCount ?? 0) === 0) {
|
|
25126
|
+
missingOptional.push("extension rds_tools");
|
|
25127
|
+
} else {
|
|
25128
|
+
const fnRes = await params.client.query("select has_function_privilege($1, 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok", [role]);
|
|
25129
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
25130
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
25131
|
+
}
|
|
25132
|
+
}
|
|
25133
|
+
}
|
|
25134
|
+
const optionalFns = [
|
|
25135
|
+
"pg_catalog.pg_stat_file(text)",
|
|
25136
|
+
"pg_catalog.pg_stat_file(text, boolean)",
|
|
25137
|
+
"pg_catalog.pg_ls_dir(text)",
|
|
25138
|
+
"pg_catalog.pg_ls_dir(text, boolean, boolean)"
|
|
25139
|
+
];
|
|
25140
|
+
for (const fn of optionalFns) {
|
|
25141
|
+
const fnRes = await params.client.query("select has_function_privilege($1, $2, 'EXECUTE') as ok", [role, fn]);
|
|
25142
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
25143
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
25144
|
+
}
|
|
25145
|
+
}
|
|
25146
|
+
}
|
|
25147
|
+
return { ok: missingRequired.length === 0, missingRequired, missingOptional };
|
|
25148
|
+
} finally {
|
|
25149
|
+
try {
|
|
25150
|
+
await params.client.query("rollback;");
|
|
25151
|
+
} catch {}
|
|
25152
|
+
}
|
|
25153
|
+
}
|
|
25154
|
+
|
|
25155
|
+
// lib/supabase.ts
|
|
25156
|
+
var SUPABASE_API_BASE = "https://api.supabase.com";
|
|
25157
|
+
function isValidProjectRef(ref) {
|
|
25158
|
+
return /^[a-z0-9]{10,30}$/i.test(ref);
|
|
25159
|
+
}
|
|
25160
|
+
|
|
25161
|
+
class SupabaseClient {
|
|
25162
|
+
config;
|
|
25163
|
+
constructor(config2) {
|
|
25164
|
+
if (!config2.projectRef) {
|
|
25165
|
+
throw new Error("Supabase project reference is required");
|
|
25166
|
+
}
|
|
25167
|
+
if (!config2.accessToken) {
|
|
25168
|
+
throw new Error("Supabase access token is required");
|
|
25169
|
+
}
|
|
25170
|
+
if (!isValidProjectRef(config2.projectRef)) {
|
|
25171
|
+
throw new Error(`Invalid Supabase project reference format: "${config2.projectRef}". Expected 10-30 alphanumeric characters.`);
|
|
25172
|
+
}
|
|
25173
|
+
this.config = config2;
|
|
25174
|
+
}
|
|
25175
|
+
async query(sql, readOnly = false) {
|
|
25176
|
+
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(this.config.projectRef)}/database/query`;
|
|
25177
|
+
const response = await fetch(url, {
|
|
25178
|
+
method: "POST",
|
|
25179
|
+
headers: {
|
|
25180
|
+
"Content-Type": "application/json",
|
|
25181
|
+
Authorization: `Bearer ${this.config.accessToken}`
|
|
25182
|
+
},
|
|
25183
|
+
body: JSON.stringify({
|
|
25184
|
+
query: sql,
|
|
25185
|
+
read_only: readOnly
|
|
25186
|
+
})
|
|
25187
|
+
});
|
|
25188
|
+
const body = await response.text();
|
|
25189
|
+
let data;
|
|
25190
|
+
try {
|
|
25191
|
+
data = JSON.parse(body);
|
|
25192
|
+
} catch {
|
|
25193
|
+
throw this.createPgError({
|
|
25194
|
+
message: `Supabase API returned non-JSON response: ${body.slice(0, 200)}`,
|
|
25195
|
+
httpStatus: response.status
|
|
25196
|
+
});
|
|
25197
|
+
}
|
|
25198
|
+
if (!response.ok) {
|
|
25199
|
+
throw this.parseApiError(data, response.status);
|
|
25200
|
+
}
|
|
25201
|
+
if (data && typeof data === "object" && "error" in data && data.error) {
|
|
25202
|
+
throw this.parseApiError(data, response.status);
|
|
25203
|
+
}
|
|
25204
|
+
const rows = Array.isArray(data) ? data : [];
|
|
25205
|
+
return {
|
|
25206
|
+
rows,
|
|
25207
|
+
rowCount: rows.length
|
|
25208
|
+
};
|
|
25209
|
+
}
|
|
25210
|
+
async testConnection() {
|
|
25211
|
+
const result = await this.query("SELECT current_database() as db, version() as version", true);
|
|
25212
|
+
const row = result.rows[0] ?? {};
|
|
25213
|
+
return {
|
|
25214
|
+
database: String(row.db ?? ""),
|
|
25215
|
+
version: String(row.version ?? "")
|
|
25216
|
+
};
|
|
25217
|
+
}
|
|
25218
|
+
async getCurrentDatabase() {
|
|
25219
|
+
const result = await this.query("SELECT current_database() as db", true);
|
|
25220
|
+
const row = result.rows[0] ?? {};
|
|
25221
|
+
return String(row.db ?? "");
|
|
25222
|
+
}
|
|
25223
|
+
parseApiError(data, httpStatus) {
|
|
25224
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
25225
|
+
const errObj = "error" in data && data.error ? data.error : data;
|
|
25226
|
+
const pgCode = this.extractPgErrorCode(errObj);
|
|
25227
|
+
const message = this.extractErrorMessage(errObj);
|
|
25228
|
+
const detail = this.extractField(errObj, ["details", "detail"]);
|
|
25229
|
+
const hint = this.extractField(errObj, ["hint"]);
|
|
25230
|
+
return this.createPgError({
|
|
25231
|
+
message,
|
|
25232
|
+
code: pgCode,
|
|
25233
|
+
detail,
|
|
25234
|
+
hint,
|
|
25235
|
+
httpStatus,
|
|
25236
|
+
supabaseErrorCode: typeof errObj === "object" && errObj && "code" in errObj ? String(errObj.code ?? "") : undefined
|
|
25237
|
+
});
|
|
25238
|
+
}
|
|
25239
|
+
return this.createPgError({
|
|
25240
|
+
message: `Supabase API error (HTTP ${httpStatus})`,
|
|
25241
|
+
httpStatus
|
|
25242
|
+
});
|
|
25243
|
+
}
|
|
25244
|
+
extractPgErrorCode(errObj) {
|
|
25245
|
+
if (!errObj || typeof errObj !== "object")
|
|
25246
|
+
return;
|
|
25247
|
+
const obj = errObj;
|
|
25248
|
+
if (typeof obj.code === "string") {
|
|
25249
|
+
const code = obj.code;
|
|
25250
|
+
if (/^\d{5}$/.test(code)) {
|
|
25251
|
+
return code;
|
|
25252
|
+
}
|
|
25253
|
+
return this.mapSupabaseCodeToPg(code);
|
|
25254
|
+
}
|
|
25255
|
+
return;
|
|
25256
|
+
}
|
|
25257
|
+
mapSupabaseCodeToPg(code) {
|
|
25258
|
+
const mapping = {
|
|
25259
|
+
PGRST301: "28000",
|
|
25260
|
+
PGRST302: "28P01",
|
|
25261
|
+
"42501": "42501",
|
|
25262
|
+
PGRST000: "42501",
|
|
25263
|
+
"42601": "42601",
|
|
25264
|
+
"42P01": "42P01",
|
|
25265
|
+
PGRST200: "42P01",
|
|
25266
|
+
"42883": "42883",
|
|
25267
|
+
"08000": "08000",
|
|
25268
|
+
"08003": "08003",
|
|
25269
|
+
"08006": "08006",
|
|
25270
|
+
"42710": "42710"
|
|
25271
|
+
};
|
|
25272
|
+
return mapping[code];
|
|
25273
|
+
}
|
|
25274
|
+
extractErrorMessage(errObj) {
|
|
25275
|
+
if (!errObj || typeof errObj !== "object") {
|
|
25276
|
+
return "Unknown Supabase API error";
|
|
25277
|
+
}
|
|
25278
|
+
const obj = errObj;
|
|
25279
|
+
for (const field of ["message", "error", "msg", "description"]) {
|
|
25280
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
25281
|
+
return obj[field];
|
|
25282
|
+
}
|
|
25283
|
+
}
|
|
25284
|
+
if (obj.error && typeof obj.error === "object") {
|
|
25285
|
+
return this.extractErrorMessage(obj.error);
|
|
25286
|
+
}
|
|
25287
|
+
return "Unknown Supabase API error";
|
|
25288
|
+
}
|
|
25289
|
+
extractField(errObj, fieldNames) {
|
|
25290
|
+
if (!errObj || typeof errObj !== "object")
|
|
25291
|
+
return;
|
|
25292
|
+
const obj = errObj;
|
|
25293
|
+
for (const field of fieldNames) {
|
|
25294
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
25295
|
+
return obj[field];
|
|
25296
|
+
}
|
|
25297
|
+
}
|
|
25298
|
+
return;
|
|
25299
|
+
}
|
|
25300
|
+
createPgError(opts) {
|
|
25301
|
+
const err = new Error(opts.message);
|
|
25302
|
+
if (opts.code)
|
|
25303
|
+
err.code = opts.code;
|
|
25304
|
+
if (opts.detail)
|
|
25305
|
+
err.detail = opts.detail;
|
|
25306
|
+
if (opts.hint)
|
|
25307
|
+
err.hint = opts.hint;
|
|
25308
|
+
if (opts.httpStatus)
|
|
25309
|
+
err.httpStatus = opts.httpStatus;
|
|
25310
|
+
if (opts.supabaseErrorCode)
|
|
25311
|
+
err.supabaseErrorCode = opts.supabaseErrorCode;
|
|
25312
|
+
return err;
|
|
25313
|
+
}
|
|
25314
|
+
}
|
|
25315
|
+
async function fetchPoolerDatabaseUrl(config2, username) {
|
|
25316
|
+
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config2.projectRef)}/config/database/pooler`;
|
|
25317
|
+
try {
|
|
25318
|
+
const response = await fetch(url, {
|
|
25319
|
+
method: "GET",
|
|
25320
|
+
headers: {
|
|
25321
|
+
Authorization: `Bearer ${config2.accessToken}`
|
|
25322
|
+
}
|
|
25323
|
+
});
|
|
25324
|
+
if (!response.ok) {
|
|
25325
|
+
return null;
|
|
25326
|
+
}
|
|
25327
|
+
const data = await response.json();
|
|
25328
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
25329
|
+
const pooler = data[0];
|
|
25330
|
+
if (pooler.db_host && pooler.db_port && pooler.db_name) {
|
|
25331
|
+
return `postgresql://${username}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
|
|
25332
|
+
}
|
|
25333
|
+
if (typeof pooler.connection_string === "string") {
|
|
25334
|
+
try {
|
|
25335
|
+
const connUrl = new URL(pooler.connection_string);
|
|
25336
|
+
const portPart = connUrl.port ? `:${connUrl.port}` : "";
|
|
25337
|
+
return `postgresql://${username}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
|
|
25338
|
+
} catch {
|
|
25339
|
+
return null;
|
|
25340
|
+
}
|
|
25341
|
+
}
|
|
25342
|
+
}
|
|
25343
|
+
return null;
|
|
25344
|
+
} catch {
|
|
25345
|
+
return null;
|
|
25346
|
+
}
|
|
24305
25347
|
}
|
|
24306
|
-
|
|
25348
|
+
function resolveSupabaseConfig(opts) {
|
|
25349
|
+
const accessToken = opts.accessToken?.trim() || process.env.SUPABASE_ACCESS_TOKEN?.trim() || "";
|
|
25350
|
+
const projectRef = opts.projectRef?.trim() || process.env.SUPABASE_PROJECT_REF?.trim() || "";
|
|
25351
|
+
if (!accessToken) {
|
|
25352
|
+
throw new Error(`Supabase access token is required.
|
|
25353
|
+
` + `Provide it via --supabase-access-token or SUPABASE_ACCESS_TOKEN environment variable.
|
|
25354
|
+
` + "Generate a token at: https://supabase.com/dashboard/account/tokens");
|
|
25355
|
+
}
|
|
25356
|
+
if (!projectRef) {
|
|
25357
|
+
throw new Error(`Supabase project reference is required.
|
|
25358
|
+
` + `Provide it via --supabase-project-ref or SUPABASE_PROJECT_REF environment variable.
|
|
25359
|
+
` + "Find your project ref in the Supabase dashboard URL: https://supabase.com/dashboard/project/<ref>");
|
|
25360
|
+
}
|
|
25361
|
+
return { accessToken, projectRef };
|
|
25362
|
+
}
|
|
25363
|
+
function extractProjectRefFromUrl(dbUrl) {
|
|
25364
|
+
try {
|
|
25365
|
+
const url = new URL(dbUrl);
|
|
25366
|
+
const host = url.hostname;
|
|
25367
|
+
const match = host.match(/^(?:db\.)?([^.]+)\.supabase\.co$/i);
|
|
25368
|
+
if (match && match[1]) {
|
|
25369
|
+
return match[1];
|
|
25370
|
+
}
|
|
25371
|
+
if (host.includes("pooler.supabase.com")) {
|
|
25372
|
+
const username = url.username;
|
|
25373
|
+
const userMatch = username.match(/^postgres\.([a-z0-9]+)$/i);
|
|
25374
|
+
if (userMatch && userMatch[1]) {
|
|
25375
|
+
return userMatch[1];
|
|
25376
|
+
}
|
|
25377
|
+
}
|
|
25378
|
+
const poolerMatch = host.match(/^([a-z0-9]+)\.pooler\.supabase\.com$/i);
|
|
25379
|
+
if (poolerMatch && poolerMatch[1] && !poolerMatch[1].startsWith("aws-")) {
|
|
25380
|
+
return poolerMatch[1];
|
|
25381
|
+
}
|
|
25382
|
+
return;
|
|
25383
|
+
} catch {
|
|
25384
|
+
return;
|
|
25385
|
+
}
|
|
25386
|
+
}
|
|
25387
|
+
async function applyInitPlanViaSupabase(params) {
|
|
24307
25388
|
const applied = [];
|
|
24308
25389
|
const skippedOptional = [];
|
|
24309
25390
|
const executeStep = async (step) => {
|
|
24310
|
-
|
|
24311
|
-
|
|
24312
|
-
|
|
24313
|
-
|
|
24314
|
-
} catch (e) {
|
|
24315
|
-
try {
|
|
24316
|
-
await params.client.query("rollback;");
|
|
24317
|
-
} catch {}
|
|
24318
|
-
throw e;
|
|
24319
|
-
}
|
|
25391
|
+
const wrappedSql = `BEGIN;
|
|
25392
|
+
${step.sql}
|
|
25393
|
+
COMMIT;`;
|
|
25394
|
+
await params.client.query(wrappedSql, false);
|
|
24320
25395
|
};
|
|
24321
25396
|
for (const step of params.plan.steps.filter((s) => !s.optional)) {
|
|
24322
25397
|
try {
|
|
25398
|
+
if (params.verbose) {
|
|
25399
|
+
console.log(`Executing step: ${step.name}`);
|
|
25400
|
+
}
|
|
24323
25401
|
await executeStep(step);
|
|
24324
25402
|
applied.push(step.name);
|
|
24325
25403
|
} catch (e) {
|
|
@@ -24341,12 +25419,13 @@ async function applyInitPlan(params) {
|
|
|
24341
25419
|
"constraint",
|
|
24342
25420
|
"file",
|
|
24343
25421
|
"line",
|
|
24344
|
-
"routine"
|
|
25422
|
+
"routine",
|
|
25423
|
+
"httpStatus",
|
|
25424
|
+
"supabaseErrorCode"
|
|
24345
25425
|
];
|
|
24346
|
-
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
wrapped[field] = errAny[field];
|
|
25426
|
+
for (const field of pgErrorFields) {
|
|
25427
|
+
if (errAny[field] !== undefined) {
|
|
25428
|
+
wrapped[field] = errAny[field];
|
|
24350
25429
|
}
|
|
24351
25430
|
}
|
|
24352
25431
|
if (e instanceof Error && e.stack) {
|
|
@@ -24357,6 +25436,9 @@ async function applyInitPlan(params) {
|
|
|
24357
25436
|
}
|
|
24358
25437
|
for (const step of params.plan.steps.filter((s) => s.optional)) {
|
|
24359
25438
|
try {
|
|
25439
|
+
if (params.verbose) {
|
|
25440
|
+
console.log(`Executing optional step: ${step.name}`);
|
|
25441
|
+
}
|
|
24360
25442
|
await executeStep(step);
|
|
24361
25443
|
applied.push(step.name);
|
|
24362
25444
|
} catch {
|
|
@@ -24365,98 +25447,133 @@ async function applyInitPlan(params) {
|
|
|
24365
25447
|
}
|
|
24366
25448
|
return { applied, skippedOptional };
|
|
24367
25449
|
}
|
|
24368
|
-
async function
|
|
24369
|
-
|
|
24370
|
-
|
|
24371
|
-
|
|
24372
|
-
|
|
24373
|
-
|
|
24374
|
-
|
|
24375
|
-
|
|
24376
|
-
|
|
24377
|
-
|
|
24378
|
-
|
|
24379
|
-
|
|
24380
|
-
}
|
|
24381
|
-
|
|
24382
|
-
|
|
24383
|
-
|
|
24384
|
-
}
|
|
24385
|
-
|
|
24386
|
-
|
|
24387
|
-
|
|
24388
|
-
|
|
24389
|
-
|
|
24390
|
-
|
|
24391
|
-
|
|
24392
|
-
|
|
24393
|
-
|
|
24394
|
-
|
|
25450
|
+
async function verifyInitSetupViaSupabase(params) {
|
|
25451
|
+
const missingRequired = [];
|
|
25452
|
+
const missingOptional = [];
|
|
25453
|
+
const role = params.monitoringUser;
|
|
25454
|
+
const db = params.database;
|
|
25455
|
+
if (!isValidIdentifier(role)) {
|
|
25456
|
+
throw new Error(`Invalid monitoring user name: "${role}". Must be a valid PostgreSQL identifier (letters, digits, underscores, max 63 chars, starting with letter or underscore).`);
|
|
25457
|
+
}
|
|
25458
|
+
const roleRes = await params.client.query(`SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
25459
|
+
const roleExists = roleRes.rowCount > 0;
|
|
25460
|
+
if (!roleExists) {
|
|
25461
|
+
missingRequired.push(`role "${role}" does not exist`);
|
|
25462
|
+
return { ok: false, missingRequired, missingOptional };
|
|
25463
|
+
}
|
|
25464
|
+
const connectRes = await params.client.query(`SELECT has_database_privilege('${escapeLiteral2(role)}', '${escapeLiteral2(db)}', 'CONNECT') as ok`, true);
|
|
25465
|
+
if (!connectRes.rows?.[0]?.ok) {
|
|
25466
|
+
missingRequired.push(`CONNECT on database "${db}"`);
|
|
25467
|
+
}
|
|
25468
|
+
const pgMonitorRes = await params.client.query(`SELECT pg_has_role('${escapeLiteral2(role)}', 'pg_monitor', 'member') as ok`, true);
|
|
25469
|
+
if (!pgMonitorRes.rows?.[0]?.ok) {
|
|
25470
|
+
missingRequired.push("membership in role pg_monitor");
|
|
25471
|
+
}
|
|
25472
|
+
const pgIndexRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'pg_catalog.pg_index', 'SELECT') as ok`, true);
|
|
25473
|
+
if (!pgIndexRes.rows?.[0]?.ok) {
|
|
25474
|
+
missingRequired.push("SELECT on pg_catalog.pg_index");
|
|
25475
|
+
}
|
|
25476
|
+
const schemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'postgres_ai'", true);
|
|
25477
|
+
if (schemaExistsRes.rowCount === 0) {
|
|
25478
|
+
missingRequired.push("schema postgres_ai exists");
|
|
25479
|
+
} else {
|
|
25480
|
+
const schemaPrivRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'postgres_ai', 'USAGE') as ok`, true);
|
|
25481
|
+
if (!schemaPrivRes.rows?.[0]?.ok) {
|
|
24395
25482
|
missingRequired.push("USAGE on schema postgres_ai");
|
|
24396
25483
|
}
|
|
24397
|
-
|
|
24398
|
-
|
|
24399
|
-
|
|
24400
|
-
|
|
24401
|
-
|
|
24402
|
-
|
|
24403
|
-
|
|
24404
|
-
|
|
25484
|
+
}
|
|
25485
|
+
const viewExistsRes = await params.client.query("SELECT to_regclass('postgres_ai.pg_statistic') IS NOT NULL as ok", true);
|
|
25486
|
+
if (!viewExistsRes.rows?.[0]?.ok) {
|
|
25487
|
+
missingRequired.push("view postgres_ai.pg_statistic exists");
|
|
25488
|
+
} else {
|
|
25489
|
+
const viewPrivRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'postgres_ai.pg_statistic', 'SELECT') as ok`, true);
|
|
25490
|
+
if (!viewPrivRes.rows?.[0]?.ok) {
|
|
25491
|
+
missingRequired.push("SELECT on view postgres_ai.pg_statistic");
|
|
24405
25492
|
}
|
|
24406
|
-
|
|
25493
|
+
}
|
|
25494
|
+
const publicSchemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'public'", true);
|
|
25495
|
+
if (publicSchemaExistsRes.rowCount === 0) {
|
|
25496
|
+
missingRequired.push("schema public exists");
|
|
25497
|
+
} else {
|
|
25498
|
+
const schemaUsageRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'public', 'USAGE') as ok`, true);
|
|
24407
25499
|
if (!schemaUsageRes.rows?.[0]?.ok) {
|
|
24408
25500
|
missingRequired.push("USAGE on schema public");
|
|
24409
25501
|
}
|
|
24410
|
-
|
|
24411
|
-
|
|
24412
|
-
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
24419
|
-
|
|
25502
|
+
}
|
|
25503
|
+
const rolcfgRes = await params.client.query(`SELECT rolconfig FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
25504
|
+
const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
|
|
25505
|
+
const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
|
|
25506
|
+
if (typeof spLine !== "string" || !spLine) {
|
|
25507
|
+
missingRequired.push("role search_path is set");
|
|
25508
|
+
} else {
|
|
25509
|
+
const sp = spLine.toLowerCase();
|
|
25510
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
25511
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
24420
25512
|
}
|
|
24421
|
-
|
|
25513
|
+
}
|
|
25514
|
+
const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
25515
|
+
if (explainFnExistsRes.rowCount === 0) {
|
|
25516
|
+
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
25517
|
+
} else {
|
|
25518
|
+
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
24422
25519
|
if (!explainFnRes.rows?.[0]?.ok) {
|
|
24423
25520
|
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
24424
25521
|
}
|
|
24425
|
-
|
|
25522
|
+
}
|
|
25523
|
+
const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
25524
|
+
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
25525
|
+
missingRequired.push("function postgres_ai.table_describe exists");
|
|
25526
|
+
} else {
|
|
25527
|
+
const tableDescribeFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.table_describe(text)', 'EXECUTE') as ok`, true);
|
|
24426
25528
|
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
24427
25529
|
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
24428
25530
|
}
|
|
24429
|
-
|
|
24430
|
-
|
|
24431
|
-
|
|
24432
|
-
|
|
24433
|
-
|
|
24434
|
-
|
|
24435
|
-
|
|
24436
|
-
|
|
24437
|
-
|
|
24438
|
-
|
|
25531
|
+
}
|
|
25532
|
+
if (params.includeOptionalPermissions) {
|
|
25533
|
+
const extRes = await params.client.query("SELECT 1 FROM pg_extension WHERE extname = 'rds_tools'", true);
|
|
25534
|
+
if (extRes.rowCount === 0) {
|
|
25535
|
+
missingOptional.push("extension rds_tools");
|
|
25536
|
+
} else {
|
|
25537
|
+
try {
|
|
25538
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok`, true);
|
|
25539
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
25540
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
24439
25541
|
}
|
|
25542
|
+
} catch {
|
|
25543
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
24440
25544
|
}
|
|
24441
|
-
|
|
24442
|
-
|
|
24443
|
-
|
|
24444
|
-
|
|
24445
|
-
|
|
24446
|
-
|
|
24447
|
-
|
|
24448
|
-
|
|
25545
|
+
}
|
|
25546
|
+
const optionalFns = [
|
|
25547
|
+
"pg_catalog.pg_stat_file(text)",
|
|
25548
|
+
"pg_catalog.pg_stat_file(text, boolean)",
|
|
25549
|
+
"pg_catalog.pg_ls_dir(text)",
|
|
25550
|
+
"pg_catalog.pg_ls_dir(text, boolean, boolean)"
|
|
25551
|
+
];
|
|
25552
|
+
for (const fn of optionalFns) {
|
|
25553
|
+
try {
|
|
25554
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', '${fn}', 'EXECUTE') as ok`, true);
|
|
24449
25555
|
if (!fnRes.rows?.[0]?.ok) {
|
|
24450
25556
|
missingOptional.push(`EXECUTE on ${fn}`);
|
|
24451
25557
|
}
|
|
25558
|
+
} catch {
|
|
25559
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
24452
25560
|
}
|
|
24453
25561
|
}
|
|
24454
|
-
return { ok: missingRequired.length === 0, missingRequired, missingOptional };
|
|
24455
|
-
} finally {
|
|
24456
|
-
try {
|
|
24457
|
-
await params.client.query("rollback;");
|
|
24458
|
-
} catch {}
|
|
24459
25562
|
}
|
|
25563
|
+
return {
|
|
25564
|
+
ok: missingRequired.length === 0,
|
|
25565
|
+
missingRequired,
|
|
25566
|
+
missingOptional
|
|
25567
|
+
};
|
|
25568
|
+
}
|
|
25569
|
+
function isValidIdentifier(name) {
|
|
25570
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(name);
|
|
25571
|
+
}
|
|
25572
|
+
function escapeLiteral2(value) {
|
|
25573
|
+
if (value.includes("\x00")) {
|
|
25574
|
+
throw new Error("SQL literal cannot contain null bytes");
|
|
25575
|
+
}
|
|
25576
|
+
return value.replace(/'/g, "''");
|
|
24460
25577
|
}
|
|
24461
25578
|
|
|
24462
25579
|
// lib/pkce.ts
|
|
@@ -24958,17 +26075,17 @@ where
|
|
|
24958
26075
|
statement_timeout_seconds: 300
|
|
24959
26076
|
},
|
|
24960
26077
|
pg_invalid_indexes: {
|
|
24961
|
-
description: "This metric identifies invalid indexes in the database. It provides insights into
|
|
26078
|
+
description: "This metric identifies invalid indexes in the database with decision tree data for remediation. It provides insights into whether to DROP (if duplicate exists), RECREATE (if backs constraint), or flag as UNCERTAIN (if additional RCA is needed to check query plans). Decision tree: 1) Valid duplicate exists -> DROP, 2) Backs PK/UNIQUE constraint -> RECREATE, 3) Table < 10K rows -> RECREATE (small tables rebuild quickly, typically under 1 second), 4) Otherwise -> UNCERTAIN (need query plan analysis to assess impact).",
|
|
24962
26079
|
sqls: {
|
|
24963
26080
|
11: `with fk_indexes as ( /* pgwatch_generated */
|
|
24964
26081
|
select
|
|
24965
|
-
schemaname as
|
|
24966
|
-
|
|
24967
|
-
(
|
|
24968
|
-
(
|
|
24969
|
-
|
|
24970
|
-
|
|
24971
|
-
|
|
26082
|
+
schemaname as schema_name,
|
|
26083
|
+
indexrelid,
|
|
26084
|
+
(indexrelid::regclass)::text as index_name,
|
|
26085
|
+
(relid::regclass)::text as table_name,
|
|
26086
|
+
(confrelid::regclass)::text as fk_table_ref,
|
|
26087
|
+
array_to_string(indclass, ', ') as opclasses
|
|
26088
|
+
from pg_stat_all_indexes
|
|
24972
26089
|
join pg_index using (indexrelid)
|
|
24973
26090
|
left join pg_constraint
|
|
24974
26091
|
on array_to_string(indkey, ',') = array_to_string(conkey, ',')
|
|
@@ -24977,37 +26094,58 @@ where
|
|
|
24977
26094
|
and contype = 'f'
|
|
24978
26095
|
where idx_scan = 0
|
|
24979
26096
|
and indisunique is false
|
|
24980
|
-
and conkey is not null
|
|
24981
|
-
),
|
|
26097
|
+
and conkey is not null
|
|
26098
|
+
),
|
|
26099
|
+
-- Find valid indexes that could be duplicates (same table, same columns)
|
|
26100
|
+
valid_duplicates as (
|
|
26101
|
+
select
|
|
26102
|
+
inv.indexrelid as invalid_indexrelid,
|
|
26103
|
+
val.indexrelid as valid_indexrelid,
|
|
26104
|
+
(val.indexrelid::regclass)::text as valid_index_name,
|
|
26105
|
+
pg_get_indexdef(val.indexrelid) as valid_index_definition
|
|
26106
|
+
from pg_index inv
|
|
26107
|
+
join pg_index val on inv.indrelid = val.indrelid -- same table
|
|
26108
|
+
and inv.indkey = val.indkey -- same columns (in same order)
|
|
26109
|
+
and inv.indexrelid != val.indexrelid -- different index
|
|
26110
|
+
and val.indisvalid = true -- valid index
|
|
26111
|
+
where inv.indisvalid = false
|
|
26112
|
+
),
|
|
26113
|
+
data as (
|
|
24982
26114
|
select
|
|
24983
26115
|
pci.relname as tag_index_name,
|
|
24984
26116
|
pn.nspname as tag_schema_name,
|
|
24985
26117
|
pct.relname as tag_table_name,
|
|
24986
|
-
quote_ident(pn.nspname) as tag_schema_name,
|
|
24987
|
-
quote_ident(pci.relname) as tag_index_name,
|
|
24988
|
-
quote_ident(pct.relname) as tag_table_name,
|
|
24989
26118
|
coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
|
|
24990
26119
|
pg_get_indexdef(pidx.indexrelid) as index_definition,
|
|
24991
|
-
pg_relation_size(pidx.indexrelid) index_size_bytes,
|
|
26120
|
+
pg_relation_size(pidx.indexrelid) as index_size_bytes,
|
|
26121
|
+
-- Constraint info
|
|
26122
|
+
pidx.indisprimary as is_pk,
|
|
26123
|
+
pidx.indisunique as is_unique,
|
|
26124
|
+
con.conname as constraint_name,
|
|
26125
|
+
-- Table row estimate
|
|
26126
|
+
pct.reltuples::bigint as table_row_estimate,
|
|
26127
|
+
-- Valid duplicate check
|
|
26128
|
+
(vd.valid_indexrelid is not null) as has_valid_duplicate,
|
|
26129
|
+
vd.valid_index_name,
|
|
26130
|
+
vd.valid_index_definition,
|
|
26131
|
+
-- FK support check
|
|
24992
26132
|
((
|
|
24993
26133
|
select count(1)
|
|
24994
26134
|
from fk_indexes fi
|
|
24995
|
-
where
|
|
24996
|
-
fi.
|
|
24997
|
-
and fi.tag_opclasses like (array_to_string(pidx.indclass, ', ') || '%')
|
|
26135
|
+
where fi.fk_table_ref = pct.relname
|
|
26136
|
+
and fi.opclasses like (array_to_string(pidx.indclass, ', ') || '%')
|
|
24998
26137
|
) > 0)::int as supports_fk
|
|
24999
26138
|
from pg_index pidx
|
|
25000
|
-
join pg_class
|
|
25001
|
-
join pg_class
|
|
26139
|
+
join pg_class pci on pci.oid = pidx.indexrelid
|
|
26140
|
+
join pg_class pct on pct.oid = pidx.indrelid
|
|
25002
26141
|
left join pg_namespace pn on pn.oid = pct.relnamespace
|
|
26142
|
+
left join pg_constraint con on con.conindid = pidx.indexrelid
|
|
26143
|
+
left join valid_duplicates vd on vd.invalid_indexrelid = pidx.indexrelid
|
|
25003
26144
|
where pidx.indisvalid = false
|
|
25004
|
-
),
|
|
25005
|
-
|
|
25006
|
-
sum(index_size_bytes) as index_size_bytes_sum
|
|
25007
|
-
from data
|
|
25008
|
-
), num_data as (
|
|
26145
|
+
),
|
|
26146
|
+
num_data as (
|
|
25009
26147
|
select
|
|
25010
|
-
row_number() over () num,
|
|
26148
|
+
row_number() over () as num,
|
|
25011
26149
|
data.*
|
|
25012
26150
|
from data
|
|
25013
26151
|
)
|
|
@@ -25626,7 +26764,14 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
|
|
|
25626
26764
|
index_size_bytes: indexSizeBytes,
|
|
25627
26765
|
index_size_pretty: formatBytes(indexSizeBytes),
|
|
25628
26766
|
index_definition: String(transformed.index_definition || ""),
|
|
25629
|
-
supports_fk: toBool(transformed.supports_fk)
|
|
26767
|
+
supports_fk: toBool(transformed.supports_fk),
|
|
26768
|
+
is_pk: toBool(transformed.is_pk),
|
|
26769
|
+
is_unique: toBool(transformed.is_unique),
|
|
26770
|
+
constraint_name: transformed.constraint_name ? String(transformed.constraint_name) : null,
|
|
26771
|
+
table_row_estimate: parseInt(String(transformed.table_row_estimate || 0), 10),
|
|
26772
|
+
has_valid_duplicate: toBool(transformed.has_valid_duplicate),
|
|
26773
|
+
valid_duplicate_name: transformed.valid_index_name ? String(transformed.valid_index_name) : null,
|
|
26774
|
+
valid_duplicate_definition: transformed.valid_index_definition ? String(transformed.valid_index_definition) : null
|
|
25630
26775
|
};
|
|
25631
26776
|
});
|
|
25632
26777
|
}
|
|
@@ -26715,7 +27860,7 @@ program2.command("set-default-project <project>").description("store default pro
|
|
|
26715
27860
|
writeConfig({ defaultProject: value });
|
|
26716
27861
|
console.log(`Default project saved: ${value}`);
|
|
26717
27862
|
});
|
|
26718
|
-
program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).addHelpText("after", [
|
|
27863
|
+
program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.").option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).option("--supabase", "Use Supabase Management API instead of direct PostgreSQL connection", false).option("--supabase-access-token <token>", "Supabase Management API access token (or SUPABASE_ACCESS_TOKEN env)").option("--supabase-project-ref <ref>", "Supabase project reference (or SUPABASE_PROJECT_REF env)").option("--json", "Output result as JSON (machine-readable)", false).addHelpText("after", [
|
|
26719
27864
|
"",
|
|
26720
27865
|
"Examples:",
|
|
26721
27866
|
" postgresai prepare-db postgresql://admin@host:5432/dbname",
|
|
@@ -26751,21 +27896,60 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26751
27896
|
" postgresai prepare-db <conn> --reset-password --password '...'",
|
|
26752
27897
|
"",
|
|
26753
27898
|
"Offline SQL plan (no DB connection):",
|
|
26754
|
-
" postgresai prepare-db --print-sql"
|
|
27899
|
+
" postgresai prepare-db --print-sql",
|
|
27900
|
+
"",
|
|
27901
|
+
"Supabase mode (use Management API instead of direct connection):",
|
|
27902
|
+
" postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27903
|
+
" SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27904
|
+
"",
|
|
27905
|
+
" Generate a token at: https://supabase.com/dashboard/account/tokens",
|
|
27906
|
+
" Find your project ref in: https://supabase.com/dashboard/project/<ref>",
|
|
27907
|
+
"",
|
|
27908
|
+
"Provider-specific behavior (for direct connections):",
|
|
27909
|
+
" --provider supabase Skip role creation (create user in Supabase dashboard)",
|
|
27910
|
+
" Skip ALTER USER (restricted by Supabase)"
|
|
26755
27911
|
].join(`
|
|
26756
27912
|
`)).action(async (conn, opts, cmd) => {
|
|
26757
|
-
|
|
26758
|
-
|
|
27913
|
+
const jsonOutput = opts.json;
|
|
27914
|
+
const outputJson = (data) => {
|
|
27915
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27916
|
+
};
|
|
27917
|
+
const outputError = (error2) => {
|
|
27918
|
+
if (jsonOutput) {
|
|
27919
|
+
outputJson({
|
|
27920
|
+
success: false,
|
|
27921
|
+
mode: opts.supabase ? "supabase" : "direct",
|
|
27922
|
+
error: error2
|
|
27923
|
+
});
|
|
27924
|
+
} else {
|
|
27925
|
+
console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error2.message}`);
|
|
27926
|
+
if (error2.step)
|
|
27927
|
+
console.error(` Step: ${error2.step}`);
|
|
27928
|
+
if (error2.code)
|
|
27929
|
+
console.error(` Code: ${error2.code}`);
|
|
27930
|
+
if (error2.detail)
|
|
27931
|
+
console.error(` Detail: ${error2.detail}`);
|
|
27932
|
+
if (error2.hint)
|
|
27933
|
+
console.error(` Hint: ${error2.hint}`);
|
|
27934
|
+
if (error2.httpStatus)
|
|
27935
|
+
console.error(` HTTP Status: ${error2.httpStatus}`);
|
|
27936
|
+
}
|
|
26759
27937
|
process.exitCode = 1;
|
|
27938
|
+
};
|
|
27939
|
+
if (opts.verify && opts.resetPassword) {
|
|
27940
|
+
outputError({ message: "Provide only one of --verify or --reset-password" });
|
|
26760
27941
|
return;
|
|
26761
27942
|
}
|
|
26762
27943
|
if (opts.verify && opts.printSql) {
|
|
26763
|
-
|
|
26764
|
-
process.exitCode = 1;
|
|
27944
|
+
outputError({ message: "--verify cannot be combined with --print-sql" });
|
|
26765
27945
|
return;
|
|
26766
27946
|
}
|
|
26767
27947
|
const shouldPrintSql = !!opts.printSql;
|
|
26768
27948
|
const redactPasswords = (sql) => redactPasswordsInSql(sql);
|
|
27949
|
+
const providerWarning = validateProvider(opts.provider);
|
|
27950
|
+
if (providerWarning) {
|
|
27951
|
+
console.warn(`\u26A0 ${providerWarning}`);
|
|
27952
|
+
}
|
|
26769
27953
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
26770
27954
|
if (shouldPrintSql) {
|
|
26771
27955
|
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
@@ -26775,12 +27959,14 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26775
27959
|
database,
|
|
26776
27960
|
monitoringUser: opts.monitoringUser,
|
|
26777
27961
|
monitoringPassword: monPassword,
|
|
26778
|
-
includeOptionalPermissions: includeOptionalPermissions2
|
|
27962
|
+
includeOptionalPermissions: includeOptionalPermissions2,
|
|
27963
|
+
provider: opts.provider
|
|
26779
27964
|
});
|
|
26780
27965
|
console.log(`
|
|
26781
27966
|
--- SQL plan (offline; not connected) ---`);
|
|
26782
27967
|
console.log(`-- database: ${database}`);
|
|
26783
27968
|
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
27969
|
+
console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
|
|
26784
27970
|
console.log(`-- optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
|
|
26785
27971
|
for (const step of plan.steps) {
|
|
26786
27972
|
console.log(`
|
|
@@ -26794,6 +27980,281 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26794
27980
|
return;
|
|
26795
27981
|
}
|
|
26796
27982
|
}
|
|
27983
|
+
if (opts.supabase) {
|
|
27984
|
+
let supabaseConfig;
|
|
27985
|
+
try {
|
|
27986
|
+
let projectRef = opts.supabaseProjectRef;
|
|
27987
|
+
if (!projectRef && conn) {
|
|
27988
|
+
projectRef = extractProjectRefFromUrl(conn);
|
|
27989
|
+
}
|
|
27990
|
+
supabaseConfig = resolveSupabaseConfig({
|
|
27991
|
+
accessToken: opts.supabaseAccessToken,
|
|
27992
|
+
projectRef
|
|
27993
|
+
});
|
|
27994
|
+
} catch (e) {
|
|
27995
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
27996
|
+
outputError({ message: msg });
|
|
27997
|
+
return;
|
|
27998
|
+
}
|
|
27999
|
+
const includeOptionalPermissions2 = !opts.skipOptionalPermissions;
|
|
28000
|
+
if (!jsonOutput) {
|
|
28001
|
+
console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
|
|
28002
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
28003
|
+
console.log(`Optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
|
|
28004
|
+
}
|
|
28005
|
+
const supabaseClient = new SupabaseClient(supabaseConfig);
|
|
28006
|
+
let databaseUrl = null;
|
|
28007
|
+
if (jsonOutput) {
|
|
28008
|
+
databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
|
|
28009
|
+
}
|
|
28010
|
+
try {
|
|
28011
|
+
const database = await supabaseClient.getCurrentDatabase();
|
|
28012
|
+
if (!database) {
|
|
28013
|
+
throw new Error("Failed to resolve current database name");
|
|
28014
|
+
}
|
|
28015
|
+
if (!jsonOutput) {
|
|
28016
|
+
console.log(`Database: ${database}`);
|
|
28017
|
+
}
|
|
28018
|
+
if (opts.verify) {
|
|
28019
|
+
const v = await verifyInitSetupViaSupabase({
|
|
28020
|
+
client: supabaseClient,
|
|
28021
|
+
database,
|
|
28022
|
+
monitoringUser: opts.monitoringUser,
|
|
28023
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
28024
|
+
});
|
|
28025
|
+
if (v.ok) {
|
|
28026
|
+
if (jsonOutput) {
|
|
28027
|
+
const result = {
|
|
28028
|
+
success: true,
|
|
28029
|
+
mode: "supabase",
|
|
28030
|
+
action: "verify",
|
|
28031
|
+
database,
|
|
28032
|
+
monitoringUser: opts.monitoringUser,
|
|
28033
|
+
verified: true,
|
|
28034
|
+
missingOptional: v.missingOptional
|
|
28035
|
+
};
|
|
28036
|
+
if (databaseUrl) {
|
|
28037
|
+
result.databaseUrl = databaseUrl;
|
|
28038
|
+
}
|
|
28039
|
+
outputJson(result);
|
|
28040
|
+
} else {
|
|
28041
|
+
console.log("\u2713 prepare-db verify: OK");
|
|
28042
|
+
if (v.missingOptional.length > 0) {
|
|
28043
|
+
console.log("\u26A0 Optional items missing:");
|
|
28044
|
+
for (const m of v.missingOptional)
|
|
28045
|
+
console.log(`- ${m}`);
|
|
28046
|
+
}
|
|
28047
|
+
}
|
|
28048
|
+
return;
|
|
28049
|
+
}
|
|
28050
|
+
if (jsonOutput) {
|
|
28051
|
+
const result = {
|
|
28052
|
+
success: false,
|
|
28053
|
+
mode: "supabase",
|
|
28054
|
+
action: "verify",
|
|
28055
|
+
database,
|
|
28056
|
+
monitoringUser: opts.monitoringUser,
|
|
28057
|
+
verified: false,
|
|
28058
|
+
missingRequired: v.missingRequired,
|
|
28059
|
+
missingOptional: v.missingOptional
|
|
28060
|
+
};
|
|
28061
|
+
if (databaseUrl) {
|
|
28062
|
+
result.databaseUrl = databaseUrl;
|
|
28063
|
+
}
|
|
28064
|
+
outputJson(result);
|
|
28065
|
+
} else {
|
|
28066
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
28067
|
+
for (const m of v.missingRequired)
|
|
28068
|
+
console.error(`- ${m}`);
|
|
28069
|
+
if (v.missingOptional.length > 0) {
|
|
28070
|
+
console.error("Optional items missing:");
|
|
28071
|
+
for (const m of v.missingOptional)
|
|
28072
|
+
console.error(`- ${m}`);
|
|
28073
|
+
}
|
|
28074
|
+
}
|
|
28075
|
+
process.exitCode = 1;
|
|
28076
|
+
return;
|
|
28077
|
+
}
|
|
28078
|
+
let monPassword;
|
|
28079
|
+
let passwordGenerated = false;
|
|
28080
|
+
try {
|
|
28081
|
+
const resolved = await resolveMonitoringPassword({
|
|
28082
|
+
passwordFlag: opts.password,
|
|
28083
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
28084
|
+
monitoringUser: opts.monitoringUser
|
|
28085
|
+
});
|
|
28086
|
+
monPassword = resolved.password;
|
|
28087
|
+
passwordGenerated = resolved.generated;
|
|
28088
|
+
if (resolved.generated) {
|
|
28089
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
28090
|
+
if (canPrint) {
|
|
28091
|
+
if (!jsonOutput) {
|
|
28092
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
28093
|
+
console.error("");
|
|
28094
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
28095
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
28096
|
+
console.error("");
|
|
28097
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
28098
|
+
}
|
|
28099
|
+
} else {
|
|
28100
|
+
console.error([
|
|
28101
|
+
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
28102
|
+
"",
|
|
28103
|
+
"Provide it explicitly:",
|
|
28104
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
28105
|
+
"",
|
|
28106
|
+
"Or (NOT recommended) print the generated password:",
|
|
28107
|
+
" --print-password"
|
|
28108
|
+
].join(`
|
|
28109
|
+
`));
|
|
28110
|
+
process.exitCode = 1;
|
|
28111
|
+
return;
|
|
28112
|
+
}
|
|
28113
|
+
}
|
|
28114
|
+
} catch (e) {
|
|
28115
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
28116
|
+
outputError({ message: msg });
|
|
28117
|
+
return;
|
|
28118
|
+
}
|
|
28119
|
+
const plan = await buildInitPlan({
|
|
28120
|
+
database,
|
|
28121
|
+
monitoringUser: opts.monitoringUser,
|
|
28122
|
+
monitoringPassword: monPassword,
|
|
28123
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
28124
|
+
});
|
|
28125
|
+
const supabaseApplicableSteps = plan.steps.filter((s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed");
|
|
28126
|
+
const effectivePlan = opts.resetPassword ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") } : { ...plan, steps: supabaseApplicableSteps };
|
|
28127
|
+
if (shouldPrintSql) {
|
|
28128
|
+
console.log(`
|
|
28129
|
+
--- SQL plan ---`);
|
|
28130
|
+
for (const step of effectivePlan.steps) {
|
|
28131
|
+
console.log(`
|
|
28132
|
+
-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
28133
|
+
console.log(redactPasswords(step.sql));
|
|
28134
|
+
}
|
|
28135
|
+
console.log(`
|
|
28136
|
+
--- end SQL plan ---
|
|
28137
|
+
`);
|
|
28138
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
28139
|
+
return;
|
|
28140
|
+
}
|
|
28141
|
+
const { applied, skippedOptional } = await applyInitPlanViaSupabase({
|
|
28142
|
+
client: supabaseClient,
|
|
28143
|
+
plan: effectivePlan
|
|
28144
|
+
});
|
|
28145
|
+
if (jsonOutput) {
|
|
28146
|
+
const result = {
|
|
28147
|
+
success: true,
|
|
28148
|
+
mode: "supabase",
|
|
28149
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
28150
|
+
database,
|
|
28151
|
+
monitoringUser: opts.monitoringUser,
|
|
28152
|
+
applied,
|
|
28153
|
+
skippedOptional,
|
|
28154
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
28155
|
+
};
|
|
28156
|
+
if (passwordGenerated) {
|
|
28157
|
+
result.generatedPassword = monPassword;
|
|
28158
|
+
}
|
|
28159
|
+
if (databaseUrl) {
|
|
28160
|
+
result.databaseUrl = databaseUrl;
|
|
28161
|
+
}
|
|
28162
|
+
outputJson(result);
|
|
28163
|
+
} else {
|
|
28164
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
28165
|
+
if (skippedOptional.length > 0) {
|
|
28166
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
28167
|
+
for (const s of skippedOptional)
|
|
28168
|
+
console.log(`- ${s}`);
|
|
28169
|
+
}
|
|
28170
|
+
if (process.stdout.isTTY) {
|
|
28171
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28172
|
+
}
|
|
28173
|
+
}
|
|
28174
|
+
} catch (error2) {
|
|
28175
|
+
const errAny = error2;
|
|
28176
|
+
let message = "";
|
|
28177
|
+
if (error2 instanceof Error && error2.message) {
|
|
28178
|
+
message = error2.message;
|
|
28179
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
28180
|
+
message = errAny.message;
|
|
28181
|
+
} else {
|
|
28182
|
+
message = String(error2);
|
|
28183
|
+
}
|
|
28184
|
+
if (!message || message === "[object Object]") {
|
|
28185
|
+
message = "Unknown error";
|
|
28186
|
+
}
|
|
28187
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
28188
|
+
const failedStep = stepMatch?.[1];
|
|
28189
|
+
const errorObj = { message };
|
|
28190
|
+
if (failedStep)
|
|
28191
|
+
errorObj.step = failedStep;
|
|
28192
|
+
if (errAny && typeof errAny === "object") {
|
|
28193
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
28194
|
+
errorObj.code = errAny.code;
|
|
28195
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
28196
|
+
errorObj.detail = errAny.detail;
|
|
28197
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
28198
|
+
errorObj.hint = errAny.hint;
|
|
28199
|
+
if (typeof errAny.httpStatus === "number")
|
|
28200
|
+
errorObj.httpStatus = errAny.httpStatus;
|
|
28201
|
+
}
|
|
28202
|
+
if (jsonOutput) {
|
|
28203
|
+
outputJson({
|
|
28204
|
+
success: false,
|
|
28205
|
+
mode: "supabase",
|
|
28206
|
+
error: errorObj
|
|
28207
|
+
});
|
|
28208
|
+
process.exitCode = 1;
|
|
28209
|
+
} else {
|
|
28210
|
+
console.error(`Error: prepare-db (Supabase): ${message}`);
|
|
28211
|
+
if (failedStep) {
|
|
28212
|
+
console.error(` Step: ${failedStep}`);
|
|
28213
|
+
}
|
|
28214
|
+
if (errAny && typeof errAny === "object") {
|
|
28215
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
28216
|
+
console.error(` Code: ${errAny.code}`);
|
|
28217
|
+
}
|
|
28218
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
28219
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
28220
|
+
}
|
|
28221
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
28222
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
28223
|
+
}
|
|
28224
|
+
if (typeof errAny.httpStatus === "number") {
|
|
28225
|
+
console.error(` HTTP Status: ${errAny.httpStatus}`);
|
|
28226
|
+
}
|
|
28227
|
+
}
|
|
28228
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
28229
|
+
if (errAny.code === "42501") {
|
|
28230
|
+
if (failedStep === "01.role") {
|
|
28231
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
28232
|
+
} else if (failedStep === "02.permissions") {
|
|
28233
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
28234
|
+
}
|
|
28235
|
+
console.error(" Fix: ensure your Supabase access token has sufficient permissions");
|
|
28236
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
28237
|
+
}
|
|
28238
|
+
}
|
|
28239
|
+
if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
|
|
28240
|
+
if (errAny.httpStatus === 401) {
|
|
28241
|
+
console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
|
|
28242
|
+
}
|
|
28243
|
+
if (errAny.httpStatus === 403) {
|
|
28244
|
+
console.error(" Hint: access denied; check your token permissions and project access");
|
|
28245
|
+
}
|
|
28246
|
+
if (errAny.httpStatus === 404) {
|
|
28247
|
+
console.error(" Hint: project not found; verify the project reference is correct");
|
|
28248
|
+
}
|
|
28249
|
+
if (errAny.httpStatus === 429) {
|
|
28250
|
+
console.error(" Hint: rate limited; wait a moment and try again");
|
|
28251
|
+
}
|
|
28252
|
+
}
|
|
28253
|
+
process.exitCode = 1;
|
|
28254
|
+
}
|
|
28255
|
+
}
|
|
28256
|
+
return;
|
|
28257
|
+
}
|
|
26797
28258
|
let adminConn;
|
|
26798
28259
|
try {
|
|
26799
28260
|
adminConn = resolveAdminConnection({
|
|
@@ -26808,18 +28269,24 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26808
28269
|
});
|
|
26809
28270
|
} catch (e) {
|
|
26810
28271
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26811
|
-
|
|
26812
|
-
|
|
26813
|
-
|
|
26814
|
-
|
|
28272
|
+
if (jsonOutput) {
|
|
28273
|
+
outputError({ message: msg });
|
|
28274
|
+
} else {
|
|
28275
|
+
console.error(`Error: prepare-db: ${msg}`);
|
|
28276
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
28277
|
+
console.error("");
|
|
28278
|
+
cmd.outputHelp({ error: true });
|
|
28279
|
+
}
|
|
28280
|
+
process.exitCode = 1;
|
|
26815
28281
|
}
|
|
26816
|
-
process.exitCode = 1;
|
|
26817
28282
|
return;
|
|
26818
28283
|
}
|
|
26819
28284
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
26820
|
-
|
|
26821
|
-
|
|
26822
|
-
|
|
28285
|
+
if (!jsonOutput) {
|
|
28286
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
28287
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
28288
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
28289
|
+
}
|
|
26823
28290
|
let client;
|
|
26824
28291
|
try {
|
|
26825
28292
|
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
@@ -26834,29 +28301,57 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26834
28301
|
client,
|
|
26835
28302
|
database,
|
|
26836
28303
|
monitoringUser: opts.monitoringUser,
|
|
26837
|
-
includeOptionalPermissions
|
|
28304
|
+
includeOptionalPermissions,
|
|
28305
|
+
provider: opts.provider
|
|
26838
28306
|
});
|
|
26839
28307
|
if (v.ok) {
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
26843
|
-
|
|
26844
|
-
|
|
28308
|
+
if (jsonOutput) {
|
|
28309
|
+
outputJson({
|
|
28310
|
+
success: true,
|
|
28311
|
+
mode: "direct",
|
|
28312
|
+
action: "verify",
|
|
28313
|
+
database,
|
|
28314
|
+
monitoringUser: opts.monitoringUser,
|
|
28315
|
+
provider: opts.provider,
|
|
28316
|
+
verified: true,
|
|
28317
|
+
missingOptional: v.missingOptional
|
|
28318
|
+
});
|
|
28319
|
+
} else {
|
|
28320
|
+
console.log(`\u2713 prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
28321
|
+
if (v.missingOptional.length > 0) {
|
|
28322
|
+
console.log("\u26A0 Optional items missing:");
|
|
28323
|
+
for (const m of v.missingOptional)
|
|
28324
|
+
console.log(`- ${m}`);
|
|
28325
|
+
}
|
|
26845
28326
|
}
|
|
26846
28327
|
return;
|
|
26847
28328
|
}
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
26851
|
-
|
|
26852
|
-
|
|
26853
|
-
|
|
28329
|
+
if (jsonOutput) {
|
|
28330
|
+
outputJson({
|
|
28331
|
+
success: false,
|
|
28332
|
+
mode: "direct",
|
|
28333
|
+
action: "verify",
|
|
28334
|
+
database,
|
|
28335
|
+
monitoringUser: opts.monitoringUser,
|
|
28336
|
+
verified: false,
|
|
28337
|
+
missingRequired: v.missingRequired,
|
|
28338
|
+
missingOptional: v.missingOptional
|
|
28339
|
+
});
|
|
28340
|
+
} else {
|
|
28341
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
28342
|
+
for (const m of v.missingRequired)
|
|
26854
28343
|
console.error(`- ${m}`);
|
|
28344
|
+
if (v.missingOptional.length > 0) {
|
|
28345
|
+
console.error("Optional items missing:");
|
|
28346
|
+
for (const m of v.missingOptional)
|
|
28347
|
+
console.error(`- ${m}`);
|
|
28348
|
+
}
|
|
26855
28349
|
}
|
|
26856
28350
|
process.exitCode = 1;
|
|
26857
28351
|
return;
|
|
26858
28352
|
}
|
|
26859
28353
|
let monPassword;
|
|
28354
|
+
let passwordGenerated = false;
|
|
26860
28355
|
try {
|
|
26861
28356
|
const resolved = await resolveMonitoringPassword({
|
|
26862
28357
|
passwordFlag: opts.password,
|
|
@@ -26864,15 +28359,18 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26864
28359
|
monitoringUser: opts.monitoringUser
|
|
26865
28360
|
});
|
|
26866
28361
|
monPassword = resolved.password;
|
|
28362
|
+
passwordGenerated = resolved.generated;
|
|
26867
28363
|
if (resolved.generated) {
|
|
26868
|
-
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
28364
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
26869
28365
|
if (canPrint) {
|
|
26870
|
-
|
|
26871
|
-
|
|
26872
|
-
|
|
26873
|
-
|
|
26874
|
-
|
|
26875
|
-
|
|
28366
|
+
if (!jsonOutput) {
|
|
28367
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
28368
|
+
console.error("");
|
|
28369
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
28370
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
28371
|
+
console.error("");
|
|
28372
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
28373
|
+
}
|
|
26876
28374
|
} else {
|
|
26877
28375
|
console.error([
|
|
26878
28376
|
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
@@ -26890,17 +28388,22 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26890
28388
|
}
|
|
26891
28389
|
} catch (e) {
|
|
26892
28390
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26893
|
-
|
|
26894
|
-
process.exitCode = 1;
|
|
28391
|
+
outputError({ message: msg });
|
|
26895
28392
|
return;
|
|
26896
28393
|
}
|
|
26897
28394
|
const plan = await buildInitPlan({
|
|
26898
28395
|
database,
|
|
26899
28396
|
monitoringUser: opts.monitoringUser,
|
|
26900
28397
|
monitoringPassword: monPassword,
|
|
26901
|
-
includeOptionalPermissions
|
|
28398
|
+
includeOptionalPermissions,
|
|
28399
|
+
provider: opts.provider
|
|
26902
28400
|
});
|
|
26903
28401
|
const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
|
|
28402
|
+
if (opts.resetPassword && effectivePlan.steps.length === 0) {
|
|
28403
|
+
console.error(`\u2717 --reset-password not supported for provider "${opts.provider}" (role creation is skipped)`);
|
|
28404
|
+
process.exitCode = 1;
|
|
28405
|
+
return;
|
|
28406
|
+
}
|
|
26904
28407
|
if (shouldPrintSql) {
|
|
26905
28408
|
console.log(`
|
|
26906
28409
|
--- SQL plan ---`);
|
|
@@ -26916,14 +28419,31 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26916
28419
|
return;
|
|
26917
28420
|
}
|
|
26918
28421
|
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
26919
|
-
|
|
26920
|
-
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
28422
|
+
if (jsonOutput) {
|
|
28423
|
+
const result = {
|
|
28424
|
+
success: true,
|
|
28425
|
+
mode: "direct",
|
|
28426
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
28427
|
+
database,
|
|
28428
|
+
monitoringUser: opts.monitoringUser,
|
|
28429
|
+
applied,
|
|
28430
|
+
skippedOptional,
|
|
28431
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
28432
|
+
};
|
|
28433
|
+
if (passwordGenerated) {
|
|
28434
|
+
result.generatedPassword = monPassword;
|
|
28435
|
+
}
|
|
28436
|
+
outputJson(result);
|
|
28437
|
+
} else {
|
|
28438
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
28439
|
+
if (skippedOptional.length > 0) {
|
|
28440
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
28441
|
+
for (const s of skippedOptional)
|
|
28442
|
+
console.log(`- ${s}`);
|
|
28443
|
+
}
|
|
28444
|
+
if (process.stdout.isTTY) {
|
|
28445
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28446
|
+
}
|
|
26927
28447
|
}
|
|
26928
28448
|
} catch (error2) {
|
|
26929
28449
|
const errAny = error2;
|
|
@@ -26938,45 +28458,65 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26938
28458
|
if (!message || message === "[object Object]") {
|
|
26939
28459
|
message = "Unknown error";
|
|
26940
28460
|
}
|
|
26941
|
-
console.error(`Error: prepare-db: ${message}`);
|
|
26942
28461
|
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
26943
28462
|
const failedStep = stepMatch?.[1];
|
|
26944
|
-
|
|
26945
|
-
|
|
26946
|
-
|
|
28463
|
+
const errorObj = { message };
|
|
28464
|
+
if (failedStep)
|
|
28465
|
+
errorObj.step = failedStep;
|
|
26947
28466
|
if (errAny && typeof errAny === "object") {
|
|
26948
|
-
if (typeof errAny.code === "string" && errAny.code)
|
|
26949
|
-
|
|
26950
|
-
|
|
26951
|
-
|
|
26952
|
-
|
|
26953
|
-
|
|
26954
|
-
|
|
26955
|
-
|
|
28467
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
28468
|
+
errorObj.code = errAny.code;
|
|
28469
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
28470
|
+
errorObj.detail = errAny.detail;
|
|
28471
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
28472
|
+
errorObj.hint = errAny.hint;
|
|
28473
|
+
}
|
|
28474
|
+
if (jsonOutput) {
|
|
28475
|
+
outputJson({
|
|
28476
|
+
success: false,
|
|
28477
|
+
mode: "direct",
|
|
28478
|
+
error: errorObj
|
|
28479
|
+
});
|
|
28480
|
+
process.exitCode = 1;
|
|
28481
|
+
} else {
|
|
28482
|
+
console.error(`Error: prepare-db: ${message}`);
|
|
28483
|
+
if (failedStep) {
|
|
28484
|
+
console.error(` Step: ${failedStep}`);
|
|
26956
28485
|
}
|
|
26957
|
-
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
28486
|
+
if (errAny && typeof errAny === "object") {
|
|
28487
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
28488
|
+
console.error(` Code: ${errAny.code}`);
|
|
28489
|
+
}
|
|
28490
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
28491
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
28492
|
+
}
|
|
28493
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
28494
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
26964
28495
|
}
|
|
26965
|
-
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
26966
|
-
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
26967
|
-
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
26968
|
-
}
|
|
26969
|
-
if (errAny.code === "ECONNREFUSED") {
|
|
26970
|
-
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
26971
|
-
}
|
|
26972
|
-
if (errAny.code === "ENOTFOUND") {
|
|
26973
|
-
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
26974
28496
|
}
|
|
26975
|
-
if (errAny.code === "
|
|
26976
|
-
|
|
28497
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
28498
|
+
if (errAny.code === "42501") {
|
|
28499
|
+
if (failedStep === "01.role") {
|
|
28500
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
28501
|
+
} else if (failedStep === "02.permissions") {
|
|
28502
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
28503
|
+
}
|
|
28504
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
28505
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
28506
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
28507
|
+
}
|
|
28508
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
28509
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
28510
|
+
}
|
|
28511
|
+
if (errAny.code === "ENOTFOUND") {
|
|
28512
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
28513
|
+
}
|
|
28514
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
28515
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
28516
|
+
}
|
|
26977
28517
|
}
|
|
28518
|
+
process.exitCode = 1;
|
|
26978
28519
|
}
|
|
26979
|
-
process.exitCode = 1;
|
|
26980
28520
|
} finally {
|
|
26981
28521
|
if (client) {
|
|
26982
28522
|
try {
|
|
@@ -27213,14 +28753,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
27213
28753
|
console.log(`Project directory: ${projectDir}
|
|
27214
28754
|
`);
|
|
27215
28755
|
const envFile = path5.resolve(projectDir, ".env");
|
|
27216
|
-
let existingTag = null;
|
|
27217
28756
|
let existingRegistry = null;
|
|
27218
28757
|
let existingPassword = null;
|
|
27219
28758
|
if (fs5.existsSync(envFile)) {
|
|
27220
28759
|
const existingEnv = fs5.readFileSync(envFile, "utf8");
|
|
27221
|
-
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
27222
|
-
if (tagMatch)
|
|
27223
|
-
existingTag = tagMatch[1].trim();
|
|
27224
28760
|
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
27225
28761
|
if (registryMatch)
|
|
27226
28762
|
existingRegistry = registryMatch[1].trim();
|
|
@@ -27228,7 +28764,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
27228
28764
|
if (pwdMatch)
|
|
27229
28765
|
existingPassword = pwdMatch[1].trim();
|
|
27230
28766
|
}
|
|
27231
|
-
const imageTag = opts.tag ||
|
|
28767
|
+
const imageTag = opts.tag || package_default.version;
|
|
27232
28768
|
const envLines = [`PGAI_TAG=${imageTag}`];
|
|
27233
28769
|
if (existingRegistry) {
|
|
27234
28770
|
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
@@ -27565,6 +29101,9 @@ var MONITORING_CONTAINERS = [
|
|
|
27565
29101
|
"sources-generator",
|
|
27566
29102
|
"postgres-reports"
|
|
27567
29103
|
];
|
|
29104
|
+
var COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
29105
|
+
var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
29106
|
+
var NETWORK_CLEANUP_DELAY_MS = 2000;
|
|
27568
29107
|
async function removeOrphanedContainers() {
|
|
27569
29108
|
for (const container of MONITORING_CONTAINERS) {
|
|
27570
29109
|
try {
|
|
@@ -27573,7 +29112,18 @@ async function removeOrphanedContainers() {
|
|
|
27573
29112
|
}
|
|
27574
29113
|
}
|
|
27575
29114
|
mon.command("stop").description("stop monitoring services").action(async () => {
|
|
27576
|
-
|
|
29115
|
+
let code = await runCompose(["down", "--remove-orphans"]);
|
|
29116
|
+
if (code !== 0) {
|
|
29117
|
+
await removeOrphanedContainers();
|
|
29118
|
+
await new Promise((resolve6) => setTimeout(resolve6, NETWORK_CLEANUP_DELAY_MS));
|
|
29119
|
+
code = await runCompose(["down", "--remove-orphans"]);
|
|
29120
|
+
}
|
|
29121
|
+
if (code !== 0) {
|
|
29122
|
+
try {
|
|
29123
|
+
await execFilePromise("docker", ["network", "rm", DOCKER_NETWORK_NAME]);
|
|
29124
|
+
code = 0;
|
|
29125
|
+
} catch {}
|
|
29126
|
+
}
|
|
27577
29127
|
if (code !== 0)
|
|
27578
29128
|
process.exitCode = code;
|
|
27579
29129
|
});
|
|
@@ -28340,18 +29890,36 @@ function interpretEscapes2(str2) {
|
|
|
28340
29890
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\x00/g, "\\");
|
|
28341
29891
|
}
|
|
28342
29892
|
var issues = program2.command("issues").description("issues management");
|
|
28343
|
-
issues.command("list").description("list issues").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
29893
|
+
issues.command("list").description("list issues").option("--status <status>", "filter by status: open, closed, or all (default: all)").option("--limit <n>", "max number of issues to return (default: 20)", parseInt).option("--offset <n>", "number of issues to skip (default: 0)", parseInt).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
29894
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
28344
29895
|
try {
|
|
28345
29896
|
const rootOpts = program2.opts();
|
|
28346
29897
|
const cfg = readConfig();
|
|
28347
29898
|
const { apiKey } = getConfig(rootOpts);
|
|
28348
29899
|
if (!apiKey) {
|
|
29900
|
+
spinner.stop();
|
|
28349
29901
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28350
29902
|
process.exitCode = 1;
|
|
28351
29903
|
return;
|
|
28352
29904
|
}
|
|
29905
|
+
const orgId = cfg.orgId ?? undefined;
|
|
28353
29906
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28354
|
-
|
|
29907
|
+
let statusFilter;
|
|
29908
|
+
if (opts.status === "open") {
|
|
29909
|
+
statusFilter = "open";
|
|
29910
|
+
} else if (opts.status === "closed") {
|
|
29911
|
+
statusFilter = "closed";
|
|
29912
|
+
}
|
|
29913
|
+
const result = await fetchIssues2({
|
|
29914
|
+
apiKey,
|
|
29915
|
+
apiBaseUrl,
|
|
29916
|
+
orgId,
|
|
29917
|
+
status: statusFilter,
|
|
29918
|
+
limit: opts.limit,
|
|
29919
|
+
offset: opts.offset,
|
|
29920
|
+
debug: !!opts.debug
|
|
29921
|
+
});
|
|
29922
|
+
spinner.stop();
|
|
28355
29923
|
const trimmed = Array.isArray(result) ? result.map((r) => ({
|
|
28356
29924
|
id: r.id,
|
|
28357
29925
|
title: r.title,
|
|
@@ -28360,17 +29928,20 @@ issues.command("list").description("list issues").option("--debug", "enable debu
|
|
|
28360
29928
|
})) : result;
|
|
28361
29929
|
printResult(trimmed, opts.json);
|
|
28362
29930
|
} catch (err) {
|
|
29931
|
+
spinner.stop();
|
|
28363
29932
|
const message = err instanceof Error ? err.message : String(err);
|
|
28364
29933
|
console.error(message);
|
|
28365
29934
|
process.exitCode = 1;
|
|
28366
29935
|
}
|
|
28367
29936
|
});
|
|
28368
29937
|
issues.command("view <issueId>").description("view issue details and comments").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
29938
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
|
|
28369
29939
|
try {
|
|
28370
29940
|
const rootOpts = program2.opts();
|
|
28371
29941
|
const cfg = readConfig();
|
|
28372
29942
|
const { apiKey } = getConfig(rootOpts);
|
|
28373
29943
|
if (!apiKey) {
|
|
29944
|
+
spinner.stop();
|
|
28374
29945
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28375
29946
|
process.exitCode = 1;
|
|
28376
29947
|
return;
|
|
@@ -28378,32 +29949,38 @@ issues.command("view <issueId>").description("view issue details and comments").
|
|
|
28378
29949
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28379
29950
|
const issue2 = await fetchIssue2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
28380
29951
|
if (!issue2) {
|
|
29952
|
+
spinner.stop();
|
|
28381
29953
|
console.error("Issue not found");
|
|
28382
29954
|
process.exitCode = 1;
|
|
28383
29955
|
return;
|
|
28384
29956
|
}
|
|
29957
|
+
spinner.update("Fetching comments...");
|
|
28385
29958
|
const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
29959
|
+
spinner.stop();
|
|
28386
29960
|
const combined = { issue: issue2, comments };
|
|
28387
29961
|
printResult(combined, opts.json);
|
|
28388
29962
|
} catch (err) {
|
|
29963
|
+
spinner.stop();
|
|
28389
29964
|
const message = err instanceof Error ? err.message : String(err);
|
|
28390
29965
|
console.error(message);
|
|
28391
29966
|
process.exitCode = 1;
|
|
28392
29967
|
}
|
|
28393
29968
|
});
|
|
28394
29969
|
issues.command("post-comment <issueId> <content>").description("post a new comment to an issue").option("--parent <uuid>", "parent comment id").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, content, opts) => {
|
|
29970
|
+
if (opts.debug) {
|
|
29971
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
29972
|
+
}
|
|
29973
|
+
content = interpretEscapes2(content);
|
|
29974
|
+
if (opts.debug) {
|
|
29975
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
29976
|
+
}
|
|
29977
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
28395
29978
|
try {
|
|
28396
|
-
if (opts.debug) {
|
|
28397
|
-
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
28398
|
-
}
|
|
28399
|
-
content = interpretEscapes2(content);
|
|
28400
|
-
if (opts.debug) {
|
|
28401
|
-
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
28402
|
-
}
|
|
28403
29979
|
const rootOpts = program2.opts();
|
|
28404
29980
|
const cfg = readConfig();
|
|
28405
29981
|
const { apiKey } = getConfig(rootOpts);
|
|
28406
29982
|
if (!apiKey) {
|
|
29983
|
+
spinner.stop();
|
|
28407
29984
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28408
29985
|
process.exitCode = 1;
|
|
28409
29986
|
return;
|
|
@@ -28417,41 +29994,44 @@ issues.command("post-comment <issueId> <content>").description("post a new comme
|
|
|
28417
29994
|
parentCommentId: opts.parent,
|
|
28418
29995
|
debug: !!opts.debug
|
|
28419
29996
|
});
|
|
29997
|
+
spinner.stop();
|
|
28420
29998
|
printResult(result, opts.json);
|
|
28421
29999
|
} catch (err) {
|
|
30000
|
+
spinner.stop();
|
|
28422
30001
|
const message = err instanceof Error ? err.message : String(err);
|
|
28423
30002
|
console.error(message);
|
|
28424
30003
|
process.exitCode = 1;
|
|
28425
30004
|
}
|
|
28426
30005
|
});
|
|
28427
|
-
issues.command("create <title>").description("create a new issue").option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10)).option("--project-id <id>", "project id", (v) => parseInt(v, 10)).option("--description <text>", "issue description (
|
|
30006
|
+
issues.command("create <title>").description("create a new issue").option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10)).option("--project-id <id>", "project id", (v) => parseInt(v, 10)).option("--description <text>", "issue description (use \\n for newlines)").option("--label <label>", "issue label (repeatable)", (value, previous) => {
|
|
28428
30007
|
previous.push(value);
|
|
28429
30008
|
return previous;
|
|
28430
30009
|
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
|
|
30010
|
+
const rootOpts = program2.opts();
|
|
30011
|
+
const cfg = readConfig();
|
|
30012
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30013
|
+
if (!apiKey) {
|
|
30014
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30015
|
+
process.exitCode = 1;
|
|
30016
|
+
return;
|
|
30017
|
+
}
|
|
30018
|
+
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
30019
|
+
if (!title) {
|
|
30020
|
+
console.error("title is required");
|
|
30021
|
+
process.exitCode = 1;
|
|
30022
|
+
return;
|
|
30023
|
+
}
|
|
30024
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
30025
|
+
if (typeof orgId !== "number") {
|
|
30026
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
30027
|
+
process.exitCode = 1;
|
|
30028
|
+
return;
|
|
30029
|
+
}
|
|
30030
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30031
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
30032
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
30033
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
|
|
28431
30034
|
try {
|
|
28432
|
-
const rootOpts = program2.opts();
|
|
28433
|
-
const cfg = readConfig();
|
|
28434
|
-
const { apiKey } = getConfig(rootOpts);
|
|
28435
|
-
if (!apiKey) {
|
|
28436
|
-
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28437
|
-
process.exitCode = 1;
|
|
28438
|
-
return;
|
|
28439
|
-
}
|
|
28440
|
-
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
28441
|
-
if (!title) {
|
|
28442
|
-
console.error("title is required");
|
|
28443
|
-
process.exitCode = 1;
|
|
28444
|
-
return;
|
|
28445
|
-
}
|
|
28446
|
-
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
28447
|
-
if (typeof orgId !== "number") {
|
|
28448
|
-
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
28449
|
-
process.exitCode = 1;
|
|
28450
|
-
return;
|
|
28451
|
-
}
|
|
28452
|
-
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
28453
|
-
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
28454
|
-
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
28455
30035
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28456
30036
|
const result = await createIssue2({
|
|
28457
30037
|
apiKey,
|
|
@@ -28463,57 +30043,60 @@ issues.command("create <title>").description("create a new issue").option("--org
|
|
|
28463
30043
|
labels,
|
|
28464
30044
|
debug: !!opts.debug
|
|
28465
30045
|
});
|
|
30046
|
+
spinner.stop();
|
|
28466
30047
|
printResult(result, opts.json);
|
|
28467
30048
|
} catch (err) {
|
|
30049
|
+
spinner.stop();
|
|
28468
30050
|
const message = err instanceof Error ? err.message : String(err);
|
|
28469
30051
|
console.error(message);
|
|
28470
30052
|
process.exitCode = 1;
|
|
28471
30053
|
}
|
|
28472
30054
|
});
|
|
28473
|
-
issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (
|
|
30055
|
+
issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (use \\n for newlines)").option("--description <text>", "new description (use \\n for newlines)").option("--status <value>", "status: open|closed|0|1").option("--label <label>", "set labels (repeatable). If provided, replaces existing labels.", (value, previous) => {
|
|
28474
30056
|
previous.push(value);
|
|
28475
30057
|
return previous;
|
|
28476
30058
|
}, []).option("--clear-labels", "set labels to an empty list").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
28477
|
-
|
|
28478
|
-
|
|
28479
|
-
|
|
28480
|
-
|
|
28481
|
-
|
|
28482
|
-
|
|
28483
|
-
|
|
28484
|
-
|
|
28485
|
-
|
|
28486
|
-
|
|
28487
|
-
|
|
28488
|
-
|
|
28489
|
-
|
|
28490
|
-
|
|
28491
|
-
|
|
28492
|
-
|
|
28493
|
-
|
|
28494
|
-
|
|
28495
|
-
|
|
28496
|
-
|
|
28497
|
-
|
|
28498
|
-
|
|
28499
|
-
console.error("status must be open|closed|0|1");
|
|
28500
|
-
process.exitCode = 1;
|
|
28501
|
-
return;
|
|
28502
|
-
}
|
|
28503
|
-
status = n;
|
|
28504
|
-
}
|
|
28505
|
-
if (status !== 0 && status !== 1) {
|
|
28506
|
-
console.error("status must be 0 (open) or 1 (closed)");
|
|
30059
|
+
const rootOpts = program2.opts();
|
|
30060
|
+
const cfg = readConfig();
|
|
30061
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30062
|
+
if (!apiKey) {
|
|
30063
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30064
|
+
process.exitCode = 1;
|
|
30065
|
+
return;
|
|
30066
|
+
}
|
|
30067
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30068
|
+
const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
|
|
30069
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30070
|
+
let status = undefined;
|
|
30071
|
+
if (opts.status !== undefined) {
|
|
30072
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
30073
|
+
if (raw === "open")
|
|
30074
|
+
status = 0;
|
|
30075
|
+
else if (raw === "closed")
|
|
30076
|
+
status = 1;
|
|
30077
|
+
else {
|
|
30078
|
+
const n = Number(raw);
|
|
30079
|
+
if (!Number.isFinite(n)) {
|
|
30080
|
+
console.error("status must be open|closed|0|1");
|
|
28507
30081
|
process.exitCode = 1;
|
|
28508
30082
|
return;
|
|
28509
30083
|
}
|
|
30084
|
+
status = n;
|
|
28510
30085
|
}
|
|
28511
|
-
|
|
28512
|
-
|
|
28513
|
-
|
|
28514
|
-
|
|
28515
|
-
labels = opts.label.map(String);
|
|
30086
|
+
if (status !== 0 && status !== 1) {
|
|
30087
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
30088
|
+
process.exitCode = 1;
|
|
30089
|
+
return;
|
|
28516
30090
|
}
|
|
30091
|
+
}
|
|
30092
|
+
let labels = undefined;
|
|
30093
|
+
if (opts.clearLabels) {
|
|
30094
|
+
labels = [];
|
|
30095
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
30096
|
+
labels = opts.label.map(String);
|
|
30097
|
+
}
|
|
30098
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
|
|
30099
|
+
try {
|
|
28517
30100
|
const result = await updateIssue2({
|
|
28518
30101
|
apiKey,
|
|
28519
30102
|
apiBaseUrl,
|
|
@@ -28524,40 +30107,217 @@ issues.command("update <issueId>").description("update an existing issue (title/
|
|
|
28524
30107
|
labels,
|
|
28525
30108
|
debug: !!opts.debug
|
|
28526
30109
|
});
|
|
30110
|
+
spinner.stop();
|
|
28527
30111
|
printResult(result, opts.json);
|
|
28528
30112
|
} catch (err) {
|
|
30113
|
+
spinner.stop();
|
|
28529
30114
|
const message = err instanceof Error ? err.message : String(err);
|
|
28530
30115
|
console.error(message);
|
|
28531
30116
|
process.exitCode = 1;
|
|
28532
30117
|
}
|
|
28533
30118
|
});
|
|
28534
30119
|
issues.command("update-comment <commentId> <content>").description("update an existing issue comment").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commentId, content, opts) => {
|
|
30120
|
+
if (opts.debug) {
|
|
30121
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
30122
|
+
}
|
|
30123
|
+
content = interpretEscapes2(content);
|
|
30124
|
+
if (opts.debug) {
|
|
30125
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
30126
|
+
}
|
|
30127
|
+
const rootOpts = program2.opts();
|
|
30128
|
+
const cfg = readConfig();
|
|
30129
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30130
|
+
if (!apiKey) {
|
|
30131
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30132
|
+
process.exitCode = 1;
|
|
30133
|
+
return;
|
|
30134
|
+
}
|
|
30135
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating comment...");
|
|
28535
30136
|
try {
|
|
28536
|
-
|
|
28537
|
-
|
|
28538
|
-
|
|
28539
|
-
|
|
28540
|
-
|
|
28541
|
-
|
|
30137
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30138
|
+
const result = await updateIssueComment2({
|
|
30139
|
+
apiKey,
|
|
30140
|
+
apiBaseUrl,
|
|
30141
|
+
commentId,
|
|
30142
|
+
content,
|
|
30143
|
+
debug: !!opts.debug
|
|
30144
|
+
});
|
|
30145
|
+
spinner.stop();
|
|
30146
|
+
printResult(result, opts.json);
|
|
30147
|
+
} catch (err) {
|
|
30148
|
+
spinner.stop();
|
|
30149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30150
|
+
console.error(message);
|
|
30151
|
+
process.exitCode = 1;
|
|
30152
|
+
}
|
|
30153
|
+
});
|
|
30154
|
+
issues.command("action-items <issueId>").description("list action items for an issue").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
30155
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action items...");
|
|
30156
|
+
try {
|
|
30157
|
+
const rootOpts = program2.opts();
|
|
30158
|
+
const cfg = readConfig();
|
|
30159
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30160
|
+
if (!apiKey) {
|
|
30161
|
+
spinner.stop();
|
|
30162
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30163
|
+
process.exitCode = 1;
|
|
30164
|
+
return;
|
|
28542
30165
|
}
|
|
30166
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30167
|
+
const result = await fetchActionItems2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
30168
|
+
spinner.stop();
|
|
30169
|
+
printResult(result, opts.json);
|
|
30170
|
+
} catch (err) {
|
|
30171
|
+
spinner.stop();
|
|
30172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30173
|
+
console.error(message);
|
|
30174
|
+
process.exitCode = 1;
|
|
30175
|
+
}
|
|
30176
|
+
});
|
|
30177
|
+
issues.command("view-action-item <actionItemIds...>").description("view action item(s) with all details (supports multiple IDs)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemIds, opts) => {
|
|
30178
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action item(s)...");
|
|
30179
|
+
try {
|
|
28543
30180
|
const rootOpts = program2.opts();
|
|
28544
30181
|
const cfg = readConfig();
|
|
28545
30182
|
const { apiKey } = getConfig(rootOpts);
|
|
28546
30183
|
if (!apiKey) {
|
|
30184
|
+
spinner.stop();
|
|
28547
30185
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28548
30186
|
process.exitCode = 1;
|
|
28549
30187
|
return;
|
|
28550
30188
|
}
|
|
28551
30189
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28552
|
-
const result = await
|
|
30190
|
+
const result = await fetchActionItem2({ apiKey, apiBaseUrl, actionItemIds, debug: !!opts.debug });
|
|
30191
|
+
if (result.length === 0) {
|
|
30192
|
+
spinner.stop();
|
|
30193
|
+
console.error("Action item(s) not found");
|
|
30194
|
+
process.exitCode = 1;
|
|
30195
|
+
return;
|
|
30196
|
+
}
|
|
30197
|
+
spinner.stop();
|
|
30198
|
+
printResult(result, opts.json);
|
|
30199
|
+
} catch (err) {
|
|
30200
|
+
spinner.stop();
|
|
30201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30202
|
+
console.error(message);
|
|
30203
|
+
process.exitCode = 1;
|
|
30204
|
+
}
|
|
30205
|
+
});
|
|
30206
|
+
issues.command("create-action-item <issueId> <title>").description("create a new action item for an issue").option("--description <text>", "detailed description (use \\n for newlines)").option("--sql-action <sql>", "SQL command to execute").option("--config <json>", 'config change as JSON: {"parameter":"...","value":"..."} (repeatable)', (value, previous) => {
|
|
30207
|
+
try {
|
|
30208
|
+
previous.push(JSON.parse(value));
|
|
30209
|
+
} catch {
|
|
30210
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
30211
|
+
process.exit(1);
|
|
30212
|
+
}
|
|
30213
|
+
return previous;
|
|
30214
|
+
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, rawTitle, opts) => {
|
|
30215
|
+
const rootOpts = program2.opts();
|
|
30216
|
+
const cfg = readConfig();
|
|
30217
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30218
|
+
if (!apiKey) {
|
|
30219
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30220
|
+
process.exitCode = 1;
|
|
30221
|
+
return;
|
|
30222
|
+
}
|
|
30223
|
+
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
30224
|
+
if (!title) {
|
|
30225
|
+
console.error("title is required");
|
|
30226
|
+
process.exitCode = 1;
|
|
30227
|
+
return;
|
|
30228
|
+
}
|
|
30229
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30230
|
+
const sqlAction = opts.sqlAction;
|
|
30231
|
+
const configs = Array.isArray(opts.config) && opts.config.length > 0 ? opts.config : undefined;
|
|
30232
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating action item...");
|
|
30233
|
+
try {
|
|
30234
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30235
|
+
const result = await createActionItem2({
|
|
28553
30236
|
apiKey,
|
|
28554
30237
|
apiBaseUrl,
|
|
28555
|
-
|
|
28556
|
-
|
|
30238
|
+
issueId,
|
|
30239
|
+
title,
|
|
30240
|
+
description,
|
|
30241
|
+
sqlAction,
|
|
30242
|
+
configs,
|
|
28557
30243
|
debug: !!opts.debug
|
|
28558
30244
|
});
|
|
28559
|
-
|
|
30245
|
+
spinner.stop();
|
|
30246
|
+
printResult({ id: result }, opts.json);
|
|
30247
|
+
} catch (err) {
|
|
30248
|
+
spinner.stop();
|
|
30249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30250
|
+
console.error(message);
|
|
30251
|
+
process.exitCode = 1;
|
|
30252
|
+
}
|
|
30253
|
+
});
|
|
30254
|
+
issues.command("update-action-item <actionItemId>").description("update an action item (title, description, status, sql_action, configs)").option("--title <text>", "new title (use \\n for newlines)").option("--description <text>", "new description (use \\n for newlines)").option("--done", "mark as done").option("--not-done", "mark as not done").option("--status <value>", "status: waiting_for_approval|approved|rejected").option("--status-reason <text>", "reason for status change").option("--sql-action <sql>", "SQL command (use empty string to clear)").option("--config <json>", "config change as JSON (repeatable, replaces all configs)", (value, previous) => {
|
|
30255
|
+
try {
|
|
30256
|
+
previous.push(JSON.parse(value));
|
|
30257
|
+
} catch {
|
|
30258
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
30259
|
+
process.exit(1);
|
|
30260
|
+
}
|
|
30261
|
+
return previous;
|
|
30262
|
+
}, []).option("--clear-configs", "clear all config changes").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemId, opts) => {
|
|
30263
|
+
const rootOpts = program2.opts();
|
|
30264
|
+
const cfg = readConfig();
|
|
30265
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30266
|
+
if (!apiKey) {
|
|
30267
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30268
|
+
process.exitCode = 1;
|
|
30269
|
+
return;
|
|
30270
|
+
}
|
|
30271
|
+
const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
|
|
30272
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30273
|
+
let isDone = undefined;
|
|
30274
|
+
if (opts.done)
|
|
30275
|
+
isDone = true;
|
|
30276
|
+
else if (opts.notDone)
|
|
30277
|
+
isDone = false;
|
|
30278
|
+
let status = undefined;
|
|
30279
|
+
if (opts.status !== undefined) {
|
|
30280
|
+
const validStatuses = ["waiting_for_approval", "approved", "rejected"];
|
|
30281
|
+
if (!validStatuses.includes(opts.status)) {
|
|
30282
|
+
console.error(`status must be one of: ${validStatuses.join(", ")}`);
|
|
30283
|
+
process.exitCode = 1;
|
|
30284
|
+
return;
|
|
30285
|
+
}
|
|
30286
|
+
status = opts.status;
|
|
30287
|
+
}
|
|
30288
|
+
const statusReason = opts.statusReason;
|
|
30289
|
+
const sqlAction = opts.sqlAction;
|
|
30290
|
+
let configs = undefined;
|
|
30291
|
+
if (opts.clearConfigs) {
|
|
30292
|
+
configs = [];
|
|
30293
|
+
} else if (Array.isArray(opts.config) && opts.config.length > 0) {
|
|
30294
|
+
configs = opts.config;
|
|
30295
|
+
}
|
|
30296
|
+
if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined && sqlAction === undefined && configs === undefined) {
|
|
30297
|
+
console.error("At least one update option is required");
|
|
30298
|
+
process.exitCode = 1;
|
|
30299
|
+
return;
|
|
30300
|
+
}
|
|
30301
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating action item...");
|
|
30302
|
+
try {
|
|
30303
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30304
|
+
await updateActionItem2({
|
|
30305
|
+
apiKey,
|
|
30306
|
+
apiBaseUrl,
|
|
30307
|
+
actionItemId,
|
|
30308
|
+
title,
|
|
30309
|
+
description,
|
|
30310
|
+
isDone,
|
|
30311
|
+
status,
|
|
30312
|
+
statusReason,
|
|
30313
|
+
sqlAction,
|
|
30314
|
+
configs,
|
|
30315
|
+
debug: !!opts.debug
|
|
30316
|
+
});
|
|
30317
|
+
spinner.stop();
|
|
30318
|
+
printResult({ success: true }, opts.json);
|
|
28560
30319
|
} catch (err) {
|
|
30320
|
+
spinner.stop();
|
|
28561
30321
|
const message = err instanceof Error ? err.message : String(err);
|
|
28562
30322
|
console.error(message);
|
|
28563
30323
|
process.exitCode = 1;
|