postgresai 0.16.0-dev.1 → 0.16.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +728 -6
- package/dist/bin/postgres-ai.js +2496 -255
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +457 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +730 -0
- package/lib/mcp-server.ts +597 -0
- package/lib/supabase.ts +0 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +339 -0
- package/test/dblab.test.ts +408 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +298 -0
- package/test/joe.test.ts +726 -0
- package/test/monitoring.test.ts +54 -3
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 });
|
|
@@ -13425,7 +13425,7 @@ var {
|
|
|
13425
13425
|
// package.json
|
|
13426
13426
|
var package_default = {
|
|
13427
13427
|
name: "postgresai",
|
|
13428
|
-
version: "0.16.0-dev.
|
|
13428
|
+
version: "0.16.0-dev.3",
|
|
13429
13429
|
description: "postgres_ai CLI",
|
|
13430
13430
|
license: "Apache-2.0",
|
|
13431
13431
|
private: false,
|
|
@@ -16236,11 +16236,11 @@ var safeLoadAll = renamed("safeLoadAll", "loadAll");
|
|
|
16236
16236
|
var safeDump = renamed("safeDump", "dump");
|
|
16237
16237
|
|
|
16238
16238
|
// bin/postgres-ai.ts
|
|
16239
|
-
import * as
|
|
16240
|
-
import * as
|
|
16239
|
+
import * as fs11 from "fs";
|
|
16240
|
+
import * as path9 from "path";
|
|
16241
16241
|
import * as os3 from "os";
|
|
16242
16242
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16243
|
-
import * as
|
|
16243
|
+
import * as crypto4 from "crypto";
|
|
16244
16244
|
|
|
16245
16245
|
// node_modules/pg/esm/index.mjs
|
|
16246
16246
|
var import_lib = __toESM(require_lib2(), 1);
|
|
@@ -16256,7 +16256,7 @@ var Result = import_lib.default.Result;
|
|
|
16256
16256
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16257
16257
|
var defaults = import_lib.default.defaults;
|
|
16258
16258
|
// package.json
|
|
16259
|
-
var version = "0.16.0-dev.
|
|
16259
|
+
var version = "0.16.0-dev.3";
|
|
16260
16260
|
var package_default2 = {
|
|
16261
16261
|
name: "postgresai",
|
|
16262
16262
|
version,
|
|
@@ -17267,6 +17267,305 @@ async function fetchReportFileData(params) {
|
|
|
17267
17267
|
}
|
|
17268
17268
|
}
|
|
17269
17269
|
|
|
17270
|
+
// lib/dblab.ts
|
|
17271
|
+
function isNumericProjectRef(ref) {
|
|
17272
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
17273
|
+
}
|
|
17274
|
+
async function resolveDblabInstanceId(params) {
|
|
17275
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17276
|
+
if (!apiKey) {
|
|
17277
|
+
throw new Error("API key is required");
|
|
17278
|
+
}
|
|
17279
|
+
const ref = String(params.project ?? "").trim();
|
|
17280
|
+
if (!ref) {
|
|
17281
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
17282
|
+
}
|
|
17283
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17284
|
+
const url = new URL(`${base}/dblab_instances`);
|
|
17285
|
+
url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
|
|
17286
|
+
if (typeof orgId === "number") {
|
|
17287
|
+
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
17288
|
+
}
|
|
17289
|
+
const headers = {
|
|
17290
|
+
"access-token": apiKey,
|
|
17291
|
+
"Content-Type": "application/json",
|
|
17292
|
+
Connection: "close"
|
|
17293
|
+
};
|
|
17294
|
+
if (debug) {
|
|
17295
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17296
|
+
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
17297
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17298
|
+
}
|
|
17299
|
+
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
17300
|
+
const text = await response.text();
|
|
17301
|
+
if (debug) {
|
|
17302
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
17303
|
+
console.error(`Debug: Response body: ${text}`);
|
|
17304
|
+
}
|
|
17305
|
+
if (!response.ok) {
|
|
17306
|
+
throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
|
|
17307
|
+
}
|
|
17308
|
+
let rows;
|
|
17309
|
+
try {
|
|
17310
|
+
const parsed = JSON.parse(text);
|
|
17311
|
+
rows = Array.isArray(parsed) ? parsed : [];
|
|
17312
|
+
} catch {
|
|
17313
|
+
throw new Error(`Failed to parse DBLab instances response: ${text}`);
|
|
17314
|
+
}
|
|
17315
|
+
const numeric = isNumericProjectRef(ref);
|
|
17316
|
+
const needle = ref.toLowerCase();
|
|
17317
|
+
const match = rows.find((r) => {
|
|
17318
|
+
if (numeric) {
|
|
17319
|
+
return String(r.project_id ?? "") === ref;
|
|
17320
|
+
}
|
|
17321
|
+
return r.project_alias != null && String(r.project_alias).toLowerCase() === needle || r.project_name != null && String(r.project_name).toLowerCase() === needle || r.project_label_or_name != null && String(r.project_label_or_name).toLowerCase() === needle;
|
|
17322
|
+
});
|
|
17323
|
+
if (!match) {
|
|
17324
|
+
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17325
|
+
}
|
|
17326
|
+
return String(match.id);
|
|
17327
|
+
}
|
|
17328
|
+
async function callDblabApi(params) {
|
|
17329
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
17330
|
+
if (!apiKey) {
|
|
17331
|
+
throw new Error("API key is required");
|
|
17332
|
+
}
|
|
17333
|
+
if (!instanceId) {
|
|
17334
|
+
throw new Error("instanceId is required");
|
|
17335
|
+
}
|
|
17336
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17337
|
+
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
17338
|
+
const bodyObj = {
|
|
17339
|
+
instance_id: instanceId,
|
|
17340
|
+
action,
|
|
17341
|
+
method
|
|
17342
|
+
};
|
|
17343
|
+
if (data !== undefined) {
|
|
17344
|
+
bodyObj.data = data;
|
|
17345
|
+
}
|
|
17346
|
+
const body = JSON.stringify(bodyObj);
|
|
17347
|
+
const headers = {
|
|
17348
|
+
"access-token": apiKey,
|
|
17349
|
+
"Content-Type": "application/json",
|
|
17350
|
+
Connection: "close"
|
|
17351
|
+
};
|
|
17352
|
+
if (debug) {
|
|
17353
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17354
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17355
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17356
|
+
console.error(`Debug: Request body: ${body}`);
|
|
17357
|
+
}
|
|
17358
|
+
const response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
17359
|
+
const text = await response.text();
|
|
17360
|
+
if (debug) {
|
|
17361
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
17362
|
+
console.error(`Debug: Response body: ${text}`);
|
|
17363
|
+
}
|
|
17364
|
+
if (!response.ok) {
|
|
17365
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
17366
|
+
}
|
|
17367
|
+
if (text.trim() === "") {
|
|
17368
|
+
return null;
|
|
17369
|
+
}
|
|
17370
|
+
try {
|
|
17371
|
+
return JSON.parse(text);
|
|
17372
|
+
} catch {
|
|
17373
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17374
|
+
}
|
|
17375
|
+
}
|
|
17376
|
+
async function createClone(params) {
|
|
17377
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
17378
|
+
const data = { protected: Boolean(isProtected) };
|
|
17379
|
+
if (cloneId)
|
|
17380
|
+
data.id = cloneId;
|
|
17381
|
+
if (branch)
|
|
17382
|
+
data.branch = branch;
|
|
17383
|
+
if (snapshotId)
|
|
17384
|
+
data.snapshot = { id: snapshotId };
|
|
17385
|
+
if (dbUser && dbPassword)
|
|
17386
|
+
data.db = { username: dbUser, password: dbPassword };
|
|
17387
|
+
return callDblabApi({
|
|
17388
|
+
apiKey,
|
|
17389
|
+
apiBaseUrl,
|
|
17390
|
+
instanceId,
|
|
17391
|
+
action: "/clone",
|
|
17392
|
+
method: "post",
|
|
17393
|
+
data,
|
|
17394
|
+
operation: "Failed to create clone",
|
|
17395
|
+
debug
|
|
17396
|
+
});
|
|
17397
|
+
}
|
|
17398
|
+
async function listClones(params) {
|
|
17399
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17400
|
+
return callDblabApi({
|
|
17401
|
+
apiKey,
|
|
17402
|
+
apiBaseUrl,
|
|
17403
|
+
instanceId,
|
|
17404
|
+
action: "/clones",
|
|
17405
|
+
method: "get",
|
|
17406
|
+
operation: "Failed to list clones",
|
|
17407
|
+
debug
|
|
17408
|
+
});
|
|
17409
|
+
}
|
|
17410
|
+
async function getClone(params) {
|
|
17411
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17412
|
+
if (!cloneId)
|
|
17413
|
+
throw new Error("cloneId is required");
|
|
17414
|
+
return callDblabApi({
|
|
17415
|
+
apiKey,
|
|
17416
|
+
apiBaseUrl,
|
|
17417
|
+
instanceId,
|
|
17418
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17419
|
+
method: "get",
|
|
17420
|
+
operation: "Failed to get clone",
|
|
17421
|
+
debug
|
|
17422
|
+
});
|
|
17423
|
+
}
|
|
17424
|
+
async function resetClone(params) {
|
|
17425
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
17426
|
+
if (!cloneId)
|
|
17427
|
+
throw new Error("cloneId is required");
|
|
17428
|
+
const data = { latest: latest ?? !snapshotId };
|
|
17429
|
+
if (snapshotId)
|
|
17430
|
+
data.snapshotID = snapshotId;
|
|
17431
|
+
return callDblabApi({
|
|
17432
|
+
apiKey,
|
|
17433
|
+
apiBaseUrl,
|
|
17434
|
+
instanceId,
|
|
17435
|
+
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
17436
|
+
method: "post",
|
|
17437
|
+
data,
|
|
17438
|
+
operation: "Failed to reset clone",
|
|
17439
|
+
debug
|
|
17440
|
+
});
|
|
17441
|
+
}
|
|
17442
|
+
async function destroyClone(params) {
|
|
17443
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17444
|
+
if (!cloneId)
|
|
17445
|
+
throw new Error("cloneId is required");
|
|
17446
|
+
return callDblabApi({
|
|
17447
|
+
apiKey,
|
|
17448
|
+
apiBaseUrl,
|
|
17449
|
+
instanceId,
|
|
17450
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17451
|
+
method: "delete",
|
|
17452
|
+
operation: "Failed to destroy clone",
|
|
17453
|
+
debug
|
|
17454
|
+
});
|
|
17455
|
+
}
|
|
17456
|
+
async function listBranches(params) {
|
|
17457
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17458
|
+
return callDblabApi({
|
|
17459
|
+
apiKey,
|
|
17460
|
+
apiBaseUrl,
|
|
17461
|
+
instanceId,
|
|
17462
|
+
action: "/branches",
|
|
17463
|
+
method: "get",
|
|
17464
|
+
operation: "Failed to list branches",
|
|
17465
|
+
debug
|
|
17466
|
+
});
|
|
17467
|
+
}
|
|
17468
|
+
async function createBranch(params) {
|
|
17469
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
17470
|
+
if (!branchName)
|
|
17471
|
+
throw new Error("branchName is required");
|
|
17472
|
+
const data = { branchName };
|
|
17473
|
+
if (baseBranch)
|
|
17474
|
+
data.baseBranch = baseBranch;
|
|
17475
|
+
if (snapshotId)
|
|
17476
|
+
data.snapshotID = snapshotId;
|
|
17477
|
+
return callDblabApi({
|
|
17478
|
+
apiKey,
|
|
17479
|
+
apiBaseUrl,
|
|
17480
|
+
instanceId,
|
|
17481
|
+
action: "/branch",
|
|
17482
|
+
method: "post",
|
|
17483
|
+
data,
|
|
17484
|
+
operation: "Failed to create branch",
|
|
17485
|
+
debug
|
|
17486
|
+
});
|
|
17487
|
+
}
|
|
17488
|
+
async function deleteBranch(params) {
|
|
17489
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17490
|
+
if (!branchName)
|
|
17491
|
+
throw new Error("branchName is required");
|
|
17492
|
+
return callDblabApi({
|
|
17493
|
+
apiKey,
|
|
17494
|
+
apiBaseUrl,
|
|
17495
|
+
instanceId,
|
|
17496
|
+
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
17497
|
+
method: "delete",
|
|
17498
|
+
operation: "Failed to delete branch",
|
|
17499
|
+
debug
|
|
17500
|
+
});
|
|
17501
|
+
}
|
|
17502
|
+
async function branchLog(params) {
|
|
17503
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17504
|
+
if (!branchName)
|
|
17505
|
+
throw new Error("branchName is required");
|
|
17506
|
+
return callDblabApi({
|
|
17507
|
+
apiKey,
|
|
17508
|
+
apiBaseUrl,
|
|
17509
|
+
instanceId,
|
|
17510
|
+
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
17511
|
+
method: "get",
|
|
17512
|
+
operation: "Failed to fetch branch log",
|
|
17513
|
+
debug
|
|
17514
|
+
});
|
|
17515
|
+
}
|
|
17516
|
+
async function listSnapshots(params) {
|
|
17517
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
17518
|
+
const qs = new URLSearchParams;
|
|
17519
|
+
const branch = branchName?.trim();
|
|
17520
|
+
if (branch)
|
|
17521
|
+
qs.append("branch", branch);
|
|
17522
|
+
if (dataset)
|
|
17523
|
+
qs.append("dataset", dataset);
|
|
17524
|
+
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
17525
|
+
return callDblabApi({
|
|
17526
|
+
apiKey,
|
|
17527
|
+
apiBaseUrl,
|
|
17528
|
+
instanceId,
|
|
17529
|
+
action,
|
|
17530
|
+
method: "get",
|
|
17531
|
+
operation: "Failed to list snapshots",
|
|
17532
|
+
debug
|
|
17533
|
+
});
|
|
17534
|
+
}
|
|
17535
|
+
async function createSnapshot(params) {
|
|
17536
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
17537
|
+
if (!cloneId)
|
|
17538
|
+
throw new Error("cloneId is required");
|
|
17539
|
+
const data = { cloneID: cloneId };
|
|
17540
|
+
if (message)
|
|
17541
|
+
data.message = message;
|
|
17542
|
+
return callDblabApi({
|
|
17543
|
+
apiKey,
|
|
17544
|
+
apiBaseUrl,
|
|
17545
|
+
instanceId,
|
|
17546
|
+
action: "/branch/snapshot",
|
|
17547
|
+
method: "post",
|
|
17548
|
+
data,
|
|
17549
|
+
operation: "Failed to create snapshot",
|
|
17550
|
+
debug
|
|
17551
|
+
});
|
|
17552
|
+
}
|
|
17553
|
+
async function destroySnapshot(params) {
|
|
17554
|
+
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
17555
|
+
if (!snapshotId)
|
|
17556
|
+
throw new Error("snapshotId is required");
|
|
17557
|
+
const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
|
|
17558
|
+
return callDblabApi({
|
|
17559
|
+
apiKey,
|
|
17560
|
+
apiBaseUrl,
|
|
17561
|
+
instanceId,
|
|
17562
|
+
action,
|
|
17563
|
+
method: "delete",
|
|
17564
|
+
operation: "Failed to destroy snapshot",
|
|
17565
|
+
debug
|
|
17566
|
+
});
|
|
17567
|
+
}
|
|
17568
|
+
|
|
17270
17569
|
// lib/storage.ts
|
|
17271
17570
|
import * as fs3 from "fs";
|
|
17272
17571
|
import * as path3 from "path";
|
|
@@ -17488,6 +17787,319 @@ function appendAttachmentsToContent(content, attachments) {
|
|
|
17488
17787
|
${links}`;
|
|
17489
17788
|
}
|
|
17490
17789
|
|
|
17790
|
+
// lib/joe.ts
|
|
17791
|
+
import * as fs4 from "fs";
|
|
17792
|
+
import * as path4 from "path";
|
|
17793
|
+
import * as crypto from "node:crypto";
|
|
17794
|
+
var JOE_COMMANDS = [
|
|
17795
|
+
"plan",
|
|
17796
|
+
"explain",
|
|
17797
|
+
"exec",
|
|
17798
|
+
"hypo",
|
|
17799
|
+
"activity",
|
|
17800
|
+
"terminate",
|
|
17801
|
+
"reset",
|
|
17802
|
+
"describe"
|
|
17803
|
+
];
|
|
17804
|
+
var TERMINAL_STATES = new Set([
|
|
17805
|
+
"done",
|
|
17806
|
+
"error",
|
|
17807
|
+
"timed_out"
|
|
17808
|
+
]);
|
|
17809
|
+
var AI_ENABLED_COMMANDS = new Set([
|
|
17810
|
+
"plan",
|
|
17811
|
+
"explain",
|
|
17812
|
+
"exec",
|
|
17813
|
+
"hypo",
|
|
17814
|
+
"activity",
|
|
17815
|
+
"describe"
|
|
17816
|
+
]);
|
|
17817
|
+
var EXECUTING_COMMANDS = new Set([
|
|
17818
|
+
"exec",
|
|
17819
|
+
"explain"
|
|
17820
|
+
]);
|
|
17821
|
+
var DEFAULT_BUDGET_MS = 25000;
|
|
17822
|
+
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
17823
|
+
async function callRpc(params) {
|
|
17824
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
17825
|
+
if (!apiKey) {
|
|
17826
|
+
throw new Error("API key is required");
|
|
17827
|
+
}
|
|
17828
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17829
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
17830
|
+
const payload = JSON.stringify(body);
|
|
17831
|
+
const headers = {
|
|
17832
|
+
"access-token": apiKey,
|
|
17833
|
+
"Content-Type": "application/json",
|
|
17834
|
+
Connection: "close"
|
|
17835
|
+
};
|
|
17836
|
+
if (debug) {
|
|
17837
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17838
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17839
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17840
|
+
console.error(`Debug: Request body: ${payload}`);
|
|
17841
|
+
}
|
|
17842
|
+
const response = await fetch(url.toString(), {
|
|
17843
|
+
method: "POST",
|
|
17844
|
+
headers,
|
|
17845
|
+
body: payload
|
|
17846
|
+
});
|
|
17847
|
+
const text = await response.text();
|
|
17848
|
+
if (debug) {
|
|
17849
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
17850
|
+
console.error(`Debug: Response body: ${text}`);
|
|
17851
|
+
}
|
|
17852
|
+
if (!response.ok) {
|
|
17853
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
17854
|
+
}
|
|
17855
|
+
try {
|
|
17856
|
+
return JSON.parse(text);
|
|
17857
|
+
} catch {
|
|
17858
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17859
|
+
}
|
|
17860
|
+
}
|
|
17861
|
+
async function submitCommand(params) {
|
|
17862
|
+
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
17863
|
+
if (!JOE_COMMANDS.includes(command)) {
|
|
17864
|
+
throw new Error(`Unknown Joe command: ${command}`);
|
|
17865
|
+
}
|
|
17866
|
+
const body = {
|
|
17867
|
+
project_id: projectId,
|
|
17868
|
+
command,
|
|
17869
|
+
sql: sql ?? null,
|
|
17870
|
+
args: args ?? null,
|
|
17871
|
+
session_id: sessionId ?? null
|
|
17872
|
+
};
|
|
17873
|
+
if (idempotencyKey) {
|
|
17874
|
+
body.idempotency_key = idempotencyKey;
|
|
17875
|
+
}
|
|
17876
|
+
return callRpc({
|
|
17877
|
+
apiKey,
|
|
17878
|
+
apiBaseUrl,
|
|
17879
|
+
fn: "joe_command_submit",
|
|
17880
|
+
body,
|
|
17881
|
+
operation: `Failed to submit ${command} command`,
|
|
17882
|
+
debug
|
|
17883
|
+
});
|
|
17884
|
+
}
|
|
17885
|
+
async function getCommandStatus(params) {
|
|
17886
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17887
|
+
if (!commandId) {
|
|
17888
|
+
throw new Error("commandId is required");
|
|
17889
|
+
}
|
|
17890
|
+
return callRpc({
|
|
17891
|
+
apiKey,
|
|
17892
|
+
apiBaseUrl,
|
|
17893
|
+
fn: "joe_command_status",
|
|
17894
|
+
body: { command_id: commandId },
|
|
17895
|
+
operation: "Failed to fetch command status",
|
|
17896
|
+
debug
|
|
17897
|
+
});
|
|
17898
|
+
}
|
|
17899
|
+
async function getCommandResult(params) {
|
|
17900
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17901
|
+
if (!commandId) {
|
|
17902
|
+
throw new Error("commandId is required");
|
|
17903
|
+
}
|
|
17904
|
+
return callRpc({
|
|
17905
|
+
apiKey,
|
|
17906
|
+
apiBaseUrl,
|
|
17907
|
+
fn: "joe_command_result",
|
|
17908
|
+
body: { command_id: commandId },
|
|
17909
|
+
operation: "Failed to fetch command result",
|
|
17910
|
+
debug
|
|
17911
|
+
});
|
|
17912
|
+
}
|
|
17913
|
+
function normalizeProjectRow(row) {
|
|
17914
|
+
const projectId = row.project_id ?? row.id ?? 0;
|
|
17915
|
+
return {
|
|
17916
|
+
project_id: Number(projectId),
|
|
17917
|
+
alias: row.alias ?? null,
|
|
17918
|
+
name: row.name ?? null,
|
|
17919
|
+
label: row.label ?? null,
|
|
17920
|
+
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
17921
|
+
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false)
|
|
17922
|
+
};
|
|
17923
|
+
}
|
|
17924
|
+
async function listProjects(params) {
|
|
17925
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17926
|
+
const body = {};
|
|
17927
|
+
if (typeof orgId === "number") {
|
|
17928
|
+
body.org_id = orgId;
|
|
17929
|
+
}
|
|
17930
|
+
const rows = await callRpc({
|
|
17931
|
+
apiKey,
|
|
17932
|
+
apiBaseUrl,
|
|
17933
|
+
fn: "projects_list",
|
|
17934
|
+
body,
|
|
17935
|
+
operation: "Failed to list projects",
|
|
17936
|
+
debug
|
|
17937
|
+
});
|
|
17938
|
+
if (!Array.isArray(rows)) {
|
|
17939
|
+
return [];
|
|
17940
|
+
}
|
|
17941
|
+
return rows.map(normalizeProjectRow);
|
|
17942
|
+
}
|
|
17943
|
+
function isNumericProjectRef2(ref) {
|
|
17944
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
17945
|
+
}
|
|
17946
|
+
async function resolveProjectId(params) {
|
|
17947
|
+
const ref = String(params.project ?? "").trim();
|
|
17948
|
+
if (!ref) {
|
|
17949
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
17950
|
+
}
|
|
17951
|
+
if (isNumericProjectRef2(ref)) {
|
|
17952
|
+
return Number(ref);
|
|
17953
|
+
}
|
|
17954
|
+
const projects = await listProjects({
|
|
17955
|
+
apiKey: params.apiKey,
|
|
17956
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
17957
|
+
orgId: params.orgId,
|
|
17958
|
+
debug: params.debug
|
|
17959
|
+
});
|
|
17960
|
+
const needle = ref.toLowerCase();
|
|
17961
|
+
const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
17962
|
+
if (!match) {
|
|
17963
|
+
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17964
|
+
}
|
|
17965
|
+
return match.project_id;
|
|
17966
|
+
}
|
|
17967
|
+
function sessionStorePath(dir) {
|
|
17968
|
+
return path4.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
17969
|
+
}
|
|
17970
|
+
function readSessionStore(dir) {
|
|
17971
|
+
const file = sessionStorePath(dir);
|
|
17972
|
+
if (!fs4.existsSync(file)) {
|
|
17973
|
+
return {};
|
|
17974
|
+
}
|
|
17975
|
+
try {
|
|
17976
|
+
const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
17977
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17978
|
+
return parsed;
|
|
17979
|
+
}
|
|
17980
|
+
} catch {}
|
|
17981
|
+
return {};
|
|
17982
|
+
}
|
|
17983
|
+
function readStoredSessionId(projectId, dir) {
|
|
17984
|
+
const store = readSessionStore(dir);
|
|
17985
|
+
const value = store[String(projectId)];
|
|
17986
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
17987
|
+
}
|
|
17988
|
+
function writeStoredSessionId(projectId, sessionId, dir) {
|
|
17989
|
+
const baseDir = dir ?? getConfigDir2();
|
|
17990
|
+
if (!fs4.existsSync(baseDir)) {
|
|
17991
|
+
fs4.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
17992
|
+
}
|
|
17993
|
+
const store = readSessionStore(dir);
|
|
17994
|
+
store[String(projectId)] = sessionId;
|
|
17995
|
+
fs4.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
|
|
17996
|
+
`, { mode: 384 });
|
|
17997
|
+
}
|
|
17998
|
+
function clearStoredSessionId(projectId, dir) {
|
|
17999
|
+
const file = sessionStorePath(dir);
|
|
18000
|
+
if (!fs4.existsSync(file)) {
|
|
18001
|
+
return;
|
|
18002
|
+
}
|
|
18003
|
+
const store = readSessionStore(dir);
|
|
18004
|
+
if (String(projectId) in store) {
|
|
18005
|
+
delete store[String(projectId)];
|
|
18006
|
+
fs4.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
18007
|
+
`, { mode: 384 });
|
|
18008
|
+
}
|
|
18009
|
+
}
|
|
18010
|
+
function computeIdempotencyKey(command, projectId, sql, args) {
|
|
18011
|
+
if (EXECUTING_COMMANDS.has(command)) {
|
|
18012
|
+
const canonical = `${projectId}
|
|
18013
|
+
${command}
|
|
18014
|
+
${sql ?? ""}
|
|
18015
|
+
${JSON.stringify(args ?? null)}`;
|
|
18016
|
+
return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
|
|
18017
|
+
}
|
|
18018
|
+
return `joe-${crypto.randomUUID()}`;
|
|
18019
|
+
}
|
|
18020
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
18021
|
+
async function runCommand(params) {
|
|
18022
|
+
const {
|
|
18023
|
+
apiKey,
|
|
18024
|
+
apiBaseUrl,
|
|
18025
|
+
command,
|
|
18026
|
+
projectId,
|
|
18027
|
+
sql,
|
|
18028
|
+
args,
|
|
18029
|
+
sessionId,
|
|
18030
|
+
debug
|
|
18031
|
+
} = params;
|
|
18032
|
+
const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
18033
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
18034
|
+
const now = params.now ?? Date.now;
|
|
18035
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
18036
|
+
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
|
|
18037
|
+
const submitted = await submitCommand({
|
|
18038
|
+
apiKey,
|
|
18039
|
+
apiBaseUrl,
|
|
18040
|
+
command,
|
|
18041
|
+
projectId,
|
|
18042
|
+
sql,
|
|
18043
|
+
args,
|
|
18044
|
+
sessionId,
|
|
18045
|
+
idempotencyKey,
|
|
18046
|
+
debug
|
|
18047
|
+
});
|
|
18048
|
+
const commandId = submitted.command_id;
|
|
18049
|
+
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
18050
|
+
const deadline = now() + budgetMs;
|
|
18051
|
+
let status = submitted.status;
|
|
18052
|
+
while (true) {
|
|
18053
|
+
const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
|
|
18054
|
+
status = statusResp.status;
|
|
18055
|
+
if (TERMINAL_STATES.has(status)) {
|
|
18056
|
+
break;
|
|
18057
|
+
}
|
|
18058
|
+
if (now() >= deadline) {
|
|
18059
|
+
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
18060
|
+
}
|
|
18061
|
+
await sleep(pollIntervalMs);
|
|
18062
|
+
}
|
|
18063
|
+
const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
|
|
18064
|
+
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
18065
|
+
}
|
|
18066
|
+
async function executeJoeCommand(params) {
|
|
18067
|
+
const projectId = await resolveProjectId({
|
|
18068
|
+
apiKey: params.apiKey,
|
|
18069
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
18070
|
+
project: params.project,
|
|
18071
|
+
orgId: params.orgId,
|
|
18072
|
+
debug: params.debug
|
|
18073
|
+
});
|
|
18074
|
+
let sessionId;
|
|
18075
|
+
if (params.newSession) {
|
|
18076
|
+
clearStoredSessionId(projectId, params.sessionDir);
|
|
18077
|
+
sessionId = null;
|
|
18078
|
+
} else if (params.session) {
|
|
18079
|
+
sessionId = String(params.session);
|
|
18080
|
+
} else {
|
|
18081
|
+
sessionId = readStoredSessionId(projectId, params.sessionDir);
|
|
18082
|
+
}
|
|
18083
|
+
const outcome = await runCommand({
|
|
18084
|
+
apiKey: params.apiKey,
|
|
18085
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
18086
|
+
command: params.command,
|
|
18087
|
+
projectId,
|
|
18088
|
+
sql: params.sql,
|
|
18089
|
+
args: params.args,
|
|
18090
|
+
sessionId,
|
|
18091
|
+
budgetMs: params.budgetMs,
|
|
18092
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
18093
|
+
debug: params.debug,
|
|
18094
|
+
now: params.now,
|
|
18095
|
+
sleep: params.sleep
|
|
18096
|
+
});
|
|
18097
|
+
if (outcome.sessionId) {
|
|
18098
|
+
writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
|
|
18099
|
+
}
|
|
18100
|
+
return { ...outcome, command: params.command, projectId };
|
|
18101
|
+
}
|
|
18102
|
+
|
|
17491
18103
|
// node_modules/zod/v4/core/core.js
|
|
17492
18104
|
var NEVER = Object.freeze({
|
|
17493
18105
|
status: "aborted"
|
|
@@ -17733,10 +18345,10 @@ function mergeDefs(...defs) {
|
|
|
17733
18345
|
function cloneDef(schema2) {
|
|
17734
18346
|
return mergeDefs(schema2._zod.def);
|
|
17735
18347
|
}
|
|
17736
|
-
function getElementAtPath(obj,
|
|
17737
|
-
if (!
|
|
18348
|
+
function getElementAtPath(obj, path5) {
|
|
18349
|
+
if (!path5)
|
|
17738
18350
|
return obj;
|
|
17739
|
-
return
|
|
18351
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
17740
18352
|
}
|
|
17741
18353
|
function promiseAllObject(promisesObj) {
|
|
17742
18354
|
const keys = Object.keys(promisesObj);
|
|
@@ -18100,11 +18712,11 @@ function aborted(x, startIndex = 0) {
|
|
|
18100
18712
|
}
|
|
18101
18713
|
return false;
|
|
18102
18714
|
}
|
|
18103
|
-
function prefixIssues(
|
|
18715
|
+
function prefixIssues(path5, issues) {
|
|
18104
18716
|
return issues.map((iss) => {
|
|
18105
18717
|
var _a;
|
|
18106
18718
|
(_a = iss).path ?? (_a.path = []);
|
|
18107
|
-
iss.path.unshift(
|
|
18719
|
+
iss.path.unshift(path5);
|
|
18108
18720
|
return iss;
|
|
18109
18721
|
});
|
|
18110
18722
|
}
|
|
@@ -24376,44 +24988,238 @@ class StdioServerTransport {
|
|
|
24376
24988
|
// lib/mcp-server.ts
|
|
24377
24989
|
var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
24378
24990
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
24379
|
-
|
|
24380
|
-
|
|
24381
|
-
|
|
24382
|
-
|
|
24383
|
-
|
|
24384
|
-
|
|
24385
|
-
|
|
24386
|
-
|
|
24387
|
-
|
|
24388
|
-
|
|
24389
|
-
|
|
24390
|
-
|
|
24391
|
-
|
|
24392
|
-
|
|
24393
|
-
|
|
24394
|
-
|
|
24395
|
-
|
|
24396
|
-
|
|
24397
|
-
|
|
24398
|
-
|
|
24399
|
-
|
|
24400
|
-
|
|
24401
|
-
|
|
24402
|
-
|
|
24403
|
-
|
|
24404
|
-
|
|
24405
|
-
|
|
24406
|
-
|
|
24407
|
-
|
|
24408
|
-
|
|
24409
|
-
|
|
24410
|
-
|
|
24411
|
-
|
|
24412
|
-
const issueId = String(args.issue_id || "").trim();
|
|
24413
|
-
if (!issueId) {
|
|
24414
|
-
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
24991
|
+
var JOE_TOOL_TO_COMMAND = {
|
|
24992
|
+
plan_query: "plan",
|
|
24993
|
+
explain_query: "explain",
|
|
24994
|
+
exec_sql: "exec",
|
|
24995
|
+
hypo_index: "hypo",
|
|
24996
|
+
activity: "activity",
|
|
24997
|
+
terminate_backend: "terminate",
|
|
24998
|
+
reset_clone: "reset",
|
|
24999
|
+
describe: "describe"
|
|
25000
|
+
};
|
|
25001
|
+
function joeToolDefinitions() {
|
|
25002
|
+
const sessionProps = {
|
|
25003
|
+
project_id: {
|
|
25004
|
+
type: "string",
|
|
25005
|
+
description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db."
|
|
25006
|
+
},
|
|
25007
|
+
session_id: {
|
|
25008
|
+
type: "string",
|
|
25009
|
+
description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted."
|
|
25010
|
+
},
|
|
25011
|
+
new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
|
|
25012
|
+
timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
|
|
25013
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
25014
|
+
};
|
|
25015
|
+
return [
|
|
25016
|
+
{
|
|
25017
|
+
name: "plan_query",
|
|
25018
|
+
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.",
|
|
25019
|
+
inputSchema: {
|
|
25020
|
+
type: "object",
|
|
25021
|
+
properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
|
|
25022
|
+
required: ["sql", "project_id"],
|
|
25023
|
+
additionalProperties: false
|
|
24415
25024
|
}
|
|
24416
|
-
|
|
25025
|
+
},
|
|
25026
|
+
{
|
|
25027
|
+
name: "explain_query",
|
|
25028
|
+
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).",
|
|
25029
|
+
inputSchema: {
|
|
25030
|
+
type: "object",
|
|
25031
|
+
properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
|
|
25032
|
+
required: ["sql", "project_id"],
|
|
25033
|
+
additionalProperties: false
|
|
25034
|
+
}
|
|
25035
|
+
},
|
|
25036
|
+
{
|
|
25037
|
+
name: "exec_sql",
|
|
25038
|
+
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.",
|
|
25039
|
+
inputSchema: {
|
|
25040
|
+
type: "object",
|
|
25041
|
+
properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
|
|
25042
|
+
required: ["sql", "project_id"],
|
|
25043
|
+
additionalProperties: false
|
|
25044
|
+
}
|
|
25045
|
+
},
|
|
25046
|
+
{
|
|
25047
|
+
name: "hypo_index",
|
|
25048
|
+
description: "HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
|
|
25049
|
+
inputSchema: {
|
|
25050
|
+
type: "object",
|
|
25051
|
+
properties: {
|
|
25052
|
+
sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
|
|
25053
|
+
query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
|
|
25054
|
+
...sessionProps
|
|
25055
|
+
},
|
|
25056
|
+
required: ["sql", "query", "project_id"],
|
|
25057
|
+
additionalProperties: false
|
|
25058
|
+
}
|
|
25059
|
+
},
|
|
25060
|
+
{
|
|
25061
|
+
name: "activity",
|
|
25062
|
+
description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
|
|
25063
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25064
|
+
},
|
|
25065
|
+
{
|
|
25066
|
+
name: "terminate_backend",
|
|
25067
|
+
description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25068
|
+
inputSchema: {
|
|
25069
|
+
type: "object",
|
|
25070
|
+
properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
|
|
25071
|
+
required: ["pid", "project_id"],
|
|
25072
|
+
additionalProperties: false
|
|
25073
|
+
}
|
|
25074
|
+
},
|
|
25075
|
+
{
|
|
25076
|
+
name: "reset_clone",
|
|
25077
|
+
description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25078
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25079
|
+
},
|
|
25080
|
+
{
|
|
25081
|
+
name: "describe",
|
|
25082
|
+
description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
|
|
25083
|
+
inputSchema: {
|
|
25084
|
+
type: "object",
|
|
25085
|
+
properties: {
|
|
25086
|
+
object: { type: "string", description: "Object to describe (e.g. users)." },
|
|
25087
|
+
variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
|
|
25088
|
+
...sessionProps
|
|
25089
|
+
},
|
|
25090
|
+
required: ["object", "project_id"],
|
|
25091
|
+
additionalProperties: false
|
|
25092
|
+
}
|
|
25093
|
+
},
|
|
25094
|
+
{
|
|
25095
|
+
name: "list_projects",
|
|
25096
|
+
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.",
|
|
25097
|
+
inputSchema: {
|
|
25098
|
+
type: "object",
|
|
25099
|
+
properties: {
|
|
25100
|
+
org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
|
|
25101
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
25102
|
+
},
|
|
25103
|
+
additionalProperties: false
|
|
25104
|
+
}
|
|
25105
|
+
}
|
|
25106
|
+
];
|
|
25107
|
+
}
|
|
25108
|
+
async function runJoeTool(params) {
|
|
25109
|
+
const command = JOE_TOOL_TO_COMMAND[params.toolName];
|
|
25110
|
+
const a = params.args;
|
|
25111
|
+
const project = String(a.project_id ?? "").trim();
|
|
25112
|
+
if (!project) {
|
|
25113
|
+
return { content: [{ type: "text", text: "project_id is required" }], isError: true };
|
|
25114
|
+
}
|
|
25115
|
+
let sql = null;
|
|
25116
|
+
let cmdArgs = null;
|
|
25117
|
+
if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
|
|
25118
|
+
sql = String(a.sql ?? "").trim();
|
|
25119
|
+
if (!sql) {
|
|
25120
|
+
return { content: [{ type: "text", text: "sql is required" }], isError: true };
|
|
25121
|
+
}
|
|
25122
|
+
}
|
|
25123
|
+
if (command === "hypo") {
|
|
25124
|
+
const query = String(a.query ?? "").trim();
|
|
25125
|
+
if (!query) {
|
|
25126
|
+
return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
|
|
25127
|
+
}
|
|
25128
|
+
cmdArgs = { query };
|
|
25129
|
+
}
|
|
25130
|
+
if (command === "terminate") {
|
|
25131
|
+
const pid = Number(a.pid);
|
|
25132
|
+
if (!Number.isFinite(pid)) {
|
|
25133
|
+
return { content: [{ type: "text", text: "pid (number) is required for terminate_backend" }], isError: true };
|
|
25134
|
+
}
|
|
25135
|
+
cmdArgs = { pid };
|
|
25136
|
+
}
|
|
25137
|
+
if (command === "describe") {
|
|
25138
|
+
const object4 = String(a.object ?? "").trim();
|
|
25139
|
+
if (!object4) {
|
|
25140
|
+
return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
|
|
25141
|
+
}
|
|
25142
|
+
cmdArgs = { object: object4 };
|
|
25143
|
+
if (a.variant !== undefined)
|
|
25144
|
+
cmdArgs.variant = String(a.variant);
|
|
25145
|
+
}
|
|
25146
|
+
const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
|
|
25147
|
+
const newSession = a.new_session === true;
|
|
25148
|
+
const budgetMs = a.timeout_ms !== undefined ? Number(a.timeout_ms) : undefined;
|
|
25149
|
+
const outcome = await executeJoeCommand({
|
|
25150
|
+
apiKey: params.apiKey,
|
|
25151
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
25152
|
+
command,
|
|
25153
|
+
project,
|
|
25154
|
+
sql,
|
|
25155
|
+
args: cmdArgs,
|
|
25156
|
+
session,
|
|
25157
|
+
newSession,
|
|
25158
|
+
orgId: params.orgId,
|
|
25159
|
+
budgetMs,
|
|
25160
|
+
debug: params.debug
|
|
25161
|
+
});
|
|
25162
|
+
const payload = {
|
|
25163
|
+
command,
|
|
25164
|
+
command_id: outcome.commandId,
|
|
25165
|
+
session_id: outcome.sessionId,
|
|
25166
|
+
status: outcome.status,
|
|
25167
|
+
budget_expired: outcome.budgetExpired,
|
|
25168
|
+
resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
|
|
25169
|
+
result: outcome.result
|
|
25170
|
+
};
|
|
25171
|
+
const isError = outcome.status === "error" || outcome.status === "timed_out";
|
|
25172
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
|
|
25173
|
+
}
|
|
25174
|
+
async function resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfgOrgId, debug) {
|
|
25175
|
+
const project = String(args.project || "").trim();
|
|
25176
|
+
if (!project) {
|
|
25177
|
+
throw new Error("project is required (project id or alias)");
|
|
25178
|
+
}
|
|
25179
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
|
|
25180
|
+
return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
|
|
25181
|
+
}
|
|
25182
|
+
function optStr(v) {
|
|
25183
|
+
return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
|
|
25184
|
+
}
|
|
25185
|
+
async function handleToolCall(req, rootOpts, extra) {
|
|
25186
|
+
const toolName = req.params.name;
|
|
25187
|
+
const args = req.params.arguments || {};
|
|
25188
|
+
const cfg = readConfig2();
|
|
25189
|
+
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
25190
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
25191
|
+
const debug = Boolean(args.debug ?? extra?.debug);
|
|
25192
|
+
if (!apiKey) {
|
|
25193
|
+
return {
|
|
25194
|
+
content: [
|
|
25195
|
+
{
|
|
25196
|
+
type: "text",
|
|
25197
|
+
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
|
|
25198
|
+
}
|
|
25199
|
+
],
|
|
25200
|
+
isError: true
|
|
25201
|
+
};
|
|
25202
|
+
}
|
|
25203
|
+
try {
|
|
25204
|
+
if (toolName === "list_issues") {
|
|
25205
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
25206
|
+
const statusArg = args.status ? String(args.status) : undefined;
|
|
25207
|
+
let status;
|
|
25208
|
+
if (statusArg === "open")
|
|
25209
|
+
status = "open";
|
|
25210
|
+
else if (statusArg === "closed")
|
|
25211
|
+
status = "closed";
|
|
25212
|
+
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
|
|
25213
|
+
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
25214
|
+
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
|
|
25215
|
+
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
25216
|
+
}
|
|
25217
|
+
if (toolName === "view_issue") {
|
|
25218
|
+
const issueId = String(args.issue_id || "").trim();
|
|
25219
|
+
if (!issueId) {
|
|
25220
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
25221
|
+
}
|
|
25222
|
+
const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
24417
25223
|
if (!issue2) {
|
|
24418
25224
|
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
24419
25225
|
}
|
|
@@ -24653,6 +25459,147 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24653
25459
|
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
|
|
24654
25460
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
24655
25461
|
}
|
|
25462
|
+
if (toolName === "list_projects") {
|
|
25463
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
25464
|
+
const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
25465
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
25466
|
+
}
|
|
25467
|
+
if (toolName in JOE_TOOL_TO_COMMAND) {
|
|
25468
|
+
const orgId = cfg.orgId ?? undefined;
|
|
25469
|
+
return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
|
|
25470
|
+
}
|
|
25471
|
+
if (toolName === "clone_create") {
|
|
25472
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25473
|
+
const result = await createClone({
|
|
25474
|
+
apiKey,
|
|
25475
|
+
apiBaseUrl,
|
|
25476
|
+
instanceId,
|
|
25477
|
+
cloneId: optStr(args.clone_id),
|
|
25478
|
+
branch: optStr(args.branch),
|
|
25479
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25480
|
+
dbUser: optStr(args.db_user),
|
|
25481
|
+
dbPassword: optStr(args.db_password),
|
|
25482
|
+
isProtected: args.protected === true,
|
|
25483
|
+
debug
|
|
25484
|
+
});
|
|
25485
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25486
|
+
}
|
|
25487
|
+
if (toolName === "clone_list") {
|
|
25488
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25489
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25490
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25491
|
+
}
|
|
25492
|
+
if (toolName === "clone_status") {
|
|
25493
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25494
|
+
if (!cloneId)
|
|
25495
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25496
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25497
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25498
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25499
|
+
}
|
|
25500
|
+
if (toolName === "clone_reset") {
|
|
25501
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25502
|
+
if (!cloneId)
|
|
25503
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25504
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25505
|
+
const result = await resetClone({
|
|
25506
|
+
apiKey,
|
|
25507
|
+
apiBaseUrl,
|
|
25508
|
+
instanceId,
|
|
25509
|
+
cloneId,
|
|
25510
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25511
|
+
latest: args.latest === true ? true : undefined,
|
|
25512
|
+
debug
|
|
25513
|
+
});
|
|
25514
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
|
|
25515
|
+
}
|
|
25516
|
+
if (toolName === "clone_destroy") {
|
|
25517
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25518
|
+
if (!cloneId)
|
|
25519
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25520
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25521
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25522
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
|
|
25523
|
+
}
|
|
25524
|
+
if (toolName === "branch_list") {
|
|
25525
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25526
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25527
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25528
|
+
}
|
|
25529
|
+
if (toolName === "branch_create") {
|
|
25530
|
+
const branchName = String(args.name || "").trim();
|
|
25531
|
+
if (!branchName)
|
|
25532
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25533
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25534
|
+
const result = await createBranch({
|
|
25535
|
+
apiKey,
|
|
25536
|
+
apiBaseUrl,
|
|
25537
|
+
instanceId,
|
|
25538
|
+
branchName,
|
|
25539
|
+
baseBranch: optStr(args.base_branch),
|
|
25540
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25541
|
+
debug
|
|
25542
|
+
});
|
|
25543
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25544
|
+
}
|
|
25545
|
+
if (toolName === "branch_delete") {
|
|
25546
|
+
const branchName = String(args.name || "").trim();
|
|
25547
|
+
if (!branchName)
|
|
25548
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25549
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25550
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25551
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
|
|
25552
|
+
}
|
|
25553
|
+
if (toolName === "branch_log") {
|
|
25554
|
+
const branchName = String(args.name || "").trim();
|
|
25555
|
+
if (!branchName)
|
|
25556
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25557
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25558
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25559
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25560
|
+
}
|
|
25561
|
+
if (toolName === "snapshot_list") {
|
|
25562
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25563
|
+
const result = await listSnapshots({
|
|
25564
|
+
apiKey,
|
|
25565
|
+
apiBaseUrl,
|
|
25566
|
+
instanceId,
|
|
25567
|
+
branchName: optStr(args.branch),
|
|
25568
|
+
dataset: optStr(args.dataset),
|
|
25569
|
+
debug
|
|
25570
|
+
});
|
|
25571
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25572
|
+
}
|
|
25573
|
+
if (toolName === "snapshot_create") {
|
|
25574
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25575
|
+
if (!cloneId)
|
|
25576
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25577
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25578
|
+
const result = await createSnapshot({
|
|
25579
|
+
apiKey,
|
|
25580
|
+
apiBaseUrl,
|
|
25581
|
+
instanceId,
|
|
25582
|
+
cloneId,
|
|
25583
|
+
message: optStr(args.message),
|
|
25584
|
+
debug
|
|
25585
|
+
});
|
|
25586
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25587
|
+
}
|
|
25588
|
+
if (toolName === "snapshot_destroy") {
|
|
25589
|
+
const snapshotId = String(args.snapshot_id || "").trim();
|
|
25590
|
+
if (!snapshotId)
|
|
25591
|
+
return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
|
|
25592
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25593
|
+
const result = await destroySnapshot({
|
|
25594
|
+
apiKey,
|
|
25595
|
+
apiBaseUrl,
|
|
25596
|
+
instanceId,
|
|
25597
|
+
snapshotId,
|
|
25598
|
+
force: args.force === true,
|
|
25599
|
+
debug
|
|
25600
|
+
});
|
|
25601
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
|
|
25602
|
+
}
|
|
24656
25603
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
24657
25604
|
} catch (err) {
|
|
24658
25605
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24929,6 +25876,197 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
24929
25876
|
},
|
|
24930
25877
|
additionalProperties: false
|
|
24931
25878
|
}
|
|
25879
|
+
},
|
|
25880
|
+
...joeToolDefinitions(),
|
|
25881
|
+
{
|
|
25882
|
+
name: "clone_create",
|
|
25883
|
+
description: "Create a DBLab thin clone of the project's database (same as CLI 'clone create'). Requires the joe:plan scope.",
|
|
25884
|
+
inputSchema: {
|
|
25885
|
+
type: "object",
|
|
25886
|
+
properties: {
|
|
25887
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25888
|
+
branch: { type: "string", description: "Branch to clone from" },
|
|
25889
|
+
snapshot_id: { type: "string", description: "Snapshot id to clone from" },
|
|
25890
|
+
clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
|
|
25891
|
+
db_user: { type: "string", description: "Clone DB user" },
|
|
25892
|
+
db_password: { type: "string", description: "Clone DB password" },
|
|
25893
|
+
protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
|
|
25894
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25895
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25896
|
+
},
|
|
25897
|
+
required: ["project"],
|
|
25898
|
+
additionalProperties: false
|
|
25899
|
+
}
|
|
25900
|
+
},
|
|
25901
|
+
{
|
|
25902
|
+
name: "clone_list",
|
|
25903
|
+
description: "List the project's DBLab thin clones (same as CLI 'clone list').",
|
|
25904
|
+
inputSchema: {
|
|
25905
|
+
type: "object",
|
|
25906
|
+
properties: {
|
|
25907
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25908
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25909
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25910
|
+
},
|
|
25911
|
+
required: ["project"],
|
|
25912
|
+
additionalProperties: false
|
|
25913
|
+
}
|
|
25914
|
+
},
|
|
25915
|
+
{
|
|
25916
|
+
name: "clone_status",
|
|
25917
|
+
description: "Show a DBLab clone's status (same as CLI 'clone status').",
|
|
25918
|
+
inputSchema: {
|
|
25919
|
+
type: "object",
|
|
25920
|
+
properties: {
|
|
25921
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25922
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25923
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25924
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25925
|
+
},
|
|
25926
|
+
required: ["project", "clone_id"],
|
|
25927
|
+
additionalProperties: false
|
|
25928
|
+
}
|
|
25929
|
+
},
|
|
25930
|
+
{
|
|
25931
|
+
name: "clone_reset",
|
|
25932
|
+
description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'clone reset'). Requires the joe:admin scope.",
|
|
25933
|
+
inputSchema: {
|
|
25934
|
+
type: "object",
|
|
25935
|
+
properties: {
|
|
25936
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25937
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25938
|
+
snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
|
|
25939
|
+
latest: { type: "boolean", description: "Reset to the latest snapshot" },
|
|
25940
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25941
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25942
|
+
},
|
|
25943
|
+
required: ["project", "clone_id"],
|
|
25944
|
+
additionalProperties: false
|
|
25945
|
+
}
|
|
25946
|
+
},
|
|
25947
|
+
{
|
|
25948
|
+
name: "clone_destroy",
|
|
25949
|
+
description: "Destroy a DBLab clone (same as CLI 'clone destroy'). Requires the joe:admin scope.",
|
|
25950
|
+
inputSchema: {
|
|
25951
|
+
type: "object",
|
|
25952
|
+
properties: {
|
|
25953
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25954
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25955
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25956
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25957
|
+
},
|
|
25958
|
+
required: ["project", "clone_id"],
|
|
25959
|
+
additionalProperties: false
|
|
25960
|
+
}
|
|
25961
|
+
},
|
|
25962
|
+
{
|
|
25963
|
+
name: "branch_list",
|
|
25964
|
+
description: "List the project's DBLab branches (same as CLI 'branch list').",
|
|
25965
|
+
inputSchema: {
|
|
25966
|
+
type: "object",
|
|
25967
|
+
properties: {
|
|
25968
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25969
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25970
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25971
|
+
},
|
|
25972
|
+
required: ["project"],
|
|
25973
|
+
additionalProperties: false
|
|
25974
|
+
}
|
|
25975
|
+
},
|
|
25976
|
+
{
|
|
25977
|
+
name: "branch_create",
|
|
25978
|
+
description: "Create a DBLab branch (same as CLI 'branch create'). Requires the joe:plan scope.",
|
|
25979
|
+
inputSchema: {
|
|
25980
|
+
type: "object",
|
|
25981
|
+
properties: {
|
|
25982
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25983
|
+
name: { type: "string", description: "Branch name" },
|
|
25984
|
+
snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
|
|
25985
|
+
base_branch: { type: "string", description: "Parent branch to fork from" },
|
|
25986
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25987
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25988
|
+
},
|
|
25989
|
+
required: ["project", "name"],
|
|
25990
|
+
additionalProperties: false
|
|
25991
|
+
}
|
|
25992
|
+
},
|
|
25993
|
+
{
|
|
25994
|
+
name: "branch_delete",
|
|
25995
|
+
description: "Delete a DBLab branch (same as CLI 'branch delete'). Requires the joe:admin scope.",
|
|
25996
|
+
inputSchema: {
|
|
25997
|
+
type: "object",
|
|
25998
|
+
properties: {
|
|
25999
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26000
|
+
name: { type: "string", description: "Branch name" },
|
|
26001
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26002
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26003
|
+
},
|
|
26004
|
+
required: ["project", "name"],
|
|
26005
|
+
additionalProperties: false
|
|
26006
|
+
}
|
|
26007
|
+
},
|
|
26008
|
+
{
|
|
26009
|
+
name: "branch_log",
|
|
26010
|
+
description: "Show a DBLab branch's snapshot log (same as CLI 'branch log').",
|
|
26011
|
+
inputSchema: {
|
|
26012
|
+
type: "object",
|
|
26013
|
+
properties: {
|
|
26014
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26015
|
+
name: { type: "string", description: "Branch name" },
|
|
26016
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26017
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26018
|
+
},
|
|
26019
|
+
required: ["project", "name"],
|
|
26020
|
+
additionalProperties: false
|
|
26021
|
+
}
|
|
26022
|
+
},
|
|
26023
|
+
{
|
|
26024
|
+
name: "snapshot_list",
|
|
26025
|
+
description: "List the project's DBLab snapshots (same as CLI 'snapshot list').",
|
|
26026
|
+
inputSchema: {
|
|
26027
|
+
type: "object",
|
|
26028
|
+
properties: {
|
|
26029
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26030
|
+
branch: { type: "string", description: "Filter by branch" },
|
|
26031
|
+
dataset: { type: "string", description: "Filter by dataset" },
|
|
26032
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26033
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26034
|
+
},
|
|
26035
|
+
required: ["project"],
|
|
26036
|
+
additionalProperties: false
|
|
26037
|
+
}
|
|
26038
|
+
},
|
|
26039
|
+
{
|
|
26040
|
+
name: "snapshot_create",
|
|
26041
|
+
description: "Create a DBLab snapshot from a clone (same as CLI 'snapshot create'). Requires the joe:plan scope.",
|
|
26042
|
+
inputSchema: {
|
|
26043
|
+
type: "object",
|
|
26044
|
+
properties: {
|
|
26045
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26046
|
+
clone_id: { type: "string", description: "Clone id to snapshot" },
|
|
26047
|
+
message: { type: "string", description: "Snapshot message" },
|
|
26048
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26049
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26050
|
+
},
|
|
26051
|
+
required: ["project", "clone_id"],
|
|
26052
|
+
additionalProperties: false
|
|
26053
|
+
}
|
|
26054
|
+
},
|
|
26055
|
+
{
|
|
26056
|
+
name: "snapshot_destroy",
|
|
26057
|
+
description: "Destroy a DBLab snapshot (same as CLI 'snapshot destroy'). Requires the joe:admin scope.",
|
|
26058
|
+
inputSchema: {
|
|
26059
|
+
type: "object",
|
|
26060
|
+
properties: {
|
|
26061
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26062
|
+
snapshot_id: { type: "string", description: "Snapshot id" },
|
|
26063
|
+
force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
|
|
26064
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26065
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26066
|
+
},
|
|
26067
|
+
required: ["project", "snapshot_id"],
|
|
26068
|
+
additionalProperties: false
|
|
26069
|
+
}
|
|
24932
26070
|
}
|
|
24933
26071
|
]
|
|
24934
26072
|
};
|
|
@@ -25801,11 +26939,709 @@ function renderMarkdownForTerminal(md) {
|
|
|
25801
26939
|
`);
|
|
25802
26940
|
}
|
|
25803
26941
|
|
|
26942
|
+
// lib/joe.ts
|
|
26943
|
+
import * as fs5 from "fs";
|
|
26944
|
+
import * as path5 from "path";
|
|
26945
|
+
import * as crypto2 from "node:crypto";
|
|
26946
|
+
var JOE_COMMANDS2 = [
|
|
26947
|
+
"plan",
|
|
26948
|
+
"explain",
|
|
26949
|
+
"exec",
|
|
26950
|
+
"hypo",
|
|
26951
|
+
"activity",
|
|
26952
|
+
"terminate",
|
|
26953
|
+
"reset",
|
|
26954
|
+
"describe"
|
|
26955
|
+
];
|
|
26956
|
+
var TERMINAL_STATES2 = new Set([
|
|
26957
|
+
"done",
|
|
26958
|
+
"error",
|
|
26959
|
+
"timed_out"
|
|
26960
|
+
]);
|
|
26961
|
+
var AI_ENABLED_COMMANDS2 = new Set([
|
|
26962
|
+
"plan",
|
|
26963
|
+
"explain",
|
|
26964
|
+
"exec",
|
|
26965
|
+
"hypo",
|
|
26966
|
+
"activity",
|
|
26967
|
+
"describe"
|
|
26968
|
+
]);
|
|
26969
|
+
var EXECUTING_COMMANDS2 = new Set([
|
|
26970
|
+
"exec",
|
|
26971
|
+
"explain"
|
|
26972
|
+
]);
|
|
26973
|
+
var DEFAULT_BUDGET_MS2 = 25000;
|
|
26974
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 800;
|
|
26975
|
+
async function callRpc2(params) {
|
|
26976
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26977
|
+
if (!apiKey) {
|
|
26978
|
+
throw new Error("API key is required");
|
|
26979
|
+
}
|
|
26980
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26981
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
26982
|
+
const payload = JSON.stringify(body);
|
|
26983
|
+
const headers = {
|
|
26984
|
+
"access-token": apiKey,
|
|
26985
|
+
"Content-Type": "application/json",
|
|
26986
|
+
Connection: "close"
|
|
26987
|
+
};
|
|
26988
|
+
if (debug) {
|
|
26989
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26990
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26991
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26992
|
+
console.error(`Debug: Request body: ${payload}`);
|
|
26993
|
+
}
|
|
26994
|
+
const response = await fetch(url.toString(), {
|
|
26995
|
+
method: "POST",
|
|
26996
|
+
headers,
|
|
26997
|
+
body: payload
|
|
26998
|
+
});
|
|
26999
|
+
const text = await response.text();
|
|
27000
|
+
if (debug) {
|
|
27001
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27002
|
+
console.error(`Debug: Response body: ${text}`);
|
|
27003
|
+
}
|
|
27004
|
+
if (!response.ok) {
|
|
27005
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
27006
|
+
}
|
|
27007
|
+
try {
|
|
27008
|
+
return JSON.parse(text);
|
|
27009
|
+
} catch {
|
|
27010
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
27011
|
+
}
|
|
27012
|
+
}
|
|
27013
|
+
async function submitCommand2(params) {
|
|
27014
|
+
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
27015
|
+
if (!JOE_COMMANDS2.includes(command)) {
|
|
27016
|
+
throw new Error(`Unknown Joe command: ${command}`);
|
|
27017
|
+
}
|
|
27018
|
+
const body = {
|
|
27019
|
+
project_id: projectId,
|
|
27020
|
+
command,
|
|
27021
|
+
sql: sql ?? null,
|
|
27022
|
+
args: args ?? null,
|
|
27023
|
+
session_id: sessionId ?? null
|
|
27024
|
+
};
|
|
27025
|
+
if (idempotencyKey) {
|
|
27026
|
+
body.idempotency_key = idempotencyKey;
|
|
27027
|
+
}
|
|
27028
|
+
return callRpc2({
|
|
27029
|
+
apiKey,
|
|
27030
|
+
apiBaseUrl,
|
|
27031
|
+
fn: "joe_command_submit",
|
|
27032
|
+
body,
|
|
27033
|
+
operation: `Failed to submit ${command} command`,
|
|
27034
|
+
debug
|
|
27035
|
+
});
|
|
27036
|
+
}
|
|
27037
|
+
async function getCommandStatus2(params) {
|
|
27038
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27039
|
+
if (!commandId) {
|
|
27040
|
+
throw new Error("commandId is required");
|
|
27041
|
+
}
|
|
27042
|
+
return callRpc2({
|
|
27043
|
+
apiKey,
|
|
27044
|
+
apiBaseUrl,
|
|
27045
|
+
fn: "joe_command_status",
|
|
27046
|
+
body: { command_id: commandId },
|
|
27047
|
+
operation: "Failed to fetch command status",
|
|
27048
|
+
debug
|
|
27049
|
+
});
|
|
27050
|
+
}
|
|
27051
|
+
async function getCommandResult2(params) {
|
|
27052
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27053
|
+
if (!commandId) {
|
|
27054
|
+
throw new Error("commandId is required");
|
|
27055
|
+
}
|
|
27056
|
+
return callRpc2({
|
|
27057
|
+
apiKey,
|
|
27058
|
+
apiBaseUrl,
|
|
27059
|
+
fn: "joe_command_result",
|
|
27060
|
+
body: { command_id: commandId },
|
|
27061
|
+
operation: "Failed to fetch command result",
|
|
27062
|
+
debug
|
|
27063
|
+
});
|
|
27064
|
+
}
|
|
27065
|
+
function normalizeProjectRow2(row) {
|
|
27066
|
+
const projectId = row.project_id ?? row.id ?? 0;
|
|
27067
|
+
return {
|
|
27068
|
+
project_id: Number(projectId),
|
|
27069
|
+
alias: row.alias ?? null,
|
|
27070
|
+
name: row.name ?? null,
|
|
27071
|
+
label: row.label ?? null,
|
|
27072
|
+
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
27073
|
+
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false)
|
|
27074
|
+
};
|
|
27075
|
+
}
|
|
27076
|
+
async function listProjects2(params) {
|
|
27077
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27078
|
+
const body = {};
|
|
27079
|
+
if (typeof orgId === "number") {
|
|
27080
|
+
body.org_id = orgId;
|
|
27081
|
+
}
|
|
27082
|
+
const rows = await callRpc2({
|
|
27083
|
+
apiKey,
|
|
27084
|
+
apiBaseUrl,
|
|
27085
|
+
fn: "projects_list",
|
|
27086
|
+
body,
|
|
27087
|
+
operation: "Failed to list projects",
|
|
27088
|
+
debug
|
|
27089
|
+
});
|
|
27090
|
+
if (!Array.isArray(rows)) {
|
|
27091
|
+
return [];
|
|
27092
|
+
}
|
|
27093
|
+
return rows.map(normalizeProjectRow2);
|
|
27094
|
+
}
|
|
27095
|
+
function isNumericProjectRef3(ref) {
|
|
27096
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
27097
|
+
}
|
|
27098
|
+
async function resolveProjectId2(params) {
|
|
27099
|
+
const ref = String(params.project ?? "").trim();
|
|
27100
|
+
if (!ref) {
|
|
27101
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27102
|
+
}
|
|
27103
|
+
if (isNumericProjectRef3(ref)) {
|
|
27104
|
+
return Number(ref);
|
|
27105
|
+
}
|
|
27106
|
+
const projects = await listProjects2({
|
|
27107
|
+
apiKey: params.apiKey,
|
|
27108
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27109
|
+
orgId: params.orgId,
|
|
27110
|
+
debug: params.debug
|
|
27111
|
+
});
|
|
27112
|
+
const needle = ref.toLowerCase();
|
|
27113
|
+
const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
27114
|
+
if (!match) {
|
|
27115
|
+
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27116
|
+
}
|
|
27117
|
+
return match.project_id;
|
|
27118
|
+
}
|
|
27119
|
+
function sessionStorePath2(dir) {
|
|
27120
|
+
return path5.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
27121
|
+
}
|
|
27122
|
+
function readSessionStore2(dir) {
|
|
27123
|
+
const file = sessionStorePath2(dir);
|
|
27124
|
+
if (!fs5.existsSync(file)) {
|
|
27125
|
+
return {};
|
|
27126
|
+
}
|
|
27127
|
+
try {
|
|
27128
|
+
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
27129
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
27130
|
+
return parsed;
|
|
27131
|
+
}
|
|
27132
|
+
} catch {}
|
|
27133
|
+
return {};
|
|
27134
|
+
}
|
|
27135
|
+
function readStoredSessionId2(projectId, dir) {
|
|
27136
|
+
const store = readSessionStore2(dir);
|
|
27137
|
+
const value = store[String(projectId)];
|
|
27138
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
27139
|
+
}
|
|
27140
|
+
function writeStoredSessionId2(projectId, sessionId, dir) {
|
|
27141
|
+
const baseDir = dir ?? getConfigDir2();
|
|
27142
|
+
if (!fs5.existsSync(baseDir)) {
|
|
27143
|
+
fs5.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
27144
|
+
}
|
|
27145
|
+
const store = readSessionStore2(dir);
|
|
27146
|
+
store[String(projectId)] = sessionId;
|
|
27147
|
+
fs5.writeFileSync(sessionStorePath2(dir), JSON.stringify(store, null, 2) + `
|
|
27148
|
+
`, { mode: 384 });
|
|
27149
|
+
}
|
|
27150
|
+
function clearStoredSessionId2(projectId, dir) {
|
|
27151
|
+
const file = sessionStorePath2(dir);
|
|
27152
|
+
if (!fs5.existsSync(file)) {
|
|
27153
|
+
return;
|
|
27154
|
+
}
|
|
27155
|
+
const store = readSessionStore2(dir);
|
|
27156
|
+
if (String(projectId) in store) {
|
|
27157
|
+
delete store[String(projectId)];
|
|
27158
|
+
fs5.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
27159
|
+
`, { mode: 384 });
|
|
27160
|
+
}
|
|
27161
|
+
}
|
|
27162
|
+
function computeIdempotencyKey2(command, projectId, sql, args) {
|
|
27163
|
+
if (EXECUTING_COMMANDS2.has(command)) {
|
|
27164
|
+
const canonical = `${projectId}
|
|
27165
|
+
${command}
|
|
27166
|
+
${sql ?? ""}
|
|
27167
|
+
${JSON.stringify(args ?? null)}`;
|
|
27168
|
+
return `joe-${crypto2.createHash("sha256").update(canonical).digest("hex")}`;
|
|
27169
|
+
}
|
|
27170
|
+
return `joe-${crypto2.randomUUID()}`;
|
|
27171
|
+
}
|
|
27172
|
+
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27173
|
+
async function runCommand2(params) {
|
|
27174
|
+
const {
|
|
27175
|
+
apiKey,
|
|
27176
|
+
apiBaseUrl,
|
|
27177
|
+
command,
|
|
27178
|
+
projectId,
|
|
27179
|
+
sql,
|
|
27180
|
+
args,
|
|
27181
|
+
sessionId,
|
|
27182
|
+
debug
|
|
27183
|
+
} = params;
|
|
27184
|
+
const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS2;
|
|
27185
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
27186
|
+
const now = params.now ?? Date.now;
|
|
27187
|
+
const sleep = params.sleep ?? defaultSleep2;
|
|
27188
|
+
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey2(command, projectId, sql, args);
|
|
27189
|
+
const submitted = await submitCommand2({
|
|
27190
|
+
apiKey,
|
|
27191
|
+
apiBaseUrl,
|
|
27192
|
+
command,
|
|
27193
|
+
projectId,
|
|
27194
|
+
sql,
|
|
27195
|
+
args,
|
|
27196
|
+
sessionId,
|
|
27197
|
+
idempotencyKey,
|
|
27198
|
+
debug
|
|
27199
|
+
});
|
|
27200
|
+
const commandId = submitted.command_id;
|
|
27201
|
+
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
27202
|
+
const deadline = now() + budgetMs;
|
|
27203
|
+
let status = submitted.status;
|
|
27204
|
+
while (true) {
|
|
27205
|
+
const statusResp = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27206
|
+
status = statusResp.status;
|
|
27207
|
+
if (TERMINAL_STATES2.has(status)) {
|
|
27208
|
+
break;
|
|
27209
|
+
}
|
|
27210
|
+
if (now() >= deadline) {
|
|
27211
|
+
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
27212
|
+
}
|
|
27213
|
+
await sleep(pollIntervalMs);
|
|
27214
|
+
}
|
|
27215
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27216
|
+
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
27217
|
+
}
|
|
27218
|
+
async function executeJoeCommand2(params) {
|
|
27219
|
+
const projectId = await resolveProjectId2({
|
|
27220
|
+
apiKey: params.apiKey,
|
|
27221
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27222
|
+
project: params.project,
|
|
27223
|
+
orgId: params.orgId,
|
|
27224
|
+
debug: params.debug
|
|
27225
|
+
});
|
|
27226
|
+
let sessionId;
|
|
27227
|
+
if (params.newSession) {
|
|
27228
|
+
clearStoredSessionId2(projectId, params.sessionDir);
|
|
27229
|
+
sessionId = null;
|
|
27230
|
+
} else if (params.session) {
|
|
27231
|
+
sessionId = String(params.session);
|
|
27232
|
+
} else {
|
|
27233
|
+
sessionId = readStoredSessionId2(projectId, params.sessionDir);
|
|
27234
|
+
}
|
|
27235
|
+
const outcome = await runCommand2({
|
|
27236
|
+
apiKey: params.apiKey,
|
|
27237
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27238
|
+
command: params.command,
|
|
27239
|
+
projectId,
|
|
27240
|
+
sql: params.sql,
|
|
27241
|
+
args: params.args,
|
|
27242
|
+
sessionId,
|
|
27243
|
+
budgetMs: params.budgetMs,
|
|
27244
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
27245
|
+
debug: params.debug,
|
|
27246
|
+
now: params.now,
|
|
27247
|
+
sleep: params.sleep
|
|
27248
|
+
});
|
|
27249
|
+
if (outcome.sessionId) {
|
|
27250
|
+
writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
|
|
27251
|
+
}
|
|
27252
|
+
return { ...outcome, command: params.command, projectId };
|
|
27253
|
+
}
|
|
27254
|
+
function clientSidePlanFlags(planJson) {
|
|
27255
|
+
const flags = [];
|
|
27256
|
+
const walk = (node) => {
|
|
27257
|
+
if (!node || typeof node !== "object") {
|
|
27258
|
+
return;
|
|
27259
|
+
}
|
|
27260
|
+
const nodeType = node["Node Type"];
|
|
27261
|
+
if (nodeType === "Seq Scan") {
|
|
27262
|
+
const rel = node["Relation Name"];
|
|
27263
|
+
flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
|
|
27264
|
+
}
|
|
27265
|
+
if (Array.isArray(node.Plans)) {
|
|
27266
|
+
for (const child of node.Plans) {
|
|
27267
|
+
walk(child);
|
|
27268
|
+
}
|
|
27269
|
+
}
|
|
27270
|
+
};
|
|
27271
|
+
if (planJson && typeof planJson === "object") {
|
|
27272
|
+
const root = planJson;
|
|
27273
|
+
walk(root.Plan ?? planJson);
|
|
27274
|
+
}
|
|
27275
|
+
return flags;
|
|
27276
|
+
}
|
|
27277
|
+
function formatJoeResult(command, result) {
|
|
27278
|
+
const lines = [];
|
|
27279
|
+
switch (command) {
|
|
27280
|
+
case "plan":
|
|
27281
|
+
case "explain": {
|
|
27282
|
+
if (result.plan_text) {
|
|
27283
|
+
lines.push(result.plan_text);
|
|
27284
|
+
}
|
|
27285
|
+
for (const flag of clientSidePlanFlags(result.plan_json)) {
|
|
27286
|
+
lines.push(`⚑ ${flag}`);
|
|
27287
|
+
}
|
|
27288
|
+
if (result.queryid || result.plan_fingerprint) {
|
|
27289
|
+
lines.push(`(queryid ${result.queryid ?? "?"} · plan_fp ${result.plan_fingerprint ?? "?"})`);
|
|
27290
|
+
}
|
|
27291
|
+
break;
|
|
27292
|
+
}
|
|
27293
|
+
case "exec": {
|
|
27294
|
+
const notices = result.notices ?? [];
|
|
27295
|
+
const rowCount = result.row_count ?? 0;
|
|
27296
|
+
lines.push(`${notices.join(" ") || "OK"} · ${rowCount} row${rowCount === 1 ? "" : "s"}`);
|
|
27297
|
+
if (result.result_rows && result.result_rows.length > 0) {
|
|
27298
|
+
lines.push(JSON.stringify(result.result_rows, null, 2));
|
|
27299
|
+
}
|
|
27300
|
+
break;
|
|
27301
|
+
}
|
|
27302
|
+
case "hypo": {
|
|
27303
|
+
lines.push(result.hypo_used ? "hypothetical index would be used" : "hypothetical index would NOT be used");
|
|
27304
|
+
if (result.hypo_plan !== undefined) {
|
|
27305
|
+
lines.push(JSON.stringify(result.hypo_plan, null, 2));
|
|
27306
|
+
}
|
|
27307
|
+
break;
|
|
27308
|
+
}
|
|
27309
|
+
case "activity":
|
|
27310
|
+
case "describe": {
|
|
27311
|
+
lines.push(JSON.stringify(result.snapshot ?? null, null, 2));
|
|
27312
|
+
break;
|
|
27313
|
+
}
|
|
27314
|
+
case "terminate": {
|
|
27315
|
+
lines.push(`terminated ${result.terminated ? "yes" : "no"} · pid ${result.pid ?? "?"}`);
|
|
27316
|
+
break;
|
|
27317
|
+
}
|
|
27318
|
+
case "reset": {
|
|
27319
|
+
lines.push(`reset ${result.reset ? "ok" : "no"}`);
|
|
27320
|
+
break;
|
|
27321
|
+
}
|
|
27322
|
+
}
|
|
27323
|
+
return lines.join(`
|
|
27324
|
+
`);
|
|
27325
|
+
}
|
|
27326
|
+
function formatProjectsTable(projects) {
|
|
27327
|
+
const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
|
|
27328
|
+
const rows = projects.map((p) => [
|
|
27329
|
+
String(p.project_id),
|
|
27330
|
+
p.alias ?? "-",
|
|
27331
|
+
p.name ?? "-",
|
|
27332
|
+
p.joe_ready ? "ready" : "no",
|
|
27333
|
+
p.tunnel ? "yes" : "no"
|
|
27334
|
+
]);
|
|
27335
|
+
const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
|
|
27336
|
+
const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
|
|
27337
|
+
return [pad(header), ...rows.map(pad)].join(`
|
|
27338
|
+
`);
|
|
27339
|
+
}
|
|
27340
|
+
|
|
27341
|
+
// lib/dblab.ts
|
|
27342
|
+
function isNumericProjectRef4(ref) {
|
|
27343
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
27344
|
+
}
|
|
27345
|
+
async function resolveDblabInstanceId2(params) {
|
|
27346
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27347
|
+
if (!apiKey) {
|
|
27348
|
+
throw new Error("API key is required");
|
|
27349
|
+
}
|
|
27350
|
+
const ref = String(params.project ?? "").trim();
|
|
27351
|
+
if (!ref) {
|
|
27352
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27353
|
+
}
|
|
27354
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27355
|
+
const url = new URL(`${base}/dblab_instances`);
|
|
27356
|
+
url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
|
|
27357
|
+
if (typeof orgId === "number") {
|
|
27358
|
+
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
27359
|
+
}
|
|
27360
|
+
const headers = {
|
|
27361
|
+
"access-token": apiKey,
|
|
27362
|
+
"Content-Type": "application/json",
|
|
27363
|
+
Connection: "close"
|
|
27364
|
+
};
|
|
27365
|
+
if (debug) {
|
|
27366
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27367
|
+
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
27368
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27369
|
+
}
|
|
27370
|
+
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
27371
|
+
const text = await response.text();
|
|
27372
|
+
if (debug) {
|
|
27373
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27374
|
+
console.error(`Debug: Response body: ${text}`);
|
|
27375
|
+
}
|
|
27376
|
+
if (!response.ok) {
|
|
27377
|
+
throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
|
|
27378
|
+
}
|
|
27379
|
+
let rows;
|
|
27380
|
+
try {
|
|
27381
|
+
const parsed = JSON.parse(text);
|
|
27382
|
+
rows = Array.isArray(parsed) ? parsed : [];
|
|
27383
|
+
} catch {
|
|
27384
|
+
throw new Error(`Failed to parse DBLab instances response: ${text}`);
|
|
27385
|
+
}
|
|
27386
|
+
const numeric = isNumericProjectRef4(ref);
|
|
27387
|
+
const needle = ref.toLowerCase();
|
|
27388
|
+
const match = rows.find((r) => {
|
|
27389
|
+
if (numeric) {
|
|
27390
|
+
return String(r.project_id ?? "") === ref;
|
|
27391
|
+
}
|
|
27392
|
+
return r.project_alias != null && String(r.project_alias).toLowerCase() === needle || r.project_name != null && String(r.project_name).toLowerCase() === needle || r.project_label_or_name != null && String(r.project_label_or_name).toLowerCase() === needle;
|
|
27393
|
+
});
|
|
27394
|
+
if (!match) {
|
|
27395
|
+
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27396
|
+
}
|
|
27397
|
+
return String(match.id);
|
|
27398
|
+
}
|
|
27399
|
+
async function callDblabApi2(params) {
|
|
27400
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
27401
|
+
if (!apiKey) {
|
|
27402
|
+
throw new Error("API key is required");
|
|
27403
|
+
}
|
|
27404
|
+
if (!instanceId) {
|
|
27405
|
+
throw new Error("instanceId is required");
|
|
27406
|
+
}
|
|
27407
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27408
|
+
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
27409
|
+
const bodyObj = {
|
|
27410
|
+
instance_id: instanceId,
|
|
27411
|
+
action,
|
|
27412
|
+
method
|
|
27413
|
+
};
|
|
27414
|
+
if (data !== undefined) {
|
|
27415
|
+
bodyObj.data = data;
|
|
27416
|
+
}
|
|
27417
|
+
const body = JSON.stringify(bodyObj);
|
|
27418
|
+
const headers = {
|
|
27419
|
+
"access-token": apiKey,
|
|
27420
|
+
"Content-Type": "application/json",
|
|
27421
|
+
Connection: "close"
|
|
27422
|
+
};
|
|
27423
|
+
if (debug) {
|
|
27424
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27425
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27426
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27427
|
+
console.error(`Debug: Request body: ${body}`);
|
|
27428
|
+
}
|
|
27429
|
+
const response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
27430
|
+
const text = await response.text();
|
|
27431
|
+
if (debug) {
|
|
27432
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27433
|
+
console.error(`Debug: Response body: ${text}`);
|
|
27434
|
+
}
|
|
27435
|
+
if (!response.ok) {
|
|
27436
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
27437
|
+
}
|
|
27438
|
+
if (text.trim() === "") {
|
|
27439
|
+
return null;
|
|
27440
|
+
}
|
|
27441
|
+
try {
|
|
27442
|
+
return JSON.parse(text);
|
|
27443
|
+
} catch {
|
|
27444
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
27445
|
+
}
|
|
27446
|
+
}
|
|
27447
|
+
async function createClone2(params) {
|
|
27448
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
27449
|
+
const data = { protected: Boolean(isProtected) };
|
|
27450
|
+
if (cloneId)
|
|
27451
|
+
data.id = cloneId;
|
|
27452
|
+
if (branch)
|
|
27453
|
+
data.branch = branch;
|
|
27454
|
+
if (snapshotId)
|
|
27455
|
+
data.snapshot = { id: snapshotId };
|
|
27456
|
+
if (dbUser && dbPassword)
|
|
27457
|
+
data.db = { username: dbUser, password: dbPassword };
|
|
27458
|
+
return callDblabApi2({
|
|
27459
|
+
apiKey,
|
|
27460
|
+
apiBaseUrl,
|
|
27461
|
+
instanceId,
|
|
27462
|
+
action: "/clone",
|
|
27463
|
+
method: "post",
|
|
27464
|
+
data,
|
|
27465
|
+
operation: "Failed to create clone",
|
|
27466
|
+
debug
|
|
27467
|
+
});
|
|
27468
|
+
}
|
|
27469
|
+
async function listClones2(params) {
|
|
27470
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27471
|
+
return callDblabApi2({
|
|
27472
|
+
apiKey,
|
|
27473
|
+
apiBaseUrl,
|
|
27474
|
+
instanceId,
|
|
27475
|
+
action: "/clones",
|
|
27476
|
+
method: "get",
|
|
27477
|
+
operation: "Failed to list clones",
|
|
27478
|
+
debug
|
|
27479
|
+
});
|
|
27480
|
+
}
|
|
27481
|
+
async function getClone2(params) {
|
|
27482
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27483
|
+
if (!cloneId)
|
|
27484
|
+
throw new Error("cloneId is required");
|
|
27485
|
+
return callDblabApi2({
|
|
27486
|
+
apiKey,
|
|
27487
|
+
apiBaseUrl,
|
|
27488
|
+
instanceId,
|
|
27489
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27490
|
+
method: "get",
|
|
27491
|
+
operation: "Failed to get clone",
|
|
27492
|
+
debug
|
|
27493
|
+
});
|
|
27494
|
+
}
|
|
27495
|
+
async function resetClone2(params) {
|
|
27496
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
27497
|
+
if (!cloneId)
|
|
27498
|
+
throw new Error("cloneId is required");
|
|
27499
|
+
const data = { latest: latest ?? !snapshotId };
|
|
27500
|
+
if (snapshotId)
|
|
27501
|
+
data.snapshotID = snapshotId;
|
|
27502
|
+
return callDblabApi2({
|
|
27503
|
+
apiKey,
|
|
27504
|
+
apiBaseUrl,
|
|
27505
|
+
instanceId,
|
|
27506
|
+
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
27507
|
+
method: "post",
|
|
27508
|
+
data,
|
|
27509
|
+
operation: "Failed to reset clone",
|
|
27510
|
+
debug
|
|
27511
|
+
});
|
|
27512
|
+
}
|
|
27513
|
+
async function destroyClone2(params) {
|
|
27514
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27515
|
+
if (!cloneId)
|
|
27516
|
+
throw new Error("cloneId is required");
|
|
27517
|
+
return callDblabApi2({
|
|
27518
|
+
apiKey,
|
|
27519
|
+
apiBaseUrl,
|
|
27520
|
+
instanceId,
|
|
27521
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27522
|
+
method: "delete",
|
|
27523
|
+
operation: "Failed to destroy clone",
|
|
27524
|
+
debug
|
|
27525
|
+
});
|
|
27526
|
+
}
|
|
27527
|
+
async function listBranches2(params) {
|
|
27528
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27529
|
+
return callDblabApi2({
|
|
27530
|
+
apiKey,
|
|
27531
|
+
apiBaseUrl,
|
|
27532
|
+
instanceId,
|
|
27533
|
+
action: "/branches",
|
|
27534
|
+
method: "get",
|
|
27535
|
+
operation: "Failed to list branches",
|
|
27536
|
+
debug
|
|
27537
|
+
});
|
|
27538
|
+
}
|
|
27539
|
+
async function createBranch2(params) {
|
|
27540
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
27541
|
+
if (!branchName)
|
|
27542
|
+
throw new Error("branchName is required");
|
|
27543
|
+
const data = { branchName };
|
|
27544
|
+
if (baseBranch)
|
|
27545
|
+
data.baseBranch = baseBranch;
|
|
27546
|
+
if (snapshotId)
|
|
27547
|
+
data.snapshotID = snapshotId;
|
|
27548
|
+
return callDblabApi2({
|
|
27549
|
+
apiKey,
|
|
27550
|
+
apiBaseUrl,
|
|
27551
|
+
instanceId,
|
|
27552
|
+
action: "/branch",
|
|
27553
|
+
method: "post",
|
|
27554
|
+
data,
|
|
27555
|
+
operation: "Failed to create branch",
|
|
27556
|
+
debug
|
|
27557
|
+
});
|
|
27558
|
+
}
|
|
27559
|
+
async function deleteBranch2(params) {
|
|
27560
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27561
|
+
if (!branchName)
|
|
27562
|
+
throw new Error("branchName is required");
|
|
27563
|
+
return callDblabApi2({
|
|
27564
|
+
apiKey,
|
|
27565
|
+
apiBaseUrl,
|
|
27566
|
+
instanceId,
|
|
27567
|
+
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
27568
|
+
method: "delete",
|
|
27569
|
+
operation: "Failed to delete branch",
|
|
27570
|
+
debug
|
|
27571
|
+
});
|
|
27572
|
+
}
|
|
27573
|
+
async function branchLog2(params) {
|
|
27574
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27575
|
+
if (!branchName)
|
|
27576
|
+
throw new Error("branchName is required");
|
|
27577
|
+
return callDblabApi2({
|
|
27578
|
+
apiKey,
|
|
27579
|
+
apiBaseUrl,
|
|
27580
|
+
instanceId,
|
|
27581
|
+
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
27582
|
+
method: "get",
|
|
27583
|
+
operation: "Failed to fetch branch log",
|
|
27584
|
+
debug
|
|
27585
|
+
});
|
|
27586
|
+
}
|
|
27587
|
+
async function listSnapshots2(params) {
|
|
27588
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
27589
|
+
const qs = new URLSearchParams;
|
|
27590
|
+
const branch = branchName?.trim();
|
|
27591
|
+
if (branch)
|
|
27592
|
+
qs.append("branch", branch);
|
|
27593
|
+
if (dataset)
|
|
27594
|
+
qs.append("dataset", dataset);
|
|
27595
|
+
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
27596
|
+
return callDblabApi2({
|
|
27597
|
+
apiKey,
|
|
27598
|
+
apiBaseUrl,
|
|
27599
|
+
instanceId,
|
|
27600
|
+
action,
|
|
27601
|
+
method: "get",
|
|
27602
|
+
operation: "Failed to list snapshots",
|
|
27603
|
+
debug
|
|
27604
|
+
});
|
|
27605
|
+
}
|
|
27606
|
+
async function createSnapshot2(params) {
|
|
27607
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
27608
|
+
if (!cloneId)
|
|
27609
|
+
throw new Error("cloneId is required");
|
|
27610
|
+
const data = { cloneID: cloneId };
|
|
27611
|
+
if (message)
|
|
27612
|
+
data.message = message;
|
|
27613
|
+
return callDblabApi2({
|
|
27614
|
+
apiKey,
|
|
27615
|
+
apiBaseUrl,
|
|
27616
|
+
instanceId,
|
|
27617
|
+
action: "/branch/snapshot",
|
|
27618
|
+
method: "post",
|
|
27619
|
+
data,
|
|
27620
|
+
operation: "Failed to create snapshot",
|
|
27621
|
+
debug
|
|
27622
|
+
});
|
|
27623
|
+
}
|
|
27624
|
+
async function destroySnapshot2(params) {
|
|
27625
|
+
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
27626
|
+
if (!snapshotId)
|
|
27627
|
+
throw new Error("snapshotId is required");
|
|
27628
|
+
const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
|
|
27629
|
+
return callDblabApi2({
|
|
27630
|
+
apiKey,
|
|
27631
|
+
apiBaseUrl,
|
|
27632
|
+
instanceId,
|
|
27633
|
+
action,
|
|
27634
|
+
method: "delete",
|
|
27635
|
+
operation: "Failed to destroy snapshot",
|
|
27636
|
+
debug
|
|
27637
|
+
});
|
|
27638
|
+
}
|
|
27639
|
+
|
|
25804
27640
|
// bin/postgres-ai.ts
|
|
25805
27641
|
init_util();
|
|
25806
27642
|
|
|
25807
27643
|
// lib/instances.ts
|
|
25808
|
-
import * as
|
|
27644
|
+
import * as fs6 from "fs";
|
|
25809
27645
|
|
|
25810
27646
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
25811
27647
|
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
@@ -28470,11 +30306,11 @@ class InstancesParseError extends Error {
|
|
|
28470
30306
|
}
|
|
28471
30307
|
}
|
|
28472
30308
|
function loadInstances(file) {
|
|
28473
|
-
if (!
|
|
30309
|
+
if (!fs6.existsSync(file))
|
|
28474
30310
|
return [];
|
|
28475
|
-
if (
|
|
30311
|
+
if (fs6.lstatSync(file).isDirectory())
|
|
28476
30312
|
return [];
|
|
28477
|
-
const text =
|
|
30313
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
28478
30314
|
if (text.trim() === "")
|
|
28479
30315
|
return [];
|
|
28480
30316
|
let parsed;
|
|
@@ -28652,8 +30488,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
28652
30488
|
}
|
|
28653
30489
|
|
|
28654
30490
|
// lib/storage.ts
|
|
28655
|
-
import * as
|
|
28656
|
-
import * as
|
|
30491
|
+
import * as fs7 from "fs";
|
|
30492
|
+
import * as path6 from "path";
|
|
28657
30493
|
var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28658
30494
|
var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28659
30495
|
var MIME_TYPES2 = {
|
|
@@ -28694,11 +30530,11 @@ async function uploadFile2(params) {
|
|
|
28694
30530
|
if (!filePath) {
|
|
28695
30531
|
throw new Error("filePath is required");
|
|
28696
30532
|
}
|
|
28697
|
-
const resolvedPath =
|
|
28698
|
-
if (!
|
|
30533
|
+
const resolvedPath = path6.resolve(filePath);
|
|
30534
|
+
if (!fs7.existsSync(resolvedPath)) {
|
|
28699
30535
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
28700
30536
|
}
|
|
28701
|
-
const stat =
|
|
30537
|
+
const stat = fs7.statSync(resolvedPath);
|
|
28702
30538
|
if (!stat.isFile()) {
|
|
28703
30539
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
28704
30540
|
}
|
|
@@ -28710,9 +30546,9 @@ async function uploadFile2(params) {
|
|
|
28710
30546
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
28711
30547
|
}
|
|
28712
30548
|
const url = `${base}/upload`;
|
|
28713
|
-
const fileBuffer =
|
|
28714
|
-
const fileName =
|
|
28715
|
-
const ext =
|
|
30549
|
+
const fileBuffer = fs7.readFileSync(resolvedPath);
|
|
30550
|
+
const fileName = path6.basename(resolvedPath);
|
|
30551
|
+
const ext = path6.extname(fileName).toLowerCase();
|
|
28716
30552
|
const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
|
|
28717
30553
|
const formData = new FormData;
|
|
28718
30554
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -28771,15 +30607,15 @@ async function downloadFile2(params) {
|
|
|
28771
30607
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28772
30608
|
fullUrl = `${base}${relativePath}`;
|
|
28773
30609
|
}
|
|
28774
|
-
const urlFilename =
|
|
30610
|
+
const urlFilename = path6.basename(new URL(fullUrl).pathname);
|
|
28775
30611
|
if (!urlFilename) {
|
|
28776
30612
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
28777
30613
|
}
|
|
28778
|
-
const saveTo = outputPath ?
|
|
30614
|
+
const saveTo = outputPath ? path6.resolve(outputPath) : path6.resolve(urlFilename);
|
|
28779
30615
|
if (!outputPath) {
|
|
28780
|
-
const normalizedSave =
|
|
28781
|
-
const cwd =
|
|
28782
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
30616
|
+
const normalizedSave = path6.normalize(saveTo);
|
|
30617
|
+
const cwd = path6.normalize(process.cwd());
|
|
30618
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path6.sep)) {
|
|
28783
30619
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
28784
30620
|
}
|
|
28785
30621
|
}
|
|
@@ -28811,11 +30647,11 @@ async function downloadFile2(params) {
|
|
|
28811
30647
|
}
|
|
28812
30648
|
const arrayBuffer = await response.arrayBuffer();
|
|
28813
30649
|
const buffer = Buffer.from(arrayBuffer);
|
|
28814
|
-
const parentDir =
|
|
28815
|
-
if (!
|
|
28816
|
-
|
|
30650
|
+
const parentDir = path6.dirname(saveTo);
|
|
30651
|
+
if (!fs7.existsSync(parentDir)) {
|
|
30652
|
+
fs7.mkdirSync(parentDir, { recursive: true });
|
|
28817
30653
|
}
|
|
28818
|
-
|
|
30654
|
+
fs7.writeFileSync(saveTo, buffer);
|
|
28819
30655
|
return {
|
|
28820
30656
|
savedTo: saveTo,
|
|
28821
30657
|
size: buffer.length,
|
|
@@ -28827,9 +30663,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
|
|
|
28827
30663
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
28828
30664
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28829
30665
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
28830
|
-
const name = filename ||
|
|
30666
|
+
const name = filename || path6.basename(new URL(fullUrl).pathname);
|
|
28831
30667
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
28832
|
-
const ext =
|
|
30668
|
+
const ext = path6.extname(name).toLowerCase();
|
|
28833
30669
|
if (IMAGE_EXTENSIONS2.has(ext)) {
|
|
28834
30670
|
return ``;
|
|
28835
30671
|
}
|
|
@@ -28875,8 +30711,8 @@ ${links}`;
|
|
|
28875
30711
|
// lib/init.ts
|
|
28876
30712
|
import { randomBytes } from "crypto";
|
|
28877
30713
|
import { URL as URL2, fileURLToPath } from "url";
|
|
28878
|
-
import * as
|
|
28879
|
-
import * as
|
|
30714
|
+
import * as fs8 from "fs";
|
|
30715
|
+
import * as path7 from "path";
|
|
28880
30716
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
28881
30717
|
var KNOWN_PROVIDERS = ["self-managed", "supabase"];
|
|
28882
30718
|
var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
|
|
@@ -28968,21 +30804,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
|
28968
30804
|
}
|
|
28969
30805
|
function sqlDir() {
|
|
28970
30806
|
const currentFile = fileURLToPath(import.meta.url);
|
|
28971
|
-
const currentDir =
|
|
30807
|
+
const currentDir = path7.dirname(currentFile);
|
|
28972
30808
|
const candidates = [
|
|
28973
|
-
|
|
28974
|
-
|
|
30809
|
+
path7.resolve(currentDir, "..", "sql"),
|
|
30810
|
+
path7.resolve(currentDir, "..", "..", "sql")
|
|
28975
30811
|
];
|
|
28976
30812
|
for (const candidate of candidates) {
|
|
28977
|
-
if (
|
|
30813
|
+
if (fs8.existsSync(candidate)) {
|
|
28978
30814
|
return candidate;
|
|
28979
30815
|
}
|
|
28980
30816
|
}
|
|
28981
30817
|
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
28982
30818
|
}
|
|
28983
30819
|
function loadSqlTemplate(filename) {
|
|
28984
|
-
const p =
|
|
28985
|
-
return
|
|
30820
|
+
const p = path7.join(sqlDir(), filename);
|
|
30821
|
+
return fs8.readFileSync(p, "utf8");
|
|
28986
30822
|
}
|
|
28987
30823
|
function applyTemplate(sql, vars) {
|
|
28988
30824
|
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
@@ -29452,10 +31288,6 @@ async function verifyInitSetup(params) {
|
|
|
29452
31288
|
}
|
|
29453
31289
|
}
|
|
29454
31290
|
}
|
|
29455
|
-
const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
|
|
29456
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29457
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29458
|
-
}
|
|
29459
31291
|
const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
|
|
29460
31292
|
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
29461
31293
|
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
@@ -29974,15 +31806,6 @@ async function verifyInitSetupViaSupabase(params) {
|
|
|
29974
31806
|
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
29975
31807
|
}
|
|
29976
31808
|
}
|
|
29977
|
-
const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29978
|
-
if (explainFnExistsRes.rowCount === 0) {
|
|
29979
|
-
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
29980
|
-
} else {
|
|
29981
|
-
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
29982
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
29983
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
29984
|
-
}
|
|
29985
|
-
}
|
|
29986
31809
|
const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
|
|
29987
31810
|
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
29988
31811
|
missingRequired.push("function postgres_ai.table_describe exists");
|
|
@@ -30040,9 +31863,9 @@ function escapeLiteral2(value) {
|
|
|
30040
31863
|
}
|
|
30041
31864
|
|
|
30042
31865
|
// lib/pkce.ts
|
|
30043
|
-
import * as
|
|
31866
|
+
import * as crypto3 from "crypto";
|
|
30044
31867
|
function generateRandomString(length = 64) {
|
|
30045
|
-
const bytes =
|
|
31868
|
+
const bytes = crypto3.randomBytes(length);
|
|
30046
31869
|
return base64URLEncode(bytes);
|
|
30047
31870
|
}
|
|
30048
31871
|
function base64URLEncode(buffer) {
|
|
@@ -30052,7 +31875,7 @@ function generateCodeVerifier() {
|
|
|
30052
31875
|
return generateRandomString(32);
|
|
30053
31876
|
}
|
|
30054
31877
|
function generateCodeChallenge(verifier) {
|
|
30055
|
-
const hash =
|
|
31878
|
+
const hash = crypto3.createHash("sha256").update(verifier).digest();
|
|
30056
31879
|
return base64URLEncode(hash);
|
|
30057
31880
|
}
|
|
30058
31881
|
function generateState() {
|
|
@@ -30281,8 +32104,8 @@ import { createInterface } from "readline";
|
|
|
30281
32104
|
import * as childProcess from "child_process";
|
|
30282
32105
|
|
|
30283
32106
|
// lib/checkup.ts
|
|
30284
|
-
import * as
|
|
30285
|
-
import * as
|
|
32107
|
+
import * as fs9 from "fs";
|
|
32108
|
+
import * as path8 from "path";
|
|
30286
32109
|
|
|
30287
32110
|
// lib/metrics-embedded.ts
|
|
30288
32111
|
var METRICS = {
|
|
@@ -32013,7 +33836,7 @@ function buildCheckInfoMap() {
|
|
|
32013
33836
|
}
|
|
32014
33837
|
|
|
32015
33838
|
// lib/checkup.ts
|
|
32016
|
-
var __dirname = "/
|
|
33839
|
+
var __dirname = "/home/agent/workspace/postgresai-joe-v2/cli/lib";
|
|
32017
33840
|
var SECONDS_PER_DAY = 86400;
|
|
32018
33841
|
var SECONDS_PER_HOUR = 3600;
|
|
32019
33842
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -32499,7 +34322,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
|
|
|
32499
34322
|
}
|
|
32500
34323
|
function readTextFileSafe(p) {
|
|
32501
34324
|
try {
|
|
32502
|
-
const value =
|
|
34325
|
+
const value = fs9.readFileSync(p, "utf8").trim();
|
|
32503
34326
|
return value || null;
|
|
32504
34327
|
} catch {
|
|
32505
34328
|
return null;
|
|
@@ -32512,8 +34335,8 @@ function resolveBuildTs() {
|
|
|
32512
34335
|
if (fromFile)
|
|
32513
34336
|
return fromFile;
|
|
32514
34337
|
try {
|
|
32515
|
-
const pkgRoot =
|
|
32516
|
-
const fromPkgFile = readTextFileSafe(
|
|
34338
|
+
const pkgRoot = path8.resolve(__dirname, "..");
|
|
34339
|
+
const fromPkgFile = readTextFileSafe(path8.join(pkgRoot, "BUILD_TS"));
|
|
32517
34340
|
if (fromPkgFile)
|
|
32518
34341
|
return fromPkgFile;
|
|
32519
34342
|
} catch (err) {
|
|
@@ -32521,8 +34344,8 @@ function resolveBuildTs() {
|
|
|
32521
34344
|
console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
|
|
32522
34345
|
}
|
|
32523
34346
|
try {
|
|
32524
|
-
const pkgJsonPath =
|
|
32525
|
-
const st =
|
|
34347
|
+
const pkgJsonPath = path8.resolve(__dirname, "..", "package.json");
|
|
34348
|
+
const st = fs9.statSync(pkgJsonPath);
|
|
32526
34349
|
return st.mtime.toISOString();
|
|
32527
34350
|
} catch (err) {
|
|
32528
34351
|
if (process.env.DEBUG) {
|
|
@@ -33803,7 +35626,7 @@ function summarizeG003(nodeData) {
|
|
|
33803
35626
|
}
|
|
33804
35627
|
|
|
33805
35628
|
// lib/instances.ts
|
|
33806
|
-
import * as
|
|
35629
|
+
import * as fs10 from "fs";
|
|
33807
35630
|
class InstancesParseError2 extends Error {
|
|
33808
35631
|
constructor(file, cause) {
|
|
33809
35632
|
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -33812,11 +35635,11 @@ class InstancesParseError2 extends Error {
|
|
|
33812
35635
|
}
|
|
33813
35636
|
}
|
|
33814
35637
|
function loadInstances2(file) {
|
|
33815
|
-
if (!
|
|
35638
|
+
if (!fs10.existsSync(file))
|
|
33816
35639
|
return [];
|
|
33817
|
-
if (
|
|
35640
|
+
if (fs10.lstatSync(file).isDirectory())
|
|
33818
35641
|
return [];
|
|
33819
|
-
const text =
|
|
35642
|
+
const text = fs10.readFileSync(file, "utf8");
|
|
33820
35643
|
if (text.trim() === "")
|
|
33821
35644
|
return [];
|
|
33822
35645
|
let parsed;
|
|
@@ -33849,22 +35672,22 @@ function buildInstance(name, connStr) {
|
|
|
33849
35672
|
};
|
|
33850
35673
|
}
|
|
33851
35674
|
function addInstanceToFile(file, instance) {
|
|
33852
|
-
if (
|
|
33853
|
-
|
|
35675
|
+
if (fs10.existsSync(file) && fs10.lstatSync(file).isDirectory()) {
|
|
35676
|
+
fs10.rmSync(file, { recursive: true, force: true });
|
|
33854
35677
|
}
|
|
33855
35678
|
const existing = loadInstances2(file);
|
|
33856
35679
|
if (existing.some((i3) => i3.name === instance.name)) {
|
|
33857
35680
|
throw new Error(`Monitoring target '${instance.name}' already exists`);
|
|
33858
35681
|
}
|
|
33859
35682
|
existing.push(instance);
|
|
33860
|
-
|
|
35683
|
+
fs10.writeFileSync(file, dump2(existing), "utf8");
|
|
33861
35684
|
}
|
|
33862
35685
|
function removeInstanceFromFile(file, name) {
|
|
33863
35686
|
const instances = loadInstances2(file);
|
|
33864
35687
|
const filtered = instances.filter((i3) => i3.name !== name);
|
|
33865
35688
|
if (filtered.length === instances.length)
|
|
33866
35689
|
return false;
|
|
33867
|
-
|
|
35690
|
+
fs10.writeFileSync(file, dump2(filtered), "utf8");
|
|
33868
35691
|
return true;
|
|
33869
35692
|
}
|
|
33870
35693
|
function extractSslmode(connStr) {
|
|
@@ -33959,13 +35782,13 @@ function stripMatchingQuotes(value) {
|
|
|
33959
35782
|
return trimmed;
|
|
33960
35783
|
}
|
|
33961
35784
|
var REQUIRED_ENV_KEYS = [
|
|
33962
|
-
{ key: "REPLICATOR_PASSWORD", defaultValue: () =>
|
|
35785
|
+
{ key: "REPLICATOR_PASSWORD", defaultValue: () => crypto4.randomBytes(32).toString("hex"), introducedIn: "0.13" },
|
|
33963
35786
|
{ key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
|
|
33964
|
-
{ key: "VM_AUTH_PASSWORD", defaultValue: () =>
|
|
35787
|
+
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto4.randomBytes(18).toString("base64"), introducedIn: "0.15" }
|
|
33965
35788
|
];
|
|
33966
35789
|
function ensureRequiredEnvVars(projectDir) {
|
|
33967
|
-
const envFile =
|
|
33968
|
-
const existing =
|
|
35790
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35791
|
+
const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
|
|
33969
35792
|
const added = [];
|
|
33970
35793
|
const appendLines = [];
|
|
33971
35794
|
for (const spec of REQUIRED_ENV_KEYS) {
|
|
@@ -33984,7 +35807,7 @@ function ensureRequiredEnvVars(projectDir) {
|
|
|
33984
35807
|
` : "") + appendLines.join(`
|
|
33985
35808
|
`) + `
|
|
33986
35809
|
`;
|
|
33987
|
-
|
|
35810
|
+
fs11.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
|
|
33988
35811
|
return added;
|
|
33989
35812
|
}
|
|
33990
35813
|
async function execFilePromise(file, args) {
|
|
@@ -34049,7 +35872,7 @@ function expandHomePath(p) {
|
|
|
34049
35872
|
if (s === "~")
|
|
34050
35873
|
return os3.homedir();
|
|
34051
35874
|
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
34052
|
-
return
|
|
35875
|
+
return path9.join(os3.homedir(), s.slice(2));
|
|
34053
35876
|
}
|
|
34054
35877
|
return s;
|
|
34055
35878
|
}
|
|
@@ -34098,10 +35921,10 @@ function prepareOutputDirectory(outputOpt) {
|
|
|
34098
35921
|
if (!outputOpt)
|
|
34099
35922
|
return;
|
|
34100
35923
|
const outputDir = expandHomePath(outputOpt);
|
|
34101
|
-
const outputPath =
|
|
34102
|
-
if (!
|
|
35924
|
+
const outputPath = path9.isAbsolute(outputDir) ? outputDir : path9.resolve(process.cwd(), outputDir);
|
|
35925
|
+
if (!fs11.existsSync(outputPath)) {
|
|
34103
35926
|
try {
|
|
34104
|
-
|
|
35927
|
+
fs11.mkdirSync(outputPath, { recursive: true });
|
|
34105
35928
|
} catch (e) {
|
|
34106
35929
|
const errAny = e;
|
|
34107
35930
|
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
@@ -34137,7 +35960,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
34137
35960
|
let project = (opts.project || cfg.defaultProject || "").trim();
|
|
34138
35961
|
let projectWasGenerated = false;
|
|
34139
35962
|
if (!project) {
|
|
34140
|
-
project = `project_${
|
|
35963
|
+
project = `project_${crypto4.randomBytes(6).toString("hex")}`;
|
|
34141
35964
|
projectWasGenerated = true;
|
|
34142
35965
|
try {
|
|
34143
35966
|
writeConfig({ defaultProject: project });
|
|
@@ -34183,8 +36006,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
34183
36006
|
}
|
|
34184
36007
|
function writeReportFiles(reports, outputPath) {
|
|
34185
36008
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
34186
|
-
const filePath =
|
|
34187
|
-
|
|
36009
|
+
const filePath = path9.join(outputPath, `${checkId}.json`);
|
|
36010
|
+
fs11.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
34188
36011
|
const title = report.checkTitle || checkId;
|
|
34189
36012
|
console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
|
|
34190
36013
|
}
|
|
@@ -34229,7 +36052,7 @@ function getDefaultMonitoringProjectDir() {
|
|
|
34229
36052
|
const override = process.env.PGAI_PROJECT_DIR;
|
|
34230
36053
|
if (override && override.trim())
|
|
34231
36054
|
return override.trim();
|
|
34232
|
-
return
|
|
36055
|
+
return path9.join(getConfigDir(), "monitoring");
|
|
34233
36056
|
}
|
|
34234
36057
|
async function downloadText(url) {
|
|
34235
36058
|
const controller = new AbortController;
|
|
@@ -34246,12 +36069,12 @@ async function downloadText(url) {
|
|
|
34246
36069
|
}
|
|
34247
36070
|
async function ensureDefaultMonitoringProject() {
|
|
34248
36071
|
const projectDir = getDefaultMonitoringProjectDir();
|
|
34249
|
-
const composeFile =
|
|
34250
|
-
const instancesFile =
|
|
34251
|
-
if (!
|
|
34252
|
-
|
|
36072
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36073
|
+
const instancesFile = path9.resolve(projectDir, "instances.yml");
|
|
36074
|
+
if (!fs11.existsSync(projectDir)) {
|
|
36075
|
+
fs11.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
34253
36076
|
}
|
|
34254
|
-
if (!
|
|
36077
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34255
36078
|
const refs = [
|
|
34256
36079
|
process.env.PGAI_PROJECT_REF,
|
|
34257
36080
|
package_default.version,
|
|
@@ -34263,39 +36086,39 @@ async function ensureDefaultMonitoringProject() {
|
|
|
34263
36086
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
34264
36087
|
try {
|
|
34265
36088
|
const text = await downloadText(url);
|
|
34266
|
-
|
|
36089
|
+
fs11.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
|
|
34267
36090
|
break;
|
|
34268
36091
|
} catch (err) {
|
|
34269
36092
|
lastErr = err;
|
|
34270
36093
|
}
|
|
34271
36094
|
}
|
|
34272
|
-
if (!
|
|
36095
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34273
36096
|
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
34274
36097
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
34275
36098
|
}
|
|
34276
36099
|
}
|
|
34277
|
-
if (
|
|
34278
|
-
|
|
36100
|
+
if (fs11.existsSync(instancesFile) && fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36101
|
+
fs11.rmSync(instancesFile, { recursive: true, force: true });
|
|
34279
36102
|
}
|
|
34280
|
-
if (!
|
|
36103
|
+
if (!fs11.existsSync(instancesFile)) {
|
|
34281
36104
|
const header = `# PostgreSQL instances to monitor
|
|
34282
36105
|
` + `# Add your instances using: pgai mon targets add <connection-string> <name>
|
|
34283
36106
|
|
|
34284
36107
|
`;
|
|
34285
|
-
|
|
36108
|
+
fs11.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
|
|
34286
36109
|
}
|
|
34287
|
-
const pgwatchConfig =
|
|
34288
|
-
if (!
|
|
34289
|
-
|
|
36110
|
+
const pgwatchConfig = path9.resolve(projectDir, ".pgwatch-config");
|
|
36111
|
+
if (!fs11.existsSync(pgwatchConfig)) {
|
|
36112
|
+
fs11.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
|
|
34290
36113
|
}
|
|
34291
|
-
const envFile =
|
|
34292
|
-
if (!
|
|
36114
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36115
|
+
if (!fs11.existsSync(envFile)) {
|
|
34293
36116
|
const envText = `PGAI_TAG=${package_default.version}
|
|
34294
36117
|
# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
|
|
34295
36118
|
`;
|
|
34296
|
-
|
|
36119
|
+
fs11.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
|
|
34297
36120
|
}
|
|
34298
|
-
return { fs:
|
|
36121
|
+
return { fs: fs11, path: path9, projectDir, composeFile, instancesFile };
|
|
34299
36122
|
}
|
|
34300
36123
|
function sanitizeTagForBackup(tag) {
|
|
34301
36124
|
if (tag == null)
|
|
@@ -34304,10 +36127,10 @@ function sanitizeTagForBackup(tag) {
|
|
|
34304
36127
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
34305
36128
|
}
|
|
34306
36129
|
function readDeployedTag(projectDir) {
|
|
34307
|
-
const envFile =
|
|
34308
|
-
if (!
|
|
36130
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36131
|
+
if (!fs11.existsSync(envFile))
|
|
34309
36132
|
return null;
|
|
34310
|
-
const m =
|
|
36133
|
+
const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
34311
36134
|
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
34312
36135
|
}
|
|
34313
36136
|
function isValidComposeYaml(text) {
|
|
@@ -34344,10 +36167,10 @@ function composeContentEqual(a, b) {
|
|
|
34344
36167
|
return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
|
|
34345
36168
|
}
|
|
34346
36169
|
async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
34347
|
-
if (
|
|
36170
|
+
if (fs11.existsSync(path9.resolve(projectDir, ".git")))
|
|
34348
36171
|
return false;
|
|
34349
|
-
const composeFile =
|
|
34350
|
-
if (!
|
|
36172
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36173
|
+
if (!fs11.existsSync(composeFile))
|
|
34351
36174
|
return false;
|
|
34352
36175
|
const refs = [
|
|
34353
36176
|
process.env.PGAI_PROJECT_REF,
|
|
@@ -34360,24 +36183,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
34360
36183
|
console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
|
|
34361
36184
|
return false;
|
|
34362
36185
|
}
|
|
34363
|
-
const existing =
|
|
36186
|
+
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
34364
36187
|
if (composeContentEqual(existing, fetched))
|
|
34365
36188
|
return false;
|
|
34366
36189
|
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
34367
36190
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
34368
|
-
const oldHash =
|
|
34369
|
-
const backup =
|
|
36191
|
+
const oldHash = crypto4.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
36192
|
+
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
34370
36193
|
let backupName = null;
|
|
34371
36194
|
try {
|
|
34372
|
-
|
|
34373
|
-
backupName =
|
|
36195
|
+
fs11.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
36196
|
+
backupName = path9.basename(backup);
|
|
34374
36197
|
} catch (err) {
|
|
34375
36198
|
const e = err;
|
|
34376
36199
|
if (e && e.code === "EEXIST") {
|
|
34377
|
-
backupName =
|
|
36200
|
+
backupName = path9.basename(backup);
|
|
34378
36201
|
}
|
|
34379
36202
|
}
|
|
34380
|
-
|
|
36203
|
+
fs11.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
|
|
34381
36204
|
const backupNote = backupName ? ` (backup: ${backupName})` : "";
|
|
34382
36205
|
console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
|
|
34383
36206
|
return true;
|
|
@@ -35611,12 +37434,12 @@ function resolvePaths() {
|
|
|
35611
37434
|
const startDir = process.cwd();
|
|
35612
37435
|
let currentDir = startDir;
|
|
35613
37436
|
while (true) {
|
|
35614
|
-
const composeFile =
|
|
35615
|
-
if (
|
|
35616
|
-
const instancesFile =
|
|
35617
|
-
return { fs:
|
|
37437
|
+
const composeFile = path9.resolve(currentDir, "docker-compose.yml");
|
|
37438
|
+
if (fs11.existsSync(composeFile)) {
|
|
37439
|
+
const instancesFile = path9.resolve(currentDir, "instances.yml");
|
|
37440
|
+
return { fs: fs11, path: path9, projectDir: currentDir, composeFile, instancesFile };
|
|
35618
37441
|
}
|
|
35619
|
-
const parentDir =
|
|
37442
|
+
const parentDir = path9.dirname(currentDir);
|
|
35620
37443
|
if (parentDir === currentDir)
|
|
35621
37444
|
break;
|
|
35622
37445
|
currentDir = parentDir;
|
|
@@ -35659,6 +37482,16 @@ function checkRunningContainers() {
|
|
|
35659
37482
|
return { running: false, containers: [] };
|
|
35660
37483
|
}
|
|
35661
37484
|
}
|
|
37485
|
+
function planMonitoringRegistration(args) {
|
|
37486
|
+
const projectName = args.project?.trim() || undefined;
|
|
37487
|
+
if (args.instanceId) {
|
|
37488
|
+
return { kind: "adopt", projectName };
|
|
37489
|
+
}
|
|
37490
|
+
if (!projectName) {
|
|
37491
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
37492
|
+
}
|
|
37493
|
+
return { kind: "self-register", projectName };
|
|
37494
|
+
}
|
|
35662
37495
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
35663
37496
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
35664
37497
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -35666,16 +37499,19 @@ async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
|
35666
37499
|
const instanceId = opts?.instanceId;
|
|
35667
37500
|
const retries = opts?.retries ?? (instanceId ? 1 : 0);
|
|
35668
37501
|
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
37502
|
+
const hasProjectName = !!(projectName && projectName.trim());
|
|
35669
37503
|
if (debug) {
|
|
35670
37504
|
console.error(`
|
|
35671
37505
|
Debug: Registering monitoring instance...`);
|
|
35672
37506
|
console.error(`Debug: POST ${url}`);
|
|
35673
|
-
console.error(`Debug: project_name=${projectName}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
37507
|
+
console.error(`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
35674
37508
|
}
|
|
35675
37509
|
const requestBody = {
|
|
35676
|
-
api_token: apiKey
|
|
35677
|
-
project_name: projectName
|
|
37510
|
+
api_token: apiKey
|
|
35678
37511
|
};
|
|
37512
|
+
if (hasProjectName) {
|
|
37513
|
+
requestBody.project_name = projectName;
|
|
37514
|
+
}
|
|
35679
37515
|
if (instanceId) {
|
|
35680
37516
|
requestBody.instance_id = instanceId;
|
|
35681
37517
|
}
|
|
@@ -35735,10 +37571,10 @@ function resolveAdoptedProject(reg) {
|
|
|
35735
37571
|
}
|
|
35736
37572
|
function updatePgwatchConfig(configPath, updates) {
|
|
35737
37573
|
let lines = [];
|
|
35738
|
-
if (
|
|
35739
|
-
const stats =
|
|
37574
|
+
if (fs11.existsSync(configPath)) {
|
|
37575
|
+
const stats = fs11.statSync(configPath);
|
|
35740
37576
|
if (!stats.isDirectory()) {
|
|
35741
|
-
const content =
|
|
37577
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
35742
37578
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
35743
37579
|
}
|
|
35744
37580
|
}
|
|
@@ -35750,7 +37586,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
35750
37586
|
lines.push(`${key}=${value}`);
|
|
35751
37587
|
}
|
|
35752
37588
|
}
|
|
35753
|
-
|
|
37589
|
+
fs11.writeFileSync(configPath, lines.join(`
|
|
35754
37590
|
`) + `
|
|
35755
37591
|
`, { encoding: "utf8", mode: 384 });
|
|
35756
37592
|
}
|
|
@@ -35788,12 +37624,12 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35788
37624
|
if (grafanaPassword) {
|
|
35789
37625
|
env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
|
|
35790
37626
|
} else {
|
|
35791
|
-
const cfgPath =
|
|
35792
|
-
if (
|
|
37627
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37628
|
+
if (fs11.existsSync(cfgPath)) {
|
|
35793
37629
|
try {
|
|
35794
|
-
const stats =
|
|
37630
|
+
const stats = fs11.statSync(cfgPath);
|
|
35795
37631
|
if (!stats.isDirectory()) {
|
|
35796
|
-
const content =
|
|
37632
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
35797
37633
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
35798
37634
|
if (match) {
|
|
35799
37635
|
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
@@ -35806,10 +37642,10 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35806
37642
|
}
|
|
35807
37643
|
}
|
|
35808
37644
|
}
|
|
35809
|
-
const envFilePath =
|
|
35810
|
-
if (
|
|
37645
|
+
const envFilePath = path9.resolve(projectDir, ".env");
|
|
37646
|
+
if (fs11.existsSync(envFilePath)) {
|
|
35811
37647
|
try {
|
|
35812
|
-
const envContent =
|
|
37648
|
+
const envContent = fs11.readFileSync(envFilePath, "utf8");
|
|
35813
37649
|
if (!env.VM_AUTH_USERNAME) {
|
|
35814
37650
|
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
35815
37651
|
if (m)
|
|
@@ -35866,20 +37702,20 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35866
37702
|
console.log(`Project directory: ${projectDir}
|
|
35867
37703
|
`);
|
|
35868
37704
|
if (opts.project) {
|
|
35869
|
-
const cfgPath2 =
|
|
37705
|
+
const cfgPath2 = path9.resolve(projectDir, ".pgwatch-config");
|
|
35870
37706
|
updatePgwatchConfig(cfgPath2, { project_name: opts.project });
|
|
35871
37707
|
console.log(`Using project name: ${opts.project}
|
|
35872
37708
|
`);
|
|
35873
37709
|
}
|
|
35874
|
-
const envFile =
|
|
37710
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35875
37711
|
let existingRegistry = null;
|
|
35876
37712
|
let existingPassword = null;
|
|
35877
37713
|
let existingReplicatorPassword = null;
|
|
35878
37714
|
let existingVmAuthUsername = null;
|
|
35879
37715
|
let existingVmAuthPassword = null;
|
|
35880
37716
|
let previousTag = null;
|
|
35881
|
-
if (
|
|
35882
|
-
const existingEnv =
|
|
37717
|
+
if (fs11.existsSync(envFile)) {
|
|
37718
|
+
const existingEnv = fs11.readFileSync(envFile, "utf8");
|
|
35883
37719
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
35884
37720
|
if (previousTagMatch)
|
|
35885
37721
|
previousTag = previousTagMatch[1].trim();
|
|
@@ -35907,10 +37743,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35907
37743
|
if (existingPassword) {
|
|
35908
37744
|
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
35909
37745
|
}
|
|
35910
|
-
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword ||
|
|
37746
|
+
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto4.randomBytes(32).toString("hex")}`);
|
|
35911
37747
|
envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
|
|
35912
|
-
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword ||
|
|
35913
|
-
|
|
37748
|
+
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto4.randomBytes(18).toString("base64")}`);
|
|
37749
|
+
fs11.writeFileSync(envFile, envLines.join(`
|
|
35914
37750
|
`) + `
|
|
35915
37751
|
`, { encoding: "utf8", mode: 384 });
|
|
35916
37752
|
await refreshBundledComposeIfStale(projectDir, previousTag);
|
|
@@ -35947,7 +37783,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35947
37783
|
if (apiKey) {
|
|
35948
37784
|
console.log("Using API key provided via --api-key parameter");
|
|
35949
37785
|
writeConfig({ apiKey });
|
|
35950
|
-
updatePgwatchConfig(
|
|
37786
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
|
|
35951
37787
|
console.log(`\u2713 API key saved
|
|
35952
37788
|
`);
|
|
35953
37789
|
} else if (opts.yes) {
|
|
@@ -35964,7 +37800,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35964
37800
|
const trimmedKey = inputApiKey.trim();
|
|
35965
37801
|
if (trimmedKey) {
|
|
35966
37802
|
writeConfig({ apiKey: trimmedKey });
|
|
35967
|
-
updatePgwatchConfig(
|
|
37803
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
|
|
35968
37804
|
apiKey = trimmedKey;
|
|
35969
37805
|
console.log(`\u2713 API key saved
|
|
35970
37806
|
`);
|
|
@@ -35998,7 +37834,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35998
37834
|
# Add your instances using: postgres-ai mon targets add
|
|
35999
37835
|
|
|
36000
37836
|
`;
|
|
36001
|
-
|
|
37837
|
+
fs11.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
|
|
36002
37838
|
console.log(`Instances file: ${instancesPath2}`);
|
|
36003
37839
|
console.log(`Project directory: ${projectDir2}
|
|
36004
37840
|
`);
|
|
@@ -36102,17 +37938,17 @@ You can provide either:`);
|
|
|
36102
37938
|
}
|
|
36103
37939
|
} else {
|
|
36104
37940
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
36105
|
-
const currentDir =
|
|
37941
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
36106
37942
|
const demoCandidates = [
|
|
36107
|
-
|
|
36108
|
-
|
|
37943
|
+
path9.resolve(currentDir, "..", "..", "instances.demo.yml"),
|
|
37944
|
+
path9.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
|
|
36109
37945
|
];
|
|
36110
|
-
const demoSrc = demoCandidates.find((p) =>
|
|
37946
|
+
const demoSrc = demoCandidates.find((p) => fs11.existsSync(p));
|
|
36111
37947
|
if (demoSrc) {
|
|
36112
|
-
if (
|
|
36113
|
-
|
|
37948
|
+
if (fs11.existsSync(instancesPath) && fs11.lstatSync(instancesPath).isDirectory()) {
|
|
37949
|
+
fs11.rmSync(instancesPath, { recursive: true, force: true });
|
|
36114
37950
|
}
|
|
36115
|
-
|
|
37951
|
+
fs11.copyFileSync(demoSrc, instancesPath);
|
|
36116
37952
|
console.log(`\u2713 Demo monitoring target configured
|
|
36117
37953
|
`);
|
|
36118
37954
|
} else {
|
|
@@ -36131,15 +37967,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36131
37967
|
console.log(`\u2713 Configuration updated
|
|
36132
37968
|
`);
|
|
36133
37969
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
36134
|
-
const cfgPath =
|
|
37970
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
36135
37971
|
let grafanaPassword = "";
|
|
36136
37972
|
let vmAuthUsername = "";
|
|
36137
37973
|
let vmAuthPassword = "";
|
|
36138
37974
|
try {
|
|
36139
|
-
if (
|
|
36140
|
-
const stats =
|
|
37975
|
+
if (fs11.existsSync(cfgPath)) {
|
|
37976
|
+
const stats = fs11.statSync(cfgPath);
|
|
36141
37977
|
if (!stats.isDirectory()) {
|
|
36142
|
-
const content =
|
|
37978
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
36143
37979
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
36144
37980
|
if (match) {
|
|
36145
37981
|
grafanaPassword = match[1].trim();
|
|
@@ -36151,15 +37987,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36151
37987
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
36152
37988
|
grafanaPassword = password.trim().replace(/\n/g, "");
|
|
36153
37989
|
let configContent = "";
|
|
36154
|
-
if (
|
|
36155
|
-
const stats =
|
|
37990
|
+
if (fs11.existsSync(cfgPath)) {
|
|
37991
|
+
const stats = fs11.statSync(cfgPath);
|
|
36156
37992
|
if (!stats.isDirectory()) {
|
|
36157
|
-
configContent =
|
|
37993
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
36158
37994
|
}
|
|
36159
37995
|
}
|
|
36160
37996
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
36161
37997
|
lines.push(`grafana_password=${grafanaPassword}`);
|
|
36162
|
-
|
|
37998
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
36163
37999
|
`) + `
|
|
36164
38000
|
`, "utf8");
|
|
36165
38001
|
}
|
|
@@ -36172,9 +38008,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36172
38008
|
grafanaPassword = "demo";
|
|
36173
38009
|
}
|
|
36174
38010
|
try {
|
|
36175
|
-
const envFile2 =
|
|
36176
|
-
if (
|
|
36177
|
-
const envContent =
|
|
38011
|
+
const envFile2 = path9.resolve(projectDir, ".env");
|
|
38012
|
+
if (fs11.existsSync(envFile2)) {
|
|
38013
|
+
const envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36178
38014
|
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
36179
38015
|
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
36180
38016
|
if (userMatch)
|
|
@@ -36190,13 +38026,13 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36190
38026
|
vmAuthPassword = vmPass.trim().replace(/\n/g, "");
|
|
36191
38027
|
}
|
|
36192
38028
|
let envContent = "";
|
|
36193
|
-
if (
|
|
36194
|
-
envContent =
|
|
38029
|
+
if (fs11.existsSync(envFile2)) {
|
|
38030
|
+
envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36195
38031
|
}
|
|
36196
38032
|
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 === ""));
|
|
36197
38033
|
envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
36198
38034
|
envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
36199
|
-
|
|
38035
|
+
fs11.writeFileSync(envFile2, envLines2.join(`
|
|
36200
38036
|
`) + `
|
|
36201
38037
|
`, { encoding: "utf8", mode: 384 });
|
|
36202
38038
|
}
|
|
@@ -36218,8 +38054,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36218
38054
|
console.log(`\u2713 Services started
|
|
36219
38055
|
`);
|
|
36220
38056
|
if (apiKey && !opts.demo) {
|
|
36221
|
-
const projectName = opts.project || "postgres-ai-monitoring";
|
|
36222
38057
|
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
38058
|
+
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
38059
|
+
const projectName = plan.projectName;
|
|
36223
38060
|
if (instanceId) {
|
|
36224
38061
|
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
36225
38062
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36228,16 +38065,16 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36228
38065
|
});
|
|
36229
38066
|
const adoptedProject = resolveAdoptedProject(reg);
|
|
36230
38067
|
if (adoptedProject != null) {
|
|
36231
|
-
updatePgwatchConfig(
|
|
38068
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), {
|
|
36232
38069
|
project_name: adoptedProject
|
|
36233
38070
|
});
|
|
36234
38071
|
const verb = reg?.created ? "Registered" : "Adopted";
|
|
36235
38072
|
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
36236
38073
|
`);
|
|
36237
38074
|
} else if (reg) {
|
|
36238
|
-
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project \u2014 reports will use project '${projectName}'`);
|
|
38075
|
+
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
36239
38076
|
} else {
|
|
36240
|
-
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId} \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`);
|
|
38077
|
+
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
36241
38078
|
}
|
|
36242
38079
|
const aas = await registerAasCollection(apiKey, instanceId, {
|
|
36243
38080
|
grafanaPassword,
|
|
@@ -36253,6 +38090,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36253
38090
|
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
36254
38091
|
`);
|
|
36255
38092
|
}
|
|
38093
|
+
} else if (plan.kind === "error-missing-project") {
|
|
38094
|
+
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>.");
|
|
38095
|
+
process.exitCode = 1;
|
|
36256
38096
|
} else {
|
|
36257
38097
|
registerMonitoringInstance(apiKey, projectName, {
|
|
36258
38098
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -36441,11 +38281,11 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
36441
38281
|
console.log(`Project Directory: ${projectDir}`);
|
|
36442
38282
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
36443
38283
|
console.log(`Instances File: ${instancesFile}`);
|
|
36444
|
-
if (
|
|
38284
|
+
if (fs11.existsSync(instancesFile) && !fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36445
38285
|
console.log(`
|
|
36446
38286
|
Instances configuration:
|
|
36447
38287
|
`);
|
|
36448
|
-
const text =
|
|
38288
|
+
const text = fs11.readFileSync(instancesFile, "utf8");
|
|
36449
38289
|
process.stdout.write(text);
|
|
36450
38290
|
if (!/\n$/.test(text))
|
|
36451
38291
|
console.log();
|
|
@@ -36494,8 +38334,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
36494
38334
|
console.log("\u2713 .env is up to date");
|
|
36495
38335
|
}
|
|
36496
38336
|
console.log();
|
|
36497
|
-
const gitDir =
|
|
36498
|
-
if (
|
|
38337
|
+
const gitDir = path9.resolve(projectDir, ".git");
|
|
38338
|
+
if (fs11.existsSync(gitDir)) {
|
|
36499
38339
|
console.log("Fetching latest changes...");
|
|
36500
38340
|
await execFilePromise("git", ["fetch", "origin"]);
|
|
36501
38341
|
const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -36650,7 +38490,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
36650
38490
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
36651
38491
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
36652
38492
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
36653
|
-
if (!
|
|
38493
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36654
38494
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
36655
38495
|
process.exitCode = 1;
|
|
36656
38496
|
return;
|
|
@@ -36713,7 +38553,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
36713
38553
|
});
|
|
36714
38554
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
36715
38555
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
36716
|
-
if (!
|
|
38556
|
+
if (!fs11.existsSync(file) || fs11.lstatSync(file).isDirectory()) {
|
|
36717
38557
|
console.error("instances.yml not found");
|
|
36718
38558
|
process.exitCode = 1;
|
|
36719
38559
|
return;
|
|
@@ -36741,7 +38581,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
36741
38581
|
});
|
|
36742
38582
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
36743
38583
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
36744
|
-
if (!
|
|
38584
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36745
38585
|
console.error("instances.yml not found");
|
|
36746
38586
|
process.exitCode = 1;
|
|
36747
38587
|
return;
|
|
@@ -37003,15 +38843,15 @@ To authenticate, run: pgai auth`);
|
|
|
37003
38843
|
});
|
|
37004
38844
|
auth.command("remove-key").description("remove API key").action(async () => {
|
|
37005
38845
|
const newConfigPath = getConfigPath();
|
|
37006
|
-
const hasNewConfig =
|
|
38846
|
+
const hasNewConfig = fs11.existsSync(newConfigPath);
|
|
37007
38847
|
let legacyPath;
|
|
37008
38848
|
try {
|
|
37009
38849
|
const { projectDir } = await resolveOrInitPaths();
|
|
37010
|
-
legacyPath =
|
|
38850
|
+
legacyPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37011
38851
|
} catch {
|
|
37012
|
-
legacyPath =
|
|
38852
|
+
legacyPath = path9.resolve(process.cwd(), ".pgwatch-config");
|
|
37013
38853
|
}
|
|
37014
|
-
const hasLegacyConfig =
|
|
38854
|
+
const hasLegacyConfig = fs11.existsSync(legacyPath) && fs11.statSync(legacyPath).isFile();
|
|
37015
38855
|
if (!hasNewConfig && !hasLegacyConfig) {
|
|
37016
38856
|
console.log("No API key configured");
|
|
37017
38857
|
return;
|
|
@@ -37021,11 +38861,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
|
|
|
37021
38861
|
}
|
|
37022
38862
|
if (hasLegacyConfig) {
|
|
37023
38863
|
try {
|
|
37024
|
-
const content =
|
|
38864
|
+
const content = fs11.readFileSync(legacyPath, "utf8");
|
|
37025
38865
|
const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
|
|
37026
38866
|
`).replace(/\n+$/g, `
|
|
37027
38867
|
`);
|
|
37028
|
-
|
|
38868
|
+
fs11.writeFileSync(legacyPath, filtered, "utf8");
|
|
37029
38869
|
} catch (err) {
|
|
37030
38870
|
console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
|
|
37031
38871
|
}
|
|
@@ -37036,7 +38876,7 @@ To authenticate again, run: pgai auth`);
|
|
|
37036
38876
|
});
|
|
37037
38877
|
mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
|
|
37038
38878
|
const { projectDir } = await resolveOrInitPaths();
|
|
37039
|
-
const cfgPath =
|
|
38879
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37040
38880
|
try {
|
|
37041
38881
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
37042
38882
|
const newPassword = password.trim().replace(/\n/g, "");
|
|
@@ -37046,17 +38886,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
|
|
|
37046
38886
|
return;
|
|
37047
38887
|
}
|
|
37048
38888
|
let configContent = "";
|
|
37049
|
-
if (
|
|
37050
|
-
const stats =
|
|
38889
|
+
if (fs11.existsSync(cfgPath)) {
|
|
38890
|
+
const stats = fs11.statSync(cfgPath);
|
|
37051
38891
|
if (stats.isDirectory()) {
|
|
37052
38892
|
console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
|
|
37053
38893
|
} else {
|
|
37054
|
-
configContent =
|
|
38894
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
37055
38895
|
}
|
|
37056
38896
|
}
|
|
37057
38897
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
37058
38898
|
lines.push(`grafana_password=${newPassword}`);
|
|
37059
|
-
|
|
38899
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
37060
38900
|
`) + `
|
|
37061
38901
|
`, "utf8");
|
|
37062
38902
|
console.log("\u2713 New Grafana password generated and saved");
|
|
@@ -37078,19 +38918,19 @@ Note: This command requires 'openssl' to be installed`);
|
|
|
37078
38918
|
});
|
|
37079
38919
|
mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
|
|
37080
38920
|
const { projectDir } = await resolveOrInitPaths();
|
|
37081
|
-
const cfgPath =
|
|
37082
|
-
if (!
|
|
38921
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
38922
|
+
if (!fs11.existsSync(cfgPath)) {
|
|
37083
38923
|
console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
|
|
37084
38924
|
process.exitCode = 1;
|
|
37085
38925
|
return;
|
|
37086
38926
|
}
|
|
37087
|
-
const stats =
|
|
38927
|
+
const stats = fs11.statSync(cfgPath);
|
|
37088
38928
|
if (stats.isDirectory()) {
|
|
37089
38929
|
console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
|
|
37090
38930
|
process.exitCode = 1;
|
|
37091
38931
|
return;
|
|
37092
38932
|
}
|
|
37093
|
-
const content =
|
|
38933
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
37094
38934
|
const lines = content.split(/\r?\n/);
|
|
37095
38935
|
let password = "";
|
|
37096
38936
|
for (const line of lines) {
|
|
@@ -37110,9 +38950,9 @@ Grafana credentials:`);
|
|
|
37110
38950
|
console.log(" URL: http://localhost:3000");
|
|
37111
38951
|
console.log(" Username: monitor");
|
|
37112
38952
|
console.log(` Password: ${password}`);
|
|
37113
|
-
const envFile =
|
|
37114
|
-
if (
|
|
37115
|
-
const envContent =
|
|
38953
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
38954
|
+
if (fs11.existsSync(envFile)) {
|
|
38955
|
+
const envContent = fs11.readFileSync(envFile, "utf8");
|
|
37116
38956
|
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
37117
38957
|
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
37118
38958
|
if (vmUser && vmPass) {
|
|
@@ -37838,13 +39678,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
|
|
|
37838
39678
|
});
|
|
37839
39679
|
spinner.stop();
|
|
37840
39680
|
if (opts.output) {
|
|
37841
|
-
const dir =
|
|
37842
|
-
|
|
39681
|
+
const dir = path9.resolve(opts.output);
|
|
39682
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
37843
39683
|
for (const f of result) {
|
|
37844
|
-
const safeName =
|
|
37845
|
-
const filePath =
|
|
39684
|
+
const safeName = path9.basename(f.filename);
|
|
39685
|
+
const filePath = path9.join(dir, safeName);
|
|
37846
39686
|
const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
|
|
37847
|
-
|
|
39687
|
+
fs11.writeFileSync(filePath, content, "utf-8");
|
|
37848
39688
|
console.log(filePath);
|
|
37849
39689
|
}
|
|
37850
39690
|
} else if (opts.json) {
|
|
@@ -37889,6 +39729,406 @@ function tryParseJson(s) {
|
|
|
37889
39729
|
return s;
|
|
37890
39730
|
}
|
|
37891
39731
|
}
|
|
39732
|
+
function inferCommandFromResult(r) {
|
|
39733
|
+
if (r.command)
|
|
39734
|
+
return r.command;
|
|
39735
|
+
if (r.plan_text !== undefined || r.plan_json !== undefined)
|
|
39736
|
+
return "plan";
|
|
39737
|
+
if (r.row_count !== undefined || r.result_rows !== undefined)
|
|
39738
|
+
return "exec";
|
|
39739
|
+
if (r.hypo_used !== undefined)
|
|
39740
|
+
return "hypo";
|
|
39741
|
+
if (r.terminated !== undefined)
|
|
39742
|
+
return "terminate";
|
|
39743
|
+
if (r.reset !== undefined)
|
|
39744
|
+
return "reset";
|
|
39745
|
+
if (r.snapshot !== undefined)
|
|
39746
|
+
return "activity";
|
|
39747
|
+
return null;
|
|
39748
|
+
}
|
|
39749
|
+
function printJoeOutcome(command, outcome, json3) {
|
|
39750
|
+
const budgetSeconds = Math.round(DEFAULT_BUDGET_MS2 / 1000);
|
|
39751
|
+
if (outcome.budgetExpired) {
|
|
39752
|
+
if (json3) {
|
|
39753
|
+
console.log(JSON.stringify({
|
|
39754
|
+
command_id: outcome.commandId,
|
|
39755
|
+
status: outcome.status,
|
|
39756
|
+
session_id: outcome.sessionId,
|
|
39757
|
+
budget_expired: true,
|
|
39758
|
+
resume: `pgai result ${outcome.commandId}`
|
|
39759
|
+
}, null, 2));
|
|
39760
|
+
} else {
|
|
39761
|
+
console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai result ${outcome.commandId}`);
|
|
39762
|
+
}
|
|
39763
|
+
return;
|
|
39764
|
+
}
|
|
39765
|
+
const result = outcome.result;
|
|
39766
|
+
if (outcome.status === "done" && result) {
|
|
39767
|
+
if (json3) {
|
|
39768
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39769
|
+
} else {
|
|
39770
|
+
console.log(`submitted ${outcome.commandId} \xB7 done`);
|
|
39771
|
+
const body = formatJoeResult(command, result);
|
|
39772
|
+
if (body)
|
|
39773
|
+
console.log(body);
|
|
39774
|
+
}
|
|
39775
|
+
return;
|
|
39776
|
+
}
|
|
39777
|
+
if (outcome.status === "error") {
|
|
39778
|
+
const msg = result?.error ?? "command failed";
|
|
39779
|
+
console.error(`command ${outcome.commandId} error: ${msg}`);
|
|
39780
|
+
process.exitCode = 1;
|
|
39781
|
+
return;
|
|
39782
|
+
}
|
|
39783
|
+
console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai result ${outcome.commandId}`);
|
|
39784
|
+
process.exitCode = 1;
|
|
39785
|
+
}
|
|
39786
|
+
async function runJoeCli(command, sql, args, opts) {
|
|
39787
|
+
try {
|
|
39788
|
+
const rootOpts = program2.opts();
|
|
39789
|
+
const cfg = readConfig();
|
|
39790
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39791
|
+
if (!apiKey) {
|
|
39792
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39793
|
+
process.exitCode = 1;
|
|
39794
|
+
return;
|
|
39795
|
+
}
|
|
39796
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
39797
|
+
if (!projectRef) {
|
|
39798
|
+
console.error("Project is required. Pass --project <id|alias>.");
|
|
39799
|
+
process.exitCode = 1;
|
|
39800
|
+
return;
|
|
39801
|
+
}
|
|
39802
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39803
|
+
const budgetMs = typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
|
|
39804
|
+
const outcome = await executeJoeCommand2({
|
|
39805
|
+
apiKey,
|
|
39806
|
+
apiBaseUrl,
|
|
39807
|
+
command,
|
|
39808
|
+
project: projectRef,
|
|
39809
|
+
sql,
|
|
39810
|
+
args,
|
|
39811
|
+
session: opts.session ?? null,
|
|
39812
|
+
newSession: !!opts.newSession,
|
|
39813
|
+
orgId: cfg.orgId ?? undefined,
|
|
39814
|
+
budgetMs,
|
|
39815
|
+
debug: !!opts.debug
|
|
39816
|
+
});
|
|
39817
|
+
printJoeOutcome(command, outcome, !!opts.json);
|
|
39818
|
+
} catch (err) {
|
|
39819
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39820
|
+
console.error(message);
|
|
39821
|
+
process.exitCode = 1;
|
|
39822
|
+
}
|
|
39823
|
+
}
|
|
39824
|
+
function withJoeOptions(cmd) {
|
|
39825
|
+
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");
|
|
39826
|
+
}
|
|
39827
|
+
withJoeOptions(program2.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
|
|
39828
|
+
await runJoeCli("plan", sql, null, opts);
|
|
39829
|
+
});
|
|
39830
|
+
withJoeOptions(program2.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
|
|
39831
|
+
await runJoeCli("explain", sql, null, opts);
|
|
39832
|
+
});
|
|
39833
|
+
withJoeOptions(program2.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
|
|
39834
|
+
await runJoeCli("exec", sql, null, opts);
|
|
39835
|
+
});
|
|
39836
|
+
withJoeOptions(program2.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) => {
|
|
39837
|
+
await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
|
|
39838
|
+
});
|
|
39839
|
+
withJoeOptions(program2.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
|
|
39840
|
+
await runJoeCli("activity", null, null, opts);
|
|
39841
|
+
});
|
|
39842
|
+
withJoeOptions(program2.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
39843
|
+
const pidNum = parseInt(pid, 10);
|
|
39844
|
+
if (Number.isNaN(pidNum)) {
|
|
39845
|
+
console.error("pid must be a number");
|
|
39846
|
+
process.exitCode = 1;
|
|
39847
|
+
return;
|
|
39848
|
+
}
|
|
39849
|
+
await runJoeCli("terminate", null, { pid: pidNum }, opts);
|
|
39850
|
+
});
|
|
39851
|
+
withJoeOptions(program2.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
39852
|
+
await runJoeCli("reset", null, null, opts);
|
|
39853
|
+
});
|
|
39854
|
+
withJoeOptions(program2.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) => {
|
|
39855
|
+
const args = { object: object4 };
|
|
39856
|
+
if (opts.variant)
|
|
39857
|
+
args.variant = opts.variant;
|
|
39858
|
+
await runJoeCli("describe", null, args, opts);
|
|
39859
|
+
});
|
|
39860
|
+
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) => {
|
|
39861
|
+
try {
|
|
39862
|
+
const rootOpts = program2.opts();
|
|
39863
|
+
const cfg = readConfig();
|
|
39864
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39865
|
+
if (!apiKey) {
|
|
39866
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39867
|
+
process.exitCode = 1;
|
|
39868
|
+
return;
|
|
39869
|
+
}
|
|
39870
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39871
|
+
const projects = await listProjects2({
|
|
39872
|
+
apiKey,
|
|
39873
|
+
apiBaseUrl,
|
|
39874
|
+
orgId: cfg.orgId ?? undefined,
|
|
39875
|
+
debug: !!opts.debug
|
|
39876
|
+
});
|
|
39877
|
+
if (opts.json) {
|
|
39878
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
39879
|
+
} else {
|
|
39880
|
+
console.log(formatProjectsTable(projects));
|
|
39881
|
+
}
|
|
39882
|
+
} catch (err) {
|
|
39883
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39884
|
+
console.error(message);
|
|
39885
|
+
process.exitCode = 1;
|
|
39886
|
+
}
|
|
39887
|
+
});
|
|
39888
|
+
async function resolveDblabTarget(project, debug) {
|
|
39889
|
+
const rootOpts = program2.opts();
|
|
39890
|
+
const cfg = readConfig();
|
|
39891
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39892
|
+
if (!apiKey) {
|
|
39893
|
+
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39894
|
+
}
|
|
39895
|
+
const ref = (project ?? "").trim();
|
|
39896
|
+
if (!ref) {
|
|
39897
|
+
throw new Error("--project <id|alias> is required");
|
|
39898
|
+
}
|
|
39899
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39900
|
+
const orgId = cfg.orgId ?? undefined;
|
|
39901
|
+
const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
39902
|
+
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
39903
|
+
}
|
|
39904
|
+
var clone2 = program2.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
39905
|
+
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) => {
|
|
39906
|
+
try {
|
|
39907
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39908
|
+
const result = await createClone2({
|
|
39909
|
+
apiKey,
|
|
39910
|
+
apiBaseUrl,
|
|
39911
|
+
instanceId,
|
|
39912
|
+
cloneId: opts.id,
|
|
39913
|
+
branch: opts.branch,
|
|
39914
|
+
snapshotId: opts.snapshot,
|
|
39915
|
+
dbUser: opts.dbUser,
|
|
39916
|
+
dbPassword: opts.dbPassword,
|
|
39917
|
+
isProtected: !!opts.protected,
|
|
39918
|
+
debug: !!opts.debug
|
|
39919
|
+
});
|
|
39920
|
+
printResult(result, opts.json);
|
|
39921
|
+
} catch (err) {
|
|
39922
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39923
|
+
process.exitCode = 1;
|
|
39924
|
+
}
|
|
39925
|
+
});
|
|
39926
|
+
program2.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) => {
|
|
39927
|
+
try {
|
|
39928
|
+
const rootOpts = program2.opts();
|
|
39929
|
+
const cfg = readConfig();
|
|
39930
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39931
|
+
if (!apiKey) {
|
|
39932
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39933
|
+
process.exitCode = 1;
|
|
39934
|
+
return;
|
|
39935
|
+
}
|
|
39936
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39937
|
+
const status = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
39938
|
+
if (opts.json) {
|
|
39939
|
+
console.log(JSON.stringify(status, null, 2));
|
|
39940
|
+
} else {
|
|
39941
|
+
console.log(`command ${status.command_id} \xB7 ${status.status}${status.error ? ` \xB7 ${status.error}` : ""}`);
|
|
39942
|
+
}
|
|
39943
|
+
} catch (err) {
|
|
39944
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39945
|
+
console.error(message);
|
|
39946
|
+
process.exitCode = 1;
|
|
39947
|
+
}
|
|
39948
|
+
});
|
|
39949
|
+
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) => {
|
|
39950
|
+
try {
|
|
39951
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39952
|
+
const result = await listClones2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
39953
|
+
printResult(result, opts.json);
|
|
39954
|
+
} catch (err) {
|
|
39955
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39956
|
+
process.exitCode = 1;
|
|
39957
|
+
}
|
|
39958
|
+
});
|
|
39959
|
+
program2.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) => {
|
|
39960
|
+
try {
|
|
39961
|
+
const rootOpts = program2.opts();
|
|
39962
|
+
const cfg = readConfig();
|
|
39963
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39964
|
+
if (!apiKey) {
|
|
39965
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39966
|
+
process.exitCode = 1;
|
|
39967
|
+
return;
|
|
39968
|
+
}
|
|
39969
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39970
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
39971
|
+
if (opts.json) {
|
|
39972
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39973
|
+
return;
|
|
39974
|
+
}
|
|
39975
|
+
const inferred = inferCommandFromResult(result);
|
|
39976
|
+
if (result.status === "done" && inferred) {
|
|
39977
|
+
console.log(`command ${result.command_id} \xB7 done`);
|
|
39978
|
+
const body = formatJoeResult(inferred, result);
|
|
39979
|
+
if (body)
|
|
39980
|
+
console.log(body);
|
|
39981
|
+
} else if (result.status === "error") {
|
|
39982
|
+
console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
|
|
39983
|
+
process.exitCode = 1;
|
|
39984
|
+
} else {
|
|
39985
|
+
console.log(`command ${result.command_id} \xB7 ${result.status}`);
|
|
39986
|
+
}
|
|
39987
|
+
} catch (err) {
|
|
39988
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39989
|
+
console.error(message);
|
|
39990
|
+
process.exitCode = 1;
|
|
39991
|
+
}
|
|
39992
|
+
});
|
|
39993
|
+
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) => {
|
|
39994
|
+
try {
|
|
39995
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39996
|
+
const result = await getClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
39997
|
+
printResult(result, opts.json);
|
|
39998
|
+
} catch (err) {
|
|
39999
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40000
|
+
process.exitCode = 1;
|
|
40001
|
+
}
|
|
40002
|
+
});
|
|
40003
|
+
clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot (requires joe:admin)").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) => {
|
|
40004
|
+
try {
|
|
40005
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40006
|
+
const result = await resetClone2({
|
|
40007
|
+
apiKey,
|
|
40008
|
+
apiBaseUrl,
|
|
40009
|
+
instanceId,
|
|
40010
|
+
cloneId,
|
|
40011
|
+
snapshotId: opts.snapshot,
|
|
40012
|
+
latest: opts.latest,
|
|
40013
|
+
debug: !!opts.debug
|
|
40014
|
+
});
|
|
40015
|
+
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
40016
|
+
} catch (err) {
|
|
40017
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40018
|
+
process.exitCode = 1;
|
|
40019
|
+
}
|
|
40020
|
+
});
|
|
40021
|
+
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) => {
|
|
40022
|
+
try {
|
|
40023
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40024
|
+
const result = await destroyClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
40025
|
+
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
40026
|
+
} catch (err) {
|
|
40027
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40028
|
+
process.exitCode = 1;
|
|
40029
|
+
}
|
|
40030
|
+
});
|
|
40031
|
+
var branch = program2.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
40032
|
+
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) => {
|
|
40033
|
+
try {
|
|
40034
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40035
|
+
const result = await listBranches2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
40036
|
+
printResult(result, opts.json);
|
|
40037
|
+
} catch (err) {
|
|
40038
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40039
|
+
process.exitCode = 1;
|
|
40040
|
+
}
|
|
40041
|
+
});
|
|
40042
|
+
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) => {
|
|
40043
|
+
try {
|
|
40044
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40045
|
+
const result = await createBranch2({
|
|
40046
|
+
apiKey,
|
|
40047
|
+
apiBaseUrl,
|
|
40048
|
+
instanceId,
|
|
40049
|
+
branchName: name,
|
|
40050
|
+
baseBranch: opts.baseBranch,
|
|
40051
|
+
snapshotId: opts.snapshot,
|
|
40052
|
+
debug: !!opts.debug
|
|
40053
|
+
});
|
|
40054
|
+
printResult(result, opts.json);
|
|
40055
|
+
} catch (err) {
|
|
40056
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40057
|
+
process.exitCode = 1;
|
|
40058
|
+
}
|
|
40059
|
+
});
|
|
40060
|
+
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) => {
|
|
40061
|
+
try {
|
|
40062
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40063
|
+
const result = await deleteBranch2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40064
|
+
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
40065
|
+
} catch (err) {
|
|
40066
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40067
|
+
process.exitCode = 1;
|
|
40068
|
+
}
|
|
40069
|
+
});
|
|
40070
|
+
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) => {
|
|
40071
|
+
try {
|
|
40072
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40073
|
+
const result = await branchLog2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40074
|
+
printResult(result, opts.json);
|
|
40075
|
+
} catch (err) {
|
|
40076
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40077
|
+
process.exitCode = 1;
|
|
40078
|
+
}
|
|
40079
|
+
});
|
|
40080
|
+
var snapshot = program2.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
40081
|
+
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) => {
|
|
40082
|
+
try {
|
|
40083
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40084
|
+
const result = await listSnapshots2({
|
|
40085
|
+
apiKey,
|
|
40086
|
+
apiBaseUrl,
|
|
40087
|
+
instanceId,
|
|
40088
|
+
branchName: opts.branch,
|
|
40089
|
+
dataset: opts.dataset,
|
|
40090
|
+
debug: !!opts.debug
|
|
40091
|
+
});
|
|
40092
|
+
printResult(result, opts.json);
|
|
40093
|
+
} catch (err) {
|
|
40094
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40095
|
+
process.exitCode = 1;
|
|
40096
|
+
}
|
|
40097
|
+
});
|
|
40098
|
+
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) => {
|
|
40099
|
+
try {
|
|
40100
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40101
|
+
const result = await createSnapshot2({
|
|
40102
|
+
apiKey,
|
|
40103
|
+
apiBaseUrl,
|
|
40104
|
+
instanceId,
|
|
40105
|
+
cloneId: opts.clone,
|
|
40106
|
+
message: opts.message,
|
|
40107
|
+
debug: !!opts.debug
|
|
40108
|
+
});
|
|
40109
|
+
printResult(result, opts.json);
|
|
40110
|
+
} catch (err) {
|
|
40111
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40112
|
+
process.exitCode = 1;
|
|
40113
|
+
}
|
|
40114
|
+
});
|
|
40115
|
+
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) => {
|
|
40116
|
+
try {
|
|
40117
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40118
|
+
const result = await destroySnapshot2({
|
|
40119
|
+
apiKey,
|
|
40120
|
+
apiBaseUrl,
|
|
40121
|
+
instanceId,
|
|
40122
|
+
snapshotId,
|
|
40123
|
+
force: !!opts.force,
|
|
40124
|
+
debug: !!opts.debug
|
|
40125
|
+
});
|
|
40126
|
+
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
40127
|
+
} catch (err) {
|
|
40128
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40129
|
+
process.exitCode = 1;
|
|
40130
|
+
}
|
|
40131
|
+
});
|
|
37892
40132
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
37893
40133
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
37894
40134
|
const rootOpts = program2.opts();
|
|
@@ -37962,29 +40202,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
37962
40202
|
let configDir;
|
|
37963
40203
|
switch (client) {
|
|
37964
40204
|
case "cursor":
|
|
37965
|
-
configPath =
|
|
37966
|
-
configDir =
|
|
40205
|
+
configPath = path9.join(homeDir, ".cursor", "mcp.json");
|
|
40206
|
+
configDir = path9.dirname(configPath);
|
|
37967
40207
|
break;
|
|
37968
40208
|
case "windsurf":
|
|
37969
|
-
configPath =
|
|
37970
|
-
configDir =
|
|
40209
|
+
configPath = path9.join(homeDir, ".windsurf", "mcp.json");
|
|
40210
|
+
configDir = path9.dirname(configPath);
|
|
37971
40211
|
break;
|
|
37972
40212
|
case "codex":
|
|
37973
|
-
configPath =
|
|
37974
|
-
configDir =
|
|
40213
|
+
configPath = path9.join(homeDir, ".codex", "mcp.json");
|
|
40214
|
+
configDir = path9.dirname(configPath);
|
|
37975
40215
|
break;
|
|
37976
40216
|
default:
|
|
37977
40217
|
console.error(`Configuration not implemented for: ${client}`);
|
|
37978
40218
|
process.exitCode = 1;
|
|
37979
40219
|
return;
|
|
37980
40220
|
}
|
|
37981
|
-
if (!
|
|
37982
|
-
|
|
40221
|
+
if (!fs11.existsSync(configDir)) {
|
|
40222
|
+
fs11.mkdirSync(configDir, { recursive: true });
|
|
37983
40223
|
}
|
|
37984
40224
|
let config2 = { mcpServers: {} };
|
|
37985
|
-
if (
|
|
40225
|
+
if (fs11.existsSync(configPath)) {
|
|
37986
40226
|
try {
|
|
37987
|
-
const content =
|
|
40227
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
37988
40228
|
config2 = JSON.parse(content);
|
|
37989
40229
|
if (!config2.mcpServers) {
|
|
37990
40230
|
config2.mcpServers = {};
|
|
@@ -37997,7 +40237,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
37997
40237
|
command: pgaiPath,
|
|
37998
40238
|
args: ["mcp", "start"]
|
|
37999
40239
|
};
|
|
38000
|
-
|
|
40240
|
+
fs11.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
|
|
38001
40241
|
console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
|
|
38002
40242
|
console.log(` Config file: ${configPath}`);
|
|
38003
40243
|
console.log("");
|
|
@@ -38018,5 +40258,6 @@ export {
|
|
|
38018
40258
|
registerMonitoringInstance,
|
|
38019
40259
|
refreshBundledComposeIfStale,
|
|
38020
40260
|
readDeployedTag,
|
|
40261
|
+
planMonitoringRegistration,
|
|
38021
40262
|
isValidComposeYaml
|
|
38022
40263
|
};
|