postgresai 0.17.0-rc.1 → 0.17.0-rc.3
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 +43 -7
- package/dist/bin/postgres-ai.js +673 -103
- package/package.json +1 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13430,6 +13430,11 @@ var require_dist2 = __commonJS(function(exports, module) {
|
|
|
13430
13430
|
});
|
|
13431
13431
|
|
|
13432
13432
|
// lib/util.ts
|
|
13433
|
+
function requestTimeoutSignal2(timeoutMs) {
|
|
13434
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS2;
|
|
13435
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS2, Math.floor(requested)));
|
|
13436
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
13437
|
+
}
|
|
13433
13438
|
function maskSecret2(secret) {
|
|
13434
13439
|
if (!secret)
|
|
13435
13440
|
return "";
|
|
@@ -13461,6 +13466,7 @@ function resolveBaseUrls2(opts, cfg, defaults = {}) {
|
|
|
13461
13466
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13462
13467
|
};
|
|
13463
13468
|
}
|
|
13469
|
+
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000;
|
|
13464
13470
|
var init_util = () => {};
|
|
13465
13471
|
|
|
13466
13472
|
// node_modules/commander/index.js
|
|
@@ -13524,7 +13530,7 @@ var {
|
|
|
13524
13530
|
// package.json
|
|
13525
13531
|
var package_default = {
|
|
13526
13532
|
name: "postgresai",
|
|
13527
|
-
version: "0.17.0-rc.
|
|
13533
|
+
version: "0.17.0-rc.3",
|
|
13528
13534
|
description: "postgres_ai CLI",
|
|
13529
13535
|
license: "Apache-2.0",
|
|
13530
13536
|
private: false,
|
|
@@ -16884,7 +16890,7 @@ var Result = import_lib.default.Result;
|
|
|
16884
16890
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16885
16891
|
var defaults = import_lib.default.defaults;
|
|
16886
16892
|
// package.json
|
|
16887
|
-
var version = "0.17.0-rc.
|
|
16893
|
+
var version = "0.17.0-rc.3";
|
|
16888
16894
|
var package_default2 = {
|
|
16889
16895
|
name: "postgresai",
|
|
16890
16896
|
version,
|
|
@@ -17315,6 +17321,10 @@ function resolveMcpOrgScope(args, apiKey, cfg) {
|
|
|
17315
17321
|
}
|
|
17316
17322
|
|
|
17317
17323
|
// lib/issues.ts
|
|
17324
|
+
var IssueStatus = {
|
|
17325
|
+
OPEN: 0,
|
|
17326
|
+
CLOSED: 1
|
|
17327
|
+
};
|
|
17318
17328
|
function issueRequestHeaders(apiKey, orgScope) {
|
|
17319
17329
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
17320
17330
|
Prefer: "return=representation",
|
|
@@ -17331,6 +17341,17 @@ function withVisibleHiddenFlag(issue) {
|
|
|
17331
17341
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
17332
17342
|
return rest;
|
|
17333
17343
|
}
|
|
17344
|
+
function presentIssue(issue) {
|
|
17345
|
+
const visible = withVisibleHiddenFlag(issue);
|
|
17346
|
+
return { ...visible, status: issueStatusLabel(issue.status) };
|
|
17347
|
+
}
|
|
17348
|
+
function issueStatusLabel(status) {
|
|
17349
|
+
if (status === IssueStatus.OPEN)
|
|
17350
|
+
return "open";
|
|
17351
|
+
if (status === IssueStatus.CLOSED)
|
|
17352
|
+
return "closed";
|
|
17353
|
+
return status;
|
|
17354
|
+
}
|
|
17334
17355
|
async function fetchIssues(params) {
|
|
17335
17356
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
17336
17357
|
if (!apiKey) {
|
|
@@ -17473,7 +17494,7 @@ async function fetchIssue(params) {
|
|
|
17473
17494
|
}
|
|
17474
17495
|
}
|
|
17475
17496
|
async function createIssue(params) {
|
|
17476
|
-
const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, debug } = params;
|
|
17497
|
+
const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, hidden, debug } = params;
|
|
17477
17498
|
if (!apiKey) {
|
|
17478
17499
|
throw new Error("API key is required");
|
|
17479
17500
|
}
|
|
@@ -17498,6 +17519,9 @@ async function createIssue(params) {
|
|
|
17498
17519
|
if (labels && labels.length > 0) {
|
|
17499
17520
|
bodyObj.labels = labels;
|
|
17500
17521
|
}
|
|
17522
|
+
if (hidden === true) {
|
|
17523
|
+
bodyObj.is_hidden = true;
|
|
17524
|
+
}
|
|
17501
17525
|
const body = JSON.stringify(bodyObj);
|
|
17502
17526
|
const headers = issueRequestHeaders(apiKey, orgScope);
|
|
17503
17527
|
if (debug) {
|
|
@@ -17579,15 +17603,15 @@ async function createIssueComment(params) {
|
|
|
17579
17603
|
}
|
|
17580
17604
|
}
|
|
17581
17605
|
async function updateIssue(params) {
|
|
17582
|
-
const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, debug } = params;
|
|
17606
|
+
const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, hidden, debug } = params;
|
|
17583
17607
|
if (!apiKey) {
|
|
17584
17608
|
throw new Error("API key is required");
|
|
17585
17609
|
}
|
|
17586
17610
|
if (!issueId) {
|
|
17587
17611
|
throw new Error("issueId is required");
|
|
17588
17612
|
}
|
|
17589
|
-
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
17590
|
-
throw new Error("At least one field to update is required (title, description, status, or
|
|
17613
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined) {
|
|
17614
|
+
throw new Error("At least one field to update is required (title, description, status, labels, or hidden)");
|
|
17591
17615
|
}
|
|
17592
17616
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17593
17617
|
const url = new URL(`${base}/rpc/issue_update`);
|
|
@@ -17606,6 +17630,9 @@ async function updateIssue(params) {
|
|
|
17606
17630
|
if (labels !== undefined) {
|
|
17607
17631
|
bodyObj.p_labels = labels;
|
|
17608
17632
|
}
|
|
17633
|
+
if (hidden !== undefined) {
|
|
17634
|
+
bodyObj.p_is_hidden = hidden;
|
|
17635
|
+
}
|
|
17609
17636
|
const body = JSON.stringify(bodyObj);
|
|
17610
17637
|
const headers = issueRequestHeaders(apiKey, orgScope);
|
|
17611
17638
|
if (debug) {
|
|
@@ -24947,6 +24974,13 @@ var ORG_ID_TOOL_PROPERTY = {
|
|
|
24947
24974
|
description: "Organization ID. Optional with a per-organization token (uses the stored org); REQUIRED with a global token, which can reach every organization the user belongs to and will not assume one. Use orgs_list / `pgai orgs` to discover valid values."
|
|
24948
24975
|
}
|
|
24949
24976
|
};
|
|
24977
|
+
function parseHiddenArg(raw) {
|
|
24978
|
+
if (raw === undefined || raw === null)
|
|
24979
|
+
return {};
|
|
24980
|
+
if (typeof raw === "boolean")
|
|
24981
|
+
return { value: raw };
|
|
24982
|
+
return { error: { content: [{ type: "text", text: "is_hidden must be a boolean" }], isError: true } };
|
|
24983
|
+
}
|
|
24950
24984
|
async function handleToolCall(req, rootOpts, extra) {
|
|
24951
24985
|
const toolName = req.params.name;
|
|
24952
24986
|
const args = req.params.arguments || {};
|
|
@@ -24981,7 +25015,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24981
25015
|
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
24982
25016
|
const hiddenOnly = Boolean(args.hidden_only);
|
|
24983
25017
|
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, orgScope: scope.orgScope, status, limit, offset, hiddenOnly, debug });
|
|
24984
|
-
const trimmed = issues.map((issue) =>
|
|
25018
|
+
const trimmed = issues.map((issue) => presentIssue(issue));
|
|
24985
25019
|
return { content: [{ type: "text", text: JSON.stringify(trimmed, null, 2) }] };
|
|
24986
25020
|
}
|
|
24987
25021
|
if (toolName === "view_issue") {
|
|
@@ -24997,7 +25031,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24997
25031
|
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
24998
25032
|
}
|
|
24999
25033
|
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, orgScope: scope.orgScope, debug });
|
|
25000
|
-
const combined = { issue:
|
|
25034
|
+
const combined = { issue: presentIssue(issue), comments };
|
|
25001
25035
|
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
25002
25036
|
}
|
|
25003
25037
|
if (toolName === "post_issue_comment") {
|
|
@@ -25033,6 +25067,10 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25033
25067
|
let description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
25034
25068
|
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
|
|
25035
25069
|
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
25070
|
+
const hiddenArg = parseHiddenArg(args.is_hidden);
|
|
25071
|
+
if (hiddenArg.error)
|
|
25072
|
+
return hiddenArg.error;
|
|
25073
|
+
const hidden = hiddenArg.value === true;
|
|
25036
25074
|
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
25037
25075
|
const scope = resolveMcpOrgScope(args, apiKey, cfg);
|
|
25038
25076
|
if (scope.error)
|
|
@@ -25046,7 +25084,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25046
25084
|
const uploaded = await uploadAttachments({ apiKey, orgScope: scope.orgScope, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
25047
25085
|
description = appendAttachmentsToContent(description ?? "", uploaded);
|
|
25048
25086
|
}
|
|
25049
|
-
const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, orgScope: scope.orgScope, description, projectId, labels, debug });
|
|
25087
|
+
const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, orgScope: scope.orgScope, description, projectId, labels, hidden, debug });
|
|
25050
25088
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25051
25089
|
}
|
|
25052
25090
|
if (toolName === "update_issue") {
|
|
@@ -25060,9 +25098,13 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25060
25098
|
let description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
25061
25099
|
const status = args.status !== undefined ? Number(args.status) : undefined;
|
|
25062
25100
|
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
25101
|
+
const hiddenArg = parseHiddenArg(args.is_hidden);
|
|
25102
|
+
if (hiddenArg.error)
|
|
25103
|
+
return hiddenArg.error;
|
|
25104
|
+
const hidden = hiddenArg.value;
|
|
25063
25105
|
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
25064
|
-
if (title === undefined && description === undefined && status === undefined && labels === undefined && attachments.length === 0) {
|
|
25065
|
-
return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, or attachments)" }], isError: true };
|
|
25106
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined && attachments.length === 0) {
|
|
25107
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, is_hidden, or attachments)" }], isError: true };
|
|
25066
25108
|
}
|
|
25067
25109
|
if (status !== undefined && (Number.isNaN(status) || status !== 0 && status !== 1)) {
|
|
25068
25110
|
return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
|
|
@@ -25082,7 +25124,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25082
25124
|
const uploaded = await uploadAttachments({ apiKey, orgScope: scope.orgScope, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
25083
25125
|
description = appendAttachmentsToContent(description, uploaded);
|
|
25084
25126
|
}
|
|
25085
|
-
const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, orgScope: scope.orgScope, debug });
|
|
25127
|
+
const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, hidden, orgScope: scope.orgScope, debug });
|
|
25086
25128
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25087
25129
|
}
|
|
25088
25130
|
if (toolName === "update_issue_comment") {
|
|
@@ -25293,7 +25335,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25293
25335
|
type: "object",
|
|
25294
25336
|
properties: {
|
|
25295
25337
|
org_id: { type: "number", description: "Organization ID. Optional with a per-organization token (falls back to config); REQUIRED with a global token, which can reach every organization the user belongs to and will not assume one. Use orgs_list / `pgai orgs` to discover valid values." },
|
|
25296
|
-
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
|
|
25338
|
+
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all (note: the CLI defaults to open). Rows carry status as 'open'/'closed'." },
|
|
25297
25339
|
limit: { type: "number", description: "Max number of issues to return (default: 20)" },
|
|
25298
25340
|
offset: { type: "number", description: "Number of issues to skip (default: 0)" },
|
|
25299
25341
|
hidden_only: { type: "boolean", description: "List only hidden issues (PostgresAI staff; same as CLI --hidden-only)" },
|
|
@@ -25357,6 +25399,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25357
25399
|
items: { type: "string" },
|
|
25358
25400
|
description: "Local file paths to upload and append as markdown links to the description (images render inline)"
|
|
25359
25401
|
},
|
|
25402
|
+
is_hidden: { type: "boolean", description: "Create as a hidden, staff-internal issue (PostgresAI staff only; same as CLI --hidden)" },
|
|
25360
25403
|
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25361
25404
|
},
|
|
25362
25405
|
required: ["title"],
|
|
@@ -25365,7 +25408,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25365
25408
|
},
|
|
25366
25409
|
{
|
|
25367
25410
|
name: "update_issue",
|
|
25368
|
-
description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
|
|
25411
|
+
description: "Update an existing issue (title, description, status, labels, is_hidden). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
|
|
25369
25412
|
inputSchema: {
|
|
25370
25413
|
type: "object",
|
|
25371
25414
|
properties: {
|
|
@@ -25384,6 +25427,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25384
25427
|
items: { type: "string" },
|
|
25385
25428
|
description: "Local file paths to upload and append as markdown links (images render inline). When provided without 'description', the existing description is fetched and appended to."
|
|
25386
25429
|
},
|
|
25430
|
+
is_hidden: { type: "boolean", description: "true hides the issue, false unhides it (PostgresAI staff only; same as CLI --hidden / --no-hidden). Omit (or pass null) to leave unchanged." },
|
|
25387
25431
|
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25388
25432
|
},
|
|
25389
25433
|
required: ["issue_id"],
|
|
@@ -25573,6 +25617,10 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25573
25617
|
}
|
|
25574
25618
|
|
|
25575
25619
|
// lib/issues.ts
|
|
25620
|
+
var IssueStatus2 = {
|
|
25621
|
+
OPEN: 0,
|
|
25622
|
+
CLOSED: 1
|
|
25623
|
+
};
|
|
25576
25624
|
function issueRequestHeaders2(apiKey, orgScope) {
|
|
25577
25625
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
25578
25626
|
Prefer: "return=representation",
|
|
@@ -25589,6 +25637,17 @@ function withVisibleHiddenFlag2(issue) {
|
|
|
25589
25637
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
25590
25638
|
return rest;
|
|
25591
25639
|
}
|
|
25640
|
+
function presentIssue2(issue) {
|
|
25641
|
+
const visible = withVisibleHiddenFlag2(issue);
|
|
25642
|
+
return { ...visible, status: issueStatusLabel2(issue.status) };
|
|
25643
|
+
}
|
|
25644
|
+
function issueStatusLabel2(status) {
|
|
25645
|
+
if (status === IssueStatus2.OPEN)
|
|
25646
|
+
return "open";
|
|
25647
|
+
if (status === IssueStatus2.CLOSED)
|
|
25648
|
+
return "closed";
|
|
25649
|
+
return status;
|
|
25650
|
+
}
|
|
25592
25651
|
async function fetchIssues2(params) {
|
|
25593
25652
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
25594
25653
|
if (!apiKey) {
|
|
@@ -25731,7 +25790,7 @@ async function fetchIssue2(params) {
|
|
|
25731
25790
|
}
|
|
25732
25791
|
}
|
|
25733
25792
|
async function createIssue2(params) {
|
|
25734
|
-
const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, debug } = params;
|
|
25793
|
+
const { apiKey, apiBaseUrl, orgScope, title, orgId, description, projectId, labels, hidden, debug } = params;
|
|
25735
25794
|
if (!apiKey) {
|
|
25736
25795
|
throw new Error("API key is required");
|
|
25737
25796
|
}
|
|
@@ -25756,6 +25815,9 @@ async function createIssue2(params) {
|
|
|
25756
25815
|
if (labels && labels.length > 0) {
|
|
25757
25816
|
bodyObj.labels = labels;
|
|
25758
25817
|
}
|
|
25818
|
+
if (hidden === true) {
|
|
25819
|
+
bodyObj.is_hidden = true;
|
|
25820
|
+
}
|
|
25759
25821
|
const body = JSON.stringify(bodyObj);
|
|
25760
25822
|
const headers = issueRequestHeaders2(apiKey, orgScope);
|
|
25761
25823
|
if (debug) {
|
|
@@ -25837,15 +25899,15 @@ async function createIssueComment2(params) {
|
|
|
25837
25899
|
}
|
|
25838
25900
|
}
|
|
25839
25901
|
async function updateIssue2(params) {
|
|
25840
|
-
const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, debug } = params;
|
|
25902
|
+
const { apiKey, apiBaseUrl, orgScope, issueId, title, description, status, labels, hidden, debug } = params;
|
|
25841
25903
|
if (!apiKey) {
|
|
25842
25904
|
throw new Error("API key is required");
|
|
25843
25905
|
}
|
|
25844
25906
|
if (!issueId) {
|
|
25845
25907
|
throw new Error("issueId is required");
|
|
25846
25908
|
}
|
|
25847
|
-
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
25848
|
-
throw new Error("At least one field to update is required (title, description, status, or
|
|
25909
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined && hidden === undefined) {
|
|
25910
|
+
throw new Error("At least one field to update is required (title, description, status, labels, or hidden)");
|
|
25849
25911
|
}
|
|
25850
25912
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
25851
25913
|
const url = new URL(`${base}/rpc/issue_update`);
|
|
@@ -25864,6 +25926,9 @@ async function updateIssue2(params) {
|
|
|
25864
25926
|
if (labels !== undefined) {
|
|
25865
25927
|
bodyObj.p_labels = labels;
|
|
25866
25928
|
}
|
|
25929
|
+
if (hidden !== undefined) {
|
|
25930
|
+
bodyObj.p_is_hidden = hidden;
|
|
25931
|
+
}
|
|
25867
25932
|
const body = JSON.stringify(bodyObj);
|
|
25868
25933
|
const headers = issueRequestHeaders2(apiKey, orgScope);
|
|
25869
25934
|
if (debug) {
|
|
@@ -27109,6 +27174,134 @@ async function destroySnapshot(params) {
|
|
|
27109
27174
|
// bin/postgres-ai.ts
|
|
27110
27175
|
init_util();
|
|
27111
27176
|
|
|
27177
|
+
// lib/promql.ts
|
|
27178
|
+
function describeRpcError(status, body) {
|
|
27179
|
+
try {
|
|
27180
|
+
const parsed = JSON.parse(body);
|
|
27181
|
+
const parts = [parsed.message, parsed.details, parsed.hint].filter(Boolean);
|
|
27182
|
+
if (parts.length)
|
|
27183
|
+
return parts.join(" — ");
|
|
27184
|
+
} catch {}
|
|
27185
|
+
return body.trim() || `HTTP ${status}`;
|
|
27186
|
+
}
|
|
27187
|
+
async function callRpc3(name, apiKey, body, opts) {
|
|
27188
|
+
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
27189
|
+
const res = await fetch(`${apiBaseUrl}/rpc/${name}`, {
|
|
27190
|
+
method: "POST",
|
|
27191
|
+
headers: {
|
|
27192
|
+
"Content-Type": "application/json",
|
|
27193
|
+
"access-token": apiKey,
|
|
27194
|
+
...orgScopeHeaders(getActiveOrgScope())
|
|
27195
|
+
},
|
|
27196
|
+
body: JSON.stringify(body),
|
|
27197
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 30000)
|
|
27198
|
+
});
|
|
27199
|
+
const text = await res.text().catch(() => "");
|
|
27200
|
+
if (!res.ok) {
|
|
27201
|
+
throw new Error(describeRpcError(res.status, text));
|
|
27202
|
+
}
|
|
27203
|
+
try {
|
|
27204
|
+
return text ? JSON.parse(text) : null;
|
|
27205
|
+
} catch {
|
|
27206
|
+
throw new Error(`${name} returned a body that is not JSON`);
|
|
27207
|
+
}
|
|
27208
|
+
}
|
|
27209
|
+
async function enqueueQuery(apiKey, args, opts) {
|
|
27210
|
+
const body = {
|
|
27211
|
+
p_instance_id: args.instanceId,
|
|
27212
|
+
p_kind: args.kind,
|
|
27213
|
+
p_query: args.query
|
|
27214
|
+
};
|
|
27215
|
+
if (args.kind === "promql_range") {
|
|
27216
|
+
body.p_start = args.start;
|
|
27217
|
+
body.p_end = args.end;
|
|
27218
|
+
body.p_step_s = args.stepS;
|
|
27219
|
+
} else if (args.at) {
|
|
27220
|
+
body.p_at = args.at;
|
|
27221
|
+
}
|
|
27222
|
+
const reply = await callRpc3("instance_query_enqueue", apiKey, body, opts);
|
|
27223
|
+
const jobId = reply?.job_id;
|
|
27224
|
+
if (!jobId) {
|
|
27225
|
+
throw new Error("instance_query_enqueue returned no job_id");
|
|
27226
|
+
}
|
|
27227
|
+
return { jobId, firstAnswerEstimateS: positiveSeconds(reply?.first_answer_estimate_s) };
|
|
27228
|
+
}
|
|
27229
|
+
function positiveSeconds(v) {
|
|
27230
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
27231
|
+
}
|
|
27232
|
+
async function readQueryResult(apiKey, jobId, opts) {
|
|
27233
|
+
const reply = await callRpc3("instance_query_result", apiKey, { p_job_id: jobId }, opts);
|
|
27234
|
+
if (!reply || typeof reply.status !== "string") {
|
|
27235
|
+
throw new Error("instance_query_result returned an unexpected reply");
|
|
27236
|
+
}
|
|
27237
|
+
return reply;
|
|
27238
|
+
}
|
|
27239
|
+
var TERMINAL = new Set(["done", "failed", "expired"]);
|
|
27240
|
+
function isTerminal2(status) {
|
|
27241
|
+
return TERMINAL.has(status);
|
|
27242
|
+
}
|
|
27243
|
+
function pollDelay(attempt) {
|
|
27244
|
+
return Math.min(1000 * 2 ** attempt, 15000);
|
|
27245
|
+
}
|
|
27246
|
+
async function awaitQueryResult(apiKey, jobId, opts) {
|
|
27247
|
+
const now = opts.now ?? (() => Date.now());
|
|
27248
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
27249
|
+
const deadline = now() + opts.timeoutMs;
|
|
27250
|
+
for (let attempt = 0;; attempt++) {
|
|
27251
|
+
const res = await readQueryResult(apiKey, jobId, opts);
|
|
27252
|
+
if (isTerminal2(res.status))
|
|
27253
|
+
return res;
|
|
27254
|
+
const delay = pollDelay(attempt);
|
|
27255
|
+
if (now() + delay >= deadline)
|
|
27256
|
+
return res;
|
|
27257
|
+
await sleep(delay);
|
|
27258
|
+
}
|
|
27259
|
+
}
|
|
27260
|
+
function formatMetric(metric) {
|
|
27261
|
+
const name = metric.__name__ ?? "";
|
|
27262
|
+
const labels = Object.entries(metric).filter(([k]) => k !== "__name__").sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}="${v}"`);
|
|
27263
|
+
if (!labels.length)
|
|
27264
|
+
return name || "{}";
|
|
27265
|
+
return `${name}{${labels.join(", ")}}`;
|
|
27266
|
+
}
|
|
27267
|
+
function pad(s, width) {
|
|
27268
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
27269
|
+
}
|
|
27270
|
+
function renderPromQL(payload) {
|
|
27271
|
+
const lines = [];
|
|
27272
|
+
if (!payload.result.length) {
|
|
27273
|
+
lines.push("Empty result set.");
|
|
27274
|
+
} else if (payload.resultType === "vector") {
|
|
27275
|
+
const rows = payload.result.map((s) => [formatMetric(s.metric), s.value ? s.value[1] : ""]);
|
|
27276
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27277
|
+
lines.push(`${pad("SERIES", w)} VALUE`);
|
|
27278
|
+
for (const [m, v] of rows)
|
|
27279
|
+
lines.push(`${pad(m, w)} ${v}`);
|
|
27280
|
+
} else {
|
|
27281
|
+
const rows = payload.result.map((s) => {
|
|
27282
|
+
const vs = s.values ?? [];
|
|
27283
|
+
const first = vs.length ? vs[0][1] : "";
|
|
27284
|
+
const last = vs.length ? vs[vs.length - 1][1] : "";
|
|
27285
|
+
return [formatMetric(s.metric), String(vs.length), first, last];
|
|
27286
|
+
});
|
|
27287
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27288
|
+
lines.push(`${pad("SERIES", w)} POINTS FIRST LAST`);
|
|
27289
|
+
for (const [m, n, f, l] of rows)
|
|
27290
|
+
lines.push(`${pad(m, w)} ${pad(n, 6)} ${f} ${l}`);
|
|
27291
|
+
if (rows.length)
|
|
27292
|
+
lines.push("(first and last sample per series; use --json for every point)");
|
|
27293
|
+
}
|
|
27294
|
+
const series = payload.result.length;
|
|
27295
|
+
const points = payload.result.reduce((n, s) => n + (s.values?.length ?? 0) + (s.value ? 1 : 0), 0);
|
|
27296
|
+
lines.push(`
|
|
27297
|
+
${series} series, ${points} points`);
|
|
27298
|
+
if (payload.stats.truncated) {
|
|
27299
|
+
lines.push("⚠ TRUNCATED: too large to return whole. Raise --step, narrow the time range, " + "or select fewer series.");
|
|
27300
|
+
}
|
|
27301
|
+
return lines.join(`
|
|
27302
|
+
`);
|
|
27303
|
+
}
|
|
27304
|
+
|
|
27112
27305
|
// lib/instances.ts
|
|
27113
27306
|
import * as fs4 from "fs";
|
|
27114
27307
|
|
|
@@ -38672,47 +38865,121 @@ function stripMatchingQuotes(value) {
|
|
|
38672
38865
|
}
|
|
38673
38866
|
return trimmed;
|
|
38674
38867
|
}
|
|
38868
|
+
var VM_ADMIN_AUTH_KEYS = [
|
|
38869
|
+
"VM_DELETE_AUTH_KEY",
|
|
38870
|
+
"VM_SNAPSHOT_AUTH_KEY",
|
|
38871
|
+
"VM_FORCE_MERGE_AUTH_KEY",
|
|
38872
|
+
"VM_PPROF_AUTH_KEY"
|
|
38873
|
+
];
|
|
38675
38874
|
var REQUIRED_ENV_KEYS = [
|
|
38676
38875
|
{ key: "REPLICATOR_PASSWORD", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.13" },
|
|
38677
38876
|
{ key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
|
|
38678
|
-
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" }
|
|
38877
|
+
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" },
|
|
38878
|
+
{ key: "VM_DELETE_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
|
|
38879
|
+
{ key: "VM_SNAPSHOT_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
|
|
38880
|
+
{ key: "VM_FORCE_MERGE_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" },
|
|
38881
|
+
{ key: "VM_PPROF_AUTH_KEY", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.17" }
|
|
38679
38882
|
];
|
|
38680
38883
|
function ensureRequiredEnvVars(projectDir) {
|
|
38681
38884
|
const envFile = path7.resolve(projectDir, ".env");
|
|
38682
38885
|
const existing = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
|
|
38683
38886
|
const added = [];
|
|
38684
38887
|
const appendLines = [];
|
|
38888
|
+
let content = existing;
|
|
38685
38889
|
for (const spec of REQUIRED_ENV_KEYS) {
|
|
38686
|
-
const
|
|
38687
|
-
if (!
|
|
38890
|
+
const present = new RegExp(`^[ ]*(?:export[ ]+)?${spec.key}=`, "m");
|
|
38891
|
+
if (!present.test(content)) {
|
|
38688
38892
|
appendLines.push(`${spec.key}=${spec.defaultValue()}`);
|
|
38689
38893
|
added.push(spec.key);
|
|
38894
|
+
continue;
|
|
38895
|
+
}
|
|
38896
|
+
if (VM_ADMIN_AUTH_KEYS.includes(spec.key)) {
|
|
38897
|
+
const blank = new RegExp(`^([ ]*(?:export[ ]+)?${spec.key}=)[ ]*(?:""|'')?[ ]*\r?$`, "gm");
|
|
38898
|
+
if (blank.test(content)) {
|
|
38899
|
+
const value = spec.defaultValue();
|
|
38900
|
+
content = content.replace(blank, (_match, prefix) => `${prefix}${value}`);
|
|
38901
|
+
added.push(spec.key);
|
|
38902
|
+
}
|
|
38690
38903
|
}
|
|
38691
38904
|
}
|
|
38692
|
-
if (appendLines.length === 0) {
|
|
38905
|
+
if (appendLines.length === 0 && content === existing) {
|
|
38693
38906
|
return added;
|
|
38694
38907
|
}
|
|
38695
|
-
const needsTrailingNewline =
|
|
38908
|
+
const needsTrailingNewline = content.length > 0 && !content.endsWith(`
|
|
38696
38909
|
`);
|
|
38697
|
-
const newContent =
|
|
38698
|
-
` : "") + appendLines.join(`
|
|
38910
|
+
const newContent = content + (needsTrailingNewline ? `
|
|
38911
|
+
` : "") + (appendLines.length > 0 ? appendLines.join(`
|
|
38699
38912
|
`) + `
|
|
38700
|
-
|
|
38701
|
-
|
|
38913
|
+
` : "");
|
|
38914
|
+
writeEnvFile(envFile, newContent);
|
|
38702
38915
|
return added;
|
|
38703
38916
|
}
|
|
38917
|
+
function writeEnvFile(envFile, content) {
|
|
38918
|
+
let targetFile = envFile;
|
|
38919
|
+
try {
|
|
38920
|
+
if (fs9.existsSync(envFile))
|
|
38921
|
+
targetFile = fs9.realpathSync(envFile);
|
|
38922
|
+
} catch {}
|
|
38923
|
+
const tmpFile = `${targetFile}.tmp`;
|
|
38924
|
+
fs9.writeFileSync(tmpFile, content, { encoding: "utf8", mode: 384 });
|
|
38925
|
+
try {
|
|
38926
|
+
fs9.chmodSync(tmpFile, 384);
|
|
38927
|
+
if (fs9.existsSync(targetFile)) {
|
|
38928
|
+
const st = fs9.statSync(targetFile);
|
|
38929
|
+
fs9.chownSync(tmpFile, st.uid, st.gid);
|
|
38930
|
+
}
|
|
38931
|
+
} catch {}
|
|
38932
|
+
try {
|
|
38933
|
+
fs9.renameSync(tmpFile, targetFile);
|
|
38934
|
+
} catch (error) {
|
|
38935
|
+
try {
|
|
38936
|
+
fs9.unlinkSync(tmpFile);
|
|
38937
|
+
} catch {}
|
|
38938
|
+
throw error;
|
|
38939
|
+
}
|
|
38940
|
+
}
|
|
38704
38941
|
var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
|
|
38705
38942
|
"PGAI_TAG",
|
|
38706
38943
|
"PGAI_REGISTRY",
|
|
38707
38944
|
"GF_SECURITY_ADMIN_PASSWORD",
|
|
38708
38945
|
"REPLICATOR_PASSWORD",
|
|
38709
38946
|
"VM_AUTH_USERNAME",
|
|
38710
|
-
"VM_AUTH_PASSWORD"
|
|
38947
|
+
"VM_AUTH_PASSWORD",
|
|
38948
|
+
"VM_DELETE_AUTH_KEY",
|
|
38949
|
+
"VM_SNAPSHOT_AUTH_KEY",
|
|
38950
|
+
"VM_FORCE_MERGE_AUTH_KEY",
|
|
38951
|
+
"VM_PPROF_AUTH_KEY",
|
|
38952
|
+
"PGAI_INSTANCE_ID",
|
|
38953
|
+
"INSTANCE_JOBS_USER"
|
|
38711
38954
|
];
|
|
38712
|
-
function
|
|
38955
|
+
function statOwner(file) {
|
|
38956
|
+
if (!file)
|
|
38957
|
+
return;
|
|
38958
|
+
let st;
|
|
38959
|
+
try {
|
|
38960
|
+
st = fs9.statSync(file);
|
|
38961
|
+
} catch {
|
|
38962
|
+
return;
|
|
38963
|
+
}
|
|
38964
|
+
if (!st.isFile())
|
|
38965
|
+
return null;
|
|
38966
|
+
return `${st.uid}:${st.gid}`;
|
|
38967
|
+
}
|
|
38968
|
+
function processOwner() {
|
|
38969
|
+
if (typeof process.getuid !== "function" || typeof process.getgid !== "function")
|
|
38970
|
+
return null;
|
|
38971
|
+
return `${process.getuid()}:${process.getgid()}`;
|
|
38972
|
+
}
|
|
38973
|
+
function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile) {
|
|
38713
38974
|
const readRaw = (key) => {
|
|
38714
|
-
const
|
|
38715
|
-
|
|
38975
|
+
const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
|
|
38976
|
+
let value = null;
|
|
38977
|
+
for (const m of existingEnv.matchAll(re)) {
|
|
38978
|
+
value = m[1].trim();
|
|
38979
|
+
}
|
|
38980
|
+
if (value === null)
|
|
38981
|
+
return null;
|
|
38982
|
+
return stripMatchingQuotes(value) === "" ? null : value;
|
|
38716
38983
|
};
|
|
38717
38984
|
const envLines = [`PGAI_TAG=${imageTag}`];
|
|
38718
38985
|
const registry = readRaw("PGAI_REGISTRY");
|
|
@@ -38727,11 +38994,23 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
|
|
|
38727
38994
|
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername ? stripMatchingQuotes(vmAuthUsername) : "vmauth"}`);
|
|
38728
38995
|
const vmAuthPassword = readRaw("VM_AUTH_PASSWORD");
|
|
38729
38996
|
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword ? stripMatchingQuotes(vmAuthPassword) : crypto2.randomBytes(18).toString("base64")}`);
|
|
38997
|
+
const instanceIdValue = instanceId?.trim() || readRaw("PGAI_INSTANCE_ID");
|
|
38998
|
+
if (instanceIdValue) {
|
|
38999
|
+
envLines.push(`PGAI_INSTANCE_ID=${stripMatchingQuotes(instanceIdValue)}`);
|
|
39000
|
+
}
|
|
39001
|
+
const owner = statOwner(credentialFile);
|
|
39002
|
+
const credentialOwner = owner === undefined ? processOwner() : owner;
|
|
39003
|
+
if (credentialOwner)
|
|
39004
|
+
envLines.push(`INSTANCE_JOBS_USER=${credentialOwner}`);
|
|
39005
|
+
for (const key of VM_ADMIN_AUTH_KEYS) {
|
|
39006
|
+
const existing = readRaw(key);
|
|
39007
|
+
envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
|
|
39008
|
+
}
|
|
38730
39009
|
const preservedKeys = [];
|
|
38731
39010
|
for (const line of existingEnv.split(/\r?\n/)) {
|
|
38732
39011
|
if (!line.trim())
|
|
38733
39012
|
continue;
|
|
38734
|
-
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
39013
|
+
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
38735
39014
|
if (m) {
|
|
38736
39015
|
if (LOCAL_INSTALL_MANAGED_ENV_KEYS.includes(m[1]))
|
|
38737
39016
|
continue;
|
|
@@ -38743,9 +39022,9 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
|
|
|
38743
39022
|
`) + `
|
|
38744
39023
|
`, preservedKeys };
|
|
38745
39024
|
}
|
|
38746
|
-
async function execFilePromise(file, args) {
|
|
39025
|
+
async function execFilePromise(file, args, options = {}) {
|
|
38747
39026
|
return new Promise((resolve, reject) => {
|
|
38748
|
-
childProcess.execFile(file, args, (error, stdout, stderr) => {
|
|
39027
|
+
childProcess.execFile(file, args, options, (error, stdout, stderr) => {
|
|
38749
39028
|
if (error) {
|
|
38750
39029
|
const err = error;
|
|
38751
39030
|
err.code = typeof error.code === "number" ? error.code : 1;
|
|
@@ -40484,6 +40763,12 @@ function isDockerRunning() {
|
|
|
40484
40763
|
return false;
|
|
40485
40764
|
}
|
|
40486
40765
|
}
|
|
40766
|
+
function shouldScaleOutNodeExporter(args) {
|
|
40767
|
+
const upIndex = args.indexOf("up");
|
|
40768
|
+
if (upIndex === -1)
|
|
40769
|
+
return false;
|
|
40770
|
+
return !args.slice(upIndex + 1).some((a) => !a.startsWith("-"));
|
|
40771
|
+
}
|
|
40487
40772
|
function getComposeCmd() {
|
|
40488
40773
|
const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
|
|
40489
40774
|
if (tryCmd("docker", ["compose", "version"]))
|
|
@@ -40505,16 +40790,6 @@ function checkRunningContainers() {
|
|
|
40505
40790
|
return { running: false, containers: [] };
|
|
40506
40791
|
}
|
|
40507
40792
|
}
|
|
40508
|
-
function planMonitoringRegistration(args) {
|
|
40509
|
-
const projectName = args.project?.trim() || undefined;
|
|
40510
|
-
if (args.instanceId) {
|
|
40511
|
-
return { kind: "adopt", projectName };
|
|
40512
|
-
}
|
|
40513
|
-
if (!projectName) {
|
|
40514
|
-
return { kind: "error-missing-project", projectName: undefined };
|
|
40515
|
-
}
|
|
40516
|
-
return { kind: "self-register", projectName };
|
|
40517
|
-
}
|
|
40518
40793
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
40519
40794
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
40520
40795
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -40549,7 +40824,8 @@ Debug: Registering monitoring instance...`);
|
|
|
40549
40824
|
"Content-Type": "application/json",
|
|
40550
40825
|
...orgScopeHeaders2(opts?.orgScope)
|
|
40551
40826
|
},
|
|
40552
|
-
body: JSON.stringify(requestBody)
|
|
40827
|
+
body: JSON.stringify(requestBody),
|
|
40828
|
+
signal: requestTimeoutSignal2(opts?.timeoutMs).signal
|
|
40553
40829
|
});
|
|
40554
40830
|
const body = await res.text().catch(() => "");
|
|
40555
40831
|
if (!res.ok) {
|
|
@@ -40584,6 +40860,25 @@ Debug: Registering monitoring instance...`);
|
|
|
40584
40860
|
}
|
|
40585
40861
|
return null;
|
|
40586
40862
|
}
|
|
40863
|
+
var INSTANCE_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
40864
|
+
function instanceIdToPersist(reg, requestedId) {
|
|
40865
|
+
for (const candidate of [reg?.instanceId, requestedId]) {
|
|
40866
|
+
const value = candidate?.trim();
|
|
40867
|
+
if (value && INSTANCE_ID_RE.test(value))
|
|
40868
|
+
return value;
|
|
40869
|
+
}
|
|
40870
|
+
return null;
|
|
40871
|
+
}
|
|
40872
|
+
function planMonitoringRegistration(args) {
|
|
40873
|
+
const projectName = args.project?.trim() || undefined;
|
|
40874
|
+
if (args.instanceId) {
|
|
40875
|
+
return { kind: "adopt", projectName };
|
|
40876
|
+
}
|
|
40877
|
+
if (!projectName) {
|
|
40878
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
40879
|
+
}
|
|
40880
|
+
return { kind: "self-register", projectName };
|
|
40881
|
+
}
|
|
40587
40882
|
var PROJECT_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
40588
40883
|
function resolveAdoptedProject(reg) {
|
|
40589
40884
|
if (!reg)
|
|
@@ -40600,8 +40895,11 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40600
40895
|
let lines = [];
|
|
40601
40896
|
if (fs9.existsSync(configPath)) {
|
|
40602
40897
|
const stats = fs9.statSync(configPath);
|
|
40603
|
-
if (!stats.
|
|
40604
|
-
|
|
40898
|
+
if (!stats.isFile()) {
|
|
40899
|
+
throw new Error(`${configPath} exists but is not a regular file; refusing to write through it`);
|
|
40900
|
+
}
|
|
40901
|
+
{
|
|
40902
|
+
const content = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "");
|
|
40605
40903
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
40606
40904
|
}
|
|
40607
40905
|
}
|
|
@@ -40609,6 +40907,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40609
40907
|
const existingIndex = lines.findIndex((l) => l.startsWith(key + "="));
|
|
40610
40908
|
if (existingIndex >= 0) {
|
|
40611
40909
|
lines[existingIndex] = `${key}=${value}`;
|
|
40910
|
+
lines = lines.filter((l, i) => i === existingIndex || !l.startsWith(key + "="));
|
|
40612
40911
|
} else {
|
|
40613
40912
|
lines.push(`${key}=${value}`);
|
|
40614
40913
|
}
|
|
@@ -40623,6 +40922,129 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40623
40922
|
console.error(`Warning: could not restrict permissions on ${configPath}: ${message}`);
|
|
40624
40923
|
}
|
|
40625
40924
|
}
|
|
40925
|
+
function applyApiKey(projectDir, apiKey) {
|
|
40926
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40927
|
+
try {
|
|
40928
|
+
updatePgwatchConfig(configPath, { api_key: apiKey });
|
|
40929
|
+
} catch (err) {
|
|
40930
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
40931
|
+
return false;
|
|
40932
|
+
}
|
|
40933
|
+
console.log(`\u2713 API key saved
|
|
40934
|
+
`);
|
|
40935
|
+
return true;
|
|
40936
|
+
}
|
|
40937
|
+
var DEFAULT_PROMQL_TIMEOUT_S = 11 * 60;
|
|
40938
|
+
function formatSeconds(total) {
|
|
40939
|
+
const s = Math.max(0, Math.round(total));
|
|
40940
|
+
if (s < 90)
|
|
40941
|
+
return `${s}s`;
|
|
40942
|
+
const m = Math.round(s / 60);
|
|
40943
|
+
if (m < 60)
|
|
40944
|
+
return `${m}m`;
|
|
40945
|
+
const h = Math.floor(m / 60);
|
|
40946
|
+
const rem = m % 60;
|
|
40947
|
+
return rem ? `${h}h${rem}m` : `${h}h`;
|
|
40948
|
+
}
|
|
40949
|
+
function parseDurationSeconds(v) {
|
|
40950
|
+
const m = /^(\d+)(s|m|h)?$/.exec(v.trim());
|
|
40951
|
+
if (!m)
|
|
40952
|
+
return null;
|
|
40953
|
+
const n = parseInt(m[1], 10);
|
|
40954
|
+
if (!Number.isFinite(n))
|
|
40955
|
+
return null;
|
|
40956
|
+
return n * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1);
|
|
40957
|
+
}
|
|
40958
|
+
function readInstanceIdFromProject() {
|
|
40959
|
+
try {
|
|
40960
|
+
const { projectDir } = resolvePaths();
|
|
40961
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40962
|
+
if (!fs9.existsSync(configPath))
|
|
40963
|
+
return null;
|
|
40964
|
+
const line = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "").split(/\r?\n/).find((l) => l.startsWith("instance_id="));
|
|
40965
|
+
const value = line?.slice("instance_id=".length).trim();
|
|
40966
|
+
return value && INSTANCE_ID_RE.test(value) ? value : null;
|
|
40967
|
+
} catch {
|
|
40968
|
+
return null;
|
|
40969
|
+
}
|
|
40970
|
+
}
|
|
40971
|
+
function applyProjectName(projectDir, project) {
|
|
40972
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40973
|
+
try {
|
|
40974
|
+
updatePgwatchConfig(configPath, { project_name: project });
|
|
40975
|
+
} catch (err) {
|
|
40976
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
40977
|
+
return false;
|
|
40978
|
+
}
|
|
40979
|
+
console.log(`Using project name: ${project}
|
|
40980
|
+
`);
|
|
40981
|
+
return true;
|
|
40982
|
+
}
|
|
40983
|
+
function persistInstanceId(projectDir, reg, requestedId, alsoWrite = {}) {
|
|
40984
|
+
const instanceId = instanceIdToPersist(reg, requestedId);
|
|
40985
|
+
const updates = { ...alsoWrite, ...instanceId ? { instance_id: instanceId } : {} };
|
|
40986
|
+
if (Object.keys(updates).length === 0)
|
|
40987
|
+
return false;
|
|
40988
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40989
|
+
try {
|
|
40990
|
+
updatePgwatchConfig(configPath, updates);
|
|
40991
|
+
return instanceId != null;
|
|
40992
|
+
} catch (err) {
|
|
40993
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40994
|
+
console.error(`Warning: could not record the monitoring instance id in ${configPath}: ${message}`);
|
|
40995
|
+
return false;
|
|
40996
|
+
}
|
|
40997
|
+
}
|
|
40998
|
+
async function adoptAndPersist(projectDir, apiKey, projectName, instanceId, opts, register = registerMonitoringInstance) {
|
|
40999
|
+
const reg = await register(apiKey, projectName, {
|
|
41000
|
+
orgScope: opts.orgScope,
|
|
41001
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41002
|
+
debug: opts.debug,
|
|
41003
|
+
instanceId
|
|
41004
|
+
});
|
|
41005
|
+
const adoptedProject = resolveAdoptedProject(reg);
|
|
41006
|
+
const persisted = persistInstanceId(projectDir, reg, instanceId, {
|
|
41007
|
+
...adoptedProject != null ? { project_name: adoptedProject } : {}
|
|
41008
|
+
});
|
|
41009
|
+
if (!persisted) {
|
|
41010
|
+
console.error(`\u26A0 Adopted instance ${instanceId} but could not record it in .pgwatch-config \u2014 ` + `instance jobs will idle until that file is writable and local-install is re-run
|
|
41011
|
+
`);
|
|
41012
|
+
}
|
|
41013
|
+
if (adoptedProject != null) {
|
|
41014
|
+
const verb = reg?.created ? "Registered" : "Adopted";
|
|
41015
|
+
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
41016
|
+
`);
|
|
41017
|
+
} else if (reg) {
|
|
41018
|
+
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41019
|
+
} else {
|
|
41020
|
+
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41021
|
+
}
|
|
41022
|
+
return reg;
|
|
41023
|
+
}
|
|
41024
|
+
async function selfRegisterAndPersist(projectDir, apiKey, projectName, opts, register = registerMonitoringInstance) {
|
|
41025
|
+
const reg = await register(apiKey, projectName, {
|
|
41026
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41027
|
+
debug: opts.debug,
|
|
41028
|
+
orgScope: opts.orgScope
|
|
41029
|
+
});
|
|
41030
|
+
const persisted = persistInstanceId(projectDir, reg, undefined);
|
|
41031
|
+
if (persisted) {
|
|
41032
|
+
console.log(`\u2713 Registered monitoring instance (project: ${projectName})
|
|
41033
|
+
`);
|
|
41034
|
+
return reg;
|
|
41035
|
+
}
|
|
41036
|
+
const minted = reg?.instanceId?.trim();
|
|
41037
|
+
if (minted) {
|
|
41038
|
+
console.error(`\u26A0 Registered monitoring instance ${minted} but could not record it in .pgwatch-config \u2014 ` + `instance jobs will idle. Add 'instance_id=${minted}' to that file, or re-run with ` + `--instance-id ${minted} (which adopts it rather than registering a second one)
|
|
41039
|
+
`);
|
|
41040
|
+
} else {
|
|
41041
|
+
const reRun = projectName ? `'postgresai mon local-install --project ${projectName}'` : "'postgresai mon local-install --project <name>'";
|
|
41042
|
+
console.error(`\u26A0 Could not register this monitoring instance \u2014 instance jobs will idle until ` + `${reRun} is re-run
|
|
41043
|
+
`);
|
|
41044
|
+
}
|
|
41045
|
+
process.exitCode = 1;
|
|
41046
|
+
return reg;
|
|
41047
|
+
}
|
|
40626
41048
|
async function applyMonitoringTargetsConfig() {
|
|
40627
41049
|
console.log("Applying monitoring target configuration...");
|
|
40628
41050
|
const generateCode = await runCompose(["run", "--rm", "sources-generator"]);
|
|
@@ -40696,7 +41118,7 @@ async function runCompose(args, grafanaPassword) {
|
|
|
40696
41118
|
}
|
|
40697
41119
|
}
|
|
40698
41120
|
const finalArgs = [...args];
|
|
40699
|
-
if (process.platform === "darwin" && args
|
|
41121
|
+
if (process.platform === "darwin" && shouldScaleOutNodeExporter(args)) {
|
|
40700
41122
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
40701
41123
|
}
|
|
40702
41124
|
return new Promise((resolve) => {
|
|
@@ -40708,6 +41130,51 @@ async function runCompose(args, grafanaPassword) {
|
|
|
40708
41130
|
child.on("close", (code) => resolve(code || 0));
|
|
40709
41131
|
});
|
|
40710
41132
|
}
|
|
41133
|
+
async function applySinkPrometheusConfig(projectDir) {
|
|
41134
|
+
const composeCmd = getComposeCmd();
|
|
41135
|
+
if (!composeCmd) {
|
|
41136
|
+
console.error(`
|
|
41137
|
+
\u2717 docker compose not found, so the sink-prometheus config was not applied.`);
|
|
41138
|
+
return false;
|
|
41139
|
+
}
|
|
41140
|
+
let running;
|
|
41141
|
+
try {
|
|
41142
|
+
const { stdout } = await execFilePromise(composeCmd[0], [
|
|
41143
|
+
...composeCmd.slice(1),
|
|
41144
|
+
"-f",
|
|
41145
|
+
path7.resolve(projectDir, "docker-compose.yml"),
|
|
41146
|
+
"ps",
|
|
41147
|
+
"-q",
|
|
41148
|
+
"sink-prometheus"
|
|
41149
|
+
], { cwd: projectDir });
|
|
41150
|
+
running = stdout.trim().length > 0;
|
|
41151
|
+
} catch (error) {
|
|
41152
|
+
console.error(`
|
|
41153
|
+
\u2717 Could not determine whether sink-prometheus is running.`);
|
|
41154
|
+
console.error(" The admin-endpoint keys may NOT be in effect. To apply them:");
|
|
41155
|
+
console.error(" docker compose up -d --no-deps sink-prometheus");
|
|
41156
|
+
if (process.env.DEBUG) {
|
|
41157
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
41158
|
+
}
|
|
41159
|
+
return false;
|
|
41160
|
+
}
|
|
41161
|
+
if (!running) {
|
|
41162
|
+
console.log(`
|
|
41163
|
+
(sink-prometheus is not running - the new config applies when you start the stack)`);
|
|
41164
|
+
return true;
|
|
41165
|
+
}
|
|
41166
|
+
console.log(`
|
|
41167
|
+
Applying sink-prometheus config (VictoriaMetrics admin keys, #359)...`);
|
|
41168
|
+
const code = await runCompose(["up", "-d", "--no-deps", "sink-prometheus"]);
|
|
41169
|
+
if (code !== 0) {
|
|
41170
|
+
console.error("\u2717 Could not apply the sink-prometheus config.");
|
|
41171
|
+
console.error(" The admin-endpoint keys are NOT in effect until you run:");
|
|
41172
|
+
console.error(" docker compose up -d --no-deps sink-prometheus");
|
|
41173
|
+
return false;
|
|
41174
|
+
}
|
|
41175
|
+
console.log("\u2713 sink-prometheus is running the current config");
|
|
41176
|
+
return true;
|
|
41177
|
+
}
|
|
40711
41178
|
program2.command("help").description("show help").action(() => {
|
|
40712
41179
|
program2.outputHelp();
|
|
40713
41180
|
});
|
|
@@ -40734,19 +41201,17 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
40734
41201
|
const { projectDir, instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
40735
41202
|
console.log(`Project directory: ${projectDir}
|
|
40736
41203
|
`);
|
|
40737
|
-
if (opts.project) {
|
|
40738
|
-
|
|
40739
|
-
|
|
40740
|
-
console.log(`Using project name: ${opts.project}
|
|
40741
|
-
`);
|
|
41204
|
+
if (opts.project && !applyProjectName(projectDir, opts.project)) {
|
|
41205
|
+
process.exitCode = 1;
|
|
41206
|
+
return;
|
|
40742
41207
|
}
|
|
40743
41208
|
const envFile = path7.resolve(projectDir, ".env");
|
|
40744
41209
|
const existingEnv = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
|
|
40745
41210
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
40746
41211
|
const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
|
|
40747
41212
|
const imageTag = opts.tag || package_default.version;
|
|
40748
|
-
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag);
|
|
40749
|
-
|
|
41213
|
+
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"));
|
|
41214
|
+
writeEnvFile(envFile, envContent);
|
|
40750
41215
|
if (preservedKeys.length > 0) {
|
|
40751
41216
|
console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
|
|
40752
41217
|
`);
|
|
@@ -40785,9 +41250,10 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40785
41250
|
if (apiKey) {
|
|
40786
41251
|
console.log("Using API key provided via --api-key parameter");
|
|
40787
41252
|
writeConfig({ apiKey });
|
|
40788
|
-
|
|
40789
|
-
|
|
40790
|
-
|
|
41253
|
+
if (!applyApiKey(projectDir, apiKey)) {
|
|
41254
|
+
process.exitCode = 1;
|
|
41255
|
+
return;
|
|
41256
|
+
}
|
|
40791
41257
|
} else if (opts.yes) {
|
|
40792
41258
|
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
40793
41259
|
console.error("\u26A0 Reports will be generated locally only");
|
|
@@ -40802,10 +41268,11 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40802
41268
|
const trimmedKey = inputApiKey.trim();
|
|
40803
41269
|
if (trimmedKey) {
|
|
40804
41270
|
writeConfig({ apiKey: trimmedKey });
|
|
40805
|
-
|
|
41271
|
+
if (!applyApiKey(projectDir, trimmedKey)) {
|
|
41272
|
+
process.exitCode = 1;
|
|
41273
|
+
return;
|
|
41274
|
+
}
|
|
40806
41275
|
apiKey = trimmedKey;
|
|
40807
|
-
console.log(`\u2713 API key saved
|
|
40808
|
-
`);
|
|
40809
41276
|
break;
|
|
40810
41277
|
}
|
|
40811
41278
|
console.error("\u26A0 API key cannot be empty");
|
|
@@ -41042,9 +41509,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41042
41509
|
const envLines = envContent.split(/\r?\n/).filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l)).filter((l, i, arr) => !(i === arr.length - 1 && l === ""));
|
|
41043
41510
|
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
41044
41511
|
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
41045
|
-
|
|
41512
|
+
writeEnvFile(envFile, envLines.join(`
|
|
41046
41513
|
`) + `
|
|
41047
|
-
|
|
41514
|
+
`);
|
|
41048
41515
|
}
|
|
41049
41516
|
console.log(`\u2713 VictoriaMetrics auth configured
|
|
41050
41517
|
`);
|
|
@@ -41086,25 +41553,11 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41086
41553
|
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
41087
41554
|
const projectName = plan.projectName;
|
|
41088
41555
|
if (instanceId) {
|
|
41089
|
-
const reg = await
|
|
41556
|
+
const reg = await adoptAndPersist(projectDir, apiKey, projectName, instanceId, {
|
|
41090
41557
|
orgScope: regOrgScope,
|
|
41091
41558
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41092
|
-
debug: !!process.env.DEBUG
|
|
41093
|
-
instanceId
|
|
41559
|
+
debug: !!process.env.DEBUG
|
|
41094
41560
|
});
|
|
41095
|
-
const adoptedProject = resolveAdoptedProject(reg);
|
|
41096
|
-
if (adoptedProject != null) {
|
|
41097
|
-
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
|
|
41098
|
-
project_name: adoptedProject
|
|
41099
|
-
});
|
|
41100
|
-
const verb = reg?.created ? "Registered" : "Adopted";
|
|
41101
|
-
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
41102
|
-
`);
|
|
41103
|
-
} else if (reg) {
|
|
41104
|
-
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41105
|
-
} else {
|
|
41106
|
-
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41107
|
-
}
|
|
41108
41561
|
if (reg === null) {
|
|
41109
41562
|
console.error(`\u26A0 Skipping AAS auto-collection arming because instance adoption failed; it can be enabled later by re-running 'postgresai mon local-install'
|
|
41110
41563
|
`);
|
|
@@ -41129,7 +41582,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41129
41582
|
console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
|
|
41130
41583
|
process.exitCode = 1;
|
|
41131
41584
|
} else {
|
|
41132
|
-
|
|
41585
|
+
await selfRegisterAndPersist(projectDir, apiKey, projectName, {
|
|
41133
41586
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41134
41587
|
debug: !!process.env.DEBUG,
|
|
41135
41588
|
orgScope: regOrgScope
|
|
@@ -41198,7 +41651,8 @@ var MONITORING_CONTAINERS = [
|
|
|
41198
41651
|
"self-postgres-exporter",
|
|
41199
41652
|
"flask-pgss-api",
|
|
41200
41653
|
"sources-generator",
|
|
41201
|
-
"postgres-reports"
|
|
41654
|
+
"postgres-reports",
|
|
41655
|
+
"instance-jobs"
|
|
41202
41656
|
];
|
|
41203
41657
|
var COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
41204
41658
|
var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
@@ -41251,15 +41705,31 @@ mon.command("logs [service]").option("-f, --follow", "follow logs", false).optio
|
|
|
41251
41705
|
if (code !== 0)
|
|
41252
41706
|
process.exitCode = code;
|
|
41253
41707
|
});
|
|
41254
|
-
|
|
41255
|
-
|
|
41708
|
+
function judgeServiceHealth(service, present, status, health) {
|
|
41709
|
+
if (!present) {
|
|
41710
|
+
return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: "unreachable" };
|
|
41711
|
+
}
|
|
41712
|
+
if (status !== "running") {
|
|
41713
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (status: ${status})` };
|
|
41714
|
+
}
|
|
41715
|
+
if (service.readHealth && health && health !== "healthy" && health !== "starting") {
|
|
41716
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (${health})` };
|
|
41717
|
+
}
|
|
41718
|
+
return { ok: true, mark: "\u2713", label: "healthy" };
|
|
41719
|
+
}
|
|
41720
|
+
function monitoringHealthServices() {
|
|
41721
|
+
return [
|
|
41256
41722
|
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
41257
41723
|
{ name: "Prometheus", container: "sink-prometheus" },
|
|
41258
41724
|
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
41259
41725
|
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
41260
41726
|
{ name: "Target DB", container: "target-db" },
|
|
41261
|
-
{ name: "Sink Postgres", container: "sink-postgres" }
|
|
41727
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
41728
|
+
{ name: "Instance jobs", container: "instance-jobs", optional: true, readHealth: true }
|
|
41262
41729
|
];
|
|
41730
|
+
}
|
|
41731
|
+
mon.command("health").description("health check for monitoring services").option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0).action(async (opts) => {
|
|
41732
|
+
const services = monitoringHealthServices();
|
|
41263
41733
|
const waitTime = opts.wait || 0;
|
|
41264
41734
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
41265
41735
|
console.log(`Checking service health...
|
|
@@ -41275,16 +41745,12 @@ mon.command("health").description("health check for monitoring services").option
|
|
|
41275
41745
|
for (const service of services) {
|
|
41276
41746
|
try {
|
|
41277
41747
|
const result = spawnSync2("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
41278
|
-
const
|
|
41279
|
-
if
|
|
41280
|
-
|
|
41281
|
-
}
|
|
41282
|
-
|
|
41283
|
-
allHealthy = false;
|
|
41284
|
-
} else {
|
|
41285
|
-
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41748
|
+
const present = result.status === 0;
|
|
41749
|
+
const health = present && service.readHealth ? (spawnSync2("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
|
|
41750
|
+
const verdict = judgeServiceHealth(service, present, (result.stdout || "").trim(), health);
|
|
41751
|
+
console.log(`${verdict.mark} ${service.name}: ${verdict.label}`);
|
|
41752
|
+
if (!verdict.ok)
|
|
41286
41753
|
allHealthy = false;
|
|
41287
|
-
}
|
|
41288
41754
|
} catch (error) {
|
|
41289
41755
|
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41290
41756
|
allHealthy = false;
|
|
@@ -41345,8 +41811,13 @@ mon.command("update-config").description("apply monitoring services configuratio
|
|
|
41345
41811
|
}
|
|
41346
41812
|
await refreshBundledComposeIfStale(projectDir);
|
|
41347
41813
|
const code = await runCompose(["run", "--rm", "sources-generator"]);
|
|
41348
|
-
if (code !== 0)
|
|
41814
|
+
if (code !== 0) {
|
|
41349
41815
|
process.exitCode = code;
|
|
41816
|
+
return;
|
|
41817
|
+
}
|
|
41818
|
+
if (!await applySinkPrometheusConfig(projectDir)) {
|
|
41819
|
+
process.exitCode = 1;
|
|
41820
|
+
}
|
|
41350
41821
|
});
|
|
41351
41822
|
mon.command("update").description("update monitoring stack (migrate .env, pull images)").action(async () => {
|
|
41352
41823
|
console.log(`Updating PostgresAI monitoring stack...
|
|
@@ -41388,6 +41859,10 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
41388
41859
|
Updating Docker images...`);
|
|
41389
41860
|
const code = await runCompose(["pull"]);
|
|
41390
41861
|
if (code === 0) {
|
|
41862
|
+
if (!await applySinkPrometheusConfig(projectDir)) {
|
|
41863
|
+
process.exitCode = 1;
|
|
41864
|
+
return;
|
|
41865
|
+
}
|
|
41391
41866
|
console.log(`
|
|
41392
41867
|
\u2713 Update completed successfully`);
|
|
41393
41868
|
console.log(`
|
|
@@ -42089,8 +42564,87 @@ program2.command("orgs").description("list organizations this credential can rea
|
|
|
42089
42564
|
process.exitCode = 1;
|
|
42090
42565
|
}
|
|
42091
42566
|
});
|
|
42567
|
+
withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config)").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
|
|
42568
|
+
try {
|
|
42569
|
+
const rootOpts = program2.opts();
|
|
42570
|
+
const cfg = readConfig();
|
|
42571
|
+
const { apiKey } = getConfig(rootOpts);
|
|
42572
|
+
if (!apiKey) {
|
|
42573
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
42574
|
+
process.exitCode = 1;
|
|
42575
|
+
return;
|
|
42576
|
+
}
|
|
42577
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42578
|
+
const instanceId = opts.instance || readInstanceIdFromProject();
|
|
42579
|
+
if (!instanceId) {
|
|
42580
|
+
console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
|
|
42581
|
+
console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
|
|
42582
|
+
process.exitCode = 1;
|
|
42583
|
+
return;
|
|
42584
|
+
}
|
|
42585
|
+
const kind = opts.range ? "promql_range" : "promql_instant";
|
|
42586
|
+
const enqueue = { instanceId, kind, query: expr };
|
|
42587
|
+
if (opts.range) {
|
|
42588
|
+
if (!opts.start || !opts.end || !opts.step) {
|
|
42589
|
+
console.error("--range needs --start, --end and --step.");
|
|
42590
|
+
process.exitCode = 1;
|
|
42591
|
+
return;
|
|
42592
|
+
}
|
|
42593
|
+
const stepS = parseDurationSeconds(opts.step);
|
|
42594
|
+
if (stepS === null || stepS < 1) {
|
|
42595
|
+
console.error(`Invalid --step ${opts.step}: expected a duration like 60s, 5m or 1h.`);
|
|
42596
|
+
process.exitCode = 1;
|
|
42597
|
+
return;
|
|
42598
|
+
}
|
|
42599
|
+
enqueue.start = opts.start;
|
|
42600
|
+
enqueue.end = opts.end;
|
|
42601
|
+
enqueue.stepS = stepS;
|
|
42602
|
+
} else if (opts.at) {
|
|
42603
|
+
enqueue.at = opts.at;
|
|
42604
|
+
}
|
|
42605
|
+
const explicitTimeoutS = opts.timeout ? parseDurationSeconds(opts.timeout) : null;
|
|
42606
|
+
if (opts.timeout && (explicitTimeoutS === null || explicitTimeoutS < 1)) {
|
|
42607
|
+
console.error(`Invalid --timeout ${opts.timeout}: expected a duration like 60s or 2m.`);
|
|
42608
|
+
process.exitCode = 1;
|
|
42609
|
+
return;
|
|
42610
|
+
}
|
|
42611
|
+
const { jobId, firstAnswerEstimateS } = await enqueueQuery(apiKey, enqueue, { apiBaseUrl });
|
|
42612
|
+
const timeoutS = explicitTimeoutS ?? (firstAnswerEstimateS === null ? DEFAULT_PROMQL_TIMEOUT_S : Math.min(Math.max(Math.ceil(firstAnswerEstimateS * 1.5) + 30, 60), 1800));
|
|
42613
|
+
console.error(firstAnswerEstimateS === null ? `Queued as job ${jobId}; waiting up to ${formatSeconds(timeoutS)} for the instance's next poll.` : `Queued as job ${jobId}; this instance polls roughly every ${formatSeconds(firstAnswerEstimateS)} ` + `(jittered, so sometimes longer). Waiting up to ${formatSeconds(timeoutS)}.`);
|
|
42614
|
+
const res = await awaitQueryResult(apiKey, jobId, {
|
|
42615
|
+
apiBaseUrl,
|
|
42616
|
+
timeoutMs: timeoutS * 1000
|
|
42617
|
+
});
|
|
42618
|
+
if (!isTerminal2(res.status)) {
|
|
42619
|
+
console.error(`Timed out after ${timeoutS}s. Job ${jobId} is still ${res.status}.`);
|
|
42620
|
+
console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running (it is behind " + "a compose profile), or wait \u2014 a first collection can queue ~20 periods ahead of it." : "The instance is working on it \u2014 wait, or re-run with a longer --timeout.");
|
|
42621
|
+
process.exitCode = 1;
|
|
42622
|
+
return;
|
|
42623
|
+
}
|
|
42624
|
+
if (res.status !== "done" || res.outcome !== "ok") {
|
|
42625
|
+
console.error(`Query ${res.status}${res.failure_class ? ` (${res.failure_class})` : ""}.`);
|
|
42626
|
+
if (res.error)
|
|
42627
|
+
console.error(res.error);
|
|
42628
|
+
process.exitCode = 1;
|
|
42629
|
+
return;
|
|
42630
|
+
}
|
|
42631
|
+
if (!res.result) {
|
|
42632
|
+
console.error("The job finished but carried no result.");
|
|
42633
|
+
process.exitCode = 1;
|
|
42634
|
+
return;
|
|
42635
|
+
}
|
|
42636
|
+
if (opts.json) {
|
|
42637
|
+
console.log(JSON.stringify(res.result, null, 2));
|
|
42638
|
+
return;
|
|
42639
|
+
}
|
|
42640
|
+
console.log(renderPromQL(res.result));
|
|
42641
|
+
} catch (err) {
|
|
42642
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
42643
|
+
process.exitCode = 1;
|
|
42644
|
+
}
|
|
42645
|
+
});
|
|
42092
42646
|
var issues = program2.command("issues").description("issues management");
|
|
42093
|
-
withOrgOptions(issues.command("list")).description("list issues").option("--status <status>", "filter by status: open, closed, or all
|
|
42647
|
+
withOrgOptions(issues.command("list")).description("list issues").option("--status <status>", "filter by status: open (default), closed, or 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("--hidden-only", "list only hidden issues (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
42094
42648
|
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
42095
42649
|
try {
|
|
42096
42650
|
const rootOpts = program2.opts();
|
|
@@ -42105,11 +42659,15 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42105
42659
|
const orgScope = getActiveOrgScope2() ?? { source: "token" };
|
|
42106
42660
|
const orgId = configOrgIdForBody(apiKey, cfg.orgId);
|
|
42107
42661
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42662
|
+
const statusArg = (opts.status ?? "open").trim().toLowerCase();
|
|
42108
42663
|
let statusFilter;
|
|
42109
|
-
if (
|
|
42110
|
-
statusFilter =
|
|
42111
|
-
} else if (
|
|
42112
|
-
|
|
42664
|
+
if (statusArg === "open" || statusArg === "closed") {
|
|
42665
|
+
statusFilter = statusArg;
|
|
42666
|
+
} else if (statusArg !== "all") {
|
|
42667
|
+
spinner.stop();
|
|
42668
|
+
console.error("--status must be open, closed, or all");
|
|
42669
|
+
process.exitCode = 1;
|
|
42670
|
+
return;
|
|
42113
42671
|
}
|
|
42114
42672
|
const result = await fetchIssues2({
|
|
42115
42673
|
apiKey,
|
|
@@ -42123,7 +42681,7 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42123
42681
|
debug: !!opts.debug
|
|
42124
42682
|
});
|
|
42125
42683
|
spinner.stop();
|
|
42126
|
-
const trimmed = Array.isArray(result) ? result.map((r) =>
|
|
42684
|
+
const trimmed = Array.isArray(result) ? result.map((r) => presentIssue2({
|
|
42127
42685
|
id: r.id,
|
|
42128
42686
|
title: r.title,
|
|
42129
42687
|
status: r.status,
|
|
@@ -42161,7 +42719,7 @@ withOrgOptions(issues.command("view <issueId>")).description("view issue details
|
|
|
42161
42719
|
spinner.update("Fetching comments...");
|
|
42162
42720
|
const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
42163
42721
|
spinner.stop();
|
|
42164
|
-
const combined = { issue:
|
|
42722
|
+
const combined = { issue: presentIssue2(issue), comments };
|
|
42165
42723
|
printResult(combined, opts.json);
|
|
42166
42724
|
} catch (err) {
|
|
42167
42725
|
spinner.stop();
|
|
@@ -42228,7 +42786,7 @@ withOrgOptions(issues.command("create <title>")).description("create a new issue
|
|
|
42228
42786
|
}, []).option("--attach <path>", "attach a file (uploads to storage and appends a markdown link to the description; repeatable)", (value, previous) => {
|
|
42229
42787
|
previous.push(value);
|
|
42230
42788
|
return previous;
|
|
42231
|
-
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
|
|
42789
|
+
}, []).option("--hidden", "create as a hidden issue (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
|
|
42232
42790
|
const rootOpts = program2.opts();
|
|
42233
42791
|
const cfg = readConfig();
|
|
42234
42792
|
const { apiKey } = getConfig(rootOpts);
|
|
@@ -42289,6 +42847,7 @@ withOrgOptions(issues.command("create <title>")).description("create a new issue
|
|
|
42289
42847
|
description: augmentedDescription,
|
|
42290
42848
|
projectId,
|
|
42291
42849
|
labels,
|
|
42850
|
+
hidden: opts.hidden === true,
|
|
42292
42851
|
debug: !!opts.debug
|
|
42293
42852
|
});
|
|
42294
42853
|
spinner.stop();
|
|
@@ -42306,7 +42865,7 @@ withOrgOptions(issues.command("update <issueId>")).description("update an existi
|
|
|
42306
42865
|
}, []).option("--clear-labels", "set labels to an empty list").option("--attach <path>", "attach a file (uploads and appends a markdown link to --description; if --description is omitted the existing description is fetched and appended to; repeatable)", (value, previous) => {
|
|
42307
42866
|
previous.push(value);
|
|
42308
42867
|
return previous;
|
|
42309
|
-
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
42868
|
+
}, []).option("--hidden", "hide the issue (PostgresAI staff)").option("--no-hidden", "unhide the issue (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
42310
42869
|
const rootOpts = program2.opts();
|
|
42311
42870
|
const cfg = readConfig();
|
|
42312
42871
|
const { apiKey } = getConfig(rootOpts);
|
|
@@ -42377,6 +42936,7 @@ withOrgOptions(issues.command("update <issueId>")).description("update an existi
|
|
|
42377
42936
|
description,
|
|
42378
42937
|
status,
|
|
42379
42938
|
labels,
|
|
42939
|
+
hidden: typeof opts.hidden === "boolean" ? opts.hidden : undefined,
|
|
42380
42940
|
debug: !!opts.debug
|
|
42381
42941
|
});
|
|
42382
42942
|
spinner.stop();
|
|
@@ -43363,13 +43923,23 @@ if (__require.main == __require.module) {
|
|
|
43363
43923
|
export {
|
|
43364
43924
|
LOCAL_INSTALL_MANAGED_ENV_KEYS,
|
|
43365
43925
|
ORG_SCOPED_COMMANDS,
|
|
43926
|
+
adoptAndPersist,
|
|
43927
|
+
applyApiKey,
|
|
43928
|
+
applyProjectName,
|
|
43366
43929
|
buildLocalInstallEnv,
|
|
43930
|
+
instanceIdToPersist,
|
|
43367
43931
|
isValidComposeYaml,
|
|
43932
|
+
judgeServiceHealth,
|
|
43933
|
+
monitoringHealthServices,
|
|
43934
|
+
persistInstanceId,
|
|
43368
43935
|
planMonitoringRegistration,
|
|
43369
43936
|
program2 as program,
|
|
43370
43937
|
readDeployedTag,
|
|
43371
43938
|
refreshBundledComposeIfStale,
|
|
43372
43939
|
registerMonitoringInstance,
|
|
43373
43940
|
resolveAdoptedProject,
|
|
43941
|
+
selfRegisterAndPersist,
|
|
43942
|
+
shouldScaleOutNodeExporter,
|
|
43943
|
+
statOwner,
|
|
43374
43944
|
updatePgwatchConfig
|
|
43375
43945
|
};
|