postgresai 0.17.0-rc.3 → 0.17.0-rc.5
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/dist/bin/postgres-ai.js +730 -120
- package/package.json +1 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13466,8 +13466,11 @@ function resolveBaseUrls2(opts, cfg, defaults = {}) {
|
|
|
13466
13466
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13467
13467
|
};
|
|
13468
13468
|
}
|
|
13469
|
-
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000;
|
|
13470
|
-
var init_util = () => {
|
|
13469
|
+
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, SENSITIVE_KEY_SRC2 = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)", SENSITIVE_PAIR_SEPARATOR_SRC2 = `["']?\\s*[:=]`, CREDENTIAL_CHAR_SRC2 = "[A-Za-z0-9_+/=~.-]", TEXT_SENSITIVE_PAIR2, CREDENTIAL_RUN_ONLY2;
|
|
13470
|
+
var init_util = __esm(() => {
|
|
13471
|
+
TEXT_SENSITIVE_PAIR2 = new RegExp(`(${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2})[^\\s,;&]+)*)` + `)`, "gi");
|
|
13472
|
+
CREDENTIAL_RUN_ONLY2 = new RegExp(`^${CREDENTIAL_CHAR_SRC2}+$`);
|
|
13473
|
+
});
|
|
13471
13474
|
|
|
13472
13475
|
// node_modules/commander/index.js
|
|
13473
13476
|
var exports_commander = {};
|
|
@@ -13530,7 +13533,7 @@ var {
|
|
|
13530
13533
|
// package.json
|
|
13531
13534
|
var package_default = {
|
|
13532
13535
|
name: "postgresai",
|
|
13533
|
-
version: "0.17.0-rc.
|
|
13536
|
+
version: "0.17.0-rc.5",
|
|
13534
13537
|
description: "postgres_ai CLI",
|
|
13535
13538
|
license: "Apache-2.0",
|
|
13536
13539
|
private: false,
|
|
@@ -16890,7 +16893,7 @@ var Result = import_lib.default.Result;
|
|
|
16890
16893
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16891
16894
|
var defaults = import_lib.default.defaults;
|
|
16892
16895
|
// package.json
|
|
16893
|
-
var version = "0.17.0-rc.
|
|
16896
|
+
var version = "0.17.0-rc.5";
|
|
16894
16897
|
var package_default2 = {
|
|
16895
16898
|
name: "postgresai",
|
|
16896
16899
|
version,
|
|
@@ -17166,6 +17169,22 @@ class HttpStatusError extends Error {
|
|
|
17166
17169
|
function isRetryableHttpStatus(status) {
|
|
17167
17170
|
return status >= 500 || status === 429;
|
|
17168
17171
|
}
|
|
17172
|
+
function isUnknownRpcSignature(status, text, rpc) {
|
|
17173
|
+
if (status !== 404)
|
|
17174
|
+
return false;
|
|
17175
|
+
try {
|
|
17176
|
+
const parsed = JSON.parse(text);
|
|
17177
|
+
return typeof parsed.message === "string" && /Could not find the .* function/.test(parsed.message) && parsed.message.includes(`${rpc}(`);
|
|
17178
|
+
} catch {
|
|
17179
|
+
return false;
|
|
17180
|
+
}
|
|
17181
|
+
}
|
|
17182
|
+
function positiveSeconds(v) {
|
|
17183
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
17184
|
+
}
|
|
17185
|
+
function asyncPollDelayMs(attempt) {
|
|
17186
|
+
return Math.min(1000 * 2 ** attempt, 15000);
|
|
17187
|
+
}
|
|
17169
17188
|
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
|
|
17170
17189
|
|
|
17171
17190
|
class HttpRequestTimeoutError extends Error {
|
|
@@ -17219,9 +17238,120 @@ function redactSecretsForLog(text) {
|
|
|
17219
17238
|
return JSON.stringify(redactSecrets(parsed));
|
|
17220
17239
|
}
|
|
17221
17240
|
var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
|
|
17222
|
-
var
|
|
17241
|
+
var SENSITIVE_KEY_SRC = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)";
|
|
17242
|
+
var SENSITIVE_PAIR_SEPARATOR_SRC = `["']?\\s*[:=]`;
|
|
17243
|
+
var CREDENTIAL_CHAR_SRC = "[A-Za-z0-9_+/=~.-]";
|
|
17244
|
+
var MIN_CREDENTIAL_RUN = 12;
|
|
17245
|
+
var TEXT_SENSITIVE_PAIR = new RegExp(`(${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC})[^\\s,;&]+)*)` + `)`, "gi");
|
|
17246
|
+
var CREDENTIAL_RUN_ONLY = new RegExp(`^${CREDENTIAL_CHAR_SRC}+$`);
|
|
17247
|
+
var RUN_LEADING_PUNCTUATION = /^[("'\[{<]+/;
|
|
17248
|
+
var RUN_TRAILING_PUNCTUATION = /[)"'\]}>!?.,;:]+$/;
|
|
17249
|
+
var LONG_CONSONANT_RUN = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]{5,}/;
|
|
17250
|
+
var UUID_SHAPE = /^[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}$/;
|
|
17251
|
+
function isWordShaped(s) {
|
|
17252
|
+
if (!/^[A-Za-z]+$/.test(s))
|
|
17253
|
+
return false;
|
|
17254
|
+
if (s.length <= 2)
|
|
17255
|
+
return true;
|
|
17256
|
+
if (hasInternalCapital(s))
|
|
17257
|
+
return false;
|
|
17258
|
+
if (!/[aeiouyAEIOUY]/.test(s))
|
|
17259
|
+
return false;
|
|
17260
|
+
return !LONG_CONSONANT_RUN.test(s);
|
|
17261
|
+
}
|
|
17262
|
+
function hasInternalCapital(s) {
|
|
17263
|
+
return /[a-z]/.test(s) && /.[A-Z]/.test(s);
|
|
17264
|
+
}
|
|
17265
|
+
function hasHardStructure(core) {
|
|
17266
|
+
if (/=/.test(core.replace(/={1,2}$/, "")))
|
|
17267
|
+
return true;
|
|
17268
|
+
return UUID_SHAPE.test(core);
|
|
17269
|
+
}
|
|
17270
|
+
function isSeparatedName(core) {
|
|
17271
|
+
const segments = core.split(/[/_.-]/).filter((part) => part.length > 0);
|
|
17272
|
+
if (segments.length < 2)
|
|
17273
|
+
return false;
|
|
17274
|
+
if (!segments.some(isNameSegment))
|
|
17275
|
+
return false;
|
|
17276
|
+
return segments.every((part) => isNameSegment(part) || /^[0-9]{1,4}$/.test(part));
|
|
17277
|
+
}
|
|
17278
|
+
function isNameSegment(part) {
|
|
17279
|
+
return isWordShaped(part) || /^[A-Z]+$/.test(part) && part.length <= 6;
|
|
17280
|
+
}
|
|
17281
|
+
function hasTokenSignal(core) {
|
|
17282
|
+
if (/[0-9_+]/.test(core))
|
|
17283
|
+
return true;
|
|
17284
|
+
if (/=$/.test(core))
|
|
17285
|
+
return true;
|
|
17286
|
+
if (hasInternalCapital(core))
|
|
17287
|
+
return true;
|
|
17288
|
+
return LONG_CONSONANT_RUN.test(core);
|
|
17289
|
+
}
|
|
17290
|
+
function runCore(run) {
|
|
17291
|
+
return run.replace(RUN_LEADING_PUNCTUATION, "").replace(RUN_TRAILING_PUNCTUATION, "");
|
|
17292
|
+
}
|
|
17293
|
+
function looksLikeCredentialRun(core) {
|
|
17294
|
+
return core.length >= MIN_CREDENTIAL_RUN && !hasHardStructure(core) && hasTokenSignal(core);
|
|
17295
|
+
}
|
|
17296
|
+
function isChainableFragment(core) {
|
|
17297
|
+
return core.length > 0 && CREDENTIAL_RUN_ONLY.test(core) && !hasHardStructure(core) && !isWordShaped(core);
|
|
17298
|
+
}
|
|
17299
|
+
function replaceSensitivePair(_match, keyPrefix, quotedValue, _bareValue, tail) {
|
|
17300
|
+
if (quotedValue !== undefined)
|
|
17301
|
+
return `${keyPrefix}[REDACTED]`;
|
|
17302
|
+
const runs = [...(tail ?? "").matchAll(/(\s+)(\S+)/g)].map((m) => {
|
|
17303
|
+
const run = m[2] ?? "";
|
|
17304
|
+
return { gap: m[1] ?? "", run, core: runCore(run) };
|
|
17305
|
+
});
|
|
17306
|
+
const absorbed = runs.map(() => false);
|
|
17307
|
+
for (let i = 0;i < runs.length; ) {
|
|
17308
|
+
if (!isChainableFragment(runs[i].core)) {
|
|
17309
|
+
i++;
|
|
17310
|
+
continue;
|
|
17311
|
+
}
|
|
17312
|
+
let end = i;
|
|
17313
|
+
while (end < runs.length && isChainableFragment(runs[end].core))
|
|
17314
|
+
end++;
|
|
17315
|
+
const members = runs.slice(i, end);
|
|
17316
|
+
const chain = members.map((r) => r.core).join("");
|
|
17317
|
+
const allNames = members.every((r) => isSeparatedName(r.core));
|
|
17318
|
+
if (!allNames && looksLikeCredentialRun(chain))
|
|
17319
|
+
for (let k = i;k < end; k++)
|
|
17320
|
+
absorbed[k] = true;
|
|
17321
|
+
i = end;
|
|
17322
|
+
}
|
|
17323
|
+
let kept = "";
|
|
17324
|
+
for (let k = 0;k < runs.length; k++) {
|
|
17325
|
+
const r = runs[k];
|
|
17326
|
+
if (!absorbed[k]) {
|
|
17327
|
+
kept += r.gap + r.run;
|
|
17328
|
+
continue;
|
|
17329
|
+
}
|
|
17330
|
+
const lead = r.run.match(RUN_LEADING_PUNCTUATION)?.[0] ?? "";
|
|
17331
|
+
const trail = r.run.match(RUN_TRAILING_PUNCTUATION)?.[0] ?? "";
|
|
17332
|
+
if (lead || trail)
|
|
17333
|
+
kept += r.gap + lead + trail;
|
|
17334
|
+
}
|
|
17335
|
+
return `${keyPrefix}[REDACTED]${kept}`;
|
|
17336
|
+
}
|
|
17223
17337
|
function redactTextSecrets(text) {
|
|
17224
|
-
return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR,
|
|
17338
|
+
return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, replaceSensitivePair);
|
|
17339
|
+
}
|
|
17340
|
+
function isControlCodePoint(cp) {
|
|
17341
|
+
return cp < 32 || cp === 127 || cp >= 128 && cp <= 159 || cp === 8232 || cp === 8233;
|
|
17342
|
+
}
|
|
17343
|
+
function stripControlsToSpace(text) {
|
|
17344
|
+
let out = "";
|
|
17345
|
+
for (const ch of text) {
|
|
17346
|
+
const cp = ch.codePointAt(0);
|
|
17347
|
+
out += cp !== undefined && isControlCodePoint(cp) ? " " : ch;
|
|
17348
|
+
}
|
|
17349
|
+
return out;
|
|
17350
|
+
}
|
|
17351
|
+
function whitespaceTolerantSecretPattern(secret) {
|
|
17352
|
+
const chars = Array.from(secret.replace(/\s+/g, ""));
|
|
17353
|
+
const body = chars.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
|
|
17354
|
+
return new RegExp(body, "g");
|
|
17225
17355
|
}
|
|
17226
17356
|
function normalizeBaseUrl(value) {
|
|
17227
17357
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
@@ -26457,68 +26587,103 @@ function renderMarkdownForTerminal(md) {
|
|
|
26457
26587
|
// lib/joe.ts
|
|
26458
26588
|
var DEFAULT_BUDGET_MS = 25000;
|
|
26459
26589
|
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
26590
|
+
var JOE_ASYNC_FALLBACK_WAIT_MS = 10 * 60 * 1000;
|
|
26591
|
+
var JOE_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
|
|
26592
|
+
var JOE_ASYNC_MAX_POLL_FAILURES = 5;
|
|
26460
26593
|
async function callRpc(params) {
|
|
26594
|
+
return (await callRpcRaw(params)).data;
|
|
26595
|
+
}
|
|
26596
|
+
async function callRpcRaw(params) {
|
|
26461
26597
|
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26462
26598
|
if (!apiKey) {
|
|
26463
26599
|
throw new Error("API key is required");
|
|
26464
26600
|
}
|
|
26465
26601
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26466
26602
|
const url = new URL(`${base}/rpc/${fn}`);
|
|
26467
|
-
const payload = JSON.stringify(body);
|
|
26468
26603
|
const headers = buildAuthHeaders(apiKey);
|
|
26469
|
-
|
|
26470
|
-
const
|
|
26471
|
-
|
|
26472
|
-
|
|
26473
|
-
|
|
26474
|
-
|
|
26475
|
-
|
|
26476
|
-
|
|
26477
|
-
|
|
26478
|
-
|
|
26479
|
-
|
|
26480
|
-
|
|
26481
|
-
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26604
|
+
const send = async (bodyObj) => {
|
|
26605
|
+
const payload = JSON.stringify(bodyObj);
|
|
26606
|
+
if (debug) {
|
|
26607
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26608
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26609
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26610
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
26611
|
+
}
|
|
26612
|
+
let response;
|
|
26613
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
26614
|
+
try {
|
|
26615
|
+
response = await fetch(url.toString(), {
|
|
26616
|
+
method: "POST",
|
|
26617
|
+
headers,
|
|
26618
|
+
body: payload,
|
|
26619
|
+
signal: requestTimeout.signal
|
|
26620
|
+
});
|
|
26621
|
+
} catch (err) {
|
|
26622
|
+
if (isFetchTimeout(err)) {
|
|
26623
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26624
|
+
}
|
|
26625
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26487
26626
|
}
|
|
26488
|
-
|
|
26489
|
-
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
|
|
26493
|
-
|
|
26627
|
+
const text = await response.text();
|
|
26628
|
+
if (debug) {
|
|
26629
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26630
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26631
|
+
}
|
|
26632
|
+
return { response, text };
|
|
26633
|
+
};
|
|
26634
|
+
let { response, text } = await send(body);
|
|
26635
|
+
if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
|
|
26636
|
+
({ response, text } = await send(params.fallbackBody));
|
|
26494
26637
|
}
|
|
26495
26638
|
if (!response.ok) {
|
|
26496
26639
|
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26497
26640
|
}
|
|
26498
26641
|
try {
|
|
26499
|
-
return JSON.parse(text);
|
|
26642
|
+
return { data: JSON.parse(text), status: response.status };
|
|
26500
26643
|
} catch {
|
|
26501
26644
|
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26502
26645
|
}
|
|
26503
26646
|
}
|
|
26504
|
-
|
|
26647
|
+
function joeAsyncWaitMs(handle) {
|
|
26648
|
+
const expires = positiveSeconds(handle.expires_in_s);
|
|
26649
|
+
return Math.min(expires ? expires * 1000 : JOE_ASYNC_FALLBACK_WAIT_MS, JOE_ASYNC_MAX_WAIT_MS);
|
|
26650
|
+
}
|
|
26651
|
+
function asyncHandleCommandId(reply, operation) {
|
|
26652
|
+
const commandId = reply?.command_id;
|
|
26653
|
+
if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
|
|
26654
|
+
throw new Error(`${operation}: the platform accepted the command over the job channel but ` + `returned no command id: ${redactSecretsForLog(JSON.stringify(reply))}`);
|
|
26655
|
+
}
|
|
26656
|
+
return commandId;
|
|
26657
|
+
}
|
|
26658
|
+
async function startCommandDetailed(params) {
|
|
26505
26659
|
const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
|
|
26506
26660
|
if (!String(command ?? "").trim()) {
|
|
26507
26661
|
throw new Error("command text is required");
|
|
26508
26662
|
}
|
|
26509
|
-
const
|
|
26663
|
+
const operation = "Failed to run Joe command";
|
|
26664
|
+
const body = { instance_id: instanceId, command };
|
|
26665
|
+
const { data, status } = await callRpcRaw({
|
|
26510
26666
|
apiKey,
|
|
26511
26667
|
apiBaseUrl,
|
|
26512
26668
|
fn: "joe_command_run",
|
|
26513
|
-
body: {
|
|
26514
|
-
|
|
26669
|
+
body: { ...body, accept_async: true },
|
|
26670
|
+
fallbackBody: body,
|
|
26671
|
+
operation,
|
|
26515
26672
|
debug,
|
|
26516
26673
|
timeoutMs
|
|
26517
26674
|
});
|
|
26518
|
-
if (
|
|
26519
|
-
|
|
26675
|
+
if (status === 202) {
|
|
26676
|
+
const handle = data ?? {};
|
|
26677
|
+
return {
|
|
26678
|
+
commandId: asyncHandleCommandId(data, operation),
|
|
26679
|
+
asyncWaitMs: joeAsyncWaitMs(handle),
|
|
26680
|
+
firstAnswerEstimateS: positiveSeconds(handle.first_answer_estimate_s)
|
|
26681
|
+
};
|
|
26520
26682
|
}
|
|
26521
|
-
|
|
26683
|
+
if (typeof data !== "string" || !/^[0-9]+$/.test(data)) {
|
|
26684
|
+
throw new Error(`${operation}: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(data))}`);
|
|
26685
|
+
}
|
|
26686
|
+
return { commandId: data, asyncWaitMs: null, firstAnswerEstimateS: null };
|
|
26522
26687
|
}
|
|
26523
26688
|
async function getCommandOutput(params) {
|
|
26524
26689
|
const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
|
|
@@ -26549,7 +26714,10 @@ function normalizeProjectRow(row) {
|
|
|
26549
26714
|
joe_ready: Boolean(row.joe_ready ?? false),
|
|
26550
26715
|
tunnel: Boolean(row.tunnel ?? false),
|
|
26551
26716
|
instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
|
|
26552
|
-
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
|
|
26717
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id),
|
|
26718
|
+
...row.monitoring_instance_ids !== undefined && {
|
|
26719
|
+
monitoring_instance_ids: row.monitoring_instance_ids ?? []
|
|
26720
|
+
}
|
|
26553
26721
|
};
|
|
26554
26722
|
}
|
|
26555
26723
|
async function listProjects(params) {
|
|
@@ -26645,18 +26813,29 @@ function buildJoeCommandText(command, input = {}) {
|
|
|
26645
26813
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26646
26814
|
async function runCommand(params) {
|
|
26647
26815
|
const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
|
|
26648
|
-
const
|
|
26649
|
-
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
26816
|
+
const explicitBudgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : null;
|
|
26650
26817
|
const now = params.now ?? Date.now;
|
|
26651
26818
|
const sleep = params.sleep ?? defaultSleep;
|
|
26652
|
-
const
|
|
26819
|
+
const started = await startCommandDetailed({ apiKey, apiBaseUrl, instanceId, command, debug });
|
|
26820
|
+
const commandId = started.commandId;
|
|
26821
|
+
const budgetMs = explicitBudgetMs ?? started.asyncWaitMs ?? DEFAULT_BUDGET_MS;
|
|
26822
|
+
const pollDelayMs = (attempt) => params.pollIntervalMs ?? (started.asyncWaitMs === null ? DEFAULT_POLL_INTERVAL_MS : asyncPollDelayMs(attempt));
|
|
26653
26823
|
const deadline = now() + budgetMs;
|
|
26654
26824
|
let status = "pending";
|
|
26655
26825
|
const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
|
|
26656
26826
|
if (now() >= deadline) {
|
|
26657
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
26827
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
26658
26828
|
}
|
|
26659
|
-
|
|
26829
|
+
let announced = false;
|
|
26830
|
+
const announce = () => {
|
|
26831
|
+
if (announced || started.asyncWaitMs === null)
|
|
26832
|
+
return;
|
|
26833
|
+
announced = true;
|
|
26834
|
+
const estimate = started.firstAnswerEstimateS;
|
|
26835
|
+
console.error(`Queued on the Joe job channel (command ${commandId}` + (estimate ? `, reaches Joe in about ${estimate}s` : "") + `); waiting up to ${Math.round(budgetMs / 1000)}s...`);
|
|
26836
|
+
};
|
|
26837
|
+
let transientFailures = 0;
|
|
26838
|
+
for (let attempt = 0;; attempt++) {
|
|
26660
26839
|
let output;
|
|
26661
26840
|
try {
|
|
26662
26841
|
output = await getCommandOutput({
|
|
@@ -26668,25 +26847,28 @@ async function runCommand(params) {
|
|
|
26668
26847
|
});
|
|
26669
26848
|
} catch (err) {
|
|
26670
26849
|
if (err instanceof HttpRequestTimeoutError) {
|
|
26671
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
26850
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
26672
26851
|
}
|
|
26673
26852
|
if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
|
|
26674
|
-
|
|
26675
|
-
|
|
26853
|
+
transientFailures += 1;
|
|
26854
|
+
if (now() >= deadline || started.asyncWaitMs !== null && transientFailures >= JOE_ASYNC_MAX_POLL_FAILURES) {
|
|
26855
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
26676
26856
|
}
|
|
26677
|
-
await sleep(
|
|
26857
|
+
await sleep(pollDelayMs(attempt));
|
|
26678
26858
|
continue;
|
|
26679
26859
|
}
|
|
26680
26860
|
throw err;
|
|
26681
26861
|
}
|
|
26862
|
+
transientFailures = 0;
|
|
26682
26863
|
status = output.status;
|
|
26683
26864
|
if (status === "ok" || status === "error") {
|
|
26684
|
-
return { commandId, status, output, budgetExpired: false };
|
|
26865
|
+
return { commandId, status, output, budgetExpired: false, budgetMs };
|
|
26685
26866
|
}
|
|
26686
26867
|
if (now() >= deadline) {
|
|
26687
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
26868
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
26688
26869
|
}
|
|
26689
|
-
|
|
26870
|
+
announce();
|
|
26871
|
+
await sleep(pollDelayMs(attempt));
|
|
26690
26872
|
}
|
|
26691
26873
|
}
|
|
26692
26874
|
async function executeJoeCommand(params) {
|
|
@@ -26797,46 +26979,58 @@ function formatProjectsTable(projects) {
|
|
|
26797
26979
|
}
|
|
26798
26980
|
|
|
26799
26981
|
// lib/joe.ts
|
|
26982
|
+
var JOE_ASYNC_FALLBACK_WAIT_MS2 = 10 * 60 * 1000;
|
|
26983
|
+
var JOE_ASYNC_MAX_WAIT_MS2 = 60 * 60 * 1000;
|
|
26800
26984
|
async function callRpc2(params) {
|
|
26985
|
+
return (await callRpcRaw2(params)).data;
|
|
26986
|
+
}
|
|
26987
|
+
async function callRpcRaw2(params) {
|
|
26801
26988
|
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26802
26989
|
if (!apiKey) {
|
|
26803
26990
|
throw new Error("API key is required");
|
|
26804
26991
|
}
|
|
26805
26992
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26806
26993
|
const url = new URL(`${base}/rpc/${fn}`);
|
|
26807
|
-
const payload = JSON.stringify(body);
|
|
26808
26994
|
const headers = buildAuthHeaders(apiKey);
|
|
26809
|
-
|
|
26810
|
-
const
|
|
26811
|
-
|
|
26812
|
-
|
|
26813
|
-
|
|
26814
|
-
|
|
26815
|
-
|
|
26816
|
-
|
|
26817
|
-
|
|
26818
|
-
|
|
26819
|
-
|
|
26820
|
-
|
|
26821
|
-
|
|
26822
|
-
|
|
26823
|
-
|
|
26824
|
-
|
|
26825
|
-
|
|
26826
|
-
|
|
26995
|
+
const send = async (bodyObj) => {
|
|
26996
|
+
const payload = JSON.stringify(bodyObj);
|
|
26997
|
+
if (debug) {
|
|
26998
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26999
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27000
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27001
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
27002
|
+
}
|
|
27003
|
+
let response;
|
|
27004
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
27005
|
+
try {
|
|
27006
|
+
response = await fetch(url.toString(), {
|
|
27007
|
+
method: "POST",
|
|
27008
|
+
headers,
|
|
27009
|
+
body: payload,
|
|
27010
|
+
signal: requestTimeout.signal
|
|
27011
|
+
});
|
|
27012
|
+
} catch (err) {
|
|
27013
|
+
if (isFetchTimeout(err)) {
|
|
27014
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
27015
|
+
}
|
|
27016
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26827
27017
|
}
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
|
|
26832
|
-
|
|
26833
|
-
|
|
27018
|
+
const text = await response.text();
|
|
27019
|
+
if (debug) {
|
|
27020
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27021
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
27022
|
+
}
|
|
27023
|
+
return { response, text };
|
|
27024
|
+
};
|
|
27025
|
+
let { response, text } = await send(body);
|
|
27026
|
+
if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
|
|
27027
|
+
({ response, text } = await send(params.fallbackBody));
|
|
26834
27028
|
}
|
|
26835
27029
|
if (!response.ok) {
|
|
26836
27030
|
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26837
27031
|
}
|
|
26838
27032
|
try {
|
|
26839
|
-
return JSON.parse(text);
|
|
27033
|
+
return { data: JSON.parse(text), status: response.status };
|
|
26840
27034
|
} catch {
|
|
26841
27035
|
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26842
27036
|
}
|
|
@@ -26855,7 +27049,10 @@ function normalizeProjectRow2(row) {
|
|
|
26855
27049
|
joe_ready: Boolean(row.joe_ready ?? false),
|
|
26856
27050
|
tunnel: Boolean(row.tunnel ?? false),
|
|
26857
27051
|
instance_id: row.instance_id == null ? null : preserveIntegerId2(row.instance_id),
|
|
26858
|
-
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id)
|
|
27052
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id),
|
|
27053
|
+
...row.monitoring_instance_ids !== undefined && {
|
|
27054
|
+
monitoring_instance_ids: row.monitoring_instance_ids ?? []
|
|
27055
|
+
}
|
|
26859
27056
|
};
|
|
26860
27057
|
}
|
|
26861
27058
|
async function listProjects2(params) {
|
|
@@ -26914,24 +27111,10 @@ async function resolveDblabInstanceId(params) {
|
|
|
26914
27111
|
}
|
|
26915
27112
|
return String(match.dblab_instance_id);
|
|
26916
27113
|
}
|
|
26917
|
-
async function
|
|
26918
|
-
const { apiKey, apiBaseUrl,
|
|
26919
|
-
if (!apiKey) {
|
|
26920
|
-
throw new Error("API key is required");
|
|
26921
|
-
}
|
|
26922
|
-
if (!instanceId) {
|
|
26923
|
-
throw new Error("instanceId is required");
|
|
26924
|
-
}
|
|
27114
|
+
async function postDblabRpc(rpc, bodyObj, params) {
|
|
27115
|
+
const { apiKey, apiBaseUrl, operation, debug } = params;
|
|
26925
27116
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26926
|
-
const url = new URL(`${base}/rpc
|
|
26927
|
-
const bodyObj = {
|
|
26928
|
-
instance_id: instanceId,
|
|
26929
|
-
action,
|
|
26930
|
-
method
|
|
26931
|
-
};
|
|
26932
|
-
if (data !== undefined) {
|
|
26933
|
-
bodyObj.data = data;
|
|
26934
|
-
}
|
|
27117
|
+
const url = new URL(`${base}/rpc/${rpc}`);
|
|
26935
27118
|
const body = JSON.stringify(bodyObj);
|
|
26936
27119
|
const headers = buildAuthHeaders(apiKey);
|
|
26937
27120
|
if (debug) {
|
|
@@ -26955,13 +27138,163 @@ async function callDblabApi(params) {
|
|
|
26955
27138
|
}
|
|
26956
27139
|
throw new Error(describeFetchError(operation, base, err));
|
|
26957
27140
|
}
|
|
26958
|
-
|
|
27141
|
+
let text;
|
|
27142
|
+
try {
|
|
27143
|
+
text = await response.text();
|
|
27144
|
+
} catch (err) {
|
|
27145
|
+
if (isFetchTimeout(err)) {
|
|
27146
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
27147
|
+
}
|
|
27148
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
27149
|
+
}
|
|
26959
27150
|
if (debug) {
|
|
26960
27151
|
console.error(`Debug: Response status: ${response.status}`);
|
|
26961
27152
|
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26962
27153
|
}
|
|
27154
|
+
return { response, text };
|
|
27155
|
+
}
|
|
27156
|
+
var DBLAB_ASYNC_FALLBACK_WAIT_MS = 15 * 60 * 1000;
|
|
27157
|
+
var DBLAB_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
|
|
27158
|
+
var DBLAB_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
27159
|
+
var DBLAB_JOB_PENDING = new Set(["queued", "running"]);
|
|
27160
|
+
function dblabMayHaveLandedAdvice() {
|
|
27161
|
+
return "The platform may have accepted this call even though the reply was lost — " + "check the instance's current state before running this command again.";
|
|
27162
|
+
}
|
|
27163
|
+
function dblabNotResentAdvice(jobId) {
|
|
27164
|
+
return `The call (job ${jobId}) was NOT re-sent and may still complete on the instance — ` + "check its current state (e.g. `pgai dblab clone list`) before running this command again.";
|
|
27165
|
+
}
|
|
27166
|
+
function stripControlsKeepingLines(text) {
|
|
27167
|
+
return text.split(`
|
|
27168
|
+
`).map(stripControlsToSpace).join(`
|
|
27169
|
+
`);
|
|
27170
|
+
}
|
|
27171
|
+
function scrubBoxText(text) {
|
|
27172
|
+
return stripControlsToSpace(redactTextSecrets(redactSecretsForLog(text))).replace(/\s+/g, " ").trim();
|
|
27173
|
+
}
|
|
27174
|
+
async function pollDblabCallOnce(jobIdRaw, jobId, params) {
|
|
27175
|
+
const { operation } = params;
|
|
27176
|
+
const advice = dblabNotResentAdvice(jobId);
|
|
27177
|
+
let response;
|
|
27178
|
+
let text;
|
|
27179
|
+
try {
|
|
27180
|
+
({ response, text } = await postDblabRpc("dblab_call_result", { p_job_id: jobIdRaw }, params));
|
|
27181
|
+
} catch (err) {
|
|
27182
|
+
return { kind: "transient", why: err instanceof Error ? err.message : String(err) };
|
|
27183
|
+
}
|
|
26963
27184
|
if (!response.ok) {
|
|
26964
|
-
|
|
27185
|
+
if (isRetryableHttpStatus(response.status)) {
|
|
27186
|
+
return { kind: "transient", why: `HTTP ${response.status}` };
|
|
27187
|
+
}
|
|
27188
|
+
throw new HttpStatusError(`${stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText))}
|
|
27189
|
+
${advice}`, response.status);
|
|
27190
|
+
}
|
|
27191
|
+
let res;
|
|
27192
|
+
try {
|
|
27193
|
+
res = JSON.parse(text);
|
|
27194
|
+
} catch {
|
|
27195
|
+
throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
|
|
27196
|
+
}
|
|
27197
|
+
if (!res || typeof res.status !== "string") {
|
|
27198
|
+
throw new Error(`${operation}: dblab_call_result returned an unexpected reply: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
|
|
27199
|
+
}
|
|
27200
|
+
return { kind: "result", res };
|
|
27201
|
+
}
|
|
27202
|
+
async function awaitDblabCall(handle, params) {
|
|
27203
|
+
const { operation } = params;
|
|
27204
|
+
const jobIdRaw = typeof handle.job_id === "string" ? handle.job_id : "";
|
|
27205
|
+
const jobId = jobIdRaw ? scrubBoxText(jobIdRaw) : "";
|
|
27206
|
+
if (!jobId) {
|
|
27207
|
+
throw new Error(`${operation}: the platform accepted the call but returned no job_id. ` + dblabMayHaveLandedAdvice());
|
|
27208
|
+
}
|
|
27209
|
+
const now = params.now ?? (() => Date.now());
|
|
27210
|
+
const sleep = params.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
27211
|
+
const waitMs = Math.min((positiveSeconds(handle.expires_in_s) ?? 0) * 1000 || DBLAB_ASYNC_FALLBACK_WAIT_MS, DBLAB_ASYNC_MAX_WAIT_MS);
|
|
27212
|
+
const deadline = now() + waitMs;
|
|
27213
|
+
const estimate = positiveSeconds(handle.first_answer_estimate_s);
|
|
27214
|
+
let announced = false;
|
|
27215
|
+
let failures = 0;
|
|
27216
|
+
let lastStatus = "unknown";
|
|
27217
|
+
for (let attempt = 0;; attempt++) {
|
|
27218
|
+
const outcome = await pollDblabCallOnce(jobIdRaw, jobId, params);
|
|
27219
|
+
if (outcome.kind === "transient") {
|
|
27220
|
+
failures += 1;
|
|
27221
|
+
if (failures >= DBLAB_POLL_MAX_CONSECUTIVE_FAILURES) {
|
|
27222
|
+
throw new Error(`${operation}: lost contact with the platform while waiting (${failures} polls in a row failed, ` + `last: ${outcome.why}). ${dblabNotResentAdvice(jobId)}`);
|
|
27223
|
+
}
|
|
27224
|
+
} else {
|
|
27225
|
+
failures = 0;
|
|
27226
|
+
const res = outcome.res;
|
|
27227
|
+
lastStatus = scrubBoxText(res.status);
|
|
27228
|
+
if (res.status === "done" && res.outcome === "ok") {
|
|
27229
|
+
return res.result ?? null;
|
|
27230
|
+
}
|
|
27231
|
+
if (!DBLAB_JOB_PENDING.has(res.status)) {
|
|
27232
|
+
const why = typeof res.error === "string" && res.error ? res.error : res.status === "expired" ? "no agent claimed it before it expired" : `outcome ${String(res.outcome ?? "unknown")}`;
|
|
27233
|
+
const klass = typeof res.failure_class === "string" && res.failure_class ? ` [${scrubBoxText(res.failure_class)}]` : "";
|
|
27234
|
+
const advice = res.status === "expired" ? "" : ` ${dblabNotResentAdvice(jobId)}`;
|
|
27235
|
+
throw new Error(`${operation}: the DBLab instance did not complete the call (status ${scrubBoxText(res.status)}) — ` + `${scrubBoxText(why)}${klass}.${advice}`);
|
|
27236
|
+
}
|
|
27237
|
+
}
|
|
27238
|
+
if (!announced && outcome.kind === "result") {
|
|
27239
|
+
announced = true;
|
|
27240
|
+
console.error(`Waiting for the DBLab instance to pick up the call (job ${jobId}` + (estimate ? `, typically ~${estimate}s` : "") + ")...");
|
|
27241
|
+
}
|
|
27242
|
+
const delay = asyncPollDelayMs(attempt);
|
|
27243
|
+
if (now() + delay >= deadline) {
|
|
27244
|
+
throw new Error(`${operation}: timed out waiting for the DBLab instance (job ${jobId}, last status ${lastStatus}). ` + dblabNotResentAdvice(jobId));
|
|
27245
|
+
}
|
|
27246
|
+
await sleep(delay);
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
27249
|
+
async function enqueueDblabCall(bodyObj, params, isWrite) {
|
|
27250
|
+
try {
|
|
27251
|
+
return await postDblabRpc("dblab_api_call", bodyObj, params);
|
|
27252
|
+
} catch (err) {
|
|
27253
|
+
if (!isWrite)
|
|
27254
|
+
throw err;
|
|
27255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27256
|
+
throw new Error(`${message}
|
|
27257
|
+
${dblabMayHaveLandedAdvice()}`, { cause: err });
|
|
27258
|
+
}
|
|
27259
|
+
}
|
|
27260
|
+
async function callDblabApi(params) {
|
|
27261
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
27262
|
+
if (!apiKey) {
|
|
27263
|
+
throw new Error("API key is required");
|
|
27264
|
+
}
|
|
27265
|
+
if (!instanceId) {
|
|
27266
|
+
throw new Error("instanceId is required");
|
|
27267
|
+
}
|
|
27268
|
+
const bodyObj = {
|
|
27269
|
+
instance_id: instanceId,
|
|
27270
|
+
action,
|
|
27271
|
+
method
|
|
27272
|
+
};
|
|
27273
|
+
if (data !== undefined) {
|
|
27274
|
+
bodyObj.data = data;
|
|
27275
|
+
}
|
|
27276
|
+
const rpcParams = { apiKey, apiBaseUrl, operation, debug };
|
|
27277
|
+
const isWrite = method.toLowerCase() !== "get";
|
|
27278
|
+
let { response, text } = await enqueueDblabCall({ ...bodyObj, accept_async: true }, rpcParams, isWrite);
|
|
27279
|
+
if (isUnknownRpcSignature(response.status, text, "dblab_api_call")) {
|
|
27280
|
+
({ response, text } = await enqueueDblabCall(bodyObj, rpcParams, isWrite));
|
|
27281
|
+
}
|
|
27282
|
+
if (response.status === 202) {
|
|
27283
|
+
let handle;
|
|
27284
|
+
try {
|
|
27285
|
+
handle = JSON.parse(text);
|
|
27286
|
+
} catch {
|
|
27287
|
+
throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ` + dblabMayHaveLandedAdvice());
|
|
27288
|
+
}
|
|
27289
|
+
if (!handle || typeof handle !== "object") {
|
|
27290
|
+
throw new Error(`${operation}: the platform accepted the call but returned no handle. ` + dblabMayHaveLandedAdvice());
|
|
27291
|
+
}
|
|
27292
|
+
return await awaitDblabCall(handle, rpcParams);
|
|
27293
|
+
}
|
|
27294
|
+
if (!response.ok) {
|
|
27295
|
+
const lost = isWrite && response.status >= 500;
|
|
27296
|
+
throw new HttpStatusError(stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText)) + (lost ? `
|
|
27297
|
+
${dblabMayHaveLandedAdvice()}` : ""), response.status);
|
|
26965
27298
|
}
|
|
26966
27299
|
if (text.trim() === "") {
|
|
26967
27300
|
return null;
|
|
@@ -26969,7 +27302,7 @@ async function callDblabApi(params) {
|
|
|
26969
27302
|
try {
|
|
26970
27303
|
return JSON.parse(text);
|
|
26971
27304
|
} catch {
|
|
26972
|
-
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
27305
|
+
throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}`);
|
|
26973
27306
|
}
|
|
26974
27307
|
}
|
|
26975
27308
|
async function createClone(params) {
|
|
@@ -27206,6 +27539,37 @@ async function callRpc3(name, apiKey, body, opts) {
|
|
|
27206
27539
|
throw new Error(`${name} returned a body that is not JSON`);
|
|
27207
27540
|
}
|
|
27208
27541
|
}
|
|
27542
|
+
async function resolveMonitoringInstanceId(params) {
|
|
27543
|
+
const ref = String(params.project ?? "").trim();
|
|
27544
|
+
if (!ref) {
|
|
27545
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27546
|
+
}
|
|
27547
|
+
const projects = await listProjects2({
|
|
27548
|
+
apiKey: params.apiKey,
|
|
27549
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27550
|
+
orgId: params.orgId
|
|
27551
|
+
});
|
|
27552
|
+
const needle = ref.toLowerCase();
|
|
27553
|
+
const matches = isNumericProjectRef2(ref) ? projects.filter((p) => String(p.project_id) === BigInt(ref).toString()) : projects.filter((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
27554
|
+
if (matches.length === 0) {
|
|
27555
|
+
throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27556
|
+
}
|
|
27557
|
+
if (matches.length > 1) {
|
|
27558
|
+
throw new Error(`'${ref}' matches ${matches.length} projects (ids ${matches.map((p) => p.project_id).join(", ")}). ` + "Pass --project <id> instead.");
|
|
27559
|
+
}
|
|
27560
|
+
const match = matches[0];
|
|
27561
|
+
const ids = match.monitoring_instance_ids;
|
|
27562
|
+
if (ids === undefined) {
|
|
27563
|
+
throw new Error("This platform does not report monitoring instances per project yet. Pass --instance <uuid> instead.");
|
|
27564
|
+
}
|
|
27565
|
+
if (ids.length === 0) {
|
|
27566
|
+
throw new Error(`Project '${ref}' has no active monitoring instance.`);
|
|
27567
|
+
}
|
|
27568
|
+
if (ids.length > 1) {
|
|
27569
|
+
throw new Error(`Project '${ref}' has ${ids.length} active monitoring instances: ${ids.join(", ")}. Pass --instance <uuid>.`);
|
|
27570
|
+
}
|
|
27571
|
+
return ids[0];
|
|
27572
|
+
}
|
|
27209
27573
|
async function enqueueQuery(apiKey, args, opts) {
|
|
27210
27574
|
const body = {
|
|
27211
27575
|
p_instance_id: args.instanceId,
|
|
@@ -27224,9 +27588,9 @@ async function enqueueQuery(apiKey, args, opts) {
|
|
|
27224
27588
|
if (!jobId) {
|
|
27225
27589
|
throw new Error("instance_query_enqueue returned no job_id");
|
|
27226
27590
|
}
|
|
27227
|
-
return { jobId, firstAnswerEstimateS:
|
|
27591
|
+
return { jobId, firstAnswerEstimateS: positiveSeconds2(reply?.first_answer_estimate_s) };
|
|
27228
27592
|
}
|
|
27229
|
-
function
|
|
27593
|
+
function positiveSeconds2(v) {
|
|
27230
27594
|
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
27231
27595
|
}
|
|
27232
27596
|
async function readQueryResult(apiKey, jobId, opts) {
|
|
@@ -30566,6 +30930,38 @@ async function resolveDatasourceId(adminPassword, debug = false) {
|
|
|
30566
30930
|
return null;
|
|
30567
30931
|
}
|
|
30568
30932
|
}
|
|
30933
|
+
function aasSuccessMessage(r) {
|
|
30934
|
+
const armed = r.armedFor === "pull" ? " (armed for pull: the platform will query this instance's Grafana)" : r.armedFor === "jobs" ? " (armed for the job channel: this instance answers its own queries)" : "";
|
|
30935
|
+
const base = `AAS auto-collection registered${armed}`;
|
|
30936
|
+
if (r.vcpus === 0) {
|
|
30937
|
+
return `${base} — collection stays OFF until a source-DB vCPU count is known (the platform stamps it at provision time; --vcpus <n> sets it for a manual install)`;
|
|
30938
|
+
}
|
|
30939
|
+
if (r.urlPending) {
|
|
30940
|
+
return `${base} — collection starts once this deploy reports the Grafana url to the platform`;
|
|
30941
|
+
}
|
|
30942
|
+
return base;
|
|
30943
|
+
}
|
|
30944
|
+
var PLATFORM_ERROR_MAX = 300;
|
|
30945
|
+
function formatPlatformError(body, secrets = []) {
|
|
30946
|
+
if (!body || typeof body !== "object")
|
|
30947
|
+
return "";
|
|
30948
|
+
const b = body;
|
|
30949
|
+
const str = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : "";
|
|
30950
|
+
const text = str(b.details) || str(b.message);
|
|
30951
|
+
if (!text)
|
|
30952
|
+
return "";
|
|
30953
|
+
const code = str(b.code);
|
|
30954
|
+
let out = stripControlsToSpace(code ? `${code}: ${text}` : text).replace(/\s+/g, " ").trim();
|
|
30955
|
+
for (const secret of secrets) {
|
|
30956
|
+
const compact = secret.replace(/\s+/g, "");
|
|
30957
|
+
if (compact.length < 8)
|
|
30958
|
+
continue;
|
|
30959
|
+
out = out.replace(whitespaceTolerantSecretPattern(compact), "[redacted]");
|
|
30960
|
+
}
|
|
30961
|
+
out = redactTextSecrets(out);
|
|
30962
|
+
out = out.replace(/glsa_[A-Za-z0-9_-]+/g, "[redacted]");
|
|
30963
|
+
return out.length > PLATFORM_ERROR_MAX ? `${out.slice(0, PLATFORM_ERROR_MAX - 1).trimEnd()}…` : out;
|
|
30964
|
+
}
|
|
30569
30965
|
async function registerAasCollection(apiKey, instanceId, opts) {
|
|
30570
30966
|
const debug = !!opts.debug;
|
|
30571
30967
|
try {
|
|
@@ -30615,11 +31011,19 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
30615
31011
|
})
|
|
30616
31012
|
});
|
|
30617
31013
|
if (!res.ok) {
|
|
31014
|
+
const detail = formatPlatformError(await res.json().catch(() => null), [apiKey, saToken]);
|
|
31015
|
+
const suffix = detail ? ` — ${detail}` : "";
|
|
30618
31016
|
if (debug)
|
|
30619
|
-
console.error(`Debug: AAS register failed: HTTP ${res.status}`);
|
|
30620
|
-
return { ok: false, reason: `platform returned HTTP ${res.status}` };
|
|
31017
|
+
console.error(`Debug: AAS register failed: HTTP ${res.status}${suffix}`);
|
|
31018
|
+
return { ok: false, reason: `platform returned HTTP ${res.status}${suffix}` };
|
|
30621
31019
|
}
|
|
30622
|
-
|
|
31020
|
+
const reply = await res.json().catch(() => null);
|
|
31021
|
+
return {
|
|
31022
|
+
ok: true,
|
|
31023
|
+
vcpus: typeof reply?.vcpus === "number" ? reply.vcpus : undefined,
|
|
31024
|
+
urlPending: typeof reply?.url_pending === "boolean" ? reply.url_pending : undefined,
|
|
31025
|
+
armedFor: typeof reply?.armed_for === "string" ? reply.armed_for : undefined
|
|
31026
|
+
};
|
|
30623
31027
|
} catch (err) {
|
|
30624
31028
|
if (debug)
|
|
30625
31029
|
console.error(`Debug: AAS register error: ${err.message}`);
|
|
@@ -38950,8 +39354,41 @@ var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
|
|
|
38950
39354
|
"VM_FORCE_MERGE_AUTH_KEY",
|
|
38951
39355
|
"VM_PPROF_AUTH_KEY",
|
|
38952
39356
|
"PGAI_INSTANCE_ID",
|
|
38953
|
-
"INSTANCE_JOBS_USER"
|
|
39357
|
+
"INSTANCE_JOBS_USER",
|
|
39358
|
+
"COMPOSE_PROFILES"
|
|
38954
39359
|
];
|
|
39360
|
+
var INSTANCE_JOBS_PROFILE = "instance-jobs";
|
|
39361
|
+
function composeProfilesValue(existing, want) {
|
|
39362
|
+
const profiles = [];
|
|
39363
|
+
for (const raw of (existing ?? "").split(",")) {
|
|
39364
|
+
const token = raw.trim();
|
|
39365
|
+
if (token && !profiles.includes(token))
|
|
39366
|
+
profiles.push(token);
|
|
39367
|
+
}
|
|
39368
|
+
const at = profiles.indexOf(INSTANCE_JOBS_PROFILE);
|
|
39369
|
+
if (want === true && at === -1)
|
|
39370
|
+
profiles.push(INSTANCE_JOBS_PROFILE);
|
|
39371
|
+
if (want === false && at !== -1)
|
|
39372
|
+
profiles.splice(at, 1);
|
|
39373
|
+
return profiles.length > 0 ? profiles.join(",") : null;
|
|
39374
|
+
}
|
|
39375
|
+
function instanceJobsProfileEnabled(composeProfiles) {
|
|
39376
|
+
return (composeProfiles ?? "").split(",").map((p) => p.trim()).includes(INSTANCE_JOBS_PROFILE);
|
|
39377
|
+
}
|
|
39378
|
+
function resolveInstanceJobsRequest(flag, envValue) {
|
|
39379
|
+
if (flag !== undefined)
|
|
39380
|
+
return { enabled: flag };
|
|
39381
|
+
const value = (envValue ?? "").trim().toLowerCase();
|
|
39382
|
+
if (value === "")
|
|
39383
|
+
return {};
|
|
39384
|
+
if (["1", "true", "yes", "on"].includes(value))
|
|
39385
|
+
return { enabled: true };
|
|
39386
|
+
if (["0", "false", "no", "off"].includes(value))
|
|
39387
|
+
return { enabled: false };
|
|
39388
|
+
return {
|
|
39389
|
+
error: `PGAI_INSTANCE_JOBS=${envValue} is not a boolean ` + "(use true/false, 1/0, yes/no, on/off, or leave it empty to keep the current setting)"
|
|
39390
|
+
};
|
|
39391
|
+
}
|
|
38955
39392
|
function statOwner(file) {
|
|
38956
39393
|
if (!file)
|
|
38957
39394
|
return;
|
|
@@ -38970,7 +39407,7 @@ function processOwner() {
|
|
|
38970
39407
|
return null;
|
|
38971
39408
|
return `${process.getuid()}:${process.getgid()}`;
|
|
38972
39409
|
}
|
|
38973
|
-
function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile) {
|
|
39410
|
+
function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile, instanceJobs) {
|
|
38974
39411
|
const readRaw = (key) => {
|
|
38975
39412
|
const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
|
|
38976
39413
|
let value = null;
|
|
@@ -39002,6 +39439,10 @@ function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile)
|
|
|
39002
39439
|
const credentialOwner = owner === undefined ? processOwner() : owner;
|
|
39003
39440
|
if (credentialOwner)
|
|
39004
39441
|
envLines.push(`INSTANCE_JOBS_USER=${credentialOwner}`);
|
|
39442
|
+
const existingProfiles = readRaw("COMPOSE_PROFILES");
|
|
39443
|
+
const composeProfiles = composeProfilesValue(existingProfiles === null ? null : stripMatchingQuotes(existingProfiles), instanceJobs);
|
|
39444
|
+
if (composeProfiles)
|
|
39445
|
+
envLines.push(`COMPOSE_PROFILES=${composeProfiles}`);
|
|
39005
39446
|
for (const key of VM_ADMIN_AUTH_KEYS) {
|
|
39006
39447
|
const existing = readRaw(key);
|
|
39007
39448
|
envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
|
|
@@ -41053,7 +41494,7 @@ async function applyMonitoringTargetsConfig() {
|
|
|
41053
41494
|
console.log("Restarting pgwatch collectors to pick up target changes...");
|
|
41054
41495
|
return runCompose(["up", "-d", "--force-recreate", "pgwatch-prometheus", "pgwatch-postgres"]);
|
|
41055
41496
|
}
|
|
41056
|
-
async function runCompose(args, grafanaPassword) {
|
|
41497
|
+
async function runCompose(args, grafanaPassword, extraEnv) {
|
|
41057
41498
|
let composeFile;
|
|
41058
41499
|
let projectDir;
|
|
41059
41500
|
try {
|
|
@@ -41117,6 +41558,8 @@ async function runCompose(args, grafanaPassword) {
|
|
|
41117
41558
|
}
|
|
41118
41559
|
}
|
|
41119
41560
|
}
|
|
41561
|
+
if (extraEnv)
|
|
41562
|
+
Object.assign(env, extraEnv);
|
|
41120
41563
|
const finalArgs = [...args];
|
|
41121
41564
|
if (process.platform === "darwin" && shouldScaleOutNodeExporter(args)) {
|
|
41122
41565
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
@@ -41175,6 +41618,127 @@ Applying sink-prometheus config (VictoriaMetrics admin keys, #359)...`);
|
|
|
41175
41618
|
console.log("\u2713 sink-prometheus is running the current config");
|
|
41176
41619
|
return true;
|
|
41177
41620
|
}
|
|
41621
|
+
function readComposeProfiles(knownProjectDir) {
|
|
41622
|
+
const fromProcess = process.env.COMPOSE_PROFILES;
|
|
41623
|
+
if (fromProcess !== undefined && fromProcess.trim() !== "")
|
|
41624
|
+
return fromProcess;
|
|
41625
|
+
let projectDir;
|
|
41626
|
+
if (knownProjectDir) {
|
|
41627
|
+
projectDir = knownProjectDir;
|
|
41628
|
+
} else {
|
|
41629
|
+
try {
|
|
41630
|
+
({ projectDir } = resolvePaths());
|
|
41631
|
+
} catch {
|
|
41632
|
+
projectDir = getDefaultMonitoringProjectDir();
|
|
41633
|
+
}
|
|
41634
|
+
}
|
|
41635
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
41636
|
+
let content;
|
|
41637
|
+
try {
|
|
41638
|
+
content = fs9.readFileSync(envFile, "utf8");
|
|
41639
|
+
} catch (err) {
|
|
41640
|
+
if (err?.code !== "ENOENT") {
|
|
41641
|
+
console.error(`\u26A0 Could not read ${envFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
41642
|
+
console.error(" Treating the instance-jobs profile as disabled; if it is in fact enabled,");
|
|
41643
|
+
console.error(" an absent container will be reported as '- not enabled' rather than a fault.");
|
|
41644
|
+
}
|
|
41645
|
+
return null;
|
|
41646
|
+
}
|
|
41647
|
+
let value = null;
|
|
41648
|
+
for (const m of content.matchAll(/^[ \t]*(?:export[ \t]+)?COMPOSE_PROFILES=(.*)$/gm)) {
|
|
41649
|
+
value = stripMatchingQuotes(m[1].trim());
|
|
41650
|
+
}
|
|
41651
|
+
return value;
|
|
41652
|
+
}
|
|
41653
|
+
function instanceJobsRemovalOutcome(before, after, removeExitCode) {
|
|
41654
|
+
if (before === "unknown" || after === "unknown") {
|
|
41655
|
+
return {
|
|
41656
|
+
ok: false,
|
|
41657
|
+
message: `\u2717 Could not determine whether the instance-jobs container is present.
|
|
41658
|
+
` + " The profile is off in .env, but a container left behind keeps polling."
|
|
41659
|
+
};
|
|
41660
|
+
}
|
|
41661
|
+
if (before === "absent") {
|
|
41662
|
+
return { ok: true, message: "\u2713 instance-jobs is disabled (no container was present)" };
|
|
41663
|
+
}
|
|
41664
|
+
if (removeExitCode !== 0 || after !== "absent") {
|
|
41665
|
+
return {
|
|
41666
|
+
ok: false,
|
|
41667
|
+
message: `\u2717 Could not remove the instance-jobs container.
|
|
41668
|
+
` + ` The profile is off in .env, but the container is still there and still polling. To retry:
|
|
41669
|
+
` + ` COMPOSE_PROFILES=${INSTANCE_JOBS_PROFILE} docker compose rm -sf ${INSTANCE_JOBS_PROFILE}`
|
|
41670
|
+
};
|
|
41671
|
+
}
|
|
41672
|
+
return before === "running" ? {
|
|
41673
|
+
ok: true,
|
|
41674
|
+
message: "\u26A0 instance-jobs was RUNNING and has been removed \u2014 outbound collection on this machine is now OFF"
|
|
41675
|
+
} : { ok: true, message: "\u2713 instance-jobs container removed (it was not running)" };
|
|
41676
|
+
}
|
|
41677
|
+
function instanceJobsStartSkipReason(storeRunning) {
|
|
41678
|
+
if (storeRunning === true)
|
|
41679
|
+
return null;
|
|
41680
|
+
return storeRunning === false ? "the stack is not running, so instance-jobs was not started - it comes up with the rest on `mon start`" : "could not tell whether the stack is running, so instance-jobs was not started - run `mon start`, then `mon health`";
|
|
41681
|
+
}
|
|
41682
|
+
async function composeServiceRunning(projectDir, service) {
|
|
41683
|
+
const composeCmd = getComposeCmd();
|
|
41684
|
+
if (!composeCmd)
|
|
41685
|
+
return null;
|
|
41686
|
+
try {
|
|
41687
|
+
const { stdout } = await execFilePromise(composeCmd[0], [...composeCmd.slice(1), "-f", path7.resolve(projectDir, "docker-compose.yml"), "ps", "-q", service], { cwd: projectDir });
|
|
41688
|
+
return stdout.trim().length > 0;
|
|
41689
|
+
} catch {
|
|
41690
|
+
return null;
|
|
41691
|
+
}
|
|
41692
|
+
}
|
|
41693
|
+
async function applyInstanceJobsProfile(projectDir, enabled) {
|
|
41694
|
+
if (enabled === undefined)
|
|
41695
|
+
return true;
|
|
41696
|
+
if (enabled) {
|
|
41697
|
+
const skip = instanceJobsStartSkipReason(await composeServiceRunning(projectDir, "sink-prometheus"));
|
|
41698
|
+
if (skip) {
|
|
41699
|
+
console.log(`
|
|
41700
|
+
(${skip})`);
|
|
41701
|
+
return true;
|
|
41702
|
+
}
|
|
41703
|
+
console.log(`
|
|
41704
|
+
Starting the instance-jobs container (compose profile enabled)...`);
|
|
41705
|
+
const code = await runCompose(["up", "-d", "--no-deps", INSTANCE_JOBS_PROFILE]);
|
|
41706
|
+
if (code !== 0) {
|
|
41707
|
+
console.error("\u2717 Could not start the instance-jobs container.");
|
|
41708
|
+
console.error(" The channel is enabled in .env but nothing is polling. To retry:");
|
|
41709
|
+
console.error(` docker compose up -d --no-deps ${INSTANCE_JOBS_PROFILE}`);
|
|
41710
|
+
return false;
|
|
41711
|
+
}
|
|
41712
|
+
console.log("\u2713 instance-jobs is running");
|
|
41713
|
+
return true;
|
|
41714
|
+
}
|
|
41715
|
+
const before = await inspectInstanceJobsState();
|
|
41716
|
+
if (before === "absent" || before === "unknown") {
|
|
41717
|
+
const verdict = instanceJobsRemovalOutcome(before, before, 0);
|
|
41718
|
+
(verdict.ok ? console.log : console.error)(`
|
|
41719
|
+
${verdict.message}`);
|
|
41720
|
+
return verdict.ok;
|
|
41721
|
+
}
|
|
41722
|
+
const code = await runCompose(["rm", "-sf", INSTANCE_JOBS_PROFILE], undefined, { COMPOSE_PROFILES: INSTANCE_JOBS_PROFILE });
|
|
41723
|
+
const verdict = instanceJobsRemovalOutcome(before, await inspectInstanceJobsState(), code);
|
|
41724
|
+
(verdict.ok ? console.log : console.error)(`
|
|
41725
|
+
${verdict.message}`);
|
|
41726
|
+
return verdict.ok;
|
|
41727
|
+
}
|
|
41728
|
+
async function inspectInstanceJobsState() {
|
|
41729
|
+
try {
|
|
41730
|
+
const { stdout } = await execFilePromise("docker", [
|
|
41731
|
+
"inspect",
|
|
41732
|
+
"-f",
|
|
41733
|
+
"{{.State.Running}}",
|
|
41734
|
+
INSTANCE_JOBS_PROFILE
|
|
41735
|
+
]);
|
|
41736
|
+
return stdout.trim() === "true" ? "running" : "stopped";
|
|
41737
|
+
} catch (error) {
|
|
41738
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
41739
|
+
return /no such (object|container)/i.test(text) ? "absent" : "unknown";
|
|
41740
|
+
}
|
|
41741
|
+
}
|
|
41178
41742
|
program2.command("help").description("show help").action(() => {
|
|
41179
41743
|
program2.outputHelp();
|
|
41180
41744
|
});
|
|
@@ -41188,7 +41752,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
41188
41752
|
" PGAI_ENABLE_IPV6=false (accepted: true|false|yes|no, lowercase)",
|
|
41189
41753
|
""
|
|
41190
41754
|
].join(`
|
|
41191
|
-
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("--instance-id <uuid>", "adopt a console-provisioned monitoring instance instead of self-registering a new one (set automatically by the provisioning flow; PGAI_INSTANCE_ID env also works)").option("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
41755
|
+
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("--instance-id <uuid>", "adopt a console-provisioned monitoring instance instead of self-registering a new one (set automatically by the provisioning flow; PGAI_INSTANCE_ID env also works)").option("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("--instance-jobs", "enable the instance-jobs compose profile (outbound collection channel; PGAI_INSTANCE_JOBS env also works). Writes COMPOSE_PROFILES into the stack .env, so every later mon command covers the container").option("--no-instance-jobs", "disable the instance-jobs compose profile and remove its container").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
41192
41756
|
const globalOpts = program2.opts();
|
|
41193
41757
|
let apiKey = opts.apiKey || globalOpts.apiKey;
|
|
41194
41758
|
console.log(`
|
|
@@ -41210,7 +41774,14 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
41210
41774
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
41211
41775
|
const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
|
|
41212
41776
|
const imageTag = opts.tag || package_default.version;
|
|
41213
|
-
const
|
|
41777
|
+
const instanceJobsRequest = resolveInstanceJobsRequest(opts.instanceJobs, process.env.PGAI_INSTANCE_JOBS);
|
|
41778
|
+
if (instanceJobsRequest.error) {
|
|
41779
|
+
console.error(`\u2717 ${instanceJobsRequest.error}`);
|
|
41780
|
+
process.exitCode = 1;
|
|
41781
|
+
return;
|
|
41782
|
+
}
|
|
41783
|
+
const instanceJobs = instanceJobsRequest.enabled;
|
|
41784
|
+
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"), instanceJobs);
|
|
41214
41785
|
writeEnvFile(envFile, envContent);
|
|
41215
41786
|
if (preservedKeys.length > 0) {
|
|
41216
41787
|
console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
|
|
@@ -41241,6 +41812,8 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
41241
41812
|
console.error(`\u26A0 Monitoring services are already running: ${containers.join(", ")}`);
|
|
41242
41813
|
console.log(`Use 'postgres-ai mon restart' to restart them
|
|
41243
41814
|
`);
|
|
41815
|
+
if (!await applyInstanceJobsProfile(projectDir, instanceJobs))
|
|
41816
|
+
process.exitCode = 1;
|
|
41244
41817
|
return;
|
|
41245
41818
|
}
|
|
41246
41819
|
if (!opts.demo) {
|
|
@@ -41530,6 +42103,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41530
42103
|
}
|
|
41531
42104
|
console.log(`\u2713 Services started
|
|
41532
42105
|
`);
|
|
42106
|
+
const instanceJobsApplied = await applyInstanceJobsProfile(projectDir, instanceJobs);
|
|
42107
|
+
if (!instanceJobsApplied)
|
|
42108
|
+
process.exitCode = 1;
|
|
41533
42109
|
if (apiKey && !opts.demo) {
|
|
41534
42110
|
let regOrgScope;
|
|
41535
42111
|
if (isGlobalTokenValue(apiKey)) {
|
|
@@ -41571,7 +42147,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41571
42147
|
debug: !!process.env.DEBUG
|
|
41572
42148
|
});
|
|
41573
42149
|
if (aas.ok) {
|
|
41574
|
-
console.log(`\u2713
|
|
42150
|
+
console.log(`\u2713 ${aasSuccessMessage(aas)}
|
|
41575
42151
|
`);
|
|
41576
42152
|
} else {
|
|
41577
42153
|
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
@@ -41590,9 +42166,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41590
42166
|
}
|
|
41591
42167
|
}
|
|
41592
42168
|
console.log("=================================");
|
|
41593
|
-
console.log(" Local install completed!");
|
|
42169
|
+
console.log(instanceJobsApplied ? " Local install completed!" : " Local install completed WITH ONE FAILURE");
|
|
41594
42170
|
console.log(`=================================
|
|
41595
42171
|
`);
|
|
42172
|
+
if (!instanceJobsApplied) {
|
|
42173
|
+
console.log(instanceJobs ? `\u2717 The instance-jobs container did not start. Everything below is running; the outbound
|
|
42174
|
+
` + " collection channel is not. See the error above, then `postgres-ai mon health`.\n" : `\u2717 The instance-jobs container could not be removed. Everything below is running, and so
|
|
42175
|
+
` + ` is a container that keeps polling. See the error above.
|
|
42176
|
+
`);
|
|
42177
|
+
}
|
|
41596
42178
|
console.log("What's running:");
|
|
41597
42179
|
if (opts.demo) {
|
|
41598
42180
|
console.log(" \u2705 Demo PostgreSQL database (monitoring target)");
|
|
@@ -41707,7 +42289,7 @@ mon.command("logs [service]").option("-f, --follow", "follow logs", false).optio
|
|
|
41707
42289
|
});
|
|
41708
42290
|
function judgeServiceHealth(service, present, status, health) {
|
|
41709
42291
|
if (!present) {
|
|
41710
|
-
return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: "unreachable" };
|
|
42292
|
+
return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: service.absentLabel ?? "unreachable" };
|
|
41711
42293
|
}
|
|
41712
42294
|
if (status !== "running") {
|
|
41713
42295
|
return { ok: false, mark: "\u2717", label: `unhealthy (status: ${status})` };
|
|
@@ -41717,7 +42299,7 @@ function judgeServiceHealth(service, present, status, health) {
|
|
|
41717
42299
|
}
|
|
41718
42300
|
return { ok: true, mark: "\u2713", label: "healthy" };
|
|
41719
42301
|
}
|
|
41720
|
-
function monitoringHealthServices() {
|
|
42302
|
+
function monitoringHealthServices(instanceJobsEnabled = false) {
|
|
41721
42303
|
return [
|
|
41722
42304
|
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
41723
42305
|
{ name: "Prometheus", container: "sink-prometheus" },
|
|
@@ -41725,11 +42307,17 @@ function monitoringHealthServices() {
|
|
|
41725
42307
|
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
41726
42308
|
{ name: "Target DB", container: "target-db" },
|
|
41727
42309
|
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
41728
|
-
{
|
|
42310
|
+
{
|
|
42311
|
+
name: "Instance jobs",
|
|
42312
|
+
container: "instance-jobs",
|
|
42313
|
+
optional: !instanceJobsEnabled,
|
|
42314
|
+
readHealth: true,
|
|
42315
|
+
absentLabel: "enabled but not running"
|
|
42316
|
+
}
|
|
41729
42317
|
];
|
|
41730
42318
|
}
|
|
41731
42319
|
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();
|
|
42320
|
+
const services = monitoringHealthServices(instanceJobsProfileEnabled(readComposeProfiles()));
|
|
41733
42321
|
const waitTime = opts.wait || 0;
|
|
41734
42322
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
41735
42323
|
console.log(`Checking service health...
|
|
@@ -41863,6 +42451,12 @@ Updating Docker images...`);
|
|
|
41863
42451
|
process.exitCode = 1;
|
|
41864
42452
|
return;
|
|
41865
42453
|
}
|
|
42454
|
+
if (instanceJobsProfileEnabled(readComposeProfiles(projectDir))) {
|
|
42455
|
+
if (!await applyInstanceJobsProfile(projectDir, true)) {
|
|
42456
|
+
process.exitCode = 1;
|
|
42457
|
+
return;
|
|
42458
|
+
}
|
|
42459
|
+
}
|
|
41866
42460
|
console.log(`
|
|
41867
42461
|
\u2713 Update completed successfully`);
|
|
41868
42462
|
console.log(`
|
|
@@ -42564,7 +43158,7 @@ program2.command("orgs").description("list organizations this credential can rea
|
|
|
42564
43158
|
process.exitCode = 1;
|
|
42565
43159
|
}
|
|
42566
43160
|
});
|
|
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) => {
|
|
43161
|
+
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); not with --project").option("--project <id|alias>", "the project's monitoring instance, by project id, alias or name; not with --instance").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
43162
|
try {
|
|
42569
43163
|
const rootOpts = program2.opts();
|
|
42570
43164
|
const cfg = readConfig();
|
|
@@ -42575,9 +43169,19 @@ withOrgOptions(program2.command("promql")).description("run a PromQL query on a
|
|
|
42575
43169
|
return;
|
|
42576
43170
|
}
|
|
42577
43171
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42578
|
-
|
|
43172
|
+
if (opts.instance && opts.project) {
|
|
43173
|
+
console.error("Pass --instance or --project, not both.");
|
|
43174
|
+
process.exitCode = 1;
|
|
43175
|
+
return;
|
|
43176
|
+
}
|
|
43177
|
+
const instanceId = opts.instance || (opts.project ? await resolveMonitoringInstanceId({
|
|
43178
|
+
apiKey,
|
|
43179
|
+
apiBaseUrl,
|
|
43180
|
+
project: opts.project,
|
|
43181
|
+
orgId: configOrgIdForBody(apiKey, cfg.orgId)
|
|
43182
|
+
}) : readInstanceIdFromProject());
|
|
42579
43183
|
if (!instanceId) {
|
|
42580
|
-
console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
|
|
43184
|
+
console.error("No monitoring instance. Pass --project <id|alias> or --instance <uuid>, or run this on a box whose");
|
|
42581
43185
|
console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
|
|
42582
43186
|
process.exitCode = 1;
|
|
42583
43187
|
return;
|
|
@@ -42617,7 +43221,7 @@ withOrgOptions(program2.command("promql")).description("run a PromQL query on a
|
|
|
42617
43221
|
});
|
|
42618
43222
|
if (!isTerminal2(res.status)) {
|
|
42619
43223
|
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 " + "
|
|
43224
|
+
console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running on that instance " + "(`postgresai mon health`; it is behind a compose profile, enabled with " + "`mon local-install --instance-jobs`), 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
43225
|
process.exitCode = 1;
|
|
42622
43226
|
return;
|
|
42623
43227
|
}
|
|
@@ -43429,7 +44033,7 @@ function tryParseJson(s) {
|
|
|
43429
44033
|
}
|
|
43430
44034
|
}
|
|
43431
44035
|
function printJoeOutcome(outcome, json, budgetMs) {
|
|
43432
|
-
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
44036
|
+
const effectiveBudgetMs = Number.isFinite(outcome.budgetMs) ? outcome.budgetMs : typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
43433
44037
|
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
43434
44038
|
if (outcome.budgetExpired) {
|
|
43435
44039
|
if (json) {
|
|
@@ -43927,17 +44531,23 @@ export {
|
|
|
43927
44531
|
applyApiKey,
|
|
43928
44532
|
applyProjectName,
|
|
43929
44533
|
buildLocalInstallEnv,
|
|
44534
|
+
composeProfilesValue,
|
|
43930
44535
|
instanceIdToPersist,
|
|
44536
|
+
instanceJobsProfileEnabled,
|
|
44537
|
+
instanceJobsRemovalOutcome,
|
|
44538
|
+
instanceJobsStartSkipReason,
|
|
43931
44539
|
isValidComposeYaml,
|
|
43932
44540
|
judgeServiceHealth,
|
|
43933
44541
|
monitoringHealthServices,
|
|
43934
44542
|
persistInstanceId,
|
|
43935
44543
|
planMonitoringRegistration,
|
|
43936
44544
|
program2 as program,
|
|
44545
|
+
readComposeProfiles,
|
|
43937
44546
|
readDeployedTag,
|
|
43938
44547
|
refreshBundledComposeIfStale,
|
|
43939
44548
|
registerMonitoringInstance,
|
|
43940
44549
|
resolveAdoptedProject,
|
|
44550
|
+
resolveInstanceJobsRequest,
|
|
43941
44551
|
selfRegisterAndPersist,
|
|
43942
44552
|
shouldScaleOutNodeExporter,
|
|
43943
44553
|
statOwner,
|