postgresai 0.16.0-dev.2 → 0.16.0-dev.4
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 +657 -0
- package/dist/bin/postgres-ai.js +2454 -271
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +428 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +739 -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 +415 -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/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.4",
|
|
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.4";
|
|
16260
16260
|
var package_default2 = {
|
|
16261
16261
|
name: "postgresai",
|
|
16262
16262
|
version,
|
|
@@ -17267,9 +17267,601 @@ async function fetchReportFileData(params) {
|
|
|
17267
17267
|
}
|
|
17268
17268
|
}
|
|
17269
17269
|
|
|
17270
|
-
// lib/
|
|
17270
|
+
// lib/joe.ts
|
|
17271
17271
|
import * as fs3 from "fs";
|
|
17272
17272
|
import * as path3 from "path";
|
|
17273
|
+
import * as crypto from "node:crypto";
|
|
17274
|
+
var JOE_COMMANDS = [
|
|
17275
|
+
"plan",
|
|
17276
|
+
"explain",
|
|
17277
|
+
"exec",
|
|
17278
|
+
"hypo",
|
|
17279
|
+
"activity",
|
|
17280
|
+
"terminate",
|
|
17281
|
+
"reset",
|
|
17282
|
+
"describe"
|
|
17283
|
+
];
|
|
17284
|
+
var TERMINAL_STATES = new Set([
|
|
17285
|
+
"done",
|
|
17286
|
+
"error",
|
|
17287
|
+
"timed_out"
|
|
17288
|
+
]);
|
|
17289
|
+
var AI_ENABLED_COMMANDS = new Set([
|
|
17290
|
+
"plan",
|
|
17291
|
+
"explain",
|
|
17292
|
+
"exec",
|
|
17293
|
+
"hypo",
|
|
17294
|
+
"activity",
|
|
17295
|
+
"describe"
|
|
17296
|
+
]);
|
|
17297
|
+
var EXECUTING_COMMANDS = new Set([
|
|
17298
|
+
"exec",
|
|
17299
|
+
"explain"
|
|
17300
|
+
]);
|
|
17301
|
+
var DEFAULT_BUDGET_MS = 25000;
|
|
17302
|
+
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
17303
|
+
async function callRpc(params) {
|
|
17304
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
17305
|
+
if (!apiKey) {
|
|
17306
|
+
throw new Error("API key is required");
|
|
17307
|
+
}
|
|
17308
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17309
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
17310
|
+
const payload = JSON.stringify(body);
|
|
17311
|
+
const headers = {
|
|
17312
|
+
"access-token": apiKey,
|
|
17313
|
+
"Content-Type": "application/json",
|
|
17314
|
+
Connection: "close"
|
|
17315
|
+
};
|
|
17316
|
+
if (debug) {
|
|
17317
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17318
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17319
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17320
|
+
console.error(`Debug: Request body: ${payload}`);
|
|
17321
|
+
}
|
|
17322
|
+
const response = await fetch(url.toString(), {
|
|
17323
|
+
method: "POST",
|
|
17324
|
+
headers,
|
|
17325
|
+
body: payload
|
|
17326
|
+
});
|
|
17327
|
+
const text = await response.text();
|
|
17328
|
+
if (debug) {
|
|
17329
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
17330
|
+
console.error(`Debug: Response body: ${text}`);
|
|
17331
|
+
}
|
|
17332
|
+
if (!response.ok) {
|
|
17333
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
17334
|
+
}
|
|
17335
|
+
try {
|
|
17336
|
+
return JSON.parse(text);
|
|
17337
|
+
} catch {
|
|
17338
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17339
|
+
}
|
|
17340
|
+
}
|
|
17341
|
+
async function submitCommand(params) {
|
|
17342
|
+
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
17343
|
+
if (!JOE_COMMANDS.includes(command)) {
|
|
17344
|
+
throw new Error(`Unknown Joe command: ${command}`);
|
|
17345
|
+
}
|
|
17346
|
+
const body = {
|
|
17347
|
+
project_id: projectId,
|
|
17348
|
+
command,
|
|
17349
|
+
sql: sql ?? null,
|
|
17350
|
+
args: args ?? null,
|
|
17351
|
+
session_id: sessionId ?? null
|
|
17352
|
+
};
|
|
17353
|
+
if (idempotencyKey) {
|
|
17354
|
+
body.idempotency_key = idempotencyKey;
|
|
17355
|
+
}
|
|
17356
|
+
return callRpc({
|
|
17357
|
+
apiKey,
|
|
17358
|
+
apiBaseUrl,
|
|
17359
|
+
fn: "joe_command_submit",
|
|
17360
|
+
body,
|
|
17361
|
+
operation: `Failed to submit ${command} command`,
|
|
17362
|
+
debug
|
|
17363
|
+
});
|
|
17364
|
+
}
|
|
17365
|
+
async function getCommandStatus(params) {
|
|
17366
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17367
|
+
if (!commandId) {
|
|
17368
|
+
throw new Error("commandId is required");
|
|
17369
|
+
}
|
|
17370
|
+
return callRpc({
|
|
17371
|
+
apiKey,
|
|
17372
|
+
apiBaseUrl,
|
|
17373
|
+
fn: "joe_command_status",
|
|
17374
|
+
body: { command_id: commandId },
|
|
17375
|
+
operation: "Failed to fetch command status",
|
|
17376
|
+
debug
|
|
17377
|
+
});
|
|
17378
|
+
}
|
|
17379
|
+
async function getCommandResult(params) {
|
|
17380
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
17381
|
+
if (!commandId) {
|
|
17382
|
+
throw new Error("commandId is required");
|
|
17383
|
+
}
|
|
17384
|
+
return callRpc({
|
|
17385
|
+
apiKey,
|
|
17386
|
+
apiBaseUrl,
|
|
17387
|
+
fn: "joe_command_result",
|
|
17388
|
+
body: { command_id: commandId },
|
|
17389
|
+
operation: "Failed to fetch command result",
|
|
17390
|
+
debug
|
|
17391
|
+
});
|
|
17392
|
+
}
|
|
17393
|
+
function normalizeProjectRow(row) {
|
|
17394
|
+
const projectId = row.project_id ?? row.id ?? 0;
|
|
17395
|
+
return {
|
|
17396
|
+
project_id: Number(projectId),
|
|
17397
|
+
alias: row.alias ?? null,
|
|
17398
|
+
name: row.name ?? null,
|
|
17399
|
+
label: row.label ?? null,
|
|
17400
|
+
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
17401
|
+
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
17402
|
+
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
17403
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
|
|
17404
|
+
};
|
|
17405
|
+
}
|
|
17406
|
+
async function listProjects(params) {
|
|
17407
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17408
|
+
const body = {};
|
|
17409
|
+
if (typeof orgId === "number") {
|
|
17410
|
+
body.org_id = orgId;
|
|
17411
|
+
}
|
|
17412
|
+
const rows = await callRpc({
|
|
17413
|
+
apiKey,
|
|
17414
|
+
apiBaseUrl,
|
|
17415
|
+
fn: "projects_list",
|
|
17416
|
+
body,
|
|
17417
|
+
operation: "Failed to list projects",
|
|
17418
|
+
debug
|
|
17419
|
+
});
|
|
17420
|
+
if (!Array.isArray(rows)) {
|
|
17421
|
+
return [];
|
|
17422
|
+
}
|
|
17423
|
+
return rows.map(normalizeProjectRow);
|
|
17424
|
+
}
|
|
17425
|
+
function isNumericProjectRef(ref) {
|
|
17426
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
17427
|
+
}
|
|
17428
|
+
async function resolveProjectId(params) {
|
|
17429
|
+
const ref = String(params.project ?? "").trim();
|
|
17430
|
+
if (!ref) {
|
|
17431
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
17432
|
+
}
|
|
17433
|
+
if (isNumericProjectRef(ref)) {
|
|
17434
|
+
return Number(ref);
|
|
17435
|
+
}
|
|
17436
|
+
const projects = await listProjects({
|
|
17437
|
+
apiKey: params.apiKey,
|
|
17438
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
17439
|
+
orgId: params.orgId,
|
|
17440
|
+
debug: params.debug
|
|
17441
|
+
});
|
|
17442
|
+
const needle = ref.toLowerCase();
|
|
17443
|
+
const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
17444
|
+
if (!match) {
|
|
17445
|
+
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17446
|
+
}
|
|
17447
|
+
return match.project_id;
|
|
17448
|
+
}
|
|
17449
|
+
function sessionStorePath(dir) {
|
|
17450
|
+
return path3.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
17451
|
+
}
|
|
17452
|
+
function readSessionStore(dir) {
|
|
17453
|
+
const file = sessionStorePath(dir);
|
|
17454
|
+
if (!fs3.existsSync(file)) {
|
|
17455
|
+
return {};
|
|
17456
|
+
}
|
|
17457
|
+
try {
|
|
17458
|
+
const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
17459
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17460
|
+
return parsed;
|
|
17461
|
+
}
|
|
17462
|
+
} catch {}
|
|
17463
|
+
return {};
|
|
17464
|
+
}
|
|
17465
|
+
function readStoredSessionId(projectId, dir) {
|
|
17466
|
+
const store = readSessionStore(dir);
|
|
17467
|
+
const value = store[String(projectId)];
|
|
17468
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
17469
|
+
}
|
|
17470
|
+
function writeStoredSessionId(projectId, sessionId, dir) {
|
|
17471
|
+
const baseDir = dir ?? getConfigDir2();
|
|
17472
|
+
if (!fs3.existsSync(baseDir)) {
|
|
17473
|
+
fs3.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
17474
|
+
}
|
|
17475
|
+
const store = readSessionStore(dir);
|
|
17476
|
+
store[String(projectId)] = sessionId;
|
|
17477
|
+
fs3.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
|
|
17478
|
+
`, { mode: 384 });
|
|
17479
|
+
}
|
|
17480
|
+
function clearStoredSessionId(projectId, dir) {
|
|
17481
|
+
const file = sessionStorePath(dir);
|
|
17482
|
+
if (!fs3.existsSync(file)) {
|
|
17483
|
+
return;
|
|
17484
|
+
}
|
|
17485
|
+
const store = readSessionStore(dir);
|
|
17486
|
+
if (String(projectId) in store) {
|
|
17487
|
+
delete store[String(projectId)];
|
|
17488
|
+
fs3.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
17489
|
+
`, { mode: 384 });
|
|
17490
|
+
}
|
|
17491
|
+
}
|
|
17492
|
+
function computeIdempotencyKey(command, projectId, sql, args) {
|
|
17493
|
+
if (EXECUTING_COMMANDS.has(command)) {
|
|
17494
|
+
const canonical = `${projectId}
|
|
17495
|
+
${command}
|
|
17496
|
+
${sql ?? ""}
|
|
17497
|
+
${JSON.stringify(args ?? null)}`;
|
|
17498
|
+
return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
|
|
17499
|
+
}
|
|
17500
|
+
return `joe-${crypto.randomUUID()}`;
|
|
17501
|
+
}
|
|
17502
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17503
|
+
async function runCommand(params) {
|
|
17504
|
+
const {
|
|
17505
|
+
apiKey,
|
|
17506
|
+
apiBaseUrl,
|
|
17507
|
+
command,
|
|
17508
|
+
projectId,
|
|
17509
|
+
sql,
|
|
17510
|
+
args,
|
|
17511
|
+
sessionId,
|
|
17512
|
+
debug
|
|
17513
|
+
} = params;
|
|
17514
|
+
const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
17515
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
17516
|
+
const now = params.now ?? Date.now;
|
|
17517
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
17518
|
+
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
|
|
17519
|
+
const submitted = await submitCommand({
|
|
17520
|
+
apiKey,
|
|
17521
|
+
apiBaseUrl,
|
|
17522
|
+
command,
|
|
17523
|
+
projectId,
|
|
17524
|
+
sql,
|
|
17525
|
+
args,
|
|
17526
|
+
sessionId,
|
|
17527
|
+
idempotencyKey,
|
|
17528
|
+
debug
|
|
17529
|
+
});
|
|
17530
|
+
const commandId = submitted.command_id;
|
|
17531
|
+
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
17532
|
+
const deadline = now() + budgetMs;
|
|
17533
|
+
let status = submitted.status;
|
|
17534
|
+
while (true) {
|
|
17535
|
+
const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
|
|
17536
|
+
status = statusResp.status;
|
|
17537
|
+
if (TERMINAL_STATES.has(status)) {
|
|
17538
|
+
break;
|
|
17539
|
+
}
|
|
17540
|
+
if (now() >= deadline) {
|
|
17541
|
+
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
17542
|
+
}
|
|
17543
|
+
await sleep(pollIntervalMs);
|
|
17544
|
+
}
|
|
17545
|
+
const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
|
|
17546
|
+
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
17547
|
+
}
|
|
17548
|
+
async function executeJoeCommand(params) {
|
|
17549
|
+
const projectId = await resolveProjectId({
|
|
17550
|
+
apiKey: params.apiKey,
|
|
17551
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
17552
|
+
project: params.project,
|
|
17553
|
+
orgId: params.orgId,
|
|
17554
|
+
debug: params.debug
|
|
17555
|
+
});
|
|
17556
|
+
let sessionId;
|
|
17557
|
+
if (params.newSession) {
|
|
17558
|
+
clearStoredSessionId(projectId, params.sessionDir);
|
|
17559
|
+
sessionId = null;
|
|
17560
|
+
} else if (params.session) {
|
|
17561
|
+
sessionId = String(params.session);
|
|
17562
|
+
} else {
|
|
17563
|
+
sessionId = readStoredSessionId(projectId, params.sessionDir);
|
|
17564
|
+
}
|
|
17565
|
+
const outcome = await runCommand({
|
|
17566
|
+
apiKey: params.apiKey,
|
|
17567
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
17568
|
+
command: params.command,
|
|
17569
|
+
projectId,
|
|
17570
|
+
sql: params.sql,
|
|
17571
|
+
args: params.args,
|
|
17572
|
+
sessionId,
|
|
17573
|
+
budgetMs: params.budgetMs,
|
|
17574
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
17575
|
+
debug: params.debug,
|
|
17576
|
+
now: params.now,
|
|
17577
|
+
sleep: params.sleep
|
|
17578
|
+
});
|
|
17579
|
+
if (outcome.sessionId) {
|
|
17580
|
+
writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
|
|
17581
|
+
}
|
|
17582
|
+
return { ...outcome, command: params.command, projectId };
|
|
17583
|
+
}
|
|
17584
|
+
|
|
17585
|
+
// lib/dblab.ts
|
|
17586
|
+
function isNumericProjectRef2(ref) {
|
|
17587
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
17588
|
+
}
|
|
17589
|
+
async function resolveDblabInstanceId(params) {
|
|
17590
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
17591
|
+
if (!apiKey) {
|
|
17592
|
+
throw new Error("API key is required");
|
|
17593
|
+
}
|
|
17594
|
+
const ref = String(params.project ?? "").trim();
|
|
17595
|
+
if (!ref) {
|
|
17596
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
17597
|
+
}
|
|
17598
|
+
let projects;
|
|
17599
|
+
try {
|
|
17600
|
+
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
17601
|
+
} catch (err) {
|
|
17602
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17603
|
+
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
17604
|
+
}
|
|
17605
|
+
const numeric = isNumericProjectRef2(ref);
|
|
17606
|
+
const needle = ref.toLowerCase();
|
|
17607
|
+
const match = projects.find((p) => {
|
|
17608
|
+
if (numeric) {
|
|
17609
|
+
return String(p.project_id) === ref;
|
|
17610
|
+
}
|
|
17611
|
+
return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
|
|
17612
|
+
});
|
|
17613
|
+
if (!match) {
|
|
17614
|
+
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
17615
|
+
}
|
|
17616
|
+
if (match.dblab_instance_id == null) {
|
|
17617
|
+
throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
|
|
17618
|
+
}
|
|
17619
|
+
return String(match.dblab_instance_id);
|
|
17620
|
+
}
|
|
17621
|
+
async function callDblabApi(params) {
|
|
17622
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
17623
|
+
if (!apiKey) {
|
|
17624
|
+
throw new Error("API key is required");
|
|
17625
|
+
}
|
|
17626
|
+
if (!instanceId) {
|
|
17627
|
+
throw new Error("instanceId is required");
|
|
17628
|
+
}
|
|
17629
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
17630
|
+
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
17631
|
+
const bodyObj = {
|
|
17632
|
+
instance_id: instanceId,
|
|
17633
|
+
action,
|
|
17634
|
+
method
|
|
17635
|
+
};
|
|
17636
|
+
if (data !== undefined) {
|
|
17637
|
+
bodyObj.data = data;
|
|
17638
|
+
}
|
|
17639
|
+
const body = JSON.stringify(bodyObj);
|
|
17640
|
+
const headers = {
|
|
17641
|
+
"access-token": apiKey,
|
|
17642
|
+
"Content-Type": "application/json",
|
|
17643
|
+
Connection: "close"
|
|
17644
|
+
};
|
|
17645
|
+
if (debug) {
|
|
17646
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
17647
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
17648
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17649
|
+
console.error(`Debug: Request body: ${body}`);
|
|
17650
|
+
}
|
|
17651
|
+
const response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
17652
|
+
const text = await response.text();
|
|
17653
|
+
if (debug) {
|
|
17654
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
17655
|
+
console.error(`Debug: Response body: ${text}`);
|
|
17656
|
+
}
|
|
17657
|
+
if (!response.ok) {
|
|
17658
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
17659
|
+
}
|
|
17660
|
+
if (text.trim() === "") {
|
|
17661
|
+
return null;
|
|
17662
|
+
}
|
|
17663
|
+
try {
|
|
17664
|
+
return JSON.parse(text);
|
|
17665
|
+
} catch {
|
|
17666
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
async function createClone(params) {
|
|
17670
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
17671
|
+
const data = { protected: Boolean(isProtected) };
|
|
17672
|
+
if (cloneId)
|
|
17673
|
+
data.id = cloneId;
|
|
17674
|
+
if (branch)
|
|
17675
|
+
data.branch = branch;
|
|
17676
|
+
if (snapshotId)
|
|
17677
|
+
data.snapshot = { id: snapshotId };
|
|
17678
|
+
if (dbUser && dbPassword)
|
|
17679
|
+
data.db = { username: dbUser, password: dbPassword };
|
|
17680
|
+
return callDblabApi({
|
|
17681
|
+
apiKey,
|
|
17682
|
+
apiBaseUrl,
|
|
17683
|
+
instanceId,
|
|
17684
|
+
action: "/clone",
|
|
17685
|
+
method: "post",
|
|
17686
|
+
data,
|
|
17687
|
+
operation: "Failed to create clone",
|
|
17688
|
+
debug
|
|
17689
|
+
});
|
|
17690
|
+
}
|
|
17691
|
+
async function listClones(params) {
|
|
17692
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17693
|
+
return callDblabApi({
|
|
17694
|
+
apiKey,
|
|
17695
|
+
apiBaseUrl,
|
|
17696
|
+
instanceId,
|
|
17697
|
+
action: "/clones",
|
|
17698
|
+
method: "get",
|
|
17699
|
+
operation: "Failed to list clones",
|
|
17700
|
+
debug
|
|
17701
|
+
});
|
|
17702
|
+
}
|
|
17703
|
+
async function getClone(params) {
|
|
17704
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17705
|
+
if (!cloneId)
|
|
17706
|
+
throw new Error("cloneId is required");
|
|
17707
|
+
return callDblabApi({
|
|
17708
|
+
apiKey,
|
|
17709
|
+
apiBaseUrl,
|
|
17710
|
+
instanceId,
|
|
17711
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17712
|
+
method: "get",
|
|
17713
|
+
operation: "Failed to get clone",
|
|
17714
|
+
debug
|
|
17715
|
+
});
|
|
17716
|
+
}
|
|
17717
|
+
async function resetClone(params) {
|
|
17718
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
17719
|
+
if (!cloneId)
|
|
17720
|
+
throw new Error("cloneId is required");
|
|
17721
|
+
const data = { latest: latest ?? !snapshotId };
|
|
17722
|
+
if (snapshotId)
|
|
17723
|
+
data.snapshotID = snapshotId;
|
|
17724
|
+
return callDblabApi({
|
|
17725
|
+
apiKey,
|
|
17726
|
+
apiBaseUrl,
|
|
17727
|
+
instanceId,
|
|
17728
|
+
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
17729
|
+
method: "post",
|
|
17730
|
+
data,
|
|
17731
|
+
operation: "Failed to reset clone",
|
|
17732
|
+
debug
|
|
17733
|
+
});
|
|
17734
|
+
}
|
|
17735
|
+
async function destroyClone(params) {
|
|
17736
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
17737
|
+
if (!cloneId)
|
|
17738
|
+
throw new Error("cloneId is required");
|
|
17739
|
+
return callDblabApi({
|
|
17740
|
+
apiKey,
|
|
17741
|
+
apiBaseUrl,
|
|
17742
|
+
instanceId,
|
|
17743
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
17744
|
+
method: "delete",
|
|
17745
|
+
operation: "Failed to destroy clone",
|
|
17746
|
+
debug
|
|
17747
|
+
});
|
|
17748
|
+
}
|
|
17749
|
+
async function listBranches(params) {
|
|
17750
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
17751
|
+
return callDblabApi({
|
|
17752
|
+
apiKey,
|
|
17753
|
+
apiBaseUrl,
|
|
17754
|
+
instanceId,
|
|
17755
|
+
action: "/branches",
|
|
17756
|
+
method: "get",
|
|
17757
|
+
operation: "Failed to list branches",
|
|
17758
|
+
debug
|
|
17759
|
+
});
|
|
17760
|
+
}
|
|
17761
|
+
async function createBranch(params) {
|
|
17762
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
17763
|
+
if (!branchName)
|
|
17764
|
+
throw new Error("branchName is required");
|
|
17765
|
+
const data = { branchName };
|
|
17766
|
+
if (baseBranch)
|
|
17767
|
+
data.baseBranch = baseBranch;
|
|
17768
|
+
if (snapshotId)
|
|
17769
|
+
data.snapshotID = snapshotId;
|
|
17770
|
+
return callDblabApi({
|
|
17771
|
+
apiKey,
|
|
17772
|
+
apiBaseUrl,
|
|
17773
|
+
instanceId,
|
|
17774
|
+
action: "/branch",
|
|
17775
|
+
method: "post",
|
|
17776
|
+
data,
|
|
17777
|
+
operation: "Failed to create branch",
|
|
17778
|
+
debug
|
|
17779
|
+
});
|
|
17780
|
+
}
|
|
17781
|
+
async function deleteBranch(params) {
|
|
17782
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17783
|
+
if (!branchName)
|
|
17784
|
+
throw new Error("branchName is required");
|
|
17785
|
+
return callDblabApi({
|
|
17786
|
+
apiKey,
|
|
17787
|
+
apiBaseUrl,
|
|
17788
|
+
instanceId,
|
|
17789
|
+
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
17790
|
+
method: "delete",
|
|
17791
|
+
operation: "Failed to delete branch",
|
|
17792
|
+
debug
|
|
17793
|
+
});
|
|
17794
|
+
}
|
|
17795
|
+
async function branchLog(params) {
|
|
17796
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
17797
|
+
if (!branchName)
|
|
17798
|
+
throw new Error("branchName is required");
|
|
17799
|
+
return callDblabApi({
|
|
17800
|
+
apiKey,
|
|
17801
|
+
apiBaseUrl,
|
|
17802
|
+
instanceId,
|
|
17803
|
+
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
17804
|
+
method: "get",
|
|
17805
|
+
operation: "Failed to fetch branch log",
|
|
17806
|
+
debug
|
|
17807
|
+
});
|
|
17808
|
+
}
|
|
17809
|
+
async function listSnapshots(params) {
|
|
17810
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
17811
|
+
const qs = new URLSearchParams;
|
|
17812
|
+
const branch = branchName?.trim();
|
|
17813
|
+
if (branch)
|
|
17814
|
+
qs.append("branch", branch);
|
|
17815
|
+
if (dataset)
|
|
17816
|
+
qs.append("dataset", dataset);
|
|
17817
|
+
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
17818
|
+
return callDblabApi({
|
|
17819
|
+
apiKey,
|
|
17820
|
+
apiBaseUrl,
|
|
17821
|
+
instanceId,
|
|
17822
|
+
action,
|
|
17823
|
+
method: "get",
|
|
17824
|
+
operation: "Failed to list snapshots",
|
|
17825
|
+
debug
|
|
17826
|
+
});
|
|
17827
|
+
}
|
|
17828
|
+
async function createSnapshot(params) {
|
|
17829
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
17830
|
+
if (!cloneId)
|
|
17831
|
+
throw new Error("cloneId is required");
|
|
17832
|
+
const data = { cloneID: cloneId };
|
|
17833
|
+
if (message)
|
|
17834
|
+
data.message = message;
|
|
17835
|
+
return callDblabApi({
|
|
17836
|
+
apiKey,
|
|
17837
|
+
apiBaseUrl,
|
|
17838
|
+
instanceId,
|
|
17839
|
+
action: "/branch/snapshot",
|
|
17840
|
+
method: "post",
|
|
17841
|
+
data,
|
|
17842
|
+
operation: "Failed to create snapshot",
|
|
17843
|
+
debug
|
|
17844
|
+
});
|
|
17845
|
+
}
|
|
17846
|
+
async function destroySnapshot(params) {
|
|
17847
|
+
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
17848
|
+
if (!snapshotId)
|
|
17849
|
+
throw new Error("snapshotId is required");
|
|
17850
|
+
const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
|
|
17851
|
+
return callDblabApi({
|
|
17852
|
+
apiKey,
|
|
17853
|
+
apiBaseUrl,
|
|
17854
|
+
instanceId,
|
|
17855
|
+
action,
|
|
17856
|
+
method: "delete",
|
|
17857
|
+
operation: "Failed to destroy snapshot",
|
|
17858
|
+
debug
|
|
17859
|
+
});
|
|
17860
|
+
}
|
|
17861
|
+
|
|
17862
|
+
// lib/storage.ts
|
|
17863
|
+
import * as fs4 from "fs";
|
|
17864
|
+
import * as path4 from "path";
|
|
17273
17865
|
var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
|
|
17274
17866
|
var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
|
|
17275
17867
|
var MIME_TYPES = {
|
|
@@ -17310,11 +17902,11 @@ async function uploadFile(params) {
|
|
|
17310
17902
|
if (!filePath) {
|
|
17311
17903
|
throw new Error("filePath is required");
|
|
17312
17904
|
}
|
|
17313
|
-
const resolvedPath =
|
|
17314
|
-
if (!
|
|
17905
|
+
const resolvedPath = path4.resolve(filePath);
|
|
17906
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
17315
17907
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
17316
17908
|
}
|
|
17317
|
-
const stat =
|
|
17909
|
+
const stat = fs4.statSync(resolvedPath);
|
|
17318
17910
|
if (!stat.isFile()) {
|
|
17319
17911
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
17320
17912
|
}
|
|
@@ -17326,9 +17918,9 @@ async function uploadFile(params) {
|
|
|
17326
17918
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
17327
17919
|
}
|
|
17328
17920
|
const url = `${base}/upload`;
|
|
17329
|
-
const fileBuffer =
|
|
17330
|
-
const fileName =
|
|
17331
|
-
const ext =
|
|
17921
|
+
const fileBuffer = fs4.readFileSync(resolvedPath);
|
|
17922
|
+
const fileName = path4.basename(resolvedPath);
|
|
17923
|
+
const ext = path4.extname(fileName).toLowerCase();
|
|
17332
17924
|
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
|
17333
17925
|
const formData = new FormData;
|
|
17334
17926
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -17387,15 +17979,15 @@ async function downloadFile(params) {
|
|
|
17387
17979
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
17388
17980
|
fullUrl = `${base}${relativePath}`;
|
|
17389
17981
|
}
|
|
17390
|
-
const urlFilename =
|
|
17982
|
+
const urlFilename = path4.basename(new URL(fullUrl).pathname);
|
|
17391
17983
|
if (!urlFilename) {
|
|
17392
17984
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
17393
17985
|
}
|
|
17394
|
-
const saveTo = outputPath ?
|
|
17986
|
+
const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
|
|
17395
17987
|
if (!outputPath) {
|
|
17396
|
-
const normalizedSave =
|
|
17397
|
-
const cwd =
|
|
17398
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
17988
|
+
const normalizedSave = path4.normalize(saveTo);
|
|
17989
|
+
const cwd = path4.normalize(process.cwd());
|
|
17990
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
|
|
17399
17991
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
17400
17992
|
}
|
|
17401
17993
|
}
|
|
@@ -17427,11 +18019,11 @@ async function downloadFile(params) {
|
|
|
17427
18019
|
}
|
|
17428
18020
|
const arrayBuffer = await response.arrayBuffer();
|
|
17429
18021
|
const buffer = Buffer.from(arrayBuffer);
|
|
17430
|
-
const parentDir =
|
|
17431
|
-
if (!
|
|
17432
|
-
|
|
18022
|
+
const parentDir = path4.dirname(saveTo);
|
|
18023
|
+
if (!fs4.existsSync(parentDir)) {
|
|
18024
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
17433
18025
|
}
|
|
17434
|
-
|
|
18026
|
+
fs4.writeFileSync(saveTo, buffer);
|
|
17435
18027
|
return {
|
|
17436
18028
|
savedTo: saveTo,
|
|
17437
18029
|
size: buffer.length,
|
|
@@ -17443,9 +18035,9 @@ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
|
|
|
17443
18035
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
17444
18036
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
17445
18037
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
17446
|
-
const name = filename ||
|
|
18038
|
+
const name = filename || path4.basename(new URL(fullUrl).pathname);
|
|
17447
18039
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
17448
|
-
const ext =
|
|
18040
|
+
const ext = path4.extname(name).toLowerCase();
|
|
17449
18041
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
17450
18042
|
return ``;
|
|
17451
18043
|
}
|
|
@@ -17733,10 +18325,10 @@ function mergeDefs(...defs) {
|
|
|
17733
18325
|
function cloneDef(schema2) {
|
|
17734
18326
|
return mergeDefs(schema2._zod.def);
|
|
17735
18327
|
}
|
|
17736
|
-
function getElementAtPath(obj,
|
|
17737
|
-
if (!
|
|
18328
|
+
function getElementAtPath(obj, path5) {
|
|
18329
|
+
if (!path5)
|
|
17738
18330
|
return obj;
|
|
17739
|
-
return
|
|
18331
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
17740
18332
|
}
|
|
17741
18333
|
function promiseAllObject(promisesObj) {
|
|
17742
18334
|
const keys = Object.keys(promisesObj);
|
|
@@ -18100,11 +18692,11 @@ function aborted(x, startIndex = 0) {
|
|
|
18100
18692
|
}
|
|
18101
18693
|
return false;
|
|
18102
18694
|
}
|
|
18103
|
-
function prefixIssues(
|
|
18695
|
+
function prefixIssues(path5, issues) {
|
|
18104
18696
|
return issues.map((iss) => {
|
|
18105
18697
|
var _a;
|
|
18106
18698
|
(_a = iss).path ?? (_a.path = []);
|
|
18107
|
-
iss.path.unshift(
|
|
18699
|
+
iss.path.unshift(path5);
|
|
18108
18700
|
return iss;
|
|
18109
18701
|
});
|
|
18110
18702
|
}
|
|
@@ -24376,49 +24968,243 @@ class StdioServerTransport {
|
|
|
24376
24968
|
// lib/mcp-server.ts
|
|
24377
24969
|
var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
24378
24970
|
`).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 };
|
|
24971
|
+
var JOE_TOOL_TO_COMMAND = {
|
|
24972
|
+
plan_query: "plan",
|
|
24973
|
+
explain_query: "explain",
|
|
24974
|
+
exec_sql: "exec",
|
|
24975
|
+
hypo_index: "hypo",
|
|
24976
|
+
activity: "activity",
|
|
24977
|
+
terminate_backend: "terminate",
|
|
24978
|
+
reset_clone: "reset",
|
|
24979
|
+
describe: "describe"
|
|
24980
|
+
};
|
|
24981
|
+
function joeToolDefinitions() {
|
|
24982
|
+
const sessionProps = {
|
|
24983
|
+
project_id: {
|
|
24984
|
+
type: "string",
|
|
24985
|
+
description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db."
|
|
24986
|
+
},
|
|
24987
|
+
session_id: {
|
|
24988
|
+
type: "string",
|
|
24989
|
+
description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted."
|
|
24990
|
+
},
|
|
24991
|
+
new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
|
|
24992
|
+
timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
|
|
24993
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
24994
|
+
};
|
|
24995
|
+
return [
|
|
24996
|
+
{
|
|
24997
|
+
name: "plan_query",
|
|
24998
|
+
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.",
|
|
24999
|
+
inputSchema: {
|
|
25000
|
+
type: "object",
|
|
25001
|
+
properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
|
|
25002
|
+
required: ["sql", "project_id"],
|
|
25003
|
+
additionalProperties: false
|
|
24415
25004
|
}
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
25005
|
+
},
|
|
25006
|
+
{
|
|
25007
|
+
name: "explain_query",
|
|
25008
|
+
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).",
|
|
25009
|
+
inputSchema: {
|
|
25010
|
+
type: "object",
|
|
25011
|
+
properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
|
|
25012
|
+
required: ["sql", "project_id"],
|
|
25013
|
+
additionalProperties: false
|
|
24419
25014
|
}
|
|
24420
|
-
|
|
24421
|
-
|
|
25015
|
+
},
|
|
25016
|
+
{
|
|
25017
|
+
name: "exec_sql",
|
|
25018
|
+
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.",
|
|
25019
|
+
inputSchema: {
|
|
25020
|
+
type: "object",
|
|
25021
|
+
properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
|
|
25022
|
+
required: ["sql", "project_id"],
|
|
25023
|
+
additionalProperties: false
|
|
25024
|
+
}
|
|
25025
|
+
},
|
|
25026
|
+
{
|
|
25027
|
+
name: "hypo_index",
|
|
25028
|
+
description: "HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
|
|
25029
|
+
inputSchema: {
|
|
25030
|
+
type: "object",
|
|
25031
|
+
properties: {
|
|
25032
|
+
sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
|
|
25033
|
+
query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
|
|
25034
|
+
...sessionProps
|
|
25035
|
+
},
|
|
25036
|
+
required: ["sql", "query", "project_id"],
|
|
25037
|
+
additionalProperties: false
|
|
25038
|
+
}
|
|
25039
|
+
},
|
|
25040
|
+
{
|
|
25041
|
+
name: "activity",
|
|
25042
|
+
description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
|
|
25043
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25044
|
+
},
|
|
25045
|
+
{
|
|
25046
|
+
name: "terminate_backend",
|
|
25047
|
+
description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25048
|
+
inputSchema: {
|
|
25049
|
+
type: "object",
|
|
25050
|
+
properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
|
|
25051
|
+
required: ["pid", "project_id"],
|
|
25052
|
+
additionalProperties: false
|
|
25053
|
+
}
|
|
25054
|
+
},
|
|
25055
|
+
{
|
|
25056
|
+
name: "reset_clone",
|
|
25057
|
+
description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
|
|
25058
|
+
inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
|
|
25059
|
+
},
|
|
25060
|
+
{
|
|
25061
|
+
name: "describe",
|
|
25062
|
+
description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
|
|
25063
|
+
inputSchema: {
|
|
25064
|
+
type: "object",
|
|
25065
|
+
properties: {
|
|
25066
|
+
object: { type: "string", description: "Object to describe (e.g. users)." },
|
|
25067
|
+
variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
|
|
25068
|
+
...sessionProps
|
|
25069
|
+
},
|
|
25070
|
+
required: ["object", "project_id"],
|
|
25071
|
+
additionalProperties: false
|
|
25072
|
+
}
|
|
25073
|
+
},
|
|
25074
|
+
{
|
|
25075
|
+
name: "list_projects",
|
|
25076
|
+
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.",
|
|
25077
|
+
inputSchema: {
|
|
25078
|
+
type: "object",
|
|
25079
|
+
properties: {
|
|
25080
|
+
org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
|
|
25081
|
+
debug: { type: "boolean", description: "Enable verbose debug logs." }
|
|
25082
|
+
},
|
|
25083
|
+
additionalProperties: false
|
|
25084
|
+
}
|
|
25085
|
+
}
|
|
25086
|
+
];
|
|
25087
|
+
}
|
|
25088
|
+
async function runJoeTool(params) {
|
|
25089
|
+
const command = JOE_TOOL_TO_COMMAND[params.toolName];
|
|
25090
|
+
const a = params.args;
|
|
25091
|
+
const project = String(a.project_id ?? "").trim();
|
|
25092
|
+
if (!project) {
|
|
25093
|
+
return { content: [{ type: "text", text: "project_id is required" }], isError: true };
|
|
25094
|
+
}
|
|
25095
|
+
let sql = null;
|
|
25096
|
+
let cmdArgs = null;
|
|
25097
|
+
if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
|
|
25098
|
+
sql = String(a.sql ?? "").trim();
|
|
25099
|
+
if (!sql) {
|
|
25100
|
+
return { content: [{ type: "text", text: "sql is required" }], isError: true };
|
|
25101
|
+
}
|
|
25102
|
+
}
|
|
25103
|
+
if (command === "hypo") {
|
|
25104
|
+
const query = String(a.query ?? "").trim();
|
|
25105
|
+
if (!query) {
|
|
25106
|
+
return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
|
|
25107
|
+
}
|
|
25108
|
+
cmdArgs = { query };
|
|
25109
|
+
}
|
|
25110
|
+
if (command === "terminate") {
|
|
25111
|
+
const pid = Number(a.pid);
|
|
25112
|
+
if (!Number.isFinite(pid)) {
|
|
25113
|
+
return { content: [{ type: "text", text: "pid (number) is required for terminate_backend" }], isError: true };
|
|
25114
|
+
}
|
|
25115
|
+
cmdArgs = { pid };
|
|
25116
|
+
}
|
|
25117
|
+
if (command === "describe") {
|
|
25118
|
+
const object4 = String(a.object ?? "").trim();
|
|
25119
|
+
if (!object4) {
|
|
25120
|
+
return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
|
|
25121
|
+
}
|
|
25122
|
+
cmdArgs = { object: object4 };
|
|
25123
|
+
if (a.variant !== undefined)
|
|
25124
|
+
cmdArgs.variant = String(a.variant);
|
|
25125
|
+
}
|
|
25126
|
+
const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
|
|
25127
|
+
const newSession = a.new_session === true;
|
|
25128
|
+
const budgetMs = a.timeout_ms !== undefined ? Number(a.timeout_ms) : undefined;
|
|
25129
|
+
const outcome = await executeJoeCommand({
|
|
25130
|
+
apiKey: params.apiKey,
|
|
25131
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
25132
|
+
command,
|
|
25133
|
+
project,
|
|
25134
|
+
sql,
|
|
25135
|
+
args: cmdArgs,
|
|
25136
|
+
session,
|
|
25137
|
+
newSession,
|
|
25138
|
+
orgId: params.orgId,
|
|
25139
|
+
budgetMs,
|
|
25140
|
+
debug: params.debug
|
|
25141
|
+
});
|
|
25142
|
+
const payload = {
|
|
25143
|
+
command,
|
|
25144
|
+
command_id: outcome.commandId,
|
|
25145
|
+
session_id: outcome.sessionId,
|
|
25146
|
+
status: outcome.status,
|
|
25147
|
+
budget_expired: outcome.budgetExpired,
|
|
25148
|
+
resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
|
|
25149
|
+
result: outcome.result
|
|
25150
|
+
};
|
|
25151
|
+
const isError = outcome.status === "error" || outcome.status === "timed_out";
|
|
25152
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
|
|
25153
|
+
}
|
|
25154
|
+
async function resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfgOrgId, debug) {
|
|
25155
|
+
const project = String(args.project || "").trim();
|
|
25156
|
+
if (!project) {
|
|
25157
|
+
throw new Error("project is required (project id or alias)");
|
|
25158
|
+
}
|
|
25159
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
|
|
25160
|
+
return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
|
|
25161
|
+
}
|
|
25162
|
+
function optStr(v) {
|
|
25163
|
+
return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
|
|
25164
|
+
}
|
|
25165
|
+
async function handleToolCall(req, rootOpts, extra) {
|
|
25166
|
+
const toolName = req.params.name;
|
|
25167
|
+
const args = req.params.arguments || {};
|
|
25168
|
+
const cfg = readConfig2();
|
|
25169
|
+
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
25170
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
25171
|
+
const debug = Boolean(args.debug ?? extra?.debug);
|
|
25172
|
+
if (!apiKey) {
|
|
25173
|
+
return {
|
|
25174
|
+
content: [
|
|
25175
|
+
{
|
|
25176
|
+
type: "text",
|
|
25177
|
+
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
|
|
25178
|
+
}
|
|
25179
|
+
],
|
|
25180
|
+
isError: true
|
|
25181
|
+
};
|
|
25182
|
+
}
|
|
25183
|
+
try {
|
|
25184
|
+
if (toolName === "list_issues") {
|
|
25185
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
25186
|
+
const statusArg = args.status ? String(args.status) : undefined;
|
|
25187
|
+
let status;
|
|
25188
|
+
if (statusArg === "open")
|
|
25189
|
+
status = "open";
|
|
25190
|
+
else if (statusArg === "closed")
|
|
25191
|
+
status = "closed";
|
|
25192
|
+
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
|
|
25193
|
+
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
25194
|
+
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
|
|
25195
|
+
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
25196
|
+
}
|
|
25197
|
+
if (toolName === "view_issue") {
|
|
25198
|
+
const issueId = String(args.issue_id || "").trim();
|
|
25199
|
+
if (!issueId) {
|
|
25200
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
25201
|
+
}
|
|
25202
|
+
const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
25203
|
+
if (!issue2) {
|
|
25204
|
+
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
25205
|
+
}
|
|
25206
|
+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
|
|
25207
|
+
const combined = { issue: issue2, comments };
|
|
24422
25208
|
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
24423
25209
|
}
|
|
24424
25210
|
if (toolName === "post_issue_comment") {
|
|
@@ -24653,6 +25439,147 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24653
25439
|
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
|
|
24654
25440
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
24655
25441
|
}
|
|
25442
|
+
if (toolName === "list_projects") {
|
|
25443
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
25444
|
+
const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
25445
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
25446
|
+
}
|
|
25447
|
+
if (toolName in JOE_TOOL_TO_COMMAND) {
|
|
25448
|
+
const orgId = cfg.orgId ?? undefined;
|
|
25449
|
+
return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
|
|
25450
|
+
}
|
|
25451
|
+
if (toolName === "clone_create") {
|
|
25452
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25453
|
+
const result = await createClone({
|
|
25454
|
+
apiKey,
|
|
25455
|
+
apiBaseUrl,
|
|
25456
|
+
instanceId,
|
|
25457
|
+
cloneId: optStr(args.clone_id),
|
|
25458
|
+
branch: optStr(args.branch),
|
|
25459
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25460
|
+
dbUser: optStr(args.db_user),
|
|
25461
|
+
dbPassword: optStr(args.db_password),
|
|
25462
|
+
isProtected: args.protected === true,
|
|
25463
|
+
debug
|
|
25464
|
+
});
|
|
25465
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25466
|
+
}
|
|
25467
|
+
if (toolName === "clone_list") {
|
|
25468
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25469
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25470
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25471
|
+
}
|
|
25472
|
+
if (toolName === "clone_status") {
|
|
25473
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25474
|
+
if (!cloneId)
|
|
25475
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25476
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25477
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25478
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25479
|
+
}
|
|
25480
|
+
if (toolName === "clone_reset") {
|
|
25481
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25482
|
+
if (!cloneId)
|
|
25483
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25484
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25485
|
+
const result = await resetClone({
|
|
25486
|
+
apiKey,
|
|
25487
|
+
apiBaseUrl,
|
|
25488
|
+
instanceId,
|
|
25489
|
+
cloneId,
|
|
25490
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25491
|
+
latest: args.latest === true ? true : undefined,
|
|
25492
|
+
debug
|
|
25493
|
+
});
|
|
25494
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
|
|
25495
|
+
}
|
|
25496
|
+
if (toolName === "clone_destroy") {
|
|
25497
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25498
|
+
if (!cloneId)
|
|
25499
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25500
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25501
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
|
|
25502
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
|
|
25503
|
+
}
|
|
25504
|
+
if (toolName === "branch_list") {
|
|
25505
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25506
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
|
|
25507
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25508
|
+
}
|
|
25509
|
+
if (toolName === "branch_create") {
|
|
25510
|
+
const branchName = String(args.name || "").trim();
|
|
25511
|
+
if (!branchName)
|
|
25512
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25513
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25514
|
+
const result = await createBranch({
|
|
25515
|
+
apiKey,
|
|
25516
|
+
apiBaseUrl,
|
|
25517
|
+
instanceId,
|
|
25518
|
+
branchName,
|
|
25519
|
+
baseBranch: optStr(args.base_branch),
|
|
25520
|
+
snapshotId: optStr(args.snapshot_id),
|
|
25521
|
+
debug
|
|
25522
|
+
});
|
|
25523
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25524
|
+
}
|
|
25525
|
+
if (toolName === "branch_delete") {
|
|
25526
|
+
const branchName = String(args.name || "").trim();
|
|
25527
|
+
if (!branchName)
|
|
25528
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25529
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25530
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25531
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
|
|
25532
|
+
}
|
|
25533
|
+
if (toolName === "branch_log") {
|
|
25534
|
+
const branchName = String(args.name || "").trim();
|
|
25535
|
+
if (!branchName)
|
|
25536
|
+
return { content: [{ type: "text", text: "name is required" }], isError: true };
|
|
25537
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25538
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
|
|
25539
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25540
|
+
}
|
|
25541
|
+
if (toolName === "snapshot_list") {
|
|
25542
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25543
|
+
const result = await listSnapshots({
|
|
25544
|
+
apiKey,
|
|
25545
|
+
apiBaseUrl,
|
|
25546
|
+
instanceId,
|
|
25547
|
+
branchName: optStr(args.branch),
|
|
25548
|
+
dataset: optStr(args.dataset),
|
|
25549
|
+
debug
|
|
25550
|
+
});
|
|
25551
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25552
|
+
}
|
|
25553
|
+
if (toolName === "snapshot_create") {
|
|
25554
|
+
const cloneId = String(args.clone_id || "").trim();
|
|
25555
|
+
if (!cloneId)
|
|
25556
|
+
return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
|
|
25557
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25558
|
+
const result = await createSnapshot({
|
|
25559
|
+
apiKey,
|
|
25560
|
+
apiBaseUrl,
|
|
25561
|
+
instanceId,
|
|
25562
|
+
cloneId,
|
|
25563
|
+
message: optStr(args.message),
|
|
25564
|
+
debug
|
|
25565
|
+
});
|
|
25566
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
25567
|
+
}
|
|
25568
|
+
if (toolName === "snapshot_destroy") {
|
|
25569
|
+
const snapshotId = String(args.snapshot_id || "").trim();
|
|
25570
|
+
if (!snapshotId)
|
|
25571
|
+
return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
|
|
25572
|
+
const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
|
|
25573
|
+
const result = await destroySnapshot({
|
|
25574
|
+
apiKey,
|
|
25575
|
+
apiBaseUrl,
|
|
25576
|
+
instanceId,
|
|
25577
|
+
snapshotId,
|
|
25578
|
+
force: args.force === true,
|
|
25579
|
+
debug
|
|
25580
|
+
});
|
|
25581
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
|
|
25582
|
+
}
|
|
24656
25583
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
24657
25584
|
} catch (err) {
|
|
24658
25585
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24929,6 +25856,197 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
24929
25856
|
},
|
|
24930
25857
|
additionalProperties: false
|
|
24931
25858
|
}
|
|
25859
|
+
},
|
|
25860
|
+
...joeToolDefinitions(),
|
|
25861
|
+
{
|
|
25862
|
+
name: "clone_create",
|
|
25863
|
+
description: "Create a DBLab thin clone of the project's database (same as CLI 'clone create'). Requires the joe:plan scope.",
|
|
25864
|
+
inputSchema: {
|
|
25865
|
+
type: "object",
|
|
25866
|
+
properties: {
|
|
25867
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25868
|
+
branch: { type: "string", description: "Branch to clone from" },
|
|
25869
|
+
snapshot_id: { type: "string", description: "Snapshot id to clone from" },
|
|
25870
|
+
clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
|
|
25871
|
+
db_user: { type: "string", description: "Clone DB user" },
|
|
25872
|
+
db_password: { type: "string", description: "Clone DB password" },
|
|
25873
|
+
protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
|
|
25874
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25875
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25876
|
+
},
|
|
25877
|
+
required: ["project"],
|
|
25878
|
+
additionalProperties: false
|
|
25879
|
+
}
|
|
25880
|
+
},
|
|
25881
|
+
{
|
|
25882
|
+
name: "clone_list",
|
|
25883
|
+
description: "List the project's DBLab thin clones (same as CLI 'clone list').",
|
|
25884
|
+
inputSchema: {
|
|
25885
|
+
type: "object",
|
|
25886
|
+
properties: {
|
|
25887
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25888
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25889
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25890
|
+
},
|
|
25891
|
+
required: ["project"],
|
|
25892
|
+
additionalProperties: false
|
|
25893
|
+
}
|
|
25894
|
+
},
|
|
25895
|
+
{
|
|
25896
|
+
name: "clone_status",
|
|
25897
|
+
description: "Show a DBLab clone's status (same as CLI 'clone status').",
|
|
25898
|
+
inputSchema: {
|
|
25899
|
+
type: "object",
|
|
25900
|
+
properties: {
|
|
25901
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25902
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25903
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25904
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25905
|
+
},
|
|
25906
|
+
required: ["project", "clone_id"],
|
|
25907
|
+
additionalProperties: false
|
|
25908
|
+
}
|
|
25909
|
+
},
|
|
25910
|
+
{
|
|
25911
|
+
name: "clone_reset",
|
|
25912
|
+
description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'clone reset'). Requires the joe:admin scope.",
|
|
25913
|
+
inputSchema: {
|
|
25914
|
+
type: "object",
|
|
25915
|
+
properties: {
|
|
25916
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25917
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25918
|
+
snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
|
|
25919
|
+
latest: { type: "boolean", description: "Reset to the latest snapshot" },
|
|
25920
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25921
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25922
|
+
},
|
|
25923
|
+
required: ["project", "clone_id"],
|
|
25924
|
+
additionalProperties: false
|
|
25925
|
+
}
|
|
25926
|
+
},
|
|
25927
|
+
{
|
|
25928
|
+
name: "clone_destroy",
|
|
25929
|
+
description: "Destroy a DBLab clone (same as CLI 'clone destroy'). Requires the joe:admin scope.",
|
|
25930
|
+
inputSchema: {
|
|
25931
|
+
type: "object",
|
|
25932
|
+
properties: {
|
|
25933
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25934
|
+
clone_id: { type: "string", description: "Clone id" },
|
|
25935
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25936
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25937
|
+
},
|
|
25938
|
+
required: ["project", "clone_id"],
|
|
25939
|
+
additionalProperties: false
|
|
25940
|
+
}
|
|
25941
|
+
},
|
|
25942
|
+
{
|
|
25943
|
+
name: "branch_list",
|
|
25944
|
+
description: "List the project's DBLab branches (same as CLI 'branch list').",
|
|
25945
|
+
inputSchema: {
|
|
25946
|
+
type: "object",
|
|
25947
|
+
properties: {
|
|
25948
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25949
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25950
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25951
|
+
},
|
|
25952
|
+
required: ["project"],
|
|
25953
|
+
additionalProperties: false
|
|
25954
|
+
}
|
|
25955
|
+
},
|
|
25956
|
+
{
|
|
25957
|
+
name: "branch_create",
|
|
25958
|
+
description: "Create a DBLab branch (same as CLI 'branch create'). Requires the joe:plan scope.",
|
|
25959
|
+
inputSchema: {
|
|
25960
|
+
type: "object",
|
|
25961
|
+
properties: {
|
|
25962
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25963
|
+
name: { type: "string", description: "Branch name" },
|
|
25964
|
+
snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
|
|
25965
|
+
base_branch: { type: "string", description: "Parent branch to fork from" },
|
|
25966
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25967
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25968
|
+
},
|
|
25969
|
+
required: ["project", "name"],
|
|
25970
|
+
additionalProperties: false
|
|
25971
|
+
}
|
|
25972
|
+
},
|
|
25973
|
+
{
|
|
25974
|
+
name: "branch_delete",
|
|
25975
|
+
description: "Delete a DBLab branch (same as CLI 'branch delete'). Requires the joe:admin scope.",
|
|
25976
|
+
inputSchema: {
|
|
25977
|
+
type: "object",
|
|
25978
|
+
properties: {
|
|
25979
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25980
|
+
name: { type: "string", description: "Branch name" },
|
|
25981
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25982
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25983
|
+
},
|
|
25984
|
+
required: ["project", "name"],
|
|
25985
|
+
additionalProperties: false
|
|
25986
|
+
}
|
|
25987
|
+
},
|
|
25988
|
+
{
|
|
25989
|
+
name: "branch_log",
|
|
25990
|
+
description: "Show a DBLab branch's snapshot log (same as CLI 'branch log').",
|
|
25991
|
+
inputSchema: {
|
|
25992
|
+
type: "object",
|
|
25993
|
+
properties: {
|
|
25994
|
+
project: { type: "string", description: "Project id or alias" },
|
|
25995
|
+
name: { type: "string", description: "Branch name" },
|
|
25996
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
25997
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
25998
|
+
},
|
|
25999
|
+
required: ["project", "name"],
|
|
26000
|
+
additionalProperties: false
|
|
26001
|
+
}
|
|
26002
|
+
},
|
|
26003
|
+
{
|
|
26004
|
+
name: "snapshot_list",
|
|
26005
|
+
description: "List the project's DBLab snapshots (same as CLI 'snapshot list').",
|
|
26006
|
+
inputSchema: {
|
|
26007
|
+
type: "object",
|
|
26008
|
+
properties: {
|
|
26009
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26010
|
+
branch: { type: "string", description: "Filter by branch" },
|
|
26011
|
+
dataset: { type: "string", description: "Filter by dataset" },
|
|
26012
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26013
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26014
|
+
},
|
|
26015
|
+
required: ["project"],
|
|
26016
|
+
additionalProperties: false
|
|
26017
|
+
}
|
|
26018
|
+
},
|
|
26019
|
+
{
|
|
26020
|
+
name: "snapshot_create",
|
|
26021
|
+
description: "Create a DBLab snapshot from a clone (same as CLI 'snapshot create'). Requires the joe:plan scope.",
|
|
26022
|
+
inputSchema: {
|
|
26023
|
+
type: "object",
|
|
26024
|
+
properties: {
|
|
26025
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26026
|
+
clone_id: { type: "string", description: "Clone id to snapshot" },
|
|
26027
|
+
message: { type: "string", description: "Snapshot message" },
|
|
26028
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26029
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26030
|
+
},
|
|
26031
|
+
required: ["project", "clone_id"],
|
|
26032
|
+
additionalProperties: false
|
|
26033
|
+
}
|
|
26034
|
+
},
|
|
26035
|
+
{
|
|
26036
|
+
name: "snapshot_destroy",
|
|
26037
|
+
description: "Destroy a DBLab snapshot (same as CLI 'snapshot destroy'). Requires the joe:admin scope.",
|
|
26038
|
+
inputSchema: {
|
|
26039
|
+
type: "object",
|
|
26040
|
+
properties: {
|
|
26041
|
+
project: { type: "string", description: "Project id or alias" },
|
|
26042
|
+
snapshot_id: { type: "string", description: "Snapshot id" },
|
|
26043
|
+
force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
|
|
26044
|
+
org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
|
|
26045
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
26046
|
+
},
|
|
26047
|
+
required: ["project", "snapshot_id"],
|
|
26048
|
+
additionalProperties: false
|
|
26049
|
+
}
|
|
24932
26050
|
}
|
|
24933
26051
|
]
|
|
24934
26052
|
};
|
|
@@ -25801,11 +26919,689 @@ function renderMarkdownForTerminal(md) {
|
|
|
25801
26919
|
`);
|
|
25802
26920
|
}
|
|
25803
26921
|
|
|
26922
|
+
// lib/joe.ts
|
|
26923
|
+
import * as fs5 from "fs";
|
|
26924
|
+
import * as path5 from "path";
|
|
26925
|
+
import * as crypto2 from "node:crypto";
|
|
26926
|
+
var JOE_COMMANDS2 = [
|
|
26927
|
+
"plan",
|
|
26928
|
+
"explain",
|
|
26929
|
+
"exec",
|
|
26930
|
+
"hypo",
|
|
26931
|
+
"activity",
|
|
26932
|
+
"terminate",
|
|
26933
|
+
"reset",
|
|
26934
|
+
"describe"
|
|
26935
|
+
];
|
|
26936
|
+
var TERMINAL_STATES2 = new Set([
|
|
26937
|
+
"done",
|
|
26938
|
+
"error",
|
|
26939
|
+
"timed_out"
|
|
26940
|
+
]);
|
|
26941
|
+
var AI_ENABLED_COMMANDS2 = new Set([
|
|
26942
|
+
"plan",
|
|
26943
|
+
"explain",
|
|
26944
|
+
"exec",
|
|
26945
|
+
"hypo",
|
|
26946
|
+
"activity",
|
|
26947
|
+
"describe"
|
|
26948
|
+
]);
|
|
26949
|
+
var EXECUTING_COMMANDS2 = new Set([
|
|
26950
|
+
"exec",
|
|
26951
|
+
"explain"
|
|
26952
|
+
]);
|
|
26953
|
+
var DEFAULT_BUDGET_MS2 = 25000;
|
|
26954
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 800;
|
|
26955
|
+
async function callRpc2(params) {
|
|
26956
|
+
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
26957
|
+
if (!apiKey) {
|
|
26958
|
+
throw new Error("API key is required");
|
|
26959
|
+
}
|
|
26960
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
26961
|
+
const url = new URL(`${base}/rpc/${fn}`);
|
|
26962
|
+
const payload = JSON.stringify(body);
|
|
26963
|
+
const headers = {
|
|
26964
|
+
"access-token": apiKey,
|
|
26965
|
+
"Content-Type": "application/json",
|
|
26966
|
+
Connection: "close"
|
|
26967
|
+
};
|
|
26968
|
+
if (debug) {
|
|
26969
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
26970
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
26971
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
26972
|
+
console.error(`Debug: Request body: ${payload}`);
|
|
26973
|
+
}
|
|
26974
|
+
const response = await fetch(url.toString(), {
|
|
26975
|
+
method: "POST",
|
|
26976
|
+
headers,
|
|
26977
|
+
body: payload
|
|
26978
|
+
});
|
|
26979
|
+
const text = await response.text();
|
|
26980
|
+
if (debug) {
|
|
26981
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
26982
|
+
console.error(`Debug: Response body: ${text}`);
|
|
26983
|
+
}
|
|
26984
|
+
if (!response.ok) {
|
|
26985
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
26986
|
+
}
|
|
26987
|
+
try {
|
|
26988
|
+
return JSON.parse(text);
|
|
26989
|
+
} catch {
|
|
26990
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
26991
|
+
}
|
|
26992
|
+
}
|
|
26993
|
+
async function submitCommand2(params) {
|
|
26994
|
+
const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
|
|
26995
|
+
if (!JOE_COMMANDS2.includes(command)) {
|
|
26996
|
+
throw new Error(`Unknown Joe command: ${command}`);
|
|
26997
|
+
}
|
|
26998
|
+
const body = {
|
|
26999
|
+
project_id: projectId,
|
|
27000
|
+
command,
|
|
27001
|
+
sql: sql ?? null,
|
|
27002
|
+
args: args ?? null,
|
|
27003
|
+
session_id: sessionId ?? null
|
|
27004
|
+
};
|
|
27005
|
+
if (idempotencyKey) {
|
|
27006
|
+
body.idempotency_key = idempotencyKey;
|
|
27007
|
+
}
|
|
27008
|
+
return callRpc2({
|
|
27009
|
+
apiKey,
|
|
27010
|
+
apiBaseUrl,
|
|
27011
|
+
fn: "joe_command_submit",
|
|
27012
|
+
body,
|
|
27013
|
+
operation: `Failed to submit ${command} command`,
|
|
27014
|
+
debug
|
|
27015
|
+
});
|
|
27016
|
+
}
|
|
27017
|
+
async function getCommandStatus2(params) {
|
|
27018
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27019
|
+
if (!commandId) {
|
|
27020
|
+
throw new Error("commandId is required");
|
|
27021
|
+
}
|
|
27022
|
+
return callRpc2({
|
|
27023
|
+
apiKey,
|
|
27024
|
+
apiBaseUrl,
|
|
27025
|
+
fn: "joe_command_status",
|
|
27026
|
+
body: { command_id: commandId },
|
|
27027
|
+
operation: "Failed to fetch command status",
|
|
27028
|
+
debug
|
|
27029
|
+
});
|
|
27030
|
+
}
|
|
27031
|
+
async function getCommandResult2(params) {
|
|
27032
|
+
const { apiKey, apiBaseUrl, commandId, debug } = params;
|
|
27033
|
+
if (!commandId) {
|
|
27034
|
+
throw new Error("commandId is required");
|
|
27035
|
+
}
|
|
27036
|
+
return callRpc2({
|
|
27037
|
+
apiKey,
|
|
27038
|
+
apiBaseUrl,
|
|
27039
|
+
fn: "joe_command_result",
|
|
27040
|
+
body: { command_id: commandId },
|
|
27041
|
+
operation: "Failed to fetch command result",
|
|
27042
|
+
debug
|
|
27043
|
+
});
|
|
27044
|
+
}
|
|
27045
|
+
function normalizeProjectRow2(row) {
|
|
27046
|
+
const projectId = row.project_id ?? row.id ?? 0;
|
|
27047
|
+
return {
|
|
27048
|
+
project_id: Number(projectId),
|
|
27049
|
+
alias: row.alias ?? null,
|
|
27050
|
+
name: row.name ?? null,
|
|
27051
|
+
label: row.label ?? null,
|
|
27052
|
+
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
27053
|
+
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
27054
|
+
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
27055
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
|
|
27056
|
+
};
|
|
27057
|
+
}
|
|
27058
|
+
async function listProjects2(params) {
|
|
27059
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27060
|
+
const body = {};
|
|
27061
|
+
if (typeof orgId === "number") {
|
|
27062
|
+
body.org_id = orgId;
|
|
27063
|
+
}
|
|
27064
|
+
const rows = await callRpc2({
|
|
27065
|
+
apiKey,
|
|
27066
|
+
apiBaseUrl,
|
|
27067
|
+
fn: "projects_list",
|
|
27068
|
+
body,
|
|
27069
|
+
operation: "Failed to list projects",
|
|
27070
|
+
debug
|
|
27071
|
+
});
|
|
27072
|
+
if (!Array.isArray(rows)) {
|
|
27073
|
+
return [];
|
|
27074
|
+
}
|
|
27075
|
+
return rows.map(normalizeProjectRow2);
|
|
27076
|
+
}
|
|
27077
|
+
function isNumericProjectRef3(ref) {
|
|
27078
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
27079
|
+
}
|
|
27080
|
+
async function resolveProjectId2(params) {
|
|
27081
|
+
const ref = String(params.project ?? "").trim();
|
|
27082
|
+
if (!ref) {
|
|
27083
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27084
|
+
}
|
|
27085
|
+
if (isNumericProjectRef3(ref)) {
|
|
27086
|
+
return Number(ref);
|
|
27087
|
+
}
|
|
27088
|
+
const projects = await listProjects2({
|
|
27089
|
+
apiKey: params.apiKey,
|
|
27090
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27091
|
+
orgId: params.orgId,
|
|
27092
|
+
debug: params.debug
|
|
27093
|
+
});
|
|
27094
|
+
const needle = ref.toLowerCase();
|
|
27095
|
+
const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
|
|
27096
|
+
if (!match) {
|
|
27097
|
+
throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27098
|
+
}
|
|
27099
|
+
return match.project_id;
|
|
27100
|
+
}
|
|
27101
|
+
function sessionStorePath2(dir) {
|
|
27102
|
+
return path5.join(dir ?? getConfigDir2(), "joe-sessions.json");
|
|
27103
|
+
}
|
|
27104
|
+
function readSessionStore2(dir) {
|
|
27105
|
+
const file = sessionStorePath2(dir);
|
|
27106
|
+
if (!fs5.existsSync(file)) {
|
|
27107
|
+
return {};
|
|
27108
|
+
}
|
|
27109
|
+
try {
|
|
27110
|
+
const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
27111
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
27112
|
+
return parsed;
|
|
27113
|
+
}
|
|
27114
|
+
} catch {}
|
|
27115
|
+
return {};
|
|
27116
|
+
}
|
|
27117
|
+
function readStoredSessionId2(projectId, dir) {
|
|
27118
|
+
const store = readSessionStore2(dir);
|
|
27119
|
+
const value = store[String(projectId)];
|
|
27120
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
27121
|
+
}
|
|
27122
|
+
function writeStoredSessionId2(projectId, sessionId, dir) {
|
|
27123
|
+
const baseDir = dir ?? getConfigDir2();
|
|
27124
|
+
if (!fs5.existsSync(baseDir)) {
|
|
27125
|
+
fs5.mkdirSync(baseDir, { recursive: true, mode: 448 });
|
|
27126
|
+
}
|
|
27127
|
+
const store = readSessionStore2(dir);
|
|
27128
|
+
store[String(projectId)] = sessionId;
|
|
27129
|
+
fs5.writeFileSync(sessionStorePath2(dir), JSON.stringify(store, null, 2) + `
|
|
27130
|
+
`, { mode: 384 });
|
|
27131
|
+
}
|
|
27132
|
+
function clearStoredSessionId2(projectId, dir) {
|
|
27133
|
+
const file = sessionStorePath2(dir);
|
|
27134
|
+
if (!fs5.existsSync(file)) {
|
|
27135
|
+
return;
|
|
27136
|
+
}
|
|
27137
|
+
const store = readSessionStore2(dir);
|
|
27138
|
+
if (String(projectId) in store) {
|
|
27139
|
+
delete store[String(projectId)];
|
|
27140
|
+
fs5.writeFileSync(file, JSON.stringify(store, null, 2) + `
|
|
27141
|
+
`, { mode: 384 });
|
|
27142
|
+
}
|
|
27143
|
+
}
|
|
27144
|
+
function computeIdempotencyKey2(command, projectId, sql, args) {
|
|
27145
|
+
if (EXECUTING_COMMANDS2.has(command)) {
|
|
27146
|
+
const canonical = `${projectId}
|
|
27147
|
+
${command}
|
|
27148
|
+
${sql ?? ""}
|
|
27149
|
+
${JSON.stringify(args ?? null)}`;
|
|
27150
|
+
return `joe-${crypto2.createHash("sha256").update(canonical).digest("hex")}`;
|
|
27151
|
+
}
|
|
27152
|
+
return `joe-${crypto2.randomUUID()}`;
|
|
27153
|
+
}
|
|
27154
|
+
var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27155
|
+
async function runCommand2(params) {
|
|
27156
|
+
const {
|
|
27157
|
+
apiKey,
|
|
27158
|
+
apiBaseUrl,
|
|
27159
|
+
command,
|
|
27160
|
+
projectId,
|
|
27161
|
+
sql,
|
|
27162
|
+
args,
|
|
27163
|
+
sessionId,
|
|
27164
|
+
debug
|
|
27165
|
+
} = params;
|
|
27166
|
+
const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS2;
|
|
27167
|
+
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
|
|
27168
|
+
const now = params.now ?? Date.now;
|
|
27169
|
+
const sleep = params.sleep ?? defaultSleep2;
|
|
27170
|
+
const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey2(command, projectId, sql, args);
|
|
27171
|
+
const submitted = await submitCommand2({
|
|
27172
|
+
apiKey,
|
|
27173
|
+
apiBaseUrl,
|
|
27174
|
+
command,
|
|
27175
|
+
projectId,
|
|
27176
|
+
sql,
|
|
27177
|
+
args,
|
|
27178
|
+
sessionId,
|
|
27179
|
+
idempotencyKey,
|
|
27180
|
+
debug
|
|
27181
|
+
});
|
|
27182
|
+
const commandId = submitted.command_id;
|
|
27183
|
+
const newSessionId = submitted.session_id ?? sessionId ?? null;
|
|
27184
|
+
const deadline = now() + budgetMs;
|
|
27185
|
+
let status = submitted.status;
|
|
27186
|
+
while (true) {
|
|
27187
|
+
const statusResp = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27188
|
+
status = statusResp.status;
|
|
27189
|
+
if (TERMINAL_STATES2.has(status)) {
|
|
27190
|
+
break;
|
|
27191
|
+
}
|
|
27192
|
+
if (now() >= deadline) {
|
|
27193
|
+
return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
|
|
27194
|
+
}
|
|
27195
|
+
await sleep(pollIntervalMs);
|
|
27196
|
+
}
|
|
27197
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
|
|
27198
|
+
return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
|
|
27199
|
+
}
|
|
27200
|
+
async function executeJoeCommand2(params) {
|
|
27201
|
+
const projectId = await resolveProjectId2({
|
|
27202
|
+
apiKey: params.apiKey,
|
|
27203
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27204
|
+
project: params.project,
|
|
27205
|
+
orgId: params.orgId,
|
|
27206
|
+
debug: params.debug
|
|
27207
|
+
});
|
|
27208
|
+
let sessionId;
|
|
27209
|
+
if (params.newSession) {
|
|
27210
|
+
clearStoredSessionId2(projectId, params.sessionDir);
|
|
27211
|
+
sessionId = null;
|
|
27212
|
+
} else if (params.session) {
|
|
27213
|
+
sessionId = String(params.session);
|
|
27214
|
+
} else {
|
|
27215
|
+
sessionId = readStoredSessionId2(projectId, params.sessionDir);
|
|
27216
|
+
}
|
|
27217
|
+
const outcome = await runCommand2({
|
|
27218
|
+
apiKey: params.apiKey,
|
|
27219
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
27220
|
+
command: params.command,
|
|
27221
|
+
projectId,
|
|
27222
|
+
sql: params.sql,
|
|
27223
|
+
args: params.args,
|
|
27224
|
+
sessionId,
|
|
27225
|
+
budgetMs: params.budgetMs,
|
|
27226
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
27227
|
+
debug: params.debug,
|
|
27228
|
+
now: params.now,
|
|
27229
|
+
sleep: params.sleep
|
|
27230
|
+
});
|
|
27231
|
+
if (outcome.sessionId) {
|
|
27232
|
+
writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
|
|
27233
|
+
}
|
|
27234
|
+
return { ...outcome, command: params.command, projectId };
|
|
27235
|
+
}
|
|
27236
|
+
function clientSidePlanFlags(planJson) {
|
|
27237
|
+
const flags = [];
|
|
27238
|
+
const walk = (node) => {
|
|
27239
|
+
if (!node || typeof node !== "object") {
|
|
27240
|
+
return;
|
|
27241
|
+
}
|
|
27242
|
+
const nodeType = node["Node Type"];
|
|
27243
|
+
if (nodeType === "Seq Scan") {
|
|
27244
|
+
const rel = node["Relation Name"];
|
|
27245
|
+
flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
|
|
27246
|
+
}
|
|
27247
|
+
if (Array.isArray(node.Plans)) {
|
|
27248
|
+
for (const child of node.Plans) {
|
|
27249
|
+
walk(child);
|
|
27250
|
+
}
|
|
27251
|
+
}
|
|
27252
|
+
};
|
|
27253
|
+
if (planJson && typeof planJson === "object") {
|
|
27254
|
+
const root = planJson;
|
|
27255
|
+
walk(root.Plan ?? planJson);
|
|
27256
|
+
}
|
|
27257
|
+
return flags;
|
|
27258
|
+
}
|
|
27259
|
+
function formatJoeResult(command, result) {
|
|
27260
|
+
const lines = [];
|
|
27261
|
+
switch (command) {
|
|
27262
|
+
case "plan":
|
|
27263
|
+
case "explain": {
|
|
27264
|
+
if (result.plan_text) {
|
|
27265
|
+
lines.push(result.plan_text);
|
|
27266
|
+
}
|
|
27267
|
+
for (const flag of clientSidePlanFlags(result.plan_json)) {
|
|
27268
|
+
lines.push(`⚑ ${flag}`);
|
|
27269
|
+
}
|
|
27270
|
+
if (result.queryid || result.plan_fingerprint) {
|
|
27271
|
+
lines.push(`(queryid ${result.queryid ?? "?"} · plan_fp ${result.plan_fingerprint ?? "?"})`);
|
|
27272
|
+
}
|
|
27273
|
+
break;
|
|
27274
|
+
}
|
|
27275
|
+
case "exec": {
|
|
27276
|
+
const notices = result.notices ?? [];
|
|
27277
|
+
const rowCount = result.row_count ?? 0;
|
|
27278
|
+
lines.push(`${notices.join(" ") || "OK"} · ${rowCount} row${rowCount === 1 ? "" : "s"}`);
|
|
27279
|
+
if (result.result_rows && result.result_rows.length > 0) {
|
|
27280
|
+
lines.push(JSON.stringify(result.result_rows, null, 2));
|
|
27281
|
+
}
|
|
27282
|
+
break;
|
|
27283
|
+
}
|
|
27284
|
+
case "hypo": {
|
|
27285
|
+
lines.push(result.hypo_used ? "hypothetical index would be used" : "hypothetical index would NOT be used");
|
|
27286
|
+
if (result.hypo_plan !== undefined) {
|
|
27287
|
+
lines.push(JSON.stringify(result.hypo_plan, null, 2));
|
|
27288
|
+
}
|
|
27289
|
+
break;
|
|
27290
|
+
}
|
|
27291
|
+
case "activity":
|
|
27292
|
+
case "describe": {
|
|
27293
|
+
lines.push(JSON.stringify(result.snapshot ?? null, null, 2));
|
|
27294
|
+
break;
|
|
27295
|
+
}
|
|
27296
|
+
case "terminate": {
|
|
27297
|
+
lines.push(`terminated ${result.terminated ? "yes" : "no"} · pid ${result.pid ?? "?"}`);
|
|
27298
|
+
break;
|
|
27299
|
+
}
|
|
27300
|
+
case "reset": {
|
|
27301
|
+
lines.push(`reset ${result.reset ? "ok" : "no"}`);
|
|
27302
|
+
break;
|
|
27303
|
+
}
|
|
27304
|
+
}
|
|
27305
|
+
return lines.join(`
|
|
27306
|
+
`);
|
|
27307
|
+
}
|
|
27308
|
+
function formatProjectsTable(projects) {
|
|
27309
|
+
const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
|
|
27310
|
+
const rows = projects.map((p) => [
|
|
27311
|
+
String(p.project_id),
|
|
27312
|
+
p.alias ?? "-",
|
|
27313
|
+
p.name ?? "-",
|
|
27314
|
+
p.joe_ready ? "ready" : "no",
|
|
27315
|
+
p.tunnel ? "yes" : "no"
|
|
27316
|
+
]);
|
|
27317
|
+
const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
|
|
27318
|
+
const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
|
|
27319
|
+
return [pad(header), ...rows.map(pad)].join(`
|
|
27320
|
+
`);
|
|
27321
|
+
}
|
|
27322
|
+
|
|
27323
|
+
// lib/dblab.ts
|
|
27324
|
+
function isNumericProjectRef4(ref) {
|
|
27325
|
+
return /^[0-9]+$/.test(ref.trim());
|
|
27326
|
+
}
|
|
27327
|
+
async function resolveDblabInstanceId2(params) {
|
|
27328
|
+
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
27329
|
+
if (!apiKey) {
|
|
27330
|
+
throw new Error("API key is required");
|
|
27331
|
+
}
|
|
27332
|
+
const ref = String(params.project ?? "").trim();
|
|
27333
|
+
if (!ref) {
|
|
27334
|
+
throw new Error("project is required (--project <id|alias>)");
|
|
27335
|
+
}
|
|
27336
|
+
let projects;
|
|
27337
|
+
try {
|
|
27338
|
+
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
27339
|
+
} catch (err) {
|
|
27340
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27341
|
+
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
27342
|
+
}
|
|
27343
|
+
const numeric = isNumericProjectRef4(ref);
|
|
27344
|
+
const needle = ref.toLowerCase();
|
|
27345
|
+
const match = projects.find((p) => {
|
|
27346
|
+
if (numeric) {
|
|
27347
|
+
return String(p.project_id) === ref;
|
|
27348
|
+
}
|
|
27349
|
+
return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
|
|
27350
|
+
});
|
|
27351
|
+
if (!match) {
|
|
27352
|
+
throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
|
|
27353
|
+
}
|
|
27354
|
+
if (match.dblab_instance_id == null) {
|
|
27355
|
+
throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
|
|
27356
|
+
}
|
|
27357
|
+
return String(match.dblab_instance_id);
|
|
27358
|
+
}
|
|
27359
|
+
async function callDblabApi2(params) {
|
|
27360
|
+
const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
|
|
27361
|
+
if (!apiKey) {
|
|
27362
|
+
throw new Error("API key is required");
|
|
27363
|
+
}
|
|
27364
|
+
if (!instanceId) {
|
|
27365
|
+
throw new Error("instanceId is required");
|
|
27366
|
+
}
|
|
27367
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27368
|
+
const url = new URL(`${base}/rpc/dblab_api_call`);
|
|
27369
|
+
const bodyObj = {
|
|
27370
|
+
instance_id: instanceId,
|
|
27371
|
+
action,
|
|
27372
|
+
method
|
|
27373
|
+
};
|
|
27374
|
+
if (data !== undefined) {
|
|
27375
|
+
bodyObj.data = data;
|
|
27376
|
+
}
|
|
27377
|
+
const body = JSON.stringify(bodyObj);
|
|
27378
|
+
const headers = {
|
|
27379
|
+
"access-token": apiKey,
|
|
27380
|
+
"Content-Type": "application/json",
|
|
27381
|
+
Connection: "close"
|
|
27382
|
+
};
|
|
27383
|
+
if (debug) {
|
|
27384
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27385
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27386
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27387
|
+
console.error(`Debug: Request body: ${body}`);
|
|
27388
|
+
}
|
|
27389
|
+
const response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
27390
|
+
const text = await response.text();
|
|
27391
|
+
if (debug) {
|
|
27392
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27393
|
+
console.error(`Debug: Response body: ${text}`);
|
|
27394
|
+
}
|
|
27395
|
+
if (!response.ok) {
|
|
27396
|
+
throw new Error(formatHttpError(operation, response.status, text));
|
|
27397
|
+
}
|
|
27398
|
+
if (text.trim() === "") {
|
|
27399
|
+
return null;
|
|
27400
|
+
}
|
|
27401
|
+
try {
|
|
27402
|
+
return JSON.parse(text);
|
|
27403
|
+
} catch {
|
|
27404
|
+
throw new Error(`${operation}: failed to parse response: ${text}`);
|
|
27405
|
+
}
|
|
27406
|
+
}
|
|
27407
|
+
async function createClone2(params) {
|
|
27408
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
|
|
27409
|
+
const data = { protected: Boolean(isProtected) };
|
|
27410
|
+
if (cloneId)
|
|
27411
|
+
data.id = cloneId;
|
|
27412
|
+
if (branch)
|
|
27413
|
+
data.branch = branch;
|
|
27414
|
+
if (snapshotId)
|
|
27415
|
+
data.snapshot = { id: snapshotId };
|
|
27416
|
+
if (dbUser && dbPassword)
|
|
27417
|
+
data.db = { username: dbUser, password: dbPassword };
|
|
27418
|
+
return callDblabApi2({
|
|
27419
|
+
apiKey,
|
|
27420
|
+
apiBaseUrl,
|
|
27421
|
+
instanceId,
|
|
27422
|
+
action: "/clone",
|
|
27423
|
+
method: "post",
|
|
27424
|
+
data,
|
|
27425
|
+
operation: "Failed to create clone",
|
|
27426
|
+
debug
|
|
27427
|
+
});
|
|
27428
|
+
}
|
|
27429
|
+
async function listClones2(params) {
|
|
27430
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27431
|
+
return callDblabApi2({
|
|
27432
|
+
apiKey,
|
|
27433
|
+
apiBaseUrl,
|
|
27434
|
+
instanceId,
|
|
27435
|
+
action: "/clones",
|
|
27436
|
+
method: "get",
|
|
27437
|
+
operation: "Failed to list clones",
|
|
27438
|
+
debug
|
|
27439
|
+
});
|
|
27440
|
+
}
|
|
27441
|
+
async function getClone2(params) {
|
|
27442
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27443
|
+
if (!cloneId)
|
|
27444
|
+
throw new Error("cloneId is required");
|
|
27445
|
+
return callDblabApi2({
|
|
27446
|
+
apiKey,
|
|
27447
|
+
apiBaseUrl,
|
|
27448
|
+
instanceId,
|
|
27449
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27450
|
+
method: "get",
|
|
27451
|
+
operation: "Failed to get clone",
|
|
27452
|
+
debug
|
|
27453
|
+
});
|
|
27454
|
+
}
|
|
27455
|
+
async function resetClone2(params) {
|
|
27456
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
|
|
27457
|
+
if (!cloneId)
|
|
27458
|
+
throw new Error("cloneId is required");
|
|
27459
|
+
const data = { latest: latest ?? !snapshotId };
|
|
27460
|
+
if (snapshotId)
|
|
27461
|
+
data.snapshotID = snapshotId;
|
|
27462
|
+
return callDblabApi2({
|
|
27463
|
+
apiKey,
|
|
27464
|
+
apiBaseUrl,
|
|
27465
|
+
instanceId,
|
|
27466
|
+
action: `/clone/${encodeURIComponent(cloneId)}/reset`,
|
|
27467
|
+
method: "post",
|
|
27468
|
+
data,
|
|
27469
|
+
operation: "Failed to reset clone",
|
|
27470
|
+
debug
|
|
27471
|
+
});
|
|
27472
|
+
}
|
|
27473
|
+
async function destroyClone2(params) {
|
|
27474
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
|
|
27475
|
+
if (!cloneId)
|
|
27476
|
+
throw new Error("cloneId is required");
|
|
27477
|
+
return callDblabApi2({
|
|
27478
|
+
apiKey,
|
|
27479
|
+
apiBaseUrl,
|
|
27480
|
+
instanceId,
|
|
27481
|
+
action: `/clone/${encodeURIComponent(cloneId)}`,
|
|
27482
|
+
method: "delete",
|
|
27483
|
+
operation: "Failed to destroy clone",
|
|
27484
|
+
debug
|
|
27485
|
+
});
|
|
27486
|
+
}
|
|
27487
|
+
async function listBranches2(params) {
|
|
27488
|
+
const { apiKey, apiBaseUrl, instanceId, debug } = params;
|
|
27489
|
+
return callDblabApi2({
|
|
27490
|
+
apiKey,
|
|
27491
|
+
apiBaseUrl,
|
|
27492
|
+
instanceId,
|
|
27493
|
+
action: "/branches",
|
|
27494
|
+
method: "get",
|
|
27495
|
+
operation: "Failed to list branches",
|
|
27496
|
+
debug
|
|
27497
|
+
});
|
|
27498
|
+
}
|
|
27499
|
+
async function createBranch2(params) {
|
|
27500
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
|
|
27501
|
+
if (!branchName)
|
|
27502
|
+
throw new Error("branchName is required");
|
|
27503
|
+
const data = { branchName };
|
|
27504
|
+
if (baseBranch)
|
|
27505
|
+
data.baseBranch = baseBranch;
|
|
27506
|
+
if (snapshotId)
|
|
27507
|
+
data.snapshotID = snapshotId;
|
|
27508
|
+
return callDblabApi2({
|
|
27509
|
+
apiKey,
|
|
27510
|
+
apiBaseUrl,
|
|
27511
|
+
instanceId,
|
|
27512
|
+
action: "/branch",
|
|
27513
|
+
method: "post",
|
|
27514
|
+
data,
|
|
27515
|
+
operation: "Failed to create branch",
|
|
27516
|
+
debug
|
|
27517
|
+
});
|
|
27518
|
+
}
|
|
27519
|
+
async function deleteBranch2(params) {
|
|
27520
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27521
|
+
if (!branchName)
|
|
27522
|
+
throw new Error("branchName is required");
|
|
27523
|
+
return callDblabApi2({
|
|
27524
|
+
apiKey,
|
|
27525
|
+
apiBaseUrl,
|
|
27526
|
+
instanceId,
|
|
27527
|
+
action: `/branch/${encodeURIComponent(branchName)}`,
|
|
27528
|
+
method: "delete",
|
|
27529
|
+
operation: "Failed to delete branch",
|
|
27530
|
+
debug
|
|
27531
|
+
});
|
|
27532
|
+
}
|
|
27533
|
+
async function branchLog2(params) {
|
|
27534
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
|
|
27535
|
+
if (!branchName)
|
|
27536
|
+
throw new Error("branchName is required");
|
|
27537
|
+
return callDblabApi2({
|
|
27538
|
+
apiKey,
|
|
27539
|
+
apiBaseUrl,
|
|
27540
|
+
instanceId,
|
|
27541
|
+
action: `/branch/${encodeURIComponent(branchName)}/log`,
|
|
27542
|
+
method: "get",
|
|
27543
|
+
operation: "Failed to fetch branch log",
|
|
27544
|
+
debug
|
|
27545
|
+
});
|
|
27546
|
+
}
|
|
27547
|
+
async function listSnapshots2(params) {
|
|
27548
|
+
const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
|
|
27549
|
+
const qs = new URLSearchParams;
|
|
27550
|
+
const branch = branchName?.trim();
|
|
27551
|
+
if (branch)
|
|
27552
|
+
qs.append("branch", branch);
|
|
27553
|
+
if (dataset)
|
|
27554
|
+
qs.append("dataset", dataset);
|
|
27555
|
+
const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
27556
|
+
return callDblabApi2({
|
|
27557
|
+
apiKey,
|
|
27558
|
+
apiBaseUrl,
|
|
27559
|
+
instanceId,
|
|
27560
|
+
action,
|
|
27561
|
+
method: "get",
|
|
27562
|
+
operation: "Failed to list snapshots",
|
|
27563
|
+
debug
|
|
27564
|
+
});
|
|
27565
|
+
}
|
|
27566
|
+
async function createSnapshot2(params) {
|
|
27567
|
+
const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
|
|
27568
|
+
if (!cloneId)
|
|
27569
|
+
throw new Error("cloneId is required");
|
|
27570
|
+
const data = { cloneID: cloneId };
|
|
27571
|
+
if (message)
|
|
27572
|
+
data.message = message;
|
|
27573
|
+
return callDblabApi2({
|
|
27574
|
+
apiKey,
|
|
27575
|
+
apiBaseUrl,
|
|
27576
|
+
instanceId,
|
|
27577
|
+
action: "/branch/snapshot",
|
|
27578
|
+
method: "post",
|
|
27579
|
+
data,
|
|
27580
|
+
operation: "Failed to create snapshot",
|
|
27581
|
+
debug
|
|
27582
|
+
});
|
|
27583
|
+
}
|
|
27584
|
+
async function destroySnapshot2(params) {
|
|
27585
|
+
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
27586
|
+
if (!snapshotId)
|
|
27587
|
+
throw new Error("snapshotId is required");
|
|
27588
|
+
const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
|
|
27589
|
+
return callDblabApi2({
|
|
27590
|
+
apiKey,
|
|
27591
|
+
apiBaseUrl,
|
|
27592
|
+
instanceId,
|
|
27593
|
+
action,
|
|
27594
|
+
method: "delete",
|
|
27595
|
+
operation: "Failed to destroy snapshot",
|
|
27596
|
+
debug
|
|
27597
|
+
});
|
|
27598
|
+
}
|
|
27599
|
+
|
|
25804
27600
|
// bin/postgres-ai.ts
|
|
25805
27601
|
init_util();
|
|
25806
27602
|
|
|
25807
27603
|
// lib/instances.ts
|
|
25808
|
-
import * as
|
|
27604
|
+
import * as fs6 from "fs";
|
|
25809
27605
|
|
|
25810
27606
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
25811
27607
|
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
@@ -28470,11 +30266,11 @@ class InstancesParseError extends Error {
|
|
|
28470
30266
|
}
|
|
28471
30267
|
}
|
|
28472
30268
|
function loadInstances(file) {
|
|
28473
|
-
if (!
|
|
30269
|
+
if (!fs6.existsSync(file))
|
|
28474
30270
|
return [];
|
|
28475
|
-
if (
|
|
30271
|
+
if (fs6.lstatSync(file).isDirectory())
|
|
28476
30272
|
return [];
|
|
28477
|
-
const text =
|
|
30273
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
28478
30274
|
if (text.trim() === "")
|
|
28479
30275
|
return [];
|
|
28480
30276
|
let parsed;
|
|
@@ -28652,8 +30448,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
28652
30448
|
}
|
|
28653
30449
|
|
|
28654
30450
|
// lib/storage.ts
|
|
28655
|
-
import * as
|
|
28656
|
-
import * as
|
|
30451
|
+
import * as fs7 from "fs";
|
|
30452
|
+
import * as path6 from "path";
|
|
28657
30453
|
var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28658
30454
|
var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
|
|
28659
30455
|
var MIME_TYPES2 = {
|
|
@@ -28694,11 +30490,11 @@ async function uploadFile2(params) {
|
|
|
28694
30490
|
if (!filePath) {
|
|
28695
30491
|
throw new Error("filePath is required");
|
|
28696
30492
|
}
|
|
28697
|
-
const resolvedPath =
|
|
28698
|
-
if (!
|
|
30493
|
+
const resolvedPath = path6.resolve(filePath);
|
|
30494
|
+
if (!fs7.existsSync(resolvedPath)) {
|
|
28699
30495
|
throw new Error(`File not found: ${resolvedPath}`);
|
|
28700
30496
|
}
|
|
28701
|
-
const stat =
|
|
30497
|
+
const stat = fs7.statSync(resolvedPath);
|
|
28702
30498
|
if (!stat.isFile()) {
|
|
28703
30499
|
throw new Error(`Not a file: ${resolvedPath}`);
|
|
28704
30500
|
}
|
|
@@ -28710,9 +30506,9 @@ async function uploadFile2(params) {
|
|
|
28710
30506
|
console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
|
|
28711
30507
|
}
|
|
28712
30508
|
const url = `${base}/upload`;
|
|
28713
|
-
const fileBuffer =
|
|
28714
|
-
const fileName =
|
|
28715
|
-
const ext =
|
|
30509
|
+
const fileBuffer = fs7.readFileSync(resolvedPath);
|
|
30510
|
+
const fileName = path6.basename(resolvedPath);
|
|
30511
|
+
const ext = path6.extname(fileName).toLowerCase();
|
|
28716
30512
|
const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
|
|
28717
30513
|
const formData = new FormData;
|
|
28718
30514
|
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
|
|
@@ -28771,15 +30567,15 @@ async function downloadFile2(params) {
|
|
|
28771
30567
|
const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28772
30568
|
fullUrl = `${base}${relativePath}`;
|
|
28773
30569
|
}
|
|
28774
|
-
const urlFilename =
|
|
30570
|
+
const urlFilename = path6.basename(new URL(fullUrl).pathname);
|
|
28775
30571
|
if (!urlFilename) {
|
|
28776
30572
|
throw new Error("Cannot derive filename from URL; please specify --output");
|
|
28777
30573
|
}
|
|
28778
|
-
const saveTo = outputPath ?
|
|
30574
|
+
const saveTo = outputPath ? path6.resolve(outputPath) : path6.resolve(urlFilename);
|
|
28779
30575
|
if (!outputPath) {
|
|
28780
|
-
const normalizedSave =
|
|
28781
|
-
const cwd =
|
|
28782
|
-
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd +
|
|
30576
|
+
const normalizedSave = path6.normalize(saveTo);
|
|
30577
|
+
const cwd = path6.normalize(process.cwd());
|
|
30578
|
+
if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path6.sep)) {
|
|
28783
30579
|
throw new Error("Derived output path escapes current directory; please specify --output");
|
|
28784
30580
|
}
|
|
28785
30581
|
}
|
|
@@ -28811,11 +30607,11 @@ async function downloadFile2(params) {
|
|
|
28811
30607
|
}
|
|
28812
30608
|
const arrayBuffer = await response.arrayBuffer();
|
|
28813
30609
|
const buffer = Buffer.from(arrayBuffer);
|
|
28814
|
-
const parentDir =
|
|
28815
|
-
if (!
|
|
28816
|
-
|
|
30610
|
+
const parentDir = path6.dirname(saveTo);
|
|
30611
|
+
if (!fs7.existsSync(parentDir)) {
|
|
30612
|
+
fs7.mkdirSync(parentDir, { recursive: true });
|
|
28817
30613
|
}
|
|
28818
|
-
|
|
30614
|
+
fs7.writeFileSync(saveTo, buffer);
|
|
28819
30615
|
return {
|
|
28820
30616
|
savedTo: saveTo,
|
|
28821
30617
|
size: buffer.length,
|
|
@@ -28827,9 +30623,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
|
|
|
28827
30623
|
const base = normalizeBaseUrl(storageBaseUrl);
|
|
28828
30624
|
const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
|
|
28829
30625
|
const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
|
|
28830
|
-
const name = filename ||
|
|
30626
|
+
const name = filename || path6.basename(new URL(fullUrl).pathname);
|
|
28831
30627
|
const safeName = name.replace(/[\[\]()]/g, "\\$&");
|
|
28832
|
-
const ext =
|
|
30628
|
+
const ext = path6.extname(name).toLowerCase();
|
|
28833
30629
|
if (IMAGE_EXTENSIONS2.has(ext)) {
|
|
28834
30630
|
return ``;
|
|
28835
30631
|
}
|
|
@@ -28875,8 +30671,8 @@ ${links}`;
|
|
|
28875
30671
|
// lib/init.ts
|
|
28876
30672
|
import { randomBytes } from "crypto";
|
|
28877
30673
|
import { URL as URL2, fileURLToPath } from "url";
|
|
28878
|
-
import * as
|
|
28879
|
-
import * as
|
|
30674
|
+
import * as fs8 from "fs";
|
|
30675
|
+
import * as path7 from "path";
|
|
28880
30676
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
28881
30677
|
var KNOWN_PROVIDERS = ["self-managed", "supabase"];
|
|
28882
30678
|
var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
|
|
@@ -28968,21 +30764,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
|
28968
30764
|
}
|
|
28969
30765
|
function sqlDir() {
|
|
28970
30766
|
const currentFile = fileURLToPath(import.meta.url);
|
|
28971
|
-
const currentDir =
|
|
30767
|
+
const currentDir = path7.dirname(currentFile);
|
|
28972
30768
|
const candidates = [
|
|
28973
|
-
|
|
28974
|
-
|
|
30769
|
+
path7.resolve(currentDir, "..", "sql"),
|
|
30770
|
+
path7.resolve(currentDir, "..", "..", "sql")
|
|
28975
30771
|
];
|
|
28976
30772
|
for (const candidate of candidates) {
|
|
28977
|
-
if (
|
|
30773
|
+
if (fs8.existsSync(candidate)) {
|
|
28978
30774
|
return candidate;
|
|
28979
30775
|
}
|
|
28980
30776
|
}
|
|
28981
30777
|
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
28982
30778
|
}
|
|
28983
30779
|
function loadSqlTemplate(filename) {
|
|
28984
|
-
const p =
|
|
28985
|
-
return
|
|
30780
|
+
const p = path7.join(sqlDir(), filename);
|
|
30781
|
+
return fs8.readFileSync(p, "utf8");
|
|
28986
30782
|
}
|
|
28987
30783
|
function applyTemplate(sql, vars) {
|
|
28988
30784
|
return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
@@ -29452,10 +31248,6 @@ async function verifyInitSetup(params) {
|
|
|
29452
31248
|
}
|
|
29453
31249
|
}
|
|
29454
31250
|
}
|
|
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
31251
|
const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
|
|
29460
31252
|
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
29461
31253
|
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
@@ -29974,15 +31766,6 @@ async function verifyInitSetupViaSupabase(params) {
|
|
|
29974
31766
|
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
29975
31767
|
}
|
|
29976
31768
|
}
|
|
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
31769
|
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
31770
|
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
29988
31771
|
missingRequired.push("function postgres_ai.table_describe exists");
|
|
@@ -30040,9 +31823,9 @@ function escapeLiteral2(value) {
|
|
|
30040
31823
|
}
|
|
30041
31824
|
|
|
30042
31825
|
// lib/pkce.ts
|
|
30043
|
-
import * as
|
|
31826
|
+
import * as crypto3 from "crypto";
|
|
30044
31827
|
function generateRandomString(length = 64) {
|
|
30045
|
-
const bytes =
|
|
31828
|
+
const bytes = crypto3.randomBytes(length);
|
|
30046
31829
|
return base64URLEncode(bytes);
|
|
30047
31830
|
}
|
|
30048
31831
|
function base64URLEncode(buffer) {
|
|
@@ -30052,7 +31835,7 @@ function generateCodeVerifier() {
|
|
|
30052
31835
|
return generateRandomString(32);
|
|
30053
31836
|
}
|
|
30054
31837
|
function generateCodeChallenge(verifier) {
|
|
30055
|
-
const hash =
|
|
31838
|
+
const hash = crypto3.createHash("sha256").update(verifier).digest();
|
|
30056
31839
|
return base64URLEncode(hash);
|
|
30057
31840
|
}
|
|
30058
31841
|
function generateState() {
|
|
@@ -30281,8 +32064,8 @@ import { createInterface } from "readline";
|
|
|
30281
32064
|
import * as childProcess from "child_process";
|
|
30282
32065
|
|
|
30283
32066
|
// lib/checkup.ts
|
|
30284
|
-
import * as
|
|
30285
|
-
import * as
|
|
32067
|
+
import * as fs9 from "fs";
|
|
32068
|
+
import * as path8 from "path";
|
|
30286
32069
|
|
|
30287
32070
|
// lib/metrics-embedded.ts
|
|
30288
32071
|
var METRICS = {
|
|
@@ -32013,7 +33796,7 @@ function buildCheckInfoMap() {
|
|
|
32013
33796
|
}
|
|
32014
33797
|
|
|
32015
33798
|
// lib/checkup.ts
|
|
32016
|
-
var __dirname = "/
|
|
33799
|
+
var __dirname = "/home/agent/workspace/postgresai-joe-v2/cli/lib";
|
|
32017
33800
|
var SECONDS_PER_DAY = 86400;
|
|
32018
33801
|
var SECONDS_PER_HOUR = 3600;
|
|
32019
33802
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -32499,7 +34282,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
|
|
|
32499
34282
|
}
|
|
32500
34283
|
function readTextFileSafe(p) {
|
|
32501
34284
|
try {
|
|
32502
|
-
const value =
|
|
34285
|
+
const value = fs9.readFileSync(p, "utf8").trim();
|
|
32503
34286
|
return value || null;
|
|
32504
34287
|
} catch {
|
|
32505
34288
|
return null;
|
|
@@ -32512,8 +34295,8 @@ function resolveBuildTs() {
|
|
|
32512
34295
|
if (fromFile)
|
|
32513
34296
|
return fromFile;
|
|
32514
34297
|
try {
|
|
32515
|
-
const pkgRoot =
|
|
32516
|
-
const fromPkgFile = readTextFileSafe(
|
|
34298
|
+
const pkgRoot = path8.resolve(__dirname, "..");
|
|
34299
|
+
const fromPkgFile = readTextFileSafe(path8.join(pkgRoot, "BUILD_TS"));
|
|
32517
34300
|
if (fromPkgFile)
|
|
32518
34301
|
return fromPkgFile;
|
|
32519
34302
|
} catch (err) {
|
|
@@ -32521,8 +34304,8 @@ function resolveBuildTs() {
|
|
|
32521
34304
|
console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
|
|
32522
34305
|
}
|
|
32523
34306
|
try {
|
|
32524
|
-
const pkgJsonPath =
|
|
32525
|
-
const st =
|
|
34307
|
+
const pkgJsonPath = path8.resolve(__dirname, "..", "package.json");
|
|
34308
|
+
const st = fs9.statSync(pkgJsonPath);
|
|
32526
34309
|
return st.mtime.toISOString();
|
|
32527
34310
|
} catch (err) {
|
|
32528
34311
|
if (process.env.DEBUG) {
|
|
@@ -33803,7 +35586,7 @@ function summarizeG003(nodeData) {
|
|
|
33803
35586
|
}
|
|
33804
35587
|
|
|
33805
35588
|
// lib/instances.ts
|
|
33806
|
-
import * as
|
|
35589
|
+
import * as fs10 from "fs";
|
|
33807
35590
|
class InstancesParseError2 extends Error {
|
|
33808
35591
|
constructor(file, cause) {
|
|
33809
35592
|
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -33812,11 +35595,11 @@ class InstancesParseError2 extends Error {
|
|
|
33812
35595
|
}
|
|
33813
35596
|
}
|
|
33814
35597
|
function loadInstances2(file) {
|
|
33815
|
-
if (!
|
|
35598
|
+
if (!fs10.existsSync(file))
|
|
33816
35599
|
return [];
|
|
33817
|
-
if (
|
|
35600
|
+
if (fs10.lstatSync(file).isDirectory())
|
|
33818
35601
|
return [];
|
|
33819
|
-
const text =
|
|
35602
|
+
const text = fs10.readFileSync(file, "utf8");
|
|
33820
35603
|
if (text.trim() === "")
|
|
33821
35604
|
return [];
|
|
33822
35605
|
let parsed;
|
|
@@ -33849,22 +35632,22 @@ function buildInstance(name, connStr) {
|
|
|
33849
35632
|
};
|
|
33850
35633
|
}
|
|
33851
35634
|
function addInstanceToFile(file, instance) {
|
|
33852
|
-
if (
|
|
33853
|
-
|
|
35635
|
+
if (fs10.existsSync(file) && fs10.lstatSync(file).isDirectory()) {
|
|
35636
|
+
fs10.rmSync(file, { recursive: true, force: true });
|
|
33854
35637
|
}
|
|
33855
35638
|
const existing = loadInstances2(file);
|
|
33856
35639
|
if (existing.some((i3) => i3.name === instance.name)) {
|
|
33857
35640
|
throw new Error(`Monitoring target '${instance.name}' already exists`);
|
|
33858
35641
|
}
|
|
33859
35642
|
existing.push(instance);
|
|
33860
|
-
|
|
35643
|
+
fs10.writeFileSync(file, dump2(existing), "utf8");
|
|
33861
35644
|
}
|
|
33862
35645
|
function removeInstanceFromFile(file, name) {
|
|
33863
35646
|
const instances = loadInstances2(file);
|
|
33864
35647
|
const filtered = instances.filter((i3) => i3.name !== name);
|
|
33865
35648
|
if (filtered.length === instances.length)
|
|
33866
35649
|
return false;
|
|
33867
|
-
|
|
35650
|
+
fs10.writeFileSync(file, dump2(filtered), "utf8");
|
|
33868
35651
|
return true;
|
|
33869
35652
|
}
|
|
33870
35653
|
function extractSslmode(connStr) {
|
|
@@ -33959,13 +35742,13 @@ function stripMatchingQuotes(value) {
|
|
|
33959
35742
|
return trimmed;
|
|
33960
35743
|
}
|
|
33961
35744
|
var REQUIRED_ENV_KEYS = [
|
|
33962
|
-
{ key: "REPLICATOR_PASSWORD", defaultValue: () =>
|
|
35745
|
+
{ key: "REPLICATOR_PASSWORD", defaultValue: () => crypto4.randomBytes(32).toString("hex"), introducedIn: "0.13" },
|
|
33963
35746
|
{ key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
|
|
33964
|
-
{ key: "VM_AUTH_PASSWORD", defaultValue: () =>
|
|
35747
|
+
{ key: "VM_AUTH_PASSWORD", defaultValue: () => crypto4.randomBytes(18).toString("base64"), introducedIn: "0.15" }
|
|
33965
35748
|
];
|
|
33966
35749
|
function ensureRequiredEnvVars(projectDir) {
|
|
33967
|
-
const envFile =
|
|
33968
|
-
const existing =
|
|
35750
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35751
|
+
const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
|
|
33969
35752
|
const added = [];
|
|
33970
35753
|
const appendLines = [];
|
|
33971
35754
|
for (const spec of REQUIRED_ENV_KEYS) {
|
|
@@ -33984,7 +35767,7 @@ function ensureRequiredEnvVars(projectDir) {
|
|
|
33984
35767
|
` : "") + appendLines.join(`
|
|
33985
35768
|
`) + `
|
|
33986
35769
|
`;
|
|
33987
|
-
|
|
35770
|
+
fs11.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
|
|
33988
35771
|
return added;
|
|
33989
35772
|
}
|
|
33990
35773
|
async function execFilePromise(file, args) {
|
|
@@ -34049,7 +35832,7 @@ function expandHomePath(p) {
|
|
|
34049
35832
|
if (s === "~")
|
|
34050
35833
|
return os3.homedir();
|
|
34051
35834
|
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
34052
|
-
return
|
|
35835
|
+
return path9.join(os3.homedir(), s.slice(2));
|
|
34053
35836
|
}
|
|
34054
35837
|
return s;
|
|
34055
35838
|
}
|
|
@@ -34098,10 +35881,10 @@ function prepareOutputDirectory(outputOpt) {
|
|
|
34098
35881
|
if (!outputOpt)
|
|
34099
35882
|
return;
|
|
34100
35883
|
const outputDir = expandHomePath(outputOpt);
|
|
34101
|
-
const outputPath =
|
|
34102
|
-
if (!
|
|
35884
|
+
const outputPath = path9.isAbsolute(outputDir) ? outputDir : path9.resolve(process.cwd(), outputDir);
|
|
35885
|
+
if (!fs11.existsSync(outputPath)) {
|
|
34103
35886
|
try {
|
|
34104
|
-
|
|
35887
|
+
fs11.mkdirSync(outputPath, { recursive: true });
|
|
34105
35888
|
} catch (e) {
|
|
34106
35889
|
const errAny = e;
|
|
34107
35890
|
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
@@ -34137,7 +35920,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
34137
35920
|
let project = (opts.project || cfg.defaultProject || "").trim();
|
|
34138
35921
|
let projectWasGenerated = false;
|
|
34139
35922
|
if (!project) {
|
|
34140
|
-
project = `project_${
|
|
35923
|
+
project = `project_${crypto4.randomBytes(6).toString("hex")}`;
|
|
34141
35924
|
projectWasGenerated = true;
|
|
34142
35925
|
try {
|
|
34143
35926
|
writeConfig({ defaultProject: project });
|
|
@@ -34183,8 +35966,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
34183
35966
|
}
|
|
34184
35967
|
function writeReportFiles(reports, outputPath) {
|
|
34185
35968
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
34186
|
-
const filePath =
|
|
34187
|
-
|
|
35969
|
+
const filePath = path9.join(outputPath, `${checkId}.json`);
|
|
35970
|
+
fs11.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
34188
35971
|
const title = report.checkTitle || checkId;
|
|
34189
35972
|
console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
|
|
34190
35973
|
}
|
|
@@ -34229,7 +36012,7 @@ function getDefaultMonitoringProjectDir() {
|
|
|
34229
36012
|
const override = process.env.PGAI_PROJECT_DIR;
|
|
34230
36013
|
if (override && override.trim())
|
|
34231
36014
|
return override.trim();
|
|
34232
|
-
return
|
|
36015
|
+
return path9.join(getConfigDir(), "monitoring");
|
|
34233
36016
|
}
|
|
34234
36017
|
async function downloadText(url) {
|
|
34235
36018
|
const controller = new AbortController;
|
|
@@ -34246,12 +36029,12 @@ async function downloadText(url) {
|
|
|
34246
36029
|
}
|
|
34247
36030
|
async function ensureDefaultMonitoringProject() {
|
|
34248
36031
|
const projectDir = getDefaultMonitoringProjectDir();
|
|
34249
|
-
const composeFile =
|
|
34250
|
-
const instancesFile =
|
|
34251
|
-
if (!
|
|
34252
|
-
|
|
36032
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36033
|
+
const instancesFile = path9.resolve(projectDir, "instances.yml");
|
|
36034
|
+
if (!fs11.existsSync(projectDir)) {
|
|
36035
|
+
fs11.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
34253
36036
|
}
|
|
34254
|
-
if (!
|
|
36037
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34255
36038
|
const refs = [
|
|
34256
36039
|
process.env.PGAI_PROJECT_REF,
|
|
34257
36040
|
package_default.version,
|
|
@@ -34263,39 +36046,39 @@ async function ensureDefaultMonitoringProject() {
|
|
|
34263
36046
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
34264
36047
|
try {
|
|
34265
36048
|
const text = await downloadText(url);
|
|
34266
|
-
|
|
36049
|
+
fs11.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
|
|
34267
36050
|
break;
|
|
34268
36051
|
} catch (err) {
|
|
34269
36052
|
lastErr = err;
|
|
34270
36053
|
}
|
|
34271
36054
|
}
|
|
34272
|
-
if (!
|
|
36055
|
+
if (!fs11.existsSync(composeFile)) {
|
|
34273
36056
|
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
34274
36057
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
34275
36058
|
}
|
|
34276
36059
|
}
|
|
34277
|
-
if (
|
|
34278
|
-
|
|
36060
|
+
if (fs11.existsSync(instancesFile) && fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36061
|
+
fs11.rmSync(instancesFile, { recursive: true, force: true });
|
|
34279
36062
|
}
|
|
34280
|
-
if (!
|
|
36063
|
+
if (!fs11.existsSync(instancesFile)) {
|
|
34281
36064
|
const header = `# PostgreSQL instances to monitor
|
|
34282
36065
|
` + `# Add your instances using: pgai mon targets add <connection-string> <name>
|
|
34283
36066
|
|
|
34284
36067
|
`;
|
|
34285
|
-
|
|
36068
|
+
fs11.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
|
|
34286
36069
|
}
|
|
34287
|
-
const pgwatchConfig =
|
|
34288
|
-
if (!
|
|
34289
|
-
|
|
36070
|
+
const pgwatchConfig = path9.resolve(projectDir, ".pgwatch-config");
|
|
36071
|
+
if (!fs11.existsSync(pgwatchConfig)) {
|
|
36072
|
+
fs11.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
|
|
34290
36073
|
}
|
|
34291
|
-
const envFile =
|
|
34292
|
-
if (!
|
|
36074
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36075
|
+
if (!fs11.existsSync(envFile)) {
|
|
34293
36076
|
const envText = `PGAI_TAG=${package_default.version}
|
|
34294
36077
|
# PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
|
|
34295
36078
|
`;
|
|
34296
|
-
|
|
36079
|
+
fs11.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
|
|
34297
36080
|
}
|
|
34298
|
-
return { fs:
|
|
36081
|
+
return { fs: fs11, path: path9, projectDir, composeFile, instancesFile };
|
|
34299
36082
|
}
|
|
34300
36083
|
function sanitizeTagForBackup(tag) {
|
|
34301
36084
|
if (tag == null)
|
|
@@ -34304,10 +36087,10 @@ function sanitizeTagForBackup(tag) {
|
|
|
34304
36087
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
34305
36088
|
}
|
|
34306
36089
|
function readDeployedTag(projectDir) {
|
|
34307
|
-
const envFile =
|
|
34308
|
-
if (!
|
|
36090
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
36091
|
+
if (!fs11.existsSync(envFile))
|
|
34309
36092
|
return null;
|
|
34310
|
-
const m =
|
|
36093
|
+
const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
34311
36094
|
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
34312
36095
|
}
|
|
34313
36096
|
function isValidComposeYaml(text) {
|
|
@@ -34344,10 +36127,10 @@ function composeContentEqual(a, b) {
|
|
|
34344
36127
|
return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
|
|
34345
36128
|
}
|
|
34346
36129
|
async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
34347
|
-
if (
|
|
36130
|
+
if (fs11.existsSync(path9.resolve(projectDir, ".git")))
|
|
34348
36131
|
return false;
|
|
34349
|
-
const composeFile =
|
|
34350
|
-
if (!
|
|
36132
|
+
const composeFile = path9.resolve(projectDir, "docker-compose.yml");
|
|
36133
|
+
if (!fs11.existsSync(composeFile))
|
|
34351
36134
|
return false;
|
|
34352
36135
|
const refs = [
|
|
34353
36136
|
process.env.PGAI_PROJECT_REF,
|
|
@@ -34360,24 +36143,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
34360
36143
|
console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
|
|
34361
36144
|
return false;
|
|
34362
36145
|
}
|
|
34363
|
-
const existing =
|
|
36146
|
+
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
34364
36147
|
if (composeContentEqual(existing, fetched))
|
|
34365
36148
|
return false;
|
|
34366
36149
|
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
34367
36150
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
34368
|
-
const oldHash =
|
|
34369
|
-
const backup =
|
|
36151
|
+
const oldHash = crypto4.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
36152
|
+
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
34370
36153
|
let backupName = null;
|
|
34371
36154
|
try {
|
|
34372
|
-
|
|
34373
|
-
backupName =
|
|
36155
|
+
fs11.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
36156
|
+
backupName = path9.basename(backup);
|
|
34374
36157
|
} catch (err) {
|
|
34375
36158
|
const e = err;
|
|
34376
36159
|
if (e && e.code === "EEXIST") {
|
|
34377
|
-
backupName =
|
|
36160
|
+
backupName = path9.basename(backup);
|
|
34378
36161
|
}
|
|
34379
36162
|
}
|
|
34380
|
-
|
|
36163
|
+
fs11.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
|
|
34381
36164
|
const backupNote = backupName ? ` (backup: ${backupName})` : "";
|
|
34382
36165
|
console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
|
|
34383
36166
|
return true;
|
|
@@ -35611,12 +37394,12 @@ function resolvePaths() {
|
|
|
35611
37394
|
const startDir = process.cwd();
|
|
35612
37395
|
let currentDir = startDir;
|
|
35613
37396
|
while (true) {
|
|
35614
|
-
const composeFile =
|
|
35615
|
-
if (
|
|
35616
|
-
const instancesFile =
|
|
35617
|
-
return { fs:
|
|
37397
|
+
const composeFile = path9.resolve(currentDir, "docker-compose.yml");
|
|
37398
|
+
if (fs11.existsSync(composeFile)) {
|
|
37399
|
+
const instancesFile = path9.resolve(currentDir, "instances.yml");
|
|
37400
|
+
return { fs: fs11, path: path9, projectDir: currentDir, composeFile, instancesFile };
|
|
35618
37401
|
}
|
|
35619
|
-
const parentDir =
|
|
37402
|
+
const parentDir = path9.dirname(currentDir);
|
|
35620
37403
|
if (parentDir === currentDir)
|
|
35621
37404
|
break;
|
|
35622
37405
|
currentDir = parentDir;
|
|
@@ -35748,10 +37531,10 @@ function resolveAdoptedProject(reg) {
|
|
|
35748
37531
|
}
|
|
35749
37532
|
function updatePgwatchConfig(configPath, updates) {
|
|
35750
37533
|
let lines = [];
|
|
35751
|
-
if (
|
|
35752
|
-
const stats =
|
|
37534
|
+
if (fs11.existsSync(configPath)) {
|
|
37535
|
+
const stats = fs11.statSync(configPath);
|
|
35753
37536
|
if (!stats.isDirectory()) {
|
|
35754
|
-
const content =
|
|
37537
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
35755
37538
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
35756
37539
|
}
|
|
35757
37540
|
}
|
|
@@ -35763,7 +37546,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
35763
37546
|
lines.push(`${key}=${value}`);
|
|
35764
37547
|
}
|
|
35765
37548
|
}
|
|
35766
|
-
|
|
37549
|
+
fs11.writeFileSync(configPath, lines.join(`
|
|
35767
37550
|
`) + `
|
|
35768
37551
|
`, { encoding: "utf8", mode: 384 });
|
|
35769
37552
|
}
|
|
@@ -35801,12 +37584,12 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35801
37584
|
if (grafanaPassword) {
|
|
35802
37585
|
env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
|
|
35803
37586
|
} else {
|
|
35804
|
-
const cfgPath =
|
|
35805
|
-
if (
|
|
37587
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37588
|
+
if (fs11.existsSync(cfgPath)) {
|
|
35806
37589
|
try {
|
|
35807
|
-
const stats =
|
|
37590
|
+
const stats = fs11.statSync(cfgPath);
|
|
35808
37591
|
if (!stats.isDirectory()) {
|
|
35809
|
-
const content =
|
|
37592
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
35810
37593
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
35811
37594
|
if (match) {
|
|
35812
37595
|
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
@@ -35819,10 +37602,10 @@ async function runCompose(args, grafanaPassword) {
|
|
|
35819
37602
|
}
|
|
35820
37603
|
}
|
|
35821
37604
|
}
|
|
35822
|
-
const envFilePath =
|
|
35823
|
-
if (
|
|
37605
|
+
const envFilePath = path9.resolve(projectDir, ".env");
|
|
37606
|
+
if (fs11.existsSync(envFilePath)) {
|
|
35824
37607
|
try {
|
|
35825
|
-
const envContent =
|
|
37608
|
+
const envContent = fs11.readFileSync(envFilePath, "utf8");
|
|
35826
37609
|
if (!env.VM_AUTH_USERNAME) {
|
|
35827
37610
|
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
35828
37611
|
if (m)
|
|
@@ -35879,20 +37662,20 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35879
37662
|
console.log(`Project directory: ${projectDir}
|
|
35880
37663
|
`);
|
|
35881
37664
|
if (opts.project) {
|
|
35882
|
-
const cfgPath2 =
|
|
37665
|
+
const cfgPath2 = path9.resolve(projectDir, ".pgwatch-config");
|
|
35883
37666
|
updatePgwatchConfig(cfgPath2, { project_name: opts.project });
|
|
35884
37667
|
console.log(`Using project name: ${opts.project}
|
|
35885
37668
|
`);
|
|
35886
37669
|
}
|
|
35887
|
-
const envFile =
|
|
37670
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
35888
37671
|
let existingRegistry = null;
|
|
35889
37672
|
let existingPassword = null;
|
|
35890
37673
|
let existingReplicatorPassword = null;
|
|
35891
37674
|
let existingVmAuthUsername = null;
|
|
35892
37675
|
let existingVmAuthPassword = null;
|
|
35893
37676
|
let previousTag = null;
|
|
35894
|
-
if (
|
|
35895
|
-
const existingEnv =
|
|
37677
|
+
if (fs11.existsSync(envFile)) {
|
|
37678
|
+
const existingEnv = fs11.readFileSync(envFile, "utf8");
|
|
35896
37679
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
35897
37680
|
if (previousTagMatch)
|
|
35898
37681
|
previousTag = previousTagMatch[1].trim();
|
|
@@ -35920,10 +37703,10 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35920
37703
|
if (existingPassword) {
|
|
35921
37704
|
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
35922
37705
|
}
|
|
35923
|
-
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword ||
|
|
37706
|
+
envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto4.randomBytes(32).toString("hex")}`);
|
|
35924
37707
|
envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
|
|
35925
|
-
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword ||
|
|
35926
|
-
|
|
37708
|
+
envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto4.randomBytes(18).toString("base64")}`);
|
|
37709
|
+
fs11.writeFileSync(envFile, envLines.join(`
|
|
35927
37710
|
`) + `
|
|
35928
37711
|
`, { encoding: "utf8", mode: 384 });
|
|
35929
37712
|
await refreshBundledComposeIfStale(projectDir, previousTag);
|
|
@@ -35960,7 +37743,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35960
37743
|
if (apiKey) {
|
|
35961
37744
|
console.log("Using API key provided via --api-key parameter");
|
|
35962
37745
|
writeConfig({ apiKey });
|
|
35963
|
-
updatePgwatchConfig(
|
|
37746
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
|
|
35964
37747
|
console.log(`\u2713 API key saved
|
|
35965
37748
|
`);
|
|
35966
37749
|
} else if (opts.yes) {
|
|
@@ -35977,7 +37760,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
35977
37760
|
const trimmedKey = inputApiKey.trim();
|
|
35978
37761
|
if (trimmedKey) {
|
|
35979
37762
|
writeConfig({ apiKey: trimmedKey });
|
|
35980
|
-
updatePgwatchConfig(
|
|
37763
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
|
|
35981
37764
|
apiKey = trimmedKey;
|
|
35982
37765
|
console.log(`\u2713 API key saved
|
|
35983
37766
|
`);
|
|
@@ -36011,7 +37794,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
36011
37794
|
# Add your instances using: postgres-ai mon targets add
|
|
36012
37795
|
|
|
36013
37796
|
`;
|
|
36014
|
-
|
|
37797
|
+
fs11.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
|
|
36015
37798
|
console.log(`Instances file: ${instancesPath2}`);
|
|
36016
37799
|
console.log(`Project directory: ${projectDir2}
|
|
36017
37800
|
`);
|
|
@@ -36115,17 +37898,17 @@ You can provide either:`);
|
|
|
36115
37898
|
}
|
|
36116
37899
|
} else {
|
|
36117
37900
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
36118
|
-
const currentDir =
|
|
37901
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
36119
37902
|
const demoCandidates = [
|
|
36120
|
-
|
|
36121
|
-
|
|
37903
|
+
path9.resolve(currentDir, "..", "..", "instances.demo.yml"),
|
|
37904
|
+
path9.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
|
|
36122
37905
|
];
|
|
36123
|
-
const demoSrc = demoCandidates.find((p) =>
|
|
37906
|
+
const demoSrc = demoCandidates.find((p) => fs11.existsSync(p));
|
|
36124
37907
|
if (demoSrc) {
|
|
36125
|
-
if (
|
|
36126
|
-
|
|
37908
|
+
if (fs11.existsSync(instancesPath) && fs11.lstatSync(instancesPath).isDirectory()) {
|
|
37909
|
+
fs11.rmSync(instancesPath, { recursive: true, force: true });
|
|
36127
37910
|
}
|
|
36128
|
-
|
|
37911
|
+
fs11.copyFileSync(demoSrc, instancesPath);
|
|
36129
37912
|
console.log(`\u2713 Demo monitoring target configured
|
|
36130
37913
|
`);
|
|
36131
37914
|
} else {
|
|
@@ -36144,15 +37927,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36144
37927
|
console.log(`\u2713 Configuration updated
|
|
36145
37928
|
`);
|
|
36146
37929
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
36147
|
-
const cfgPath =
|
|
37930
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
36148
37931
|
let grafanaPassword = "";
|
|
36149
37932
|
let vmAuthUsername = "";
|
|
36150
37933
|
let vmAuthPassword = "";
|
|
36151
37934
|
try {
|
|
36152
|
-
if (
|
|
36153
|
-
const stats =
|
|
37935
|
+
if (fs11.existsSync(cfgPath)) {
|
|
37936
|
+
const stats = fs11.statSync(cfgPath);
|
|
36154
37937
|
if (!stats.isDirectory()) {
|
|
36155
|
-
const content =
|
|
37938
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
36156
37939
|
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
36157
37940
|
if (match) {
|
|
36158
37941
|
grafanaPassword = match[1].trim();
|
|
@@ -36164,15 +37947,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36164
37947
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
36165
37948
|
grafanaPassword = password.trim().replace(/\n/g, "");
|
|
36166
37949
|
let configContent = "";
|
|
36167
|
-
if (
|
|
36168
|
-
const stats =
|
|
37950
|
+
if (fs11.existsSync(cfgPath)) {
|
|
37951
|
+
const stats = fs11.statSync(cfgPath);
|
|
36169
37952
|
if (!stats.isDirectory()) {
|
|
36170
|
-
configContent =
|
|
37953
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
36171
37954
|
}
|
|
36172
37955
|
}
|
|
36173
37956
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
36174
37957
|
lines.push(`grafana_password=${grafanaPassword}`);
|
|
36175
|
-
|
|
37958
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
36176
37959
|
`) + `
|
|
36177
37960
|
`, "utf8");
|
|
36178
37961
|
}
|
|
@@ -36185,9 +37968,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36185
37968
|
grafanaPassword = "demo";
|
|
36186
37969
|
}
|
|
36187
37970
|
try {
|
|
36188
|
-
const envFile2 =
|
|
36189
|
-
if (
|
|
36190
|
-
const envContent =
|
|
37971
|
+
const envFile2 = path9.resolve(projectDir, ".env");
|
|
37972
|
+
if (fs11.existsSync(envFile2)) {
|
|
37973
|
+
const envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36191
37974
|
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
36192
37975
|
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
36193
37976
|
if (userMatch)
|
|
@@ -36203,13 +37986,13 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36203
37986
|
vmAuthPassword = vmPass.trim().replace(/\n/g, "");
|
|
36204
37987
|
}
|
|
36205
37988
|
let envContent = "";
|
|
36206
|
-
if (
|
|
36207
|
-
envContent =
|
|
37989
|
+
if (fs11.existsSync(envFile2)) {
|
|
37990
|
+
envContent = fs11.readFileSync(envFile2, "utf8");
|
|
36208
37991
|
}
|
|
36209
37992
|
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 === ""));
|
|
36210
37993
|
envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
36211
37994
|
envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
36212
|
-
|
|
37995
|
+
fs11.writeFileSync(envFile2, envLines2.join(`
|
|
36213
37996
|
`) + `
|
|
36214
37997
|
`, { encoding: "utf8", mode: 384 });
|
|
36215
37998
|
}
|
|
@@ -36242,7 +38025,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
36242
38025
|
});
|
|
36243
38026
|
const adoptedProject = resolveAdoptedProject(reg);
|
|
36244
38027
|
if (adoptedProject != null) {
|
|
36245
|
-
updatePgwatchConfig(
|
|
38028
|
+
updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), {
|
|
36246
38029
|
project_name: adoptedProject
|
|
36247
38030
|
});
|
|
36248
38031
|
const verb = reg?.created ? "Registered" : "Adopted";
|
|
@@ -36458,11 +38241,11 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
36458
38241
|
console.log(`Project Directory: ${projectDir}`);
|
|
36459
38242
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
36460
38243
|
console.log(`Instances File: ${instancesFile}`);
|
|
36461
|
-
if (
|
|
38244
|
+
if (fs11.existsSync(instancesFile) && !fs11.lstatSync(instancesFile).isDirectory()) {
|
|
36462
38245
|
console.log(`
|
|
36463
38246
|
Instances configuration:
|
|
36464
38247
|
`);
|
|
36465
|
-
const text =
|
|
38248
|
+
const text = fs11.readFileSync(instancesFile, "utf8");
|
|
36466
38249
|
process.stdout.write(text);
|
|
36467
38250
|
if (!/\n$/.test(text))
|
|
36468
38251
|
console.log();
|
|
@@ -36511,8 +38294,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
36511
38294
|
console.log("\u2713 .env is up to date");
|
|
36512
38295
|
}
|
|
36513
38296
|
console.log();
|
|
36514
|
-
const gitDir =
|
|
36515
|
-
if (
|
|
38297
|
+
const gitDir = path9.resolve(projectDir, ".git");
|
|
38298
|
+
if (fs11.existsSync(gitDir)) {
|
|
36516
38299
|
console.log("Fetching latest changes...");
|
|
36517
38300
|
await execFilePromise("git", ["fetch", "origin"]);
|
|
36518
38301
|
const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -36667,7 +38450,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
36667
38450
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
36668
38451
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
36669
38452
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
36670
|
-
if (!
|
|
38453
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36671
38454
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
36672
38455
|
process.exitCode = 1;
|
|
36673
38456
|
return;
|
|
@@ -36730,7 +38513,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
36730
38513
|
});
|
|
36731
38514
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
36732
38515
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
36733
|
-
if (!
|
|
38516
|
+
if (!fs11.existsSync(file) || fs11.lstatSync(file).isDirectory()) {
|
|
36734
38517
|
console.error("instances.yml not found");
|
|
36735
38518
|
process.exitCode = 1;
|
|
36736
38519
|
return;
|
|
@@ -36758,7 +38541,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
36758
38541
|
});
|
|
36759
38542
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
36760
38543
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
36761
|
-
if (!
|
|
38544
|
+
if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
|
|
36762
38545
|
console.error("instances.yml not found");
|
|
36763
38546
|
process.exitCode = 1;
|
|
36764
38547
|
return;
|
|
@@ -37020,15 +38803,15 @@ To authenticate, run: pgai auth`);
|
|
|
37020
38803
|
});
|
|
37021
38804
|
auth.command("remove-key").description("remove API key").action(async () => {
|
|
37022
38805
|
const newConfigPath = getConfigPath();
|
|
37023
|
-
const hasNewConfig =
|
|
38806
|
+
const hasNewConfig = fs11.existsSync(newConfigPath);
|
|
37024
38807
|
let legacyPath;
|
|
37025
38808
|
try {
|
|
37026
38809
|
const { projectDir } = await resolveOrInitPaths();
|
|
37027
|
-
legacyPath =
|
|
38810
|
+
legacyPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37028
38811
|
} catch {
|
|
37029
|
-
legacyPath =
|
|
38812
|
+
legacyPath = path9.resolve(process.cwd(), ".pgwatch-config");
|
|
37030
38813
|
}
|
|
37031
|
-
const hasLegacyConfig =
|
|
38814
|
+
const hasLegacyConfig = fs11.existsSync(legacyPath) && fs11.statSync(legacyPath).isFile();
|
|
37032
38815
|
if (!hasNewConfig && !hasLegacyConfig) {
|
|
37033
38816
|
console.log("No API key configured");
|
|
37034
38817
|
return;
|
|
@@ -37038,11 +38821,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
|
|
|
37038
38821
|
}
|
|
37039
38822
|
if (hasLegacyConfig) {
|
|
37040
38823
|
try {
|
|
37041
|
-
const content =
|
|
38824
|
+
const content = fs11.readFileSync(legacyPath, "utf8");
|
|
37042
38825
|
const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
|
|
37043
38826
|
`).replace(/\n+$/g, `
|
|
37044
38827
|
`);
|
|
37045
|
-
|
|
38828
|
+
fs11.writeFileSync(legacyPath, filtered, "utf8");
|
|
37046
38829
|
} catch (err) {
|
|
37047
38830
|
console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
|
|
37048
38831
|
}
|
|
@@ -37053,7 +38836,7 @@ To authenticate again, run: pgai auth`);
|
|
|
37053
38836
|
});
|
|
37054
38837
|
mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
|
|
37055
38838
|
const { projectDir } = await resolveOrInitPaths();
|
|
37056
|
-
const cfgPath =
|
|
38839
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
37057
38840
|
try {
|
|
37058
38841
|
const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
|
|
37059
38842
|
const newPassword = password.trim().replace(/\n/g, "");
|
|
@@ -37063,17 +38846,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
|
|
|
37063
38846
|
return;
|
|
37064
38847
|
}
|
|
37065
38848
|
let configContent = "";
|
|
37066
|
-
if (
|
|
37067
|
-
const stats =
|
|
38849
|
+
if (fs11.existsSync(cfgPath)) {
|
|
38850
|
+
const stats = fs11.statSync(cfgPath);
|
|
37068
38851
|
if (stats.isDirectory()) {
|
|
37069
38852
|
console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
|
|
37070
38853
|
} else {
|
|
37071
|
-
configContent =
|
|
38854
|
+
configContent = fs11.readFileSync(cfgPath, "utf8");
|
|
37072
38855
|
}
|
|
37073
38856
|
}
|
|
37074
38857
|
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
37075
38858
|
lines.push(`grafana_password=${newPassword}`);
|
|
37076
|
-
|
|
38859
|
+
fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
|
|
37077
38860
|
`) + `
|
|
37078
38861
|
`, "utf8");
|
|
37079
38862
|
console.log("\u2713 New Grafana password generated and saved");
|
|
@@ -37095,19 +38878,19 @@ Note: This command requires 'openssl' to be installed`);
|
|
|
37095
38878
|
});
|
|
37096
38879
|
mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
|
|
37097
38880
|
const { projectDir } = await resolveOrInitPaths();
|
|
37098
|
-
const cfgPath =
|
|
37099
|
-
if (!
|
|
38881
|
+
const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
38882
|
+
if (!fs11.existsSync(cfgPath)) {
|
|
37100
38883
|
console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
|
|
37101
38884
|
process.exitCode = 1;
|
|
37102
38885
|
return;
|
|
37103
38886
|
}
|
|
37104
|
-
const stats =
|
|
38887
|
+
const stats = fs11.statSync(cfgPath);
|
|
37105
38888
|
if (stats.isDirectory()) {
|
|
37106
38889
|
console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
|
|
37107
38890
|
process.exitCode = 1;
|
|
37108
38891
|
return;
|
|
37109
38892
|
}
|
|
37110
|
-
const content =
|
|
38893
|
+
const content = fs11.readFileSync(cfgPath, "utf8");
|
|
37111
38894
|
const lines = content.split(/\r?\n/);
|
|
37112
38895
|
let password = "";
|
|
37113
38896
|
for (const line of lines) {
|
|
@@ -37127,9 +38910,9 @@ Grafana credentials:`);
|
|
|
37127
38910
|
console.log(" URL: http://localhost:3000");
|
|
37128
38911
|
console.log(" Username: monitor");
|
|
37129
38912
|
console.log(` Password: ${password}`);
|
|
37130
|
-
const envFile =
|
|
37131
|
-
if (
|
|
37132
|
-
const envContent =
|
|
38913
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
38914
|
+
if (fs11.existsSync(envFile)) {
|
|
38915
|
+
const envContent = fs11.readFileSync(envFile, "utf8");
|
|
37133
38916
|
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
37134
38917
|
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
37135
38918
|
if (vmUser && vmPass) {
|
|
@@ -37855,13 +39638,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
|
|
|
37855
39638
|
});
|
|
37856
39639
|
spinner.stop();
|
|
37857
39640
|
if (opts.output) {
|
|
37858
|
-
const dir =
|
|
37859
|
-
|
|
39641
|
+
const dir = path9.resolve(opts.output);
|
|
39642
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
37860
39643
|
for (const f of result) {
|
|
37861
|
-
const safeName =
|
|
37862
|
-
const filePath =
|
|
39644
|
+
const safeName = path9.basename(f.filename);
|
|
39645
|
+
const filePath = path9.join(dir, safeName);
|
|
37863
39646
|
const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
|
|
37864
|
-
|
|
39647
|
+
fs11.writeFileSync(filePath, content, "utf-8");
|
|
37865
39648
|
console.log(filePath);
|
|
37866
39649
|
}
|
|
37867
39650
|
} else if (opts.json) {
|
|
@@ -37906,6 +39689,406 @@ function tryParseJson(s) {
|
|
|
37906
39689
|
return s;
|
|
37907
39690
|
}
|
|
37908
39691
|
}
|
|
39692
|
+
function inferCommandFromResult(r) {
|
|
39693
|
+
if (r.command)
|
|
39694
|
+
return r.command;
|
|
39695
|
+
if (r.plan_text !== undefined || r.plan_json !== undefined)
|
|
39696
|
+
return "plan";
|
|
39697
|
+
if (r.row_count !== undefined || r.result_rows !== undefined)
|
|
39698
|
+
return "exec";
|
|
39699
|
+
if (r.hypo_used !== undefined)
|
|
39700
|
+
return "hypo";
|
|
39701
|
+
if (r.terminated !== undefined)
|
|
39702
|
+
return "terminate";
|
|
39703
|
+
if (r.reset !== undefined)
|
|
39704
|
+
return "reset";
|
|
39705
|
+
if (r.snapshot !== undefined)
|
|
39706
|
+
return "activity";
|
|
39707
|
+
return null;
|
|
39708
|
+
}
|
|
39709
|
+
function printJoeOutcome(command, outcome, json3) {
|
|
39710
|
+
const budgetSeconds = Math.round(DEFAULT_BUDGET_MS2 / 1000);
|
|
39711
|
+
if (outcome.budgetExpired) {
|
|
39712
|
+
if (json3) {
|
|
39713
|
+
console.log(JSON.stringify({
|
|
39714
|
+
command_id: outcome.commandId,
|
|
39715
|
+
status: outcome.status,
|
|
39716
|
+
session_id: outcome.sessionId,
|
|
39717
|
+
budget_expired: true,
|
|
39718
|
+
resume: `pgai result ${outcome.commandId}`
|
|
39719
|
+
}, null, 2));
|
|
39720
|
+
} else {
|
|
39721
|
+
console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai result ${outcome.commandId}`);
|
|
39722
|
+
}
|
|
39723
|
+
return;
|
|
39724
|
+
}
|
|
39725
|
+
const result = outcome.result;
|
|
39726
|
+
if (outcome.status === "done" && result) {
|
|
39727
|
+
if (json3) {
|
|
39728
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39729
|
+
} else {
|
|
39730
|
+
console.log(`submitted ${outcome.commandId} \xB7 done`);
|
|
39731
|
+
const body = formatJoeResult(command, result);
|
|
39732
|
+
if (body)
|
|
39733
|
+
console.log(body);
|
|
39734
|
+
}
|
|
39735
|
+
return;
|
|
39736
|
+
}
|
|
39737
|
+
if (outcome.status === "error") {
|
|
39738
|
+
const msg = result?.error ?? "command failed";
|
|
39739
|
+
console.error(`command ${outcome.commandId} error: ${msg}`);
|
|
39740
|
+
process.exitCode = 1;
|
|
39741
|
+
return;
|
|
39742
|
+
}
|
|
39743
|
+
console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai result ${outcome.commandId}`);
|
|
39744
|
+
process.exitCode = 1;
|
|
39745
|
+
}
|
|
39746
|
+
async function runJoeCli(command, sql, args, opts) {
|
|
39747
|
+
try {
|
|
39748
|
+
const rootOpts = program2.opts();
|
|
39749
|
+
const cfg = readConfig();
|
|
39750
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39751
|
+
if (!apiKey) {
|
|
39752
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39753
|
+
process.exitCode = 1;
|
|
39754
|
+
return;
|
|
39755
|
+
}
|
|
39756
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
39757
|
+
if (!projectRef) {
|
|
39758
|
+
console.error("Project is required. Pass --project <id|alias>.");
|
|
39759
|
+
process.exitCode = 1;
|
|
39760
|
+
return;
|
|
39761
|
+
}
|
|
39762
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39763
|
+
const budgetMs = typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
|
|
39764
|
+
const outcome = await executeJoeCommand2({
|
|
39765
|
+
apiKey,
|
|
39766
|
+
apiBaseUrl,
|
|
39767
|
+
command,
|
|
39768
|
+
project: projectRef,
|
|
39769
|
+
sql,
|
|
39770
|
+
args,
|
|
39771
|
+
session: opts.session ?? null,
|
|
39772
|
+
newSession: !!opts.newSession,
|
|
39773
|
+
orgId: cfg.orgId ?? undefined,
|
|
39774
|
+
budgetMs,
|
|
39775
|
+
debug: !!opts.debug
|
|
39776
|
+
});
|
|
39777
|
+
printJoeOutcome(command, outcome, !!opts.json);
|
|
39778
|
+
} catch (err) {
|
|
39779
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39780
|
+
console.error(message);
|
|
39781
|
+
process.exitCode = 1;
|
|
39782
|
+
}
|
|
39783
|
+
}
|
|
39784
|
+
function withJoeOptions(cmd) {
|
|
39785
|
+
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");
|
|
39786
|
+
}
|
|
39787
|
+
withJoeOptions(program2.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
|
|
39788
|
+
await runJoeCli("plan", sql, null, opts);
|
|
39789
|
+
});
|
|
39790
|
+
withJoeOptions(program2.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
|
|
39791
|
+
await runJoeCli("explain", sql, null, opts);
|
|
39792
|
+
});
|
|
39793
|
+
withJoeOptions(program2.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
|
|
39794
|
+
await runJoeCli("exec", sql, null, opts);
|
|
39795
|
+
});
|
|
39796
|
+
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) => {
|
|
39797
|
+
await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
|
|
39798
|
+
});
|
|
39799
|
+
withJoeOptions(program2.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
|
|
39800
|
+
await runJoeCli("activity", null, null, opts);
|
|
39801
|
+
});
|
|
39802
|
+
withJoeOptions(program2.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
39803
|
+
const pidNum = parseInt(pid, 10);
|
|
39804
|
+
if (Number.isNaN(pidNum)) {
|
|
39805
|
+
console.error("pid must be a number");
|
|
39806
|
+
process.exitCode = 1;
|
|
39807
|
+
return;
|
|
39808
|
+
}
|
|
39809
|
+
await runJoeCli("terminate", null, { pid: pidNum }, opts);
|
|
39810
|
+
});
|
|
39811
|
+
withJoeOptions(program2.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
39812
|
+
await runJoeCli("reset", null, null, opts);
|
|
39813
|
+
});
|
|
39814
|
+
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) => {
|
|
39815
|
+
const args = { object: object4 };
|
|
39816
|
+
if (opts.variant)
|
|
39817
|
+
args.variant = opts.variant;
|
|
39818
|
+
await runJoeCli("describe", null, args, opts);
|
|
39819
|
+
});
|
|
39820
|
+
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) => {
|
|
39821
|
+
try {
|
|
39822
|
+
const rootOpts = program2.opts();
|
|
39823
|
+
const cfg = readConfig();
|
|
39824
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39825
|
+
if (!apiKey) {
|
|
39826
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39827
|
+
process.exitCode = 1;
|
|
39828
|
+
return;
|
|
39829
|
+
}
|
|
39830
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39831
|
+
const projects = await listProjects2({
|
|
39832
|
+
apiKey,
|
|
39833
|
+
apiBaseUrl,
|
|
39834
|
+
orgId: cfg.orgId ?? undefined,
|
|
39835
|
+
debug: !!opts.debug
|
|
39836
|
+
});
|
|
39837
|
+
if (opts.json) {
|
|
39838
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
39839
|
+
} else {
|
|
39840
|
+
console.log(formatProjectsTable(projects));
|
|
39841
|
+
}
|
|
39842
|
+
} catch (err) {
|
|
39843
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39844
|
+
console.error(message);
|
|
39845
|
+
process.exitCode = 1;
|
|
39846
|
+
}
|
|
39847
|
+
});
|
|
39848
|
+
async function resolveDblabTarget(project, debug) {
|
|
39849
|
+
const rootOpts = program2.opts();
|
|
39850
|
+
const cfg = readConfig();
|
|
39851
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39852
|
+
if (!apiKey) {
|
|
39853
|
+
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39854
|
+
}
|
|
39855
|
+
const ref = (project ?? "").trim();
|
|
39856
|
+
if (!ref) {
|
|
39857
|
+
throw new Error("--project <id|alias> is required");
|
|
39858
|
+
}
|
|
39859
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39860
|
+
const orgId = cfg.orgId ?? undefined;
|
|
39861
|
+
const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
39862
|
+
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
39863
|
+
}
|
|
39864
|
+
var clone2 = program2.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
39865
|
+
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) => {
|
|
39866
|
+
try {
|
|
39867
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39868
|
+
const result = await createClone2({
|
|
39869
|
+
apiKey,
|
|
39870
|
+
apiBaseUrl,
|
|
39871
|
+
instanceId,
|
|
39872
|
+
cloneId: opts.id,
|
|
39873
|
+
branch: opts.branch,
|
|
39874
|
+
snapshotId: opts.snapshot,
|
|
39875
|
+
dbUser: opts.dbUser,
|
|
39876
|
+
dbPassword: opts.dbPassword,
|
|
39877
|
+
isProtected: !!opts.protected,
|
|
39878
|
+
debug: !!opts.debug
|
|
39879
|
+
});
|
|
39880
|
+
printResult(result, opts.json);
|
|
39881
|
+
} catch (err) {
|
|
39882
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39883
|
+
process.exitCode = 1;
|
|
39884
|
+
}
|
|
39885
|
+
});
|
|
39886
|
+
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) => {
|
|
39887
|
+
try {
|
|
39888
|
+
const rootOpts = program2.opts();
|
|
39889
|
+
const cfg = readConfig();
|
|
39890
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39891
|
+
if (!apiKey) {
|
|
39892
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39893
|
+
process.exitCode = 1;
|
|
39894
|
+
return;
|
|
39895
|
+
}
|
|
39896
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39897
|
+
const status = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
39898
|
+
if (opts.json) {
|
|
39899
|
+
console.log(JSON.stringify(status, null, 2));
|
|
39900
|
+
} else {
|
|
39901
|
+
console.log(`command ${status.command_id} \xB7 ${status.status}${status.error ? ` \xB7 ${status.error}` : ""}`);
|
|
39902
|
+
}
|
|
39903
|
+
} catch (err) {
|
|
39904
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39905
|
+
console.error(message);
|
|
39906
|
+
process.exitCode = 1;
|
|
39907
|
+
}
|
|
39908
|
+
});
|
|
39909
|
+
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) => {
|
|
39910
|
+
try {
|
|
39911
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39912
|
+
const result = await listClones2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
39913
|
+
printResult(result, opts.json);
|
|
39914
|
+
} catch (err) {
|
|
39915
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39916
|
+
process.exitCode = 1;
|
|
39917
|
+
}
|
|
39918
|
+
});
|
|
39919
|
+
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) => {
|
|
39920
|
+
try {
|
|
39921
|
+
const rootOpts = program2.opts();
|
|
39922
|
+
const cfg = readConfig();
|
|
39923
|
+
const { apiKey } = getConfig(rootOpts);
|
|
39924
|
+
if (!apiKey) {
|
|
39925
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
39926
|
+
process.exitCode = 1;
|
|
39927
|
+
return;
|
|
39928
|
+
}
|
|
39929
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
39930
|
+
const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
39931
|
+
if (opts.json) {
|
|
39932
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39933
|
+
return;
|
|
39934
|
+
}
|
|
39935
|
+
const inferred = inferCommandFromResult(result);
|
|
39936
|
+
if (result.status === "done" && inferred) {
|
|
39937
|
+
console.log(`command ${result.command_id} \xB7 done`);
|
|
39938
|
+
const body = formatJoeResult(inferred, result);
|
|
39939
|
+
if (body)
|
|
39940
|
+
console.log(body);
|
|
39941
|
+
} else if (result.status === "error") {
|
|
39942
|
+
console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
|
|
39943
|
+
process.exitCode = 1;
|
|
39944
|
+
} else {
|
|
39945
|
+
console.log(`command ${result.command_id} \xB7 ${result.status}`);
|
|
39946
|
+
}
|
|
39947
|
+
} catch (err) {
|
|
39948
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39949
|
+
console.error(message);
|
|
39950
|
+
process.exitCode = 1;
|
|
39951
|
+
}
|
|
39952
|
+
});
|
|
39953
|
+
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) => {
|
|
39954
|
+
try {
|
|
39955
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39956
|
+
const result = await getClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
39957
|
+
printResult(result, opts.json);
|
|
39958
|
+
} catch (err) {
|
|
39959
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39960
|
+
process.exitCode = 1;
|
|
39961
|
+
}
|
|
39962
|
+
});
|
|
39963
|
+
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) => {
|
|
39964
|
+
try {
|
|
39965
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39966
|
+
const result = await resetClone2({
|
|
39967
|
+
apiKey,
|
|
39968
|
+
apiBaseUrl,
|
|
39969
|
+
instanceId,
|
|
39970
|
+
cloneId,
|
|
39971
|
+
snapshotId: opts.snapshot,
|
|
39972
|
+
latest: opts.latest,
|
|
39973
|
+
debug: !!opts.debug
|
|
39974
|
+
});
|
|
39975
|
+
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
39976
|
+
} catch (err) {
|
|
39977
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39978
|
+
process.exitCode = 1;
|
|
39979
|
+
}
|
|
39980
|
+
});
|
|
39981
|
+
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) => {
|
|
39982
|
+
try {
|
|
39983
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39984
|
+
const result = await destroyClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
39985
|
+
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
39986
|
+
} catch (err) {
|
|
39987
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39988
|
+
process.exitCode = 1;
|
|
39989
|
+
}
|
|
39990
|
+
});
|
|
39991
|
+
var branch = program2.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
39992
|
+
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) => {
|
|
39993
|
+
try {
|
|
39994
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
39995
|
+
const result = await listBranches2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
39996
|
+
printResult(result, opts.json);
|
|
39997
|
+
} catch (err) {
|
|
39998
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
39999
|
+
process.exitCode = 1;
|
|
40000
|
+
}
|
|
40001
|
+
});
|
|
40002
|
+
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) => {
|
|
40003
|
+
try {
|
|
40004
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40005
|
+
const result = await createBranch2({
|
|
40006
|
+
apiKey,
|
|
40007
|
+
apiBaseUrl,
|
|
40008
|
+
instanceId,
|
|
40009
|
+
branchName: name,
|
|
40010
|
+
baseBranch: opts.baseBranch,
|
|
40011
|
+
snapshotId: opts.snapshot,
|
|
40012
|
+
debug: !!opts.debug
|
|
40013
|
+
});
|
|
40014
|
+
printResult(result, opts.json);
|
|
40015
|
+
} catch (err) {
|
|
40016
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40017
|
+
process.exitCode = 1;
|
|
40018
|
+
}
|
|
40019
|
+
});
|
|
40020
|
+
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) => {
|
|
40021
|
+
try {
|
|
40022
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40023
|
+
const result = await deleteBranch2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40024
|
+
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
40025
|
+
} catch (err) {
|
|
40026
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40027
|
+
process.exitCode = 1;
|
|
40028
|
+
}
|
|
40029
|
+
});
|
|
40030
|
+
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) => {
|
|
40031
|
+
try {
|
|
40032
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40033
|
+
const result = await branchLog2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
40034
|
+
printResult(result, opts.json);
|
|
40035
|
+
} catch (err) {
|
|
40036
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40037
|
+
process.exitCode = 1;
|
|
40038
|
+
}
|
|
40039
|
+
});
|
|
40040
|
+
var snapshot = program2.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
40041
|
+
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) => {
|
|
40042
|
+
try {
|
|
40043
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40044
|
+
const result = await listSnapshots2({
|
|
40045
|
+
apiKey,
|
|
40046
|
+
apiBaseUrl,
|
|
40047
|
+
instanceId,
|
|
40048
|
+
branchName: opts.branch,
|
|
40049
|
+
dataset: opts.dataset,
|
|
40050
|
+
debug: !!opts.debug
|
|
40051
|
+
});
|
|
40052
|
+
printResult(result, opts.json);
|
|
40053
|
+
} catch (err) {
|
|
40054
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40055
|
+
process.exitCode = 1;
|
|
40056
|
+
}
|
|
40057
|
+
});
|
|
40058
|
+
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) => {
|
|
40059
|
+
try {
|
|
40060
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40061
|
+
const result = await createSnapshot2({
|
|
40062
|
+
apiKey,
|
|
40063
|
+
apiBaseUrl,
|
|
40064
|
+
instanceId,
|
|
40065
|
+
cloneId: opts.clone,
|
|
40066
|
+
message: opts.message,
|
|
40067
|
+
debug: !!opts.debug
|
|
40068
|
+
});
|
|
40069
|
+
printResult(result, opts.json);
|
|
40070
|
+
} catch (err) {
|
|
40071
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40072
|
+
process.exitCode = 1;
|
|
40073
|
+
}
|
|
40074
|
+
});
|
|
40075
|
+
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) => {
|
|
40076
|
+
try {
|
|
40077
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
40078
|
+
const result = await destroySnapshot2({
|
|
40079
|
+
apiKey,
|
|
40080
|
+
apiBaseUrl,
|
|
40081
|
+
instanceId,
|
|
40082
|
+
snapshotId,
|
|
40083
|
+
force: !!opts.force,
|
|
40084
|
+
debug: !!opts.debug
|
|
40085
|
+
});
|
|
40086
|
+
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
40087
|
+
} catch (err) {
|
|
40088
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
40089
|
+
process.exitCode = 1;
|
|
40090
|
+
}
|
|
40091
|
+
});
|
|
37909
40092
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
37910
40093
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
37911
40094
|
const rootOpts = program2.opts();
|
|
@@ -37979,29 +40162,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
37979
40162
|
let configDir;
|
|
37980
40163
|
switch (client) {
|
|
37981
40164
|
case "cursor":
|
|
37982
|
-
configPath =
|
|
37983
|
-
configDir =
|
|
40165
|
+
configPath = path9.join(homeDir, ".cursor", "mcp.json");
|
|
40166
|
+
configDir = path9.dirname(configPath);
|
|
37984
40167
|
break;
|
|
37985
40168
|
case "windsurf":
|
|
37986
|
-
configPath =
|
|
37987
|
-
configDir =
|
|
40169
|
+
configPath = path9.join(homeDir, ".windsurf", "mcp.json");
|
|
40170
|
+
configDir = path9.dirname(configPath);
|
|
37988
40171
|
break;
|
|
37989
40172
|
case "codex":
|
|
37990
|
-
configPath =
|
|
37991
|
-
configDir =
|
|
40173
|
+
configPath = path9.join(homeDir, ".codex", "mcp.json");
|
|
40174
|
+
configDir = path9.dirname(configPath);
|
|
37992
40175
|
break;
|
|
37993
40176
|
default:
|
|
37994
40177
|
console.error(`Configuration not implemented for: ${client}`);
|
|
37995
40178
|
process.exitCode = 1;
|
|
37996
40179
|
return;
|
|
37997
40180
|
}
|
|
37998
|
-
if (!
|
|
37999
|
-
|
|
40181
|
+
if (!fs11.existsSync(configDir)) {
|
|
40182
|
+
fs11.mkdirSync(configDir, { recursive: true });
|
|
38000
40183
|
}
|
|
38001
40184
|
let config2 = { mcpServers: {} };
|
|
38002
|
-
if (
|
|
40185
|
+
if (fs11.existsSync(configPath)) {
|
|
38003
40186
|
try {
|
|
38004
|
-
const content =
|
|
40187
|
+
const content = fs11.readFileSync(configPath, "utf8");
|
|
38005
40188
|
config2 = JSON.parse(content);
|
|
38006
40189
|
if (!config2.mcpServers) {
|
|
38007
40190
|
config2.mcpServers = {};
|
|
@@ -38014,7 +40197,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
|
|
|
38014
40197
|
command: pgaiPath,
|
|
38015
40198
|
args: ["mcp", "start"]
|
|
38016
40199
|
};
|
|
38017
|
-
|
|
40200
|
+
fs11.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
|
|
38018
40201
|
console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
|
|
38019
40202
|
console.log(` Config file: ${configPath}`);
|
|
38020
40203
|
console.log("");
|