postgresai 0.16.0-dev.1 → 0.16.0-dev.11
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/CHANGELOG.md +11 -0
- package/bin/postgres-ai.ts +408 -25
- package/dist/bin/postgres-ai.js +940 -90
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/checkup-summary.ts +25 -0
- package/lib/checkup.ts +88 -2
- package/lib/init.ts +29 -8
- package/lib/joe.ts +703 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +241 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/auth.test.ts +30 -1
- package/test/checkup.integration.test.ts +31 -21
- package/test/checkup.test.ts +48 -3
- package/test/init.integration.test.ts +9 -79
- package/test/init.test.ts +35 -0
- package/test/joe.cli.test.ts +628 -0
- package/test/joe.test.ts +855 -0
- package/test/monitoring.test.ts +54 -3
- package/test/schema-validation.test.ts +40 -0
- package/test/test-utils.ts +5 -0
- package/test/util.test.ts +227 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -6,43 +6,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
6
6
|
var __defProp = Object.defineProperty;
|
|
7
7
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
function __accessProp(key) {
|
|
10
|
-
return this[key];
|
|
11
|
-
}
|
|
12
|
-
var __toESMCache_node;
|
|
13
|
-
var __toESMCache_esm;
|
|
14
9
|
var __toESM = (mod, isNodeMode, target) => {
|
|
15
|
-
var canCache = mod != null && typeof mod === "object";
|
|
16
|
-
if (canCache) {
|
|
17
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
18
|
-
var cached = cache.get(mod);
|
|
19
|
-
if (cached)
|
|
20
|
-
return cached;
|
|
21
|
-
}
|
|
22
10
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
23
11
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
24
12
|
for (let key of __getOwnPropNames(mod))
|
|
25
13
|
if (!__hasOwnProp.call(to, key))
|
|
26
14
|
__defProp(to, key, {
|
|
27
|
-
get:
|
|
15
|
+
get: () => mod[key],
|
|
28
16
|
enumerable: true
|
|
29
17
|
});
|
|
30
|
-
if (canCache)
|
|
31
|
-
cache.set(mod, to);
|
|
32
18
|
return to;
|
|
33
19
|
};
|
|
34
20
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
35
|
-
var __returnValue = (v) => v;
|
|
36
|
-
function __exportSetter(name, newValue) {
|
|
37
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
38
|
-
}
|
|
39
21
|
var __export = (target, all) => {
|
|
40
22
|
for (var name in all)
|
|
41
23
|
__defProp(target, name, {
|
|
42
24
|
get: all[name],
|
|
43
25
|
enumerable: true,
|
|
44
26
|
configurable: true,
|
|
45
|
-
set:
|
|
27
|
+
set: (newValue) => all[name] = () => newValue
|
|
46
28
|
});
|
|
47
29
|
};
|
|
48
30
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -1020,7 +1002,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1020
1002
|
this._exitCallback = (err) => {
|
|
1021
1003
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1022
1004
|
throw err;
|
|
1023
|
-
}
|
|
1005
|
+
} else {}
|
|
1024
1006
|
};
|
|
1025
1007
|
}
|
|
1026
1008
|
return this;
|
|
@@ -13106,7 +13088,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
13106
13088
|
}
|
|
13107
13089
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
13108
13090
|
function getTime(strictTimeZone) {
|
|
13109
|
-
return function
|
|
13091
|
+
return function time(str2) {
|
|
13110
13092
|
const matches = TIME.exec(str2);
|
|
13111
13093
|
if (!matches)
|
|
13112
13094
|
return false;
|
|
@@ -13323,43 +13305,92 @@ var require_dist2 = __commonJS((exports, module) => {
|
|
|
13323
13305
|
var exports_util2 = {};
|
|
13324
13306
|
__export(exports_util2, {
|
|
13325
13307
|
resolveBaseUrls: () => resolveBaseUrls2,
|
|
13308
|
+
requestTimeoutSignal: () => requestTimeoutSignal2,
|
|
13309
|
+
redactTextSecrets: () => redactTextSecrets2,
|
|
13310
|
+
redactSecretsForLog: () => redactSecretsForLog2,
|
|
13311
|
+
redactSecrets: () => redactSecrets2,
|
|
13326
13312
|
normalizeBaseUrl: () => normalizeBaseUrl2,
|
|
13327
13313
|
maskSecret: () => maskSecret2,
|
|
13328
|
-
|
|
13314
|
+
isRetryableHttpStatus: () => isRetryableHttpStatus2,
|
|
13315
|
+
isFetchTimeout: () => isFetchTimeout2,
|
|
13316
|
+
formatHttpError: () => formatHttpError2,
|
|
13317
|
+
describeFetchError: () => describeFetchError2,
|
|
13318
|
+
HttpStatusError: () => HttpStatusError2,
|
|
13319
|
+
HttpRequestTimeoutError: () => HttpRequestTimeoutError2,
|
|
13320
|
+
DEFAULT_HTTP_REQUEST_TIMEOUT_MS: () => DEFAULT_HTTP_REQUEST_TIMEOUT_MS2
|
|
13329
13321
|
});
|
|
13330
13322
|
function isHtmlContent2(text) {
|
|
13331
13323
|
const trimmed = text.trim();
|
|
13332
13324
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
13333
13325
|
}
|
|
13334
|
-
function formatHttpError2(operation, status, responseBody) {
|
|
13335
|
-
const
|
|
13336
|
-
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
13326
|
+
function formatHttpError2(operation, status, responseBody, statusText) {
|
|
13327
|
+
const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
|
|
13337
13328
|
const remediation = status === 401 ? `
|
|
13338
13329
|
${AUTH_REMEDIATION_HINT2}` : "";
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13330
|
+
let bodyMessage;
|
|
13331
|
+
let bodyDetails;
|
|
13332
|
+
let bodyCode;
|
|
13333
|
+
let bodyHint;
|
|
13334
|
+
if (responseBody && !isHtmlContent2(responseBody)) {
|
|
13343
13335
|
try {
|
|
13344
13336
|
const errObj = JSON.parse(responseBody);
|
|
13345
|
-
const message = errObj.message
|
|
13346
|
-
if (
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13337
|
+
const message = errObj.message ?? errObj.error;
|
|
13338
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
13339
|
+
bodyMessage = redactTextSecrets2(message.trim());
|
|
13340
|
+
}
|
|
13341
|
+
const details = errObj.details ?? errObj.detail;
|
|
13342
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
13343
|
+
bodyDetails = redactTextSecrets2(details.trim());
|
|
13344
|
+
}
|
|
13345
|
+
if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
|
|
13346
|
+
bodyCode = errObj.code.trim();
|
|
13347
|
+
}
|
|
13348
|
+
if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
|
|
13349
|
+
bodyHint = redactTextSecrets2(errObj.hint.trim());
|
|
13350
|
+
}
|
|
13351
|
+
if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
|
|
13352
|
+
bodyDetails = redactSecretsForLog2(JSON.stringify(errObj));
|
|
13352
13353
|
}
|
|
13353
13354
|
} catch {
|
|
13354
13355
|
const trimmed = responseBody.trim();
|
|
13355
13356
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
13356
|
-
|
|
13357
|
-
${trimmed}`;
|
|
13357
|
+
bodyDetails = redactTextSecrets2(trimmed);
|
|
13358
13358
|
}
|
|
13359
13359
|
}
|
|
13360
13360
|
}
|
|
13361
|
+
const trimmedReason = statusText?.trim();
|
|
13362
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
13363
|
+
const safeReasonPhrase = reasonPhrase ? redactTextSecrets2(reasonPhrase) : undefined;
|
|
13364
|
+
const headline = bodyMessage ?? safeReasonPhrase ?? generic;
|
|
13365
|
+
const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
|
|
13366
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
|
|
13367
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
13368
|
+
errMsg += `
|
|
13369
|
+
${bodyDetails}`;
|
|
13370
|
+
}
|
|
13371
|
+
if (bodyHint) {
|
|
13372
|
+
errMsg += `
|
|
13373
|
+
Hint: ${bodyHint}`;
|
|
13374
|
+
}
|
|
13361
13375
|
return errMsg + remediation;
|
|
13362
13376
|
}
|
|
13377
|
+
function describeFetchError2(operation, url, err) {
|
|
13378
|
+
const cause = err?.cause;
|
|
13379
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
13380
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
13381
|
+
}
|
|
13382
|
+
function isRetryableHttpStatus2(status) {
|
|
13383
|
+
return status >= 500 || status === 429;
|
|
13384
|
+
}
|
|
13385
|
+
function requestTimeoutSignal2(timeoutMs) {
|
|
13386
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS2;
|
|
13387
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS2, Math.floor(requested)));
|
|
13388
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
13389
|
+
}
|
|
13390
|
+
function isFetchTimeout2(err) {
|
|
13391
|
+
const name = err?.name;
|
|
13392
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
13393
|
+
}
|
|
13363
13394
|
function maskSecret2(secret) {
|
|
13364
13395
|
if (!secret)
|
|
13365
13396
|
return "";
|
|
@@ -13369,6 +13400,35 @@ function maskSecret2(secret) {
|
|
|
13369
13400
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
13370
13401
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
13371
13402
|
}
|
|
13403
|
+
function isSensitiveLogKey2(key) {
|
|
13404
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
13405
|
+
return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
|
|
13406
|
+
}
|
|
13407
|
+
function redactSecrets2(value) {
|
|
13408
|
+
if (Array.isArray(value)) {
|
|
13409
|
+
return value.map(redactSecrets2);
|
|
13410
|
+
}
|
|
13411
|
+
if (value && typeof value === "object") {
|
|
13412
|
+
const out = {};
|
|
13413
|
+
for (const [key, child] of Object.entries(value)) {
|
|
13414
|
+
out[key] = isSensitiveLogKey2(key) && child != null ? "[REDACTED]" : redactSecrets2(child);
|
|
13415
|
+
}
|
|
13416
|
+
return out;
|
|
13417
|
+
}
|
|
13418
|
+
return value;
|
|
13419
|
+
}
|
|
13420
|
+
function redactSecretsForLog2(text) {
|
|
13421
|
+
let parsed;
|
|
13422
|
+
try {
|
|
13423
|
+
parsed = JSON.parse(text);
|
|
13424
|
+
} catch {
|
|
13425
|
+
return redactTextSecrets2(text);
|
|
13426
|
+
}
|
|
13427
|
+
return JSON.stringify(redactSecrets2(parsed));
|
|
13428
|
+
}
|
|
13429
|
+
function redactTextSecrets2(text) {
|
|
13430
|
+
return text.replace(TEXT_URL_USERINFO2, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR2, "$1[REDACTED]");
|
|
13431
|
+
}
|
|
13372
13432
|
function normalizeBaseUrl2(value) {
|
|
13373
13433
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
13374
13434
|
try {
|
|
@@ -13391,7 +13451,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
13391
13451
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13392
13452
|
};
|
|
13393
13453
|
}
|
|
13394
|
-
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
13454
|
+
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2, HttpStatusError2, DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, HttpRequestTimeoutError2, TEXT_URL_USERINFO2, TEXT_SENSITIVE_PAIR2;
|
|
13395
13455
|
var init_util = __esm(() => {
|
|
13396
13456
|
HTTP_STATUS_MESSAGES2 = {
|
|
13397
13457
|
400: "Bad Request",
|
|
@@ -13405,6 +13465,36 @@ var init_util = __esm(() => {
|
|
|
13405
13465
|
503: "Service Unavailable - server temporarily unavailable",
|
|
13406
13466
|
504: "Gateway Timeout - server temporarily unavailable"
|
|
13407
13467
|
};
|
|
13468
|
+
STANDARD_REASON_PHRASES2 = {
|
|
13469
|
+
400: "Bad Request",
|
|
13470
|
+
401: "Unauthorized",
|
|
13471
|
+
403: "Forbidden",
|
|
13472
|
+
404: "Not Found",
|
|
13473
|
+
408: "Request Timeout",
|
|
13474
|
+
409: "Conflict",
|
|
13475
|
+
413: "Payload Too Large",
|
|
13476
|
+
429: "Too Many Requests",
|
|
13477
|
+
500: "Internal Server Error",
|
|
13478
|
+
502: "Bad Gateway",
|
|
13479
|
+
503: "Service Unavailable",
|
|
13480
|
+
504: "Gateway Timeout"
|
|
13481
|
+
};
|
|
13482
|
+
HttpStatusError2 = class HttpStatusError2 extends Error {
|
|
13483
|
+
status;
|
|
13484
|
+
constructor(message, status) {
|
|
13485
|
+
super(message);
|
|
13486
|
+
this.name = "HttpStatusError";
|
|
13487
|
+
this.status = status;
|
|
13488
|
+
}
|
|
13489
|
+
};
|
|
13490
|
+
HttpRequestTimeoutError2 = class HttpRequestTimeoutError2 extends Error {
|
|
13491
|
+
constructor(operation, timeoutMs) {
|
|
13492
|
+
super(`${operation}: request timed out after ${timeoutMs}ms`);
|
|
13493
|
+
this.name = "HttpRequestTimeoutError";
|
|
13494
|
+
}
|
|
13495
|
+
};
|
|
13496
|
+
TEXT_URL_USERINFO2 = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
|
|
13497
|
+
TEXT_SENSITIVE_PAIR2 = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
13408
13498
|
});
|
|
13409
13499
|
|
|
13410
13500
|
// node_modules/commander/esm.mjs
|
|
@@ -13425,7 +13515,7 @@ var {
|
|
|
13425
13515
|
// package.json
|
|
13426
13516
|
var package_default = {
|
|
13427
13517
|
name: "postgresai",
|
|
13428
|
-
version: "0.16.0-dev.
|
|
13518
|
+
version: "0.16.0-dev.11",
|
|
13429
13519
|
description: "postgres_ai CLI",
|
|
13430
13520
|
license: "Apache-2.0",
|
|
13431
13521
|
private: false,
|
|
@@ -16256,7 +16346,7 @@ var Result = import_lib.default.Result;
|
|
|
16256
16346
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16257
16347
|
var defaults = import_lib.default.defaults;
|
|
16258
16348
|
// package.json
|
|
16259
|
-
var version = "0.16.0-dev.
|
|
16349
|
+
var version = "0.16.0-dev.11";
|
|
16260
16350
|
var package_default2 = {
|
|
16261
16351
|
name: "postgresai",
|
|
16262
16352
|
version,
|
|
@@ -16398,35 +16488,105 @@ function isHtmlContent(text) {
|
|
|
16398
16488
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
16399
16489
|
}
|
|
16400
16490
|
var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16491
|
+
var STANDARD_REASON_PHRASES = {
|
|
16492
|
+
400: "Bad Request",
|
|
16493
|
+
401: "Unauthorized",
|
|
16494
|
+
403: "Forbidden",
|
|
16495
|
+
404: "Not Found",
|
|
16496
|
+
408: "Request Timeout",
|
|
16497
|
+
409: "Conflict",
|
|
16498
|
+
413: "Payload Too Large",
|
|
16499
|
+
429: "Too Many Requests",
|
|
16500
|
+
500: "Internal Server Error",
|
|
16501
|
+
502: "Bad Gateway",
|
|
16502
|
+
503: "Service Unavailable",
|
|
16503
|
+
504: "Gateway Timeout"
|
|
16504
|
+
};
|
|
16505
|
+
function formatHttpError(operation, status, responseBody, statusText) {
|
|
16506
|
+
const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
|
|
16404
16507
|
const remediation = status === 401 ? `
|
|
16405
16508
|
${AUTH_REMEDIATION_HINT}` : "";
|
|
16406
|
-
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
16509
|
+
let bodyMessage;
|
|
16510
|
+
let bodyDetails;
|
|
16511
|
+
let bodyCode;
|
|
16512
|
+
let bodyHint;
|
|
16513
|
+
if (responseBody && !isHtmlContent(responseBody)) {
|
|
16410
16514
|
try {
|
|
16411
16515
|
const errObj = JSON.parse(responseBody);
|
|
16412
|
-
const message = errObj.message
|
|
16413
|
-
if (
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16516
|
+
const message = errObj.message ?? errObj.error;
|
|
16517
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
16518
|
+
bodyMessage = redactTextSecrets(message.trim());
|
|
16519
|
+
}
|
|
16520
|
+
const details = errObj.details ?? errObj.detail;
|
|
16521
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
16522
|
+
bodyDetails = redactTextSecrets(details.trim());
|
|
16523
|
+
}
|
|
16524
|
+
if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
|
|
16525
|
+
bodyCode = errObj.code.trim();
|
|
16526
|
+
}
|
|
16527
|
+
if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
|
|
16528
|
+
bodyHint = redactTextSecrets(errObj.hint.trim());
|
|
16529
|
+
}
|
|
16530
|
+
if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
|
|
16531
|
+
bodyDetails = redactSecretsForLog(JSON.stringify(errObj));
|
|
16419
16532
|
}
|
|
16420
16533
|
} catch {
|
|
16421
16534
|
const trimmed = responseBody.trim();
|
|
16422
16535
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
16423
|
-
|
|
16424
|
-
${trimmed}`;
|
|
16536
|
+
bodyDetails = redactTextSecrets(trimmed);
|
|
16425
16537
|
}
|
|
16426
16538
|
}
|
|
16427
16539
|
}
|
|
16540
|
+
const trimmedReason = statusText?.trim();
|
|
16541
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
16542
|
+
const safeReasonPhrase = reasonPhrase ? redactTextSecrets(reasonPhrase) : undefined;
|
|
16543
|
+
const headline = bodyMessage ?? safeReasonPhrase ?? generic;
|
|
16544
|
+
const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
|
|
16545
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
|
|
16546
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
16547
|
+
errMsg += `
|
|
16548
|
+
${bodyDetails}`;
|
|
16549
|
+
}
|
|
16550
|
+
if (bodyHint) {
|
|
16551
|
+
errMsg += `
|
|
16552
|
+
Hint: ${bodyHint}`;
|
|
16553
|
+
}
|
|
16428
16554
|
return errMsg + remediation;
|
|
16429
16555
|
}
|
|
16556
|
+
function describeFetchError(operation, url, err) {
|
|
16557
|
+
const cause = err?.cause;
|
|
16558
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
16559
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
16560
|
+
}
|
|
16561
|
+
|
|
16562
|
+
class HttpStatusError extends Error {
|
|
16563
|
+
status;
|
|
16564
|
+
constructor(message, status) {
|
|
16565
|
+
super(message);
|
|
16566
|
+
this.name = "HttpStatusError";
|
|
16567
|
+
this.status = status;
|
|
16568
|
+
}
|
|
16569
|
+
}
|
|
16570
|
+
function isRetryableHttpStatus(status) {
|
|
16571
|
+
return status >= 500 || status === 429;
|
|
16572
|
+
}
|
|
16573
|
+
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
|
|
16574
|
+
|
|
16575
|
+
class HttpRequestTimeoutError extends Error {
|
|
16576
|
+
constructor(operation, timeoutMs) {
|
|
16577
|
+
super(`${operation}: request timed out after ${timeoutMs}ms`);
|
|
16578
|
+
this.name = "HttpRequestTimeoutError";
|
|
16579
|
+
}
|
|
16580
|
+
}
|
|
16581
|
+
function requestTimeoutSignal(timeoutMs) {
|
|
16582
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS;
|
|
16583
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, Math.floor(requested)));
|
|
16584
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
16585
|
+
}
|
|
16586
|
+
function isFetchTimeout(err) {
|
|
16587
|
+
const name = err?.name;
|
|
16588
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
16589
|
+
}
|
|
16430
16590
|
function maskSecret(secret) {
|
|
16431
16591
|
if (!secret)
|
|
16432
16592
|
return "";
|
|
@@ -16436,6 +16596,37 @@ function maskSecret(secret) {
|
|
|
16436
16596
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
16437
16597
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
16438
16598
|
}
|
|
16599
|
+
function isSensitiveLogKey(key) {
|
|
16600
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
16601
|
+
return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
|
|
16602
|
+
}
|
|
16603
|
+
function redactSecrets(value) {
|
|
16604
|
+
if (Array.isArray(value)) {
|
|
16605
|
+
return value.map(redactSecrets);
|
|
16606
|
+
}
|
|
16607
|
+
if (value && typeof value === "object") {
|
|
16608
|
+
const out = {};
|
|
16609
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16610
|
+
out[key] = isSensitiveLogKey(key) && child != null ? "[REDACTED]" : redactSecrets(child);
|
|
16611
|
+
}
|
|
16612
|
+
return out;
|
|
16613
|
+
}
|
|
16614
|
+
return value;
|
|
16615
|
+
}
|
|
16616
|
+
function redactSecretsForLog(text) {
|
|
16617
|
+
let parsed;
|
|
16618
|
+
try {
|
|
16619
|
+
parsed = JSON.parse(text);
|
|
16620
|
+
} catch {
|
|
16621
|
+
return redactTextSecrets(text);
|
|
16622
|
+
}
|
|
16623
|
+
return JSON.stringify(redactSecrets(parsed));
|
|
16624
|
+
}
|
|
16625
|
+
var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
|
|
16626
|
+
var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
|
|
16627
|
+
function redactTextSecrets(text) {
|
|
16628
|
+
return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
|
|
16629
|
+
}
|
|
16439
16630
|
function normalizeBaseUrl(value) {
|
|
16440
16631
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
16441
16632
|
try {
|
|
@@ -20973,7 +21164,7 @@ function finalize(ctx, schema2) {
|
|
|
20973
21164
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
20974
21165
|
} else if (ctx.target === "draft-04") {
|
|
20975
21166
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
20976
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
21167
|
+
} else if (ctx.target === "openapi-3.0") {} else {}
|
|
20977
21168
|
if (ctx.external?.uri) {
|
|
20978
21169
|
const id = ctx.external.registry.get(schema2)?.id;
|
|
20979
21170
|
if (!id)
|
|
@@ -21192,7 +21383,7 @@ var literalProcessor = (schema2, ctx, json2, _params) => {
|
|
|
21192
21383
|
if (val === undefined) {
|
|
21193
21384
|
if (ctx.unrepresentable === "throw") {
|
|
21194
21385
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
21195
|
-
}
|
|
21386
|
+
} else {}
|
|
21196
21387
|
} else if (typeof val === "bigint") {
|
|
21197
21388
|
if (ctx.unrepresentable === "throw") {
|
|
21198
21389
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -25801,6 +25992,352 @@ function renderMarkdownForTerminal(md) {
|
|
|
25801
25992
|
`);
|
|
25802
25993
|
}
|
|
25803
25994
|
|
|
25995
|
+
// lib/joe.ts
|
|
25996
|
+
var DEFAULT_BUDGET_MS = 25000;
|
|
25997
|
+
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
25998
|
+
async function callRpc(params) {
|
|
25999
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26000
|
+
if (!apiKey) {
|
|
26001
|
+
throw new Error("API key is required");
|
|
26002
|
+
}
|
|
26003
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26004
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
26005
|
+
const payload = JSON.stringify(body);
|
|
26006
|
+
const headers = {
|
|
26007
|
+
"access-token": apiKey,
|
|
26008
|
+
"Content-Type": "application/json",
|
|
26009
|
+
Connection: "close"
|
|
26010
|
+
};
|
|
26011
|
+
if (debug) {
|
|
26012
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26013
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26014
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26015
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
26016
|
+
}
|
|
26017
|
+
let response;
|
|
26018
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
26019
|
+
try {
|
|
26020
|
+
response = await fetch(url.toString(), {
|
|
26021
|
+
method: "POST",
|
|
26022
|
+
headers,
|
|
26023
|
+
body: payload,
|
|
26024
|
+
signal: requestTimeout.signal
|
|
26025
|
+
});
|
|
26026
|
+
} catch (err) {
|
|
26027
|
+
if (isFetchTimeout(err)) {
|
|
26028
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26029
|
+
}
|
|
26030
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
26031
|
+
}
|
|
26032
|
+
const text = await response.text();
|
|
26033
|
+
if (debug) {
|
|
26034
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26035
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
26036
|
+
}
|
|
26037
|
+
if (!response.ok) {
|
|
26038
|
+
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
26039
|
+
}
|
|
26040
|
+
try {
|
|
26041
|
+
return JSON.parse(text);
|
|
26042
|
+
} catch {
|
|
26043
|
+
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
26044
|
+
}
|
|
26045
|
+
}
|
|
26046
|
+
async function startCommand(params) {
|
|
26047
|
+
const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
|
|
26048
|
+
if (!String(command ?? "").trim()) {
|
|
26049
|
+
throw new Error("command text is required");
|
|
26050
|
+
}
|
|
26051
|
+
const commandId = await callRpc({
|
|
26052
|
+
apiKey,
|
|
26053
|
+
apiBaseUrl,
|
|
26054
|
+
fn: "joe_command_run",
|
|
26055
|
+
body: { instance_id: instanceId, command },
|
|
26056
|
+
operation: "Failed to run Joe command",
|
|
26057
|
+
debug,
|
|
26058
|
+
timeoutMs
|
|
26059
|
+
});
|
|
26060
|
+
if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
|
|
26061
|
+
throw new Error(`Failed to run Joe command: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(commandId))}`);
|
|
26062
|
+
}
|
|
26063
|
+
return commandId;
|
|
26064
|
+
}
|
|
26065
|
+
async function getCommandOutput(params) {
|
|
26066
|
+
const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
|
|
26067
|
+
if (!commandId) {
|
|
26068
|
+
throw new Error("commandId is required");
|
|
26069
|
+
}
|
|
26070
|
+
return callRpc({
|
|
26071
|
+
apiKey,
|
|
26072
|
+
apiBaseUrl,
|
|
26073
|
+
fn: "joe_command_output",
|
|
26074
|
+
body: { command_id: commandId },
|
|
26075
|
+
operation: "Failed to fetch command output",
|
|
26076
|
+
debug,
|
|
26077
|
+
timeoutMs
|
|
26078
|
+
});
|
|
26079
|
+
}
|
|
26080
|
+
function preserveIntegerId(value) {
|
|
26081
|
+
if (typeof value === "number")
|
|
26082
|
+
return value;
|
|
26083
|
+
const parsed = Number(value);
|
|
26084
|
+
return Number.isSafeInteger(parsed) ? parsed : value;
|
|
26085
|
+
}
|
|
26086
|
+
function normalizeProjectRow(row) {
|
|
26087
|
+
return {
|
|
26088
|
+
project_id: preserveIntegerId(row.project_id ?? 0),
|
|
26089
|
+
alias: row.alias ?? null,
|
|
26090
|
+
name: row.name ?? null,
|
|
26091
|
+
joe_ready: Boolean(row.joe_ready ?? false),
|
|
26092
|
+
tunnel: Boolean(row.tunnel ?? false),
|
|
26093
|
+
instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
|
|
26094
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
26097
|
+
async function listProjects(params) {
|
|
26098
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
26099
|
+
const body = {};
|
|
26100
|
+
if (typeof orgId === "number") {
|
|
26101
|
+
body.org_id = orgId;
|
|
26102
|
+
}
|
|
26103
|
+
const rows = await callRpc({
|
|
26104
|
+
apiKey,
|
|
26105
|
+
apiBaseUrl,
|
|
26106
|
+
fn: "projects_list",
|
|
26107
|
+
body,
|
|
26108
|
+
operation: "Failed to list projects",
|
|
26109
|
+
debug
|
|
26110
|
+
});
|
|
26111
|
+
if (!Array.isArray(rows)) {
|
|
26112
|
+
return [];
|
|
26113
|
+
}
|
|
26114
|
+
return rows.map(normalizeProjectRow);
|
|
26115
|
+
}
|
|
26116
|
+
function isNumericProjectRef(ref) {
|
|
26117
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
26118
|
+
}
|
|
26119
|
+
async function resolveJoeInstanceId(params) {
|
|
26120
|
+
const ref = String(params.project ?? "").trim();
|
|
26121
|
+
if (!ref) {
|
|
26122
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
26123
|
+
}
|
|
26124
|
+
const projects = await listProjects({
|
|
26125
|
+
apiKey: params.apiKey,
|
|
26126
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26127
|
+
orgId: params.orgId,
|
|
26128
|
+
debug: params.debug
|
|
26129
|
+
});
|
|
26130
|
+
const needle = ref.toLowerCase();
|
|
26131
|
+
const match = isNumericProjectRef(ref) ? projects.find((p) => String(p.project_id) === String(preserveIntegerId(ref))) : projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
26132
|
+
if (!match) {
|
|
26133
|
+
throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
26134
|
+
}
|
|
26135
|
+
if (match.instance_id == null) {
|
|
26136
|
+
throw new Error(`Project '${ref}' has no Joe instance. Run 'pgai projects' to see which projects have Joe ready.`);
|
|
26137
|
+
}
|
|
26138
|
+
return match.instance_id;
|
|
26139
|
+
}
|
|
26140
|
+
var DESCRIBE_VARIANTS = [
|
|
26141
|
+
"\\d",
|
|
26142
|
+
"\\d+",
|
|
26143
|
+
"\\dt",
|
|
26144
|
+
"\\dt+",
|
|
26145
|
+
"\\di",
|
|
26146
|
+
"\\di+",
|
|
26147
|
+
"\\l",
|
|
26148
|
+
"\\l+",
|
|
26149
|
+
"\\dv",
|
|
26150
|
+
"\\dv+",
|
|
26151
|
+
"\\dm",
|
|
26152
|
+
"\\dm+"
|
|
26153
|
+
];
|
|
26154
|
+
function buildJoeCommandText(command, input = {}) {
|
|
26155
|
+
const arg = String(input.arg ?? "").trim();
|
|
26156
|
+
switch (command) {
|
|
26157
|
+
case "plan":
|
|
26158
|
+
case "explain":
|
|
26159
|
+
case "exec":
|
|
26160
|
+
case "hypo": {
|
|
26161
|
+
if (!arg) {
|
|
26162
|
+
throw new Error(`${command} requires an argument`);
|
|
26163
|
+
}
|
|
26164
|
+
return `${command} ${arg}`;
|
|
26165
|
+
}
|
|
26166
|
+
case "activity":
|
|
26167
|
+
case "reset":
|
|
26168
|
+
return command;
|
|
26169
|
+
case "terminate": {
|
|
26170
|
+
if (!/^[1-9][0-9]*$/.test(arg)) {
|
|
26171
|
+
throw new Error("pid must be a positive integer");
|
|
26172
|
+
}
|
|
26173
|
+
return `terminate ${arg}`;
|
|
26174
|
+
}
|
|
26175
|
+
case "describe": {
|
|
26176
|
+
if (!arg) {
|
|
26177
|
+
throw new Error("describe requires an object name");
|
|
26178
|
+
}
|
|
26179
|
+
const variant = String(input.variant ?? "\\d").trim();
|
|
26180
|
+
if (!DESCRIBE_VARIANTS.includes(variant)) {
|
|
26181
|
+
throw new Error(`Unsupported describe variant '${variant}'. Supported: ${DESCRIBE_VARIANTS.join(" ")}`);
|
|
26182
|
+
}
|
|
26183
|
+
return `${variant} ${arg}`;
|
|
26184
|
+
}
|
|
26185
|
+
}
|
|
26186
|
+
}
|
|
26187
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26188
|
+
async function runCommand(params) {
|
|
26189
|
+
const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
|
|
26190
|
+
const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : DEFAULT_BUDGET_MS;
|
|
26191
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
26192
|
+
const now = params.now ?? Date.now;
|
|
26193
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
26194
|
+
const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
|
|
26195
|
+
const deadline = now() + budgetMs;
|
|
26196
|
+
let status = "pending";
|
|
26197
|
+
const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
|
|
26198
|
+
if (now() >= deadline) {
|
|
26199
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26200
|
+
}
|
|
26201
|
+
for (;; ) {
|
|
26202
|
+
let output;
|
|
26203
|
+
try {
|
|
26204
|
+
output = await getCommandOutput({
|
|
26205
|
+
apiKey,
|
|
26206
|
+
apiBaseUrl,
|
|
26207
|
+
commandId,
|
|
26208
|
+
debug,
|
|
26209
|
+
timeoutMs: remainingRequestMs()
|
|
26210
|
+
});
|
|
26211
|
+
} catch (err) {
|
|
26212
|
+
if (err instanceof HttpRequestTimeoutError) {
|
|
26213
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26214
|
+
}
|
|
26215
|
+
if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
|
|
26216
|
+
if (now() >= deadline) {
|
|
26217
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26218
|
+
}
|
|
26219
|
+
await sleep(pollIntervalMs);
|
|
26220
|
+
continue;
|
|
26221
|
+
}
|
|
26222
|
+
throw err;
|
|
26223
|
+
}
|
|
26224
|
+
status = output.status;
|
|
26225
|
+
if (status === "ok" || status === "error") {
|
|
26226
|
+
return { commandId, status, output, budgetExpired: false };
|
|
26227
|
+
}
|
|
26228
|
+
if (now() >= deadline) {
|
|
26229
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
26230
|
+
}
|
|
26231
|
+
await sleep(pollIntervalMs);
|
|
26232
|
+
}
|
|
26233
|
+
}
|
|
26234
|
+
async function executeJoeCommand(params) {
|
|
26235
|
+
const commandText = buildJoeCommandText(params.command, params.input);
|
|
26236
|
+
let instanceId;
|
|
26237
|
+
const directRef = String(params.instanceId ?? "").trim();
|
|
26238
|
+
if (directRef) {
|
|
26239
|
+
if (!/^[0-9]+$/.test(directRef)) {
|
|
26240
|
+
throw new Error("instanceId must be a numeric Joe instance id");
|
|
26241
|
+
}
|
|
26242
|
+
instanceId = directRef;
|
|
26243
|
+
} else if (String(params.project ?? "").trim()) {
|
|
26244
|
+
instanceId = await resolveJoeInstanceId({
|
|
26245
|
+
apiKey: params.apiKey,
|
|
26246
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26247
|
+
project: String(params.project),
|
|
26248
|
+
orgId: params.orgId,
|
|
26249
|
+
debug: params.debug
|
|
26250
|
+
});
|
|
26251
|
+
} else {
|
|
26252
|
+
throw new Error("either instanceId or project is required");
|
|
26253
|
+
}
|
|
26254
|
+
const outcome = await runCommand({
|
|
26255
|
+
apiKey: params.apiKey,
|
|
26256
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
26257
|
+
instanceId,
|
|
26258
|
+
command: commandText,
|
|
26259
|
+
budgetMs: params.budgetMs,
|
|
26260
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
26261
|
+
debug: params.debug,
|
|
26262
|
+
now: params.now,
|
|
26263
|
+
sleep: params.sleep
|
|
26264
|
+
});
|
|
26265
|
+
return { ...outcome, command: params.command, instanceId, commandText };
|
|
26266
|
+
}
|
|
26267
|
+
function clientSidePlanFlags(planJson) {
|
|
26268
|
+
const flags = [];
|
|
26269
|
+
const walk = (node) => {
|
|
26270
|
+
if (!node || typeof node !== "object") {
|
|
26271
|
+
return;
|
|
26272
|
+
}
|
|
26273
|
+
const nodeType = node["Node Type"];
|
|
26274
|
+
if (nodeType === "Seq Scan") {
|
|
26275
|
+
const rel = node["Relation Name"];
|
|
26276
|
+
flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
|
|
26277
|
+
}
|
|
26278
|
+
if (Array.isArray(node.Plans)) {
|
|
26279
|
+
for (const child of node.Plans) {
|
|
26280
|
+
walk(child);
|
|
26281
|
+
}
|
|
26282
|
+
}
|
|
26283
|
+
};
|
|
26284
|
+
if (Array.isArray(planJson)) {
|
|
26285
|
+
for (const entry of planJson) {
|
|
26286
|
+
if (entry && typeof entry === "object") {
|
|
26287
|
+
walk(entry.Plan ?? entry);
|
|
26288
|
+
}
|
|
26289
|
+
}
|
|
26290
|
+
return flags;
|
|
26291
|
+
}
|
|
26292
|
+
if (planJson && typeof planJson === "object") {
|
|
26293
|
+
const root = planJson;
|
|
26294
|
+
walk(root.Plan ?? planJson);
|
|
26295
|
+
}
|
|
26296
|
+
return flags;
|
|
26297
|
+
}
|
|
26298
|
+
function formatJoeOutput(output) {
|
|
26299
|
+
const lines = [];
|
|
26300
|
+
const section = (value, label) => {
|
|
26301
|
+
if (value == null || value.trim() === "")
|
|
26302
|
+
return;
|
|
26303
|
+
if (lines.length > 0)
|
|
26304
|
+
lines.push("");
|
|
26305
|
+
if (label)
|
|
26306
|
+
lines.push(`${label}:`);
|
|
26307
|
+
lines.push(value);
|
|
26308
|
+
};
|
|
26309
|
+
section(output.response);
|
|
26310
|
+
section(output.plan_text, "plan");
|
|
26311
|
+
const flags = clientSidePlanFlags(output.plan_json).map((flag) => `⚑ ${flag}`);
|
|
26312
|
+
if (flags.length > 0) {
|
|
26313
|
+
lines.push(...flags);
|
|
26314
|
+
}
|
|
26315
|
+
section(output.plan_execution_text, "execution plan (EXPLAIN ANALYZE)");
|
|
26316
|
+
section(output.stats, "stats");
|
|
26317
|
+
section(output.recommendations, "recommendations");
|
|
26318
|
+
if (output.queryid) {
|
|
26319
|
+
if (lines.length > 0)
|
|
26320
|
+
lines.push("");
|
|
26321
|
+
lines.push(`(queryid ${output.queryid})`);
|
|
26322
|
+
}
|
|
26323
|
+
return lines.join(`
|
|
26324
|
+
`);
|
|
26325
|
+
}
|
|
26326
|
+
function formatProjectsTable(projects) {
|
|
26327
|
+
const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
|
|
26328
|
+
const rows = projects.map((p) => [
|
|
26329
|
+
String(p.project_id),
|
|
26330
|
+
p.alias ?? "-",
|
|
26331
|
+
p.name ?? "-",
|
|
26332
|
+
p.joe_ready ? "ready" : "no",
|
|
26333
|
+
p.tunnel ? "yes" : "no"
|
|
26334
|
+
]);
|
|
26335
|
+
const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
|
|
26336
|
+
const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
|
|
26337
|
+
return [pad(header), ...rows.map(pad)].join(`
|
|
26338
|
+
`);
|
|
26339
|
+
}
|
|
26340
|
+
|
|
25804
26341
|
// bin/postgres-ai.ts
|
|
25805
26342
|
init_util();
|
|
25806
26343
|
|
|
@@ -29452,10 +29989,6 @@ async function verifyInitSetup(params) {
|
|
|
29452
29989
|
}
|
|
29453
29990
|
}
|
|
29454
29991
|
}
|
|
29455
|
-
const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
|
|
29456
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29457
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29458
|
-
}
|
|
29459
29992
|
const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
|
|
29460
29993
|
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
29461
29994
|
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
@@ -29522,10 +30055,28 @@ async function checkCurrentUserPermissions(client) {
|
|
|
29522
30055
|
|
|
29523
30056
|
union all
|
|
29524
30057
|
|
|
30058
|
+
select
|
|
30059
|
+
'postgres_ai schema exists' as permission_name,
|
|
30060
|
+
'optional' as status,
|
|
30061
|
+
to_regnamespace('postgres_ai') is not null as granted
|
|
30062
|
+
|
|
30063
|
+
union all
|
|
30064
|
+
|
|
30065
|
+
select
|
|
30066
|
+
'usage on postgres_ai schema' as permission_name,
|
|
30067
|
+
'optional' as status,
|
|
30068
|
+
case
|
|
30069
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
30070
|
+
else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
|
|
30071
|
+
end as granted
|
|
30072
|
+
|
|
30073
|
+
union all
|
|
30074
|
+
|
|
29525
30075
|
select
|
|
29526
30076
|
'postgres_ai.pg_statistic view exists' as permission_name,
|
|
29527
30077
|
'optional' as status,
|
|
29528
30078
|
case
|
|
30079
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
29529
30080
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
29530
30081
|
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
29531
30082
|
end as granted
|
|
@@ -29536,6 +30087,7 @@ async function checkCurrentUserPermissions(client) {
|
|
|
29536
30087
|
'select on postgres_ai.pg_statistic' as permission_name,
|
|
29537
30088
|
'optional' as status,
|
|
29538
30089
|
case
|
|
30090
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
29539
30091
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
29540
30092
|
when to_regclass('postgres_ai.pg_statistic') is null then null
|
|
29541
30093
|
else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
|
|
@@ -29555,6 +30107,10 @@ async function checkCurrentUserPermissions(client) {
|
|
|
29555
30107
|
when permission_name like 'select on pg_catalog.pg_index' then
|
|
29556
30108
|
format('grant select on pg_catalog.pg_index to %I;', current_user)
|
|
29557
30109
|
end
|
|
30110
|
+
when permission_name = 'postgres_ai schema exists' and granted = false then
|
|
30111
|
+
'-- run postgresai prepare-db or create the postgres_ai schema and pg_statistic view manually'
|
|
30112
|
+
when permission_name = 'usage on postgres_ai schema' and granted = false then
|
|
30113
|
+
format('grant usage on schema postgres_ai to %I;', current_user)
|
|
29558
30114
|
when permission_name = 'postgres_ai.pg_statistic view exists' and granted = false then
|
|
29559
30115
|
'-- create postgres_ai.pg_statistic view (see setup script)'
|
|
29560
30116
|
when permission_name = 'select on postgres_ai.pg_statistic' and granted = false then
|
|
@@ -29581,6 +30137,10 @@ function formatPermissionCheckMessages(result) {
|
|
|
29581
30137
|
const warnings = [];
|
|
29582
30138
|
const errors3 = [];
|
|
29583
30139
|
for (const row of result.missingOptional) {
|
|
30140
|
+
if (row.permission_name === "postgres_ai schema exists") {
|
|
30141
|
+
warnings.push("Warning: optional: postgres_ai schema not found — F004/F005 (bloat estimates) will be skipped; run prepare-db or create the view manually to enable them.");
|
|
30142
|
+
continue;
|
|
30143
|
+
}
|
|
29584
30144
|
const fix = row.fix_command ? ` Fix: ${row.fix_command}` : "";
|
|
29585
30145
|
warnings.push(`Warning: optional permission missing — ${row.permission_name}.${fix}`);
|
|
29586
30146
|
}
|
|
@@ -29974,15 +30534,6 @@ async function verifyInitSetupViaSupabase(params) {
|
|
|
29974
30534
|
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
29975
30535
|
}
|
|
29976
30536
|
}
|
|
29977
|
-
const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29978
|
-
if (explainFnExistsRes.rowCount === 0) {
|
|
29979
|
-
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
29980
|
-
} else {
|
|
29981
|
-
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
29982
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29983
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29984
|
-
}
|
|
29985
|
-
}
|
|
29986
30537
|
const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29987
30538
|
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
29988
30539
|
missingRequired.push("function postgres_ai.table_describe exists");
|
|
@@ -32013,7 +32564,7 @@ function buildCheckInfoMap() {
|
|
|
32013
32564
|
}
|
|
32014
32565
|
|
|
32015
32566
|
// lib/checkup.ts
|
|
32016
|
-
var __dirname = "/
|
|
32567
|
+
var __dirname = "/Users/nik/gitlab/postgresai-wt-joe-cli/cli/lib";
|
|
32017
32568
|
var SECONDS_PER_DAY = 86400;
|
|
32018
32569
|
var SECONDS_PER_HOUR = 3600;
|
|
32019
32570
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -32778,12 +33329,70 @@ async function generateF003(client, nodeName) {
|
|
|
32778
33329
|
};
|
|
32779
33330
|
return report;
|
|
32780
33331
|
}
|
|
33332
|
+
function bloatErrorStatus(err) {
|
|
33333
|
+
const error2 = err instanceof Error ? err.message : String(err);
|
|
33334
|
+
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code || "") : "";
|
|
33335
|
+
const normalized = error2.toLowerCase();
|
|
33336
|
+
let reason = "query_error";
|
|
33337
|
+
if (code === "3F000" || normalized.includes('schema "postgres_ai" does not exist')) {
|
|
33338
|
+
reason = "missing_schema";
|
|
33339
|
+
} else if (code === "42P01" || normalized.includes('relation "postgres_ai.pg_statistic" does not exist')) {
|
|
33340
|
+
reason = "missing_view";
|
|
33341
|
+
} else if (code === "42501" || normalized.includes("permission denied")) {
|
|
33342
|
+
reason = "missing_grant";
|
|
33343
|
+
}
|
|
33344
|
+
return { ok: false, reason, error: error2 };
|
|
33345
|
+
}
|
|
33346
|
+
async function getBloatCheckStatus(client) {
|
|
33347
|
+
try {
|
|
33348
|
+
const result = await client.query(`
|
|
33349
|
+
select
|
|
33350
|
+
to_regnamespace('postgres_ai') is not null as schema_exists,
|
|
33351
|
+
case
|
|
33352
|
+
when to_regnamespace('postgres_ai') is null then false
|
|
33353
|
+
else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
|
|
33354
|
+
end as schema_usage,
|
|
33355
|
+
case
|
|
33356
|
+
when to_regnamespace('postgres_ai') is null then false
|
|
33357
|
+
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
|
|
33358
|
+
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
33359
|
+
end as view_exists,
|
|
33360
|
+
case
|
|
33361
|
+
when to_regnamespace('postgres_ai') is null then false
|
|
33362
|
+
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
|
|
33363
|
+
when to_regclass('postgres_ai.pg_statistic') is null then false
|
|
33364
|
+
else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'SELECT')
|
|
33365
|
+
end as view_select
|
|
33366
|
+
`);
|
|
33367
|
+
const capability = result.rows[0] || {};
|
|
33368
|
+
if (!capability.schema_exists) {
|
|
33369
|
+
return { ok: false, reason: "missing_schema", error: 'schema "postgres_ai" does not exist' };
|
|
33370
|
+
}
|
|
33371
|
+
if (!capability.schema_usage) {
|
|
33372
|
+
return { ok: false, reason: "missing_grant", error: "permission denied for schema postgres_ai" };
|
|
33373
|
+
}
|
|
33374
|
+
if (!capability.view_exists) {
|
|
33375
|
+
return { ok: false, reason: "missing_view", error: 'relation "postgres_ai.pg_statistic" does not exist' };
|
|
33376
|
+
}
|
|
33377
|
+
if (!capability.view_select) {
|
|
33378
|
+
return { ok: false, reason: "missing_grant", error: "permission denied for relation postgres_ai.pg_statistic" };
|
|
33379
|
+
}
|
|
33380
|
+
return { ok: true, reason: null, error: null };
|
|
33381
|
+
} catch (err) {
|
|
33382
|
+
return bloatErrorStatus(err);
|
|
33383
|
+
}
|
|
33384
|
+
}
|
|
32781
33385
|
async function generateF004(client, nodeName) {
|
|
32782
33386
|
const report = createBaseReport("F004", "Autovacuum: heap bloat (estimated)", nodeName);
|
|
32783
33387
|
const postgresVersion = await getPostgresVersion(client);
|
|
32784
33388
|
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
|
|
32785
33389
|
let bloatedTables = [];
|
|
33390
|
+
let status = await getBloatCheckStatus(client);
|
|
32786
33391
|
try {
|
|
33392
|
+
if (!status.ok)
|
|
33393
|
+
throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
|
|
33394
|
+
code: status.reason === "missing_schema" ? "3F000" : status.reason === "missing_view" ? "42P01" : status.reason === "missing_grant" ? "42501" : undefined
|
|
33395
|
+
});
|
|
32787
33396
|
const sql = getMetricSql(METRIC_NAMES.F004, pgMajorVersion);
|
|
32788
33397
|
const bloatResult = await client.query(sql);
|
|
32789
33398
|
const vacuumStatsResult = await client.query(`
|
|
@@ -32827,7 +33436,8 @@ async function generateF004(client, nodeName) {
|
|
|
32827
33436
|
};
|
|
32828
33437
|
});
|
|
32829
33438
|
} catch (err) {
|
|
32830
|
-
|
|
33439
|
+
status = bloatErrorStatus(err);
|
|
33440
|
+
const errorMsg = status.error || "Unknown error";
|
|
32831
33441
|
console.error(`[F004] Error estimating table bloat: ${errorMsg}`);
|
|
32832
33442
|
if (errorMsg.includes("postgres_ai.")) {
|
|
32833
33443
|
console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
|
|
@@ -32837,6 +33447,7 @@ async function generateF004(client, nodeName) {
|
|
|
32837
33447
|
const totalCount = bloatedTables.length;
|
|
32838
33448
|
const totalBloatSizeBytes = bloatedTables.reduce((sum, t) => sum + t.bloat_size, 0);
|
|
32839
33449
|
const dbEntry = {
|
|
33450
|
+
status,
|
|
32840
33451
|
bloated_tables: bloatedTables,
|
|
32841
33452
|
total_count: totalCount,
|
|
32842
33453
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
@@ -32855,7 +33466,12 @@ async function generateF005(client, nodeName) {
|
|
|
32855
33466
|
const postgresVersion = await getPostgresVersion(client);
|
|
32856
33467
|
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
|
|
32857
33468
|
let bloatedIndexes = [];
|
|
33469
|
+
let status = await getBloatCheckStatus(client);
|
|
32858
33470
|
try {
|
|
33471
|
+
if (!status.ok)
|
|
33472
|
+
throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
|
|
33473
|
+
code: status.reason === "missing_schema" ? "3F000" : status.reason === "missing_view" ? "42P01" : status.reason === "missing_grant" ? "42501" : undefined
|
|
33474
|
+
});
|
|
32859
33475
|
const sql = getMetricSql(METRIC_NAMES.F005, pgMajorVersion);
|
|
32860
33476
|
const bloatResult = await client.query(sql);
|
|
32861
33477
|
const vacuumStatsResult = await client.query(`
|
|
@@ -32904,7 +33520,8 @@ async function generateF005(client, nodeName) {
|
|
|
32904
33520
|
};
|
|
32905
33521
|
});
|
|
32906
33522
|
} catch (err) {
|
|
32907
|
-
|
|
33523
|
+
status = bloatErrorStatus(err);
|
|
33524
|
+
const errorMsg = status.error || "Unknown error";
|
|
32908
33525
|
console.error(`[F005] Error estimating index bloat: ${errorMsg}`);
|
|
32909
33526
|
if (errorMsg.includes("postgres_ai.")) {
|
|
32910
33527
|
console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
|
|
@@ -32914,6 +33531,7 @@ async function generateF005(client, nodeName) {
|
|
|
32914
33531
|
const totalCount = bloatedIndexes.length;
|
|
32915
33532
|
const totalBloatSizeBytes = bloatedIndexes.reduce((sum, idx) => sum + idx.bloat_size, 0);
|
|
32916
33533
|
const dbEntry = {
|
|
33534
|
+
status,
|
|
32917
33535
|
bloated_indexes: bloatedIndexes,
|
|
32918
33536
|
total_count: totalCount,
|
|
32919
33537
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
@@ -33586,6 +34204,10 @@ function generateCheckSummary(checkId, report) {
|
|
|
33586
34204
|
return summarizeF001(nodeData);
|
|
33587
34205
|
case "F003":
|
|
33588
34206
|
return summarizeF003(nodeData);
|
|
34207
|
+
case "F004":
|
|
34208
|
+
return summarizeBloat(nodeData, "table");
|
|
34209
|
+
case "F005":
|
|
34210
|
+
return summarizeBloat(nodeData, "index");
|
|
33589
34211
|
case "G001":
|
|
33590
34212
|
return summarizeG001(nodeData);
|
|
33591
34213
|
case "G003":
|
|
@@ -33771,6 +34393,25 @@ function summarizeF003(nodeData) {
|
|
|
33771
34393
|
}
|
|
33772
34394
|
return { status: "warning", message: parts.join(", ") };
|
|
33773
34395
|
}
|
|
34396
|
+
function summarizeBloat(nodeData, kind) {
|
|
34397
|
+
const data = nodeData?.data || {};
|
|
34398
|
+
let totalCount = 0;
|
|
34399
|
+
for (const dbData of Object.values(data)) {
|
|
34400
|
+
const dbEntry = dbData;
|
|
34401
|
+
if (dbEntry?.status?.ok === false) {
|
|
34402
|
+
const reason = String(dbEntry.status.reason || "query_error").replaceAll("_", " ");
|
|
34403
|
+
return { status: "warning", message: `Bloat estimate degraded: ${reason}` };
|
|
34404
|
+
}
|
|
34405
|
+
totalCount += dbEntry?.total_count || 0;
|
|
34406
|
+
}
|
|
34407
|
+
if (totalCount === 0) {
|
|
34408
|
+
return { status: "ok", message: `No bloated ${kind}${kind === "index" ? "es" : "s"} found` };
|
|
34409
|
+
}
|
|
34410
|
+
return {
|
|
34411
|
+
status: "warning",
|
|
34412
|
+
message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
|
|
34413
|
+
};
|
|
34414
|
+
}
|
|
33774
34415
|
function summarizeG001(nodeData) {
|
|
33775
34416
|
const data = nodeData?.data || {};
|
|
33776
34417
|
const settingsCount = Object.keys(data).length;
|
|
@@ -34127,9 +34768,15 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
34127
34768
|
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
34128
34769
|
return null;
|
|
34129
34770
|
}
|
|
34130
|
-
|
|
34131
|
-
|
|
34132
|
-
|
|
34771
|
+
if (opts.markdown) {
|
|
34772
|
+
console.error("Notice: no API key configured \u2014 regular report upload is disabled.");
|
|
34773
|
+
console.error(" The full report JSON will still be sent to the PostgresAI API for markdown conversion.");
|
|
34774
|
+
console.error(" To avoid sending report data, replace --markdown with --no-upload and --json or --output.");
|
|
34775
|
+
} else {
|
|
34776
|
+
console.error("Notice: no API key configured \u2014 results will NOT be uploaded to PostgresAI.");
|
|
34777
|
+
console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
|
|
34778
|
+
console.error(" To run locally without this notice, pass --no-upload.");
|
|
34779
|
+
}
|
|
34133
34780
|
return;
|
|
34134
34781
|
}
|
|
34135
34782
|
const cfg = readConfig();
|
|
@@ -35354,7 +36001,7 @@ program2.command("unprepare-db [conn]").description("remove monitoring setup: dr
|
|
|
35354
36001
|
closeReadline();
|
|
35355
36002
|
}
|
|
35356
36003
|
});
|
|
35357
|
-
program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").option("--markdown", "output markdown
|
|
36004
|
+
program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").option("--markdown", "output markdown via PostgresAI API (transmits the full report JSON)").addHelpText("after", [
|
|
35358
36005
|
"",
|
|
35359
36006
|
"Available checks:",
|
|
35360
36007
|
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
|
|
@@ -35365,7 +36012,7 @@ program2.command("checkup [checkIdOrConn] [conn]").description("generate health
|
|
|
35365
36012
|
" postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
|
|
35366
36013
|
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
35367
36014
|
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
|
|
35368
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --
|
|
36015
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --markdown"
|
|
35369
36016
|
].join(`
|
|
35370
36017
|
`)).action(async (checkIdOrConn, connArg, opts, cmd) => {
|
|
35371
36018
|
const checkIdPattern = /^[A-Z]\d{3}$/i;
|
|
@@ -35405,6 +36052,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35405
36052
|
return;
|
|
35406
36053
|
}
|
|
35407
36054
|
const uploadExplicitlyDisabled = opts.upload === false;
|
|
36055
|
+
if (uploadExplicitlyDisabled && shouldConvertMarkdown) {
|
|
36056
|
+
console.error("Error: --no-upload and --markdown are mutually exclusive");
|
|
36057
|
+
console.error("Markdown conversion is performed by the PostgresAI API and transmits the full report JSON.");
|
|
36058
|
+
console.error("Drop --no-upload to allow transmission, or use --json or --output for local-only output.");
|
|
36059
|
+
process.exitCode = 1;
|
|
36060
|
+
return;
|
|
36061
|
+
}
|
|
35408
36062
|
let shouldUpload = !uploadExplicitlyDisabled;
|
|
35409
36063
|
const outputPath = prepareOutputDirectory(opts.output);
|
|
35410
36064
|
if (outputPath === null) {
|
|
@@ -35587,7 +36241,9 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35587
36241
|
console.log(`
|
|
35588
36242
|
For details:`);
|
|
35589
36243
|
console.log(" --json Output JSON");
|
|
35590
|
-
|
|
36244
|
+
if (!uploadExplicitlyDisabled) {
|
|
36245
|
+
console.log(" --markdown Output markdown via PostgresAI API");
|
|
36246
|
+
}
|
|
35591
36247
|
console.log(" --output <dir> Save to directory");
|
|
35592
36248
|
}
|
|
35593
36249
|
} catch (error2) {
|
|
@@ -35659,6 +36315,16 @@ function checkRunningContainers() {
|
|
|
35659
36315
|
return { running: false, containers: [] };
|
|
35660
36316
|
}
|
|
35661
36317
|
}
|
|
36318
|
+
function planMonitoringRegistration(args) {
|
|
36319
|
+
const projectName = args.project?.trim() || undefined;
|
|
36320
|
+
if (args.instanceId) {
|
|
36321
|
+
return { kind: "adopt", projectName };
|
|
36322
|
+
}
|
|
36323
|
+
if (!projectName) {
|
|
36324
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
36325
|
+
}
|
|
36326
|
+
return { kind: "self-register", projectName };
|
|
36327
|
+
}
|
|
35662
36328
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
35663
36329
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
35664
36330
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -35666,16 +36332,19 @@ async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
|
35666
36332
|
const instanceId = opts?.instanceId;
|
|
35667
36333
|
const retries = opts?.retries ?? (instanceId ? 1 : 0);
|
|
35668
36334
|
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
36335
|
+
const hasProjectName = !!(projectName && projectName.trim());
|
|
35669
36336
|
if (debug) {
|
|
35670
36337
|
console.error(`
|
|
35671
36338
|
Debug: Registering monitoring instance...`);
|
|
35672
36339
|
console.error(`Debug: POST ${url}`);
|
|
35673
|
-
console.error(`Debug: project_name=${projectName}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
36340
|
+
console.error(`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
35674
36341
|
}
|
|
35675
36342
|
const requestBody = {
|
|
35676
|
-
api_token: apiKey
|
|
35677
|
-
project_name: projectName
|
|
36343
|
+
api_token: apiKey
|
|
35678
36344
|
};
|
|
36345
|
+
if (hasProjectName) {
|
|
36346
|
+
requestBody.project_name = projectName;
|
|
36347
|
+
}
|
|
35679
36348
|
if (instanceId) {
|
|
35680
36349
|
requestBody.instance_id = instanceId;
|
|
35681
36350
|
}
|
|
@@ -36218,8 +36887,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36218
36887
|
console.log(`\u2713 Services started
|
|
36219
36888
|
`);
|
|
36220
36889
|
if (apiKey && !opts.demo) {
|
|
36221
|
-
const projectName = opts.project || "postgres-ai-monitoring";
|
|
36222
36890
|
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
36891
|
+
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
36892
|
+
const projectName = plan.projectName;
|
|
36223
36893
|
if (instanceId) {
|
|
36224
36894
|
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
36225
36895
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36235,9 +36905,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36235
36905
|
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
36236
36906
|
`);
|
|
36237
36907
|
} else if (reg) {
|
|
36238
|
-
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project \u2014 reports will use project '${projectName}'`);
|
|
36908
|
+
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>`));
|
|
36239
36909
|
} else {
|
|
36240
|
-
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId} \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`);
|
|
36910
|
+
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>`));
|
|
36241
36911
|
}
|
|
36242
36912
|
const aas = await registerAasCollection(apiKey, instanceId, {
|
|
36243
36913
|
grafanaPassword,
|
|
@@ -36253,6 +36923,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36253
36923
|
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
36254
36924
|
`);
|
|
36255
36925
|
}
|
|
36926
|
+
} else if (plan.kind === "error-missing-project") {
|
|
36927
|
+
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>.");
|
|
36928
|
+
process.exitCode = 1;
|
|
36256
36929
|
} else {
|
|
36257
36930
|
registerMonitoringInstance(apiKey, projectName, {
|
|
36258
36931
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36785,7 +37458,7 @@ targets.command("test <name>").description("test monitoring target database conn
|
|
|
36785
37458
|
}
|
|
36786
37459
|
});
|
|
36787
37460
|
var auth = program2.command("auth").description("authentication and API key management");
|
|
36788
|
-
|
|
37461
|
+
async function runAuthLogin(opts) {
|
|
36789
37462
|
if (opts.setKey) {
|
|
36790
37463
|
const trimmedKey = opts.setKey.trim();
|
|
36791
37464
|
if (!trimmedKey) {
|
|
@@ -36986,7 +37659,12 @@ Authentication failed: ${message}`);
|
|
|
36986
37659
|
console.error(`Authentication error: ${message}`);
|
|
36987
37660
|
process.exit(1);
|
|
36988
37661
|
}
|
|
36989
|
-
}
|
|
37662
|
+
}
|
|
37663
|
+
function configureLoginCommand(command) {
|
|
37664
|
+
return command.description("authenticate via browser (OAuth) or store API key directly").option("--set-key <key>", "store API key directly without OAuth flow").option("--port <port>", "local callback server port (default: random)", parseInt).option("--debug", "enable debug output");
|
|
37665
|
+
}
|
|
37666
|
+
configureLoginCommand(auth.command("login", { isDefault: true })).action(runAuthLogin);
|
|
37667
|
+
configureLoginCommand(program2.command("login")).action(runAuthLogin);
|
|
36990
37668
|
auth.command("show-key").description("show API key (masked)").action(async () => {
|
|
36991
37669
|
const cfg = readConfig();
|
|
36992
37670
|
if (!cfg.apiKey) {
|
|
@@ -37889,6 +38567,177 @@ function tryParseJson(s) {
|
|
|
37889
38567
|
return s;
|
|
37890
38568
|
}
|
|
37891
38569
|
}
|
|
38570
|
+
function printJoeOutcome(outcome, json3, budgetMs) {
|
|
38571
|
+
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
38572
|
+
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
38573
|
+
if (outcome.budgetExpired) {
|
|
38574
|
+
if (json3) {
|
|
38575
|
+
console.log(JSON.stringify({
|
|
38576
|
+
command_id: outcome.commandId,
|
|
38577
|
+
status: outcome.status,
|
|
38578
|
+
budget_expired: true,
|
|
38579
|
+
resume: `pgai joe result ${outcome.commandId}`
|
|
38580
|
+
}, null, 2));
|
|
38581
|
+
} else {
|
|
38582
|
+
console.log(`started ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
|
|
38583
|
+
}
|
|
38584
|
+
return;
|
|
38585
|
+
}
|
|
38586
|
+
const output = outcome.output;
|
|
38587
|
+
if (outcome.status === "ok") {
|
|
38588
|
+
if (!output) {
|
|
38589
|
+
console.error(`command ${outcome.commandId} ok but output is empty`);
|
|
38590
|
+
process.exitCode = 1;
|
|
38591
|
+
return;
|
|
38592
|
+
}
|
|
38593
|
+
if (json3)
|
|
38594
|
+
console.log(JSON.stringify(output, null, 2));
|
|
38595
|
+
else {
|
|
38596
|
+
console.log(`command ${outcome.commandId} \xB7 ok`);
|
|
38597
|
+
const body = formatJoeOutput(output);
|
|
38598
|
+
if (body)
|
|
38599
|
+
console.log(body);
|
|
38600
|
+
}
|
|
38601
|
+
return;
|
|
38602
|
+
}
|
|
38603
|
+
if (json3 && output)
|
|
38604
|
+
console.log(JSON.stringify(output, null, 2));
|
|
38605
|
+
console.error(`command ${outcome.commandId} error: ${output?.error ?? "command failed"}`);
|
|
38606
|
+
process.exitCode = 1;
|
|
38607
|
+
}
|
|
38608
|
+
async function runJoeCli(command, arg, opts) {
|
|
38609
|
+
try {
|
|
38610
|
+
const rootOpts = program2.opts();
|
|
38611
|
+
const cfg = readConfig();
|
|
38612
|
+
const { apiKey } = getConfig(rootOpts);
|
|
38613
|
+
if (!apiKey) {
|
|
38614
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
38615
|
+
process.exitCode = 1;
|
|
38616
|
+
return;
|
|
38617
|
+
}
|
|
38618
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
38619
|
+
const instanceRef = (opts.instanceId ?? "").toString().trim();
|
|
38620
|
+
if (!projectRef && !instanceRef) {
|
|
38621
|
+
console.error("Specify --instance-id <id> (or --project <id|alias> once projects_list is available).");
|
|
38622
|
+
process.exitCode = 1;
|
|
38623
|
+
return;
|
|
38624
|
+
}
|
|
38625
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
38626
|
+
if (typeof opts.budget === "number" && (!Number.isFinite(opts.budget) || opts.budget < 0)) {
|
|
38627
|
+
throw new Error("--budget must be a non-negative number of seconds");
|
|
38628
|
+
}
|
|
38629
|
+
const budgetMs = typeof opts.budget === "number" ? opts.budget * 1000 : undefined;
|
|
38630
|
+
const outcome = await executeJoeCommand({
|
|
38631
|
+
apiKey,
|
|
38632
|
+
apiBaseUrl,
|
|
38633
|
+
command,
|
|
38634
|
+
project: projectRef || undefined,
|
|
38635
|
+
instanceId: instanceRef || undefined,
|
|
38636
|
+
input: { arg, variant: opts.variant ?? null },
|
|
38637
|
+
orgId: cfg.orgId ?? undefined,
|
|
38638
|
+
budgetMs,
|
|
38639
|
+
debug: !!opts.debug
|
|
38640
|
+
});
|
|
38641
|
+
printJoeOutcome(outcome, !!opts.json, budgetMs);
|
|
38642
|
+
} catch (err) {
|
|
38643
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38644
|
+
console.error(message);
|
|
38645
|
+
process.exitCode = 1;
|
|
38646
|
+
}
|
|
38647
|
+
}
|
|
38648
|
+
function withJoeOptions(cmd) {
|
|
38649
|
+
return cmd.option("--instance-id <id>", "target the Joe instance id directly (skips --project resolution; the v1 path while projects_list is unavailable)").option("--project <id|alias>", "target project by numeric id OR alias/name (requires projects_list)").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseFloat(v)).option("--debug", "enable debug output").option("--json", "output raw JSON");
|
|
38650
|
+
}
|
|
38651
|
+
var joe = program2.command("joe").description("Joe \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones");
|
|
38652
|
+
withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
|
|
38653
|
+
await runJoeCli("plan", sql, opts);
|
|
38654
|
+
});
|
|
38655
|
+
withJoeOptions(joe.command("explain <sql>").description("EXPLAIN + EXPLAIN ANALYZE a query (EXECUTES on the ephemeral clone)")).action(async (sql, opts) => {
|
|
38656
|
+
await runJoeCli("explain", sql, opts);
|
|
38657
|
+
});
|
|
38658
|
+
withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
|
|
38659
|
+
await runJoeCli("exec", sql, opts);
|
|
38660
|
+
});
|
|
38661
|
+
withJoeOptions(joe.command("hypo <args>").description("HypoPG hypothetical indexes (e.g. `hypo create index on users (email)`, `hypo desc`, `hypo reset`)")).action(async (args, opts) => {
|
|
38662
|
+
await runJoeCli("hypo", args, opts);
|
|
38663
|
+
});
|
|
38664
|
+
withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity) on the clone")).action(async (opts) => {
|
|
38665
|
+
await runJoeCli("activity", null, opts);
|
|
38666
|
+
});
|
|
38667
|
+
withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
38668
|
+
await runJoeCli("terminate", pid, opts);
|
|
38669
|
+
});
|
|
38670
|
+
withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
38671
|
+
await runJoeCli("reset", null, opts);
|
|
38672
|
+
});
|
|
38673
|
+
withJoeOptions(joe.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
|
|
38674
|
+
await runJoeCli("describe", object4, opts);
|
|
38675
|
+
});
|
|
38676
|
+
joe.command("result <commandId>").description("fetch a Joe command's output by id (resume a budget-expired one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
|
|
38677
|
+
try {
|
|
38678
|
+
const rootOpts = program2.opts();
|
|
38679
|
+
const cfg = readConfig();
|
|
38680
|
+
const { apiKey } = getConfig(rootOpts);
|
|
38681
|
+
if (!apiKey) {
|
|
38682
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
38683
|
+
process.exitCode = 1;
|
|
38684
|
+
return;
|
|
38685
|
+
}
|
|
38686
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
38687
|
+
const output = await getCommandOutput({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
38688
|
+
if (opts.json) {
|
|
38689
|
+
console.log(JSON.stringify(output, null, 2));
|
|
38690
|
+
if (output.status !== "ok") {
|
|
38691
|
+
process.exitCode = 1;
|
|
38692
|
+
}
|
|
38693
|
+
return;
|
|
38694
|
+
}
|
|
38695
|
+
if (output.status === "ok") {
|
|
38696
|
+
console.log(`command ${output.command_id} \xB7 ok`);
|
|
38697
|
+
const body = formatJoeOutput(output);
|
|
38698
|
+
if (body)
|
|
38699
|
+
console.log(body);
|
|
38700
|
+
} else if (output.status === "error") {
|
|
38701
|
+
console.error(`command ${output.command_id} error: ${output.error ?? "command failed"}`);
|
|
38702
|
+
process.exitCode = 1;
|
|
38703
|
+
} else {
|
|
38704
|
+
console.error(`command ${output.command_id} \xB7 ${output.status} \u2014 result is not ready`);
|
|
38705
|
+
process.exitCode = 1;
|
|
38706
|
+
}
|
|
38707
|
+
} catch (err) {
|
|
38708
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38709
|
+
console.error(message);
|
|
38710
|
+
process.exitCode = 1;
|
|
38711
|
+
}
|
|
38712
|
+
});
|
|
38713
|
+
program2.command("projects").description("list the org's projects (shows which have Joe ready) \u2014 org-level, not a Joe endpoint").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
38714
|
+
try {
|
|
38715
|
+
const rootOpts = program2.opts();
|
|
38716
|
+
const cfg = readConfig();
|
|
38717
|
+
const { apiKey } = getConfig(rootOpts);
|
|
38718
|
+
if (!apiKey) {
|
|
38719
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
38720
|
+
process.exitCode = 1;
|
|
38721
|
+
return;
|
|
38722
|
+
}
|
|
38723
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
38724
|
+
const projects = await listProjects({
|
|
38725
|
+
apiKey,
|
|
38726
|
+
apiBaseUrl,
|
|
38727
|
+
orgId: cfg.orgId ?? undefined,
|
|
38728
|
+
debug: !!opts.debug
|
|
38729
|
+
});
|
|
38730
|
+
if (opts.json) {
|
|
38731
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
38732
|
+
} else {
|
|
38733
|
+
console.log(formatProjectsTable(projects));
|
|
38734
|
+
}
|
|
38735
|
+
} catch (err) {
|
|
38736
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38737
|
+
console.error(message);
|
|
38738
|
+
process.exitCode = 1;
|
|
38739
|
+
}
|
|
38740
|
+
});
|
|
37892
38741
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
37893
38742
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
37894
38743
|
const rootOpts = program2.opts();
|
|
@@ -38018,5 +38867,6 @@ export {
|
|
|
38018
38867
|
registerMonitoringInstance,
|
|
38019
38868
|
refreshBundledComposeIfStale,
|
|
38020
38869
|
readDeployedTag,
|
|
38870
|
+
planMonitoringRegistration,
|
|
38021
38871
|
isValidComposeYaml
|
|
38022
38872
|
};
|