postgresai 0.16.0-dev.0 → 0.16.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -1
- package/bin/postgres-ai.ts +789 -6
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +2646 -283
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/aas-onboard.ts +41 -7
- package/lib/dblab.ts +449 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +761 -0
- package/lib/mcp-server.ts +625 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +125 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/aas-onboard.test.ts +84 -0
- package/test/dblab.cli.test.ts +373 -0
- package/test/dblab.test.ts +488 -0
- package/test/e2e/pgai-e2e-smoke.sh +192 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +469 -0
- package/test/joe.test.ts +858 -0
- package/test/monitoring.test.ts +54 -3
- package/test/util.test.ts +111 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -9980,8 +9980,8 @@ var require_utils3 = __commonJS((exports, module) => {
|
|
|
9980
9980
|
}
|
|
9981
9981
|
return ind;
|
|
9982
9982
|
}
|
|
9983
|
-
function removeDotSegments(
|
|
9984
|
-
let input =
|
|
9983
|
+
function removeDotSegments(path5) {
|
|
9984
|
+
let input = path5;
|
|
9985
9985
|
const output = [];
|
|
9986
9986
|
let nextSlash = -1;
|
|
9987
9987
|
let len = 0;
|
|
@@ -10171,8 +10171,8 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
10171
10171
|
wsComponent.secure = undefined;
|
|
10172
10172
|
}
|
|
10173
10173
|
if (wsComponent.resourceName) {
|
|
10174
|
-
const [
|
|
10175
|
-
wsComponent.path =
|
|
10174
|
+
const [path5, query] = wsComponent.resourceName.split("?");
|
|
10175
|
+
wsComponent.path = path5 && path5 !== "/" ? path5 : undefined;
|
|
10176
10176
|
wsComponent.query = query;
|
|
10177
10177
|
wsComponent.resourceName = undefined;
|
|
10178
10178
|
}
|
|
@@ -13307,12 +13307,12 @@ var require_dist2 = __commonJS((exports, module) => {
|
|
|
13307
13307
|
throw new Error(`Unknown format "${name}"`);
|
|
13308
13308
|
return f;
|
|
13309
13309
|
};
|
|
13310
|
-
function addFormats(ajv, list,
|
|
13310
|
+
function addFormats(ajv, list, fs5, exportName) {
|
|
13311
13311
|
var _a2;
|
|
13312
13312
|
var _b;
|
|
13313
13313
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== undefined || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
|
|
13314
13314
|
for (const f of list)
|
|
13315
|
-
ajv.addFormat(f,
|
|
13315
|
+
ajv.addFormat(f, fs5[f]);
|
|
13316
13316
|
}
|
|
13317
13317
|
module.exports = exports = formatsPlugin;
|
|
13318
13318
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -13323,43 +13323,55 @@ var require_dist2 = __commonJS((exports, module) => {
|
|
|
13323
13323
|
var exports_util2 = {};
|
|
13324
13324
|
__export(exports_util2, {
|
|
13325
13325
|
resolveBaseUrls: () => resolveBaseUrls2,
|
|
13326
|
+
redactSecretsForLog: () => redactSecretsForLog2,
|
|
13326
13327
|
normalizeBaseUrl: () => normalizeBaseUrl2,
|
|
13327
13328
|
maskSecret: () => maskSecret2,
|
|
13328
|
-
formatHttpError: () => formatHttpError2
|
|
13329
|
+
formatHttpError: () => formatHttpError2,
|
|
13330
|
+
describeFetchError: () => describeFetchError2
|
|
13329
13331
|
});
|
|
13330
13332
|
function isHtmlContent2(text) {
|
|
13331
13333
|
const trimmed = text.trim();
|
|
13332
13334
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
13333
13335
|
}
|
|
13334
|
-
function formatHttpError2(operation, status, responseBody) {
|
|
13335
|
-
const
|
|
13336
|
-
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
13336
|
+
function formatHttpError2(operation, status, responseBody, statusText) {
|
|
13337
|
+
const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
|
|
13337
13338
|
const remediation = status === 401 ? `
|
|
13338
13339
|
${AUTH_REMEDIATION_HINT2}` : "";
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
}
|
|
13340
|
+
let bodyMessage;
|
|
13341
|
+
let bodyDetails;
|
|
13342
|
+
if (responseBody && !isHtmlContent2(responseBody)) {
|
|
13343
13343
|
try {
|
|
13344
13344
|
const errObj = JSON.parse(responseBody);
|
|
13345
|
-
const message = errObj.message
|
|
13346
|
-
if (
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13345
|
+
const message = errObj.message ?? errObj.error;
|
|
13346
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
13347
|
+
bodyMessage = message.trim();
|
|
13348
|
+
}
|
|
13349
|
+
const details = errObj.details ?? errObj.detail;
|
|
13350
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
13351
|
+
bodyDetails = details.trim();
|
|
13352
13352
|
}
|
|
13353
13353
|
} catch {
|
|
13354
13354
|
const trimmed = responseBody.trim();
|
|
13355
13355
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
13356
|
-
|
|
13357
|
-
${trimmed}`;
|
|
13356
|
+
bodyDetails = trimmed;
|
|
13358
13357
|
}
|
|
13359
13358
|
}
|
|
13360
13359
|
}
|
|
13360
|
+
const trimmedReason = statusText?.trim();
|
|
13361
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
13362
|
+
const headline = bodyMessage ?? reasonPhrase ?? generic;
|
|
13363
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}`;
|
|
13364
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
13365
|
+
errMsg += `
|
|
13366
|
+
${bodyDetails}`;
|
|
13367
|
+
}
|
|
13361
13368
|
return errMsg + remediation;
|
|
13362
13369
|
}
|
|
13370
|
+
function describeFetchError2(operation, url, err) {
|
|
13371
|
+
const cause = err?.cause;
|
|
13372
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
13373
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
13374
|
+
}
|
|
13363
13375
|
function maskSecret2(secret) {
|
|
13364
13376
|
if (!secret)
|
|
13365
13377
|
return "";
|
|
@@ -13369,6 +13381,28 @@ function maskSecret2(secret) {
|
|
|
13369
13381
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
13370
13382
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
13371
13383
|
}
|
|
13384
|
+
function redactSecretsForLog2(text) {
|
|
13385
|
+
let parsed;
|
|
13386
|
+
try {
|
|
13387
|
+
parsed = JSON.parse(text);
|
|
13388
|
+
} catch {
|
|
13389
|
+
return text;
|
|
13390
|
+
}
|
|
13391
|
+
const walk = (node) => {
|
|
13392
|
+
if (Array.isArray(node)) {
|
|
13393
|
+
return node.map(walk);
|
|
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));
|
|
13405
|
+
}
|
|
13372
13406
|
function normalizeBaseUrl2(value) {
|
|
13373
13407
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
13374
13408
|
try {
|
|
@@ -13391,7 +13425,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
13391
13425
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13392
13426
|
};
|
|
13393
13427
|
}
|
|
13394
|
-
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
13428
|
+
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2, SENSITIVE_LOG_KEY2;
|
|
13395
13429
|
var init_util = __esm(() => {
|
|
13396
13430
|
HTTP_STATUS_MESSAGES2 = {
|
|
13397
13431
|
400: "Bad Request",
|
|
@@ -13405,6 +13439,21 @@ var init_util = __esm(() => {
|
|
|
13405
13439
|
503: "Service Unavailable - server temporarily unavailable",
|
|
13406
13440
|
504: "Gateway Timeout - server temporarily unavailable"
|
|
13407
13441
|
};
|
|
13442
|
+
STANDARD_REASON_PHRASES2 = {
|
|
13443
|
+
400: "Bad Request",
|
|
13444
|
+
401: "Unauthorized",
|
|
13445
|
+
403: "Forbidden",
|
|
13446
|
+
404: "Not Found",
|
|
13447
|
+
408: "Request Timeout",
|
|
13448
|
+
409: "Conflict",
|
|
13449
|
+
413: "Payload Too Large",
|
|
13450
|
+
429: "Too Many Requests",
|
|
13451
|
+
500: "Internal Server Error",
|
|
13452
|
+
502: "Bad Gateway",
|
|
13453
|
+
503: "Service Unavailable",
|
|
13454
|
+
504: "Gateway Timeout"
|
|
13455
|
+
};
|
|
13456
|
+
SENSITIVE_LOG_KEY2 = /password|connstr/i;
|
|
13408
13457
|
});
|
|
13409
13458
|
|
|
13410
13459
|
// node_modules/commander/esm.mjs
|
|
@@ -13425,7 +13474,7 @@ var {
|
|
|
13425
13474
|
// package.json
|
|
13426
13475
|
var package_default = {
|
|
13427
13476
|
name: "postgresai",
|
|
13428
|
-
version: "0.16.0-dev.
|
|
13477
|
+
version: "0.16.0-dev.10",
|
|
13429
13478
|
description: "postgres_ai CLI",
|
|
13430
13479
|
license: "Apache-2.0",
|
|
13431
13480
|
private: false,
|
|
@@ -16236,11 +16285,11 @@ var safeLoadAll = renamed("safeLoadAll", "loadAll");
|
|
|
16236
16285
|
var safeDump = renamed("safeDump", "dump");
|
|
16237
16286
|
|
|
16238
16287
|
// bin/postgres-ai.ts
|
|
16239
|
-
import * as
|
|
16240
|
-
import * as
|
|
16288
|
+
import * as fs11 from "fs";
|
|
16289
|
+
import * as path9 from "path";
|
|
16241
16290
|
import * as os3 from "os";
|
|
16242
16291
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16243
|
-
import * as
|
|
16292
|
+
import * as crypto4 from "crypto";
|
|
16244
16293
|
|
|
16245
16294
|
// node_modules/pg/esm/index.mjs
|
|
16246
16295
|
var import_lib = __toESM(require_lib2(), 1);
|
|
@@ -16256,7 +16305,7 @@ var Result = import_lib.default.Result;
|
|
|
16256
16305
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16257
16306
|
var defaults = import_lib.default.defaults;
|
|
16258
16307
|
// package.json
|
|
16259
|
-
var version = "0.16.0-dev.
|
|
16308
|
+
var version = "0.16.0-dev.10";
|
|
16260
16309
|
var package_default2 = {
|
|
16261
16310
|
name: "postgresai",
|
|
16262
16311
|
version,
|
|
@@ -16398,35 +16447,59 @@ function isHtmlContent(text) {
|
|
|
16398
16447
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
16399
16448
|
}
|
|
16400
16449
|
var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16450
|
+
var STANDARD_REASON_PHRASES = {
|
|
16451
|
+
400: "Bad Request",
|
|
16452
|
+
401: "Unauthorized",
|
|
16453
|
+
403: "Forbidden",
|
|
16454
|
+
404: "Not Found",
|
|
16455
|
+
408: "Request Timeout",
|
|
16456
|
+
409: "Conflict",
|
|
16457
|
+
413: "Payload Too Large",
|
|
16458
|
+
429: "Too Many Requests",
|
|
16459
|
+
500: "Internal Server Error",
|
|
16460
|
+
502: "Bad Gateway",
|
|
16461
|
+
503: "Service Unavailable",
|
|
16462
|
+
504: "Gateway Timeout"
|
|
16463
|
+
};
|
|
16464
|
+
function formatHttpError(operation, status, responseBody, statusText) {
|
|
16465
|
+
const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
|
|
16404
16466
|
const remediation = status === 401 ? `
|
|
16405
16467
|
${AUTH_REMEDIATION_HINT}` : "";
|
|
16406
|
-
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
}
|
|
16468
|
+
let bodyMessage;
|
|
16469
|
+
let bodyDetails;
|
|
16470
|
+
if (responseBody && !isHtmlContent(responseBody)) {
|
|
16410
16471
|
try {
|
|
16411
16472
|
const errObj = JSON.parse(responseBody);
|
|
16412
|
-
const message = errObj.message
|
|
16413
|
-
if (
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16473
|
+
const message = errObj.message ?? errObj.error;
|
|
16474
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
16475
|
+
bodyMessage = message.trim();
|
|
16476
|
+
}
|
|
16477
|
+
const details = errObj.details ?? errObj.detail;
|
|
16478
|
+
if (typeof details === "string" && details.trim().length > 0) {
|
|
16479
|
+
bodyDetails = details.trim();
|
|
16419
16480
|
}
|
|
16420
16481
|
} catch {
|
|
16421
16482
|
const trimmed = responseBody.trim();
|
|
16422
16483
|
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
16423
|
-
|
|
16424
|
-
${trimmed}`;
|
|
16484
|
+
bodyDetails = trimmed;
|
|
16425
16485
|
}
|
|
16426
16486
|
}
|
|
16427
16487
|
}
|
|
16488
|
+
const trimmedReason = statusText?.trim();
|
|
16489
|
+
const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
|
|
16490
|
+
const headline = bodyMessage ?? reasonPhrase ?? generic;
|
|
16491
|
+
let errMsg = `${operation}: HTTP ${status} - ${headline}`;
|
|
16492
|
+
if (bodyDetails && bodyDetails !== headline) {
|
|
16493
|
+
errMsg += `
|
|
16494
|
+
${bodyDetails}`;
|
|
16495
|
+
}
|
|
16428
16496
|
return errMsg + remediation;
|
|
16429
16497
|
}
|
|
16498
|
+
function describeFetchError(operation, url, err) {
|
|
16499
|
+
const cause = err?.cause;
|
|
16500
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
16501
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
16502
|
+
}
|
|
16430
16503
|
function maskSecret(secret) {
|
|
16431
16504
|
if (!secret)
|
|
16432
16505
|
return "";
|
|
@@ -16436,6 +16509,29 @@ function maskSecret(secret) {
|
|
|
16436
16509
|
return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
|
|
16437
16510
|
return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
|
|
16438
16511
|
}
|
|
16512
|
+
var SENSITIVE_LOG_KEY = /password|connstr/i;
|
|
16513
|
+
function redactSecretsForLog(text) {
|
|
16514
|
+
let parsed;
|
|
16515
|
+
try {
|
|
16516
|
+
parsed = JSON.parse(text);
|
|
16517
|
+
} catch {
|
|
16518
|
+
return text;
|
|
16519
|
+
}
|
|
16520
|
+
const walk = (node) => {
|
|
16521
|
+
if (Array.isArray(node)) {
|
|
16522
|
+
return node.map(walk);
|
|
16523
|
+
}
|
|
16524
|
+
if (node && typeof node === "object") {
|
|
16525
|
+
const out = {};
|
|
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));
|
|
16534
|
+
}
|
|
16439
16535
|
function normalizeBaseUrl(value) {
|
|
16440
16536
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
16441
16537
|
try {
|
|
@@ -17267,9 +17363,611 @@ async function fetchReportFileData(params) {
|
|
|
17267
17363
|
}
|
|
17268
17364
|
}
|
|
17269
17365
|
|
|
17270
|
-
// lib/
|
|
17366
|
+
// lib/joe.ts
|
|
17271
17367
|
import * as fs3 from "fs";
|
|
17272
17368
|
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";
|
|
17273
17971
|
var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
|
|
17274
17972
|
var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
|
|
17275
17973
|
var MIME_TYPES = {
|
|
@@ -17310,11 +18008,11 @@ async function uploadFile(params) {
|
|
|
17310
18008
|
if (!filePath) {
|
|
17311
18009
|
throw new Error("filePath is required");
|
|
17312
18010
|
}
|
|
17313
|
-
const resolvedPath =
|
|
17314
|
-
if (!
|
|
18011
|
+
const resolvedPath = path4.resolve(filePath);
|
|
18012
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
17315
18013
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
17316
18014
|
}
|
|
17317
|
-
const stat =
|
|
18015
|
+
const stat = fs4.statSync(resolvedPath);
|
|
17318
18016
|
if (!stat.isFile()) {
|
|
17319
18017
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
17320
18018
|
}
|
|
@@ -17326,9 +18024,9 @@ async function uploadFile(params) {
|
|
|
17326
18024
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
17327
18025
|
}
|
|
17328
18026
|
const url = `${base}/upload`;
|
|
17329
|
-
const fileBuffer =
|
|
17330
|
-
const fileName =
|
|
17331
|
-
const ext =
|
|
18027
|
+
const fileBuffer = fs4.readFileSync(resolvedPath);
|
|
18028
|
+
const fileName = path4.basename(resolvedPath);
|
|
18029
|
+
const ext = path4.extname(fileName).toLowerCase();
|
|
17332
18030
|
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
|
17333
18031
|
const formData = new FormData;
|
|
17334
18032
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -17387,15 +18085,15 @@ async function downloadFile(params) {
|
|
|
17387
18085
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
17388
18086
|
fullUrl = `${base}${relativePath}`;
|
|
17389
18087
|
}
|
|
17390
|
-
const urlFilename =
|
|
18088
|
+
const urlFilename = path4.basename(new URL(fullUrl).pathname);
|
|
17391
18089
|
if (!urlFilename) {
|
|
17392
18090
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
17393
18091
|
}
|
|
17394
|
-
const saveTo = outputPath ?
|
|
18092
|
+
const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
|
|
17395
18093
|
if (!outputPath) {
|
|
17396
|
-
const normalizedSave =
|
|
17397
|
-
const cwd =
|
|
17398
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
18094
|
+
const normalizedSave = path4.normalize(saveTo);
|
|
18095
|
+
const cwd = path4.normalize(process.cwd());
|
|
18096
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
|
|
17399
18097
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
17400
18098
|
}
|
|
17401
18099
|
}
|
|
@@ -17427,11 +18125,11 @@ async function downloadFile(params) {
|
|
|
17427
18125
|
}
|
|
17428
18126
|
const arrayBuffer = await response.arrayBuffer();
|
|
17429
18127
|
const buffer = Buffer.from(arrayBuffer);
|
|
17430
|
-
const parentDir =
|
|
17431
|
-
if (!
|
|
17432
|
-
|
|
18128
|
+
const parentDir = path4.dirname(saveTo);
|
|
18129
|
+
if (!fs4.existsSync(parentDir)) {
|
|
18130
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
17433
18131
|
}
|
|
17434
|
-
|
|
18132
|
+
fs4.writeFileSync(saveTo, buffer);
|
|
17435
18133
|
return {
|
|
17436
18134
|
savedTo: saveTo,
|
|
17437
18135
|
size: buffer.length,
|
|
@@ -17443,9 +18141,9 @@ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
|
|
|
17443
18141
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
17444
18142
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
17445
18143
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
17446
|
-
const name = filename ||
|
|
18144
|
+
const name = filename || path4.basename(new URL(fullUrl).pathname);
|
|
17447
18145
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
17448
|
-
const ext =
|
|
18146
|
+
const ext = path4.extname(name).toLowerCase();
|
|
17449
18147
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
17450
18148
|
return ``;
|
|
17451
18149
|
}
|
|
@@ -17733,10 +18431,10 @@ function mergeDefs(...defs) {
|
|
|
17733
18431
|
function cloneDef(schema2) {
|
|
17734
18432
|
return mergeDefs(schema2._zod.def);
|
|
17735
18433
|
}
|
|
17736
|
-
function getElementAtPath(obj,
|
|
17737
|
-
if (!
|
|
18434
|
+
function getElementAtPath(obj, path5) {
|
|
18435
|
+
if (!path5)
|
|
17738
18436
|
return obj;
|
|
17739
|
-
return
|
|
18437
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
17740
18438
|
}
|
|
17741
18439
|
function promiseAllObject(promisesObj) {
|
|
17742
18440
|
const keys = Object.keys(promisesObj);
|
|
@@ -18100,11 +18798,11 @@ function aborted(x, startIndex = 0) {
|
|
|
18100
18798
|
}
|
|
18101
18799
|
return false;
|
|
18102
18800
|
}
|
|
18103
|
-
function prefixIssues(
|
|
18801
|
+
function prefixIssues(path5, issues) {
|
|
18104
18802
|
return issues.map((iss) => {
|
|
18105
18803
|
var _a;
|
|
18106
18804
|
(_a = iss).path ?? (_a.path = []);
|
|
18107
|
-
iss.path.unshift(
|
|
18805
|
+
iss.path.unshift(path5);
|
|
18108
18806
|
return iss;
|
|
18109
18807
|
});
|
|
18110
18808
|
}
|
|
@@ -24376,14 +25074,219 @@ class StdioServerTransport {
|
|
|
24376
25074
|
// lib/mcp-server.ts
|
|
24377
25075
|
var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
24378
25076
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
24379
|
-
|
|
24380
|
-
|
|
24381
|
-
|
|
24382
|
-
|
|
24383
|
-
|
|
24384
|
-
|
|
24385
|
-
|
|
24386
|
-
|
|
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
|
+
async function handleToolCall(req, rootOpts, extra) {
|
|
25283
|
+
const toolName = req.params.name;
|
|
25284
|
+
const args = req.params.arguments || {};
|
|
25285
|
+
const cfg = readConfig2();
|
|
25286
|
+
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
25287
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
25288
|
+
const debug = Boolean(args.debug ?? extra?.debug);
|
|
25289
|
+
if (!apiKey) {
|
|
24387
25290
|
return {
|
|
24388
25291
|
content: [
|
|
24389
25292
|
{
|
|
@@ -24653,6 +25556,147 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24653
25556
|
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
|
|
24654
25557
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
24655
25558
|
}
|
|
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
|
+
}
|
|
24656
25700
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
24657
25701
|
} catch (err) {
|
|
24658
25702
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24929,6 +25973,197 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
24929
25973
|
},
|
|
24930
25974
|
additionalProperties: false
|
|
24931
25975
|
}
|
|
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
|
+
}
|
|
24932
26167
|
}
|
|
24933
26168
|
]
|
|
24934
26169
|
};
|
|
@@ -25801,11 +27036,699 @@ function renderMarkdownForTerminal(md) {
|
|
|
25801
27036
|
`);
|
|
25802
27037
|
}
|
|
25803
27038
|
|
|
27039
|
+
// lib/joe.ts
|
|
27040
|
+
import * as fs5 from "fs";
|
|
27041
|
+
import * as path5 from "path";
|
|
27042
|
+
import * as crypto2 from "node:crypto";
|
|
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) {
|
|
27073
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
27074
|
+
if (!apiKey) {
|
|
27075
|
+
throw new Error("API key is required");
|
|
27076
|
+
}
|
|
27077
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27078
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
27079
|
+
const payload = JSON.stringify(body);
|
|
27080
|
+
const headers = {
|
|
27081
|
+
"access-token": apiKey,
|
|
27082
|
+
"Content-Type": "application/json",
|
|
27083
|
+
Connection: "close"
|
|
27084
|
+
};
|
|
27085
|
+
if (debug) {
|
|
27086
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27087
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27088
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27089
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
27090
|
+
}
|
|
27091
|
+
let response;
|
|
27092
|
+
try {
|
|
27093
|
+
response = await fetch(url.toString(), {
|
|
27094
|
+
method: "POST",
|
|
27095
|
+
headers,
|
|
27096
|
+
body: payload
|
|
27097
|
+
});
|
|
27098
|
+
} catch (err) {
|
|
27099
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
27100
|
+
}
|
|
27101
|
+
const text = await response.text();
|
|
27102
|
+
if (debug) {
|
|
27103
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27104
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
27105
|
+
}
|
|
27106
|
+
if (!response.ok) {
|
|
27107
|
+
throw new Error(formatHttpError(operation, response.status, text, response.statusText));
|
|
27108
|
+
}
|
|
27109
|
+
try {
|
|
27110
|
+
return JSON.parse(text);
|
|
27111
|
+
} catch {
|
|
27112
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
27113
|
+
}
|
|
27114
|
+
}
|
|
27115
|
+
async function submitCommand2(params) {
|
|
27116
|
+
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
27117
|
+
if (!JOE_COMMANDS2.includes(command)) {
|
|
27118
|
+
throw new Error(`Unknown Joe command: ${command}`);
|
|
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;
|
|
27129
|
+
}
|
|
27130
|
+
return callRpc2({
|
|
27131
|
+
apiKey,
|
|
27132
|
+
apiBaseUrl,
|
|
27133
|
+
fn: "joe_command_submit",
|
|
27134
|
+
body,
|
|
27135
|
+
operation: `Failed to submit ${command} command`,
|
|
27136
|
+
debug
|
|
27137
|
+
});
|
|
27138
|
+
}
|
|
27139
|
+
async function getCommandStatus2(params) {
|
|
27140
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27141
|
+
if (!commandId) {
|
|
27142
|
+
throw new Error("commandId is required");
|
|
27143
|
+
}
|
|
27144
|
+
return callRpc2({
|
|
27145
|
+
apiKey,
|
|
27146
|
+
apiBaseUrl,
|
|
27147
|
+
fn: "joe_command_status",
|
|
27148
|
+
body: { command_id: commandId },
|
|
27149
|
+
operation: "Failed to fetch command status",
|
|
27150
|
+
debug
|
|
27151
|
+
});
|
|
27152
|
+
}
|
|
27153
|
+
async function getCommandResult2(params) {
|
|
27154
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27155
|
+
if (!commandId) {
|
|
27156
|
+
throw new Error("commandId is required");
|
|
27157
|
+
}
|
|
27158
|
+
return callRpc2({
|
|
27159
|
+
apiKey,
|
|
27160
|
+
apiBaseUrl,
|
|
27161
|
+
fn: "joe_command_result",
|
|
27162
|
+
body: { command_id: commandId },
|
|
27163
|
+
operation: "Failed to fetch command result",
|
|
27164
|
+
debug
|
|
27165
|
+
});
|
|
27166
|
+
}
|
|
27167
|
+
function normalizeProjectRow2(row) {
|
|
27168
|
+
const projectId = row.project_id ?? row.id ?? 0;
|
|
27169
|
+
return {
|
|
27170
|
+
project_id: Number(projectId),
|
|
27171
|
+
alias: row.alias ?? null,
|
|
27172
|
+
name: row.name ?? null,
|
|
27173
|
+
label: row.label ?? null,
|
|
27174
|
+
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
27175
|
+
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
27176
|
+
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
27177
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
|
|
27178
|
+
};
|
|
27179
|
+
}
|
|
27180
|
+
async function listProjects2(params) {
|
|
27181
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27182
|
+
const body = {};
|
|
27183
|
+
if (typeof orgId === "number") {
|
|
27184
|
+
body.org_id = orgId;
|
|
27185
|
+
}
|
|
27186
|
+
const rows = await callRpc2({
|
|
27187
|
+
apiKey,
|
|
27188
|
+
apiBaseUrl,
|
|
27189
|
+
fn: "projects_list",
|
|
27190
|
+
body,
|
|
27191
|
+
operation: "Failed to list projects",
|
|
27192
|
+
debug
|
|
27193
|
+
});
|
|
27194
|
+
if (!Array.isArray(rows)) {
|
|
27195
|
+
return [];
|
|
27196
|
+
}
|
|
27197
|
+
return rows.map(normalizeProjectRow2);
|
|
27198
|
+
}
|
|
27199
|
+
function isNumericProjectRef3(ref) {
|
|
27200
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
27201
|
+
}
|
|
27202
|
+
async function resolveProjectId2(params) {
|
|
27203
|
+
const ref = String(params.project ?? "").trim();
|
|
27204
|
+
if (!ref) {
|
|
27205
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27206
|
+
}
|
|
27207
|
+
if (isNumericProjectRef3(ref)) {
|
|
27208
|
+
return Number(ref);
|
|
27209
|
+
}
|
|
27210
|
+
const projects = await listProjects2({
|
|
27211
|
+
apiKey: params.apiKey,
|
|
27212
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27213
|
+
orgId: params.orgId,
|
|
27214
|
+
debug: params.debug
|
|
27215
|
+
});
|
|
27216
|
+
const needle = ref.toLowerCase();
|
|
27217
|
+
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);
|
|
27218
|
+
if (!match) {
|
|
27219
|
+
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27220
|
+
}
|
|
27221
|
+
return match.project_id;
|
|
27222
|
+
}
|
|
27223
|
+
function sessionStorePath2(dir) {
|
|
27224
|
+
return path5.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
27225
|
+
}
|
|
27226
|
+
function readSessionStore2(dir) {
|
|
27227
|
+
const file = sessionStorePath2(dir);
|
|
27228
|
+
if (!fs5.existsSync(file)) {
|
|
27229
|
+
return {};
|
|
27230
|
+
}
|
|
27231
|
+
try {
|
|
27232
|
+
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
27233
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
27234
|
+
return parsed;
|
|
27235
|
+
}
|
|
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
|
+
}
|
|
27274
|
+
return `joe-${crypto2.randomUUID()}`;
|
|
27275
|
+
}
|
|
27276
|
+
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27277
|
+
async function runCommand2(params) {
|
|
27278
|
+
const {
|
|
27279
|
+
apiKey,
|
|
27280
|
+
apiBaseUrl,
|
|
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;
|
|
27290
|
+
const now = params.now ?? Date.now;
|
|
27291
|
+
const sleep = params.sleep ?? defaultSleep2;
|
|
27292
|
+
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey2(command, projectId, sql, args);
|
|
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;
|
|
27306
|
+
const deadline = now() + budgetMs;
|
|
27307
|
+
let status = submitted.status;
|
|
27308
|
+
while (true) {
|
|
27309
|
+
const statusResp = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27310
|
+
status = statusResp.status;
|
|
27311
|
+
if (TERMINAL_STATES2.has(status)) {
|
|
27312
|
+
break;
|
|
27313
|
+
}
|
|
27314
|
+
if (now() >= deadline) {
|
|
27315
|
+
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
27316
|
+
}
|
|
27317
|
+
await sleep(pollIntervalMs);
|
|
27318
|
+
}
|
|
27319
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27320
|
+
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
27321
|
+
}
|
|
27322
|
+
async function executeJoeCommand2(params) {
|
|
27323
|
+
const projectId = await resolveProjectId2({
|
|
27324
|
+
apiKey: params.apiKey,
|
|
27325
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27326
|
+
project: params.project,
|
|
27327
|
+
orgId: params.orgId,
|
|
27328
|
+
debug: params.debug
|
|
27329
|
+
});
|
|
27330
|
+
let sessionId;
|
|
27331
|
+
if (params.newSession) {
|
|
27332
|
+
clearStoredSessionId2(projectId, params.sessionDir);
|
|
27333
|
+
sessionId = null;
|
|
27334
|
+
} else if (params.session) {
|
|
27335
|
+
sessionId = String(params.session);
|
|
27336
|
+
} else {
|
|
27337
|
+
sessionId = readStoredSessionId2(projectId, params.sessionDir);
|
|
27338
|
+
}
|
|
27339
|
+
const outcome = await runCommand2({
|
|
27340
|
+
apiKey: params.apiKey,
|
|
27341
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27342
|
+
command: params.command,
|
|
27343
|
+
projectId,
|
|
27344
|
+
sql: params.sql,
|
|
27345
|
+
args: params.args,
|
|
27346
|
+
sessionId,
|
|
27347
|
+
budgetMs: params.budgetMs,
|
|
27348
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
27349
|
+
debug: params.debug,
|
|
27350
|
+
now: params.now,
|
|
27351
|
+
sleep: params.sleep
|
|
27352
|
+
});
|
|
27353
|
+
if (outcome.sessionId) {
|
|
27354
|
+
writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
|
|
27355
|
+
}
|
|
27356
|
+
return { ...outcome, command: params.command, projectId };
|
|
27357
|
+
}
|
|
27358
|
+
function clientSidePlanFlags(planJson) {
|
|
27359
|
+
const flags = [];
|
|
27360
|
+
const walk = (node) => {
|
|
27361
|
+
if (!node || typeof node !== "object") {
|
|
27362
|
+
return;
|
|
27363
|
+
}
|
|
27364
|
+
const nodeType = node["Node Type"];
|
|
27365
|
+
if (nodeType === "Seq Scan") {
|
|
27366
|
+
const rel = node["Relation Name"];
|
|
27367
|
+
flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
|
|
27368
|
+
}
|
|
27369
|
+
if (Array.isArray(node.Plans)) {
|
|
27370
|
+
for (const child of node.Plans) {
|
|
27371
|
+
walk(child);
|
|
27372
|
+
}
|
|
27373
|
+
}
|
|
27374
|
+
};
|
|
27375
|
+
if (planJson && typeof planJson === "object") {
|
|
27376
|
+
const root = planJson;
|
|
27377
|
+
walk(root.Plan ?? planJson);
|
|
27378
|
+
}
|
|
27379
|
+
return flags;
|
|
27380
|
+
}
|
|
27381
|
+
function formatJoeResult(command, result) {
|
|
27382
|
+
const lines = [];
|
|
27383
|
+
switch (command) {
|
|
27384
|
+
case "plan":
|
|
27385
|
+
case "explain": {
|
|
27386
|
+
if (result.plan_text) {
|
|
27387
|
+
lines.push(result.plan_text);
|
|
27388
|
+
}
|
|
27389
|
+
for (const flag of clientSidePlanFlags(result.plan_json)) {
|
|
27390
|
+
lines.push(`⚑ ${flag}`);
|
|
27391
|
+
}
|
|
27392
|
+
if (result.queryid || result.plan_fingerprint) {
|
|
27393
|
+
lines.push(`(queryid ${result.queryid ?? "?"} · plan_fp ${result.plan_fingerprint ?? "?"})`);
|
|
27394
|
+
}
|
|
27395
|
+
break;
|
|
27396
|
+
}
|
|
27397
|
+
case "exec": {
|
|
27398
|
+
const notices = result.notices ?? [];
|
|
27399
|
+
const rowCount = result.row_count ?? 0;
|
|
27400
|
+
lines.push(`${notices.join(" ") || "OK"} · ${rowCount} row${rowCount === 1 ? "" : "s"}`);
|
|
27401
|
+
if (result.result_rows && result.result_rows.length > 0) {
|
|
27402
|
+
lines.push(JSON.stringify(result.result_rows, null, 2));
|
|
27403
|
+
}
|
|
27404
|
+
break;
|
|
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
|
+
}
|
|
27426
|
+
}
|
|
27427
|
+
return lines.join(`
|
|
27428
|
+
`);
|
|
27429
|
+
}
|
|
27430
|
+
function formatProjectsTable(projects) {
|
|
27431
|
+
const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
|
|
27432
|
+
const rows = projects.map((p) => [
|
|
27433
|
+
String(p.project_id),
|
|
27434
|
+
p.alias ?? "-",
|
|
27435
|
+
p.name ?? "-",
|
|
27436
|
+
p.joe_ready ? "ready" : "no",
|
|
27437
|
+
p.tunnel ? "yes" : "no"
|
|
27438
|
+
]);
|
|
27439
|
+
const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
|
|
27440
|
+
const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
|
|
27441
|
+
return [pad(header), ...rows.map(pad)].join(`
|
|
27442
|
+
`);
|
|
27443
|
+
}
|
|
27444
|
+
|
|
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
|
+
|
|
25804
27727
|
// bin/postgres-ai.ts
|
|
25805
27728
|
init_util();
|
|
25806
27729
|
|
|
25807
27730
|
// lib/instances.ts
|
|
25808
|
-
import * as
|
|
27731
|
+
import * as fs6 from "fs";
|
|
25809
27732
|
|
|
25810
27733
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
25811
27734
|
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
@@ -28470,11 +30393,11 @@ class InstancesParseError extends Error {
|
|
|
28470
30393
|
}
|
|
28471
30394
|
}
|
|
28472
30395
|
function loadInstances(file) {
|
|
28473
|
-
if (!
|
|
30396
|
+
if (!fs6.existsSync(file))
|
|
28474
30397
|
return [];
|
|
28475
|
-
if (
|
|
30398
|
+
if (fs6.lstatSync(file).isDirectory())
|
|
28476
30399
|
return [];
|
|
28477
|
-
const text =
|
|
30400
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
28478
30401
|
if (text.trim() === "")
|
|
28479
30402
|
return [];
|
|
28480
30403
|
let parsed;
|
|
@@ -28498,7 +30421,7 @@ function grafanaBaseUrl() {
|
|
|
28498
30421
|
return (process.env.PGAI_GRAFANA_LOCAL_URL || "http://localhost:3000").replace(/\/+$/, "");
|
|
28499
30422
|
}
|
|
28500
30423
|
function grafanaAdminUser() {
|
|
28501
|
-
return process.env.GF_SECURITY_ADMIN_USER || "
|
|
30424
|
+
return process.env.GF_SECURITY_ADMIN_USER || "monitor";
|
|
28502
30425
|
}
|
|
28503
30426
|
function parseVcpus(raw) {
|
|
28504
30427
|
if (raw === undefined || raw === null || raw === "")
|
|
@@ -28575,9 +30498,14 @@ async function resolveDatasourceId(adminPassword, debug = false) {
|
|
|
28575
30498
|
return null;
|
|
28576
30499
|
const list = await res.json().catch(() => []);
|
|
28577
30500
|
const prom = list.filter((d) => d.type === "prometheus");
|
|
28578
|
-
if (prom.length
|
|
30501
|
+
if (prom.length > 1) {
|
|
30502
|
+
if (debug)
|
|
30503
|
+
console.error(`Debug: AAS: ${prom.length} prometheus datasources (ambiguous); not retrying`);
|
|
30504
|
+
return "ambiguous";
|
|
30505
|
+
}
|
|
30506
|
+
if (prom.length === 0) {
|
|
28579
30507
|
if (debug)
|
|
28580
|
-
console.error(`Debug: AAS:
|
|
30508
|
+
console.error(`Debug: AAS: no prometheus datasource resolvable yet`);
|
|
28581
30509
|
return null;
|
|
28582
30510
|
}
|
|
28583
30511
|
return typeof prom[0].id === "number" ? prom[0].id : null;
|
|
@@ -28593,7 +30521,23 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
28593
30521
|
const labels = resolveAasLabels(opts.instancesPath);
|
|
28594
30522
|
if (!labels)
|
|
28595
30523
|
return { ok: false, reason: "could not determine a single (cluster, node_name) target" };
|
|
28596
|
-
const
|
|
30524
|
+
const maxAttempts = opts.datasourceMaxAttempts ?? 20;
|
|
30525
|
+
const retryDelayMs = opts.datasourceRetryDelayMs ?? 3000;
|
|
30526
|
+
let datasourceId = null;
|
|
30527
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
30528
|
+
const resolved = await resolveDatasourceId(opts.grafanaPassword, debug);
|
|
30529
|
+
if (typeof resolved === "number") {
|
|
30530
|
+
datasourceId = resolved;
|
|
30531
|
+
break;
|
|
30532
|
+
}
|
|
30533
|
+
if (resolved === "ambiguous")
|
|
30534
|
+
break;
|
|
30535
|
+
if (attempt < maxAttempts) {
|
|
30536
|
+
if (debug)
|
|
30537
|
+
console.error(`Debug: AAS: datasource not resolvable yet (attempt ${attempt}/${maxAttempts}); waiting for Grafana…`);
|
|
30538
|
+
await new Promise((resolve4) => setTimeout(resolve4, retryDelayMs));
|
|
30539
|
+
}
|
|
30540
|
+
}
|
|
28597
30541
|
if (datasourceId == null)
|
|
28598
30542
|
return { ok: false, reason: "could not resolve the Prometheus datasource id" };
|
|
28599
30543
|
const saToken = await mintAasServiceAccountToken(opts.grafanaPassword, debug);
|
|
@@ -28631,8 +30575,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
28631
30575
|
}
|
|
28632
30576
|
|
|
28633
30577
|
// lib/storage.ts
|
|
28634
|
-
import * as
|
|
28635
|
-
import * as
|
|
30578
|
+
import * as fs7 from "fs";
|
|
30579
|
+
import * as path6 from "path";
|
|
28636
30580
|
var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28637
30581
|
var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28638
30582
|
var MIME_TYPES2 = {
|
|
@@ -28673,11 +30617,11 @@ async function uploadFile2(params) {
|
|
|
28673
30617
|
if (!filePath) {
|
|
28674
30618
|
throw new Error("filePath is required");
|
|
28675
30619
|
}
|
|
28676
|
-
const resolvedPath =
|
|
28677
|
-
if (!
|
|
30620
|
+
const resolvedPath = path6.resolve(filePath);
|
|
30621
|
+
if (!fs7.existsSync(resolvedPath)) {
|
|
28678
30622
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
28679
30623
|
}
|
|
28680
|
-
const stat =
|
|
30624
|
+
const stat = fs7.statSync(resolvedPath);
|
|
28681
30625
|
if (!stat.isFile()) {
|
|
28682
30626
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
28683
30627
|
}
|
|
@@ -28689,9 +30633,9 @@ async function uploadFile2(params) {
|
|
|
28689
30633
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
28690
30634
|
}
|
|
28691
30635
|
const url = `${base}/upload`;
|
|
28692
|
-
const fileBuffer =
|
|
28693
|
-
const fileName =
|
|
28694
|
-
const ext =
|
|
30636
|
+
const fileBuffer = fs7.readFileSync(resolvedPath);
|
|
30637
|
+
const fileName = path6.basename(resolvedPath);
|
|
30638
|
+
const ext = path6.extname(fileName).toLowerCase();
|
|
28695
30639
|
const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
|
|
28696
30640
|
const formData = new FormData;
|
|
28697
30641
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -28750,15 +30694,15 @@ async function downloadFile2(params) {
|
|
|
28750
30694
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28751
30695
|
fullUrl = `${base}${relativePath}`;
|
|
28752
30696
|
}
|
|
28753
|
-
const urlFilename =
|
|
30697
|
+
const urlFilename = path6.basename(new URL(fullUrl).pathname);
|
|
28754
30698
|
if (!urlFilename) {
|
|
28755
30699
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
28756
30700
|
}
|
|
28757
|
-
const saveTo = outputPath ?
|
|
30701
|
+
const saveTo = outputPath ? path6.resolve(outputPath) : path6.resolve(urlFilename);
|
|
28758
30702
|
if (!outputPath) {
|
|
28759
|
-
const normalizedSave =
|
|
28760
|
-
const cwd =
|
|
28761
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
30703
|
+
const normalizedSave = path6.normalize(saveTo);
|
|
30704
|
+
const cwd = path6.normalize(process.cwd());
|
|
30705
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path6.sep)) {
|
|
28762
30706
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
28763
30707
|
}
|
|
28764
30708
|
}
|
|
@@ -28790,11 +30734,11 @@ async function downloadFile2(params) {
|
|
|
28790
30734
|
}
|
|
28791
30735
|
const arrayBuffer = await response.arrayBuffer();
|
|
28792
30736
|
const buffer = Buffer.from(arrayBuffer);
|
|
28793
|
-
const parentDir =
|
|
28794
|
-
if (!
|
|
28795
|
-
|
|
30737
|
+
const parentDir = path6.dirname(saveTo);
|
|
30738
|
+
if (!fs7.existsSync(parentDir)) {
|
|
30739
|
+
fs7.mkdirSync(parentDir, { recursive: true });
|
|
28796
30740
|
}
|
|
28797
|
-
|
|
30741
|
+
fs7.writeFileSync(saveTo, buffer);
|
|
28798
30742
|
return {
|
|
28799
30743
|
savedTo: saveTo,
|
|
28800
30744
|
size: buffer.length,
|
|
@@ -28806,9 +30750,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
|
|
|
28806
30750
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
28807
30751
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28808
30752
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
28809
|
-
const name = filename ||
|
|
30753
|
+
const name = filename || path6.basename(new URL(fullUrl).pathname);
|
|
28810
30754
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
28811
|
-
const ext =
|
|
30755
|
+
const ext = path6.extname(name).toLowerCase();
|
|
28812
30756
|
if (IMAGE_EXTENSIONS2.has(ext)) {
|
|
28813
30757
|
return ``;
|
|
28814
30758
|
}
|
|
@@ -28854,8 +30798,8 @@ ${links}`;
|
|
|
28854
30798
|
// lib/init.ts
|
|
28855
30799
|
import { randomBytes } from "crypto";
|
|
28856
30800
|
import { URL as URL2, fileURLToPath } from "url";
|
|
28857
|
-
import * as
|
|
28858
|
-
import * as
|
|
30801
|
+
import * as fs8 from "fs";
|
|
30802
|
+
import * as path7 from "path";
|
|
28859
30803
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
28860
30804
|
var KNOWN_PROVIDERS = ["self-managed", "supabase"];
|
|
28861
30805
|
var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
|
|
@@ -28947,21 +30891,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
|
28947
30891
|
}
|
|
28948
30892
|
function sqlDir() {
|
|
28949
30893
|
const currentFile = fileURLToPath(import.meta.url);
|
|
28950
|
-
const currentDir =
|
|
30894
|
+
const currentDir = path7.dirname(currentFile);
|
|
28951
30895
|
const candidates = [
|
|
28952
|
-
|
|
28953
|
-
|
|
30896
|
+
path7.resolve(currentDir, "..", "sql"),
|
|
30897
|
+
path7.resolve(currentDir, "..", "..", "sql")
|
|
28954
30898
|
];
|
|
28955
30899
|
for (const candidate of candidates) {
|
|
28956
|
-
if (
|
|
30900
|
+
if (fs8.existsSync(candidate)) {
|
|
28957
30901
|
return candidate;
|
|
28958
30902
|
}
|
|
28959
30903
|
}
|
|
28960
30904
|
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
28961
30905
|
}
|
|
28962
30906
|
function loadSqlTemplate(filename) {
|
|
28963
|
-
const p =
|
|
28964
|
-
return
|
|
30907
|
+
const p = path7.join(sqlDir(), filename);
|
|
30908
|
+
return fs8.readFileSync(p, "utf8");
|
|
28965
30909
|
}
|
|
28966
30910
|
function applyTemplate(sql, vars) {
|
|
28967
30911
|
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
@@ -29431,10 +31375,6 @@ async function verifyInitSetup(params) {
|
|
|
29431
31375
|
}
|
|
29432
31376
|
}
|
|
29433
31377
|
}
|
|
29434
|
-
const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
|
|
29435
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29436
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29437
|
-
}
|
|
29438
31378
|
const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
|
|
29439
31379
|
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
29440
31380
|
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
@@ -29953,15 +31893,6 @@ async function verifyInitSetupViaSupabase(params) {
|
|
|
29953
31893
|
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
29954
31894
|
}
|
|
29955
31895
|
}
|
|
29956
|
-
const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29957
|
-
if (explainFnExistsRes.rowCount === 0) {
|
|
29958
|
-
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
29959
|
-
} else {
|
|
29960
|
-
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
29961
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29962
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29963
|
-
}
|
|
29964
|
-
}
|
|
29965
31896
|
const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29966
31897
|
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
29967
31898
|
missingRequired.push("function postgres_ai.table_describe exists");
|
|
@@ -30019,9 +31950,9 @@ function escapeLiteral2(value) {
|
|
|
30019
31950
|
}
|
|
30020
31951
|
|
|
30021
31952
|
// lib/pkce.ts
|
|
30022
|
-
import * as
|
|
31953
|
+
import * as crypto3 from "crypto";
|
|
30023
31954
|
function generateRandomString(length = 64) {
|
|
30024
|
-
const bytes =
|
|
31955
|
+
const bytes = crypto3.randomBytes(length);
|
|
30025
31956
|
return base64URLEncode(bytes);
|
|
30026
31957
|
}
|
|
30027
31958
|
function base64URLEncode(buffer) {
|
|
@@ -30031,7 +31962,7 @@ function generateCodeVerifier() {
|
|
|
30031
31962
|
return generateRandomString(32);
|
|
30032
31963
|
}
|
|
30033
31964
|
function generateCodeChallenge(verifier) {
|
|
30034
|
-
const hash =
|
|
31965
|
+
const hash = crypto3.createHash("sha256").update(verifier).digest();
|
|
30035
31966
|
return base64URLEncode(hash);
|
|
30036
31967
|
}
|
|
30037
31968
|
function generateState() {
|
|
@@ -30260,8 +32191,8 @@ import { createInterface } from "readline";
|
|
|
30260
32191
|
import * as childProcess from "child_process";
|
|
30261
32192
|
|
|
30262
32193
|
// lib/checkup.ts
|
|
30263
|
-
import * as
|
|
30264
|
-
import * as
|
|
32194
|
+
import * as fs9 from "fs";
|
|
32195
|
+
import * as path8 from "path";
|
|
30265
32196
|
|
|
30266
32197
|
// lib/metrics-embedded.ts
|
|
30267
32198
|
var METRICS = {
|
|
@@ -31992,7 +33923,7 @@ function buildCheckInfoMap() {
|
|
|
31992
33923
|
}
|
|
31993
33924
|
|
|
31994
33925
|
// lib/checkup.ts
|
|
31995
|
-
var __dirname = "/
|
|
33926
|
+
var __dirname = "/home/agent/workspace/postgresai-joe-v2/cli/lib";
|
|
31996
33927
|
var SECONDS_PER_DAY = 86400;
|
|
31997
33928
|
var SECONDS_PER_HOUR = 3600;
|
|
31998
33929
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -32478,7 +34409,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
|
|
|
32478
34409
|
}
|
|
32479
34410
|
function readTextFileSafe(p) {
|
|
32480
34411
|
try {
|
|
32481
|
-
const value =
|
|
34412
|
+
const value = fs9.readFileSync(p, "utf8").trim();
|
|
32482
34413
|
return value || null;
|
|
32483
34414
|
} catch {
|
|
32484
34415
|
return null;
|
|
@@ -32491,8 +34422,8 @@ function resolveBuildTs() {
|
|
|
32491
34422
|
if (fromFile)
|
|
32492
34423
|
return fromFile;
|
|
32493
34424
|
try {
|
|
32494
|
-
const pkgRoot =
|
|
32495
|
-
const fromPkgFile = readTextFileSafe(
|
|
34425
|
+
const pkgRoot = path8.resolve(__dirname, "..");
|
|
34426
|
+
const fromPkgFile = readTextFileSafe(path8.join(pkgRoot, "BUILD_TS"));
|
|
32496
34427
|
if (fromPkgFile)
|
|
32497
34428
|
return fromPkgFile;
|
|
32498
34429
|
} catch (err) {
|
|
@@ -32500,8 +34431,8 @@ function resolveBuildTs() {
|
|
|
32500
34431
|
console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
|
|
32501
34432
|
}
|
|
32502
34433
|
try {
|
|
32503
|
-
const pkgJsonPath =
|
|
32504
|
-
const st =
|
|
34434
|
+
const pkgJsonPath = path8.resolve(__dirname, "..", "package.json");
|
|
34435
|
+
const st = fs9.statSync(pkgJsonPath);
|
|
32505
34436
|
return st.mtime.toISOString();
|
|
32506
34437
|
} catch (err) {
|
|
32507
34438
|
if (process.env.DEBUG) {
|
|
@@ -33782,7 +35713,7 @@ function summarizeG003(nodeData) {
|
|
|
33782
35713
|
}
|
|
33783
35714
|
|
|
33784
35715
|
// lib/instances.ts
|
|
33785
|
-
import * as
|
|
35716
|
+
import * as fs10 from "fs";
|
|
33786
35717
|
class InstancesParseError2 extends Error {
|
|
33787
35718
|
constructor(file, cause) {
|
|
33788
35719
|
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -33791,11 +35722,11 @@ class InstancesParseError2 extends Error {
|
|
|
33791
35722
|
}
|
|
33792
35723
|
}
|
|
33793
35724
|
function loadInstances2(file) {
|
|
33794
|
-
if (!
|
|
35725
|
+
if (!fs10.existsSync(file))
|
|
33795
35726
|
return [];
|
|
33796
|
-
if (
|
|
35727
|
+
if (fs10.lstatSync(file).isDirectory())
|
|
33797
35728
|
return [];
|
|
33798
|
-
const text =
|
|
35729
|
+
const text = fs10.readFileSync(file, "utf8");
|
|
33799
35730
|
if (text.trim() === "")
|
|
33800
35731
|
return [];
|
|
33801
35732
|
let parsed;
|
|
@@ -33828,22 +35759,22 @@ function buildInstance(name, connStr) {
|
|
|
33828
35759
|
};
|
|
33829
35760
|
}
|
|
33830
35761
|
function addInstanceToFile(file, instance) {
|
|
33831
|
-
if (
|
|
33832
|
-
|
|
35762
|
+
if (fs10.existsSync(file) && fs10.lstatSync(file).isDirectory()) {
|
|
35763
|
+
fs10.rmSync(file, { recursive: true, force: true });
|
|
33833
35764
|
}
|
|
33834
35765
|
const existing = loadInstances2(file);
|
|
33835
35766
|
if (existing.some((i3) => i3.name === instance.name)) {
|
|
33836
35767
|
throw new Error(`Monitoring target '${instance.name}' already exists`);
|
|
33837
35768
|
}
|
|
33838
35769
|
existing.push(instance);
|
|
33839
|
-
|
|
35770
|
+
fs10.writeFileSync(file, dump2(existing), "utf8");
|
|
33840
35771
|
}
|
|
33841
35772
|
function removeInstanceFromFile(file, name) {
|
|
33842
35773
|
const instances = loadInstances2(file);
|
|
33843
35774
|
const filtered = instances.filter((i3) => i3.name !== name);
|
|
33844
35775
|
if (filtered.length === instances.length)
|
|
33845
35776
|
return false;
|
|
33846
|
-
|
|
35777
|
+
fs10.writeFileSync(file, dump2(filtered), "utf8");
|
|
33847
35778
|
return true;
|
|
33848
35779
|
}
|
|
33849
35780
|
function extractSslmode(connStr) {
|
|
@@ -33938,13 +35869,13 @@ function stripMatchingQuotes(value) {
|
|
|
33938
35869
|
return trimmed;
|
|
33939
35870
|
}
|
|
33940
35871
|
var REQUIRED_ENV_KEYS = [
|
|
33941
|
-
{ key: "REPLICATOR_PASSWORD", defaultValue: () =>
|
|
35872
|
+
{ key: "REPLICATOR_PASSWORD", defaultValue: () => crypto4.randomBytes(32).toString("hex"), introducedIn: "0.13" },
|
|
33942
35873
|
{ key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
|
|
33943
|
-
{ key: "VM_AUTH_PASSWORD", defaultValue: () =>
|
|
35874
|
+
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto4.randomBytes(18).toString("base64"), introducedIn: "0.15" }
|
|
33944
35875
|
];
|
|
33945
35876
|
function ensureRequiredEnvVars(projectDir) {
|
|
33946
|
-
const envFile =
|
|
33947
|
-
const existing =
|
|
35877
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35878
|
+
const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
|
|
33948
35879
|
const added = [];
|
|
33949
35880
|
const appendLines = [];
|
|
33950
35881
|
for (const spec of REQUIRED_ENV_KEYS) {
|
|
@@ -33963,7 +35894,7 @@ function ensureRequiredEnvVars(projectDir) {
|
|
|
33963
35894
|
` : "") + appendLines.join(`
|
|
33964
35895
|
`) + `
|
|
33965
35896
|
`;
|
|
33966
|
-
|
|
35897
|
+
fs11.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
|
|
33967
35898
|
return added;
|
|
33968
35899
|
}
|
|
33969
35900
|
async function execFilePromise(file, args) {
|
|
@@ -34028,7 +35959,7 @@ function expandHomePath(p) {
|
|
|
34028
35959
|
if (s === "~")
|
|
34029
35960
|
return os3.homedir();
|
|
34030
35961
|
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
34031
|
-
return
|
|
35962
|
+
return path9.join(os3.homedir(), s.slice(2));
|
|
34032
35963
|
}
|
|
34033
35964
|
return s;
|
|
34034
35965
|
}
|
|
@@ -34077,10 +36008,10 @@ function prepareOutputDirectory(outputOpt) {
|
|
|
34077
36008
|
if (!outputOpt)
|
|
34078
36009
|
return;
|
|
34079
36010
|
const outputDir = expandHomePath(outputOpt);
|
|
34080
|
-
const outputPath =
|
|
34081
|
-
if (!
|
|
36011
|
+
const outputPath = path9.isAbsolute(outputDir) ? outputDir : path9.resolve(process.cwd(), outputDir);
|
|
36012
|
+
if (!fs11.existsSync(outputPath)) {
|
|
34082
36013
|
try {
|
|
34083
|
-
|
|
36014
|
+
fs11.mkdirSync(outputPath, { recursive: true });
|
|
34084
36015
|
} catch (e) {
|
|
34085
36016
|
const errAny = e;
|
|
34086
36017
|
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
@@ -34116,7 +36047,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
34116
36047
|
let project = (opts.project || cfg.defaultProject || "").trim();
|
|
34117
36048
|
let projectWasGenerated = false;
|
|
34118
36049
|
if (!project) {
|
|
34119
|
-
project = `project_${
|
|
36050
|
+
project = `project_${crypto4.randomBytes(6).toString("hex")}`;
|
|
34120
36051
|
projectWasGenerated = true;
|
|
34121
36052
|
try {
|
|
34122
36053
|
writeConfig({ defaultProject: project });
|
|
@@ -34162,8 +36093,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
34162
36093
|
}
|
|
34163
36094
|
function writeReportFiles(reports, outputPath) {
|
|
34164
36095
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
34165
|
-
const filePath =
|
|
34166
|
-
|
|
36096
|
+
const filePath = path9.join(outputPath, `${checkId}.json`);
|
|
36097
|
+
fs11.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
34167
36098
|
const title = report.checkTitle || checkId;
|
|
34168
36099
|
console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
|
|
34169
36100
|
}
|
|
@@ -34208,7 +36139,7 @@ function getDefaultMonitoringProjectDir() {
|
|
|
34208
36139
|
const override = process.env.PGAI_PROJECT_DIR;
|
|
34209
36140
|
if (override && override.trim())
|
|
34210
36141
|
return override.trim();
|
|
34211
|
-
return
|
|
36142
|
+
return path9.join(getConfigDir(), "monitoring");
|
|
34212
36143
|
}
|
|
34213
36144
|
async function downloadText(url) {
|
|
34214
36145
|
const controller = new AbortController;
|
|
@@ -34225,12 +36156,12 @@ async function downloadText(url) {
|
|
|
34225
36156
|
}
|
|
34226
36157
|
async function ensureDefaultMonitoringProject() {
|
|
34227
36158
|
const projectDir = getDefaultMonitoringProjectDir();
|
|
34228
|
-
const composeFile =
|
|
34229
|
-
const instancesFile =
|
|
34230
|
-
if (!
|
|
34231
|
-
|
|
36159
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36160
|
+
const instancesFile = path9.resolve(projectDir, "instances.yml");
|
|
36161
|
+
if (!fs11.existsSync(projectDir)) {
|
|
36162
|
+
fs11.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
34232
36163
|
}
|
|
34233
|
-
if (!
|
|
36164
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34234
36165
|
const refs = [
|
|
34235
36166
|
process.env.PGAI_PROJECT_REF,
|
|
34236
36167
|
package_default.version,
|
|
@@ -34242,39 +36173,39 @@ async function ensureDefaultMonitoringProject() {
|
|
|
34242
36173
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
34243
36174
|
try {
|
|
34244
36175
|
const text = await downloadText(url);
|
|
34245
|
-
|
|
36176
|
+
fs11.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
|
|
34246
36177
|
break;
|
|
34247
36178
|
} catch (err) {
|
|
34248
36179
|
lastErr = err;
|
|
34249
36180
|
}
|
|
34250
36181
|
}
|
|
34251
|
-
if (!
|
|
36182
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34252
36183
|
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
34253
36184
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
34254
36185
|
}
|
|
34255
36186
|
}
|
|
34256
|
-
if (
|
|
34257
|
-
|
|
36187
|
+
if (fs11.existsSync(instancesFile) && fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36188
|
+
fs11.rmSync(instancesFile, { recursive: true, force: true });
|
|
34258
36189
|
}
|
|
34259
|
-
if (!
|
|
36190
|
+
if (!fs11.existsSync(instancesFile)) {
|
|
34260
36191
|
const header = `# PostgreSQL instances to monitor
|
|
34261
36192
|
` + `# Add your instances using: pgai mon targets add <connection-string> <name>
|
|
34262
36193
|
|
|
34263
36194
|
`;
|
|
34264
|
-
|
|
36195
|
+
fs11.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
|
|
34265
36196
|
}
|
|
34266
|
-
const pgwatchConfig =
|
|
34267
|
-
if (!
|
|
34268
|
-
|
|
36197
|
+
const pgwatchConfig = path9.resolve(projectDir, ".pgwatch-config");
|
|
36198
|
+
if (!fs11.existsSync(pgwatchConfig)) {
|
|
36199
|
+
fs11.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
|
|
34269
36200
|
}
|
|
34270
|
-
const envFile =
|
|
34271
|
-
if (!
|
|
36201
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36202
|
+
if (!fs11.existsSync(envFile)) {
|
|
34272
36203
|
const envText = `PGAI_TAG=${package_default.version}
|
|
34273
36204
|
# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
|
|
34274
36205
|
`;
|
|
34275
|
-
|
|
36206
|
+
fs11.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
|
|
34276
36207
|
}
|
|
34277
|
-
return { fs:
|
|
36208
|
+
return { fs: fs11, path: path9, projectDir, composeFile, instancesFile };
|
|
34278
36209
|
}
|
|
34279
36210
|
function sanitizeTagForBackup(tag) {
|
|
34280
36211
|
if (tag == null)
|
|
@@ -34283,10 +36214,10 @@ function sanitizeTagForBackup(tag) {
|
|
|
34283
36214
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
34284
36215
|
}
|
|
34285
36216
|
function readDeployedTag(projectDir) {
|
|
34286
|
-
const envFile =
|
|
34287
|
-
if (!
|
|
36217
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36218
|
+
if (!fs11.existsSync(envFile))
|
|
34288
36219
|
return null;
|
|
34289
|
-
const m =
|
|
36220
|
+
const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
34290
36221
|
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
34291
36222
|
}
|
|
34292
36223
|
function isValidComposeYaml(text) {
|
|
@@ -34323,10 +36254,10 @@ function composeContentEqual(a, b) {
|
|
|
34323
36254
|
return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
|
|
34324
36255
|
}
|
|
34325
36256
|
async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
34326
|
-
if (
|
|
36257
|
+
if (fs11.existsSync(path9.resolve(projectDir, ".git")))
|
|
34327
36258
|
return false;
|
|
34328
|
-
const composeFile =
|
|
34329
|
-
if (!
|
|
36259
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36260
|
+
if (!fs11.existsSync(composeFile))
|
|
34330
36261
|
return false;
|
|
34331
36262
|
const refs = [
|
|
34332
36263
|
process.env.PGAI_PROJECT_REF,
|
|
@@ -34339,24 +36270,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
34339
36270
|
console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
|
|
34340
36271
|
return false;
|
|
34341
36272
|
}
|
|
34342
|
-
const existing =
|
|
36273
|
+
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
34343
36274
|
if (composeContentEqual(existing, fetched))
|
|
34344
36275
|
return false;
|
|
34345
36276
|
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
34346
36277
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
34347
|
-
const oldHash =
|
|
34348
|
-
const backup =
|
|
36278
|
+
const oldHash = crypto4.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
36279
|
+
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
34349
36280
|
let backupName = null;
|
|
34350
36281
|
try {
|
|
34351
|
-
|
|
34352
|
-
backupName =
|
|
36282
|
+
fs11.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
36283
|
+
backupName = path9.basename(backup);
|
|
34353
36284
|
} catch (err) {
|
|
34354
36285
|
const e = err;
|
|
34355
36286
|
if (e && e.code === "EEXIST") {
|
|
34356
|
-
backupName =
|
|
36287
|
+
backupName = path9.basename(backup);
|
|
34357
36288
|
}
|
|
34358
36289
|
}
|
|
34359
|
-
|
|
36290
|
+
fs11.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
|
|
34360
36291
|
const backupNote = backupName ? ` (backup: ${backupName})` : "";
|
|
34361
36292
|
console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
|
|
34362
36293
|
return true;
|
|
@@ -35590,12 +37521,12 @@ function resolvePaths() {
|
|
|
35590
37521
|
const startDir = process.cwd();
|
|
35591
37522
|
let currentDir = startDir;
|
|
35592
37523
|
while (true) {
|
|
35593
|
-
const composeFile =
|
|
35594
|
-
if (
|
|
35595
|
-
const instancesFile =
|
|
35596
|
-
return { fs:
|
|
37524
|
+
const composeFile = path9.resolve(currentDir, "docker-compose.yml");
|
|
37525
|
+
if (fs11.existsSync(composeFile)) {
|
|
37526
|
+
const instancesFile = path9.resolve(currentDir, "instances.yml");
|
|
37527
|
+
return { fs: fs11, path: path9, projectDir: currentDir, composeFile, instancesFile };
|
|
35597
37528
|
}
|
|
35598
|
-
const parentDir =
|
|
37529
|
+
const parentDir = path9.dirname(currentDir);
|
|
35599
37530
|
if (parentDir === currentDir)
|
|
35600
37531
|
break;
|
|
35601
37532
|
currentDir = parentDir;
|
|
@@ -35638,6 +37569,16 @@ function checkRunningContainers() {
|
|
|
35638
37569
|
return { running: false, containers: [] };
|
|
35639
37570
|
}
|
|
35640
37571
|
}
|
|
37572
|
+
function planMonitoringRegistration(args) {
|
|
37573
|
+
const projectName = args.project?.trim() || undefined;
|
|
37574
|
+
if (args.instanceId) {
|
|
37575
|
+
return { kind: "adopt", projectName };
|
|
37576
|
+
}
|
|
37577
|
+
if (!projectName) {
|
|
37578
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
37579
|
+
}
|
|
37580
|
+
return { kind: "self-register", projectName };
|
|
37581
|
+
}
|
|
35641
37582
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
35642
37583
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
35643
37584
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -35645,16 +37586,19 @@ async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
|
35645
37586
|
const instanceId = opts?.instanceId;
|
|
35646
37587
|
const retries = opts?.retries ?? (instanceId ? 1 : 0);
|
|
35647
37588
|
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
37589
|
+
const hasProjectName = !!(projectName && projectName.trim());
|
|
35648
37590
|
if (debug) {
|
|
35649
37591
|
console.error(`
|
|
35650
37592
|
Debug: Registering monitoring instance...`);
|
|
35651
37593
|
console.error(`Debug: POST ${url}`);
|
|
35652
|
-
console.error(`Debug: project_name=${projectName}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
37594
|
+
console.error(`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
35653
37595
|
}
|
|
35654
37596
|
const requestBody = {
|
|
35655
|
-
api_token: apiKey
|
|
35656
|
-
project_name: projectName
|
|
37597
|
+
api_token: apiKey
|
|
35657
37598
|
};
|
|
37599
|
+
if (hasProjectName) {
|
|
37600
|
+
requestBody.project_name = projectName;
|
|
37601
|
+
}
|
|
35658
37602
|
if (instanceId) {
|
|
35659
37603
|
requestBody.instance_id = instanceId;
|
|
35660
37604
|
}
|
|
@@ -35714,10 +37658,10 @@ function resolveAdoptedProject(reg) {
|
|
|
35714
37658
|
}
|
|
35715
37659
|
function updatePgwatchConfig(configPath, updates) {
|
|
35716
37660
|
let lines = [];
|
|
35717
|
-
if (
|
|
35718
|
-
const stats =
|
|
37661
|
+
if (fs11.existsSync(configPath)) {
|
|
37662
|
+
const stats = fs11.statSync(configPath);
|
|
35719
37663
|
if (!stats.isDirectory()) {
|
|
35720
|
-
const content =
|
|
37664
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
35721
37665
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
35722
37666
|
}
|
|
35723
37667
|
}
|
|
@@ -35729,7 +37673,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
35729
37673
|
lines.push(`${key}=${value}`);
|
|
35730
37674
|
}
|
|
35731
37675
|
}
|
|
35732
|
-
|
|
37676
|
+
fs11.writeFileSync(configPath, lines.join(`
|
|
35733
37677
|
`) + `
|
|
35734
37678
|
`, { encoding: "utf8", mode: 384 });
|
|
35735
37679
|
}
|
|
@@ -35767,12 +37711,12 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35767
37711
|
if (grafanaPassword) {
|
|
35768
37712
|
env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
|
|
35769
37713
|
} else {
|
|
35770
|
-
const cfgPath =
|
|
35771
|
-
if (
|
|
37714
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37715
|
+
if (fs11.existsSync(cfgPath)) {
|
|
35772
37716
|
try {
|
|
35773
|
-
const stats =
|
|
37717
|
+
const stats = fs11.statSync(cfgPath);
|
|
35774
37718
|
if (!stats.isDirectory()) {
|
|
35775
|
-
const content =
|
|
37719
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
35776
37720
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
35777
37721
|
if (match) {
|
|
35778
37722
|
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
@@ -35785,10 +37729,10 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35785
37729
|
}
|
|
35786
37730
|
}
|
|
35787
37731
|
}
|
|
35788
|
-
const envFilePath =
|
|
35789
|
-
if (
|
|
37732
|
+
const envFilePath = path9.resolve(projectDir, ".env");
|
|
37733
|
+
if (fs11.existsSync(envFilePath)) {
|
|
35790
37734
|
try {
|
|
35791
|
-
const envContent =
|
|
37735
|
+
const envContent = fs11.readFileSync(envFilePath, "utf8");
|
|
35792
37736
|
if (!env.VM_AUTH_USERNAME) {
|
|
35793
37737
|
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
35794
37738
|
if (m)
|
|
@@ -35845,20 +37789,20 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35845
37789
|
console.log(`Project directory: ${projectDir}
|
|
35846
37790
|
`);
|
|
35847
37791
|
if (opts.project) {
|
|
35848
|
-
const cfgPath2 =
|
|
37792
|
+
const cfgPath2 = path9.resolve(projectDir, ".pgwatch-config");
|
|
35849
37793
|
updatePgwatchConfig(cfgPath2, { project_name: opts.project });
|
|
35850
37794
|
console.log(`Using project name: ${opts.project}
|
|
35851
37795
|
`);
|
|
35852
37796
|
}
|
|
35853
|
-
const envFile =
|
|
37797
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35854
37798
|
let existingRegistry = null;
|
|
35855
37799
|
let existingPassword = null;
|
|
35856
37800
|
let existingReplicatorPassword = null;
|
|
35857
37801
|
let existingVmAuthUsername = null;
|
|
35858
37802
|
let existingVmAuthPassword = null;
|
|
35859
37803
|
let previousTag = null;
|
|
35860
|
-
if (
|
|
35861
|
-
const existingEnv =
|
|
37804
|
+
if (fs11.existsSync(envFile)) {
|
|
37805
|
+
const existingEnv = fs11.readFileSync(envFile, "utf8");
|
|
35862
37806
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
35863
37807
|
if (previousTagMatch)
|
|
35864
37808
|
previousTag = previousTagMatch[1].trim();
|
|
@@ -35886,10 +37830,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35886
37830
|
if (existingPassword) {
|
|
35887
37831
|
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
35888
37832
|
}
|
|
35889
|
-
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword ||
|
|
37833
|
+
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto4.randomBytes(32).toString("hex")}`);
|
|
35890
37834
|
envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
|
|
35891
|
-
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword ||
|
|
35892
|
-
|
|
37835
|
+
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto4.randomBytes(18).toString("base64")}`);
|
|
37836
|
+
fs11.writeFileSync(envFile, envLines.join(`
|
|
35893
37837
|
`) + `
|
|
35894
37838
|
`, { encoding: "utf8", mode: 384 });
|
|
35895
37839
|
await refreshBundledComposeIfStale(projectDir, previousTag);
|
|
@@ -35926,7 +37870,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35926
37870
|
if (apiKey) {
|
|
35927
37871
|
console.log("Using API key provided via --api-key parameter");
|
|
35928
37872
|
writeConfig({ apiKey });
|
|
35929
|
-
updatePgwatchConfig(
|
|
37873
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
|
|
35930
37874
|
console.log(`\u2713 API key saved
|
|
35931
37875
|
`);
|
|
35932
37876
|
} else if (opts.yes) {
|
|
@@ -35943,7 +37887,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35943
37887
|
const trimmedKey = inputApiKey.trim();
|
|
35944
37888
|
if (trimmedKey) {
|
|
35945
37889
|
writeConfig({ apiKey: trimmedKey });
|
|
35946
|
-
updatePgwatchConfig(
|
|
37890
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
|
|
35947
37891
|
apiKey = trimmedKey;
|
|
35948
37892
|
console.log(`\u2713 API key saved
|
|
35949
37893
|
`);
|
|
@@ -35977,7 +37921,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35977
37921
|
# Add your instances using: postgres-ai mon targets add
|
|
35978
37922
|
|
|
35979
37923
|
`;
|
|
35980
|
-
|
|
37924
|
+
fs11.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
|
|
35981
37925
|
console.log(`Instances file: ${instancesPath2}`);
|
|
35982
37926
|
console.log(`Project directory: ${projectDir2}
|
|
35983
37927
|
`);
|
|
@@ -36081,17 +38025,17 @@ You can provide either:`);
|
|
|
36081
38025
|
}
|
|
36082
38026
|
} else {
|
|
36083
38027
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
36084
|
-
const currentDir =
|
|
38028
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
36085
38029
|
const demoCandidates = [
|
|
36086
|
-
|
|
36087
|
-
|
|
38030
|
+
path9.resolve(currentDir, "..", "..", "instances.demo.yml"),
|
|
38031
|
+
path9.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
|
|
36088
38032
|
];
|
|
36089
|
-
const demoSrc = demoCandidates.find((p) =>
|
|
38033
|
+
const demoSrc = demoCandidates.find((p) => fs11.existsSync(p));
|
|
36090
38034
|
if (demoSrc) {
|
|
36091
|
-
if (
|
|
36092
|
-
|
|
38035
|
+
if (fs11.existsSync(instancesPath) && fs11.lstatSync(instancesPath).isDirectory()) {
|
|
38036
|
+
fs11.rmSync(instancesPath, { recursive: true, force: true });
|
|
36093
38037
|
}
|
|
36094
|
-
|
|
38038
|
+
fs11.copyFileSync(demoSrc, instancesPath);
|
|
36095
38039
|
console.log(`\u2713 Demo monitoring target configured
|
|
36096
38040
|
`);
|
|
36097
38041
|
} else {
|
|
@@ -36110,15 +38054,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36110
38054
|
console.log(`\u2713 Configuration updated
|
|
36111
38055
|
`);
|
|
36112
38056
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
36113
|
-
const cfgPath =
|
|
38057
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
36114
38058
|
let grafanaPassword = "";
|
|
36115
38059
|
let vmAuthUsername = "";
|
|
36116
38060
|
let vmAuthPassword = "";
|
|
36117
38061
|
try {
|
|
36118
|
-
if (
|
|
36119
|
-
const stats =
|
|
38062
|
+
if (fs11.existsSync(cfgPath)) {
|
|
38063
|
+
const stats = fs11.statSync(cfgPath);
|
|
36120
38064
|
if (!stats.isDirectory()) {
|
|
36121
|
-
const content =
|
|
38065
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
36122
38066
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
36123
38067
|
if (match) {
|
|
36124
38068
|
grafanaPassword = match[1].trim();
|
|
@@ -36130,15 +38074,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36130
38074
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
36131
38075
|
grafanaPassword = password.trim().replace(/\n/g, "");
|
|
36132
38076
|
let configContent = "";
|
|
36133
|
-
if (
|
|
36134
|
-
const stats =
|
|
38077
|
+
if (fs11.existsSync(cfgPath)) {
|
|
38078
|
+
const stats = fs11.statSync(cfgPath);
|
|
36135
38079
|
if (!stats.isDirectory()) {
|
|
36136
|
-
configContent =
|
|
38080
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
36137
38081
|
}
|
|
36138
38082
|
}
|
|
36139
38083
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
36140
38084
|
lines.push(`grafana_password=${grafanaPassword}`);
|
|
36141
|
-
|
|
38085
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
36142
38086
|
`) + `
|
|
36143
38087
|
`, "utf8");
|
|
36144
38088
|
}
|
|
@@ -36151,9 +38095,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36151
38095
|
grafanaPassword = "demo";
|
|
36152
38096
|
}
|
|
36153
38097
|
try {
|
|
36154
|
-
const envFile2 =
|
|
36155
|
-
if (
|
|
36156
|
-
const envContent =
|
|
38098
|
+
const envFile2 = path9.resolve(projectDir, ".env");
|
|
38099
|
+
if (fs11.existsSync(envFile2)) {
|
|
38100
|
+
const envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36157
38101
|
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
36158
38102
|
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
36159
38103
|
if (userMatch)
|
|
@@ -36169,13 +38113,13 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36169
38113
|
vmAuthPassword = vmPass.trim().replace(/\n/g, "");
|
|
36170
38114
|
}
|
|
36171
38115
|
let envContent = "";
|
|
36172
|
-
if (
|
|
36173
|
-
envContent =
|
|
38116
|
+
if (fs11.existsSync(envFile2)) {
|
|
38117
|
+
envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36174
38118
|
}
|
|
36175
38119
|
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 === ""));
|
|
36176
38120
|
envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
36177
38121
|
envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
36178
|
-
|
|
38122
|
+
fs11.writeFileSync(envFile2, envLines2.join(`
|
|
36179
38123
|
`) + `
|
|
36180
38124
|
`, { encoding: "utf8", mode: 384 });
|
|
36181
38125
|
}
|
|
@@ -36197,8 +38141,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36197
38141
|
console.log(`\u2713 Services started
|
|
36198
38142
|
`);
|
|
36199
38143
|
if (apiKey && !opts.demo) {
|
|
36200
|
-
const projectName = opts.project || "postgres-ai-monitoring";
|
|
36201
38144
|
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
38145
|
+
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
38146
|
+
const projectName = plan.projectName;
|
|
36202
38147
|
if (instanceId) {
|
|
36203
38148
|
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
36204
38149
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36207,16 +38152,16 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36207
38152
|
});
|
|
36208
38153
|
const adoptedProject = resolveAdoptedProject(reg);
|
|
36209
38154
|
if (adoptedProject != null) {
|
|
36210
|
-
updatePgwatchConfig(
|
|
38155
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), {
|
|
36211
38156
|
project_name: adoptedProject
|
|
36212
38157
|
});
|
|
36213
38158
|
const verb = reg?.created ? "Registered" : "Adopted";
|
|
36214
38159
|
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
36215
38160
|
`);
|
|
36216
38161
|
} else if (reg) {
|
|
36217
|
-
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project \u2014 reports will use project '${projectName}'`);
|
|
38162
|
+
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
36218
38163
|
} else {
|
|
36219
|
-
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId} \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`);
|
|
38164
|
+
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
36220
38165
|
}
|
|
36221
38166
|
const aas = await registerAasCollection(apiKey, instanceId, {
|
|
36222
38167
|
grafanaPassword,
|
|
@@ -36232,6 +38177,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36232
38177
|
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
36233
38178
|
`);
|
|
36234
38179
|
}
|
|
38180
|
+
} else if (plan.kind === "error-missing-project") {
|
|
38181
|
+
console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
|
|
38182
|
+
process.exitCode = 1;
|
|
36235
38183
|
} else {
|
|
36236
38184
|
registerMonitoringInstance(apiKey, projectName, {
|
|
36237
38185
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36420,11 +38368,11 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
36420
38368
|
console.log(`Project Directory: ${projectDir}`);
|
|
36421
38369
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
36422
38370
|
console.log(`Instances File: ${instancesFile}`);
|
|
36423
|
-
if (
|
|
38371
|
+
if (fs11.existsSync(instancesFile) && !fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36424
38372
|
console.log(`
|
|
36425
38373
|
Instances configuration:
|
|
36426
38374
|
`);
|
|
36427
|
-
const text =
|
|
38375
|
+
const text = fs11.readFileSync(instancesFile, "utf8");
|
|
36428
38376
|
process.stdout.write(text);
|
|
36429
38377
|
if (!/\n$/.test(text))
|
|
36430
38378
|
console.log();
|
|
@@ -36473,8 +38421,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
36473
38421
|
console.log("\u2713 .env is up to date");
|
|
36474
38422
|
}
|
|
36475
38423
|
console.log();
|
|
36476
|
-
const gitDir =
|
|
36477
|
-
if (
|
|
38424
|
+
const gitDir = path9.resolve(projectDir, ".git");
|
|
38425
|
+
if (fs11.existsSync(gitDir)) {
|
|
36478
38426
|
console.log("Fetching latest changes...");
|
|
36479
38427
|
await execFilePromise("git", ["fetch", "origin"]);
|
|
36480
38428
|
const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -36629,7 +38577,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
36629
38577
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
36630
38578
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
36631
38579
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
36632
|
-
if (!
|
|
38580
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36633
38581
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
36634
38582
|
process.exitCode = 1;
|
|
36635
38583
|
return;
|
|
@@ -36692,7 +38640,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
36692
38640
|
});
|
|
36693
38641
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
36694
38642
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
36695
|
-
if (!
|
|
38643
|
+
if (!fs11.existsSync(file) || fs11.lstatSync(file).isDirectory()) {
|
|
36696
38644
|
console.error("instances.yml not found");
|
|
36697
38645
|
process.exitCode = 1;
|
|
36698
38646
|
return;
|
|
@@ -36720,7 +38668,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
36720
38668
|
});
|
|
36721
38669
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
36722
38670
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
36723
|
-
if (!
|
|
38671
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36724
38672
|
console.error("instances.yml not found");
|
|
36725
38673
|
process.exitCode = 1;
|
|
36726
38674
|
return;
|
|
@@ -36982,15 +38930,15 @@ To authenticate, run: pgai auth`);
|
|
|
36982
38930
|
});
|
|
36983
38931
|
auth.command("remove-key").description("remove API key").action(async () => {
|
|
36984
38932
|
const newConfigPath = getConfigPath();
|
|
36985
|
-
const hasNewConfig =
|
|
38933
|
+
const hasNewConfig = fs11.existsSync(newConfigPath);
|
|
36986
38934
|
let legacyPath;
|
|
36987
38935
|
try {
|
|
36988
38936
|
const { projectDir } = await resolveOrInitPaths();
|
|
36989
|
-
legacyPath =
|
|
38937
|
+
legacyPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
36990
38938
|
} catch {
|
|
36991
|
-
legacyPath =
|
|
38939
|
+
legacyPath = path9.resolve(process.cwd(), ".pgwatch-config");
|
|
36992
38940
|
}
|
|
36993
|
-
const hasLegacyConfig =
|
|
38941
|
+
const hasLegacyConfig = fs11.existsSync(legacyPath) && fs11.statSync(legacyPath).isFile();
|
|
36994
38942
|
if (!hasNewConfig && !hasLegacyConfig) {
|
|
36995
38943
|
console.log("No API key configured");
|
|
36996
38944
|
return;
|
|
@@ -37000,11 +38948,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
|
|
|
37000
38948
|
}
|
|
37001
38949
|
if (hasLegacyConfig) {
|
|
37002
38950
|
try {
|
|
37003
|
-
const content =
|
|
38951
|
+
const content = fs11.readFileSync(legacyPath, "utf8");
|
|
37004
38952
|
const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
|
|
37005
38953
|
`).replace(/\n+$/g, `
|
|
37006
38954
|
`);
|
|
37007
|
-
|
|
38955
|
+
fs11.writeFileSync(legacyPath, filtered, "utf8");
|
|
37008
38956
|
} catch (err) {
|
|
37009
38957
|
console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
|
|
37010
38958
|
}
|
|
@@ -37015,7 +38963,7 @@ To authenticate again, run: pgai auth`);
|
|
|
37015
38963
|
});
|
|
37016
38964
|
mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
|
|
37017
38965
|
const { projectDir } = await resolveOrInitPaths();
|
|
37018
|
-
const cfgPath =
|
|
38966
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37019
38967
|
try {
|
|
37020
38968
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
37021
38969
|
const newPassword = password.trim().replace(/\n/g, "");
|
|
@@ -37025,17 +38973,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
|
|
|
37025
38973
|
return;
|
|
37026
38974
|
}
|
|
37027
38975
|
let configContent = "";
|
|
37028
|
-
if (
|
|
37029
|
-
const stats =
|
|
38976
|
+
if (fs11.existsSync(cfgPath)) {
|
|
38977
|
+
const stats = fs11.statSync(cfgPath);
|
|
37030
38978
|
if (stats.isDirectory()) {
|
|
37031
38979
|
console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
|
|
37032
38980
|
} else {
|
|
37033
|
-
configContent =
|
|
38981
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
37034
38982
|
}
|
|
37035
38983
|
}
|
|
37036
38984
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
37037
38985
|
lines.push(`grafana_password=${newPassword}`);
|
|
37038
|
-
|
|
38986
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
37039
38987
|
`) + `
|
|
37040
38988
|
`, "utf8");
|
|
37041
38989
|
console.log("\u2713 New Grafana password generated and saved");
|
|
@@ -37057,19 +39005,19 @@ Note: This command requires 'openssl' to be installed`);
|
|
|
37057
39005
|
});
|
|
37058
39006
|
mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
|
|
37059
39007
|
const { projectDir } = await resolveOrInitPaths();
|
|
37060
|
-
const cfgPath =
|
|
37061
|
-
if (!
|
|
39008
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
39009
|
+
if (!fs11.existsSync(cfgPath)) {
|
|
37062
39010
|
console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
|
|
37063
39011
|
process.exitCode = 1;
|
|
37064
39012
|
return;
|
|
37065
39013
|
}
|
|
37066
|
-
const stats =
|
|
39014
|
+
const stats = fs11.statSync(cfgPath);
|
|
37067
39015
|
if (stats.isDirectory()) {
|
|
37068
39016
|
console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
|
|
37069
39017
|
process.exitCode = 1;
|
|
37070
39018
|
return;
|
|
37071
39019
|
}
|
|
37072
|
-
const content =
|
|
39020
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
37073
39021
|
const lines = content.split(/\r?\n/);
|
|
37074
39022
|
let password = "";
|
|
37075
39023
|
for (const line of lines) {
|
|
@@ -37089,9 +39037,9 @@ Grafana credentials:`);
|
|
|
37089
39037
|
console.log(" URL: http://localhost:3000");
|
|
37090
39038
|
console.log(" Username: monitor");
|
|
37091
39039
|
console.log(` Password: ${password}`);
|
|
37092
|
-
const envFile =
|
|
37093
|
-
if (
|
|
37094
|
-
const envContent =
|
|
39040
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
39041
|
+
if (fs11.existsSync(envFile)) {
|
|
39042
|
+
const envContent = fs11.readFileSync(envFile, "utf8");
|
|
37095
39043
|
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
37096
39044
|
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
37097
39045
|
if (vmUser && vmPass) {
|
|
@@ -37817,13 +39765,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
|
|
|
37817
39765
|
});
|
|
37818
39766
|
spinner.stop();
|
|
37819
39767
|
if (opts.output) {
|
|
37820
|
-
const dir =
|
|
37821
|
-
|
|
39768
|
+
const dir = path9.resolve(opts.output);
|
|
39769
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
37822
39770
|
for (const f of result) {
|
|
37823
|
-
const safeName =
|
|
37824
|
-
const filePath =
|
|
39771
|
+
const safeName = path9.basename(f.filename);
|
|
39772
|
+
const filePath = path9.join(dir, safeName);
|
|
37825
39773
|
const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
|
|
37826
|
-
|
|
39774
|
+
fs11.writeFileSync(filePath, content, "utf-8");
|
|
37827
39775
|
console.log(filePath);
|
|
37828
39776
|
}
|
|
37829
39777
|
} else if (opts.json) {
|
|
@@ -37868,6 +39816,419 @@ function tryParseJson(s) {
|
|
|
37868
39816
|
return s;
|
|
37869
39817
|
}
|
|
37870
39818
|
}
|
|
39819
|
+
function inferCommandFromResult(r) {
|
|
39820
|
+
if (r.command)
|
|
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;
|
|
39838
|
+
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
39839
|
+
if (outcome.budgetExpired) {
|
|
39840
|
+
if (json3) {
|
|
39841
|
+
console.log(JSON.stringify({
|
|
39842
|
+
command_id: outcome.commandId,
|
|
39843
|
+
status: outcome.status,
|
|
39844
|
+
session_id: outcome.sessionId,
|
|
39845
|
+
budget_expired: true,
|
|
39846
|
+
resume: `pgai joe result ${outcome.commandId}`
|
|
39847
|
+
}, null, 2));
|
|
39848
|
+
} else {
|
|
39849
|
+
console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
|
|
39850
|
+
}
|
|
39851
|
+
return;
|
|
39852
|
+
}
|
|
39853
|
+
const result = outcome.result;
|
|
39854
|
+
if (outcome.status === "done" && result) {
|
|
39855
|
+
if (json3) {
|
|
39856
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39857
|
+
} else {
|
|
39858
|
+
console.log(`submitted ${outcome.commandId} \xB7 done`);
|
|
39859
|
+
const body = formatJoeResult(command, result);
|
|
39860
|
+
if (body)
|
|
39861
|
+
console.log(body);
|
|
39862
|
+
}
|
|
39863
|
+
return;
|
|
39864
|
+
}
|
|
39865
|
+
if (outcome.status === "error") {
|
|
39866
|
+
const msg = result?.error ?? "command failed";
|
|
39867
|
+
console.error(`command ${outcome.commandId} error: ${msg}`);
|
|
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}`);
|
|
39872
|
+
process.exitCode = 1;
|
|
39873
|
+
}
|
|
39874
|
+
async function runJoeCli(command, sql, args, opts) {
|
|
39875
|
+
try {
|
|
39876
|
+
const rootOpts = program2.opts();
|
|
39877
|
+
const cfg = readConfig();
|
|
39878
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39879
|
+
if (!apiKey) {
|
|
39880
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39881
|
+
process.exitCode = 1;
|
|
39882
|
+
return;
|
|
39883
|
+
}
|
|
39884
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
39885
|
+
if (!projectRef) {
|
|
39886
|
+
console.error("Project is required. Pass --project <id|alias>.");
|
|
39887
|
+
process.exitCode = 1;
|
|
39888
|
+
return;
|
|
39889
|
+
}
|
|
39890
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39891
|
+
const budgetMs = typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
|
|
39892
|
+
const outcome = await executeJoeCommand2({
|
|
39893
|
+
apiKey,
|
|
39894
|
+
apiBaseUrl,
|
|
39895
|
+
command,
|
|
39896
|
+
project: projectRef,
|
|
39897
|
+
sql,
|
|
39898
|
+
args,
|
|
39899
|
+
session: opts.session ?? null,
|
|
39900
|
+
newSession: !!opts.newSession,
|
|
39901
|
+
orgId: cfg.orgId ?? undefined,
|
|
39902
|
+
budgetMs,
|
|
39903
|
+
debug: !!opts.debug
|
|
39904
|
+
});
|
|
39905
|
+
printJoeOutcome(command, outcome, !!opts.json, budgetMs);
|
|
39906
|
+
} catch (err) {
|
|
39907
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39908
|
+
console.error(message);
|
|
39909
|
+
process.exitCode = 1;
|
|
39910
|
+
}
|
|
39911
|
+
}
|
|
39912
|
+
function withJoeOptions(cmd) {
|
|
39913
|
+
return cmd.option("--project <id|alias>", "target project by numeric id OR alias/name").option("--session <id>", "run on a specific session's clone").option("--new-session", "start a fresh session/clone (null session)").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseInt(v, 10)).option("--debug", "enable debug output").option("--json", "output raw JSON");
|
|
39914
|
+
}
|
|
39915
|
+
var joe = program2.command("joe").description("Joe API v2 \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones");
|
|
39916
|
+
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, null, opts);
|
|
39918
|
+
});
|
|
39919
|
+
withJoeOptions(joe.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
|
|
39920
|
+
await runJoeCli("explain", sql, null, opts);
|
|
39921
|
+
});
|
|
39922
|
+
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, null, opts);
|
|
39924
|
+
});
|
|
39925
|
+
withJoeOptions(joe.command("hypo <indexSql>").description("HypoPG hypothetical index + re-plan (no real index, no data change)").requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")).action(async (indexSql, opts) => {
|
|
39926
|
+
await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
|
|
39927
|
+
});
|
|
39928
|
+
withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
|
|
39929
|
+
await runJoeCli("activity", null, null, opts);
|
|
39930
|
+
});
|
|
39931
|
+
withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
39932
|
+
const pidStr = (pid ?? "").trim();
|
|
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);
|
|
39939
|
+
});
|
|
39940
|
+
withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
39941
|
+
await runJoeCli("reset", null, null, opts);
|
|
39942
|
+
});
|
|
39943
|
+
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
|
+
const args = { object: object4 };
|
|
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;
|
|
39952
|
+
});
|
|
39953
|
+
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) => {
|
|
39954
|
+
try {
|
|
39955
|
+
const rootOpts = program2.opts();
|
|
39956
|
+
const cfg = readConfig();
|
|
39957
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39958
|
+
if (!apiKey) {
|
|
39959
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39960
|
+
process.exitCode = 1;
|
|
39961
|
+
return;
|
|
39962
|
+
}
|
|
39963
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39964
|
+
const projects = await listProjects2({
|
|
39965
|
+
apiKey,
|
|
39966
|
+
apiBaseUrl,
|
|
39967
|
+
orgId: cfg.orgId ?? undefined,
|
|
39968
|
+
debug: !!opts.debug
|
|
39969
|
+
});
|
|
39970
|
+
if (opts.json) {
|
|
39971
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
39972
|
+
} else {
|
|
39973
|
+
console.log(formatProjectsTable(projects));
|
|
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;
|
|
40028
|
+
return;
|
|
40029
|
+
}
|
|
40030
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40031
|
+
const status = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
40032
|
+
if (opts.json) {
|
|
40033
|
+
console.log(JSON.stringify(status, null, 2));
|
|
40034
|
+
} else {
|
|
40035
|
+
console.log(`command ${status.command_id} \xB7 ${status.status}${status.error ? ` \xB7 ${status.error}` : ""}`);
|
|
40036
|
+
}
|
|
40037
|
+
} catch (err) {
|
|
40038
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40039
|
+
console.error(message);
|
|
40040
|
+
process.exitCode = 1;
|
|
40041
|
+
}
|
|
40042
|
+
});
|
|
40043
|
+
clone2.command("list").description("list the project's thin clones").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
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) => {
|
|
40054
|
+
try {
|
|
40055
|
+
const rootOpts = program2.opts();
|
|
40056
|
+
const cfg = readConfig();
|
|
40057
|
+
const { apiKey } = getConfig(rootOpts);
|
|
40058
|
+
if (!apiKey) {
|
|
40059
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
40060
|
+
process.exitCode = 1;
|
|
40061
|
+
return;
|
|
40062
|
+
}
|
|
40063
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
40064
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
40065
|
+
if (opts.json) {
|
|
40066
|
+
console.log(JSON.stringify(result, null, 2));
|
|
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;
|
|
40084
|
+
} else {
|
|
40085
|
+
console.log(`command ${result.command_id} \xB7 ${result.status}`);
|
|
40086
|
+
}
|
|
40087
|
+
} catch (err) {
|
|
40088
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40089
|
+
console.error(message);
|
|
40090
|
+
process.exitCode = 1;
|
|
40091
|
+
}
|
|
40092
|
+
});
|
|
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
|
+
});
|
|
37871
40232
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
37872
40233
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
37873
40234
|
const rootOpts = program2.opts();
|
|
@@ -37941,29 +40302,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
37941
40302
|
let configDir;
|
|
37942
40303
|
switch (client) {
|
|
37943
40304
|
case "cursor":
|
|
37944
|
-
configPath =
|
|
37945
|
-
configDir =
|
|
40305
|
+
configPath = path9.join(homeDir, ".cursor", "mcp.json");
|
|
40306
|
+
configDir = path9.dirname(configPath);
|
|
37946
40307
|
break;
|
|
37947
40308
|
case "windsurf":
|
|
37948
|
-
configPath =
|
|
37949
|
-
configDir =
|
|
40309
|
+
configPath = path9.join(homeDir, ".windsurf", "mcp.json");
|
|
40310
|
+
configDir = path9.dirname(configPath);
|
|
37950
40311
|
break;
|
|
37951
40312
|
case "codex":
|
|
37952
|
-
configPath =
|
|
37953
|
-
configDir =
|
|
40313
|
+
configPath = path9.join(homeDir, ".codex", "mcp.json");
|
|
40314
|
+
configDir = path9.dirname(configPath);
|
|
37954
40315
|
break;
|
|
37955
40316
|
default:
|
|
37956
40317
|
console.error(`Configuration not implemented for: ${client}`);
|
|
37957
40318
|
process.exitCode = 1;
|
|
37958
40319
|
return;
|
|
37959
40320
|
}
|
|
37960
|
-
if (!
|
|
37961
|
-
|
|
40321
|
+
if (!fs11.existsSync(configDir)) {
|
|
40322
|
+
fs11.mkdirSync(configDir, { recursive: true });
|
|
37962
40323
|
}
|
|
37963
40324
|
let config2 = { mcpServers: {} };
|
|
37964
|
-
if (
|
|
40325
|
+
if (fs11.existsSync(configPath)) {
|
|
37965
40326
|
try {
|
|
37966
|
-
const content =
|
|
40327
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
37967
40328
|
config2 = JSON.parse(content);
|
|
37968
40329
|
if (!config2.mcpServers) {
|
|
37969
40330
|
config2.mcpServers = {};
|
|
@@ -37976,7 +40337,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
37976
40337
|
command: pgaiPath,
|
|
37977
40338
|
args: ["mcp", "start"]
|
|
37978
40339
|
};
|
|
37979
|
-
|
|
40340
|
+
fs11.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
|
|
37980
40341
|
console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
|
|
37981
40342
|
console.log(` Config file: ${configPath}`);
|
|
37982
40343
|
console.log("");
|
|
@@ -37997,5 +40358,7 @@ export {
|
|
|
37997
40358
|
registerMonitoringInstance,
|
|
37998
40359
|
refreshBundledComposeIfStale,
|
|
37999
40360
|
readDeployedTag,
|
|
38000
|
-
|
|
40361
|
+
planMonitoringRegistration,
|
|
40362
|
+
isValidComposeYaml,
|
|
40363
|
+
inferCommandFromResult
|
|
38001
40364
|
};
|