postgresai 0.14.0-beta.12 → 0.14.0-beta.14
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 +1234 -170
- package/dist/bin/postgres-ai.js +2480 -410
- package/dist/sql/02.extensions.sql +8 -0
- package/dist/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/dist/sql/sql/02.extensions.sql +8 -0
- package/dist/sql/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/dist/sql/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/sql/uninit/03.role.sql +27 -0
- package/dist/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/uninit/03.role.sql +27 -0
- package/lib/checkup.ts +69 -3
- package/lib/init.ts +184 -26
- 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/sql/02.extensions.sql +8 -0
- package/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/sql/uninit/01.helpers.sql +5 -0
- package/sql/uninit/02.permissions.sql +30 -0
- package/sql/uninit/03.role.sql +27 -0
- 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 +501 -2
- 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/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/dist/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /package/dist/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
- /package/dist/sql/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/dist/sql/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /package/dist/sql/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
- /package/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /package/sql/{05.helpers.sql → 06.helpers.sql} +0 -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.14",
|
|
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.14";
|
|
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,150 +24340,396 @@ async function updateIssueComment2(params) {
|
|
|
23910
24340
|
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
23911
24341
|
}
|
|
23912
24342
|
}
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
|
|
23917
|
-
return "";
|
|
23918
|
-
if (secret.length <= 8)
|
|
23919
|
-
return "****";
|
|
23920
|
-
if (secret.length <= 16)
|
|
23921
|
-
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
23922
|
-
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
23923
|
-
}
|
|
23924
|
-
function normalizeBaseUrl2(value) {
|
|
23925
|
-
const trimmed = (value || "").replace(/\/$/, "");
|
|
23926
|
-
try {
|
|
23927
|
-
new URL(trimmed);
|
|
23928
|
-
} catch {
|
|
23929
|
-
throw new Error(`Invalid base URL: ${value}`);
|
|
23930
|
-
}
|
|
23931
|
-
return trimmed;
|
|
23932
|
-
}
|
|
23933
|
-
function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
23934
|
-
const defApi = defaults2.apiBaseUrl || "https://postgres.ai/api/general/";
|
|
23935
|
-
const defUi = defaults2.uiBaseUrl || "https://console.postgres.ai";
|
|
23936
|
-
const apiCandidate = opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi;
|
|
23937
|
-
const uiCandidate = opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi;
|
|
23938
|
-
return {
|
|
23939
|
-
apiBaseUrl: normalizeBaseUrl2(apiCandidate),
|
|
23940
|
-
uiBaseUrl: normalizeBaseUrl2(uiCandidate)
|
|
23941
|
-
};
|
|
23942
|
-
}
|
|
23943
|
-
|
|
23944
|
-
// lib/init.ts
|
|
23945
|
-
import { randomBytes } from "crypto";
|
|
23946
|
-
import { URL as URL2, fileURLToPath } from "url";
|
|
23947
|
-
import * as fs3 from "fs";
|
|
23948
|
-
import * as path3 from "path";
|
|
23949
|
-
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
23950
|
-
function sslModeToConfig(mode) {
|
|
23951
|
-
if (mode.toLowerCase() === "disable")
|
|
23952
|
-
return false;
|
|
23953
|
-
if (mode.toLowerCase() === "verify-full" || mode.toLowerCase() === "verify-ca")
|
|
23954
|
-
return true;
|
|
23955
|
-
return { rejectUnauthorized: false };
|
|
23956
|
-
}
|
|
23957
|
-
function extractSslModeFromUri(uri) {
|
|
23958
|
-
try {
|
|
23959
|
-
return new URL2(uri).searchParams.get("sslmode") ?? undefined;
|
|
23960
|
-
} catch {
|
|
23961
|
-
return uri.match(/[?&]sslmode=([^&]+)/i)?.[1];
|
|
23962
|
-
}
|
|
23963
|
-
}
|
|
23964
|
-
function stripSslModeFromUri(uri) {
|
|
23965
|
-
try {
|
|
23966
|
-
const u = new URL2(uri);
|
|
23967
|
-
u.searchParams.delete("sslmode");
|
|
23968
|
-
return u.toString();
|
|
23969
|
-
} catch {
|
|
23970
|
-
return uri.replace(/[?&]sslmode=[^&]*/gi, "").replace(/\?&/, "?").replace(/\?$/, "");
|
|
24343
|
+
async function fetchActionItem2(params) {
|
|
24344
|
+
const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
|
|
24345
|
+
if (!apiKey) {
|
|
24346
|
+
throw new Error("API key is required");
|
|
23971
24347
|
}
|
|
23972
|
-
}
|
|
23973
|
-
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
const msg = typeof e.message === "string" ? e.message.toLowerCase() : "";
|
|
23978
|
-
const code = typeof e.code === "string" ? e.code : "";
|
|
23979
|
-
const fallbackPatterns = [
|
|
23980
|
-
"the server does not support ssl",
|
|
23981
|
-
"ssl off",
|
|
23982
|
-
"server does not support ssl connections"
|
|
23983
|
-
];
|
|
23984
|
-
for (const pattern of fallbackPatterns) {
|
|
23985
|
-
if (msg.includes(pattern))
|
|
23986
|
-
return true;
|
|
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");
|
|
23987
24353
|
}
|
|
23988
|
-
|
|
23989
|
-
|
|
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(",")})`);
|
|
23990
24360
|
}
|
|
23991
|
-
|
|
23992
|
-
|
|
23993
|
-
|
|
23994
|
-
|
|
23995
|
-
|
|
23996
|
-
await client.connect();
|
|
23997
|
-
return client;
|
|
24361
|
+
const headers = {
|
|
24362
|
+
"access-token": apiKey,
|
|
24363
|
+
Prefer: "return=representation",
|
|
24364
|
+
"Content-Type": "application/json",
|
|
24365
|
+
Connection: "close"
|
|
23998
24366
|
};
|
|
23999
|
-
if (
|
|
24000
|
-
const
|
|
24001
|
-
|
|
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)}`);
|
|
24002
24373
|
}
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
}
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
}
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
const noSslConfig = { ...adminConn.clientConfig, ssl: false };
|
|
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) {
|
|
24014
24384
|
try {
|
|
24015
|
-
const
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
if (isSslNegotiationError(noSslErr)) {
|
|
24019
|
-
const msg = noSslErr?.message || "";
|
|
24020
|
-
if (msg.toLowerCase().includes("ssl") && msg.toLowerCase().includes("required")) {
|
|
24021
|
-
throw sslErr;
|
|
24022
|
-
}
|
|
24385
|
+
const parsed = JSON.parse(data);
|
|
24386
|
+
if (Array.isArray(parsed)) {
|
|
24387
|
+
return parsed;
|
|
24023
24388
|
}
|
|
24024
|
-
|
|
24025
|
-
}
|
|
24026
|
-
|
|
24027
|
-
}
|
|
24028
|
-
function sqlDir() {
|
|
24029
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
24030
|
-
const currentDir = path3.dirname(currentFile);
|
|
24031
|
-
const candidates = [
|
|
24032
|
-
path3.resolve(currentDir, "..", "sql"),
|
|
24033
|
-
path3.resolve(currentDir, "..", "..", "sql")
|
|
24034
|
-
];
|
|
24035
|
-
for (const candidate of candidates) {
|
|
24036
|
-
if (fs3.existsSync(candidate)) {
|
|
24037
|
-
return candidate;
|
|
24389
|
+
return parsed ? [parsed] : [];
|
|
24390
|
+
} catch {
|
|
24391
|
+
throw new Error(`Failed to parse action item response: ${data}`);
|
|
24038
24392
|
}
|
|
24393
|
+
} else {
|
|
24394
|
+
throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
|
|
24039
24395
|
}
|
|
24040
|
-
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
24041
|
-
}
|
|
24042
|
-
function loadSqlTemplate(filename) {
|
|
24043
|
-
const p = path3.join(sqlDir(), filename);
|
|
24044
|
-
return fs3.readFileSync(p, "utf8");
|
|
24045
|
-
}
|
|
24046
|
-
function applyTemplate(sql, vars) {
|
|
24047
|
-
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
24048
|
-
const v = vars[key];
|
|
24049
|
-
if (v === undefined)
|
|
24050
|
-
throw new Error(`Missing SQL template var: ${key}`);
|
|
24051
|
-
return v;
|
|
24052
|
-
});
|
|
24053
24396
|
}
|
|
24054
|
-
function
|
|
24055
|
-
|
|
24056
|
-
|
|
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
|
+
}
|
|
24580
|
+
|
|
24581
|
+
// lib/util.ts
|
|
24582
|
+
function maskSecret2(secret) {
|
|
24583
|
+
if (!secret)
|
|
24584
|
+
return "";
|
|
24585
|
+
if (secret.length <= 8)
|
|
24586
|
+
return "****";
|
|
24587
|
+
if (secret.length <= 16)
|
|
24588
|
+
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
24589
|
+
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
24590
|
+
}
|
|
24591
|
+
function normalizeBaseUrl2(value) {
|
|
24592
|
+
const trimmed = (value || "").replace(/\/$/, "");
|
|
24593
|
+
try {
|
|
24594
|
+
new URL(trimmed);
|
|
24595
|
+
} catch {
|
|
24596
|
+
throw new Error(`Invalid base URL: ${value}`);
|
|
24597
|
+
}
|
|
24598
|
+
return trimmed;
|
|
24599
|
+
}
|
|
24600
|
+
function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
24601
|
+
const defApi = defaults2.apiBaseUrl || "https://postgres.ai/api/general/";
|
|
24602
|
+
const defUi = defaults2.uiBaseUrl || "https://console.postgres.ai";
|
|
24603
|
+
const apiCandidate = opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi;
|
|
24604
|
+
const uiCandidate = opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi;
|
|
24605
|
+
return {
|
|
24606
|
+
apiBaseUrl: normalizeBaseUrl2(apiCandidate),
|
|
24607
|
+
uiBaseUrl: normalizeBaseUrl2(uiCandidate)
|
|
24608
|
+
};
|
|
24609
|
+
}
|
|
24610
|
+
|
|
24611
|
+
// lib/init.ts
|
|
24612
|
+
import { randomBytes } from "crypto";
|
|
24613
|
+
import { URL as URL2, fileURLToPath } from "url";
|
|
24614
|
+
import * as fs3 from "fs";
|
|
24615
|
+
import * as path3 from "path";
|
|
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
|
+
}
|
|
24626
|
+
function sslModeToConfig(mode) {
|
|
24627
|
+
if (mode.toLowerCase() === "disable")
|
|
24628
|
+
return false;
|
|
24629
|
+
if (mode.toLowerCase() === "verify-full" || mode.toLowerCase() === "verify-ca")
|
|
24630
|
+
return true;
|
|
24631
|
+
return { rejectUnauthorized: false };
|
|
24632
|
+
}
|
|
24633
|
+
function extractSslModeFromUri(uri) {
|
|
24634
|
+
try {
|
|
24635
|
+
return new URL2(uri).searchParams.get("sslmode") ?? undefined;
|
|
24636
|
+
} catch {
|
|
24637
|
+
return uri.match(/[?&]sslmode=([^&]+)/i)?.[1];
|
|
24638
|
+
}
|
|
24639
|
+
}
|
|
24640
|
+
function stripSslModeFromUri(uri) {
|
|
24641
|
+
try {
|
|
24642
|
+
const u = new URL2(uri);
|
|
24643
|
+
u.searchParams.delete("sslmode");
|
|
24644
|
+
return u.toString();
|
|
24645
|
+
} catch {
|
|
24646
|
+
return uri.replace(/[?&]sslmode=[^&]*/gi, "").replace(/\?&/, "?").replace(/\?$/, "");
|
|
24647
|
+
}
|
|
24648
|
+
}
|
|
24649
|
+
function isSslNegotiationError(err) {
|
|
24650
|
+
if (!err || typeof err !== "object")
|
|
24651
|
+
return false;
|
|
24652
|
+
const e = err;
|
|
24653
|
+
const msg = typeof e.message === "string" ? e.message.toLowerCase() : "";
|
|
24654
|
+
const code = typeof e.code === "string" ? e.code : "";
|
|
24655
|
+
const fallbackPatterns = [
|
|
24656
|
+
"the server does not support ssl",
|
|
24657
|
+
"ssl off",
|
|
24658
|
+
"server does not support ssl connections"
|
|
24659
|
+
];
|
|
24660
|
+
for (const pattern of fallbackPatterns) {
|
|
24661
|
+
if (msg.includes(pattern))
|
|
24662
|
+
return true;
|
|
24663
|
+
}
|
|
24664
|
+
if (code === "08P01" && (msg.includes("ssl") || msg.includes("unsupported"))) {
|
|
24665
|
+
return true;
|
|
24666
|
+
}
|
|
24667
|
+
return false;
|
|
24668
|
+
}
|
|
24669
|
+
async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
24670
|
+
const tryConnect = async (config2) => {
|
|
24671
|
+
const client = new ClientClass(config2);
|
|
24672
|
+
await client.connect();
|
|
24673
|
+
return client;
|
|
24674
|
+
};
|
|
24675
|
+
if (!adminConn.sslFallbackEnabled) {
|
|
24676
|
+
const client = await tryConnect(adminConn.clientConfig);
|
|
24677
|
+
return { client, usedSsl: !!adminConn.clientConfig.ssl };
|
|
24678
|
+
}
|
|
24679
|
+
try {
|
|
24680
|
+
const client = await tryConnect(adminConn.clientConfig);
|
|
24681
|
+
return { client, usedSsl: true };
|
|
24682
|
+
} catch (sslErr) {
|
|
24683
|
+
if (!isSslNegotiationError(sslErr)) {
|
|
24684
|
+
throw sslErr;
|
|
24685
|
+
}
|
|
24686
|
+
if (verbose) {
|
|
24687
|
+
console.log("SSL connection failed, retrying without SSL...");
|
|
24688
|
+
}
|
|
24689
|
+
const noSslConfig = { ...adminConn.clientConfig, ssl: false };
|
|
24690
|
+
try {
|
|
24691
|
+
const client = await tryConnect(noSslConfig);
|
|
24692
|
+
return { client, usedSsl: false };
|
|
24693
|
+
} catch (noSslErr) {
|
|
24694
|
+
if (isSslNegotiationError(noSslErr)) {
|
|
24695
|
+
const msg = noSslErr?.message || "";
|
|
24696
|
+
if (msg.toLowerCase().includes("ssl") && msg.toLowerCase().includes("required")) {
|
|
24697
|
+
throw sslErr;
|
|
24698
|
+
}
|
|
24699
|
+
}
|
|
24700
|
+
throw noSslErr;
|
|
24701
|
+
}
|
|
24702
|
+
}
|
|
24703
|
+
}
|
|
24704
|
+
function sqlDir() {
|
|
24705
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
24706
|
+
const currentDir = path3.dirname(currentFile);
|
|
24707
|
+
const candidates = [
|
|
24708
|
+
path3.resolve(currentDir, "..", "sql"),
|
|
24709
|
+
path3.resolve(currentDir, "..", "..", "sql")
|
|
24710
|
+
];
|
|
24711
|
+
for (const candidate of candidates) {
|
|
24712
|
+
if (fs3.existsSync(candidate)) {
|
|
24713
|
+
return candidate;
|
|
24714
|
+
}
|
|
24715
|
+
}
|
|
24716
|
+
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
24717
|
+
}
|
|
24718
|
+
function loadSqlTemplate(filename) {
|
|
24719
|
+
const p = path3.join(sqlDir(), filename);
|
|
24720
|
+
return fs3.readFileSync(p, "utf8");
|
|
24721
|
+
}
|
|
24722
|
+
function applyTemplate(sql, vars) {
|
|
24723
|
+
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
24724
|
+
const v = vars[key];
|
|
24725
|
+
if (v === undefined)
|
|
24726
|
+
throw new Error(`Missing SQL template var: ${key}`);
|
|
24727
|
+
return v;
|
|
24728
|
+
});
|
|
24729
|
+
}
|
|
24730
|
+
function quoteIdent(ident) {
|
|
24731
|
+
if (ident.includes("\x00")) {
|
|
24732
|
+
throw new Error("Identifier cannot contain null bytes");
|
|
24057
24733
|
}
|
|
24058
24734
|
return `"${ident.replace(/"/g, '""')}"`;
|
|
24059
24735
|
}
|
|
@@ -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,24 +24958,40 @@ 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
|
+
steps.push({
|
|
24965
|
+
name: "02.extensions",
|
|
24966
|
+
sql: loadSqlTemplate("02.extensions.sql")
|
|
24967
|
+
});
|
|
24968
|
+
let permissionsSql = applyTemplate(loadSqlTemplate("03.permissions.sql"), vars);
|
|
24969
|
+
if (SKIP_ALTER_USER_PROVIDERS.includes(provider)) {
|
|
24970
|
+
permissionsSql = permissionsSql.split(`
|
|
24971
|
+
`).filter((line) => {
|
|
24972
|
+
const trimmed = line.trim();
|
|
24973
|
+
if (trimmed.startsWith("--") || trimmed === "")
|
|
24974
|
+
return true;
|
|
24975
|
+
return !/^\s*alter\s+user\s+/i.test(line);
|
|
24976
|
+
}).join(`
|
|
24977
|
+
`);
|
|
24978
|
+
}
|
|
24285
24979
|
steps.push({
|
|
24286
|
-
name: "
|
|
24287
|
-
sql:
|
|
24980
|
+
name: "03.permissions",
|
|
24981
|
+
sql: permissionsSql
|
|
24288
24982
|
});
|
|
24289
24983
|
steps.push({
|
|
24290
|
-
name: "
|
|
24291
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
24984
|
+
name: "06.helpers",
|
|
24985
|
+
sql: applyTemplate(loadSqlTemplate("06.helpers.sql"), vars)
|
|
24292
24986
|
});
|
|
24293
24987
|
if (params.includeOptionalPermissions) {
|
|
24294
24988
|
steps.push({
|
|
24295
|
-
name: "
|
|
24296
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
24989
|
+
name: "04.optional_rds",
|
|
24990
|
+
sql: applyTemplate(loadSqlTemplate("04.optional_rds.sql"), vars),
|
|
24297
24991
|
optional: true
|
|
24298
24992
|
}, {
|
|
24299
|
-
name: "
|
|
24300
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
24993
|
+
name: "05.optional_self_managed",
|
|
24994
|
+
sql: applyTemplate(loadSqlTemplate("05.optional_self_managed.sql"), vars),
|
|
24301
24995
|
optional: true
|
|
24302
24996
|
});
|
|
24303
24997
|
}
|
|
@@ -24365,6 +25059,62 @@ async function applyInitPlan(params) {
|
|
|
24365
25059
|
}
|
|
24366
25060
|
return { applied, skippedOptional };
|
|
24367
25061
|
}
|
|
25062
|
+
async function buildUninitPlan(params) {
|
|
25063
|
+
const monitoringUser = params.monitoringUser || DEFAULT_MONITORING_USER;
|
|
25064
|
+
const database = params.database;
|
|
25065
|
+
const provider = params.provider ?? "self-managed";
|
|
25066
|
+
const dropRole = params.dropRole ?? true;
|
|
25067
|
+
const qRole = quoteIdent(monitoringUser);
|
|
25068
|
+
const qDb = quoteIdent(database);
|
|
25069
|
+
const qRoleLiteral = quoteLiteral(monitoringUser);
|
|
25070
|
+
const steps = [];
|
|
25071
|
+
const vars = {
|
|
25072
|
+
ROLE_IDENT: qRole,
|
|
25073
|
+
DB_IDENT: qDb,
|
|
25074
|
+
ROLE_LITERAL: qRoleLiteral
|
|
25075
|
+
};
|
|
25076
|
+
steps.push({
|
|
25077
|
+
name: "01.drop_helpers",
|
|
25078
|
+
sql: applyTemplate(loadSqlTemplate("uninit/01.helpers.sql"), vars)
|
|
25079
|
+
});
|
|
25080
|
+
steps.push({
|
|
25081
|
+
name: "02.revoke_permissions",
|
|
25082
|
+
sql: applyTemplate(loadSqlTemplate("uninit/02.permissions.sql"), vars)
|
|
25083
|
+
});
|
|
25084
|
+
if (dropRole && !SKIP_ROLE_CREATION_PROVIDERS.includes(provider)) {
|
|
25085
|
+
steps.push({
|
|
25086
|
+
name: "03.drop_role",
|
|
25087
|
+
sql: applyTemplate(loadSqlTemplate("uninit/03.role.sql"), vars)
|
|
25088
|
+
});
|
|
25089
|
+
}
|
|
25090
|
+
return { monitoringUser, database, steps, dropRole };
|
|
25091
|
+
}
|
|
25092
|
+
async function applyUninitPlan(params) {
|
|
25093
|
+
const applied = [];
|
|
25094
|
+
const errors3 = [];
|
|
25095
|
+
const executeStep = async (step) => {
|
|
25096
|
+
await params.client.query("begin;");
|
|
25097
|
+
try {
|
|
25098
|
+
await params.client.query(step.sql, step.params);
|
|
25099
|
+
await params.client.query("commit;");
|
|
25100
|
+
} catch (e) {
|
|
25101
|
+
try {
|
|
25102
|
+
await params.client.query("rollback;");
|
|
25103
|
+
} catch {}
|
|
25104
|
+
throw e;
|
|
25105
|
+
}
|
|
25106
|
+
};
|
|
25107
|
+
for (const step of params.plan.steps) {
|
|
25108
|
+
try {
|
|
25109
|
+
await executeStep(step);
|
|
25110
|
+
applied.push(step.name);
|
|
25111
|
+
} catch (e) {
|
|
25112
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
25113
|
+
errors3.push(`${step.name}: ${msg}`);
|
|
25114
|
+
}
|
|
25115
|
+
}
|
|
25116
|
+
return { applied, errors: errors3 };
|
|
25117
|
+
}
|
|
24368
25118
|
async function verifyInitSetup(params) {
|
|
24369
25119
|
await params.client.query("begin isolation level repeatable read;");
|
|
24370
25120
|
try {
|
|
@@ -24372,6 +25122,7 @@ async function verifyInitSetup(params) {
|
|
|
24372
25122
|
const missingOptional = [];
|
|
24373
25123
|
const role = params.monitoringUser;
|
|
24374
25124
|
const db = params.database;
|
|
25125
|
+
const provider = params.provider ?? "self-managed";
|
|
24375
25126
|
const roleRes = await params.client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [role]);
|
|
24376
25127
|
const roleExists = (roleRes.rowCount ?? 0) > 0;
|
|
24377
25128
|
if (!roleExists) {
|
|
@@ -24407,15 +25158,17 @@ async function verifyInitSetup(params) {
|
|
|
24407
25158
|
if (!schemaUsageRes.rows?.[0]?.ok) {
|
|
24408
25159
|
missingRequired.push("USAGE on schema public");
|
|
24409
25160
|
}
|
|
24410
|
-
|
|
24411
|
-
|
|
24412
|
-
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
25161
|
+
if (!SKIP_SEARCH_PATH_CHECK_PROVIDERS.includes(provider)) {
|
|
25162
|
+
const rolcfgRes = await params.client.query("select rolconfig from pg_catalog.pg_roles where rolname = $1", [role]);
|
|
25163
|
+
const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
|
|
25164
|
+
const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
|
|
25165
|
+
if (typeof spLine !== "string" || !spLine) {
|
|
25166
|
+
missingRequired.push("role search_path is set");
|
|
25167
|
+
} else {
|
|
25168
|
+
const sp = spLine.toLowerCase();
|
|
25169
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
25170
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
25171
|
+
}
|
|
24419
25172
|
}
|
|
24420
25173
|
}
|
|
24421
25174
|
const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
|
|
@@ -24459,6 +25212,430 @@ async function verifyInitSetup(params) {
|
|
|
24459
25212
|
}
|
|
24460
25213
|
}
|
|
24461
25214
|
|
|
25215
|
+
// lib/supabase.ts
|
|
25216
|
+
var SUPABASE_API_BASE = "https://api.supabase.com";
|
|
25217
|
+
function isValidProjectRef(ref) {
|
|
25218
|
+
return /^[a-z0-9]{10,30}$/i.test(ref);
|
|
25219
|
+
}
|
|
25220
|
+
|
|
25221
|
+
class SupabaseClient {
|
|
25222
|
+
config;
|
|
25223
|
+
constructor(config2) {
|
|
25224
|
+
if (!config2.projectRef) {
|
|
25225
|
+
throw new Error("Supabase project reference is required");
|
|
25226
|
+
}
|
|
25227
|
+
if (!config2.accessToken) {
|
|
25228
|
+
throw new Error("Supabase access token is required");
|
|
25229
|
+
}
|
|
25230
|
+
if (!isValidProjectRef(config2.projectRef)) {
|
|
25231
|
+
throw new Error(`Invalid Supabase project reference format: "${config2.projectRef}". Expected 10-30 alphanumeric characters.`);
|
|
25232
|
+
}
|
|
25233
|
+
this.config = config2;
|
|
25234
|
+
}
|
|
25235
|
+
async query(sql, readOnly = false) {
|
|
25236
|
+
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(this.config.projectRef)}/database/query`;
|
|
25237
|
+
const response = await fetch(url, {
|
|
25238
|
+
method: "POST",
|
|
25239
|
+
headers: {
|
|
25240
|
+
"Content-Type": "application/json",
|
|
25241
|
+
Authorization: `Bearer ${this.config.accessToken}`
|
|
25242
|
+
},
|
|
25243
|
+
body: JSON.stringify({
|
|
25244
|
+
query: sql,
|
|
25245
|
+
read_only: readOnly
|
|
25246
|
+
})
|
|
25247
|
+
});
|
|
25248
|
+
const body = await response.text();
|
|
25249
|
+
let data;
|
|
25250
|
+
try {
|
|
25251
|
+
data = JSON.parse(body);
|
|
25252
|
+
} catch {
|
|
25253
|
+
throw this.createPgError({
|
|
25254
|
+
message: `Supabase API returned non-JSON response: ${body.slice(0, 200)}`,
|
|
25255
|
+
httpStatus: response.status
|
|
25256
|
+
});
|
|
25257
|
+
}
|
|
25258
|
+
if (!response.ok) {
|
|
25259
|
+
throw this.parseApiError(data, response.status);
|
|
25260
|
+
}
|
|
25261
|
+
if (data && typeof data === "object" && "error" in data && data.error) {
|
|
25262
|
+
throw this.parseApiError(data, response.status);
|
|
25263
|
+
}
|
|
25264
|
+
const rows = Array.isArray(data) ? data : [];
|
|
25265
|
+
return {
|
|
25266
|
+
rows,
|
|
25267
|
+
rowCount: rows.length
|
|
25268
|
+
};
|
|
25269
|
+
}
|
|
25270
|
+
async testConnection() {
|
|
25271
|
+
const result = await this.query("SELECT current_database() as db, version() as version", true);
|
|
25272
|
+
const row = result.rows[0] ?? {};
|
|
25273
|
+
return {
|
|
25274
|
+
database: String(row.db ?? ""),
|
|
25275
|
+
version: String(row.version ?? "")
|
|
25276
|
+
};
|
|
25277
|
+
}
|
|
25278
|
+
async getCurrentDatabase() {
|
|
25279
|
+
const result = await this.query("SELECT current_database() as db", true);
|
|
25280
|
+
const row = result.rows[0] ?? {};
|
|
25281
|
+
return String(row.db ?? "");
|
|
25282
|
+
}
|
|
25283
|
+
parseApiError(data, httpStatus) {
|
|
25284
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
25285
|
+
const errObj = "error" in data && data.error ? data.error : data;
|
|
25286
|
+
const pgCode = this.extractPgErrorCode(errObj);
|
|
25287
|
+
const message = this.extractErrorMessage(errObj);
|
|
25288
|
+
const detail = this.extractField(errObj, ["details", "detail"]);
|
|
25289
|
+
const hint = this.extractField(errObj, ["hint"]);
|
|
25290
|
+
return this.createPgError({
|
|
25291
|
+
message,
|
|
25292
|
+
code: pgCode,
|
|
25293
|
+
detail,
|
|
25294
|
+
hint,
|
|
25295
|
+
httpStatus,
|
|
25296
|
+
supabaseErrorCode: typeof errObj === "object" && errObj && "code" in errObj ? String(errObj.code ?? "") : undefined
|
|
25297
|
+
});
|
|
25298
|
+
}
|
|
25299
|
+
return this.createPgError({
|
|
25300
|
+
message: `Supabase API error (HTTP ${httpStatus})`,
|
|
25301
|
+
httpStatus
|
|
25302
|
+
});
|
|
25303
|
+
}
|
|
25304
|
+
extractPgErrorCode(errObj) {
|
|
25305
|
+
if (!errObj || typeof errObj !== "object")
|
|
25306
|
+
return;
|
|
25307
|
+
const obj = errObj;
|
|
25308
|
+
if (typeof obj.code === "string") {
|
|
25309
|
+
const code = obj.code;
|
|
25310
|
+
if (/^\d{5}$/.test(code)) {
|
|
25311
|
+
return code;
|
|
25312
|
+
}
|
|
25313
|
+
return this.mapSupabaseCodeToPg(code);
|
|
25314
|
+
}
|
|
25315
|
+
return;
|
|
25316
|
+
}
|
|
25317
|
+
mapSupabaseCodeToPg(code) {
|
|
25318
|
+
const mapping = {
|
|
25319
|
+
PGRST301: "28000",
|
|
25320
|
+
PGRST302: "28P01",
|
|
25321
|
+
"42501": "42501",
|
|
25322
|
+
PGRST000: "42501",
|
|
25323
|
+
"42601": "42601",
|
|
25324
|
+
"42P01": "42P01",
|
|
25325
|
+
PGRST200: "42P01",
|
|
25326
|
+
"42883": "42883",
|
|
25327
|
+
"08000": "08000",
|
|
25328
|
+
"08003": "08003",
|
|
25329
|
+
"08006": "08006",
|
|
25330
|
+
"42710": "42710"
|
|
25331
|
+
};
|
|
25332
|
+
return mapping[code];
|
|
25333
|
+
}
|
|
25334
|
+
extractErrorMessage(errObj) {
|
|
25335
|
+
if (!errObj || typeof errObj !== "object") {
|
|
25336
|
+
return "Unknown Supabase API error";
|
|
25337
|
+
}
|
|
25338
|
+
const obj = errObj;
|
|
25339
|
+
for (const field of ["message", "error", "msg", "description"]) {
|
|
25340
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
25341
|
+
return obj[field];
|
|
25342
|
+
}
|
|
25343
|
+
}
|
|
25344
|
+
if (obj.error && typeof obj.error === "object") {
|
|
25345
|
+
return this.extractErrorMessage(obj.error);
|
|
25346
|
+
}
|
|
25347
|
+
return "Unknown Supabase API error";
|
|
25348
|
+
}
|
|
25349
|
+
extractField(errObj, fieldNames) {
|
|
25350
|
+
if (!errObj || typeof errObj !== "object")
|
|
25351
|
+
return;
|
|
25352
|
+
const obj = errObj;
|
|
25353
|
+
for (const field of fieldNames) {
|
|
25354
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
25355
|
+
return obj[field];
|
|
25356
|
+
}
|
|
25357
|
+
}
|
|
25358
|
+
return;
|
|
25359
|
+
}
|
|
25360
|
+
createPgError(opts) {
|
|
25361
|
+
const err = new Error(opts.message);
|
|
25362
|
+
if (opts.code)
|
|
25363
|
+
err.code = opts.code;
|
|
25364
|
+
if (opts.detail)
|
|
25365
|
+
err.detail = opts.detail;
|
|
25366
|
+
if (opts.hint)
|
|
25367
|
+
err.hint = opts.hint;
|
|
25368
|
+
if (opts.httpStatus)
|
|
25369
|
+
err.httpStatus = opts.httpStatus;
|
|
25370
|
+
if (opts.supabaseErrorCode)
|
|
25371
|
+
err.supabaseErrorCode = opts.supabaseErrorCode;
|
|
25372
|
+
return err;
|
|
25373
|
+
}
|
|
25374
|
+
}
|
|
25375
|
+
async function fetchPoolerDatabaseUrl(config2, username) {
|
|
25376
|
+
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config2.projectRef)}/config/database/pooler`;
|
|
25377
|
+
try {
|
|
25378
|
+
const response = await fetch(url, {
|
|
25379
|
+
method: "GET",
|
|
25380
|
+
headers: {
|
|
25381
|
+
Authorization: `Bearer ${config2.accessToken}`
|
|
25382
|
+
}
|
|
25383
|
+
});
|
|
25384
|
+
if (!response.ok) {
|
|
25385
|
+
return null;
|
|
25386
|
+
}
|
|
25387
|
+
const data = await response.json();
|
|
25388
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
25389
|
+
const pooler = data[0];
|
|
25390
|
+
if (pooler.db_host && pooler.db_port && pooler.db_name) {
|
|
25391
|
+
return `postgresql://${username}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
|
|
25392
|
+
}
|
|
25393
|
+
if (typeof pooler.connection_string === "string") {
|
|
25394
|
+
try {
|
|
25395
|
+
const connUrl = new URL(pooler.connection_string);
|
|
25396
|
+
const portPart = connUrl.port ? `:${connUrl.port}` : "";
|
|
25397
|
+
return `postgresql://${username}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
|
|
25398
|
+
} catch {
|
|
25399
|
+
return null;
|
|
25400
|
+
}
|
|
25401
|
+
}
|
|
25402
|
+
}
|
|
25403
|
+
return null;
|
|
25404
|
+
} catch {
|
|
25405
|
+
return null;
|
|
25406
|
+
}
|
|
25407
|
+
}
|
|
25408
|
+
function resolveSupabaseConfig(opts) {
|
|
25409
|
+
const accessToken = opts.accessToken?.trim() || process.env.SUPABASE_ACCESS_TOKEN?.trim() || "";
|
|
25410
|
+
const projectRef = opts.projectRef?.trim() || process.env.SUPABASE_PROJECT_REF?.trim() || "";
|
|
25411
|
+
if (!accessToken) {
|
|
25412
|
+
throw new Error(`Supabase access token is required.
|
|
25413
|
+
` + `Provide it via --supabase-access-token or SUPABASE_ACCESS_TOKEN environment variable.
|
|
25414
|
+
` + "Generate a token at: https://supabase.com/dashboard/account/tokens");
|
|
25415
|
+
}
|
|
25416
|
+
if (!projectRef) {
|
|
25417
|
+
throw new Error(`Supabase project reference is required.
|
|
25418
|
+
` + `Provide it via --supabase-project-ref or SUPABASE_PROJECT_REF environment variable.
|
|
25419
|
+
` + "Find your project ref in the Supabase dashboard URL: https://supabase.com/dashboard/project/<ref>");
|
|
25420
|
+
}
|
|
25421
|
+
return { accessToken, projectRef };
|
|
25422
|
+
}
|
|
25423
|
+
function extractProjectRefFromUrl(dbUrl) {
|
|
25424
|
+
try {
|
|
25425
|
+
const url = new URL(dbUrl);
|
|
25426
|
+
const host = url.hostname;
|
|
25427
|
+
const match = host.match(/^(?:db\.)?([^.]+)\.supabase\.co$/i);
|
|
25428
|
+
if (match && match[1]) {
|
|
25429
|
+
return match[1];
|
|
25430
|
+
}
|
|
25431
|
+
if (host.includes("pooler.supabase.com")) {
|
|
25432
|
+
const username = url.username;
|
|
25433
|
+
const userMatch = username.match(/^postgres\.([a-z0-9]+)$/i);
|
|
25434
|
+
if (userMatch && userMatch[1]) {
|
|
25435
|
+
return userMatch[1];
|
|
25436
|
+
}
|
|
25437
|
+
}
|
|
25438
|
+
const poolerMatch = host.match(/^([a-z0-9]+)\.pooler\.supabase\.com$/i);
|
|
25439
|
+
if (poolerMatch && poolerMatch[1] && !poolerMatch[1].startsWith("aws-")) {
|
|
25440
|
+
return poolerMatch[1];
|
|
25441
|
+
}
|
|
25442
|
+
return;
|
|
25443
|
+
} catch {
|
|
25444
|
+
return;
|
|
25445
|
+
}
|
|
25446
|
+
}
|
|
25447
|
+
async function applyInitPlanViaSupabase(params) {
|
|
25448
|
+
const applied = [];
|
|
25449
|
+
const skippedOptional = [];
|
|
25450
|
+
const executeStep = async (step) => {
|
|
25451
|
+
const wrappedSql = `BEGIN;
|
|
25452
|
+
${step.sql}
|
|
25453
|
+
COMMIT;`;
|
|
25454
|
+
await params.client.query(wrappedSql, false);
|
|
25455
|
+
};
|
|
25456
|
+
for (const step of params.plan.steps.filter((s) => !s.optional)) {
|
|
25457
|
+
try {
|
|
25458
|
+
if (params.verbose) {
|
|
25459
|
+
console.log(`Executing step: ${step.name}`);
|
|
25460
|
+
}
|
|
25461
|
+
await executeStep(step);
|
|
25462
|
+
applied.push(step.name);
|
|
25463
|
+
} catch (e) {
|
|
25464
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
25465
|
+
const errAny = e;
|
|
25466
|
+
const wrapped = new Error(`Failed at step "${step.name}": ${msg}`);
|
|
25467
|
+
const pgErrorFields = [
|
|
25468
|
+
"code",
|
|
25469
|
+
"detail",
|
|
25470
|
+
"hint",
|
|
25471
|
+
"position",
|
|
25472
|
+
"internalPosition",
|
|
25473
|
+
"internalQuery",
|
|
25474
|
+
"where",
|
|
25475
|
+
"schema",
|
|
25476
|
+
"table",
|
|
25477
|
+
"column",
|
|
25478
|
+
"dataType",
|
|
25479
|
+
"constraint",
|
|
25480
|
+
"file",
|
|
25481
|
+
"line",
|
|
25482
|
+
"routine",
|
|
25483
|
+
"httpStatus",
|
|
25484
|
+
"supabaseErrorCode"
|
|
25485
|
+
];
|
|
25486
|
+
for (const field of pgErrorFields) {
|
|
25487
|
+
if (errAny[field] !== undefined) {
|
|
25488
|
+
wrapped[field] = errAny[field];
|
|
25489
|
+
}
|
|
25490
|
+
}
|
|
25491
|
+
if (e instanceof Error && e.stack) {
|
|
25492
|
+
wrapped.stack = e.stack;
|
|
25493
|
+
}
|
|
25494
|
+
throw wrapped;
|
|
25495
|
+
}
|
|
25496
|
+
}
|
|
25497
|
+
for (const step of params.plan.steps.filter((s) => s.optional)) {
|
|
25498
|
+
try {
|
|
25499
|
+
if (params.verbose) {
|
|
25500
|
+
console.log(`Executing optional step: ${step.name}`);
|
|
25501
|
+
}
|
|
25502
|
+
await executeStep(step);
|
|
25503
|
+
applied.push(step.name);
|
|
25504
|
+
} catch {
|
|
25505
|
+
skippedOptional.push(step.name);
|
|
25506
|
+
}
|
|
25507
|
+
}
|
|
25508
|
+
return { applied, skippedOptional };
|
|
25509
|
+
}
|
|
25510
|
+
async function verifyInitSetupViaSupabase(params) {
|
|
25511
|
+
const missingRequired = [];
|
|
25512
|
+
const missingOptional = [];
|
|
25513
|
+
const role = params.monitoringUser;
|
|
25514
|
+
const db = params.database;
|
|
25515
|
+
if (!isValidIdentifier(role)) {
|
|
25516
|
+
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).`);
|
|
25517
|
+
}
|
|
25518
|
+
const roleRes = await params.client.query(`SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
25519
|
+
const roleExists = roleRes.rowCount > 0;
|
|
25520
|
+
if (!roleExists) {
|
|
25521
|
+
missingRequired.push(`role "${role}" does not exist`);
|
|
25522
|
+
return { ok: false, missingRequired, missingOptional };
|
|
25523
|
+
}
|
|
25524
|
+
const connectRes = await params.client.query(`SELECT has_database_privilege('${escapeLiteral2(role)}', '${escapeLiteral2(db)}', 'CONNECT') as ok`, true);
|
|
25525
|
+
if (!connectRes.rows?.[0]?.ok) {
|
|
25526
|
+
missingRequired.push(`CONNECT on database "${db}"`);
|
|
25527
|
+
}
|
|
25528
|
+
const pgMonitorRes = await params.client.query(`SELECT pg_has_role('${escapeLiteral2(role)}', 'pg_monitor', 'member') as ok`, true);
|
|
25529
|
+
if (!pgMonitorRes.rows?.[0]?.ok) {
|
|
25530
|
+
missingRequired.push("membership in role pg_monitor");
|
|
25531
|
+
}
|
|
25532
|
+
const pgIndexRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'pg_catalog.pg_index', 'SELECT') as ok`, true);
|
|
25533
|
+
if (!pgIndexRes.rows?.[0]?.ok) {
|
|
25534
|
+
missingRequired.push("SELECT on pg_catalog.pg_index");
|
|
25535
|
+
}
|
|
25536
|
+
const schemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'postgres_ai'", true);
|
|
25537
|
+
if (schemaExistsRes.rowCount === 0) {
|
|
25538
|
+
missingRequired.push("schema postgres_ai exists");
|
|
25539
|
+
} else {
|
|
25540
|
+
const schemaPrivRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'postgres_ai', 'USAGE') as ok`, true);
|
|
25541
|
+
if (!schemaPrivRes.rows?.[0]?.ok) {
|
|
25542
|
+
missingRequired.push("USAGE on schema postgres_ai");
|
|
25543
|
+
}
|
|
25544
|
+
}
|
|
25545
|
+
const viewExistsRes = await params.client.query("SELECT to_regclass('postgres_ai.pg_statistic') IS NOT NULL as ok", true);
|
|
25546
|
+
if (!viewExistsRes.rows?.[0]?.ok) {
|
|
25547
|
+
missingRequired.push("view postgres_ai.pg_statistic exists");
|
|
25548
|
+
} else {
|
|
25549
|
+
const viewPrivRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'postgres_ai.pg_statistic', 'SELECT') as ok`, true);
|
|
25550
|
+
if (!viewPrivRes.rows?.[0]?.ok) {
|
|
25551
|
+
missingRequired.push("SELECT on view postgres_ai.pg_statistic");
|
|
25552
|
+
}
|
|
25553
|
+
}
|
|
25554
|
+
const publicSchemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'public'", true);
|
|
25555
|
+
if (publicSchemaExistsRes.rowCount === 0) {
|
|
25556
|
+
missingRequired.push("schema public exists");
|
|
25557
|
+
} else {
|
|
25558
|
+
const schemaUsageRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'public', 'USAGE') as ok`, true);
|
|
25559
|
+
if (!schemaUsageRes.rows?.[0]?.ok) {
|
|
25560
|
+
missingRequired.push("USAGE on schema public");
|
|
25561
|
+
}
|
|
25562
|
+
}
|
|
25563
|
+
const rolcfgRes = await params.client.query(`SELECT rolconfig FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
25564
|
+
const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
|
|
25565
|
+
const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
|
|
25566
|
+
if (typeof spLine !== "string" || !spLine) {
|
|
25567
|
+
missingRequired.push("role search_path is set");
|
|
25568
|
+
} else {
|
|
25569
|
+
const sp = spLine.toLowerCase();
|
|
25570
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
25571
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
25572
|
+
}
|
|
25573
|
+
}
|
|
25574
|
+
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);
|
|
25575
|
+
if (explainFnExistsRes.rowCount === 0) {
|
|
25576
|
+
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
25577
|
+
} else {
|
|
25578
|
+
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
25579
|
+
if (!explainFnRes.rows?.[0]?.ok) {
|
|
25580
|
+
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
25581
|
+
}
|
|
25582
|
+
}
|
|
25583
|
+
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);
|
|
25584
|
+
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
25585
|
+
missingRequired.push("function postgres_ai.table_describe exists");
|
|
25586
|
+
} else {
|
|
25587
|
+
const tableDescribeFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.table_describe(text)', 'EXECUTE') as ok`, true);
|
|
25588
|
+
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
25589
|
+
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
25590
|
+
}
|
|
25591
|
+
}
|
|
25592
|
+
if (params.includeOptionalPermissions) {
|
|
25593
|
+
const extRes = await params.client.query("SELECT 1 FROM pg_extension WHERE extname = 'rds_tools'", true);
|
|
25594
|
+
if (extRes.rowCount === 0) {
|
|
25595
|
+
missingOptional.push("extension rds_tools");
|
|
25596
|
+
} else {
|
|
25597
|
+
try {
|
|
25598
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok`, true);
|
|
25599
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
25600
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
25601
|
+
}
|
|
25602
|
+
} catch {
|
|
25603
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
25604
|
+
}
|
|
25605
|
+
}
|
|
25606
|
+
const optionalFns = [
|
|
25607
|
+
"pg_catalog.pg_stat_file(text)",
|
|
25608
|
+
"pg_catalog.pg_stat_file(text, boolean)",
|
|
25609
|
+
"pg_catalog.pg_ls_dir(text)",
|
|
25610
|
+
"pg_catalog.pg_ls_dir(text, boolean, boolean)"
|
|
25611
|
+
];
|
|
25612
|
+
for (const fn of optionalFns) {
|
|
25613
|
+
try {
|
|
25614
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', '${fn}', 'EXECUTE') as ok`, true);
|
|
25615
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
25616
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
25617
|
+
}
|
|
25618
|
+
} catch {
|
|
25619
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
25620
|
+
}
|
|
25621
|
+
}
|
|
25622
|
+
}
|
|
25623
|
+
return {
|
|
25624
|
+
ok: missingRequired.length === 0,
|
|
25625
|
+
missingRequired,
|
|
25626
|
+
missingOptional
|
|
25627
|
+
};
|
|
25628
|
+
}
|
|
25629
|
+
function isValidIdentifier(name) {
|
|
25630
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(name);
|
|
25631
|
+
}
|
|
25632
|
+
function escapeLiteral2(value) {
|
|
25633
|
+
if (value.includes("\x00")) {
|
|
25634
|
+
throw new Error("SQL literal cannot contain null bytes");
|
|
25635
|
+
}
|
|
25636
|
+
return value.replace(/'/g, "''");
|
|
25637
|
+
}
|
|
25638
|
+
|
|
24462
25639
|
// lib/pkce.ts
|
|
24463
25640
|
import * as crypto from "crypto";
|
|
24464
25641
|
function generateRandomString(length = 64) {
|
|
@@ -24958,17 +26135,17 @@ where
|
|
|
24958
26135
|
statement_timeout_seconds: 300
|
|
24959
26136
|
},
|
|
24960
26137
|
pg_invalid_indexes: {
|
|
24961
|
-
description: "This metric identifies invalid indexes in the database. It provides insights into
|
|
26138
|
+
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
26139
|
sqls: {
|
|
24963
26140
|
11: `with fk_indexes as ( /* pgwatch_generated */
|
|
24964
26141
|
select
|
|
24965
|
-
schemaname as
|
|
24966
|
-
|
|
24967
|
-
(
|
|
24968
|
-
(
|
|
24969
|
-
|
|
24970
|
-
|
|
24971
|
-
|
|
26142
|
+
schemaname as schema_name,
|
|
26143
|
+
indexrelid,
|
|
26144
|
+
(indexrelid::regclass)::text as index_name,
|
|
26145
|
+
(relid::regclass)::text as table_name,
|
|
26146
|
+
(confrelid::regclass)::text as fk_table_ref,
|
|
26147
|
+
array_to_string(indclass, ', ') as opclasses
|
|
26148
|
+
from pg_stat_all_indexes
|
|
24972
26149
|
join pg_index using (indexrelid)
|
|
24973
26150
|
left join pg_constraint
|
|
24974
26151
|
on array_to_string(indkey, ',') = array_to_string(conkey, ',')
|
|
@@ -24977,37 +26154,58 @@ where
|
|
|
24977
26154
|
and contype = 'f'
|
|
24978
26155
|
where idx_scan = 0
|
|
24979
26156
|
and indisunique is false
|
|
24980
|
-
and conkey is not null
|
|
24981
|
-
),
|
|
26157
|
+
and conkey is not null
|
|
26158
|
+
),
|
|
26159
|
+
-- Find valid indexes that could be duplicates (same table, same columns)
|
|
26160
|
+
valid_duplicates as (
|
|
26161
|
+
select
|
|
26162
|
+
inv.indexrelid as invalid_indexrelid,
|
|
26163
|
+
val.indexrelid as valid_indexrelid,
|
|
26164
|
+
(val.indexrelid::regclass)::text as valid_index_name,
|
|
26165
|
+
pg_get_indexdef(val.indexrelid) as valid_index_definition
|
|
26166
|
+
from pg_index inv
|
|
26167
|
+
join pg_index val on inv.indrelid = val.indrelid -- same table
|
|
26168
|
+
and inv.indkey = val.indkey -- same columns (in same order)
|
|
26169
|
+
and inv.indexrelid != val.indexrelid -- different index
|
|
26170
|
+
and val.indisvalid = true -- valid index
|
|
26171
|
+
where inv.indisvalid = false
|
|
26172
|
+
),
|
|
26173
|
+
data as (
|
|
24982
26174
|
select
|
|
24983
26175
|
pci.relname as tag_index_name,
|
|
24984
26176
|
pn.nspname as tag_schema_name,
|
|
24985
26177
|
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
26178
|
coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
|
|
24990
26179
|
pg_get_indexdef(pidx.indexrelid) as index_definition,
|
|
24991
|
-
pg_relation_size(pidx.indexrelid) index_size_bytes,
|
|
26180
|
+
pg_relation_size(pidx.indexrelid) as index_size_bytes,
|
|
26181
|
+
-- Constraint info
|
|
26182
|
+
pidx.indisprimary as is_pk,
|
|
26183
|
+
pidx.indisunique as is_unique,
|
|
26184
|
+
con.conname as constraint_name,
|
|
26185
|
+
-- Table row estimate
|
|
26186
|
+
pct.reltuples::bigint as table_row_estimate,
|
|
26187
|
+
-- Valid duplicate check
|
|
26188
|
+
(vd.valid_indexrelid is not null) as has_valid_duplicate,
|
|
26189
|
+
vd.valid_index_name,
|
|
26190
|
+
vd.valid_index_definition,
|
|
26191
|
+
-- FK support check
|
|
24992
26192
|
((
|
|
24993
26193
|
select count(1)
|
|
24994
26194
|
from fk_indexes fi
|
|
24995
|
-
where
|
|
24996
|
-
fi.
|
|
24997
|
-
and fi.tag_opclasses like (array_to_string(pidx.indclass, ', ') || '%')
|
|
26195
|
+
where fi.fk_table_ref = pct.relname
|
|
26196
|
+
and fi.opclasses like (array_to_string(pidx.indclass, ', ') || '%')
|
|
24998
26197
|
) > 0)::int as supports_fk
|
|
24999
26198
|
from pg_index pidx
|
|
25000
|
-
join pg_class
|
|
25001
|
-
join pg_class
|
|
26199
|
+
join pg_class pci on pci.oid = pidx.indexrelid
|
|
26200
|
+
join pg_class pct on pct.oid = pidx.indrelid
|
|
25002
26201
|
left join pg_namespace pn on pn.oid = pct.relnamespace
|
|
26202
|
+
left join pg_constraint con on con.conindid = pidx.indexrelid
|
|
26203
|
+
left join valid_duplicates vd on vd.invalid_indexrelid = pidx.indexrelid
|
|
25003
26204
|
where pidx.indisvalid = false
|
|
25004
|
-
),
|
|
25005
|
-
|
|
25006
|
-
sum(index_size_bytes) as index_size_bytes_sum
|
|
25007
|
-
from data
|
|
25008
|
-
), num_data as (
|
|
26205
|
+
),
|
|
26206
|
+
num_data as (
|
|
25009
26207
|
select
|
|
25010
|
-
row_number() over () num,
|
|
26208
|
+
row_number() over () as num,
|
|
25011
26209
|
data.*
|
|
25012
26210
|
from data
|
|
25013
26211
|
)
|
|
@@ -25626,7 +26824,14 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
|
|
|
25626
26824
|
index_size_bytes: indexSizeBytes,
|
|
25627
26825
|
index_size_pretty: formatBytes(indexSizeBytes),
|
|
25628
26826
|
index_definition: String(transformed.index_definition || ""),
|
|
25629
|
-
supports_fk: toBool(transformed.supports_fk)
|
|
26827
|
+
supports_fk: toBool(transformed.supports_fk),
|
|
26828
|
+
is_pk: toBool(transformed.is_pk),
|
|
26829
|
+
is_unique: toBool(transformed.is_unique),
|
|
26830
|
+
constraint_name: transformed.constraint_name ? String(transformed.constraint_name) : null,
|
|
26831
|
+
table_row_estimate: parseInt(String(transformed.table_row_estimate || 0), 10),
|
|
26832
|
+
has_valid_duplicate: toBool(transformed.has_valid_duplicate),
|
|
26833
|
+
valid_duplicate_name: transformed.valid_index_name ? String(transformed.valid_index_name) : null,
|
|
26834
|
+
valid_duplicate_definition: transformed.valid_index_definition ? String(transformed.valid_index_definition) : null
|
|
25630
26835
|
};
|
|
25631
26836
|
});
|
|
25632
26837
|
}
|
|
@@ -26715,7 +27920,7 @@ program2.command("set-default-project <project>").description("store default pro
|
|
|
26715
27920
|
writeConfig({ defaultProject: value });
|
|
26716
27921
|
console.log(`Default project saved: ${value}`);
|
|
26717
27922
|
});
|
|
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", [
|
|
27923
|
+
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
27924
|
"",
|
|
26720
27925
|
"Examples:",
|
|
26721
27926
|
" postgresai prepare-db postgresql://admin@host:5432/dbname",
|
|
@@ -26751,21 +27956,60 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26751
27956
|
" postgresai prepare-db <conn> --reset-password --password '...'",
|
|
26752
27957
|
"",
|
|
26753
27958
|
"Offline SQL plan (no DB connection):",
|
|
26754
|
-
" postgresai prepare-db --print-sql"
|
|
27959
|
+
" postgresai prepare-db --print-sql",
|
|
27960
|
+
"",
|
|
27961
|
+
"Supabase mode (use Management API instead of direct connection):",
|
|
27962
|
+
" postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27963
|
+
" SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27964
|
+
"",
|
|
27965
|
+
" Generate a token at: https://supabase.com/dashboard/account/tokens",
|
|
27966
|
+
" Find your project ref in: https://supabase.com/dashboard/project/<ref>",
|
|
27967
|
+
"",
|
|
27968
|
+
"Provider-specific behavior (for direct connections):",
|
|
27969
|
+
" --provider supabase Skip role creation (create user in Supabase dashboard)",
|
|
27970
|
+
" Skip ALTER USER (restricted by Supabase)"
|
|
26755
27971
|
].join(`
|
|
26756
27972
|
`)).action(async (conn, opts, cmd) => {
|
|
26757
|
-
|
|
26758
|
-
|
|
27973
|
+
const jsonOutput = opts.json;
|
|
27974
|
+
const outputJson = (data) => {
|
|
27975
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27976
|
+
};
|
|
27977
|
+
const outputError = (error2) => {
|
|
27978
|
+
if (jsonOutput) {
|
|
27979
|
+
outputJson({
|
|
27980
|
+
success: false,
|
|
27981
|
+
mode: opts.supabase ? "supabase" : "direct",
|
|
27982
|
+
error: error2
|
|
27983
|
+
});
|
|
27984
|
+
} else {
|
|
27985
|
+
console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error2.message}`);
|
|
27986
|
+
if (error2.step)
|
|
27987
|
+
console.error(` Step: ${error2.step}`);
|
|
27988
|
+
if (error2.code)
|
|
27989
|
+
console.error(` Code: ${error2.code}`);
|
|
27990
|
+
if (error2.detail)
|
|
27991
|
+
console.error(` Detail: ${error2.detail}`);
|
|
27992
|
+
if (error2.hint)
|
|
27993
|
+
console.error(` Hint: ${error2.hint}`);
|
|
27994
|
+
if (error2.httpStatus)
|
|
27995
|
+
console.error(` HTTP Status: ${error2.httpStatus}`);
|
|
27996
|
+
}
|
|
26759
27997
|
process.exitCode = 1;
|
|
27998
|
+
};
|
|
27999
|
+
if (opts.verify && opts.resetPassword) {
|
|
28000
|
+
outputError({ message: "Provide only one of --verify or --reset-password" });
|
|
26760
28001
|
return;
|
|
26761
28002
|
}
|
|
26762
28003
|
if (opts.verify && opts.printSql) {
|
|
26763
|
-
|
|
26764
|
-
process.exitCode = 1;
|
|
28004
|
+
outputError({ message: "--verify cannot be combined with --print-sql" });
|
|
26765
28005
|
return;
|
|
26766
28006
|
}
|
|
26767
28007
|
const shouldPrintSql = !!opts.printSql;
|
|
26768
28008
|
const redactPasswords = (sql) => redactPasswordsInSql(sql);
|
|
28009
|
+
const providerWarning = validateProvider(opts.provider);
|
|
28010
|
+
if (providerWarning) {
|
|
28011
|
+
console.warn(`\u26A0 ${providerWarning}`);
|
|
28012
|
+
}
|
|
26769
28013
|
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
26770
28014
|
if (shouldPrintSql) {
|
|
26771
28015
|
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
@@ -26775,12 +28019,14 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26775
28019
|
database,
|
|
26776
28020
|
monitoringUser: opts.monitoringUser,
|
|
26777
28021
|
monitoringPassword: monPassword,
|
|
26778
|
-
includeOptionalPermissions: includeOptionalPermissions2
|
|
28022
|
+
includeOptionalPermissions: includeOptionalPermissions2,
|
|
28023
|
+
provider: opts.provider
|
|
26779
28024
|
});
|
|
26780
28025
|
console.log(`
|
|
26781
28026
|
--- SQL plan (offline; not connected) ---`);
|
|
26782
28027
|
console.log(`-- database: ${database}`);
|
|
26783
28028
|
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
28029
|
+
console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
|
|
26784
28030
|
console.log(`-- optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
|
|
26785
28031
|
for (const step of plan.steps) {
|
|
26786
28032
|
console.log(`
|
|
@@ -26794,6 +28040,285 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26794
28040
|
return;
|
|
26795
28041
|
}
|
|
26796
28042
|
}
|
|
28043
|
+
if (opts.supabase) {
|
|
28044
|
+
let supabaseConfig;
|
|
28045
|
+
try {
|
|
28046
|
+
let projectRef = opts.supabaseProjectRef;
|
|
28047
|
+
if (!projectRef && conn) {
|
|
28048
|
+
projectRef = extractProjectRefFromUrl(conn);
|
|
28049
|
+
}
|
|
28050
|
+
supabaseConfig = resolveSupabaseConfig({
|
|
28051
|
+
accessToken: opts.supabaseAccessToken,
|
|
28052
|
+
projectRef
|
|
28053
|
+
});
|
|
28054
|
+
} catch (e) {
|
|
28055
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
28056
|
+
outputError({ message: msg });
|
|
28057
|
+
return;
|
|
28058
|
+
}
|
|
28059
|
+
const includeOptionalPermissions2 = !opts.skipOptionalPermissions;
|
|
28060
|
+
if (!jsonOutput) {
|
|
28061
|
+
console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
|
|
28062
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
28063
|
+
console.log(`Optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
|
|
28064
|
+
}
|
|
28065
|
+
const supabaseClient = new SupabaseClient(supabaseConfig);
|
|
28066
|
+
let databaseUrl = null;
|
|
28067
|
+
if (jsonOutput) {
|
|
28068
|
+
databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
|
|
28069
|
+
}
|
|
28070
|
+
try {
|
|
28071
|
+
const database = await supabaseClient.getCurrentDatabase();
|
|
28072
|
+
if (!database) {
|
|
28073
|
+
throw new Error("Failed to resolve current database name");
|
|
28074
|
+
}
|
|
28075
|
+
if (!jsonOutput) {
|
|
28076
|
+
console.log(`Database: ${database}`);
|
|
28077
|
+
}
|
|
28078
|
+
if (opts.verify) {
|
|
28079
|
+
const v = await verifyInitSetupViaSupabase({
|
|
28080
|
+
client: supabaseClient,
|
|
28081
|
+
database,
|
|
28082
|
+
monitoringUser: opts.monitoringUser,
|
|
28083
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
28084
|
+
});
|
|
28085
|
+
if (v.ok) {
|
|
28086
|
+
if (jsonOutput) {
|
|
28087
|
+
const result = {
|
|
28088
|
+
success: true,
|
|
28089
|
+
mode: "supabase",
|
|
28090
|
+
action: "verify",
|
|
28091
|
+
database,
|
|
28092
|
+
monitoringUser: opts.monitoringUser,
|
|
28093
|
+
verified: true,
|
|
28094
|
+
missingOptional: v.missingOptional
|
|
28095
|
+
};
|
|
28096
|
+
if (databaseUrl) {
|
|
28097
|
+
result.databaseUrl = databaseUrl;
|
|
28098
|
+
}
|
|
28099
|
+
outputJson(result);
|
|
28100
|
+
} else {
|
|
28101
|
+
console.log("\u2713 prepare-db verify: OK");
|
|
28102
|
+
if (v.missingOptional.length > 0) {
|
|
28103
|
+
console.log("\u26A0 Optional items missing:");
|
|
28104
|
+
for (const m of v.missingOptional)
|
|
28105
|
+
console.log(`- ${m}`);
|
|
28106
|
+
}
|
|
28107
|
+
}
|
|
28108
|
+
return;
|
|
28109
|
+
}
|
|
28110
|
+
if (jsonOutput) {
|
|
28111
|
+
const result = {
|
|
28112
|
+
success: false,
|
|
28113
|
+
mode: "supabase",
|
|
28114
|
+
action: "verify",
|
|
28115
|
+
database,
|
|
28116
|
+
monitoringUser: opts.monitoringUser,
|
|
28117
|
+
verified: false,
|
|
28118
|
+
missingRequired: v.missingRequired,
|
|
28119
|
+
missingOptional: v.missingOptional
|
|
28120
|
+
};
|
|
28121
|
+
if (databaseUrl) {
|
|
28122
|
+
result.databaseUrl = databaseUrl;
|
|
28123
|
+
}
|
|
28124
|
+
outputJson(result);
|
|
28125
|
+
} else {
|
|
28126
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
28127
|
+
for (const m of v.missingRequired)
|
|
28128
|
+
console.error(`- ${m}`);
|
|
28129
|
+
if (v.missingOptional.length > 0) {
|
|
28130
|
+
console.error("Optional items missing:");
|
|
28131
|
+
for (const m of v.missingOptional)
|
|
28132
|
+
console.error(`- ${m}`);
|
|
28133
|
+
}
|
|
28134
|
+
}
|
|
28135
|
+
process.exitCode = 1;
|
|
28136
|
+
return;
|
|
28137
|
+
}
|
|
28138
|
+
let monPassword;
|
|
28139
|
+
let passwordGenerated = false;
|
|
28140
|
+
try {
|
|
28141
|
+
const resolved = await resolveMonitoringPassword({
|
|
28142
|
+
passwordFlag: opts.password,
|
|
28143
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
28144
|
+
monitoringUser: opts.monitoringUser
|
|
28145
|
+
});
|
|
28146
|
+
monPassword = resolved.password;
|
|
28147
|
+
passwordGenerated = resolved.generated;
|
|
28148
|
+
if (resolved.generated) {
|
|
28149
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
28150
|
+
if (canPrint) {
|
|
28151
|
+
if (!jsonOutput) {
|
|
28152
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
28153
|
+
console.error("");
|
|
28154
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
28155
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
28156
|
+
console.error("");
|
|
28157
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
28158
|
+
}
|
|
28159
|
+
} else {
|
|
28160
|
+
console.error([
|
|
28161
|
+
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
28162
|
+
"",
|
|
28163
|
+
"Provide it explicitly:",
|
|
28164
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
28165
|
+
"",
|
|
28166
|
+
"Or (NOT recommended) print the generated password:",
|
|
28167
|
+
" --print-password"
|
|
28168
|
+
].join(`
|
|
28169
|
+
`));
|
|
28170
|
+
process.exitCode = 1;
|
|
28171
|
+
return;
|
|
28172
|
+
}
|
|
28173
|
+
}
|
|
28174
|
+
} catch (e) {
|
|
28175
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
28176
|
+
outputError({ message: msg });
|
|
28177
|
+
return;
|
|
28178
|
+
}
|
|
28179
|
+
const plan = await buildInitPlan({
|
|
28180
|
+
database,
|
|
28181
|
+
monitoringUser: opts.monitoringUser,
|
|
28182
|
+
monitoringPassword: monPassword,
|
|
28183
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
28184
|
+
});
|
|
28185
|
+
const supabaseApplicableSteps = plan.steps.filter((s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed");
|
|
28186
|
+
const effectivePlan = opts.resetPassword ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") } : { ...plan, steps: supabaseApplicableSteps };
|
|
28187
|
+
if (shouldPrintSql) {
|
|
28188
|
+
console.log(`
|
|
28189
|
+
--- SQL plan ---`);
|
|
28190
|
+
for (const step of effectivePlan.steps) {
|
|
28191
|
+
console.log(`
|
|
28192
|
+
-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
28193
|
+
console.log(redactPasswords(step.sql));
|
|
28194
|
+
}
|
|
28195
|
+
console.log(`
|
|
28196
|
+
--- end SQL plan ---
|
|
28197
|
+
`);
|
|
28198
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
28199
|
+
return;
|
|
28200
|
+
}
|
|
28201
|
+
const { applied, skippedOptional } = await applyInitPlanViaSupabase({
|
|
28202
|
+
client: supabaseClient,
|
|
28203
|
+
plan: effectivePlan
|
|
28204
|
+
});
|
|
28205
|
+
if (jsonOutput) {
|
|
28206
|
+
const result = {
|
|
28207
|
+
success: true,
|
|
28208
|
+
mode: "supabase",
|
|
28209
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
28210
|
+
database,
|
|
28211
|
+
monitoringUser: opts.monitoringUser,
|
|
28212
|
+
applied,
|
|
28213
|
+
skippedOptional,
|
|
28214
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
28215
|
+
};
|
|
28216
|
+
if (passwordGenerated) {
|
|
28217
|
+
result.generatedPassword = monPassword;
|
|
28218
|
+
}
|
|
28219
|
+
if (databaseUrl) {
|
|
28220
|
+
result.databaseUrl = databaseUrl;
|
|
28221
|
+
}
|
|
28222
|
+
outputJson(result);
|
|
28223
|
+
} else {
|
|
28224
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
28225
|
+
if (skippedOptional.length > 0) {
|
|
28226
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
28227
|
+
for (const s of skippedOptional)
|
|
28228
|
+
console.log(`- ${s}`);
|
|
28229
|
+
}
|
|
28230
|
+
if (process.stdout.isTTY) {
|
|
28231
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28232
|
+
}
|
|
28233
|
+
}
|
|
28234
|
+
} catch (error2) {
|
|
28235
|
+
const errAny = error2;
|
|
28236
|
+
let message = "";
|
|
28237
|
+
if (error2 instanceof Error && error2.message) {
|
|
28238
|
+
message = error2.message;
|
|
28239
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
28240
|
+
message = errAny.message;
|
|
28241
|
+
} else {
|
|
28242
|
+
message = String(error2);
|
|
28243
|
+
}
|
|
28244
|
+
if (!message || message === "[object Object]") {
|
|
28245
|
+
message = "Unknown error";
|
|
28246
|
+
}
|
|
28247
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
28248
|
+
const failedStep = stepMatch?.[1];
|
|
28249
|
+
const errorObj = { message };
|
|
28250
|
+
if (failedStep)
|
|
28251
|
+
errorObj.step = failedStep;
|
|
28252
|
+
if (errAny && typeof errAny === "object") {
|
|
28253
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
28254
|
+
errorObj.code = errAny.code;
|
|
28255
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
28256
|
+
errorObj.detail = errAny.detail;
|
|
28257
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
28258
|
+
errorObj.hint = errAny.hint;
|
|
28259
|
+
if (typeof errAny.httpStatus === "number")
|
|
28260
|
+
errorObj.httpStatus = errAny.httpStatus;
|
|
28261
|
+
}
|
|
28262
|
+
if (jsonOutput) {
|
|
28263
|
+
outputJson({
|
|
28264
|
+
success: false,
|
|
28265
|
+
mode: "supabase",
|
|
28266
|
+
error: errorObj
|
|
28267
|
+
});
|
|
28268
|
+
process.exitCode = 1;
|
|
28269
|
+
} else {
|
|
28270
|
+
console.error(`Error: prepare-db (Supabase): ${message}`);
|
|
28271
|
+
if (failedStep) {
|
|
28272
|
+
console.error(` Step: ${failedStep}`);
|
|
28273
|
+
}
|
|
28274
|
+
if (errAny && typeof errAny === "object") {
|
|
28275
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
28276
|
+
console.error(` Code: ${errAny.code}`);
|
|
28277
|
+
}
|
|
28278
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
28279
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
28280
|
+
}
|
|
28281
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
28282
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
28283
|
+
}
|
|
28284
|
+
if (typeof errAny.httpStatus === "number") {
|
|
28285
|
+
console.error(` HTTP Status: ${errAny.httpStatus}`);
|
|
28286
|
+
}
|
|
28287
|
+
}
|
|
28288
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
28289
|
+
if (errAny.code === "42501") {
|
|
28290
|
+
if (failedStep === "01.role") {
|
|
28291
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
28292
|
+
} else if (failedStep === "03.permissions") {
|
|
28293
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
28294
|
+
}
|
|
28295
|
+
console.error(" Fix: ensure your Supabase access token has sufficient permissions");
|
|
28296
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
28297
|
+
}
|
|
28298
|
+
if (errAny.code === "42P06" || message.includes("already exists") && failedStep === "03.permissions") {
|
|
28299
|
+
console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
|
|
28300
|
+
console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
|
|
28301
|
+
}
|
|
28302
|
+
}
|
|
28303
|
+
if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
|
|
28304
|
+
if (errAny.httpStatus === 401) {
|
|
28305
|
+
console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
|
|
28306
|
+
}
|
|
28307
|
+
if (errAny.httpStatus === 403) {
|
|
28308
|
+
console.error(" Hint: access denied; check your token permissions and project access");
|
|
28309
|
+
}
|
|
28310
|
+
if (errAny.httpStatus === 404) {
|
|
28311
|
+
console.error(" Hint: project not found; verify the project reference is correct");
|
|
28312
|
+
}
|
|
28313
|
+
if (errAny.httpStatus === 429) {
|
|
28314
|
+
console.error(" Hint: rate limited; wait a moment and try again");
|
|
28315
|
+
}
|
|
28316
|
+
}
|
|
28317
|
+
process.exitCode = 1;
|
|
28318
|
+
}
|
|
28319
|
+
}
|
|
28320
|
+
return;
|
|
28321
|
+
}
|
|
26797
28322
|
let adminConn;
|
|
26798
28323
|
try {
|
|
26799
28324
|
adminConn = resolveAdminConnection({
|
|
@@ -26808,18 +28333,24 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26808
28333
|
});
|
|
26809
28334
|
} catch (e) {
|
|
26810
28335
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26811
|
-
|
|
26812
|
-
|
|
26813
|
-
|
|
26814
|
-
|
|
28336
|
+
if (jsonOutput) {
|
|
28337
|
+
outputError({ message: msg });
|
|
28338
|
+
} else {
|
|
28339
|
+
console.error(`Error: prepare-db: ${msg}`);
|
|
28340
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
28341
|
+
console.error("");
|
|
28342
|
+
cmd.outputHelp({ error: true });
|
|
28343
|
+
}
|
|
28344
|
+
process.exitCode = 1;
|
|
26815
28345
|
}
|
|
26816
|
-
process.exitCode = 1;
|
|
26817
28346
|
return;
|
|
26818
28347
|
}
|
|
26819
28348
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
26820
|
-
|
|
26821
|
-
|
|
26822
|
-
|
|
28349
|
+
if (!jsonOutput) {
|
|
28350
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
28351
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
28352
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
28353
|
+
}
|
|
26823
28354
|
let client;
|
|
26824
28355
|
try {
|
|
26825
28356
|
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
@@ -26834,29 +28365,57 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26834
28365
|
client,
|
|
26835
28366
|
database,
|
|
26836
28367
|
monitoringUser: opts.monitoringUser,
|
|
26837
|
-
includeOptionalPermissions
|
|
28368
|
+
includeOptionalPermissions,
|
|
28369
|
+
provider: opts.provider
|
|
26838
28370
|
});
|
|
26839
28371
|
if (v.ok) {
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
26843
|
-
|
|
26844
|
-
|
|
28372
|
+
if (jsonOutput) {
|
|
28373
|
+
outputJson({
|
|
28374
|
+
success: true,
|
|
28375
|
+
mode: "direct",
|
|
28376
|
+
action: "verify",
|
|
28377
|
+
database,
|
|
28378
|
+
monitoringUser: opts.monitoringUser,
|
|
28379
|
+
provider: opts.provider,
|
|
28380
|
+
verified: true,
|
|
28381
|
+
missingOptional: v.missingOptional
|
|
28382
|
+
});
|
|
28383
|
+
} else {
|
|
28384
|
+
console.log(`\u2713 prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
|
|
28385
|
+
if (v.missingOptional.length > 0) {
|
|
28386
|
+
console.log("\u26A0 Optional items missing:");
|
|
28387
|
+
for (const m of v.missingOptional)
|
|
28388
|
+
console.log(`- ${m}`);
|
|
28389
|
+
}
|
|
26845
28390
|
}
|
|
26846
28391
|
return;
|
|
26847
28392
|
}
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
26851
|
-
|
|
26852
|
-
|
|
26853
|
-
|
|
28393
|
+
if (jsonOutput) {
|
|
28394
|
+
outputJson({
|
|
28395
|
+
success: false,
|
|
28396
|
+
mode: "direct",
|
|
28397
|
+
action: "verify",
|
|
28398
|
+
database,
|
|
28399
|
+
monitoringUser: opts.monitoringUser,
|
|
28400
|
+
verified: false,
|
|
28401
|
+
missingRequired: v.missingRequired,
|
|
28402
|
+
missingOptional: v.missingOptional
|
|
28403
|
+
});
|
|
28404
|
+
} else {
|
|
28405
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
28406
|
+
for (const m of v.missingRequired)
|
|
26854
28407
|
console.error(`- ${m}`);
|
|
28408
|
+
if (v.missingOptional.length > 0) {
|
|
28409
|
+
console.error("Optional items missing:");
|
|
28410
|
+
for (const m of v.missingOptional)
|
|
28411
|
+
console.error(`- ${m}`);
|
|
28412
|
+
}
|
|
26855
28413
|
}
|
|
26856
28414
|
process.exitCode = 1;
|
|
26857
28415
|
return;
|
|
26858
28416
|
}
|
|
26859
28417
|
let monPassword;
|
|
28418
|
+
let passwordGenerated = false;
|
|
26860
28419
|
try {
|
|
26861
28420
|
const resolved = await resolveMonitoringPassword({
|
|
26862
28421
|
passwordFlag: opts.password,
|
|
@@ -26864,15 +28423,18 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26864
28423
|
monitoringUser: opts.monitoringUser
|
|
26865
28424
|
});
|
|
26866
28425
|
monPassword = resolved.password;
|
|
28426
|
+
passwordGenerated = resolved.generated;
|
|
26867
28427
|
if (resolved.generated) {
|
|
26868
|
-
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
28428
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
26869
28429
|
if (canPrint) {
|
|
26870
|
-
|
|
26871
|
-
|
|
26872
|
-
|
|
26873
|
-
|
|
26874
|
-
|
|
26875
|
-
|
|
28430
|
+
if (!jsonOutput) {
|
|
28431
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
28432
|
+
console.error("");
|
|
28433
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
28434
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
28435
|
+
console.error("");
|
|
28436
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
28437
|
+
}
|
|
26876
28438
|
} else {
|
|
26877
28439
|
console.error([
|
|
26878
28440
|
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
@@ -26890,40 +28452,321 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26890
28452
|
}
|
|
26891
28453
|
} catch (e) {
|
|
26892
28454
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26893
|
-
|
|
28455
|
+
outputError({ message: msg });
|
|
28456
|
+
return;
|
|
28457
|
+
}
|
|
28458
|
+
const plan = await buildInitPlan({
|
|
28459
|
+
database,
|
|
28460
|
+
monitoringUser: opts.monitoringUser,
|
|
28461
|
+
monitoringPassword: monPassword,
|
|
28462
|
+
includeOptionalPermissions,
|
|
28463
|
+
provider: opts.provider
|
|
28464
|
+
});
|
|
28465
|
+
const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
|
|
28466
|
+
if (opts.resetPassword && effectivePlan.steps.length === 0) {
|
|
28467
|
+
console.error(`\u2717 --reset-password not supported for provider "${opts.provider}" (role creation is skipped)`);
|
|
28468
|
+
process.exitCode = 1;
|
|
28469
|
+
return;
|
|
28470
|
+
}
|
|
28471
|
+
if (shouldPrintSql) {
|
|
28472
|
+
console.log(`
|
|
28473
|
+
--- SQL plan ---`);
|
|
28474
|
+
for (const step of effectivePlan.steps) {
|
|
28475
|
+
console.log(`
|
|
28476
|
+
-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
28477
|
+
console.log(redactPasswords(step.sql));
|
|
28478
|
+
}
|
|
28479
|
+
console.log(`
|
|
28480
|
+
--- end SQL plan ---
|
|
28481
|
+
`);
|
|
28482
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
28483
|
+
return;
|
|
28484
|
+
}
|
|
28485
|
+
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
28486
|
+
if (jsonOutput) {
|
|
28487
|
+
const result = {
|
|
28488
|
+
success: true,
|
|
28489
|
+
mode: "direct",
|
|
28490
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
28491
|
+
database,
|
|
28492
|
+
monitoringUser: opts.monitoringUser,
|
|
28493
|
+
applied,
|
|
28494
|
+
skippedOptional,
|
|
28495
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
28496
|
+
};
|
|
28497
|
+
if (passwordGenerated) {
|
|
28498
|
+
result.generatedPassword = monPassword;
|
|
28499
|
+
}
|
|
28500
|
+
outputJson(result);
|
|
28501
|
+
} else {
|
|
28502
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
28503
|
+
if (skippedOptional.length > 0) {
|
|
28504
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
28505
|
+
for (const s of skippedOptional)
|
|
28506
|
+
console.log(`- ${s}`);
|
|
28507
|
+
}
|
|
28508
|
+
if (process.stdout.isTTY) {
|
|
28509
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28510
|
+
}
|
|
28511
|
+
}
|
|
28512
|
+
} catch (error2) {
|
|
28513
|
+
const errAny = error2;
|
|
28514
|
+
let message = "";
|
|
28515
|
+
if (error2 instanceof Error && error2.message) {
|
|
28516
|
+
message = error2.message;
|
|
28517
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
28518
|
+
message = errAny.message;
|
|
28519
|
+
} else {
|
|
28520
|
+
message = String(error2);
|
|
28521
|
+
}
|
|
28522
|
+
if (!message || message === "[object Object]") {
|
|
28523
|
+
message = "Unknown error";
|
|
28524
|
+
}
|
|
28525
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
28526
|
+
const failedStep = stepMatch?.[1];
|
|
28527
|
+
const errorObj = { message };
|
|
28528
|
+
if (failedStep)
|
|
28529
|
+
errorObj.step = failedStep;
|
|
28530
|
+
if (errAny && typeof errAny === "object") {
|
|
28531
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
28532
|
+
errorObj.code = errAny.code;
|
|
28533
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
28534
|
+
errorObj.detail = errAny.detail;
|
|
28535
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
28536
|
+
errorObj.hint = errAny.hint;
|
|
28537
|
+
}
|
|
28538
|
+
if (jsonOutput) {
|
|
28539
|
+
outputJson({
|
|
28540
|
+
success: false,
|
|
28541
|
+
mode: "direct",
|
|
28542
|
+
error: errorObj
|
|
28543
|
+
});
|
|
28544
|
+
process.exitCode = 1;
|
|
28545
|
+
} else {
|
|
28546
|
+
console.error(`Error: prepare-db: ${message}`);
|
|
28547
|
+
if (failedStep) {
|
|
28548
|
+
console.error(` Step: ${failedStep}`);
|
|
28549
|
+
}
|
|
28550
|
+
if (errAny && typeof errAny === "object") {
|
|
28551
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
28552
|
+
console.error(` Code: ${errAny.code}`);
|
|
28553
|
+
}
|
|
28554
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
28555
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
28556
|
+
}
|
|
28557
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
28558
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
28559
|
+
}
|
|
28560
|
+
}
|
|
28561
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
28562
|
+
if (errAny.code === "42501") {
|
|
28563
|
+
if (failedStep === "01.role") {
|
|
28564
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
28565
|
+
} else if (failedStep === "03.permissions") {
|
|
28566
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
28567
|
+
}
|
|
28568
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
28569
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
28570
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
28571
|
+
}
|
|
28572
|
+
if (errAny.code === "42P06" || message.includes("already exists") && failedStep === "03.permissions") {
|
|
28573
|
+
console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
|
|
28574
|
+
console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
|
|
28575
|
+
}
|
|
28576
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
28577
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
28578
|
+
}
|
|
28579
|
+
if (errAny.code === "ENOTFOUND") {
|
|
28580
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
28581
|
+
}
|
|
28582
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
28583
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
28584
|
+
}
|
|
28585
|
+
}
|
|
28586
|
+
process.exitCode = 1;
|
|
28587
|
+
}
|
|
28588
|
+
} finally {
|
|
28589
|
+
if (client) {
|
|
28590
|
+
try {
|
|
28591
|
+
await client.end();
|
|
28592
|
+
} catch {}
|
|
28593
|
+
}
|
|
28594
|
+
}
|
|
28595
|
+
});
|
|
28596
|
+
program2.command("unprepare-db [conn]").description("remove monitoring setup: drop monitoring user, views, schema, and revoke permissions").option("--db-url <url>", "PostgreSQL connection URL (admin) (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 remove", DEFAULT_MONITORING_USER).option("--keep-role", "Keep the monitoring role (only revoke permissions and drop objects)", false).option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.").option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--force", "Skip confirmation prompt", false).option("--json", "Output result as JSON (machine-readable)", false).addHelpText("after", [
|
|
28597
|
+
"",
|
|
28598
|
+
"Examples:",
|
|
28599
|
+
" postgresai unprepare-db postgresql://admin@host:5432/dbname",
|
|
28600
|
+
' postgresai unprepare-db "dbname=dbname host=host user=admin"',
|
|
28601
|
+
" postgresai unprepare-db -h host -p 5432 -U admin -d dbname",
|
|
28602
|
+
"",
|
|
28603
|
+
"Admin password:",
|
|
28604
|
+
" --admin-password <password> or PGPASSWORD=... (libpq standard)",
|
|
28605
|
+
"",
|
|
28606
|
+
"Keep role but remove objects/permissions:",
|
|
28607
|
+
" postgresai unprepare-db <conn> --keep-role",
|
|
28608
|
+
"",
|
|
28609
|
+
"Inspect SQL without applying changes:",
|
|
28610
|
+
" postgresai unprepare-db <conn> --print-sql",
|
|
28611
|
+
"",
|
|
28612
|
+
"Offline SQL plan (no DB connection):",
|
|
28613
|
+
" postgresai unprepare-db --print-sql",
|
|
28614
|
+
"",
|
|
28615
|
+
"Skip confirmation prompt:",
|
|
28616
|
+
" postgresai unprepare-db <conn> --force"
|
|
28617
|
+
].join(`
|
|
28618
|
+
`)).action(async (conn, opts, cmd) => {
|
|
28619
|
+
const jsonOutput = opts.json;
|
|
28620
|
+
const outputJson = (data) => {
|
|
28621
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28622
|
+
};
|
|
28623
|
+
const outputError = (error2) => {
|
|
28624
|
+
if (jsonOutput) {
|
|
28625
|
+
outputJson({
|
|
28626
|
+
success: false,
|
|
28627
|
+
error: error2
|
|
28628
|
+
});
|
|
28629
|
+
} else {
|
|
28630
|
+
console.error(`Error: unprepare-db: ${error2.message}`);
|
|
28631
|
+
if (error2.step)
|
|
28632
|
+
console.error(` Step: ${error2.step}`);
|
|
28633
|
+
if (error2.code)
|
|
28634
|
+
console.error(` Code: ${error2.code}`);
|
|
28635
|
+
if (error2.detail)
|
|
28636
|
+
console.error(` Detail: ${error2.detail}`);
|
|
28637
|
+
if (error2.hint)
|
|
28638
|
+
console.error(` Hint: ${error2.hint}`);
|
|
28639
|
+
}
|
|
28640
|
+
process.exitCode = 1;
|
|
28641
|
+
};
|
|
28642
|
+
const shouldPrintSql = !!opts.printSql;
|
|
28643
|
+
const dropRole = !opts.keepRole;
|
|
28644
|
+
const providerWarning = validateProvider(opts.provider);
|
|
28645
|
+
if (providerWarning) {
|
|
28646
|
+
console.warn(`\u26A0 ${providerWarning}`);
|
|
28647
|
+
}
|
|
28648
|
+
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
|
|
28649
|
+
if (shouldPrintSql) {
|
|
28650
|
+
const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
|
|
28651
|
+
const plan = await buildUninitPlan({
|
|
28652
|
+
database,
|
|
28653
|
+
monitoringUser: opts.monitoringUser,
|
|
28654
|
+
dropRole,
|
|
28655
|
+
provider: opts.provider
|
|
28656
|
+
});
|
|
28657
|
+
console.log(`
|
|
28658
|
+
--- SQL plan (offline; not connected) ---`);
|
|
28659
|
+
console.log(`-- database: ${database}`);
|
|
28660
|
+
console.log(`-- monitoring user: ${opts.monitoringUser}`);
|
|
28661
|
+
console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
|
|
28662
|
+
console.log(`-- drop role: ${dropRole}`);
|
|
28663
|
+
for (const step of plan.steps) {
|
|
28664
|
+
console.log(`
|
|
28665
|
+
-- ${step.name}`);
|
|
28666
|
+
console.log(step.sql);
|
|
28667
|
+
}
|
|
28668
|
+
console.log(`
|
|
28669
|
+
--- end SQL plan ---
|
|
28670
|
+
`);
|
|
28671
|
+
return;
|
|
28672
|
+
}
|
|
28673
|
+
}
|
|
28674
|
+
let adminConn;
|
|
28675
|
+
try {
|
|
28676
|
+
adminConn = resolveAdminConnection({
|
|
28677
|
+
conn,
|
|
28678
|
+
dbUrlFlag: opts.dbUrl,
|
|
28679
|
+
host: opts.host ?? process.env.PGHOST,
|
|
28680
|
+
port: opts.port ?? process.env.PGPORT,
|
|
28681
|
+
username: opts.username ?? process.env.PGUSER,
|
|
28682
|
+
dbname: opts.dbname ?? process.env.PGDATABASE,
|
|
28683
|
+
adminPassword: opts.adminPassword,
|
|
28684
|
+
envPassword: process.env.PGPASSWORD
|
|
28685
|
+
});
|
|
28686
|
+
} catch (e) {
|
|
28687
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
28688
|
+
if (jsonOutput) {
|
|
28689
|
+
outputError({ message: msg });
|
|
28690
|
+
} else {
|
|
28691
|
+
console.error(`Error: unprepare-db: ${msg}`);
|
|
28692
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
28693
|
+
console.error("");
|
|
28694
|
+
cmd.outputHelp({ error: true });
|
|
28695
|
+
}
|
|
26894
28696
|
process.exitCode = 1;
|
|
28697
|
+
}
|
|
28698
|
+
return;
|
|
28699
|
+
}
|
|
28700
|
+
if (!jsonOutput) {
|
|
28701
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
28702
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
28703
|
+
console.log(`Drop role: ${dropRole}`);
|
|
28704
|
+
}
|
|
28705
|
+
if (!opts.force && !jsonOutput && !shouldPrintSql) {
|
|
28706
|
+
const answer = await new Promise((resolve6) => {
|
|
28707
|
+
const readline = getReadline();
|
|
28708
|
+
readline.question(`This will remove the monitoring setup for user "${opts.monitoringUser}"${dropRole ? " and drop the role" : ""}. Continue? [y/N] `, (ans) => resolve6(ans.trim().toLowerCase()));
|
|
28709
|
+
});
|
|
28710
|
+
if (answer !== "y" && answer !== "yes") {
|
|
28711
|
+
console.log("Aborted.");
|
|
26895
28712
|
return;
|
|
26896
28713
|
}
|
|
26897
|
-
|
|
28714
|
+
}
|
|
28715
|
+
let client;
|
|
28716
|
+
try {
|
|
28717
|
+
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
28718
|
+
client = connResult.client;
|
|
28719
|
+
const dbRes = await client.query("select current_database() as db");
|
|
28720
|
+
const database = dbRes.rows?.[0]?.db;
|
|
28721
|
+
if (typeof database !== "string" || !database) {
|
|
28722
|
+
throw new Error("Failed to resolve current database name");
|
|
28723
|
+
}
|
|
28724
|
+
const plan = await buildUninitPlan({
|
|
26898
28725
|
database,
|
|
26899
28726
|
monitoringUser: opts.monitoringUser,
|
|
26900
|
-
|
|
26901
|
-
|
|
28727
|
+
dropRole,
|
|
28728
|
+
provider: opts.provider
|
|
26902
28729
|
});
|
|
26903
|
-
const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
|
|
26904
28730
|
if (shouldPrintSql) {
|
|
26905
28731
|
console.log(`
|
|
26906
28732
|
--- SQL plan ---`);
|
|
26907
|
-
for (const step of
|
|
28733
|
+
for (const step of plan.steps) {
|
|
26908
28734
|
console.log(`
|
|
26909
|
-
-- ${step.name}
|
|
26910
|
-
console.log(
|
|
28735
|
+
-- ${step.name}`);
|
|
28736
|
+
console.log(step.sql);
|
|
26911
28737
|
}
|
|
26912
28738
|
console.log(`
|
|
26913
28739
|
--- end SQL plan ---
|
|
26914
28740
|
`);
|
|
26915
|
-
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
26916
28741
|
return;
|
|
26917
28742
|
}
|
|
26918
|
-
const { applied,
|
|
26919
|
-
|
|
26920
|
-
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
28743
|
+
const { applied, errors: errors3 } = await applyUninitPlan({ client, plan });
|
|
28744
|
+
if (jsonOutput) {
|
|
28745
|
+
outputJson({
|
|
28746
|
+
success: errors3.length === 0,
|
|
28747
|
+
action: "unprepare",
|
|
28748
|
+
database,
|
|
28749
|
+
monitoringUser: opts.monitoringUser,
|
|
28750
|
+
dropRole,
|
|
28751
|
+
applied,
|
|
28752
|
+
errors: errors3
|
|
28753
|
+
});
|
|
28754
|
+
if (errors3.length > 0) {
|
|
28755
|
+
process.exitCode = 1;
|
|
28756
|
+
}
|
|
28757
|
+
} else {
|
|
28758
|
+
if (errors3.length === 0) {
|
|
28759
|
+
console.log("\u2713 unprepare-db completed");
|
|
28760
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28761
|
+
} else {
|
|
28762
|
+
console.log("\u26A0 unprepare-db completed with errors");
|
|
28763
|
+
console.log(`Applied ${applied.length} steps`);
|
|
28764
|
+
console.log("Errors:");
|
|
28765
|
+
for (const err of errors3) {
|
|
28766
|
+
console.log(` - ${err}`);
|
|
28767
|
+
}
|
|
28768
|
+
process.exitCode = 1;
|
|
28769
|
+
}
|
|
26927
28770
|
}
|
|
26928
28771
|
} catch (error2) {
|
|
26929
28772
|
const errAny = error2;
|
|
@@ -26938,51 +28781,58 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26938
28781
|
if (!message || message === "[object Object]") {
|
|
26939
28782
|
message = "Unknown error";
|
|
26940
28783
|
}
|
|
26941
|
-
|
|
26942
|
-
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
26943
|
-
const failedStep = stepMatch?.[1];
|
|
26944
|
-
if (failedStep) {
|
|
26945
|
-
console.error(` Step: ${failedStep}`);
|
|
26946
|
-
}
|
|
28784
|
+
const errorObj = { message };
|
|
26947
28785
|
if (errAny && typeof errAny === "object") {
|
|
26948
|
-
if (typeof errAny.code === "string" && errAny.code)
|
|
26949
|
-
|
|
26950
|
-
|
|
26951
|
-
|
|
26952
|
-
|
|
26953
|
-
|
|
26954
|
-
|
|
26955
|
-
|
|
26956
|
-
|
|
26957
|
-
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
28786
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
28787
|
+
errorObj.code = errAny.code;
|
|
28788
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
28789
|
+
errorObj.detail = errAny.detail;
|
|
28790
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
28791
|
+
errorObj.hint = errAny.hint;
|
|
28792
|
+
}
|
|
28793
|
+
if (jsonOutput) {
|
|
28794
|
+
outputJson({
|
|
28795
|
+
success: false,
|
|
28796
|
+
error: errorObj
|
|
28797
|
+
});
|
|
28798
|
+
process.exitCode = 1;
|
|
28799
|
+
} else {
|
|
28800
|
+
console.error(`Error: unprepare-db: ${message}`);
|
|
28801
|
+
if (errAny && typeof errAny === "object") {
|
|
28802
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
28803
|
+
console.error(` Code: ${errAny.code}`);
|
|
28804
|
+
}
|
|
28805
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
28806
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
28807
|
+
}
|
|
28808
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
28809
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
26964
28810
|
}
|
|
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
28811
|
}
|
|
26975
|
-
if (errAny.code === "
|
|
26976
|
-
|
|
28812
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
28813
|
+
if (errAny.code === "42501") {
|
|
28814
|
+
console.error(" Context: dropping roles/objects requires sufficient privileges");
|
|
28815
|
+
console.error(" Fix: connect as a superuser (or a role with appropriate DROP privileges)");
|
|
28816
|
+
}
|
|
28817
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
28818
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
28819
|
+
}
|
|
28820
|
+
if (errAny.code === "ENOTFOUND") {
|
|
28821
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
28822
|
+
}
|
|
28823
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
28824
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
28825
|
+
}
|
|
26977
28826
|
}
|
|
28827
|
+
process.exitCode = 1;
|
|
26978
28828
|
}
|
|
26979
|
-
process.exitCode = 1;
|
|
26980
28829
|
} finally {
|
|
26981
28830
|
if (client) {
|
|
26982
28831
|
try {
|
|
26983
28832
|
await client.end();
|
|
26984
28833
|
} catch {}
|
|
26985
28834
|
}
|
|
28835
|
+
closeReadline();
|
|
26986
28836
|
}
|
|
26987
28837
|
});
|
|
26988
28838
|
program2.command("checkup [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL").option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--[no-]upload", "upload JSON results to PostgresAI (default: enabled; requires API key)", undefined).option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout (implies --no-upload)").addHelpText("after", [
|
|
@@ -27111,14 +28961,14 @@ async function resolveOrInitPaths() {
|
|
|
27111
28961
|
}
|
|
27112
28962
|
function isDockerRunning() {
|
|
27113
28963
|
try {
|
|
27114
|
-
const result = spawnSync2("docker", ["info"], { stdio: "pipe" });
|
|
28964
|
+
const result = spawnSync2("docker", ["info"], { stdio: "pipe", timeout: 5000 });
|
|
27115
28965
|
return result.status === 0;
|
|
27116
28966
|
} catch {
|
|
27117
28967
|
return false;
|
|
27118
28968
|
}
|
|
27119
28969
|
}
|
|
27120
28970
|
function getComposeCmd() {
|
|
27121
|
-
const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore" }).status === 0;
|
|
28971
|
+
const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
|
|
27122
28972
|
if (tryCmd("docker-compose", ["version"]))
|
|
27123
28973
|
return ["docker-compose"];
|
|
27124
28974
|
if (tryCmd("docker", ["compose", "version"]))
|
|
@@ -27127,7 +28977,7 @@ function getComposeCmd() {
|
|
|
27127
28977
|
}
|
|
27128
28978
|
function checkRunningContainers() {
|
|
27129
28979
|
try {
|
|
27130
|
-
const result = spawnSync2("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8" });
|
|
28980
|
+
const result = spawnSync2("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8", timeout: 5000 });
|
|
27131
28981
|
if (result.status === 0 && result.stdout) {
|
|
27132
28982
|
const containers = result.stdout.trim().split(`
|
|
27133
28983
|
`).filter(Boolean);
|
|
@@ -27213,14 +29063,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
27213
29063
|
console.log(`Project directory: ${projectDir}
|
|
27214
29064
|
`);
|
|
27215
29065
|
const envFile = path5.resolve(projectDir, ".env");
|
|
27216
|
-
let existingTag = null;
|
|
27217
29066
|
let existingRegistry = null;
|
|
27218
29067
|
let existingPassword = null;
|
|
27219
29068
|
if (fs5.existsSync(envFile)) {
|
|
27220
29069
|
const existingEnv = fs5.readFileSync(envFile, "utf8");
|
|
27221
|
-
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
27222
|
-
if (tagMatch)
|
|
27223
|
-
existingTag = tagMatch[1].trim();
|
|
27224
29070
|
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
27225
29071
|
if (registryMatch)
|
|
27226
29072
|
existingRegistry = registryMatch[1].trim();
|
|
@@ -27228,7 +29074,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
27228
29074
|
if (pwdMatch)
|
|
27229
29075
|
existingPassword = pwdMatch[1].trim();
|
|
27230
29076
|
}
|
|
27231
|
-
const imageTag = opts.tag ||
|
|
29077
|
+
const imageTag = opts.tag || package_default.version;
|
|
27232
29078
|
const envLines = [`PGAI_TAG=${imageTag}`];
|
|
27233
29079
|
if (existingRegistry) {
|
|
27234
29080
|
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
@@ -27565,6 +29411,9 @@ var MONITORING_CONTAINERS = [
|
|
|
27565
29411
|
"sources-generator",
|
|
27566
29412
|
"postgres-reports"
|
|
27567
29413
|
];
|
|
29414
|
+
var COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
29415
|
+
var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
29416
|
+
var NETWORK_CLEANUP_DELAY_MS = 2000;
|
|
27568
29417
|
async function removeOrphanedContainers() {
|
|
27569
29418
|
for (const container of MONITORING_CONTAINERS) {
|
|
27570
29419
|
try {
|
|
@@ -27573,7 +29422,18 @@ async function removeOrphanedContainers() {
|
|
|
27573
29422
|
}
|
|
27574
29423
|
}
|
|
27575
29424
|
mon.command("stop").description("stop monitoring services").action(async () => {
|
|
27576
|
-
|
|
29425
|
+
let code = await runCompose(["down", "--remove-orphans"]);
|
|
29426
|
+
if (code !== 0) {
|
|
29427
|
+
await removeOrphanedContainers();
|
|
29428
|
+
await new Promise((resolve6) => setTimeout(resolve6, NETWORK_CLEANUP_DELAY_MS));
|
|
29429
|
+
code = await runCompose(["down", "--remove-orphans"]);
|
|
29430
|
+
}
|
|
29431
|
+
if (code !== 0) {
|
|
29432
|
+
try {
|
|
29433
|
+
await execFilePromise("docker", ["network", "rm", DOCKER_NETWORK_NAME]);
|
|
29434
|
+
code = 0;
|
|
29435
|
+
} catch {}
|
|
29436
|
+
}
|
|
27577
29437
|
if (code !== 0)
|
|
27578
29438
|
process.exitCode = code;
|
|
27579
29439
|
});
|
|
@@ -28340,18 +30200,36 @@ function interpretEscapes2(str2) {
|
|
|
28340
30200
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\x00/g, "\\");
|
|
28341
30201
|
}
|
|
28342
30202
|
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) => {
|
|
30203
|
+
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) => {
|
|
30204
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
28344
30205
|
try {
|
|
28345
30206
|
const rootOpts = program2.opts();
|
|
28346
30207
|
const cfg = readConfig();
|
|
28347
30208
|
const { apiKey } = getConfig(rootOpts);
|
|
28348
30209
|
if (!apiKey) {
|
|
30210
|
+
spinner.stop();
|
|
28349
30211
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28350
30212
|
process.exitCode = 1;
|
|
28351
30213
|
return;
|
|
28352
30214
|
}
|
|
30215
|
+
const orgId = cfg.orgId ?? undefined;
|
|
28353
30216
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28354
|
-
|
|
30217
|
+
let statusFilter;
|
|
30218
|
+
if (opts.status === "open") {
|
|
30219
|
+
statusFilter = "open";
|
|
30220
|
+
} else if (opts.status === "closed") {
|
|
30221
|
+
statusFilter = "closed";
|
|
30222
|
+
}
|
|
30223
|
+
const result = await fetchIssues2({
|
|
30224
|
+
apiKey,
|
|
30225
|
+
apiBaseUrl,
|
|
30226
|
+
orgId,
|
|
30227
|
+
status: statusFilter,
|
|
30228
|
+
limit: opts.limit,
|
|
30229
|
+
offset: opts.offset,
|
|
30230
|
+
debug: !!opts.debug
|
|
30231
|
+
});
|
|
30232
|
+
spinner.stop();
|
|
28355
30233
|
const trimmed = Array.isArray(result) ? result.map((r) => ({
|
|
28356
30234
|
id: r.id,
|
|
28357
30235
|
title: r.title,
|
|
@@ -28360,17 +30238,20 @@ issues.command("list").description("list issues").option("--debug", "enable debu
|
|
|
28360
30238
|
})) : result;
|
|
28361
30239
|
printResult(trimmed, opts.json);
|
|
28362
30240
|
} catch (err) {
|
|
30241
|
+
spinner.stop();
|
|
28363
30242
|
const message = err instanceof Error ? err.message : String(err);
|
|
28364
30243
|
console.error(message);
|
|
28365
30244
|
process.exitCode = 1;
|
|
28366
30245
|
}
|
|
28367
30246
|
});
|
|
28368
30247
|
issues.command("view <issueId>").description("view issue details and comments").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
30248
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
|
|
28369
30249
|
try {
|
|
28370
30250
|
const rootOpts = program2.opts();
|
|
28371
30251
|
const cfg = readConfig();
|
|
28372
30252
|
const { apiKey } = getConfig(rootOpts);
|
|
28373
30253
|
if (!apiKey) {
|
|
30254
|
+
spinner.stop();
|
|
28374
30255
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28375
30256
|
process.exitCode = 1;
|
|
28376
30257
|
return;
|
|
@@ -28378,32 +30259,38 @@ issues.command("view <issueId>").description("view issue details and comments").
|
|
|
28378
30259
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28379
30260
|
const issue2 = await fetchIssue2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
28380
30261
|
if (!issue2) {
|
|
30262
|
+
spinner.stop();
|
|
28381
30263
|
console.error("Issue not found");
|
|
28382
30264
|
process.exitCode = 1;
|
|
28383
30265
|
return;
|
|
28384
30266
|
}
|
|
30267
|
+
spinner.update("Fetching comments...");
|
|
28385
30268
|
const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
30269
|
+
spinner.stop();
|
|
28386
30270
|
const combined = { issue: issue2, comments };
|
|
28387
30271
|
printResult(combined, opts.json);
|
|
28388
30272
|
} catch (err) {
|
|
30273
|
+
spinner.stop();
|
|
28389
30274
|
const message = err instanceof Error ? err.message : String(err);
|
|
28390
30275
|
console.error(message);
|
|
28391
30276
|
process.exitCode = 1;
|
|
28392
30277
|
}
|
|
28393
30278
|
});
|
|
28394
30279
|
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) => {
|
|
30280
|
+
if (opts.debug) {
|
|
30281
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
30282
|
+
}
|
|
30283
|
+
content = interpretEscapes2(content);
|
|
30284
|
+
if (opts.debug) {
|
|
30285
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
30286
|
+
}
|
|
30287
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
|
|
28395
30288
|
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
30289
|
const rootOpts = program2.opts();
|
|
28404
30290
|
const cfg = readConfig();
|
|
28405
30291
|
const { apiKey } = getConfig(rootOpts);
|
|
28406
30292
|
if (!apiKey) {
|
|
30293
|
+
spinner.stop();
|
|
28407
30294
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28408
30295
|
process.exitCode = 1;
|
|
28409
30296
|
return;
|
|
@@ -28417,41 +30304,44 @@ issues.command("post-comment <issueId> <content>").description("post a new comme
|
|
|
28417
30304
|
parentCommentId: opts.parent,
|
|
28418
30305
|
debug: !!opts.debug
|
|
28419
30306
|
});
|
|
30307
|
+
spinner.stop();
|
|
28420
30308
|
printResult(result, opts.json);
|
|
28421
30309
|
} catch (err) {
|
|
30310
|
+
spinner.stop();
|
|
28422
30311
|
const message = err instanceof Error ? err.message : String(err);
|
|
28423
30312
|
console.error(message);
|
|
28424
30313
|
process.exitCode = 1;
|
|
28425
30314
|
}
|
|
28426
30315
|
});
|
|
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 (
|
|
30316
|
+
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
30317
|
previous.push(value);
|
|
28429
30318
|
return previous;
|
|
28430
30319
|
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
|
|
30320
|
+
const rootOpts = program2.opts();
|
|
30321
|
+
const cfg = readConfig();
|
|
30322
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30323
|
+
if (!apiKey) {
|
|
30324
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30325
|
+
process.exitCode = 1;
|
|
30326
|
+
return;
|
|
30327
|
+
}
|
|
30328
|
+
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
30329
|
+
if (!title) {
|
|
30330
|
+
console.error("title is required");
|
|
30331
|
+
process.exitCode = 1;
|
|
30332
|
+
return;
|
|
30333
|
+
}
|
|
30334
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
30335
|
+
if (typeof orgId !== "number") {
|
|
30336
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
30337
|
+
process.exitCode = 1;
|
|
30338
|
+
return;
|
|
30339
|
+
}
|
|
30340
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30341
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
30342
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
30343
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
|
|
28431
30344
|
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
30345
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28456
30346
|
const result = await createIssue2({
|
|
28457
30347
|
apiKey,
|
|
@@ -28463,57 +30353,60 @@ issues.command("create <title>").description("create a new issue").option("--org
|
|
|
28463
30353
|
labels,
|
|
28464
30354
|
debug: !!opts.debug
|
|
28465
30355
|
});
|
|
30356
|
+
spinner.stop();
|
|
28466
30357
|
printResult(result, opts.json);
|
|
28467
30358
|
} catch (err) {
|
|
30359
|
+
spinner.stop();
|
|
28468
30360
|
const message = err instanceof Error ? err.message : String(err);
|
|
28469
30361
|
console.error(message);
|
|
28470
30362
|
process.exitCode = 1;
|
|
28471
30363
|
}
|
|
28472
30364
|
});
|
|
28473
|
-
issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (
|
|
30365
|
+
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
30366
|
previous.push(value);
|
|
28475
30367
|
return previous;
|
|
28476
30368
|
}, []).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)");
|
|
30369
|
+
const rootOpts = program2.opts();
|
|
30370
|
+
const cfg = readConfig();
|
|
30371
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30372
|
+
if (!apiKey) {
|
|
30373
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30374
|
+
process.exitCode = 1;
|
|
30375
|
+
return;
|
|
30376
|
+
}
|
|
30377
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30378
|
+
const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
|
|
30379
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30380
|
+
let status = undefined;
|
|
30381
|
+
if (opts.status !== undefined) {
|
|
30382
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
30383
|
+
if (raw === "open")
|
|
30384
|
+
status = 0;
|
|
30385
|
+
else if (raw === "closed")
|
|
30386
|
+
status = 1;
|
|
30387
|
+
else {
|
|
30388
|
+
const n = Number(raw);
|
|
30389
|
+
if (!Number.isFinite(n)) {
|
|
30390
|
+
console.error("status must be open|closed|0|1");
|
|
28507
30391
|
process.exitCode = 1;
|
|
28508
30392
|
return;
|
|
28509
30393
|
}
|
|
30394
|
+
status = n;
|
|
28510
30395
|
}
|
|
28511
|
-
|
|
28512
|
-
|
|
28513
|
-
|
|
28514
|
-
|
|
28515
|
-
labels = opts.label.map(String);
|
|
30396
|
+
if (status !== 0 && status !== 1) {
|
|
30397
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
30398
|
+
process.exitCode = 1;
|
|
30399
|
+
return;
|
|
28516
30400
|
}
|
|
30401
|
+
}
|
|
30402
|
+
let labels = undefined;
|
|
30403
|
+
if (opts.clearLabels) {
|
|
30404
|
+
labels = [];
|
|
30405
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
30406
|
+
labels = opts.label.map(String);
|
|
30407
|
+
}
|
|
30408
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
|
|
30409
|
+
try {
|
|
28517
30410
|
const result = await updateIssue2({
|
|
28518
30411
|
apiKey,
|
|
28519
30412
|
apiBaseUrl,
|
|
@@ -28524,40 +30417,217 @@ issues.command("update <issueId>").description("update an existing issue (title/
|
|
|
28524
30417
|
labels,
|
|
28525
30418
|
debug: !!opts.debug
|
|
28526
30419
|
});
|
|
30420
|
+
spinner.stop();
|
|
28527
30421
|
printResult(result, opts.json);
|
|
28528
30422
|
} catch (err) {
|
|
30423
|
+
spinner.stop();
|
|
28529
30424
|
const message = err instanceof Error ? err.message : String(err);
|
|
28530
30425
|
console.error(message);
|
|
28531
30426
|
process.exitCode = 1;
|
|
28532
30427
|
}
|
|
28533
30428
|
});
|
|
28534
30429
|
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) => {
|
|
30430
|
+
if (opts.debug) {
|
|
30431
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
30432
|
+
}
|
|
30433
|
+
content = interpretEscapes2(content);
|
|
30434
|
+
if (opts.debug) {
|
|
30435
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
30436
|
+
}
|
|
30437
|
+
const rootOpts = program2.opts();
|
|
30438
|
+
const cfg = readConfig();
|
|
30439
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30440
|
+
if (!apiKey) {
|
|
30441
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30442
|
+
process.exitCode = 1;
|
|
30443
|
+
return;
|
|
30444
|
+
}
|
|
30445
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating comment...");
|
|
28535
30446
|
try {
|
|
28536
|
-
|
|
28537
|
-
|
|
28538
|
-
|
|
28539
|
-
|
|
28540
|
-
|
|
28541
|
-
|
|
30447
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30448
|
+
const result = await updateIssueComment2({
|
|
30449
|
+
apiKey,
|
|
30450
|
+
apiBaseUrl,
|
|
30451
|
+
commentId,
|
|
30452
|
+
content,
|
|
30453
|
+
debug: !!opts.debug
|
|
30454
|
+
});
|
|
30455
|
+
spinner.stop();
|
|
30456
|
+
printResult(result, opts.json);
|
|
30457
|
+
} catch (err) {
|
|
30458
|
+
spinner.stop();
|
|
30459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30460
|
+
console.error(message);
|
|
30461
|
+
process.exitCode = 1;
|
|
30462
|
+
}
|
|
30463
|
+
});
|
|
30464
|
+
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) => {
|
|
30465
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action items...");
|
|
30466
|
+
try {
|
|
30467
|
+
const rootOpts = program2.opts();
|
|
30468
|
+
const cfg = readConfig();
|
|
30469
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30470
|
+
if (!apiKey) {
|
|
30471
|
+
spinner.stop();
|
|
30472
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30473
|
+
process.exitCode = 1;
|
|
30474
|
+
return;
|
|
28542
30475
|
}
|
|
30476
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30477
|
+
const result = await fetchActionItems2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
30478
|
+
spinner.stop();
|
|
30479
|
+
printResult(result, opts.json);
|
|
30480
|
+
} catch (err) {
|
|
30481
|
+
spinner.stop();
|
|
30482
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30483
|
+
console.error(message);
|
|
30484
|
+
process.exitCode = 1;
|
|
30485
|
+
}
|
|
30486
|
+
});
|
|
30487
|
+
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) => {
|
|
30488
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action item(s)...");
|
|
30489
|
+
try {
|
|
28543
30490
|
const rootOpts = program2.opts();
|
|
28544
30491
|
const cfg = readConfig();
|
|
28545
30492
|
const { apiKey } = getConfig(rootOpts);
|
|
28546
30493
|
if (!apiKey) {
|
|
30494
|
+
spinner.stop();
|
|
28547
30495
|
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28548
30496
|
process.exitCode = 1;
|
|
28549
30497
|
return;
|
|
28550
30498
|
}
|
|
28551
30499
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28552
|
-
const result = await
|
|
30500
|
+
const result = await fetchActionItem2({ apiKey, apiBaseUrl, actionItemIds, debug: !!opts.debug });
|
|
30501
|
+
if (result.length === 0) {
|
|
30502
|
+
spinner.stop();
|
|
30503
|
+
console.error("Action item(s) not found");
|
|
30504
|
+
process.exitCode = 1;
|
|
30505
|
+
return;
|
|
30506
|
+
}
|
|
30507
|
+
spinner.stop();
|
|
30508
|
+
printResult(result, opts.json);
|
|
30509
|
+
} catch (err) {
|
|
30510
|
+
spinner.stop();
|
|
30511
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30512
|
+
console.error(message);
|
|
30513
|
+
process.exitCode = 1;
|
|
30514
|
+
}
|
|
30515
|
+
});
|
|
30516
|
+
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) => {
|
|
30517
|
+
try {
|
|
30518
|
+
previous.push(JSON.parse(value));
|
|
30519
|
+
} catch {
|
|
30520
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
30521
|
+
process.exit(1);
|
|
30522
|
+
}
|
|
30523
|
+
return previous;
|
|
30524
|
+
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, rawTitle, opts) => {
|
|
30525
|
+
const rootOpts = program2.opts();
|
|
30526
|
+
const cfg = readConfig();
|
|
30527
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30528
|
+
if (!apiKey) {
|
|
30529
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30530
|
+
process.exitCode = 1;
|
|
30531
|
+
return;
|
|
30532
|
+
}
|
|
30533
|
+
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
30534
|
+
if (!title) {
|
|
30535
|
+
console.error("title is required");
|
|
30536
|
+
process.exitCode = 1;
|
|
30537
|
+
return;
|
|
30538
|
+
}
|
|
30539
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30540
|
+
const sqlAction = opts.sqlAction;
|
|
30541
|
+
const configs = Array.isArray(opts.config) && opts.config.length > 0 ? opts.config : undefined;
|
|
30542
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating action item...");
|
|
30543
|
+
try {
|
|
30544
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30545
|
+
const result = await createActionItem2({
|
|
28553
30546
|
apiKey,
|
|
28554
30547
|
apiBaseUrl,
|
|
28555
|
-
|
|
28556
|
-
|
|
30548
|
+
issueId,
|
|
30549
|
+
title,
|
|
30550
|
+
description,
|
|
30551
|
+
sqlAction,
|
|
30552
|
+
configs,
|
|
28557
30553
|
debug: !!opts.debug
|
|
28558
30554
|
});
|
|
28559
|
-
|
|
30555
|
+
spinner.stop();
|
|
30556
|
+
printResult({ id: result }, opts.json);
|
|
30557
|
+
} catch (err) {
|
|
30558
|
+
spinner.stop();
|
|
30559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30560
|
+
console.error(message);
|
|
30561
|
+
process.exitCode = 1;
|
|
30562
|
+
}
|
|
30563
|
+
});
|
|
30564
|
+
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) => {
|
|
30565
|
+
try {
|
|
30566
|
+
previous.push(JSON.parse(value));
|
|
30567
|
+
} catch {
|
|
30568
|
+
console.error(`Invalid JSON for --config: ${value}`);
|
|
30569
|
+
process.exit(1);
|
|
30570
|
+
}
|
|
30571
|
+
return previous;
|
|
30572
|
+
}, []).option("--clear-configs", "clear all config changes").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemId, opts) => {
|
|
30573
|
+
const rootOpts = program2.opts();
|
|
30574
|
+
const cfg = readConfig();
|
|
30575
|
+
const { apiKey } = getConfig(rootOpts);
|
|
30576
|
+
if (!apiKey) {
|
|
30577
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
30578
|
+
process.exitCode = 1;
|
|
30579
|
+
return;
|
|
30580
|
+
}
|
|
30581
|
+
const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
|
|
30582
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
30583
|
+
let isDone = undefined;
|
|
30584
|
+
if (opts.done)
|
|
30585
|
+
isDone = true;
|
|
30586
|
+
else if (opts.notDone)
|
|
30587
|
+
isDone = false;
|
|
30588
|
+
let status = undefined;
|
|
30589
|
+
if (opts.status !== undefined) {
|
|
30590
|
+
const validStatuses = ["waiting_for_approval", "approved", "rejected"];
|
|
30591
|
+
if (!validStatuses.includes(opts.status)) {
|
|
30592
|
+
console.error(`status must be one of: ${validStatuses.join(", ")}`);
|
|
30593
|
+
process.exitCode = 1;
|
|
30594
|
+
return;
|
|
30595
|
+
}
|
|
30596
|
+
status = opts.status;
|
|
30597
|
+
}
|
|
30598
|
+
const statusReason = opts.statusReason;
|
|
30599
|
+
const sqlAction = opts.sqlAction;
|
|
30600
|
+
let configs = undefined;
|
|
30601
|
+
if (opts.clearConfigs) {
|
|
30602
|
+
configs = [];
|
|
30603
|
+
} else if (Array.isArray(opts.config) && opts.config.length > 0) {
|
|
30604
|
+
configs = opts.config;
|
|
30605
|
+
}
|
|
30606
|
+
if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined && sqlAction === undefined && configs === undefined) {
|
|
30607
|
+
console.error("At least one update option is required");
|
|
30608
|
+
process.exitCode = 1;
|
|
30609
|
+
return;
|
|
30610
|
+
}
|
|
30611
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating action item...");
|
|
30612
|
+
try {
|
|
30613
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
30614
|
+
await updateActionItem2({
|
|
30615
|
+
apiKey,
|
|
30616
|
+
apiBaseUrl,
|
|
30617
|
+
actionItemId,
|
|
30618
|
+
title,
|
|
30619
|
+
description,
|
|
30620
|
+
isDone,
|
|
30621
|
+
status,
|
|
30622
|
+
statusReason,
|
|
30623
|
+
sqlAction,
|
|
30624
|
+
configs,
|
|
30625
|
+
debug: !!opts.debug
|
|
30626
|
+
});
|
|
30627
|
+
spinner.stop();
|
|
30628
|
+
printResult({ success: true }, opts.json);
|
|
28560
30629
|
} catch (err) {
|
|
30630
|
+
spinner.stop();
|
|
28561
30631
|
const message = err instanceof Error ? err.message : String(err);
|
|
28562
30632
|
console.error(message);
|
|
28563
30633
|
process.exitCode = 1;
|