postgresai 0.16.0-dev.10 → 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/README.md +1 -84
- package/bin/postgres-ai.ts +140 -540
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +790 -2282
- package/lib/checkup-summary.ts +25 -0
- package/lib/checkup.ts +88 -2
- package/lib/init.ts +29 -0
- package/lib/joe.ts +333 -391
- package/lib/mcp-server.ts +0 -625
- package/lib/util.ts +145 -29
- package/package.json +1 -1
- 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.test.ts +35 -0
- package/test/joe.cli.test.ts +355 -196
- package/test/joe.test.ts +565 -568
- package/test/schema-validation.test.ts +40 -0
- package/test/test-utils.ts +5 -0
- package/test/util.test.ts +116 -0
- package/lib/dblab.ts +0 -449
- package/test/dblab.cli.test.ts +0 -373
- package/test/dblab.test.ts +0 -488
- package/test/e2e/pgai-e2e-smoke.sh +0 -192
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;
|
|
@@ -9980,8 +9962,8 @@ var require_utils3 = __commonJS((exports, module) => {
|
|
|
9980
9962
|
}
|
|
9981
9963
|
return ind;
|
|
9982
9964
|
}
|
|
9983
|
-
function removeDotSegments(
|
|
9984
|
-
let input =
|
|
9965
|
+
function removeDotSegments(path4) {
|
|
9966
|
+
let input = path4;
|
|
9985
9967
|
const output = [];
|
|
9986
9968
|
let nextSlash = -1;
|
|
9987
9969
|
let len = 0;
|
|
@@ -10171,8 +10153,8 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
10171
10153
|
wsComponent.secure = undefined;
|
|
10172
10154
|
}
|
|
10173
10155
|
if (wsComponent.resourceName) {
|
|
10174
|
-
const [
|
|
10175
|
-
wsComponent.path =
|
|
10156
|
+
const [path4, query] = wsComponent.resourceName.split("?");
|
|
10157
|
+
wsComponent.path = path4 && path4 !== "/" ? path4 : undefined;
|
|
10176
10158
|
wsComponent.query = query;
|
|
10177
10159
|
wsComponent.resourceName = undefined;
|
|
10178
10160
|
}
|
|
@@ -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;
|
|
@@ -13307,12 +13289,12 @@ var require_dist2 = __commonJS((exports, module) => {
|
|
|
13307
13289
|
throw new Error(`Unknown format "${name}"`);
|
|
13308
13290
|
return f;
|
|
13309
13291
|
};
|
|
13310
|
-
function addFormats(ajv, list,
|
|
13292
|
+
function addFormats(ajv, list, fs4, exportName) {
|
|
13311
13293
|
var _a2;
|
|
13312
13294
|
var _b;
|
|
13313
13295
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== undefined || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
|
|
13314
13296
|
for (const f of list)
|
|
13315
|
-
ajv.addFormat(f,
|
|
13297
|
+
ajv.addFormat(f, fs4[f]);
|
|
13316
13298
|
}
|
|
13317
13299
|
module.exports = exports = formatsPlugin;
|
|
13318
13300
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -13323,11 +13305,19 @@ 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,
|
|
13326
13310
|
redactSecretsForLog: () => redactSecretsForLog2,
|
|
13311
|
+
redactSecrets: () => redactSecrets2,
|
|
13327
13312
|
normalizeBaseUrl: () => normalizeBaseUrl2,
|
|
13328
13313
|
maskSecret: () => maskSecret2,
|
|
13314
|
+
isRetryableHttpStatus: () => isRetryableHttpStatus2,
|
|
13315
|
+
isFetchTimeout: () => isFetchTimeout2,
|
|
13329
13316
|
formatHttpError: () => formatHttpError2,
|
|
13330
|
-
describeFetchError: () => describeFetchError2
|
|
13317
|
+
describeFetchError: () => describeFetchError2,
|
|
13318
|
+
HttpStatusError: () => HttpStatusError2,
|
|
13319
|
+
HttpRequestTimeoutError: () => HttpRequestTimeoutError2,
|
|
13320
|
+
DEFAULT_HTTP_REQUEST_TIMEOUT_MS: () => DEFAULT_HTTP_REQUEST_TIMEOUT_MS2
|
|
13331
13321
|
});
|
|
13332
13322
|
function isHtmlContent2(text) {
|
|
13333
13323
|
const trimmed = text.trim();
|
|
@@ -13339,31 +13329,48 @@ function formatHttpError2(operation, status, responseBody, statusText) {
|
|
|
13339
13329
|
${AUTH_REMEDIATION_HINT2}` : "";
|
|
13340
13330
|
let bodyMessage;
|
|
13341
13331
|
let bodyDetails;
|
|
13332
|
+
let bodyCode;
|
|
13333
|
+
let bodyHint;
|
|
13342
13334
|
if (responseBody && !isHtmlContent2(responseBody)) {
|
|
13343
13335
|
try {
|
|
13344
13336
|
const errObj = JSON.parse(responseBody);
|
|
13345
13337
|
const message = errObj.message ?? errObj.error;
|
|
13346
13338
|
if (typeof message === "string" && message.trim().length > 0) {
|
|
13347
|
-
bodyMessage = message.trim();
|
|
13339
|
+
bodyMessage = redactTextSecrets2(message.trim());
|
|
13348
13340
|
}
|
|
13349
13341
|
const details = errObj.details ?? errObj.detail;
|
|
13350
13342
|
if (typeof details === "string" && details.trim().length > 0) {
|
|
13351
|
-
bodyDetails = details.trim();
|
|
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
|
-
bodyDetails = trimmed;
|
|
13357
|
+
bodyDetails = redactTextSecrets2(trimmed);
|
|
13357
13358
|
}
|
|
13358
13359
|
}
|
|
13359
13360
|
}
|
|
13360
13361
|
const trimmedReason = statusText?.trim();
|
|
13361
13362
|
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
13362
|
-
const
|
|
13363
|
-
|
|
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}`;
|
|
13364
13367
|
if (bodyDetails && bodyDetails !== headline) {
|
|
13365
13368
|
errMsg += `
|
|
13366
13369
|
${bodyDetails}`;
|
|
13370
|
+
}
|
|
13371
|
+
if (bodyHint) {
|
|
13372
|
+
errMsg += `
|
|
13373
|
+
Hint: ${bodyHint}`;
|
|
13367
13374
|
}
|
|
13368
13375
|
return errMsg + remediation;
|
|
13369
13376
|
}
|
|
@@ -13372,6 +13379,18 @@ function describeFetchError2(operation, url, err) {
|
|
|
13372
13379
|
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
13373
13380
|
return `${operation}: could not reach ${url} (${detail})`;
|
|
13374
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
|
+
}
|
|
13375
13394
|
function maskSecret2(secret) {
|
|
13376
13395
|
if (!secret)
|
|
13377
13396
|
return "";
|
|
@@ -13381,27 +13400,34 @@ function maskSecret2(secret) {
|
|
|
13381
13400
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
13382
13401
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
13383
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
|
+
}
|
|
13384
13420
|
function redactSecretsForLog2(text) {
|
|
13385
13421
|
let parsed;
|
|
13386
13422
|
try {
|
|
13387
13423
|
parsed = JSON.parse(text);
|
|
13388
13424
|
} catch {
|
|
13389
|
-
return text;
|
|
13425
|
+
return redactTextSecrets2(text);
|
|
13390
13426
|
}
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
if (node && typeof node === "object") {
|
|
13396
|
-
const out = {};
|
|
13397
|
-
for (const [key, value] of Object.entries(node)) {
|
|
13398
|
-
out[key] = SENSITIVE_LOG_KEY2.test(key) && value != null ? "[REDACTED]" : walk(value);
|
|
13399
|
-
}
|
|
13400
|
-
return out;
|
|
13401
|
-
}
|
|
13402
|
-
return node;
|
|
13403
|
-
};
|
|
13404
|
-
return JSON.stringify(walk(parsed));
|
|
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]");
|
|
13405
13431
|
}
|
|
13406
13432
|
function normalizeBaseUrl2(value) {
|
|
13407
13433
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
@@ -13425,7 +13451,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
13425
13451
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13426
13452
|
};
|
|
13427
13453
|
}
|
|
13428
|
-
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2,
|
|
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;
|
|
13429
13455
|
var init_util = __esm(() => {
|
|
13430
13456
|
HTTP_STATUS_MESSAGES2 = {
|
|
13431
13457
|
400: "Bad Request",
|
|
@@ -13453,7 +13479,22 @@ var init_util = __esm(() => {
|
|
|
13453
13479
|
503: "Service Unavailable",
|
|
13454
13480
|
504: "Gateway Timeout"
|
|
13455
13481
|
};
|
|
13456
|
-
|
|
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;
|
|
13457
13498
|
});
|
|
13458
13499
|
|
|
13459
13500
|
// node_modules/commander/esm.mjs
|
|
@@ -13474,7 +13515,7 @@ var {
|
|
|
13474
13515
|
// package.json
|
|
13475
13516
|
var package_default = {
|
|
13476
13517
|
name: "postgresai",
|
|
13477
|
-
version: "0.16.0-dev.
|
|
13518
|
+
version: "0.16.0-dev.11",
|
|
13478
13519
|
description: "postgres_ai CLI",
|
|
13479
13520
|
license: "Apache-2.0",
|
|
13480
13521
|
private: false,
|
|
@@ -16285,11 +16326,11 @@ var safeLoadAll = renamed("safeLoadAll", "loadAll");
|
|
|
16285
16326
|
var safeDump = renamed("safeDump", "dump");
|
|
16286
16327
|
|
|
16287
16328
|
// bin/postgres-ai.ts
|
|
16288
|
-
import * as
|
|
16289
|
-
import * as
|
|
16329
|
+
import * as fs9 from "fs";
|
|
16330
|
+
import * as path7 from "path";
|
|
16290
16331
|
import * as os3 from "os";
|
|
16291
16332
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16292
|
-
import * as
|
|
16333
|
+
import * as crypto2 from "crypto";
|
|
16293
16334
|
|
|
16294
16335
|
// node_modules/pg/esm/index.mjs
|
|
16295
16336
|
var import_lib = __toESM(require_lib2(), 1);
|
|
@@ -16305,7 +16346,7 @@ var Result = import_lib.default.Result;
|
|
|
16305
16346
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16306
16347
|
var defaults = import_lib.default.defaults;
|
|
16307
16348
|
// package.json
|
|
16308
|
-
var version = "0.16.0-dev.
|
|
16349
|
+
var version = "0.16.0-dev.11";
|
|
16309
16350
|
var package_default2 = {
|
|
16310
16351
|
name: "postgresai",
|
|
16311
16352
|
version,
|
|
@@ -16467,31 +16508,48 @@ function formatHttpError(operation, status, responseBody, statusText) {
|
|
|
16467
16508
|
${AUTH_REMEDIATION_HINT}` : "";
|
|
16468
16509
|
let bodyMessage;
|
|
16469
16510
|
let bodyDetails;
|
|
16511
|
+
let bodyCode;
|
|
16512
|
+
let bodyHint;
|
|
16470
16513
|
if (responseBody && !isHtmlContent(responseBody)) {
|
|
16471
16514
|
try {
|
|
16472
16515
|
const errObj = JSON.parse(responseBody);
|
|
16473
16516
|
const message = errObj.message ?? errObj.error;
|
|
16474
16517
|
if (typeof message === "string" && message.trim().length > 0) {
|
|
16475
|
-
bodyMessage = message.trim();
|
|
16518
|
+
bodyMessage = redactTextSecrets(message.trim());
|
|
16476
16519
|
}
|
|
16477
16520
|
const details = errObj.details ?? errObj.detail;
|
|
16478
16521
|
if (typeof details === "string" && details.trim().length > 0) {
|
|
16479
|
-
bodyDetails = details.trim();
|
|
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));
|
|
16480
16532
|
}
|
|
16481
16533
|
} catch {
|
|
16482
16534
|
const trimmed = responseBody.trim();
|
|
16483
16535
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
16484
|
-
bodyDetails = trimmed;
|
|
16536
|
+
bodyDetails = redactTextSecrets(trimmed);
|
|
16485
16537
|
}
|
|
16486
16538
|
}
|
|
16487
16539
|
}
|
|
16488
16540
|
const trimmedReason = statusText?.trim();
|
|
16489
16541
|
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
16490
|
-
const
|
|
16491
|
-
|
|
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}`;
|
|
16492
16546
|
if (bodyDetails && bodyDetails !== headline) {
|
|
16493
16547
|
errMsg += `
|
|
16494
16548
|
${bodyDetails}`;
|
|
16549
|
+
}
|
|
16550
|
+
if (bodyHint) {
|
|
16551
|
+
errMsg += `
|
|
16552
|
+
Hint: ${bodyHint}`;
|
|
16495
16553
|
}
|
|
16496
16554
|
return errMsg + remediation;
|
|
16497
16555
|
}
|
|
@@ -16500,6 +16558,35 @@ function describeFetchError(operation, url, err) {
|
|
|
16500
16558
|
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
16501
16559
|
return `${operation}: could not reach ${url} (${detail})`;
|
|
16502
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
|
+
}
|
|
16503
16590
|
function maskSecret(secret) {
|
|
16504
16591
|
if (!secret)
|
|
16505
16592
|
return "";
|
|
@@ -16509,28 +16596,36 @@ function maskSecret(secret) {
|
|
|
16509
16596
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
16510
16597
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
16511
16598
|
}
|
|
16512
|
-
|
|
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
|
+
}
|
|
16513
16616
|
function redactSecretsForLog(text) {
|
|
16514
16617
|
let parsed;
|
|
16515
16618
|
try {
|
|
16516
16619
|
parsed = JSON.parse(text);
|
|
16517
16620
|
} catch {
|
|
16518
|
-
return text;
|
|
16621
|
+
return redactTextSecrets(text);
|
|
16519
16622
|
}
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
for (const [key, value] of Object.entries(node)) {
|
|
16527
|
-
out[key] = SENSITIVE_LOG_KEY.test(key) && value != null ? "[REDACTED]" : walk(value);
|
|
16528
|
-
}
|
|
16529
|
-
return out;
|
|
16530
|
-
}
|
|
16531
|
-
return node;
|
|
16532
|
-
};
|
|
16533
|
-
return JSON.stringify(walk(parsed));
|
|
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]");
|
|
16534
16629
|
}
|
|
16535
16630
|
function normalizeBaseUrl(value) {
|
|
16536
16631
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
@@ -17363,611 +17458,9 @@ async function fetchReportFileData(params) {
|
|
|
17363
17458
|
}
|
|
17364
17459
|
}
|
|
17365
17460
|
|
|
17366
|
-
// lib/
|
|
17461
|
+
// lib/storage.ts
|
|
17367
17462
|
import * as fs3 from "fs";
|
|
17368
17463
|
import * as path3 from "path";
|
|
17369
|
-
import * as crypto from "node:crypto";
|
|
17370
|
-
var JOE_COMMANDS = [
|
|
17371
|
-
"plan",
|
|
17372
|
-
"explain",
|
|
17373
|
-
"exec",
|
|
17374
|
-
"hypo",
|
|
17375
|
-
"activity",
|
|
17376
|
-
"terminate",
|
|
17377
|
-
"reset",
|
|
17378
|
-
"describe"
|
|
17379
|
-
];
|
|
17380
|
-
var TERMINAL_STATES = new Set([
|
|
17381
|
-
"done",
|
|
17382
|
-
"error",
|
|
17383
|
-
"timed_out"
|
|
17384
|
-
]);
|
|
17385
|
-
var AI_ENABLED_COMMANDS = new Set([
|
|
17386
|
-
"plan",
|
|
17387
|
-
"explain",
|
|
17388
|
-
"exec",
|
|
17389
|
-
"hypo",
|
|
17390
|
-
"activity",
|
|
17391
|
-
"describe"
|
|
17392
|
-
]);
|
|
17393
|
-
var EXECUTING_COMMANDS = new Set([
|
|
17394
|
-
"exec",
|
|
17395
|
-
"explain"
|
|
17396
|
-
]);
|
|
17397
|
-
var DEFAULT_BUDGET_MS = 25000;
|
|
17398
|
-
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
17399
|
-
async function callRpc(params) {
|
|
17400
|
-
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
17401
|
-
if (!apiKey) {
|
|
17402
|
-
throw new Error("API key is required");
|
|
17403
|
-
}
|
|
17404
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17405
|
-
const url = new URL(`${base}/rpc/${fn}`);
|
|
17406
|
-
const payload = JSON.stringify(body);
|
|
17407
|
-
const headers = {
|
|
17408
|
-
"access-token": apiKey,
|
|
17409
|
-
"Content-Type": "application/json",
|
|
17410
|
-
Connection: "close"
|
|
17411
|
-
};
|
|
17412
|
-
if (debug) {
|
|
17413
|
-
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17414
|
-
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17415
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17416
|
-
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
17417
|
-
}
|
|
17418
|
-
let response;
|
|
17419
|
-
try {
|
|
17420
|
-
response = await fetch(url.toString(), {
|
|
17421
|
-
method: "POST",
|
|
17422
|
-
headers,
|
|
17423
|
-
body: payload
|
|
17424
|
-
});
|
|
17425
|
-
} catch (err) {
|
|
17426
|
-
throw new Error(describeFetchError(operation, base, err));
|
|
17427
|
-
}
|
|
17428
|
-
const text = await response.text();
|
|
17429
|
-
if (debug) {
|
|
17430
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
17431
|
-
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
17432
|
-
}
|
|
17433
|
-
if (!response.ok) {
|
|
17434
|
-
throw new Error(formatHttpError(operation, response.status, text, response.statusText));
|
|
17435
|
-
}
|
|
17436
|
-
try {
|
|
17437
|
-
return JSON.parse(text);
|
|
17438
|
-
} catch {
|
|
17439
|
-
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17440
|
-
}
|
|
17441
|
-
}
|
|
17442
|
-
async function submitCommand(params) {
|
|
17443
|
-
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
17444
|
-
if (!JOE_COMMANDS.includes(command)) {
|
|
17445
|
-
throw new Error(`Unknown Joe command: ${command}`);
|
|
17446
|
-
}
|
|
17447
|
-
const body = {
|
|
17448
|
-
project_id: projectId,
|
|
17449
|
-
command,
|
|
17450
|
-
sql: sql ?? null,
|
|
17451
|
-
args: args ?? null,
|
|
17452
|
-
session_id: sessionId ?? null
|
|
17453
|
-
};
|
|
17454
|
-
if (idempotencyKey) {
|
|
17455
|
-
body.idempotency_key = idempotencyKey;
|
|
17456
|
-
}
|
|
17457
|
-
return callRpc({
|
|
17458
|
-
apiKey,
|
|
17459
|
-
apiBaseUrl,
|
|
17460
|
-
fn: "joe_command_submit",
|
|
17461
|
-
body,
|
|
17462
|
-
operation: `Failed to submit ${command} command`,
|
|
17463
|
-
debug
|
|
17464
|
-
});
|
|
17465
|
-
}
|
|
17466
|
-
async function getCommandStatus(params) {
|
|
17467
|
-
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17468
|
-
if (!commandId) {
|
|
17469
|
-
throw new Error("commandId is required");
|
|
17470
|
-
}
|
|
17471
|
-
return callRpc({
|
|
17472
|
-
apiKey,
|
|
17473
|
-
apiBaseUrl,
|
|
17474
|
-
fn: "joe_command_status",
|
|
17475
|
-
body: { command_id: commandId },
|
|
17476
|
-
operation: "Failed to fetch command status",
|
|
17477
|
-
debug
|
|
17478
|
-
});
|
|
17479
|
-
}
|
|
17480
|
-
async function getCommandResult(params) {
|
|
17481
|
-
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17482
|
-
if (!commandId) {
|
|
17483
|
-
throw new Error("commandId is required");
|
|
17484
|
-
}
|
|
17485
|
-
return callRpc({
|
|
17486
|
-
apiKey,
|
|
17487
|
-
apiBaseUrl,
|
|
17488
|
-
fn: "joe_command_result",
|
|
17489
|
-
body: { command_id: commandId },
|
|
17490
|
-
operation: "Failed to fetch command result",
|
|
17491
|
-
debug
|
|
17492
|
-
});
|
|
17493
|
-
}
|
|
17494
|
-
function normalizeProjectRow(row) {
|
|
17495
|
-
const projectId = row.project_id ?? row.id ?? 0;
|
|
17496
|
-
return {
|
|
17497
|
-
project_id: Number(projectId),
|
|
17498
|
-
alias: row.alias ?? null,
|
|
17499
|
-
name: row.name ?? null,
|
|
17500
|
-
label: row.label ?? null,
|
|
17501
|
-
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
17502
|
-
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
17503
|
-
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
17504
|
-
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
|
|
17505
|
-
};
|
|
17506
|
-
}
|
|
17507
|
-
async function listProjects(params) {
|
|
17508
|
-
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17509
|
-
const body = {};
|
|
17510
|
-
if (typeof orgId === "number") {
|
|
17511
|
-
body.org_id = orgId;
|
|
17512
|
-
}
|
|
17513
|
-
const rows = await callRpc({
|
|
17514
|
-
apiKey,
|
|
17515
|
-
apiBaseUrl,
|
|
17516
|
-
fn: "projects_list",
|
|
17517
|
-
body,
|
|
17518
|
-
operation: "Failed to list projects",
|
|
17519
|
-
debug
|
|
17520
|
-
});
|
|
17521
|
-
if (!Array.isArray(rows)) {
|
|
17522
|
-
return [];
|
|
17523
|
-
}
|
|
17524
|
-
return rows.map(normalizeProjectRow);
|
|
17525
|
-
}
|
|
17526
|
-
function isNumericProjectRef(ref) {
|
|
17527
|
-
return /^[0-9]+$/.test(ref.trim());
|
|
17528
|
-
}
|
|
17529
|
-
async function resolveProjectId(params) {
|
|
17530
|
-
const ref = String(params.project ?? "").trim();
|
|
17531
|
-
if (!ref) {
|
|
17532
|
-
throw new Error("project is required (--project <id|alias>)");
|
|
17533
|
-
}
|
|
17534
|
-
if (isNumericProjectRef(ref)) {
|
|
17535
|
-
return Number(ref);
|
|
17536
|
-
}
|
|
17537
|
-
const projects = await listProjects({
|
|
17538
|
-
apiKey: params.apiKey,
|
|
17539
|
-
apiBaseUrl: params.apiBaseUrl,
|
|
17540
|
-
orgId: params.orgId,
|
|
17541
|
-
debug: params.debug
|
|
17542
|
-
});
|
|
17543
|
-
const needle = ref.toLowerCase();
|
|
17544
|
-
const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle);
|
|
17545
|
-
if (!match) {
|
|
17546
|
-
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17547
|
-
}
|
|
17548
|
-
return match.project_id;
|
|
17549
|
-
}
|
|
17550
|
-
function sessionStorePath(dir) {
|
|
17551
|
-
return path3.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
17552
|
-
}
|
|
17553
|
-
function readSessionStore(dir) {
|
|
17554
|
-
const file = sessionStorePath(dir);
|
|
17555
|
-
if (!fs3.existsSync(file)) {
|
|
17556
|
-
return {};
|
|
17557
|
-
}
|
|
17558
|
-
try {
|
|
17559
|
-
const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
17560
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17561
|
-
return parsed;
|
|
17562
|
-
}
|
|
17563
|
-
} catch {}
|
|
17564
|
-
return {};
|
|
17565
|
-
}
|
|
17566
|
-
function readStoredSessionId(projectId, dir) {
|
|
17567
|
-
const store = readSessionStore(dir);
|
|
17568
|
-
const value = store[String(projectId)];
|
|
17569
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
17570
|
-
}
|
|
17571
|
-
function writeStoredSessionId(projectId, sessionId, dir) {
|
|
17572
|
-
const baseDir = dir ?? getConfigDir2();
|
|
17573
|
-
if (!fs3.existsSync(baseDir)) {
|
|
17574
|
-
fs3.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
17575
|
-
}
|
|
17576
|
-
const store = readSessionStore(dir);
|
|
17577
|
-
store[String(projectId)] = sessionId;
|
|
17578
|
-
fs3.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
|
|
17579
|
-
`, { mode: 384 });
|
|
17580
|
-
}
|
|
17581
|
-
function clearStoredSessionId(projectId, dir) {
|
|
17582
|
-
const file = sessionStorePath(dir);
|
|
17583
|
-
if (!fs3.existsSync(file)) {
|
|
17584
|
-
return;
|
|
17585
|
-
}
|
|
17586
|
-
const store = readSessionStore(dir);
|
|
17587
|
-
if (String(projectId) in store) {
|
|
17588
|
-
delete store[String(projectId)];
|
|
17589
|
-
fs3.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
17590
|
-
`, { mode: 384 });
|
|
17591
|
-
}
|
|
17592
|
-
}
|
|
17593
|
-
function computeIdempotencyKey(command, projectId, sql, args) {
|
|
17594
|
-
if (EXECUTING_COMMANDS.has(command)) {
|
|
17595
|
-
const canonical = `${projectId}
|
|
17596
|
-
${command}
|
|
17597
|
-
${sql ?? ""}
|
|
17598
|
-
${JSON.stringify(args ?? null)}`;
|
|
17599
|
-
return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
|
|
17600
|
-
}
|
|
17601
|
-
return `joe-${crypto.randomUUID()}`;
|
|
17602
|
-
}
|
|
17603
|
-
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17604
|
-
async function runCommand(params) {
|
|
17605
|
-
const {
|
|
17606
|
-
apiKey,
|
|
17607
|
-
apiBaseUrl,
|
|
17608
|
-
command,
|
|
17609
|
-
projectId,
|
|
17610
|
-
sql,
|
|
17611
|
-
args,
|
|
17612
|
-
sessionId,
|
|
17613
|
-
debug
|
|
17614
|
-
} = params;
|
|
17615
|
-
const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) ? params.budgetMs : DEFAULT_BUDGET_MS;
|
|
17616
|
-
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
17617
|
-
const now = params.now ?? Date.now;
|
|
17618
|
-
const sleep = params.sleep ?? defaultSleep;
|
|
17619
|
-
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
|
|
17620
|
-
const submitted = await submitCommand({
|
|
17621
|
-
apiKey,
|
|
17622
|
-
apiBaseUrl,
|
|
17623
|
-
command,
|
|
17624
|
-
projectId,
|
|
17625
|
-
sql,
|
|
17626
|
-
args,
|
|
17627
|
-
sessionId,
|
|
17628
|
-
idempotencyKey,
|
|
17629
|
-
debug
|
|
17630
|
-
});
|
|
17631
|
-
const commandId = submitted.command_id;
|
|
17632
|
-
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
17633
|
-
const deadline = now() + budgetMs;
|
|
17634
|
-
let status = submitted.status;
|
|
17635
|
-
while (true) {
|
|
17636
|
-
const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
|
|
17637
|
-
status = statusResp.status;
|
|
17638
|
-
if (TERMINAL_STATES.has(status)) {
|
|
17639
|
-
break;
|
|
17640
|
-
}
|
|
17641
|
-
if (now() >= deadline) {
|
|
17642
|
-
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
17643
|
-
}
|
|
17644
|
-
await sleep(pollIntervalMs);
|
|
17645
|
-
}
|
|
17646
|
-
const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
|
|
17647
|
-
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
17648
|
-
}
|
|
17649
|
-
async function executeJoeCommand(params) {
|
|
17650
|
-
const projectId = await resolveProjectId({
|
|
17651
|
-
apiKey: params.apiKey,
|
|
17652
|
-
apiBaseUrl: params.apiBaseUrl,
|
|
17653
|
-
project: params.project,
|
|
17654
|
-
orgId: params.orgId,
|
|
17655
|
-
debug: params.debug
|
|
17656
|
-
});
|
|
17657
|
-
let sessionId;
|
|
17658
|
-
if (params.newSession) {
|
|
17659
|
-
clearStoredSessionId(projectId, params.sessionDir);
|
|
17660
|
-
sessionId = null;
|
|
17661
|
-
} else if (params.session) {
|
|
17662
|
-
sessionId = String(params.session);
|
|
17663
|
-
} else {
|
|
17664
|
-
sessionId = readStoredSessionId(projectId, params.sessionDir);
|
|
17665
|
-
}
|
|
17666
|
-
const outcome = await runCommand({
|
|
17667
|
-
apiKey: params.apiKey,
|
|
17668
|
-
apiBaseUrl: params.apiBaseUrl,
|
|
17669
|
-
command: params.command,
|
|
17670
|
-
projectId,
|
|
17671
|
-
sql: params.sql,
|
|
17672
|
-
args: params.args,
|
|
17673
|
-
sessionId,
|
|
17674
|
-
budgetMs: params.budgetMs,
|
|
17675
|
-
pollIntervalMs: params.pollIntervalMs,
|
|
17676
|
-
debug: params.debug,
|
|
17677
|
-
now: params.now,
|
|
17678
|
-
sleep: params.sleep
|
|
17679
|
-
});
|
|
17680
|
-
if (outcome.sessionId) {
|
|
17681
|
-
writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
|
|
17682
|
-
}
|
|
17683
|
-
return { ...outcome, command: params.command, projectId };
|
|
17684
|
-
}
|
|
17685
|
-
|
|
17686
|
-
// lib/dblab.ts
|
|
17687
|
-
function isNumericProjectRef2(ref) {
|
|
17688
|
-
return /^[0-9]+$/.test(ref.trim());
|
|
17689
|
-
}
|
|
17690
|
-
async function resolveDblabInstanceId(params) {
|
|
17691
|
-
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17692
|
-
if (!apiKey) {
|
|
17693
|
-
throw new Error("API key is required");
|
|
17694
|
-
}
|
|
17695
|
-
const ref = String(params.project ?? "").trim();
|
|
17696
|
-
if (!ref) {
|
|
17697
|
-
throw new Error("project is required (--project <id|alias>)");
|
|
17698
|
-
}
|
|
17699
|
-
let projects;
|
|
17700
|
-
try {
|
|
17701
|
-
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
17702
|
-
} catch (err) {
|
|
17703
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
17704
|
-
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
17705
|
-
}
|
|
17706
|
-
const numeric = isNumericProjectRef2(ref);
|
|
17707
|
-
const needle = ref.toLowerCase();
|
|
17708
|
-
const match = projects.find((p) => {
|
|
17709
|
-
if (numeric) {
|
|
17710
|
-
return String(p.project_id) === ref;
|
|
17711
|
-
}
|
|
17712
|
-
return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
|
|
17713
|
-
});
|
|
17714
|
-
if (!match) {
|
|
17715
|
-
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17716
|
-
}
|
|
17717
|
-
if (match.dblab_instance_id == null) {
|
|
17718
|
-
throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
|
|
17719
|
-
}
|
|
17720
|
-
return String(match.dblab_instance_id);
|
|
17721
|
-
}
|
|
17722
|
-
async function callDblabApi(params) {
|
|
17723
|
-
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
17724
|
-
if (!apiKey) {
|
|
17725
|
-
throw new Error("API key is required");
|
|
17726
|
-
}
|
|
17727
|
-
if (!instanceId) {
|
|
17728
|
-
throw new Error("instanceId is required");
|
|
17729
|
-
}
|
|
17730
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17731
|
-
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
17732
|
-
const bodyObj = {
|
|
17733
|
-
instance_id: instanceId,
|
|
17734
|
-
action,
|
|
17735
|
-
method
|
|
17736
|
-
};
|
|
17737
|
-
if (data !== undefined) {
|
|
17738
|
-
bodyObj.data = data;
|
|
17739
|
-
}
|
|
17740
|
-
const body = JSON.stringify(bodyObj);
|
|
17741
|
-
const headers = {
|
|
17742
|
-
"access-token": apiKey,
|
|
17743
|
-
"Content-Type": "application/json",
|
|
17744
|
-
Connection: "close"
|
|
17745
|
-
};
|
|
17746
|
-
if (debug) {
|
|
17747
|
-
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17748
|
-
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17749
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17750
|
-
console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
|
|
17751
|
-
}
|
|
17752
|
-
let response;
|
|
17753
|
-
try {
|
|
17754
|
-
response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
17755
|
-
} catch (err) {
|
|
17756
|
-
throw new Error(describeFetchError(operation, base, err));
|
|
17757
|
-
}
|
|
17758
|
-
const text = await response.text();
|
|
17759
|
-
if (debug) {
|
|
17760
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
17761
|
-
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
17762
|
-
}
|
|
17763
|
-
if (!response.ok) {
|
|
17764
|
-
throw new Error(formatHttpError(operation, response.status, text, response.statusText));
|
|
17765
|
-
}
|
|
17766
|
-
if (text.trim() === "") {
|
|
17767
|
-
return null;
|
|
17768
|
-
}
|
|
17769
|
-
try {
|
|
17770
|
-
return JSON.parse(text);
|
|
17771
|
-
} catch {
|
|
17772
|
-
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17773
|
-
}
|
|
17774
|
-
}
|
|
17775
|
-
async function createClone(params) {
|
|
17776
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
17777
|
-
const data = { protected: Boolean(isProtected) };
|
|
17778
|
-
if (cloneId)
|
|
17779
|
-
data.id = cloneId;
|
|
17780
|
-
if (branch)
|
|
17781
|
-
data.branch = branch;
|
|
17782
|
-
if (snapshotId)
|
|
17783
|
-
data.snapshot = { id: snapshotId };
|
|
17784
|
-
if (dbUser && dbPassword)
|
|
17785
|
-
data.db = { username: dbUser, password: dbPassword };
|
|
17786
|
-
return callDblabApi({
|
|
17787
|
-
apiKey,
|
|
17788
|
-
apiBaseUrl,
|
|
17789
|
-
instanceId,
|
|
17790
|
-
action: "/clone",
|
|
17791
|
-
method: "post",
|
|
17792
|
-
data,
|
|
17793
|
-
operation: "Failed to create clone",
|
|
17794
|
-
debug
|
|
17795
|
-
});
|
|
17796
|
-
}
|
|
17797
|
-
async function listClones(params) {
|
|
17798
|
-
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17799
|
-
return callDblabApi({
|
|
17800
|
-
apiKey,
|
|
17801
|
-
apiBaseUrl,
|
|
17802
|
-
instanceId,
|
|
17803
|
-
action: "/clones",
|
|
17804
|
-
method: "get",
|
|
17805
|
-
operation: "Failed to list clones",
|
|
17806
|
-
debug
|
|
17807
|
-
});
|
|
17808
|
-
}
|
|
17809
|
-
async function getClone(params) {
|
|
17810
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17811
|
-
if (!cloneId)
|
|
17812
|
-
throw new Error("cloneId is required");
|
|
17813
|
-
return callDblabApi({
|
|
17814
|
-
apiKey,
|
|
17815
|
-
apiBaseUrl,
|
|
17816
|
-
instanceId,
|
|
17817
|
-
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17818
|
-
method: "get",
|
|
17819
|
-
operation: "Failed to get clone",
|
|
17820
|
-
debug
|
|
17821
|
-
});
|
|
17822
|
-
}
|
|
17823
|
-
async function resetClone(params) {
|
|
17824
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
17825
|
-
if (!cloneId)
|
|
17826
|
-
throw new Error("cloneId is required");
|
|
17827
|
-
const data = { latest: latest ?? !snapshotId };
|
|
17828
|
-
if (snapshotId)
|
|
17829
|
-
data.snapshotID = snapshotId;
|
|
17830
|
-
return callDblabApi({
|
|
17831
|
-
apiKey,
|
|
17832
|
-
apiBaseUrl,
|
|
17833
|
-
instanceId,
|
|
17834
|
-
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
17835
|
-
method: "post",
|
|
17836
|
-
data,
|
|
17837
|
-
operation: "Failed to reset clone",
|
|
17838
|
-
debug
|
|
17839
|
-
});
|
|
17840
|
-
}
|
|
17841
|
-
async function destroyClone(params) {
|
|
17842
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17843
|
-
if (!cloneId)
|
|
17844
|
-
throw new Error("cloneId is required");
|
|
17845
|
-
return callDblabApi({
|
|
17846
|
-
apiKey,
|
|
17847
|
-
apiBaseUrl,
|
|
17848
|
-
instanceId,
|
|
17849
|
-
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17850
|
-
method: "delete",
|
|
17851
|
-
operation: "Failed to destroy clone",
|
|
17852
|
-
debug
|
|
17853
|
-
});
|
|
17854
|
-
}
|
|
17855
|
-
async function listBranches(params) {
|
|
17856
|
-
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17857
|
-
return callDblabApi({
|
|
17858
|
-
apiKey,
|
|
17859
|
-
apiBaseUrl,
|
|
17860
|
-
instanceId,
|
|
17861
|
-
action: "/branches",
|
|
17862
|
-
method: "get",
|
|
17863
|
-
operation: "Failed to list branches",
|
|
17864
|
-
debug
|
|
17865
|
-
});
|
|
17866
|
-
}
|
|
17867
|
-
async function createBranch(params) {
|
|
17868
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
17869
|
-
if (!branchName)
|
|
17870
|
-
throw new Error("branchName is required");
|
|
17871
|
-
const data = { branchName };
|
|
17872
|
-
if (baseBranch)
|
|
17873
|
-
data.baseBranch = baseBranch;
|
|
17874
|
-
if (snapshotId)
|
|
17875
|
-
data.snapshotID = snapshotId;
|
|
17876
|
-
return callDblabApi({
|
|
17877
|
-
apiKey,
|
|
17878
|
-
apiBaseUrl,
|
|
17879
|
-
instanceId,
|
|
17880
|
-
action: "/branch",
|
|
17881
|
-
method: "post",
|
|
17882
|
-
data,
|
|
17883
|
-
operation: "Failed to create branch",
|
|
17884
|
-
debug
|
|
17885
|
-
});
|
|
17886
|
-
}
|
|
17887
|
-
async function deleteBranch(params) {
|
|
17888
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17889
|
-
if (!branchName)
|
|
17890
|
-
throw new Error("branchName is required");
|
|
17891
|
-
return callDblabApi({
|
|
17892
|
-
apiKey,
|
|
17893
|
-
apiBaseUrl,
|
|
17894
|
-
instanceId,
|
|
17895
|
-
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
17896
|
-
method: "delete",
|
|
17897
|
-
operation: "Failed to delete branch",
|
|
17898
|
-
debug
|
|
17899
|
-
});
|
|
17900
|
-
}
|
|
17901
|
-
async function branchLog(params) {
|
|
17902
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17903
|
-
if (!branchName)
|
|
17904
|
-
throw new Error("branchName is required");
|
|
17905
|
-
return callDblabApi({
|
|
17906
|
-
apiKey,
|
|
17907
|
-
apiBaseUrl,
|
|
17908
|
-
instanceId,
|
|
17909
|
-
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
17910
|
-
method: "get",
|
|
17911
|
-
operation: "Failed to fetch branch log",
|
|
17912
|
-
debug
|
|
17913
|
-
});
|
|
17914
|
-
}
|
|
17915
|
-
async function listSnapshots(params) {
|
|
17916
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
17917
|
-
const qs = new URLSearchParams;
|
|
17918
|
-
const branch = branchName?.trim();
|
|
17919
|
-
if (branch)
|
|
17920
|
-
qs.append("branch", branch);
|
|
17921
|
-
if (dataset)
|
|
17922
|
-
qs.append("dataset", dataset);
|
|
17923
|
-
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
17924
|
-
return callDblabApi({
|
|
17925
|
-
apiKey,
|
|
17926
|
-
apiBaseUrl,
|
|
17927
|
-
instanceId,
|
|
17928
|
-
action,
|
|
17929
|
-
method: "get",
|
|
17930
|
-
operation: "Failed to list snapshots",
|
|
17931
|
-
debug
|
|
17932
|
-
});
|
|
17933
|
-
}
|
|
17934
|
-
async function createSnapshot(params) {
|
|
17935
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
17936
|
-
if (!cloneId)
|
|
17937
|
-
throw new Error("cloneId is required");
|
|
17938
|
-
const data = { cloneID: cloneId };
|
|
17939
|
-
if (message)
|
|
17940
|
-
data.message = message;
|
|
17941
|
-
return callDblabApi({
|
|
17942
|
-
apiKey,
|
|
17943
|
-
apiBaseUrl,
|
|
17944
|
-
instanceId,
|
|
17945
|
-
action: "/branch/snapshot",
|
|
17946
|
-
method: "post",
|
|
17947
|
-
data,
|
|
17948
|
-
operation: "Failed to create snapshot",
|
|
17949
|
-
debug
|
|
17950
|
-
});
|
|
17951
|
-
}
|
|
17952
|
-
async function destroySnapshot(params) {
|
|
17953
|
-
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
17954
|
-
if (!snapshotId)
|
|
17955
|
-
throw new Error("snapshotId is required");
|
|
17956
|
-
const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
|
|
17957
|
-
return callDblabApi({
|
|
17958
|
-
apiKey,
|
|
17959
|
-
apiBaseUrl,
|
|
17960
|
-
instanceId,
|
|
17961
|
-
action,
|
|
17962
|
-
method: "delete",
|
|
17963
|
-
operation: "Failed to destroy snapshot",
|
|
17964
|
-
debug
|
|
17965
|
-
});
|
|
17966
|
-
}
|
|
17967
|
-
|
|
17968
|
-
// lib/storage.ts
|
|
17969
|
-
import * as fs4 from "fs";
|
|
17970
|
-
import * as path4 from "path";
|
|
17971
17464
|
var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
|
|
17972
17465
|
var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
|
|
17973
17466
|
var MIME_TYPES = {
|
|
@@ -18008,11 +17501,11 @@ async function uploadFile(params) {
|
|
|
18008
17501
|
if (!filePath) {
|
|
18009
17502
|
throw new Error("filePath is required");
|
|
18010
17503
|
}
|
|
18011
|
-
const resolvedPath =
|
|
18012
|
-
if (!
|
|
17504
|
+
const resolvedPath = path3.resolve(filePath);
|
|
17505
|
+
if (!fs3.existsSync(resolvedPath)) {
|
|
18013
17506
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
18014
17507
|
}
|
|
18015
|
-
const stat =
|
|
17508
|
+
const stat = fs3.statSync(resolvedPath);
|
|
18016
17509
|
if (!stat.isFile()) {
|
|
18017
17510
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
18018
17511
|
}
|
|
@@ -18024,9 +17517,9 @@ async function uploadFile(params) {
|
|
|
18024
17517
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
18025
17518
|
}
|
|
18026
17519
|
const url = `${base}/upload`;
|
|
18027
|
-
const fileBuffer =
|
|
18028
|
-
const fileName =
|
|
18029
|
-
const ext =
|
|
17520
|
+
const fileBuffer = fs3.readFileSync(resolvedPath);
|
|
17521
|
+
const fileName = path3.basename(resolvedPath);
|
|
17522
|
+
const ext = path3.extname(fileName).toLowerCase();
|
|
18030
17523
|
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
|
18031
17524
|
const formData = new FormData;
|
|
18032
17525
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -18085,15 +17578,15 @@ async function downloadFile(params) {
|
|
|
18085
17578
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
18086
17579
|
fullUrl = `${base}${relativePath}`;
|
|
18087
17580
|
}
|
|
18088
|
-
const urlFilename =
|
|
17581
|
+
const urlFilename = path3.basename(new URL(fullUrl).pathname);
|
|
18089
17582
|
if (!urlFilename) {
|
|
18090
17583
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
18091
17584
|
}
|
|
18092
|
-
const saveTo = outputPath ?
|
|
17585
|
+
const saveTo = outputPath ? path3.resolve(outputPath) : path3.resolve(urlFilename);
|
|
18093
17586
|
if (!outputPath) {
|
|
18094
|
-
const normalizedSave =
|
|
18095
|
-
const cwd =
|
|
18096
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
17587
|
+
const normalizedSave = path3.normalize(saveTo);
|
|
17588
|
+
const cwd = path3.normalize(process.cwd());
|
|
17589
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path3.sep)) {
|
|
18097
17590
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
18098
17591
|
}
|
|
18099
17592
|
}
|
|
@@ -18125,11 +17618,11 @@ async function downloadFile(params) {
|
|
|
18125
17618
|
}
|
|
18126
17619
|
const arrayBuffer = await response.arrayBuffer();
|
|
18127
17620
|
const buffer = Buffer.from(arrayBuffer);
|
|
18128
|
-
const parentDir =
|
|
18129
|
-
if (!
|
|
18130
|
-
|
|
17621
|
+
const parentDir = path3.dirname(saveTo);
|
|
17622
|
+
if (!fs3.existsSync(parentDir)) {
|
|
17623
|
+
fs3.mkdirSync(parentDir, { recursive: true });
|
|
18131
17624
|
}
|
|
18132
|
-
|
|
17625
|
+
fs3.writeFileSync(saveTo, buffer);
|
|
18133
17626
|
return {
|
|
18134
17627
|
savedTo: saveTo,
|
|
18135
17628
|
size: buffer.length,
|
|
@@ -18141,9 +17634,9 @@ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
|
|
|
18141
17634
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
18142
17635
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
18143
17636
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
18144
|
-
const name = filename ||
|
|
17637
|
+
const name = filename || path3.basename(new URL(fullUrl).pathname);
|
|
18145
17638
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
18146
|
-
const ext =
|
|
17639
|
+
const ext = path3.extname(name).toLowerCase();
|
|
18147
17640
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
18148
17641
|
return ``;
|
|
18149
17642
|
}
|
|
@@ -18431,10 +17924,10 @@ function mergeDefs(...defs) {
|
|
|
18431
17924
|
function cloneDef(schema2) {
|
|
18432
17925
|
return mergeDefs(schema2._zod.def);
|
|
18433
17926
|
}
|
|
18434
|
-
function getElementAtPath(obj,
|
|
18435
|
-
if (!
|
|
17927
|
+
function getElementAtPath(obj, path4) {
|
|
17928
|
+
if (!path4)
|
|
18436
17929
|
return obj;
|
|
18437
|
-
return
|
|
17930
|
+
return path4.reduce((acc, key) => acc?.[key], obj);
|
|
18438
17931
|
}
|
|
18439
17932
|
function promiseAllObject(promisesObj) {
|
|
18440
17933
|
const keys = Object.keys(promisesObj);
|
|
@@ -18798,11 +18291,11 @@ function aborted(x, startIndex = 0) {
|
|
|
18798
18291
|
}
|
|
18799
18292
|
return false;
|
|
18800
18293
|
}
|
|
18801
|
-
function prefixIssues(
|
|
18294
|
+
function prefixIssues(path4, issues) {
|
|
18802
18295
|
return issues.map((iss) => {
|
|
18803
18296
|
var _a;
|
|
18804
18297
|
(_a = iss).path ?? (_a.path = []);
|
|
18805
|
-
iss.path.unshift(
|
|
18298
|
+
iss.path.unshift(path4);
|
|
18806
18299
|
return iss;
|
|
18807
18300
|
});
|
|
18808
18301
|
}
|
|
@@ -21671,7 +21164,7 @@ function finalize(ctx, schema2) {
|
|
|
21671
21164
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
21672
21165
|
} else if (ctx.target === "draft-04") {
|
|
21673
21166
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
21674
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
21167
|
+
} else if (ctx.target === "openapi-3.0") {} else {}
|
|
21675
21168
|
if (ctx.external?.uri) {
|
|
21676
21169
|
const id = ctx.external.registry.get(schema2)?.id;
|
|
21677
21170
|
if (!id)
|
|
@@ -21890,7 +21383,7 @@ var literalProcessor = (schema2, ctx, json2, _params) => {
|
|
|
21890
21383
|
if (val === undefined) {
|
|
21891
21384
|
if (ctx.unrepresentable === "throw") {
|
|
21892
21385
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
21893
|
-
}
|
|
21386
|
+
} else {}
|
|
21894
21387
|
} else if (typeof val === "bigint") {
|
|
21895
21388
|
if (ctx.unrepresentable === "throw") {
|
|
21896
21389
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -25074,211 +24567,6 @@ class StdioServerTransport {
|
|
|
25074
24567
|
// lib/mcp-server.ts
|
|
25075
24568
|
var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
25076
24569
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
25077
|
-
var JOE_TOOL_TO_COMMAND = {
|
|
25078
|
-
plan_query: "plan",
|
|
25079
|
-
explain_query: "explain",
|
|
25080
|
-
exec_sql: "exec",
|
|
25081
|
-
hypo_index: "hypo",
|
|
25082
|
-
activity: "activity",
|
|
25083
|
-
terminate_backend: "terminate",
|
|
25084
|
-
reset_clone: "reset",
|
|
25085
|
-
describe: "describe"
|
|
25086
|
-
};
|
|
25087
|
-
function joeToolDefinitions() {
|
|
25088
|
-
const sessionProps = {
|
|
25089
|
-
project_id: {
|
|
25090
|
-
type: "string",
|
|
25091
|
-
description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db."
|
|
25092
|
-
},
|
|
25093
|
-
session_id: {
|
|
25094
|
-
type: "string",
|
|
25095
|
-
description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted."
|
|
25096
|
-
},
|
|
25097
|
-
new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
|
|
25098
|
-
timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
|
|
25099
|
-
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
25100
|
-
};
|
|
25101
|
-
return [
|
|
25102
|
-
{
|
|
25103
|
-
name: "plan_query",
|
|
25104
|
-
description: "Plan a query (EXPLAIN, plan-only — NEVER executes; the fast/safe default). Returns a sanitized plan tree (literals redacted). LLM-facing: feeds the plan/stats to your LLM when the org's AI features are enabled. Requires joe:plan.",
|
|
25105
|
-
inputSchema: {
|
|
25106
|
-
type: "object",
|
|
25107
|
-
properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
|
|
25108
|
-
required: ["sql", "project_id"],
|
|
25109
|
-
additionalProperties: false
|
|
25110
|
-
}
|
|
25111
|
-
},
|
|
25112
|
-
{
|
|
25113
|
-
name: "explain_query",
|
|
25114
|
-
description: "EXPLAIN ANALYZE a query — EXECUTES it on the hardened, ephemeral DBLab clone and returns the measured, annotated plan (node types, costs, actual-rows/timings, buffers — literals redacted), NOT raw SELECT result rows. LLM-facing: feeds the measured plan/stats to your LLM when AI features are enabled. Requires joe:exec; subject to the org execution policy (read_only allows SELECT-only).",
|
|
25115
|
-
inputSchema: {
|
|
25116
|
-
type: "object",
|
|
25117
|
-
properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
|
|
25118
|
-
required: ["sql", "project_id"],
|
|
25119
|
-
additionalProperties: false
|
|
25120
|
-
}
|
|
25121
|
-
},
|
|
25122
|
-
{
|
|
25123
|
-
name: "exec_sql",
|
|
25124
|
-
description: "WARNING: executes arbitrary SQL (DDL/DML) on the disposable clone and CAN RETURN REAL (own-data) TABLE ROWS that will enter this agent's LLM context (subject to the agent row-cap / metadata-only minimization). Prefer plan_query (non-executing) when you only need a plan. Requires an explicit joe:exec grant and is subject to the org's joe_execution_policy.",
|
|
25125
|
-
inputSchema: {
|
|
25126
|
-
type: "object",
|
|
25127
|
-
properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
|
|
25128
|
-
required: ["sql", "project_id"],
|
|
25129
|
-
additionalProperties: false
|
|
25130
|
-
}
|
|
25131
|
-
},
|
|
25132
|
-
{
|
|
25133
|
-
name: "hypo_index",
|
|
25134
|
-
description: "HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
|
|
25135
|
-
inputSchema: {
|
|
25136
|
-
type: "object",
|
|
25137
|
-
properties: {
|
|
25138
|
-
sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
|
|
25139
|
-
query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
|
|
25140
|
-
...sessionProps
|
|
25141
|
-
},
|
|
25142
|
-
required: ["sql", "query", "project_id"],
|
|
25143
|
-
additionalProperties: false
|
|
25144
|
-
}
|
|
25145
|
-
},
|
|
25146
|
-
{
|
|
25147
|
-
name: "activity",
|
|
25148
|
-
description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
|
|
25149
|
-
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25150
|
-
},
|
|
25151
|
-
{
|
|
25152
|
-
name: "terminate_backend",
|
|
25153
|
-
description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25154
|
-
inputSchema: {
|
|
25155
|
-
type: "object",
|
|
25156
|
-
properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
|
|
25157
|
-
required: ["pid", "project_id"],
|
|
25158
|
-
additionalProperties: false
|
|
25159
|
-
}
|
|
25160
|
-
},
|
|
25161
|
-
{
|
|
25162
|
-
name: "reset_clone",
|
|
25163
|
-
description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25164
|
-
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25165
|
-
},
|
|
25166
|
-
{
|
|
25167
|
-
name: "describe",
|
|
25168
|
-
description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
|
|
25169
|
-
inputSchema: {
|
|
25170
|
-
type: "object",
|
|
25171
|
-
properties: {
|
|
25172
|
-
object: { type: "string", description: "Object to describe (e.g. users)." },
|
|
25173
|
-
variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
|
|
25174
|
-
...sessionProps
|
|
25175
|
-
},
|
|
25176
|
-
required: ["object", "project_id"],
|
|
25177
|
-
additionalProperties: false
|
|
25178
|
-
}
|
|
25179
|
-
},
|
|
25180
|
-
{
|
|
25181
|
-
name: "list_projects",
|
|
25182
|
-
description: "List the org's projects and which have Joe ready (org-level discovery — NOT a Joe endpoint; mirrors `pgai projects`). Use it to find the project_ids/aliases you may address with the Joe command tools.",
|
|
25183
|
-
inputSchema: {
|
|
25184
|
-
type: "object",
|
|
25185
|
-
properties: {
|
|
25186
|
-
org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
|
|
25187
|
-
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
25188
|
-
},
|
|
25189
|
-
additionalProperties: false
|
|
25190
|
-
}
|
|
25191
|
-
}
|
|
25192
|
-
];
|
|
25193
|
-
}
|
|
25194
|
-
function sanitizeBudgetMs(value) {
|
|
25195
|
-
if (value === undefined || value === null) {
|
|
25196
|
-
return;
|
|
25197
|
-
}
|
|
25198
|
-
const n = Number(value);
|
|
25199
|
-
return Number.isFinite(n) ? n : undefined;
|
|
25200
|
-
}
|
|
25201
|
-
async function runJoeTool(params) {
|
|
25202
|
-
const command = JOE_TOOL_TO_COMMAND[params.toolName];
|
|
25203
|
-
const a = params.args;
|
|
25204
|
-
const project = String(a.project_id ?? "").trim();
|
|
25205
|
-
if (!project) {
|
|
25206
|
-
return { content: [{ type: "text", text: "project_id is required" }], isError: true };
|
|
25207
|
-
}
|
|
25208
|
-
let sql = null;
|
|
25209
|
-
let cmdArgs = null;
|
|
25210
|
-
if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
|
|
25211
|
-
sql = String(a.sql ?? "").trim();
|
|
25212
|
-
if (!sql) {
|
|
25213
|
-
return { content: [{ type: "text", text: "sql is required" }], isError: true };
|
|
25214
|
-
}
|
|
25215
|
-
}
|
|
25216
|
-
if (command === "hypo") {
|
|
25217
|
-
const query = String(a.query ?? "").trim();
|
|
25218
|
-
if (!query) {
|
|
25219
|
-
return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
|
|
25220
|
-
}
|
|
25221
|
-
cmdArgs = { query };
|
|
25222
|
-
}
|
|
25223
|
-
if (command === "terminate") {
|
|
25224
|
-
const raw = a.pid;
|
|
25225
|
-
const pid = typeof raw === "number" ? raw : typeof raw === "string" && /^[0-9]+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
|
|
25226
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
25227
|
-
return {
|
|
25228
|
-
content: [{ type: "text", text: "pid must be a strict positive integer (backend pid) for terminate_backend" }],
|
|
25229
|
-
isError: true
|
|
25230
|
-
};
|
|
25231
|
-
}
|
|
25232
|
-
cmdArgs = { pid };
|
|
25233
|
-
}
|
|
25234
|
-
if (command === "describe") {
|
|
25235
|
-
const object4 = String(a.object ?? "").trim();
|
|
25236
|
-
if (!object4) {
|
|
25237
|
-
return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
|
|
25238
|
-
}
|
|
25239
|
-
cmdArgs = { object: object4 };
|
|
25240
|
-
if (a.variant !== undefined)
|
|
25241
|
-
cmdArgs.variant = String(a.variant);
|
|
25242
|
-
}
|
|
25243
|
-
const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
|
|
25244
|
-
const newSession = a.new_session === true;
|
|
25245
|
-
const budgetMs = sanitizeBudgetMs(a.timeout_ms);
|
|
25246
|
-
const outcome = await executeJoeCommand({
|
|
25247
|
-
apiKey: params.apiKey,
|
|
25248
|
-
apiBaseUrl: params.apiBaseUrl,
|
|
25249
|
-
command,
|
|
25250
|
-
project,
|
|
25251
|
-
sql,
|
|
25252
|
-
args: cmdArgs,
|
|
25253
|
-
session,
|
|
25254
|
-
newSession,
|
|
25255
|
-
orgId: params.orgId,
|
|
25256
|
-
budgetMs,
|
|
25257
|
-
debug: params.debug
|
|
25258
|
-
});
|
|
25259
|
-
const payload = {
|
|
25260
|
-
command,
|
|
25261
|
-
command_id: outcome.commandId,
|
|
25262
|
-
session_id: outcome.sessionId,
|
|
25263
|
-
status: outcome.status,
|
|
25264
|
-
budget_expired: outcome.budgetExpired,
|
|
25265
|
-
resume: outcome.budgetExpired ? `pgai joe result ${outcome.commandId}` : undefined,
|
|
25266
|
-
result: outcome.result
|
|
25267
|
-
};
|
|
25268
|
-
const isError = outcome.status === "error" || outcome.status === "timed_out";
|
|
25269
|
-
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
|
|
25270
|
-
}
|
|
25271
|
-
async function resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfgOrgId, debug) {
|
|
25272
|
-
const project = String(args.project || "").trim();
|
|
25273
|
-
if (!project) {
|
|
25274
|
-
throw new Error("project is required (project id or alias)");
|
|
25275
|
-
}
|
|
25276
|
-
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
|
|
25277
|
-
return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
|
|
25278
|
-
}
|
|
25279
|
-
function optStr(v) {
|
|
25280
|
-
return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
|
|
25281
|
-
}
|
|
25282
24570
|
async function handleToolCall(req, rootOpts, extra) {
|
|
25283
24571
|
const toolName = req.params.name;
|
|
25284
24572
|
const args = req.params.arguments || {};
|
|
@@ -25556,147 +24844,6 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25556
24844
|
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
|
|
25557
24845
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
25558
24846
|
}
|
|
25559
|
-
if (toolName === "list_projects") {
|
|
25560
|
-
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
25561
|
-
const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
25562
|
-
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
25563
|
-
}
|
|
25564
|
-
if (toolName in JOE_TOOL_TO_COMMAND) {
|
|
25565
|
-
const orgId = cfg.orgId ?? undefined;
|
|
25566
|
-
return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
|
|
25567
|
-
}
|
|
25568
|
-
if (toolName === "clone_create") {
|
|
25569
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25570
|
-
const result = await createClone({
|
|
25571
|
-
apiKey,
|
|
25572
|
-
apiBaseUrl,
|
|
25573
|
-
instanceId,
|
|
25574
|
-
cloneId: optStr(args.clone_id),
|
|
25575
|
-
branch: optStr(args.branch),
|
|
25576
|
-
snapshotId: optStr(args.snapshot_id),
|
|
25577
|
-
dbUser: optStr(args.db_user),
|
|
25578
|
-
dbPassword: optStr(args.db_password),
|
|
25579
|
-
isProtected: args.protected === true,
|
|
25580
|
-
debug
|
|
25581
|
-
});
|
|
25582
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25583
|
-
}
|
|
25584
|
-
if (toolName === "clone_list") {
|
|
25585
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25586
|
-
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25587
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25588
|
-
}
|
|
25589
|
-
if (toolName === "clone_status") {
|
|
25590
|
-
const cloneId = String(args.clone_id || "").trim();
|
|
25591
|
-
if (!cloneId)
|
|
25592
|
-
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25593
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25594
|
-
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25595
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25596
|
-
}
|
|
25597
|
-
if (toolName === "clone_reset") {
|
|
25598
|
-
const cloneId = String(args.clone_id || "").trim();
|
|
25599
|
-
if (!cloneId)
|
|
25600
|
-
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25601
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25602
|
-
const result = await resetClone({
|
|
25603
|
-
apiKey,
|
|
25604
|
-
apiBaseUrl,
|
|
25605
|
-
instanceId,
|
|
25606
|
-
cloneId,
|
|
25607
|
-
snapshotId: optStr(args.snapshot_id),
|
|
25608
|
-
latest: args.latest === true ? true : undefined,
|
|
25609
|
-
debug
|
|
25610
|
-
});
|
|
25611
|
-
return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
|
|
25612
|
-
}
|
|
25613
|
-
if (toolName === "clone_destroy") {
|
|
25614
|
-
const cloneId = String(args.clone_id || "").trim();
|
|
25615
|
-
if (!cloneId)
|
|
25616
|
-
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25617
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25618
|
-
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25619
|
-
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
|
|
25620
|
-
}
|
|
25621
|
-
if (toolName === "branch_list") {
|
|
25622
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25623
|
-
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25624
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25625
|
-
}
|
|
25626
|
-
if (toolName === "branch_create") {
|
|
25627
|
-
const branchName = String(args.name || "").trim();
|
|
25628
|
-
if (!branchName)
|
|
25629
|
-
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25630
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25631
|
-
const result = await createBranch({
|
|
25632
|
-
apiKey,
|
|
25633
|
-
apiBaseUrl,
|
|
25634
|
-
instanceId,
|
|
25635
|
-
branchName,
|
|
25636
|
-
baseBranch: optStr(args.base_branch),
|
|
25637
|
-
snapshotId: optStr(args.snapshot_id),
|
|
25638
|
-
debug
|
|
25639
|
-
});
|
|
25640
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25641
|
-
}
|
|
25642
|
-
if (toolName === "branch_delete") {
|
|
25643
|
-
const branchName = String(args.name || "").trim();
|
|
25644
|
-
if (!branchName)
|
|
25645
|
-
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25646
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25647
|
-
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25648
|
-
return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
|
|
25649
|
-
}
|
|
25650
|
-
if (toolName === "branch_log") {
|
|
25651
|
-
const branchName = String(args.name || "").trim();
|
|
25652
|
-
if (!branchName)
|
|
25653
|
-
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25654
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25655
|
-
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25656
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25657
|
-
}
|
|
25658
|
-
if (toolName === "snapshot_list") {
|
|
25659
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25660
|
-
const result = await listSnapshots({
|
|
25661
|
-
apiKey,
|
|
25662
|
-
apiBaseUrl,
|
|
25663
|
-
instanceId,
|
|
25664
|
-
branchName: optStr(args.branch),
|
|
25665
|
-
dataset: optStr(args.dataset),
|
|
25666
|
-
debug
|
|
25667
|
-
});
|
|
25668
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25669
|
-
}
|
|
25670
|
-
if (toolName === "snapshot_create") {
|
|
25671
|
-
const cloneId = String(args.clone_id || "").trim();
|
|
25672
|
-
if (!cloneId)
|
|
25673
|
-
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25674
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25675
|
-
const result = await createSnapshot({
|
|
25676
|
-
apiKey,
|
|
25677
|
-
apiBaseUrl,
|
|
25678
|
-
instanceId,
|
|
25679
|
-
cloneId,
|
|
25680
|
-
message: optStr(args.message),
|
|
25681
|
-
debug
|
|
25682
|
-
});
|
|
25683
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25684
|
-
}
|
|
25685
|
-
if (toolName === "snapshot_destroy") {
|
|
25686
|
-
const snapshotId = String(args.snapshot_id || "").trim();
|
|
25687
|
-
if (!snapshotId)
|
|
25688
|
-
return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
|
|
25689
|
-
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25690
|
-
const result = await destroySnapshot({
|
|
25691
|
-
apiKey,
|
|
25692
|
-
apiBaseUrl,
|
|
25693
|
-
instanceId,
|
|
25694
|
-
snapshotId,
|
|
25695
|
-
force: args.force === true,
|
|
25696
|
-
debug
|
|
25697
|
-
});
|
|
25698
|
-
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
|
|
25699
|
-
}
|
|
25700
24847
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
25701
24848
|
} catch (err) {
|
|
25702
24849
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -25973,197 +25120,6 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25973
25120
|
},
|
|
25974
25121
|
additionalProperties: false
|
|
25975
25122
|
}
|
|
25976
|
-
},
|
|
25977
|
-
...joeToolDefinitions(),
|
|
25978
|
-
{
|
|
25979
|
-
name: "clone_create",
|
|
25980
|
-
description: "Create a DBLab thin clone of the project's database (same as CLI 'pgai dblab clone create'). Requires the joe:plan scope.",
|
|
25981
|
-
inputSchema: {
|
|
25982
|
-
type: "object",
|
|
25983
|
-
properties: {
|
|
25984
|
-
project: { type: "string", description: "Project id or alias" },
|
|
25985
|
-
branch: { type: "string", description: "Branch to clone from" },
|
|
25986
|
-
snapshot_id: { type: "string", description: "Snapshot id to clone from" },
|
|
25987
|
-
clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
|
|
25988
|
-
db_user: { type: "string", description: "Clone DB user" },
|
|
25989
|
-
db_password: { type: "string", description: "Clone DB password" },
|
|
25990
|
-
protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
|
|
25991
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25992
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25993
|
-
},
|
|
25994
|
-
required: ["project"],
|
|
25995
|
-
additionalProperties: false
|
|
25996
|
-
}
|
|
25997
|
-
},
|
|
25998
|
-
{
|
|
25999
|
-
name: "clone_list",
|
|
26000
|
-
description: "List the project's DBLab thin clones (same as CLI 'pgai dblab clone list').",
|
|
26001
|
-
inputSchema: {
|
|
26002
|
-
type: "object",
|
|
26003
|
-
properties: {
|
|
26004
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26005
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26006
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26007
|
-
},
|
|
26008
|
-
required: ["project"],
|
|
26009
|
-
additionalProperties: false
|
|
26010
|
-
}
|
|
26011
|
-
},
|
|
26012
|
-
{
|
|
26013
|
-
name: "clone_status",
|
|
26014
|
-
description: "Show a DBLab clone's status (same as CLI 'pgai dblab clone status').",
|
|
26015
|
-
inputSchema: {
|
|
26016
|
-
type: "object",
|
|
26017
|
-
properties: {
|
|
26018
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26019
|
-
clone_id: { type: "string", description: "Clone id" },
|
|
26020
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26021
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26022
|
-
},
|
|
26023
|
-
required: ["project", "clone_id"],
|
|
26024
|
-
additionalProperties: false
|
|
26025
|
-
}
|
|
26026
|
-
},
|
|
26027
|
-
{
|
|
26028
|
-
name: "clone_reset",
|
|
26029
|
-
description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'pgai dblab clone reset'). Requires the joe:admin scope.",
|
|
26030
|
-
inputSchema: {
|
|
26031
|
-
type: "object",
|
|
26032
|
-
properties: {
|
|
26033
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26034
|
-
clone_id: { type: "string", description: "Clone id" },
|
|
26035
|
-
snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
|
|
26036
|
-
latest: { type: "boolean", description: "Reset to the latest snapshot" },
|
|
26037
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26038
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26039
|
-
},
|
|
26040
|
-
required: ["project", "clone_id"],
|
|
26041
|
-
additionalProperties: false
|
|
26042
|
-
}
|
|
26043
|
-
},
|
|
26044
|
-
{
|
|
26045
|
-
name: "clone_destroy",
|
|
26046
|
-
description: "Destroy a DBLab clone (same as CLI 'pgai dblab clone destroy'). Requires the joe:admin scope.",
|
|
26047
|
-
inputSchema: {
|
|
26048
|
-
type: "object",
|
|
26049
|
-
properties: {
|
|
26050
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26051
|
-
clone_id: { type: "string", description: "Clone id" },
|
|
26052
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26053
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26054
|
-
},
|
|
26055
|
-
required: ["project", "clone_id"],
|
|
26056
|
-
additionalProperties: false
|
|
26057
|
-
}
|
|
26058
|
-
},
|
|
26059
|
-
{
|
|
26060
|
-
name: "branch_list",
|
|
26061
|
-
description: "List the project's DBLab branches (same as CLI 'pgai dblab branch list').",
|
|
26062
|
-
inputSchema: {
|
|
26063
|
-
type: "object",
|
|
26064
|
-
properties: {
|
|
26065
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26066
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26067
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26068
|
-
},
|
|
26069
|
-
required: ["project"],
|
|
26070
|
-
additionalProperties: false
|
|
26071
|
-
}
|
|
26072
|
-
},
|
|
26073
|
-
{
|
|
26074
|
-
name: "branch_create",
|
|
26075
|
-
description: "Create a DBLab branch (same as CLI 'pgai dblab branch create'). Requires the joe:plan scope.",
|
|
26076
|
-
inputSchema: {
|
|
26077
|
-
type: "object",
|
|
26078
|
-
properties: {
|
|
26079
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26080
|
-
name: { type: "string", description: "Branch name" },
|
|
26081
|
-
snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
|
|
26082
|
-
base_branch: { type: "string", description: "Parent branch to fork from" },
|
|
26083
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26084
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26085
|
-
},
|
|
26086
|
-
required: ["project", "name"],
|
|
26087
|
-
additionalProperties: false
|
|
26088
|
-
}
|
|
26089
|
-
},
|
|
26090
|
-
{
|
|
26091
|
-
name: "branch_delete",
|
|
26092
|
-
description: "Delete a DBLab branch (same as CLI 'pgai dblab branch delete'). Requires the joe:admin scope.",
|
|
26093
|
-
inputSchema: {
|
|
26094
|
-
type: "object",
|
|
26095
|
-
properties: {
|
|
26096
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26097
|
-
name: { type: "string", description: "Branch name" },
|
|
26098
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26099
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26100
|
-
},
|
|
26101
|
-
required: ["project", "name"],
|
|
26102
|
-
additionalProperties: false
|
|
26103
|
-
}
|
|
26104
|
-
},
|
|
26105
|
-
{
|
|
26106
|
-
name: "branch_log",
|
|
26107
|
-
description: "Show a DBLab branch's snapshot log (same as CLI 'pgai dblab branch log').",
|
|
26108
|
-
inputSchema: {
|
|
26109
|
-
type: "object",
|
|
26110
|
-
properties: {
|
|
26111
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26112
|
-
name: { type: "string", description: "Branch name" },
|
|
26113
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26114
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26115
|
-
},
|
|
26116
|
-
required: ["project", "name"],
|
|
26117
|
-
additionalProperties: false
|
|
26118
|
-
}
|
|
26119
|
-
},
|
|
26120
|
-
{
|
|
26121
|
-
name: "snapshot_list",
|
|
26122
|
-
description: "List the project's DBLab snapshots (same as CLI 'pgai dblab snapshot list').",
|
|
26123
|
-
inputSchema: {
|
|
26124
|
-
type: "object",
|
|
26125
|
-
properties: {
|
|
26126
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26127
|
-
branch: { type: "string", description: "Filter by branch" },
|
|
26128
|
-
dataset: { type: "string", description: "Filter by dataset" },
|
|
26129
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26130
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26131
|
-
},
|
|
26132
|
-
required: ["project"],
|
|
26133
|
-
additionalProperties: false
|
|
26134
|
-
}
|
|
26135
|
-
},
|
|
26136
|
-
{
|
|
26137
|
-
name: "snapshot_create",
|
|
26138
|
-
description: "Create a DBLab snapshot from a clone (same as CLI 'pgai dblab snapshot create'). Requires the joe:plan scope.",
|
|
26139
|
-
inputSchema: {
|
|
26140
|
-
type: "object",
|
|
26141
|
-
properties: {
|
|
26142
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26143
|
-
clone_id: { type: "string", description: "Clone id to snapshot" },
|
|
26144
|
-
message: { type: "string", description: "Snapshot message" },
|
|
26145
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26146
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26147
|
-
},
|
|
26148
|
-
required: ["project", "clone_id"],
|
|
26149
|
-
additionalProperties: false
|
|
26150
|
-
}
|
|
26151
|
-
},
|
|
26152
|
-
{
|
|
26153
|
-
name: "snapshot_destroy",
|
|
26154
|
-
description: "Destroy a DBLab snapshot (same as CLI 'pgai dblab snapshot destroy'). Requires the joe:admin scope.",
|
|
26155
|
-
inputSchema: {
|
|
26156
|
-
type: "object",
|
|
26157
|
-
properties: {
|
|
26158
|
-
project: { type: "string", description: "Project id or alias" },
|
|
26159
|
-
snapshot_id: { type: "string", description: "Snapshot id" },
|
|
26160
|
-
force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
|
|
26161
|
-
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26162
|
-
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26163
|
-
},
|
|
26164
|
-
required: ["project", "snapshot_id"],
|
|
26165
|
-
additionalProperties: false
|
|
26166
|
-
}
|
|
26167
25123
|
}
|
|
26168
25124
|
]
|
|
26169
25125
|
};
|
|
@@ -27037,39 +25993,9 @@ function renderMarkdownForTerminal(md) {
|
|
|
27037
25993
|
}
|
|
27038
25994
|
|
|
27039
25995
|
// lib/joe.ts
|
|
27040
|
-
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
var JOE_COMMANDS2 = [
|
|
27044
|
-
"plan",
|
|
27045
|
-
"explain",
|
|
27046
|
-
"exec",
|
|
27047
|
-
"hypo",
|
|
27048
|
-
"activity",
|
|
27049
|
-
"terminate",
|
|
27050
|
-
"reset",
|
|
27051
|
-
"describe"
|
|
27052
|
-
];
|
|
27053
|
-
var TERMINAL_STATES2 = new Set([
|
|
27054
|
-
"done",
|
|
27055
|
-
"error",
|
|
27056
|
-
"timed_out"
|
|
27057
|
-
]);
|
|
27058
|
-
var AI_ENABLED_COMMANDS2 = new Set([
|
|
27059
|
-
"plan",
|
|
27060
|
-
"explain",
|
|
27061
|
-
"exec",
|
|
27062
|
-
"hypo",
|
|
27063
|
-
"activity",
|
|
27064
|
-
"describe"
|
|
27065
|
-
]);
|
|
27066
|
-
var EXECUTING_COMMANDS2 = new Set([
|
|
27067
|
-
"exec",
|
|
27068
|
-
"explain"
|
|
27069
|
-
]);
|
|
27070
|
-
var DEFAULT_BUDGET_MS2 = 25000;
|
|
27071
|
-
var DEFAULT_POLL_INTERVAL_MS2 = 800;
|
|
27072
|
-
async function callRpc2(params) {
|
|
25996
|
+
var DEFAULT_BUDGET_MS = 25000;
|
|
25997
|
+
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
25998
|
+
async function callRpc(params) {
|
|
27073
25999
|
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
27074
26000
|
if (!apiKey) {
|
|
27075
26001
|
throw new Error("API key is required");
|
|
@@ -27089,13 +26015,18 @@ async function callRpc2(params) {
|
|
|
27089
26015
|
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
27090
26016
|
}
|
|
27091
26017
|
let response;
|
|
26018
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
27092
26019
|
try {
|
|
27093
26020
|
response = await fetch(url.toString(), {
|
|
27094
26021
|
method: "POST",
|
|
27095
26022
|
headers,
|
|
27096
|
-
body: payload
|
|
26023
|
+
body: payload,
|
|
26024
|
+
signal: requestTimeout.signal
|
|
27097
26025
|
});
|
|
27098
26026
|
} catch (err) {
|
|
26027
|
+
if (isFetchTimeout(err)) {
|
|
26028
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
26029
|
+
}
|
|
27099
26030
|
throw new Error(describeFetchError(operation, base, err));
|
|
27100
26031
|
}
|
|
27101
26032
|
const text = await response.text();
|
|
@@ -27104,86 +26035,72 @@ async function callRpc2(params) {
|
|
|
27104
26035
|
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
27105
26036
|
}
|
|
27106
26037
|
if (!response.ok) {
|
|
27107
|
-
throw new
|
|
26038
|
+
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
27108
26039
|
}
|
|
27109
26040
|
try {
|
|
27110
26041
|
return JSON.parse(text);
|
|
27111
26042
|
} catch {
|
|
27112
|
-
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
26043
|
+
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
27113
26044
|
}
|
|
27114
26045
|
}
|
|
27115
|
-
async function
|
|
27116
|
-
const { apiKey, apiBaseUrl,
|
|
27117
|
-
if (!
|
|
27118
|
-
throw new Error(
|
|
27119
|
-
}
|
|
27120
|
-
const body = {
|
|
27121
|
-
project_id: projectId,
|
|
27122
|
-
command,
|
|
27123
|
-
sql: sql ?? null,
|
|
27124
|
-
args: args ?? null,
|
|
27125
|
-
session_id: sessionId ?? null
|
|
27126
|
-
};
|
|
27127
|
-
if (idempotencyKey) {
|
|
27128
|
-
body.idempotency_key = idempotencyKey;
|
|
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");
|
|
27129
26050
|
}
|
|
27130
|
-
|
|
26051
|
+
const commandId = await callRpc({
|
|
27131
26052
|
apiKey,
|
|
27132
26053
|
apiBaseUrl,
|
|
27133
|
-
fn: "
|
|
27134
|
-
body,
|
|
27135
|
-
operation:
|
|
27136
|
-
debug
|
|
26054
|
+
fn: "joe_command_run",
|
|
26055
|
+
body: { instance_id: instanceId, command },
|
|
26056
|
+
operation: "Failed to run Joe command",
|
|
26057
|
+
debug,
|
|
26058
|
+
timeoutMs
|
|
27137
26059
|
});
|
|
27138
|
-
|
|
27139
|
-
|
|
27140
|
-
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27141
|
-
if (!commandId) {
|
|
27142
|
-
throw new Error("commandId is required");
|
|
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))}`);
|
|
27143
26062
|
}
|
|
27144
|
-
return
|
|
27145
|
-
apiKey,
|
|
27146
|
-
apiBaseUrl,
|
|
27147
|
-
fn: "joe_command_status",
|
|
27148
|
-
body: { command_id: commandId },
|
|
27149
|
-
operation: "Failed to fetch command status",
|
|
27150
|
-
debug
|
|
27151
|
-
});
|
|
26063
|
+
return commandId;
|
|
27152
26064
|
}
|
|
27153
|
-
async function
|
|
27154
|
-
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
26065
|
+
async function getCommandOutput(params) {
|
|
26066
|
+
const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
|
|
27155
26067
|
if (!commandId) {
|
|
27156
26068
|
throw new Error("commandId is required");
|
|
27157
26069
|
}
|
|
27158
|
-
return
|
|
26070
|
+
return callRpc({
|
|
27159
26071
|
apiKey,
|
|
27160
26072
|
apiBaseUrl,
|
|
27161
|
-
fn: "
|
|
26073
|
+
fn: "joe_command_output",
|
|
27162
26074
|
body: { command_id: commandId },
|
|
27163
|
-
operation: "Failed to fetch command
|
|
27164
|
-
debug
|
|
26075
|
+
operation: "Failed to fetch command output",
|
|
26076
|
+
debug,
|
|
26077
|
+
timeoutMs
|
|
27165
26078
|
});
|
|
27166
26079
|
}
|
|
27167
|
-
function
|
|
27168
|
-
|
|
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) {
|
|
27169
26087
|
return {
|
|
27170
|
-
project_id:
|
|
26088
|
+
project_id: preserveIntegerId(row.project_id ?? 0),
|
|
27171
26089
|
alias: row.alias ?? null,
|
|
27172
26090
|
name: row.name ?? null,
|
|
27173
|
-
|
|
27174
|
-
|
|
27175
|
-
|
|
27176
|
-
|
|
27177
|
-
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
|
|
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)
|
|
27178
26095
|
};
|
|
27179
26096
|
}
|
|
27180
|
-
async function
|
|
26097
|
+
async function listProjects(params) {
|
|
27181
26098
|
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27182
26099
|
const body = {};
|
|
27183
26100
|
if (typeof orgId === "number") {
|
|
27184
26101
|
body.org_id = orgId;
|
|
27185
26102
|
}
|
|
27186
|
-
const rows = await
|
|
26103
|
+
const rows = await callRpc({
|
|
27187
26104
|
apiKey,
|
|
27188
26105
|
apiBaseUrl,
|
|
27189
26106
|
fn: "projects_list",
|
|
@@ -27194,166 +26111,158 @@ async function listProjects2(params) {
|
|
|
27194
26111
|
if (!Array.isArray(rows)) {
|
|
27195
26112
|
return [];
|
|
27196
26113
|
}
|
|
27197
|
-
return rows.map(
|
|
26114
|
+
return rows.map(normalizeProjectRow);
|
|
27198
26115
|
}
|
|
27199
|
-
function
|
|
26116
|
+
function isNumericProjectRef(ref) {
|
|
27200
26117
|
return /^[0-9]+$/.test(ref.trim());
|
|
27201
26118
|
}
|
|
27202
|
-
async function
|
|
26119
|
+
async function resolveJoeInstanceId(params) {
|
|
27203
26120
|
const ref = String(params.project ?? "").trim();
|
|
27204
26121
|
if (!ref) {
|
|
27205
26122
|
throw new Error("project is required (--project <id|alias>)");
|
|
27206
26123
|
}
|
|
27207
|
-
|
|
27208
|
-
return Number(ref);
|
|
27209
|
-
}
|
|
27210
|
-
const projects = await listProjects2({
|
|
26124
|
+
const projects = await listProjects({
|
|
27211
26125
|
apiKey: params.apiKey,
|
|
27212
26126
|
apiBaseUrl: params.apiBaseUrl,
|
|
27213
26127
|
orgId: params.orgId,
|
|
27214
26128
|
debug: params.debug
|
|
27215
26129
|
});
|
|
27216
26130
|
const needle = ref.toLowerCase();
|
|
27217
|
-
const match = projects.find((p) => p.
|
|
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);
|
|
27218
26132
|
if (!match) {
|
|
27219
|
-
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27220
|
-
}
|
|
27221
|
-
|
|
27222
|
-
}
|
|
27223
|
-
|
|
27224
|
-
return
|
|
27225
|
-
}
|
|
27226
|
-
|
|
27227
|
-
|
|
27228
|
-
|
|
27229
|
-
|
|
27230
|
-
|
|
27231
|
-
|
|
27232
|
-
|
|
27233
|
-
|
|
27234
|
-
|
|
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}`;
|
|
27235
26184
|
}
|
|
27236
|
-
} catch {}
|
|
27237
|
-
return {};
|
|
27238
|
-
}
|
|
27239
|
-
function readStoredSessionId2(projectId, dir) {
|
|
27240
|
-
const store = readSessionStore2(dir);
|
|
27241
|
-
const value = store[String(projectId)];
|
|
27242
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
27243
|
-
}
|
|
27244
|
-
function writeStoredSessionId2(projectId, sessionId, dir) {
|
|
27245
|
-
const baseDir = dir ?? getConfigDir2();
|
|
27246
|
-
if (!fs5.existsSync(baseDir)) {
|
|
27247
|
-
fs5.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
27248
|
-
}
|
|
27249
|
-
const store = readSessionStore2(dir);
|
|
27250
|
-
store[String(projectId)] = sessionId;
|
|
27251
|
-
fs5.writeFileSync(sessionStorePath2(dir), JSON.stringify(store, null, 2) + `
|
|
27252
|
-
`, { mode: 384 });
|
|
27253
|
-
}
|
|
27254
|
-
function clearStoredSessionId2(projectId, dir) {
|
|
27255
|
-
const file = sessionStorePath2(dir);
|
|
27256
|
-
if (!fs5.existsSync(file)) {
|
|
27257
|
-
return;
|
|
27258
|
-
}
|
|
27259
|
-
const store = readSessionStore2(dir);
|
|
27260
|
-
if (String(projectId) in store) {
|
|
27261
|
-
delete store[String(projectId)];
|
|
27262
|
-
fs5.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
27263
|
-
`, { mode: 384 });
|
|
27264
|
-
}
|
|
27265
|
-
}
|
|
27266
|
-
function computeIdempotencyKey2(command, projectId, sql, args) {
|
|
27267
|
-
if (EXECUTING_COMMANDS2.has(command)) {
|
|
27268
|
-
const canonical = `${projectId}
|
|
27269
|
-
${command}
|
|
27270
|
-
${sql ?? ""}
|
|
27271
|
-
${JSON.stringify(args ?? null)}`;
|
|
27272
|
-
return `joe-${crypto2.createHash("sha256").update(canonical).digest("hex")}`;
|
|
27273
26185
|
}
|
|
27274
|
-
return `joe-${crypto2.randomUUID()}`;
|
|
27275
26186
|
}
|
|
27276
|
-
var
|
|
27277
|
-
async function
|
|
27278
|
-
const {
|
|
27279
|
-
|
|
27280
|
-
|
|
27281
|
-
command,
|
|
27282
|
-
projectId,
|
|
27283
|
-
sql,
|
|
27284
|
-
args,
|
|
27285
|
-
sessionId,
|
|
27286
|
-
debug
|
|
27287
|
-
} = params;
|
|
27288
|
-
const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) ? params.budgetMs : DEFAULT_BUDGET_MS2;
|
|
27289
|
-
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
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;
|
|
27290
26192
|
const now = params.now ?? Date.now;
|
|
27291
|
-
const sleep = params.sleep ??
|
|
27292
|
-
const
|
|
27293
|
-
const submitted = await submitCommand2({
|
|
27294
|
-
apiKey,
|
|
27295
|
-
apiBaseUrl,
|
|
27296
|
-
command,
|
|
27297
|
-
projectId,
|
|
27298
|
-
sql,
|
|
27299
|
-
args,
|
|
27300
|
-
sessionId,
|
|
27301
|
-
idempotencyKey,
|
|
27302
|
-
debug
|
|
27303
|
-
});
|
|
27304
|
-
const commandId = submitted.command_id;
|
|
27305
|
-
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
26193
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
26194
|
+
const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
|
|
27306
26195
|
const deadline = now() + budgetMs;
|
|
27307
|
-
let status =
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
status
|
|
27311
|
-
|
|
27312
|
-
|
|
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 };
|
|
27313
26227
|
}
|
|
27314
26228
|
if (now() >= deadline) {
|
|
27315
|
-
return { commandId,
|
|
26229
|
+
return { commandId, status, output: null, budgetExpired: true };
|
|
27316
26230
|
}
|
|
27317
26231
|
await sleep(pollIntervalMs);
|
|
27318
26232
|
}
|
|
27319
|
-
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27320
|
-
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
27321
26233
|
}
|
|
27322
|
-
async function
|
|
27323
|
-
const
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
if (params.
|
|
27332
|
-
|
|
27333
|
-
|
|
27334
|
-
|
|
27335
|
-
|
|
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
|
+
});
|
|
27336
26251
|
} else {
|
|
27337
|
-
|
|
26252
|
+
throw new Error("either instanceId or project is required");
|
|
27338
26253
|
}
|
|
27339
|
-
const outcome = await
|
|
26254
|
+
const outcome = await runCommand({
|
|
27340
26255
|
apiKey: params.apiKey,
|
|
27341
26256
|
apiBaseUrl: params.apiBaseUrl,
|
|
27342
|
-
|
|
27343
|
-
|
|
27344
|
-
sql: params.sql,
|
|
27345
|
-
args: params.args,
|
|
27346
|
-
sessionId,
|
|
26257
|
+
instanceId,
|
|
26258
|
+
command: commandText,
|
|
27347
26259
|
budgetMs: params.budgetMs,
|
|
27348
26260
|
pollIntervalMs: params.pollIntervalMs,
|
|
27349
26261
|
debug: params.debug,
|
|
27350
26262
|
now: params.now,
|
|
27351
26263
|
sleep: params.sleep
|
|
27352
26264
|
});
|
|
27353
|
-
|
|
27354
|
-
writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
|
|
27355
|
-
}
|
|
27356
|
-
return { ...outcome, command: params.command, projectId };
|
|
26265
|
+
return { ...outcome, command: params.command, instanceId, commandText };
|
|
27357
26266
|
}
|
|
27358
26267
|
function clientSidePlanFlags(planJson) {
|
|
27359
26268
|
const flags = [];
|
|
@@ -27372,57 +26281,44 @@ function clientSidePlanFlags(planJson) {
|
|
|
27372
26281
|
}
|
|
27373
26282
|
}
|
|
27374
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
|
+
}
|
|
27375
26292
|
if (planJson && typeof planJson === "object") {
|
|
27376
26293
|
const root = planJson;
|
|
27377
26294
|
walk(root.Plan ?? planJson);
|
|
27378
26295
|
}
|
|
27379
26296
|
return flags;
|
|
27380
26297
|
}
|
|
27381
|
-
function
|
|
26298
|
+
function formatJoeOutput(output) {
|
|
27382
26299
|
const lines = [];
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
|
|
27388
|
-
|
|
27389
|
-
|
|
27390
|
-
|
|
27391
|
-
|
|
27392
|
-
|
|
27393
|
-
|
|
27394
|
-
|
|
27395
|
-
|
|
27396
|
-
|
|
27397
|
-
|
|
27398
|
-
|
|
27399
|
-
|
|
27400
|
-
|
|
27401
|
-
|
|
27402
|
-
|
|
27403
|
-
|
|
27404
|
-
|
|
27405
|
-
}
|
|
27406
|
-
case "hypo": {
|
|
27407
|
-
lines.push(result.hypo_used ? "hypothetical index would be used" : "hypothetical index would NOT be used");
|
|
27408
|
-
if (result.hypo_plan !== undefined) {
|
|
27409
|
-
lines.push(JSON.stringify(result.hypo_plan, null, 2));
|
|
27410
|
-
}
|
|
27411
|
-
break;
|
|
27412
|
-
}
|
|
27413
|
-
case "activity":
|
|
27414
|
-
case "describe": {
|
|
27415
|
-
lines.push(JSON.stringify(result.snapshot ?? null, null, 2));
|
|
27416
|
-
break;
|
|
27417
|
-
}
|
|
27418
|
-
case "terminate": {
|
|
27419
|
-
lines.push(`terminated ${result.terminated ? "yes" : "no"} · pid ${result.pid ?? "?"}`);
|
|
27420
|
-
break;
|
|
27421
|
-
}
|
|
27422
|
-
case "reset": {
|
|
27423
|
-
lines.push(`reset ${result.reset ? "ok" : "no"}`);
|
|
27424
|
-
break;
|
|
27425
|
-
}
|
|
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})`);
|
|
27426
26322
|
}
|
|
27427
26323
|
return lines.join(`
|
|
27428
26324
|
`);
|
|
@@ -27442,293 +26338,11 @@ function formatProjectsTable(projects) {
|
|
|
27442
26338
|
`);
|
|
27443
26339
|
}
|
|
27444
26340
|
|
|
27445
|
-
// lib/dblab.ts
|
|
27446
|
-
function isNumericProjectRef4(ref) {
|
|
27447
|
-
return /^[0-9]+$/.test(ref.trim());
|
|
27448
|
-
}
|
|
27449
|
-
async function resolveDblabInstanceId2(params) {
|
|
27450
|
-
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27451
|
-
if (!apiKey) {
|
|
27452
|
-
throw new Error("API key is required");
|
|
27453
|
-
}
|
|
27454
|
-
const ref = String(params.project ?? "").trim();
|
|
27455
|
-
if (!ref) {
|
|
27456
|
-
throw new Error("project is required (--project <id|alias>)");
|
|
27457
|
-
}
|
|
27458
|
-
let projects;
|
|
27459
|
-
try {
|
|
27460
|
-
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
27461
|
-
} catch (err) {
|
|
27462
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
27463
|
-
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
27464
|
-
}
|
|
27465
|
-
const numeric = isNumericProjectRef4(ref);
|
|
27466
|
-
const needle = ref.toLowerCase();
|
|
27467
|
-
const match = projects.find((p) => {
|
|
27468
|
-
if (numeric) {
|
|
27469
|
-
return String(p.project_id) === ref;
|
|
27470
|
-
}
|
|
27471
|
-
return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
|
|
27472
|
-
});
|
|
27473
|
-
if (!match) {
|
|
27474
|
-
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27475
|
-
}
|
|
27476
|
-
if (match.dblab_instance_id == null) {
|
|
27477
|
-
throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
|
|
27478
|
-
}
|
|
27479
|
-
return String(match.dblab_instance_id);
|
|
27480
|
-
}
|
|
27481
|
-
async function callDblabApi2(params) {
|
|
27482
|
-
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
27483
|
-
if (!apiKey) {
|
|
27484
|
-
throw new Error("API key is required");
|
|
27485
|
-
}
|
|
27486
|
-
if (!instanceId) {
|
|
27487
|
-
throw new Error("instanceId is required");
|
|
27488
|
-
}
|
|
27489
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27490
|
-
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
27491
|
-
const bodyObj = {
|
|
27492
|
-
instance_id: instanceId,
|
|
27493
|
-
action,
|
|
27494
|
-
method
|
|
27495
|
-
};
|
|
27496
|
-
if (data !== undefined) {
|
|
27497
|
-
bodyObj.data = data;
|
|
27498
|
-
}
|
|
27499
|
-
const body = JSON.stringify(bodyObj);
|
|
27500
|
-
const headers = {
|
|
27501
|
-
"access-token": apiKey,
|
|
27502
|
-
"Content-Type": "application/json",
|
|
27503
|
-
Connection: "close"
|
|
27504
|
-
};
|
|
27505
|
-
if (debug) {
|
|
27506
|
-
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27507
|
-
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27508
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27509
|
-
console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
|
|
27510
|
-
}
|
|
27511
|
-
let response;
|
|
27512
|
-
try {
|
|
27513
|
-
response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
27514
|
-
} catch (err) {
|
|
27515
|
-
throw new Error(describeFetchError(operation, base, err));
|
|
27516
|
-
}
|
|
27517
|
-
const text = await response.text();
|
|
27518
|
-
if (debug) {
|
|
27519
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
27520
|
-
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
27521
|
-
}
|
|
27522
|
-
if (!response.ok) {
|
|
27523
|
-
throw new Error(formatHttpError(operation, response.status, text, response.statusText));
|
|
27524
|
-
}
|
|
27525
|
-
if (text.trim() === "") {
|
|
27526
|
-
return null;
|
|
27527
|
-
}
|
|
27528
|
-
try {
|
|
27529
|
-
return JSON.parse(text);
|
|
27530
|
-
} catch {
|
|
27531
|
-
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
27532
|
-
}
|
|
27533
|
-
}
|
|
27534
|
-
async function createClone2(params) {
|
|
27535
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
27536
|
-
const data = { protected: Boolean(isProtected) };
|
|
27537
|
-
if (cloneId)
|
|
27538
|
-
data.id = cloneId;
|
|
27539
|
-
if (branch)
|
|
27540
|
-
data.branch = branch;
|
|
27541
|
-
if (snapshotId)
|
|
27542
|
-
data.snapshot = { id: snapshotId };
|
|
27543
|
-
if (dbUser && dbPassword)
|
|
27544
|
-
data.db = { username: dbUser, password: dbPassword };
|
|
27545
|
-
return callDblabApi2({
|
|
27546
|
-
apiKey,
|
|
27547
|
-
apiBaseUrl,
|
|
27548
|
-
instanceId,
|
|
27549
|
-
action: "/clone",
|
|
27550
|
-
method: "post",
|
|
27551
|
-
data,
|
|
27552
|
-
operation: "Failed to create clone",
|
|
27553
|
-
debug
|
|
27554
|
-
});
|
|
27555
|
-
}
|
|
27556
|
-
async function listClones2(params) {
|
|
27557
|
-
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27558
|
-
return callDblabApi2({
|
|
27559
|
-
apiKey,
|
|
27560
|
-
apiBaseUrl,
|
|
27561
|
-
instanceId,
|
|
27562
|
-
action: "/clones",
|
|
27563
|
-
method: "get",
|
|
27564
|
-
operation: "Failed to list clones",
|
|
27565
|
-
debug
|
|
27566
|
-
});
|
|
27567
|
-
}
|
|
27568
|
-
async function getClone2(params) {
|
|
27569
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27570
|
-
if (!cloneId)
|
|
27571
|
-
throw new Error("cloneId is required");
|
|
27572
|
-
return callDblabApi2({
|
|
27573
|
-
apiKey,
|
|
27574
|
-
apiBaseUrl,
|
|
27575
|
-
instanceId,
|
|
27576
|
-
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27577
|
-
method: "get",
|
|
27578
|
-
operation: "Failed to get clone",
|
|
27579
|
-
debug
|
|
27580
|
-
});
|
|
27581
|
-
}
|
|
27582
|
-
async function resetClone2(params) {
|
|
27583
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
27584
|
-
if (!cloneId)
|
|
27585
|
-
throw new Error("cloneId is required");
|
|
27586
|
-
const data = { latest: latest ?? !snapshotId };
|
|
27587
|
-
if (snapshotId)
|
|
27588
|
-
data.snapshotID = snapshotId;
|
|
27589
|
-
return callDblabApi2({
|
|
27590
|
-
apiKey,
|
|
27591
|
-
apiBaseUrl,
|
|
27592
|
-
instanceId,
|
|
27593
|
-
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
27594
|
-
method: "post",
|
|
27595
|
-
data,
|
|
27596
|
-
operation: "Failed to reset clone",
|
|
27597
|
-
debug
|
|
27598
|
-
});
|
|
27599
|
-
}
|
|
27600
|
-
async function destroyClone2(params) {
|
|
27601
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27602
|
-
if (!cloneId)
|
|
27603
|
-
throw new Error("cloneId is required");
|
|
27604
|
-
return callDblabApi2({
|
|
27605
|
-
apiKey,
|
|
27606
|
-
apiBaseUrl,
|
|
27607
|
-
instanceId,
|
|
27608
|
-
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27609
|
-
method: "delete",
|
|
27610
|
-
operation: "Failed to destroy clone",
|
|
27611
|
-
debug
|
|
27612
|
-
});
|
|
27613
|
-
}
|
|
27614
|
-
async function listBranches2(params) {
|
|
27615
|
-
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27616
|
-
return callDblabApi2({
|
|
27617
|
-
apiKey,
|
|
27618
|
-
apiBaseUrl,
|
|
27619
|
-
instanceId,
|
|
27620
|
-
action: "/branches",
|
|
27621
|
-
method: "get",
|
|
27622
|
-
operation: "Failed to list branches",
|
|
27623
|
-
debug
|
|
27624
|
-
});
|
|
27625
|
-
}
|
|
27626
|
-
async function createBranch2(params) {
|
|
27627
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
27628
|
-
if (!branchName)
|
|
27629
|
-
throw new Error("branchName is required");
|
|
27630
|
-
const data = { branchName };
|
|
27631
|
-
if (baseBranch)
|
|
27632
|
-
data.baseBranch = baseBranch;
|
|
27633
|
-
if (snapshotId)
|
|
27634
|
-
data.snapshotID = snapshotId;
|
|
27635
|
-
return callDblabApi2({
|
|
27636
|
-
apiKey,
|
|
27637
|
-
apiBaseUrl,
|
|
27638
|
-
instanceId,
|
|
27639
|
-
action: "/branch",
|
|
27640
|
-
method: "post",
|
|
27641
|
-
data,
|
|
27642
|
-
operation: "Failed to create branch",
|
|
27643
|
-
debug
|
|
27644
|
-
});
|
|
27645
|
-
}
|
|
27646
|
-
async function deleteBranch2(params) {
|
|
27647
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27648
|
-
if (!branchName)
|
|
27649
|
-
throw new Error("branchName is required");
|
|
27650
|
-
return callDblabApi2({
|
|
27651
|
-
apiKey,
|
|
27652
|
-
apiBaseUrl,
|
|
27653
|
-
instanceId,
|
|
27654
|
-
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
27655
|
-
method: "delete",
|
|
27656
|
-
operation: "Failed to delete branch",
|
|
27657
|
-
debug
|
|
27658
|
-
});
|
|
27659
|
-
}
|
|
27660
|
-
async function branchLog2(params) {
|
|
27661
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27662
|
-
if (!branchName)
|
|
27663
|
-
throw new Error("branchName is required");
|
|
27664
|
-
return callDblabApi2({
|
|
27665
|
-
apiKey,
|
|
27666
|
-
apiBaseUrl,
|
|
27667
|
-
instanceId,
|
|
27668
|
-
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
27669
|
-
method: "get",
|
|
27670
|
-
operation: "Failed to fetch branch log",
|
|
27671
|
-
debug
|
|
27672
|
-
});
|
|
27673
|
-
}
|
|
27674
|
-
async function listSnapshots2(params) {
|
|
27675
|
-
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
27676
|
-
const qs = new URLSearchParams;
|
|
27677
|
-
const branch = branchName?.trim();
|
|
27678
|
-
if (branch)
|
|
27679
|
-
qs.append("branch", branch);
|
|
27680
|
-
if (dataset)
|
|
27681
|
-
qs.append("dataset", dataset);
|
|
27682
|
-
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
27683
|
-
return callDblabApi2({
|
|
27684
|
-
apiKey,
|
|
27685
|
-
apiBaseUrl,
|
|
27686
|
-
instanceId,
|
|
27687
|
-
action,
|
|
27688
|
-
method: "get",
|
|
27689
|
-
operation: "Failed to list snapshots",
|
|
27690
|
-
debug
|
|
27691
|
-
});
|
|
27692
|
-
}
|
|
27693
|
-
async function createSnapshot2(params) {
|
|
27694
|
-
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
27695
|
-
if (!cloneId)
|
|
27696
|
-
throw new Error("cloneId is required");
|
|
27697
|
-
const data = { cloneID: cloneId };
|
|
27698
|
-
if (message)
|
|
27699
|
-
data.message = message;
|
|
27700
|
-
return callDblabApi2({
|
|
27701
|
-
apiKey,
|
|
27702
|
-
apiBaseUrl,
|
|
27703
|
-
instanceId,
|
|
27704
|
-
action: "/branch/snapshot",
|
|
27705
|
-
method: "post",
|
|
27706
|
-
data,
|
|
27707
|
-
operation: "Failed to create snapshot",
|
|
27708
|
-
debug
|
|
27709
|
-
});
|
|
27710
|
-
}
|
|
27711
|
-
async function destroySnapshot2(params) {
|
|
27712
|
-
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
27713
|
-
if (!snapshotId)
|
|
27714
|
-
throw new Error("snapshotId is required");
|
|
27715
|
-
const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
|
|
27716
|
-
return callDblabApi2({
|
|
27717
|
-
apiKey,
|
|
27718
|
-
apiBaseUrl,
|
|
27719
|
-
instanceId,
|
|
27720
|
-
action,
|
|
27721
|
-
method: "delete",
|
|
27722
|
-
operation: "Failed to destroy snapshot",
|
|
27723
|
-
debug
|
|
27724
|
-
});
|
|
27725
|
-
}
|
|
27726
|
-
|
|
27727
26341
|
// bin/postgres-ai.ts
|
|
27728
26342
|
init_util();
|
|
27729
26343
|
|
|
27730
26344
|
// lib/instances.ts
|
|
27731
|
-
import * as
|
|
26345
|
+
import * as fs4 from "fs";
|
|
27732
26346
|
|
|
27733
26347
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
27734
26348
|
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
@@ -30393,11 +29007,11 @@ class InstancesParseError extends Error {
|
|
|
30393
29007
|
}
|
|
30394
29008
|
}
|
|
30395
29009
|
function loadInstances(file) {
|
|
30396
|
-
if (!
|
|
29010
|
+
if (!fs4.existsSync(file))
|
|
30397
29011
|
return [];
|
|
30398
|
-
if (
|
|
29012
|
+
if (fs4.lstatSync(file).isDirectory())
|
|
30399
29013
|
return [];
|
|
30400
|
-
const text =
|
|
29014
|
+
const text = fs4.readFileSync(file, "utf8");
|
|
30401
29015
|
if (text.trim() === "")
|
|
30402
29016
|
return [];
|
|
30403
29017
|
let parsed;
|
|
@@ -30575,8 +29189,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
30575
29189
|
}
|
|
30576
29190
|
|
|
30577
29191
|
// lib/storage.ts
|
|
30578
|
-
import * as
|
|
30579
|
-
import * as
|
|
29192
|
+
import * as fs5 from "fs";
|
|
29193
|
+
import * as path4 from "path";
|
|
30580
29194
|
var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
30581
29195
|
var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
30582
29196
|
var MIME_TYPES2 = {
|
|
@@ -30617,11 +29231,11 @@ async function uploadFile2(params) {
|
|
|
30617
29231
|
if (!filePath) {
|
|
30618
29232
|
throw new Error("filePath is required");
|
|
30619
29233
|
}
|
|
30620
|
-
const resolvedPath =
|
|
30621
|
-
if (!
|
|
29234
|
+
const resolvedPath = path4.resolve(filePath);
|
|
29235
|
+
if (!fs5.existsSync(resolvedPath)) {
|
|
30622
29236
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
30623
29237
|
}
|
|
30624
|
-
const stat =
|
|
29238
|
+
const stat = fs5.statSync(resolvedPath);
|
|
30625
29239
|
if (!stat.isFile()) {
|
|
30626
29240
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
30627
29241
|
}
|
|
@@ -30633,9 +29247,9 @@ async function uploadFile2(params) {
|
|
|
30633
29247
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
30634
29248
|
}
|
|
30635
29249
|
const url = `${base}/upload`;
|
|
30636
|
-
const fileBuffer =
|
|
30637
|
-
const fileName =
|
|
30638
|
-
const ext =
|
|
29250
|
+
const fileBuffer = fs5.readFileSync(resolvedPath);
|
|
29251
|
+
const fileName = path4.basename(resolvedPath);
|
|
29252
|
+
const ext = path4.extname(fileName).toLowerCase();
|
|
30639
29253
|
const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
|
|
30640
29254
|
const formData = new FormData;
|
|
30641
29255
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -30694,15 +29308,15 @@ async function downloadFile2(params) {
|
|
|
30694
29308
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
30695
29309
|
fullUrl = `${base}${relativePath}`;
|
|
30696
29310
|
}
|
|
30697
|
-
const urlFilename =
|
|
29311
|
+
const urlFilename = path4.basename(new URL(fullUrl).pathname);
|
|
30698
29312
|
if (!urlFilename) {
|
|
30699
29313
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
30700
29314
|
}
|
|
30701
|
-
const saveTo = outputPath ?
|
|
29315
|
+
const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
|
|
30702
29316
|
if (!outputPath) {
|
|
30703
|
-
const normalizedSave =
|
|
30704
|
-
const cwd =
|
|
30705
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
29317
|
+
const normalizedSave = path4.normalize(saveTo);
|
|
29318
|
+
const cwd = path4.normalize(process.cwd());
|
|
29319
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
|
|
30706
29320
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
30707
29321
|
}
|
|
30708
29322
|
}
|
|
@@ -30734,11 +29348,11 @@ async function downloadFile2(params) {
|
|
|
30734
29348
|
}
|
|
30735
29349
|
const arrayBuffer = await response.arrayBuffer();
|
|
30736
29350
|
const buffer = Buffer.from(arrayBuffer);
|
|
30737
|
-
const parentDir =
|
|
30738
|
-
if (!
|
|
30739
|
-
|
|
29351
|
+
const parentDir = path4.dirname(saveTo);
|
|
29352
|
+
if (!fs5.existsSync(parentDir)) {
|
|
29353
|
+
fs5.mkdirSync(parentDir, { recursive: true });
|
|
30740
29354
|
}
|
|
30741
|
-
|
|
29355
|
+
fs5.writeFileSync(saveTo, buffer);
|
|
30742
29356
|
return {
|
|
30743
29357
|
savedTo: saveTo,
|
|
30744
29358
|
size: buffer.length,
|
|
@@ -30750,9 +29364,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
|
|
|
30750
29364
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
30751
29365
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
30752
29366
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
30753
|
-
const name = filename ||
|
|
29367
|
+
const name = filename || path4.basename(new URL(fullUrl).pathname);
|
|
30754
29368
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
30755
|
-
const ext =
|
|
29369
|
+
const ext = path4.extname(name).toLowerCase();
|
|
30756
29370
|
if (IMAGE_EXTENSIONS2.has(ext)) {
|
|
30757
29371
|
return ``;
|
|
30758
29372
|
}
|
|
@@ -30798,8 +29412,8 @@ ${links}`;
|
|
|
30798
29412
|
// lib/init.ts
|
|
30799
29413
|
import { randomBytes } from "crypto";
|
|
30800
29414
|
import { URL as URL2, fileURLToPath } from "url";
|
|
30801
|
-
import * as
|
|
30802
|
-
import * as
|
|
29415
|
+
import * as fs6 from "fs";
|
|
29416
|
+
import * as path5 from "path";
|
|
30803
29417
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
30804
29418
|
var KNOWN_PROVIDERS = ["self-managed", "supabase"];
|
|
30805
29419
|
var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
|
|
@@ -30891,21 +29505,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
|
30891
29505
|
}
|
|
30892
29506
|
function sqlDir() {
|
|
30893
29507
|
const currentFile = fileURLToPath(import.meta.url);
|
|
30894
|
-
const currentDir =
|
|
29508
|
+
const currentDir = path5.dirname(currentFile);
|
|
30895
29509
|
const candidates = [
|
|
30896
|
-
|
|
30897
|
-
|
|
29510
|
+
path5.resolve(currentDir, "..", "sql"),
|
|
29511
|
+
path5.resolve(currentDir, "..", "..", "sql")
|
|
30898
29512
|
];
|
|
30899
29513
|
for (const candidate of candidates) {
|
|
30900
|
-
if (
|
|
29514
|
+
if (fs6.existsSync(candidate)) {
|
|
30901
29515
|
return candidate;
|
|
30902
29516
|
}
|
|
30903
29517
|
}
|
|
30904
29518
|
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
30905
29519
|
}
|
|
30906
29520
|
function loadSqlTemplate(filename) {
|
|
30907
|
-
const p =
|
|
30908
|
-
return
|
|
29521
|
+
const p = path5.join(sqlDir(), filename);
|
|
29522
|
+
return fs6.readFileSync(p, "utf8");
|
|
30909
29523
|
}
|
|
30910
29524
|
function applyTemplate(sql, vars) {
|
|
30911
29525
|
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
@@ -31441,10 +30055,28 @@ async function checkCurrentUserPermissions(client) {
|
|
|
31441
30055
|
|
|
31442
30056
|
union all
|
|
31443
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
|
+
|
|
31444
30075
|
select
|
|
31445
30076
|
'postgres_ai.pg_statistic view exists' as permission_name,
|
|
31446
30077
|
'optional' as status,
|
|
31447
30078
|
case
|
|
30079
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
31448
30080
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
31449
30081
|
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
31450
30082
|
end as granted
|
|
@@ -31455,6 +30087,7 @@ async function checkCurrentUserPermissions(client) {
|
|
|
31455
30087
|
'select on postgres_ai.pg_statistic' as permission_name,
|
|
31456
30088
|
'optional' as status,
|
|
31457
30089
|
case
|
|
30090
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
31458
30091
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
31459
30092
|
when to_regclass('postgres_ai.pg_statistic') is null then null
|
|
31460
30093
|
else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
|
|
@@ -31474,6 +30107,10 @@ async function checkCurrentUserPermissions(client) {
|
|
|
31474
30107
|
when permission_name like 'select on pg_catalog.pg_index' then
|
|
31475
30108
|
format('grant select on pg_catalog.pg_index to %I;', current_user)
|
|
31476
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)
|
|
31477
30114
|
when permission_name = 'postgres_ai.pg_statistic view exists' and granted = false then
|
|
31478
30115
|
'-- create postgres_ai.pg_statistic view (see setup script)'
|
|
31479
30116
|
when permission_name = 'select on postgres_ai.pg_statistic' and granted = false then
|
|
@@ -31500,6 +30137,10 @@ function formatPermissionCheckMessages(result) {
|
|
|
31500
30137
|
const warnings = [];
|
|
31501
30138
|
const errors3 = [];
|
|
31502
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
|
+
}
|
|
31503
30144
|
const fix = row.fix_command ? ` Fix: ${row.fix_command}` : "";
|
|
31504
30145
|
warnings.push(`Warning: optional permission missing — ${row.permission_name}.${fix}`);
|
|
31505
30146
|
}
|
|
@@ -31950,9 +30591,9 @@ function escapeLiteral2(value) {
|
|
|
31950
30591
|
}
|
|
31951
30592
|
|
|
31952
30593
|
// lib/pkce.ts
|
|
31953
|
-
import * as
|
|
30594
|
+
import * as crypto from "crypto";
|
|
31954
30595
|
function generateRandomString(length = 64) {
|
|
31955
|
-
const bytes =
|
|
30596
|
+
const bytes = crypto.randomBytes(length);
|
|
31956
30597
|
return base64URLEncode(bytes);
|
|
31957
30598
|
}
|
|
31958
30599
|
function base64URLEncode(buffer) {
|
|
@@ -31962,7 +30603,7 @@ function generateCodeVerifier() {
|
|
|
31962
30603
|
return generateRandomString(32);
|
|
31963
30604
|
}
|
|
31964
30605
|
function generateCodeChallenge(verifier) {
|
|
31965
|
-
const hash =
|
|
30606
|
+
const hash = crypto.createHash("sha256").update(verifier).digest();
|
|
31966
30607
|
return base64URLEncode(hash);
|
|
31967
30608
|
}
|
|
31968
30609
|
function generateState() {
|
|
@@ -32191,8 +30832,8 @@ import { createInterface } from "readline";
|
|
|
32191
30832
|
import * as childProcess from "child_process";
|
|
32192
30833
|
|
|
32193
30834
|
// lib/checkup.ts
|
|
32194
|
-
import * as
|
|
32195
|
-
import * as
|
|
30835
|
+
import * as fs7 from "fs";
|
|
30836
|
+
import * as path6 from "path";
|
|
32196
30837
|
|
|
32197
30838
|
// lib/metrics-embedded.ts
|
|
32198
30839
|
var METRICS = {
|
|
@@ -33923,7 +32564,7 @@ function buildCheckInfoMap() {
|
|
|
33923
32564
|
}
|
|
33924
32565
|
|
|
33925
32566
|
// lib/checkup.ts
|
|
33926
|
-
var __dirname = "/
|
|
32567
|
+
var __dirname = "/Users/nik/gitlab/postgresai-wt-joe-cli/cli/lib";
|
|
33927
32568
|
var SECONDS_PER_DAY = 86400;
|
|
33928
32569
|
var SECONDS_PER_HOUR = 3600;
|
|
33929
32570
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -34409,7 +33050,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
|
|
|
34409
33050
|
}
|
|
34410
33051
|
function readTextFileSafe(p) {
|
|
34411
33052
|
try {
|
|
34412
|
-
const value =
|
|
33053
|
+
const value = fs7.readFileSync(p, "utf8").trim();
|
|
34413
33054
|
return value || null;
|
|
34414
33055
|
} catch {
|
|
34415
33056
|
return null;
|
|
@@ -34422,8 +33063,8 @@ function resolveBuildTs() {
|
|
|
34422
33063
|
if (fromFile)
|
|
34423
33064
|
return fromFile;
|
|
34424
33065
|
try {
|
|
34425
|
-
const pkgRoot =
|
|
34426
|
-
const fromPkgFile = readTextFileSafe(
|
|
33066
|
+
const pkgRoot = path6.resolve(__dirname, "..");
|
|
33067
|
+
const fromPkgFile = readTextFileSafe(path6.join(pkgRoot, "BUILD_TS"));
|
|
34427
33068
|
if (fromPkgFile)
|
|
34428
33069
|
return fromPkgFile;
|
|
34429
33070
|
} catch (err) {
|
|
@@ -34431,8 +33072,8 @@ function resolveBuildTs() {
|
|
|
34431
33072
|
console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
|
|
34432
33073
|
}
|
|
34433
33074
|
try {
|
|
34434
|
-
const pkgJsonPath =
|
|
34435
|
-
const st =
|
|
33075
|
+
const pkgJsonPath = path6.resolve(__dirname, "..", "package.json");
|
|
33076
|
+
const st = fs7.statSync(pkgJsonPath);
|
|
34436
33077
|
return st.mtime.toISOString();
|
|
34437
33078
|
} catch (err) {
|
|
34438
33079
|
if (process.env.DEBUG) {
|
|
@@ -34688,12 +33329,70 @@ async function generateF003(client, nodeName) {
|
|
|
34688
33329
|
};
|
|
34689
33330
|
return report;
|
|
34690
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
|
+
}
|
|
34691
33385
|
async function generateF004(client, nodeName) {
|
|
34692
33386
|
const report = createBaseReport("F004", "Autovacuum: heap bloat (estimated)", nodeName);
|
|
34693
33387
|
const postgresVersion = await getPostgresVersion(client);
|
|
34694
33388
|
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
|
|
34695
33389
|
let bloatedTables = [];
|
|
33390
|
+
let status = await getBloatCheckStatus(client);
|
|
34696
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
|
+
});
|
|
34697
33396
|
const sql = getMetricSql(METRIC_NAMES.F004, pgMajorVersion);
|
|
34698
33397
|
const bloatResult = await client.query(sql);
|
|
34699
33398
|
const vacuumStatsResult = await client.query(`
|
|
@@ -34737,7 +33436,8 @@ async function generateF004(client, nodeName) {
|
|
|
34737
33436
|
};
|
|
34738
33437
|
});
|
|
34739
33438
|
} catch (err) {
|
|
34740
|
-
|
|
33439
|
+
status = bloatErrorStatus(err);
|
|
33440
|
+
const errorMsg = status.error || "Unknown error";
|
|
34741
33441
|
console.error(`[F004] Error estimating table bloat: ${errorMsg}`);
|
|
34742
33442
|
if (errorMsg.includes("postgres_ai.")) {
|
|
34743
33443
|
console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
|
|
@@ -34747,6 +33447,7 @@ async function generateF004(client, nodeName) {
|
|
|
34747
33447
|
const totalCount = bloatedTables.length;
|
|
34748
33448
|
const totalBloatSizeBytes = bloatedTables.reduce((sum, t) => sum + t.bloat_size, 0);
|
|
34749
33449
|
const dbEntry = {
|
|
33450
|
+
status,
|
|
34750
33451
|
bloated_tables: bloatedTables,
|
|
34751
33452
|
total_count: totalCount,
|
|
34752
33453
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
@@ -34765,7 +33466,12 @@ async function generateF005(client, nodeName) {
|
|
|
34765
33466
|
const postgresVersion = await getPostgresVersion(client);
|
|
34766
33467
|
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
|
|
34767
33468
|
let bloatedIndexes = [];
|
|
33469
|
+
let status = await getBloatCheckStatus(client);
|
|
34768
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
|
+
});
|
|
34769
33475
|
const sql = getMetricSql(METRIC_NAMES.F005, pgMajorVersion);
|
|
34770
33476
|
const bloatResult = await client.query(sql);
|
|
34771
33477
|
const vacuumStatsResult = await client.query(`
|
|
@@ -34814,7 +33520,8 @@ async function generateF005(client, nodeName) {
|
|
|
34814
33520
|
};
|
|
34815
33521
|
});
|
|
34816
33522
|
} catch (err) {
|
|
34817
|
-
|
|
33523
|
+
status = bloatErrorStatus(err);
|
|
33524
|
+
const errorMsg = status.error || "Unknown error";
|
|
34818
33525
|
console.error(`[F005] Error estimating index bloat: ${errorMsg}`);
|
|
34819
33526
|
if (errorMsg.includes("postgres_ai.")) {
|
|
34820
33527
|
console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
|
|
@@ -34824,6 +33531,7 @@ async function generateF005(client, nodeName) {
|
|
|
34824
33531
|
const totalCount = bloatedIndexes.length;
|
|
34825
33532
|
const totalBloatSizeBytes = bloatedIndexes.reduce((sum, idx) => sum + idx.bloat_size, 0);
|
|
34826
33533
|
const dbEntry = {
|
|
33534
|
+
status,
|
|
34827
33535
|
bloated_indexes: bloatedIndexes,
|
|
34828
33536
|
total_count: totalCount,
|
|
34829
33537
|
total_bloat_size_bytes: totalBloatSizeBytes,
|
|
@@ -35496,6 +34204,10 @@ function generateCheckSummary(checkId, report) {
|
|
|
35496
34204
|
return summarizeF001(nodeData);
|
|
35497
34205
|
case "F003":
|
|
35498
34206
|
return summarizeF003(nodeData);
|
|
34207
|
+
case "F004":
|
|
34208
|
+
return summarizeBloat(nodeData, "table");
|
|
34209
|
+
case "F005":
|
|
34210
|
+
return summarizeBloat(nodeData, "index");
|
|
35499
34211
|
case "G001":
|
|
35500
34212
|
return summarizeG001(nodeData);
|
|
35501
34213
|
case "G003":
|
|
@@ -35681,6 +34393,25 @@ function summarizeF003(nodeData) {
|
|
|
35681
34393
|
}
|
|
35682
34394
|
return { status: "warning", message: parts.join(", ") };
|
|
35683
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
|
+
}
|
|
35684
34415
|
function summarizeG001(nodeData) {
|
|
35685
34416
|
const data = nodeData?.data || {};
|
|
35686
34417
|
const settingsCount = Object.keys(data).length;
|
|
@@ -35713,7 +34444,7 @@ function summarizeG003(nodeData) {
|
|
|
35713
34444
|
}
|
|
35714
34445
|
|
|
35715
34446
|
// lib/instances.ts
|
|
35716
|
-
import * as
|
|
34447
|
+
import * as fs8 from "fs";
|
|
35717
34448
|
class InstancesParseError2 extends Error {
|
|
35718
34449
|
constructor(file, cause) {
|
|
35719
34450
|
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -35722,11 +34453,11 @@ class InstancesParseError2 extends Error {
|
|
|
35722
34453
|
}
|
|
35723
34454
|
}
|
|
35724
34455
|
function loadInstances2(file) {
|
|
35725
|
-
if (!
|
|
34456
|
+
if (!fs8.existsSync(file))
|
|
35726
34457
|
return [];
|
|
35727
|
-
if (
|
|
34458
|
+
if (fs8.lstatSync(file).isDirectory())
|
|
35728
34459
|
return [];
|
|
35729
|
-
const text =
|
|
34460
|
+
const text = fs8.readFileSync(file, "utf8");
|
|
35730
34461
|
if (text.trim() === "")
|
|
35731
34462
|
return [];
|
|
35732
34463
|
let parsed;
|
|
@@ -35759,22 +34490,22 @@ function buildInstance(name, connStr) {
|
|
|
35759
34490
|
};
|
|
35760
34491
|
}
|
|
35761
34492
|
function addInstanceToFile(file, instance) {
|
|
35762
|
-
if (
|
|
35763
|
-
|
|
34493
|
+
if (fs8.existsSync(file) && fs8.lstatSync(file).isDirectory()) {
|
|
34494
|
+
fs8.rmSync(file, { recursive: true, force: true });
|
|
35764
34495
|
}
|
|
35765
34496
|
const existing = loadInstances2(file);
|
|
35766
34497
|
if (existing.some((i3) => i3.name === instance.name)) {
|
|
35767
34498
|
throw new Error(`Monitoring target '${instance.name}' already exists`);
|
|
35768
34499
|
}
|
|
35769
34500
|
existing.push(instance);
|
|
35770
|
-
|
|
34501
|
+
fs8.writeFileSync(file, dump2(existing), "utf8");
|
|
35771
34502
|
}
|
|
35772
34503
|
function removeInstanceFromFile(file, name) {
|
|
35773
34504
|
const instances = loadInstances2(file);
|
|
35774
34505
|
const filtered = instances.filter((i3) => i3.name !== name);
|
|
35775
34506
|
if (filtered.length === instances.length)
|
|
35776
34507
|
return false;
|
|
35777
|
-
|
|
34508
|
+
fs8.writeFileSync(file, dump2(filtered), "utf8");
|
|
35778
34509
|
return true;
|
|
35779
34510
|
}
|
|
35780
34511
|
function extractSslmode(connStr) {
|
|
@@ -35869,13 +34600,13 @@ function stripMatchingQuotes(value) {
|
|
|
35869
34600
|
return trimmed;
|
|
35870
34601
|
}
|
|
35871
34602
|
var REQUIRED_ENV_KEYS = [
|
|
35872
|
-
{ key: "REPLICATOR_PASSWORD", defaultValue: () =>
|
|
34603
|
+
{ key: "REPLICATOR_PASSWORD", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.13" },
|
|
35873
34604
|
{ key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
|
|
35874
|
-
{ key: "VM_AUTH_PASSWORD", defaultValue: () =>
|
|
34605
|
+
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" }
|
|
35875
34606
|
];
|
|
35876
34607
|
function ensureRequiredEnvVars(projectDir) {
|
|
35877
|
-
const envFile =
|
|
35878
|
-
const existing =
|
|
34608
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
34609
|
+
const existing = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
|
|
35879
34610
|
const added = [];
|
|
35880
34611
|
const appendLines = [];
|
|
35881
34612
|
for (const spec of REQUIRED_ENV_KEYS) {
|
|
@@ -35894,7 +34625,7 @@ function ensureRequiredEnvVars(projectDir) {
|
|
|
35894
34625
|
` : "") + appendLines.join(`
|
|
35895
34626
|
`) + `
|
|
35896
34627
|
`;
|
|
35897
|
-
|
|
34628
|
+
fs9.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
|
|
35898
34629
|
return added;
|
|
35899
34630
|
}
|
|
35900
34631
|
async function execFilePromise(file, args) {
|
|
@@ -35959,7 +34690,7 @@ function expandHomePath(p) {
|
|
|
35959
34690
|
if (s === "~")
|
|
35960
34691
|
return os3.homedir();
|
|
35961
34692
|
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
35962
|
-
return
|
|
34693
|
+
return path7.join(os3.homedir(), s.slice(2));
|
|
35963
34694
|
}
|
|
35964
34695
|
return s;
|
|
35965
34696
|
}
|
|
@@ -36008,10 +34739,10 @@ function prepareOutputDirectory(outputOpt) {
|
|
|
36008
34739
|
if (!outputOpt)
|
|
36009
34740
|
return;
|
|
36010
34741
|
const outputDir = expandHomePath(outputOpt);
|
|
36011
|
-
const outputPath =
|
|
36012
|
-
if (!
|
|
34742
|
+
const outputPath = path7.isAbsolute(outputDir) ? outputDir : path7.resolve(process.cwd(), outputDir);
|
|
34743
|
+
if (!fs9.existsSync(outputPath)) {
|
|
36013
34744
|
try {
|
|
36014
|
-
|
|
34745
|
+
fs9.mkdirSync(outputPath, { recursive: true });
|
|
36015
34746
|
} catch (e) {
|
|
36016
34747
|
const errAny = e;
|
|
36017
34748
|
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
@@ -36037,9 +34768,15 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
36037
34768
|
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
36038
34769
|
return null;
|
|
36039
34770
|
}
|
|
36040
|
-
|
|
36041
|
-
|
|
36042
|
-
|
|
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
|
+
}
|
|
36043
34780
|
return;
|
|
36044
34781
|
}
|
|
36045
34782
|
const cfg = readConfig();
|
|
@@ -36047,7 +34784,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
36047
34784
|
let project = (opts.project || cfg.defaultProject || "").trim();
|
|
36048
34785
|
let projectWasGenerated = false;
|
|
36049
34786
|
if (!project) {
|
|
36050
|
-
project = `project_${
|
|
34787
|
+
project = `project_${crypto2.randomBytes(6).toString("hex")}`;
|
|
36051
34788
|
projectWasGenerated = true;
|
|
36052
34789
|
try {
|
|
36053
34790
|
writeConfig({ defaultProject: project });
|
|
@@ -36093,8 +34830,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
36093
34830
|
}
|
|
36094
34831
|
function writeReportFiles(reports, outputPath) {
|
|
36095
34832
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
36096
|
-
const filePath =
|
|
36097
|
-
|
|
34833
|
+
const filePath = path7.join(outputPath, `${checkId}.json`);
|
|
34834
|
+
fs9.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
36098
34835
|
const title = report.checkTitle || checkId;
|
|
36099
34836
|
console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
|
|
36100
34837
|
}
|
|
@@ -36139,7 +34876,7 @@ function getDefaultMonitoringProjectDir() {
|
|
|
36139
34876
|
const override = process.env.PGAI_PROJECT_DIR;
|
|
36140
34877
|
if (override && override.trim())
|
|
36141
34878
|
return override.trim();
|
|
36142
|
-
return
|
|
34879
|
+
return path7.join(getConfigDir(), "monitoring");
|
|
36143
34880
|
}
|
|
36144
34881
|
async function downloadText(url) {
|
|
36145
34882
|
const controller = new AbortController;
|
|
@@ -36156,12 +34893,12 @@ async function downloadText(url) {
|
|
|
36156
34893
|
}
|
|
36157
34894
|
async function ensureDefaultMonitoringProject() {
|
|
36158
34895
|
const projectDir = getDefaultMonitoringProjectDir();
|
|
36159
|
-
const composeFile =
|
|
36160
|
-
const instancesFile =
|
|
36161
|
-
if (!
|
|
36162
|
-
|
|
34896
|
+
const composeFile = path7.resolve(projectDir, "docker-compose.yml");
|
|
34897
|
+
const instancesFile = path7.resolve(projectDir, "instances.yml");
|
|
34898
|
+
if (!fs9.existsSync(projectDir)) {
|
|
34899
|
+
fs9.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
36163
34900
|
}
|
|
36164
|
-
if (!
|
|
34901
|
+
if (!fs9.existsSync(composeFile)) {
|
|
36165
34902
|
const refs = [
|
|
36166
34903
|
process.env.PGAI_PROJECT_REF,
|
|
36167
34904
|
package_default.version,
|
|
@@ -36173,39 +34910,39 @@ async function ensureDefaultMonitoringProject() {
|
|
|
36173
34910
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
36174
34911
|
try {
|
|
36175
34912
|
const text = await downloadText(url);
|
|
36176
|
-
|
|
34913
|
+
fs9.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
|
|
36177
34914
|
break;
|
|
36178
34915
|
} catch (err) {
|
|
36179
34916
|
lastErr = err;
|
|
36180
34917
|
}
|
|
36181
34918
|
}
|
|
36182
|
-
if (!
|
|
34919
|
+
if (!fs9.existsSync(composeFile)) {
|
|
36183
34920
|
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
36184
34921
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
36185
34922
|
}
|
|
36186
34923
|
}
|
|
36187
|
-
if (
|
|
36188
|
-
|
|
34924
|
+
if (fs9.existsSync(instancesFile) && fs9.lstatSync(instancesFile).isDirectory()) {
|
|
34925
|
+
fs9.rmSync(instancesFile, { recursive: true, force: true });
|
|
36189
34926
|
}
|
|
36190
|
-
if (!
|
|
34927
|
+
if (!fs9.existsSync(instancesFile)) {
|
|
36191
34928
|
const header = `# PostgreSQL instances to monitor
|
|
36192
34929
|
` + `# Add your instances using: pgai mon targets add <connection-string> <name>
|
|
36193
34930
|
|
|
36194
34931
|
`;
|
|
36195
|
-
|
|
34932
|
+
fs9.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
|
|
36196
34933
|
}
|
|
36197
|
-
const pgwatchConfig =
|
|
36198
|
-
if (!
|
|
36199
|
-
|
|
34934
|
+
const pgwatchConfig = path7.resolve(projectDir, ".pgwatch-config");
|
|
34935
|
+
if (!fs9.existsSync(pgwatchConfig)) {
|
|
34936
|
+
fs9.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
|
|
36200
34937
|
}
|
|
36201
|
-
const envFile =
|
|
36202
|
-
if (!
|
|
34938
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
34939
|
+
if (!fs9.existsSync(envFile)) {
|
|
36203
34940
|
const envText = `PGAI_TAG=${package_default.version}
|
|
36204
34941
|
# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
|
|
36205
34942
|
`;
|
|
36206
|
-
|
|
34943
|
+
fs9.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
|
|
36207
34944
|
}
|
|
36208
|
-
return { fs:
|
|
34945
|
+
return { fs: fs9, path: path7, projectDir, composeFile, instancesFile };
|
|
36209
34946
|
}
|
|
36210
34947
|
function sanitizeTagForBackup(tag) {
|
|
36211
34948
|
if (tag == null)
|
|
@@ -36214,10 +34951,10 @@ function sanitizeTagForBackup(tag) {
|
|
|
36214
34951
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
36215
34952
|
}
|
|
36216
34953
|
function readDeployedTag(projectDir) {
|
|
36217
|
-
const envFile =
|
|
36218
|
-
if (!
|
|
34954
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
34955
|
+
if (!fs9.existsSync(envFile))
|
|
36219
34956
|
return null;
|
|
36220
|
-
const m =
|
|
34957
|
+
const m = fs9.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
36221
34958
|
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
36222
34959
|
}
|
|
36223
34960
|
function isValidComposeYaml(text) {
|
|
@@ -36254,10 +34991,10 @@ function composeContentEqual(a, b) {
|
|
|
36254
34991
|
return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
|
|
36255
34992
|
}
|
|
36256
34993
|
async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
36257
|
-
if (
|
|
34994
|
+
if (fs9.existsSync(path7.resolve(projectDir, ".git")))
|
|
36258
34995
|
return false;
|
|
36259
|
-
const composeFile =
|
|
36260
|
-
if (!
|
|
34996
|
+
const composeFile = path7.resolve(projectDir, "docker-compose.yml");
|
|
34997
|
+
if (!fs9.existsSync(composeFile))
|
|
36261
34998
|
return false;
|
|
36262
34999
|
const refs = [
|
|
36263
35000
|
process.env.PGAI_PROJECT_REF,
|
|
@@ -36270,24 +35007,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
36270
35007
|
console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
|
|
36271
35008
|
return false;
|
|
36272
35009
|
}
|
|
36273
|
-
const existing =
|
|
35010
|
+
const existing = fs9.readFileSync(composeFile, "utf8");
|
|
36274
35011
|
if (composeContentEqual(existing, fetched))
|
|
36275
35012
|
return false;
|
|
36276
35013
|
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
36277
35014
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
36278
|
-
const oldHash =
|
|
36279
|
-
const backup =
|
|
35015
|
+
const oldHash = crypto2.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
35016
|
+
const backup = path7.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
36280
35017
|
let backupName = null;
|
|
36281
35018
|
try {
|
|
36282
|
-
|
|
36283
|
-
backupName =
|
|
35019
|
+
fs9.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
35020
|
+
backupName = path7.basename(backup);
|
|
36284
35021
|
} catch (err) {
|
|
36285
35022
|
const e = err;
|
|
36286
35023
|
if (e && e.code === "EEXIST") {
|
|
36287
|
-
backupName =
|
|
35024
|
+
backupName = path7.basename(backup);
|
|
36288
35025
|
}
|
|
36289
35026
|
}
|
|
36290
|
-
|
|
35027
|
+
fs9.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
|
|
36291
35028
|
const backupNote = backupName ? ` (backup: ${backupName})` : "";
|
|
36292
35029
|
console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
|
|
36293
35030
|
return true;
|
|
@@ -37264,7 +36001,7 @@ program2.command("unprepare-db [conn]").description("remove monitoring setup: dr
|
|
|
37264
36001
|
closeReadline();
|
|
37265
36002
|
}
|
|
37266
36003
|
});
|
|
37267
|
-
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", [
|
|
37268
36005
|
"",
|
|
37269
36006
|
"Available checks:",
|
|
37270
36007
|
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
|
|
@@ -37275,7 +36012,7 @@ program2.command("checkup [checkIdOrConn] [conn]").description("generate health
|
|
|
37275
36012
|
" postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
|
|
37276
36013
|
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
37277
36014
|
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
|
|
37278
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --
|
|
36015
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --markdown"
|
|
37279
36016
|
].join(`
|
|
37280
36017
|
`)).action(async (checkIdOrConn, connArg, opts, cmd) => {
|
|
37281
36018
|
const checkIdPattern = /^[A-Z]\d{3}$/i;
|
|
@@ -37315,6 +36052,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
37315
36052
|
return;
|
|
37316
36053
|
}
|
|
37317
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
|
+
}
|
|
37318
36062
|
let shouldUpload = !uploadExplicitlyDisabled;
|
|
37319
36063
|
const outputPath = prepareOutputDirectory(opts.output);
|
|
37320
36064
|
if (outputPath === null) {
|
|
@@ -37497,7 +36241,9 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
37497
36241
|
console.log(`
|
|
37498
36242
|
For details:`);
|
|
37499
36243
|
console.log(" --json Output JSON");
|
|
37500
|
-
|
|
36244
|
+
if (!uploadExplicitlyDisabled) {
|
|
36245
|
+
console.log(" --markdown Output markdown via PostgresAI API");
|
|
36246
|
+
}
|
|
37501
36247
|
console.log(" --output <dir> Save to directory");
|
|
37502
36248
|
}
|
|
37503
36249
|
} catch (error2) {
|
|
@@ -37521,12 +36267,12 @@ function resolvePaths() {
|
|
|
37521
36267
|
const startDir = process.cwd();
|
|
37522
36268
|
let currentDir = startDir;
|
|
37523
36269
|
while (true) {
|
|
37524
|
-
const composeFile =
|
|
37525
|
-
if (
|
|
37526
|
-
const instancesFile =
|
|
37527
|
-
return { fs:
|
|
36270
|
+
const composeFile = path7.resolve(currentDir, "docker-compose.yml");
|
|
36271
|
+
if (fs9.existsSync(composeFile)) {
|
|
36272
|
+
const instancesFile = path7.resolve(currentDir, "instances.yml");
|
|
36273
|
+
return { fs: fs9, path: path7, projectDir: currentDir, composeFile, instancesFile };
|
|
37528
36274
|
}
|
|
37529
|
-
const parentDir =
|
|
36275
|
+
const parentDir = path7.dirname(currentDir);
|
|
37530
36276
|
if (parentDir === currentDir)
|
|
37531
36277
|
break;
|
|
37532
36278
|
currentDir = parentDir;
|
|
@@ -37658,10 +36404,10 @@ function resolveAdoptedProject(reg) {
|
|
|
37658
36404
|
}
|
|
37659
36405
|
function updatePgwatchConfig(configPath, updates) {
|
|
37660
36406
|
let lines = [];
|
|
37661
|
-
if (
|
|
37662
|
-
const stats =
|
|
36407
|
+
if (fs9.existsSync(configPath)) {
|
|
36408
|
+
const stats = fs9.statSync(configPath);
|
|
37663
36409
|
if (!stats.isDirectory()) {
|
|
37664
|
-
const content =
|
|
36410
|
+
const content = fs9.readFileSync(configPath, "utf8");
|
|
37665
36411
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
37666
36412
|
}
|
|
37667
36413
|
}
|
|
@@ -37673,7 +36419,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
37673
36419
|
lines.push(`${key}=${value}`);
|
|
37674
36420
|
}
|
|
37675
36421
|
}
|
|
37676
|
-
|
|
36422
|
+
fs9.writeFileSync(configPath, lines.join(`
|
|
37677
36423
|
`) + `
|
|
37678
36424
|
`, { encoding: "utf8", mode: 384 });
|
|
37679
36425
|
}
|
|
@@ -37711,12 +36457,12 @@ async function runCompose(args, grafanaPassword) {
|
|
|
37711
36457
|
if (grafanaPassword) {
|
|
37712
36458
|
env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
|
|
37713
36459
|
} else {
|
|
37714
|
-
const cfgPath =
|
|
37715
|
-
if (
|
|
36460
|
+
const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
36461
|
+
if (fs9.existsSync(cfgPath)) {
|
|
37716
36462
|
try {
|
|
37717
|
-
const stats =
|
|
36463
|
+
const stats = fs9.statSync(cfgPath);
|
|
37718
36464
|
if (!stats.isDirectory()) {
|
|
37719
|
-
const content =
|
|
36465
|
+
const content = fs9.readFileSync(cfgPath, "utf8");
|
|
37720
36466
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
37721
36467
|
if (match) {
|
|
37722
36468
|
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
@@ -37729,10 +36475,10 @@ async function runCompose(args, grafanaPassword) {
|
|
|
37729
36475
|
}
|
|
37730
36476
|
}
|
|
37731
36477
|
}
|
|
37732
|
-
const envFilePath =
|
|
37733
|
-
if (
|
|
36478
|
+
const envFilePath = path7.resolve(projectDir, ".env");
|
|
36479
|
+
if (fs9.existsSync(envFilePath)) {
|
|
37734
36480
|
try {
|
|
37735
|
-
const envContent =
|
|
36481
|
+
const envContent = fs9.readFileSync(envFilePath, "utf8");
|
|
37736
36482
|
if (!env.VM_AUTH_USERNAME) {
|
|
37737
36483
|
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
37738
36484
|
if (m)
|
|
@@ -37789,20 +36535,20 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
37789
36535
|
console.log(`Project directory: ${projectDir}
|
|
37790
36536
|
`);
|
|
37791
36537
|
if (opts.project) {
|
|
37792
|
-
const cfgPath2 =
|
|
36538
|
+
const cfgPath2 = path7.resolve(projectDir, ".pgwatch-config");
|
|
37793
36539
|
updatePgwatchConfig(cfgPath2, { project_name: opts.project });
|
|
37794
36540
|
console.log(`Using project name: ${opts.project}
|
|
37795
36541
|
`);
|
|
37796
36542
|
}
|
|
37797
|
-
const envFile =
|
|
36543
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
37798
36544
|
let existingRegistry = null;
|
|
37799
36545
|
let existingPassword = null;
|
|
37800
36546
|
let existingReplicatorPassword = null;
|
|
37801
36547
|
let existingVmAuthUsername = null;
|
|
37802
36548
|
let existingVmAuthPassword = null;
|
|
37803
36549
|
let previousTag = null;
|
|
37804
|
-
if (
|
|
37805
|
-
const existingEnv =
|
|
36550
|
+
if (fs9.existsSync(envFile)) {
|
|
36551
|
+
const existingEnv = fs9.readFileSync(envFile, "utf8");
|
|
37806
36552
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
37807
36553
|
if (previousTagMatch)
|
|
37808
36554
|
previousTag = previousTagMatch[1].trim();
|
|
@@ -37830,10 +36576,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
37830
36576
|
if (existingPassword) {
|
|
37831
36577
|
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
37832
36578
|
}
|
|
37833
|
-
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword ||
|
|
36579
|
+
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto2.randomBytes(32).toString("hex")}`);
|
|
37834
36580
|
envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
|
|
37835
|
-
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword ||
|
|
37836
|
-
|
|
36581
|
+
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto2.randomBytes(18).toString("base64")}`);
|
|
36582
|
+
fs9.writeFileSync(envFile, envLines.join(`
|
|
37837
36583
|
`) + `
|
|
37838
36584
|
`, { encoding: "utf8", mode: 384 });
|
|
37839
36585
|
await refreshBundledComposeIfStale(projectDir, previousTag);
|
|
@@ -37870,7 +36616,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
37870
36616
|
if (apiKey) {
|
|
37871
36617
|
console.log("Using API key provided via --api-key parameter");
|
|
37872
36618
|
writeConfig({ apiKey });
|
|
37873
|
-
updatePgwatchConfig(
|
|
36619
|
+
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
|
|
37874
36620
|
console.log(`\u2713 API key saved
|
|
37875
36621
|
`);
|
|
37876
36622
|
} else if (opts.yes) {
|
|
@@ -37887,7 +36633,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
37887
36633
|
const trimmedKey = inputApiKey.trim();
|
|
37888
36634
|
if (trimmedKey) {
|
|
37889
36635
|
writeConfig({ apiKey: trimmedKey });
|
|
37890
|
-
updatePgwatchConfig(
|
|
36636
|
+
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
|
|
37891
36637
|
apiKey = trimmedKey;
|
|
37892
36638
|
console.log(`\u2713 API key saved
|
|
37893
36639
|
`);
|
|
@@ -37921,7 +36667,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
37921
36667
|
# Add your instances using: postgres-ai mon targets add
|
|
37922
36668
|
|
|
37923
36669
|
`;
|
|
37924
|
-
|
|
36670
|
+
fs9.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
|
|
37925
36671
|
console.log(`Instances file: ${instancesPath2}`);
|
|
37926
36672
|
console.log(`Project directory: ${projectDir2}
|
|
37927
36673
|
`);
|
|
@@ -38025,17 +36771,17 @@ You can provide either:`);
|
|
|
38025
36771
|
}
|
|
38026
36772
|
} else {
|
|
38027
36773
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
38028
|
-
const currentDir =
|
|
36774
|
+
const currentDir = path7.dirname(fileURLToPath2(import.meta.url));
|
|
38029
36775
|
const demoCandidates = [
|
|
38030
|
-
|
|
38031
|
-
|
|
36776
|
+
path7.resolve(currentDir, "..", "..", "instances.demo.yml"),
|
|
36777
|
+
path7.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
|
|
38032
36778
|
];
|
|
38033
|
-
const demoSrc = demoCandidates.find((p) =>
|
|
36779
|
+
const demoSrc = demoCandidates.find((p) => fs9.existsSync(p));
|
|
38034
36780
|
if (demoSrc) {
|
|
38035
|
-
if (
|
|
38036
|
-
|
|
36781
|
+
if (fs9.existsSync(instancesPath) && fs9.lstatSync(instancesPath).isDirectory()) {
|
|
36782
|
+
fs9.rmSync(instancesPath, { recursive: true, force: true });
|
|
38037
36783
|
}
|
|
38038
|
-
|
|
36784
|
+
fs9.copyFileSync(demoSrc, instancesPath);
|
|
38039
36785
|
console.log(`\u2713 Demo monitoring target configured
|
|
38040
36786
|
`);
|
|
38041
36787
|
} else {
|
|
@@ -38054,15 +36800,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
38054
36800
|
console.log(`\u2713 Configuration updated
|
|
38055
36801
|
`);
|
|
38056
36802
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
38057
|
-
const cfgPath =
|
|
36803
|
+
const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
38058
36804
|
let grafanaPassword = "";
|
|
38059
36805
|
let vmAuthUsername = "";
|
|
38060
36806
|
let vmAuthPassword = "";
|
|
38061
36807
|
try {
|
|
38062
|
-
if (
|
|
38063
|
-
const stats =
|
|
36808
|
+
if (fs9.existsSync(cfgPath)) {
|
|
36809
|
+
const stats = fs9.statSync(cfgPath);
|
|
38064
36810
|
if (!stats.isDirectory()) {
|
|
38065
|
-
const content =
|
|
36811
|
+
const content = fs9.readFileSync(cfgPath, "utf8");
|
|
38066
36812
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
38067
36813
|
if (match) {
|
|
38068
36814
|
grafanaPassword = match[1].trim();
|
|
@@ -38074,15 +36820,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
38074
36820
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
38075
36821
|
grafanaPassword = password.trim().replace(/\n/g, "");
|
|
38076
36822
|
let configContent = "";
|
|
38077
|
-
if (
|
|
38078
|
-
const stats =
|
|
36823
|
+
if (fs9.existsSync(cfgPath)) {
|
|
36824
|
+
const stats = fs9.statSync(cfgPath);
|
|
38079
36825
|
if (!stats.isDirectory()) {
|
|
38080
|
-
configContent =
|
|
36826
|
+
configContent = fs9.readFileSync(cfgPath, "utf8");
|
|
38081
36827
|
}
|
|
38082
36828
|
}
|
|
38083
36829
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
38084
36830
|
lines.push(`grafana_password=${grafanaPassword}`);
|
|
38085
|
-
|
|
36831
|
+
fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
38086
36832
|
`) + `
|
|
38087
36833
|
`, "utf8");
|
|
38088
36834
|
}
|
|
@@ -38095,9 +36841,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
38095
36841
|
grafanaPassword = "demo";
|
|
38096
36842
|
}
|
|
38097
36843
|
try {
|
|
38098
|
-
const envFile2 =
|
|
38099
|
-
if (
|
|
38100
|
-
const envContent =
|
|
36844
|
+
const envFile2 = path7.resolve(projectDir, ".env");
|
|
36845
|
+
if (fs9.existsSync(envFile2)) {
|
|
36846
|
+
const envContent = fs9.readFileSync(envFile2, "utf8");
|
|
38101
36847
|
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
38102
36848
|
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
38103
36849
|
if (userMatch)
|
|
@@ -38113,13 +36859,13 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
38113
36859
|
vmAuthPassword = vmPass.trim().replace(/\n/g, "");
|
|
38114
36860
|
}
|
|
38115
36861
|
let envContent = "";
|
|
38116
|
-
if (
|
|
38117
|
-
envContent =
|
|
36862
|
+
if (fs9.existsSync(envFile2)) {
|
|
36863
|
+
envContent = fs9.readFileSync(envFile2, "utf8");
|
|
38118
36864
|
}
|
|
38119
36865
|
const envLines2 = envContent.split(/\r?\n/).filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l)).filter((l, i3, arr) => !(i3 === arr.length - 1 && l === ""));
|
|
38120
36866
|
envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
38121
36867
|
envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
38122
|
-
|
|
36868
|
+
fs9.writeFileSync(envFile2, envLines2.join(`
|
|
38123
36869
|
`) + `
|
|
38124
36870
|
`, { encoding: "utf8", mode: 384 });
|
|
38125
36871
|
}
|
|
@@ -38152,7 +36898,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
38152
36898
|
});
|
|
38153
36899
|
const adoptedProject = resolveAdoptedProject(reg);
|
|
38154
36900
|
if (adoptedProject != null) {
|
|
38155
|
-
updatePgwatchConfig(
|
|
36901
|
+
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
|
|
38156
36902
|
project_name: adoptedProject
|
|
38157
36903
|
});
|
|
38158
36904
|
const verb = reg?.created ? "Registered" : "Adopted";
|
|
@@ -38368,11 +37114,11 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
38368
37114
|
console.log(`Project Directory: ${projectDir}`);
|
|
38369
37115
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
38370
37116
|
console.log(`Instances File: ${instancesFile}`);
|
|
38371
|
-
if (
|
|
37117
|
+
if (fs9.existsSync(instancesFile) && !fs9.lstatSync(instancesFile).isDirectory()) {
|
|
38372
37118
|
console.log(`
|
|
38373
37119
|
Instances configuration:
|
|
38374
37120
|
`);
|
|
38375
|
-
const text =
|
|
37121
|
+
const text = fs9.readFileSync(instancesFile, "utf8");
|
|
38376
37122
|
process.stdout.write(text);
|
|
38377
37123
|
if (!/\n$/.test(text))
|
|
38378
37124
|
console.log();
|
|
@@ -38421,8 +37167,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
38421
37167
|
console.log("\u2713 .env is up to date");
|
|
38422
37168
|
}
|
|
38423
37169
|
console.log();
|
|
38424
|
-
const gitDir =
|
|
38425
|
-
if (
|
|
37170
|
+
const gitDir = path7.resolve(projectDir, ".git");
|
|
37171
|
+
if (fs9.existsSync(gitDir)) {
|
|
38426
37172
|
console.log("Fetching latest changes...");
|
|
38427
37173
|
await execFilePromise("git", ["fetch", "origin"]);
|
|
38428
37174
|
const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -38577,7 +37323,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
38577
37323
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
38578
37324
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
38579
37325
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
38580
|
-
if (!
|
|
37326
|
+
if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
|
|
38581
37327
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
38582
37328
|
process.exitCode = 1;
|
|
38583
37329
|
return;
|
|
@@ -38640,7 +37386,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
38640
37386
|
});
|
|
38641
37387
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
38642
37388
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
38643
|
-
if (!
|
|
37389
|
+
if (!fs9.existsSync(file) || fs9.lstatSync(file).isDirectory()) {
|
|
38644
37390
|
console.error("instances.yml not found");
|
|
38645
37391
|
process.exitCode = 1;
|
|
38646
37392
|
return;
|
|
@@ -38668,7 +37414,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
38668
37414
|
});
|
|
38669
37415
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
38670
37416
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
38671
|
-
if (!
|
|
37417
|
+
if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
|
|
38672
37418
|
console.error("instances.yml not found");
|
|
38673
37419
|
process.exitCode = 1;
|
|
38674
37420
|
return;
|
|
@@ -38712,7 +37458,7 @@ targets.command("test <name>").description("test monitoring target database conn
|
|
|
38712
37458
|
}
|
|
38713
37459
|
});
|
|
38714
37460
|
var auth = program2.command("auth").description("authentication and API key management");
|
|
38715
|
-
|
|
37461
|
+
async function runAuthLogin(opts) {
|
|
38716
37462
|
if (opts.setKey) {
|
|
38717
37463
|
const trimmedKey = opts.setKey.trim();
|
|
38718
37464
|
if (!trimmedKey) {
|
|
@@ -38913,7 +37659,12 @@ Authentication failed: ${message}`);
|
|
|
38913
37659
|
console.error(`Authentication error: ${message}`);
|
|
38914
37660
|
process.exit(1);
|
|
38915
37661
|
}
|
|
38916
|
-
}
|
|
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);
|
|
38917
37668
|
auth.command("show-key").description("show API key (masked)").action(async () => {
|
|
38918
37669
|
const cfg = readConfig();
|
|
38919
37670
|
if (!cfg.apiKey) {
|
|
@@ -38930,15 +37681,15 @@ To authenticate, run: pgai auth`);
|
|
|
38930
37681
|
});
|
|
38931
37682
|
auth.command("remove-key").description("remove API key").action(async () => {
|
|
38932
37683
|
const newConfigPath = getConfigPath();
|
|
38933
|
-
const hasNewConfig =
|
|
37684
|
+
const hasNewConfig = fs9.existsSync(newConfigPath);
|
|
38934
37685
|
let legacyPath;
|
|
38935
37686
|
try {
|
|
38936
37687
|
const { projectDir } = await resolveOrInitPaths();
|
|
38937
|
-
legacyPath =
|
|
37688
|
+
legacyPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
38938
37689
|
} catch {
|
|
38939
|
-
legacyPath =
|
|
37690
|
+
legacyPath = path7.resolve(process.cwd(), ".pgwatch-config");
|
|
38940
37691
|
}
|
|
38941
|
-
const hasLegacyConfig =
|
|
37692
|
+
const hasLegacyConfig = fs9.existsSync(legacyPath) && fs9.statSync(legacyPath).isFile();
|
|
38942
37693
|
if (!hasNewConfig && !hasLegacyConfig) {
|
|
38943
37694
|
console.log("No API key configured");
|
|
38944
37695
|
return;
|
|
@@ -38948,11 +37699,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
|
|
|
38948
37699
|
}
|
|
38949
37700
|
if (hasLegacyConfig) {
|
|
38950
37701
|
try {
|
|
38951
|
-
const content =
|
|
37702
|
+
const content = fs9.readFileSync(legacyPath, "utf8");
|
|
38952
37703
|
const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
|
|
38953
37704
|
`).replace(/\n+$/g, `
|
|
38954
37705
|
`);
|
|
38955
|
-
|
|
37706
|
+
fs9.writeFileSync(legacyPath, filtered, "utf8");
|
|
38956
37707
|
} catch (err) {
|
|
38957
37708
|
console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
|
|
38958
37709
|
}
|
|
@@ -38963,7 +37714,7 @@ To authenticate again, run: pgai auth`);
|
|
|
38963
37714
|
});
|
|
38964
37715
|
mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
|
|
38965
37716
|
const { projectDir } = await resolveOrInitPaths();
|
|
38966
|
-
const cfgPath =
|
|
37717
|
+
const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
38967
37718
|
try {
|
|
38968
37719
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
38969
37720
|
const newPassword = password.trim().replace(/\n/g, "");
|
|
@@ -38973,17 +37724,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
|
|
|
38973
37724
|
return;
|
|
38974
37725
|
}
|
|
38975
37726
|
let configContent = "";
|
|
38976
|
-
if (
|
|
38977
|
-
const stats =
|
|
37727
|
+
if (fs9.existsSync(cfgPath)) {
|
|
37728
|
+
const stats = fs9.statSync(cfgPath);
|
|
38978
37729
|
if (stats.isDirectory()) {
|
|
38979
37730
|
console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
|
|
38980
37731
|
} else {
|
|
38981
|
-
configContent =
|
|
37732
|
+
configContent = fs9.readFileSync(cfgPath, "utf8");
|
|
38982
37733
|
}
|
|
38983
37734
|
}
|
|
38984
37735
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
38985
37736
|
lines.push(`grafana_password=${newPassword}`);
|
|
38986
|
-
|
|
37737
|
+
fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
38987
37738
|
`) + `
|
|
38988
37739
|
`, "utf8");
|
|
38989
37740
|
console.log("\u2713 New Grafana password generated and saved");
|
|
@@ -39005,19 +37756,19 @@ Note: This command requires 'openssl' to be installed`);
|
|
|
39005
37756
|
});
|
|
39006
37757
|
mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
|
|
39007
37758
|
const { projectDir } = await resolveOrInitPaths();
|
|
39008
|
-
const cfgPath =
|
|
39009
|
-
if (!
|
|
37759
|
+
const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
37760
|
+
if (!fs9.existsSync(cfgPath)) {
|
|
39010
37761
|
console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
|
|
39011
37762
|
process.exitCode = 1;
|
|
39012
37763
|
return;
|
|
39013
37764
|
}
|
|
39014
|
-
const stats =
|
|
37765
|
+
const stats = fs9.statSync(cfgPath);
|
|
39015
37766
|
if (stats.isDirectory()) {
|
|
39016
37767
|
console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
|
|
39017
37768
|
process.exitCode = 1;
|
|
39018
37769
|
return;
|
|
39019
37770
|
}
|
|
39020
|
-
const content =
|
|
37771
|
+
const content = fs9.readFileSync(cfgPath, "utf8");
|
|
39021
37772
|
const lines = content.split(/\r?\n/);
|
|
39022
37773
|
let password = "";
|
|
39023
37774
|
for (const line of lines) {
|
|
@@ -39037,9 +37788,9 @@ Grafana credentials:`);
|
|
|
39037
37788
|
console.log(" URL: http://localhost:3000");
|
|
39038
37789
|
console.log(" Username: monitor");
|
|
39039
37790
|
console.log(` Password: ${password}`);
|
|
39040
|
-
const envFile =
|
|
39041
|
-
if (
|
|
39042
|
-
const envContent =
|
|
37791
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
37792
|
+
if (fs9.existsSync(envFile)) {
|
|
37793
|
+
const envContent = fs9.readFileSync(envFile, "utf8");
|
|
39043
37794
|
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
39044
37795
|
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
39045
37796
|
if (vmUser && vmPass) {
|
|
@@ -39765,13 +38516,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
|
|
|
39765
38516
|
});
|
|
39766
38517
|
spinner.stop();
|
|
39767
38518
|
if (opts.output) {
|
|
39768
|
-
const dir =
|
|
39769
|
-
|
|
38519
|
+
const dir = path7.resolve(opts.output);
|
|
38520
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
39770
38521
|
for (const f of result) {
|
|
39771
|
-
const safeName =
|
|
39772
|
-
const filePath =
|
|
38522
|
+
const safeName = path7.basename(f.filename);
|
|
38523
|
+
const filePath = path7.join(dir, safeName);
|
|
39773
38524
|
const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
|
|
39774
|
-
|
|
38525
|
+
fs9.writeFileSync(filePath, content, "utf-8");
|
|
39775
38526
|
console.log(filePath);
|
|
39776
38527
|
}
|
|
39777
38528
|
} else if (opts.json) {
|
|
@@ -39816,62 +38567,45 @@ function tryParseJson(s) {
|
|
|
39816
38567
|
return s;
|
|
39817
38568
|
}
|
|
39818
38569
|
}
|
|
39819
|
-
function
|
|
39820
|
-
|
|
39821
|
-
return r.command;
|
|
39822
|
-
if (r.plan_text !== undefined || r.plan_json !== undefined)
|
|
39823
|
-
return "plan";
|
|
39824
|
-
if (r.row_count !== undefined || r.result_rows !== undefined)
|
|
39825
|
-
return "exec";
|
|
39826
|
-
if (r.hypo_used !== undefined)
|
|
39827
|
-
return "hypo";
|
|
39828
|
-
if (r.terminated !== undefined)
|
|
39829
|
-
return "terminate";
|
|
39830
|
-
if (r.reset !== undefined)
|
|
39831
|
-
return "reset";
|
|
39832
|
-
if (r.snapshot !== undefined)
|
|
39833
|
-
return "activity";
|
|
39834
|
-
return null;
|
|
39835
|
-
}
|
|
39836
|
-
function printJoeOutcome(command, outcome, json3, budgetMs) {
|
|
39837
|
-
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS2;
|
|
38570
|
+
function printJoeOutcome(outcome, json3, budgetMs) {
|
|
38571
|
+
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
39838
38572
|
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
39839
38573
|
if (outcome.budgetExpired) {
|
|
39840
38574
|
if (json3) {
|
|
39841
38575
|
console.log(JSON.stringify({
|
|
39842
38576
|
command_id: outcome.commandId,
|
|
39843
38577
|
status: outcome.status,
|
|
39844
|
-
session_id: outcome.sessionId,
|
|
39845
38578
|
budget_expired: true,
|
|
39846
38579
|
resume: `pgai joe result ${outcome.commandId}`
|
|
39847
38580
|
}, null, 2));
|
|
39848
38581
|
} else {
|
|
39849
|
-
console.log(`
|
|
38582
|
+
console.log(`started ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
|
|
39850
38583
|
}
|
|
39851
38584
|
return;
|
|
39852
38585
|
}
|
|
39853
|
-
const
|
|
39854
|
-
if (outcome.status === "
|
|
39855
|
-
if (
|
|
39856
|
-
console.
|
|
39857
|
-
|
|
39858
|
-
|
|
39859
|
-
|
|
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);
|
|
39860
38598
|
if (body)
|
|
39861
38599
|
console.log(body);
|
|
39862
38600
|
}
|
|
39863
38601
|
return;
|
|
39864
38602
|
}
|
|
39865
|
-
if (
|
|
39866
|
-
|
|
39867
|
-
|
|
39868
|
-
process.exitCode = 1;
|
|
39869
|
-
return;
|
|
39870
|
-
}
|
|
39871
|
-
console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai joe result ${outcome.commandId}`);
|
|
38603
|
+
if (json3 && output)
|
|
38604
|
+
console.log(JSON.stringify(output, null, 2));
|
|
38605
|
+
console.error(`command ${outcome.commandId} error: ${output?.error ?? "command failed"}`);
|
|
39872
38606
|
process.exitCode = 1;
|
|
39873
38607
|
}
|
|
39874
|
-
async function runJoeCli(command,
|
|
38608
|
+
async function runJoeCli(command, arg, opts) {
|
|
39875
38609
|
try {
|
|
39876
38610
|
const rootOpts = program2.opts();
|
|
39877
38611
|
const cfg = readConfig();
|
|
@@ -39882,27 +38616,29 @@ async function runJoeCli(command, sql, args, opts) {
|
|
|
39882
38616
|
return;
|
|
39883
38617
|
}
|
|
39884
38618
|
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
39885
|
-
|
|
39886
|
-
|
|
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).");
|
|
39887
38622
|
process.exitCode = 1;
|
|
39888
38623
|
return;
|
|
39889
38624
|
}
|
|
39890
38625
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39891
|
-
|
|
39892
|
-
|
|
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({
|
|
39893
38631
|
apiKey,
|
|
39894
38632
|
apiBaseUrl,
|
|
39895
38633
|
command,
|
|
39896
|
-
project: projectRef,
|
|
39897
|
-
|
|
39898
|
-
|
|
39899
|
-
session: opts.session ?? null,
|
|
39900
|
-
newSession: !!opts.newSession,
|
|
38634
|
+
project: projectRef || undefined,
|
|
38635
|
+
instanceId: instanceRef || undefined,
|
|
38636
|
+
input: { arg, variant: opts.variant ?? null },
|
|
39901
38637
|
orgId: cfg.orgId ?? undefined,
|
|
39902
38638
|
budgetMs,
|
|
39903
38639
|
debug: !!opts.debug
|
|
39904
38640
|
});
|
|
39905
|
-
printJoeOutcome(
|
|
38641
|
+
printJoeOutcome(outcome, !!opts.json, budgetMs);
|
|
39906
38642
|
} catch (err) {
|
|
39907
38643
|
const message = err instanceof Error ? err.message : String(err);
|
|
39908
38644
|
console.error(message);
|
|
@@ -39910,47 +38646,34 @@ async function runJoeCli(command, sql, args, opts) {
|
|
|
39910
38646
|
}
|
|
39911
38647
|
}
|
|
39912
38648
|
function withJoeOptions(cmd) {
|
|
39913
|
-
return cmd.option("--
|
|
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");
|
|
39914
38650
|
}
|
|
39915
|
-
var joe = program2.command("joe").description("Joe
|
|
38651
|
+
var joe = program2.command("joe").description("Joe \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones");
|
|
39916
38652
|
withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
|
|
39917
|
-
await runJoeCli("plan", sql,
|
|
38653
|
+
await runJoeCli("plan", sql, opts);
|
|
39918
38654
|
});
|
|
39919
|
-
withJoeOptions(joe.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the
|
|
39920
|
-
await runJoeCli("explain", sql,
|
|
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);
|
|
39921
38657
|
});
|
|
39922
38658
|
withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
|
|
39923
|
-
await runJoeCli("exec", sql,
|
|
38659
|
+
await runJoeCli("exec", sql, opts);
|
|
39924
38660
|
});
|
|
39925
|
-
withJoeOptions(joe.command("hypo <
|
|
39926
|
-
await runJoeCli("hypo",
|
|
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);
|
|
39927
38663
|
});
|
|
39928
|
-
withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity
|
|
39929
|
-
await runJoeCli("activity", null,
|
|
38664
|
+
withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity) on the clone")).action(async (opts) => {
|
|
38665
|
+
await runJoeCli("activity", null, opts);
|
|
39930
38666
|
});
|
|
39931
38667
|
withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
39932
|
-
|
|
39933
|
-
if (!/^[0-9]+$/.test(pidStr)) {
|
|
39934
|
-
console.error("pid must be a number");
|
|
39935
|
-
process.exitCode = 1;
|
|
39936
|
-
return;
|
|
39937
|
-
}
|
|
39938
|
-
await runJoeCli("terminate", null, { pid: parseInt(pidStr, 10) }, opts);
|
|
38668
|
+
await runJoeCli("terminate", pid, opts);
|
|
39939
38669
|
});
|
|
39940
38670
|
withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
39941
|
-
await runJoeCli("reset", null,
|
|
38671
|
+
await runJoeCli("reset", null, opts);
|
|
39942
38672
|
});
|
|
39943
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) => {
|
|
39944
|
-
|
|
39945
|
-
if (opts.variant)
|
|
39946
|
-
args.variant = opts.variant;
|
|
39947
|
-
await runJoeCli("describe", null, args, opts);
|
|
39948
|
-
});
|
|
39949
|
-
withJoeOptions(joe.command("history <terms>").description("search prior Joe analyses, metadata-only (M1b \u2014 not yet available)")).action(async (_terms, _opts) => {
|
|
39950
|
-
console.error("Joe history search is not available yet \u2014 it is planned for M1b. " + "No history-search backend is deployed; this command is a placeholder until then.");
|
|
39951
|
-
process.exitCode = 1;
|
|
38674
|
+
await runJoeCli("describe", object4, opts);
|
|
39952
38675
|
});
|
|
39953
|
-
|
|
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) => {
|
|
39954
38677
|
try {
|
|
39955
38678
|
const rootOpts = program2.opts();
|
|
39956
38679
|
const cfg = readConfig();
|
|
@@ -39961,78 +38684,25 @@ program2.command("projects").description("list the org's projects (shows which h
|
|
|
39961
38684
|
return;
|
|
39962
38685
|
}
|
|
39963
38686
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39964
|
-
const
|
|
39965
|
-
apiKey,
|
|
39966
|
-
apiBaseUrl,
|
|
39967
|
-
orgId: cfg.orgId ?? undefined,
|
|
39968
|
-
debug: !!opts.debug
|
|
39969
|
-
});
|
|
38687
|
+
const output = await getCommandOutput({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
39970
38688
|
if (opts.json) {
|
|
39971
|
-
console.log(JSON.stringify(
|
|
39972
|
-
|
|
39973
|
-
|
|
39974
|
-
|
|
39975
|
-
} catch (err) {
|
|
39976
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
39977
|
-
console.error(message);
|
|
39978
|
-
process.exitCode = 1;
|
|
39979
|
-
}
|
|
39980
|
-
});
|
|
39981
|
-
async function resolveDblabTarget(project, debug) {
|
|
39982
|
-
const rootOpts = program2.opts();
|
|
39983
|
-
const cfg = readConfig();
|
|
39984
|
-
const { apiKey } = getConfig(rootOpts);
|
|
39985
|
-
if (!apiKey) {
|
|
39986
|
-
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39987
|
-
}
|
|
39988
|
-
const ref = (project ?? "").trim();
|
|
39989
|
-
if (!ref) {
|
|
39990
|
-
throw new Error("--project <id|alias> is required");
|
|
39991
|
-
}
|
|
39992
|
-
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39993
|
-
const orgId = cfg.orgId ?? undefined;
|
|
39994
|
-
const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
39995
|
-
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
39996
|
-
}
|
|
39997
|
-
var dblab = program2.command("dblab").description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
|
|
39998
|
-
var clone2 = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
39999
|
-
clone2.command("create").description("create a thin clone of the project's database").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "branch to clone from").option("--snapshot <id>", "snapshot id to clone from").option("--id <id>", "clone id (DBLab generates one when omitted)").option("--db-user <user>", "clone DB user").option("--db-password <password>", "clone DB password").option("--protected", "protect the clone from auto-deletion").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40000
|
-
try {
|
|
40001
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40002
|
-
const result = await createClone2({
|
|
40003
|
-
apiKey,
|
|
40004
|
-
apiBaseUrl,
|
|
40005
|
-
instanceId,
|
|
40006
|
-
cloneId: opts.id,
|
|
40007
|
-
branch: opts.branch,
|
|
40008
|
-
snapshotId: opts.snapshot,
|
|
40009
|
-
dbUser: opts.dbUser,
|
|
40010
|
-
dbPassword: opts.dbPassword,
|
|
40011
|
-
isProtected: !!opts.protected,
|
|
40012
|
-
debug: !!opts.debug
|
|
40013
|
-
});
|
|
40014
|
-
printResult(result, opts.json);
|
|
40015
|
-
} catch (err) {
|
|
40016
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40017
|
-
process.exitCode = 1;
|
|
40018
|
-
}
|
|
40019
|
-
});
|
|
40020
|
-
joe.command("status <commandId>").description("show a Joe command's status (metadata only)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
|
|
40021
|
-
try {
|
|
40022
|
-
const rootOpts = program2.opts();
|
|
40023
|
-
const cfg = readConfig();
|
|
40024
|
-
const { apiKey } = getConfig(rootOpts);
|
|
40025
|
-
if (!apiKey) {
|
|
40026
|
-
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40027
|
-
process.exitCode = 1;
|
|
38689
|
+
console.log(JSON.stringify(output, null, 2));
|
|
38690
|
+
if (output.status !== "ok") {
|
|
38691
|
+
process.exitCode = 1;
|
|
38692
|
+
}
|
|
40028
38693
|
return;
|
|
40029
38694
|
}
|
|
40030
|
-
|
|
40031
|
-
|
|
40032
|
-
|
|
40033
|
-
|
|
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;
|
|
40034
38703
|
} else {
|
|
40035
|
-
console.
|
|
38704
|
+
console.error(`command ${output.command_id} \xB7 ${output.status} \u2014 result is not ready`);
|
|
38705
|
+
process.exitCode = 1;
|
|
40036
38706
|
}
|
|
40037
38707
|
} catch (err) {
|
|
40038
38708
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -40040,17 +38710,7 @@ joe.command("status <commandId>").description("show a Joe command's status (meta
|
|
|
40040
38710
|
process.exitCode = 1;
|
|
40041
38711
|
}
|
|
40042
38712
|
});
|
|
40043
|
-
|
|
40044
|
-
try {
|
|
40045
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40046
|
-
const result = await listClones2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
40047
|
-
printResult(result, opts.json);
|
|
40048
|
-
} catch (err) {
|
|
40049
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40050
|
-
process.exitCode = 1;
|
|
40051
|
-
}
|
|
40052
|
-
});
|
|
40053
|
-
joe.command("result <commandId>").description("fetch a Joe command's result by id (resume a timed-out one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
|
|
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) => {
|
|
40054
38714
|
try {
|
|
40055
38715
|
const rootOpts = program2.opts();
|
|
40056
38716
|
const cfg = readConfig();
|
|
@@ -40061,28 +38721,16 @@ joe.command("result <commandId>").description("fetch a Joe command's result by i
|
|
|
40061
38721
|
return;
|
|
40062
38722
|
}
|
|
40063
38723
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40064
|
-
const
|
|
38724
|
+
const projects = await listProjects({
|
|
38725
|
+
apiKey,
|
|
38726
|
+
apiBaseUrl,
|
|
38727
|
+
orgId: cfg.orgId ?? undefined,
|
|
38728
|
+
debug: !!opts.debug
|
|
38729
|
+
});
|
|
40065
38730
|
if (opts.json) {
|
|
40066
|
-
console.log(JSON.stringify(
|
|
40067
|
-
if (result.status === "error" || result.status === "timed_out") {
|
|
40068
|
-
process.exitCode = 1;
|
|
40069
|
-
}
|
|
40070
|
-
return;
|
|
40071
|
-
}
|
|
40072
|
-
const inferred = inferCommandFromResult(result);
|
|
40073
|
-
if (result.status === "done" && inferred) {
|
|
40074
|
-
console.log(`command ${result.command_id} \xB7 done`);
|
|
40075
|
-
const body = formatJoeResult(inferred, result);
|
|
40076
|
-
if (body)
|
|
40077
|
-
console.log(body);
|
|
40078
|
-
} else if (result.status === "error") {
|
|
40079
|
-
console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
|
|
40080
|
-
process.exitCode = 1;
|
|
40081
|
-
} else if (result.status === "timed_out") {
|
|
40082
|
-
console.error(`command ${result.command_id} timed out on the server`);
|
|
40083
|
-
process.exitCode = 1;
|
|
38731
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
40084
38732
|
} else {
|
|
40085
|
-
console.log(
|
|
38733
|
+
console.log(formatProjectsTable(projects));
|
|
40086
38734
|
}
|
|
40087
38735
|
} catch (err) {
|
|
40088
38736
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -40090,145 +38738,6 @@ joe.command("result <commandId>").description("fetch a Joe command's result by i
|
|
|
40090
38738
|
process.exitCode = 1;
|
|
40091
38739
|
}
|
|
40092
38740
|
});
|
|
40093
|
-
clone2.command("status <cloneId>").description("show a clone's status").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40094
|
-
try {
|
|
40095
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40096
|
-
const result = await getClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
40097
|
-
printResult(result, opts.json);
|
|
40098
|
-
} catch (err) {
|
|
40099
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40100
|
-
process.exitCode = 1;
|
|
40101
|
-
}
|
|
40102
|
-
});
|
|
40103
|
-
clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to reset to (default: latest)").option("--latest", "reset to the latest snapshot").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40104
|
-
try {
|
|
40105
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40106
|
-
const result = await resetClone2({
|
|
40107
|
-
apiKey,
|
|
40108
|
-
apiBaseUrl,
|
|
40109
|
-
instanceId,
|
|
40110
|
-
cloneId,
|
|
40111
|
-
snapshotId: opts.snapshot,
|
|
40112
|
-
latest: opts.latest,
|
|
40113
|
-
debug: !!opts.debug
|
|
40114
|
-
});
|
|
40115
|
-
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
40116
|
-
} catch (err) {
|
|
40117
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40118
|
-
process.exitCode = 1;
|
|
40119
|
-
}
|
|
40120
|
-
});
|
|
40121
|
-
clone2.command("destroy <cloneId>").description("destroy a clone (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
|
|
40122
|
-
try {
|
|
40123
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40124
|
-
const result = await destroyClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
40125
|
-
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
40126
|
-
} catch (err) {
|
|
40127
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40128
|
-
process.exitCode = 1;
|
|
40129
|
-
}
|
|
40130
|
-
});
|
|
40131
|
-
var branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
40132
|
-
branch.command("list").description("list the project's branches").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40133
|
-
try {
|
|
40134
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40135
|
-
const result = await listBranches2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
40136
|
-
printResult(result, opts.json);
|
|
40137
|
-
} catch (err) {
|
|
40138
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40139
|
-
process.exitCode = 1;
|
|
40140
|
-
}
|
|
40141
|
-
});
|
|
40142
|
-
branch.command("create <name>").description("create a branch").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to base the branch on").option("--base-branch <branch>", "parent branch to fork from").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40143
|
-
try {
|
|
40144
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40145
|
-
const result = await createBranch2({
|
|
40146
|
-
apiKey,
|
|
40147
|
-
apiBaseUrl,
|
|
40148
|
-
instanceId,
|
|
40149
|
-
branchName: name,
|
|
40150
|
-
baseBranch: opts.baseBranch,
|
|
40151
|
-
snapshotId: opts.snapshot,
|
|
40152
|
-
debug: !!opts.debug
|
|
40153
|
-
});
|
|
40154
|
-
printResult(result, opts.json);
|
|
40155
|
-
} catch (err) {
|
|
40156
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40157
|
-
process.exitCode = 1;
|
|
40158
|
-
}
|
|
40159
|
-
});
|
|
40160
|
-
branch.command("delete <name>").description("delete a branch (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40161
|
-
try {
|
|
40162
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40163
|
-
const result = await deleteBranch2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40164
|
-
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
40165
|
-
} catch (err) {
|
|
40166
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40167
|
-
process.exitCode = 1;
|
|
40168
|
-
}
|
|
40169
|
-
});
|
|
40170
|
-
branch.command("log <name>").description("show a branch's snapshot log").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
|
|
40171
|
-
try {
|
|
40172
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40173
|
-
const result = await branchLog2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40174
|
-
printResult(result, opts.json);
|
|
40175
|
-
} catch (err) {
|
|
40176
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40177
|
-
process.exitCode = 1;
|
|
40178
|
-
}
|
|
40179
|
-
});
|
|
40180
|
-
var snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
40181
|
-
snapshot.command("list").description("list the project's snapshots").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "filter by branch").option("--dataset <dataset>", "filter by dataset").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40182
|
-
try {
|
|
40183
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40184
|
-
const result = await listSnapshots2({
|
|
40185
|
-
apiKey,
|
|
40186
|
-
apiBaseUrl,
|
|
40187
|
-
instanceId,
|
|
40188
|
-
branchName: opts.branch,
|
|
40189
|
-
dataset: opts.dataset,
|
|
40190
|
-
debug: !!opts.debug
|
|
40191
|
-
});
|
|
40192
|
-
printResult(result, opts.json);
|
|
40193
|
-
} catch (err) {
|
|
40194
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40195
|
-
process.exitCode = 1;
|
|
40196
|
-
}
|
|
40197
|
-
});
|
|
40198
|
-
snapshot.command("create").description("create a snapshot from a clone").requiredOption("--project <id|alias>", "project id or alias").requiredOption("--clone <id>", "clone id to snapshot").option("--message <message>", "snapshot message").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
40199
|
-
try {
|
|
40200
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40201
|
-
const result = await createSnapshot2({
|
|
40202
|
-
apiKey,
|
|
40203
|
-
apiBaseUrl,
|
|
40204
|
-
instanceId,
|
|
40205
|
-
cloneId: opts.clone,
|
|
40206
|
-
message: opts.message,
|
|
40207
|
-
debug: !!opts.debug
|
|
40208
|
-
});
|
|
40209
|
-
printResult(result, opts.json);
|
|
40210
|
-
} catch (err) {
|
|
40211
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40212
|
-
process.exitCode = 1;
|
|
40213
|
-
}
|
|
40214
|
-
});
|
|
40215
|
-
snapshot.command("destroy <snapshotId>").description("destroy a snapshot (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--force", "force-delete even when dependent clones exist").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (snapshotId, opts) => {
|
|
40216
|
-
try {
|
|
40217
|
-
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40218
|
-
const result = await destroySnapshot2({
|
|
40219
|
-
apiKey,
|
|
40220
|
-
apiBaseUrl,
|
|
40221
|
-
instanceId,
|
|
40222
|
-
snapshotId,
|
|
40223
|
-
force: !!opts.force,
|
|
40224
|
-
debug: !!opts.debug
|
|
40225
|
-
});
|
|
40226
|
-
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
40227
|
-
} catch (err) {
|
|
40228
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
40229
|
-
process.exitCode = 1;
|
|
40230
|
-
}
|
|
40231
|
-
});
|
|
40232
38741
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
40233
38742
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
40234
38743
|
const rootOpts = program2.opts();
|
|
@@ -40302,29 +38811,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
40302
38811
|
let configDir;
|
|
40303
38812
|
switch (client) {
|
|
40304
38813
|
case "cursor":
|
|
40305
|
-
configPath =
|
|
40306
|
-
configDir =
|
|
38814
|
+
configPath = path7.join(homeDir, ".cursor", "mcp.json");
|
|
38815
|
+
configDir = path7.dirname(configPath);
|
|
40307
38816
|
break;
|
|
40308
38817
|
case "windsurf":
|
|
40309
|
-
configPath =
|
|
40310
|
-
configDir =
|
|
38818
|
+
configPath = path7.join(homeDir, ".windsurf", "mcp.json");
|
|
38819
|
+
configDir = path7.dirname(configPath);
|
|
40311
38820
|
break;
|
|
40312
38821
|
case "codex":
|
|
40313
|
-
configPath =
|
|
40314
|
-
configDir =
|
|
38822
|
+
configPath = path7.join(homeDir, ".codex", "mcp.json");
|
|
38823
|
+
configDir = path7.dirname(configPath);
|
|
40315
38824
|
break;
|
|
40316
38825
|
default:
|
|
40317
38826
|
console.error(`Configuration not implemented for: ${client}`);
|
|
40318
38827
|
process.exitCode = 1;
|
|
40319
38828
|
return;
|
|
40320
38829
|
}
|
|
40321
|
-
if (!
|
|
40322
|
-
|
|
38830
|
+
if (!fs9.existsSync(configDir)) {
|
|
38831
|
+
fs9.mkdirSync(configDir, { recursive: true });
|
|
40323
38832
|
}
|
|
40324
38833
|
let config2 = { mcpServers: {} };
|
|
40325
|
-
if (
|
|
38834
|
+
if (fs9.existsSync(configPath)) {
|
|
40326
38835
|
try {
|
|
40327
|
-
const content =
|
|
38836
|
+
const content = fs9.readFileSync(configPath, "utf8");
|
|
40328
38837
|
config2 = JSON.parse(content);
|
|
40329
38838
|
if (!config2.mcpServers) {
|
|
40330
38839
|
config2.mcpServers = {};
|
|
@@ -40337,7 +38846,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
40337
38846
|
command: pgaiPath,
|
|
40338
38847
|
args: ["mcp", "start"]
|
|
40339
38848
|
};
|
|
40340
|
-
|
|
38849
|
+
fs9.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
|
|
40341
38850
|
console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
|
|
40342
38851
|
console.log(` Config file: ${configPath}`);
|
|
40343
38852
|
console.log("");
|
|
@@ -40359,6 +38868,5 @@ export {
|
|
|
40359
38868
|
refreshBundledComposeIfStale,
|
|
40360
38869
|
readDeployedTag,
|
|
40361
38870
|
planMonitoringRegistration,
|
|
40362
|
-
isValidComposeYaml
|
|
40363
|
-
inferCommandFromResult
|
|
38871
|
+
isValidComposeYaml
|
|
40364
38872
|
};
|