postgresai 0.16.0-dev.1 → 0.16.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -1
- package/bin/postgres-ai.ts +789 -6
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +2621 -279
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +449 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +761 -0
- package/lib/mcp-server.ts +625 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +125 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +373 -0
- package/test/dblab.test.ts +488 -0
- package/test/e2e/pgai-e2e-smoke.sh +192 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +469 -0
- package/test/joe.test.ts +858 -0
- package/test/monitoring.test.ts +54 -3
- package/test/util.test.ts +111 -1
package/bin/postgres-ai.ts
CHANGED
|
@@ -13,6 +13,32 @@ import { Client } from "pg";
|
|
|
13
13
|
import { startMcpServer } from "../lib/mcp-server";
|
|
14
14
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
|
|
15
15
|
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
|
|
16
|
+
import {
|
|
17
|
+
executeJoeCommand,
|
|
18
|
+
listProjects,
|
|
19
|
+
getCommandResult,
|
|
20
|
+
getCommandStatus,
|
|
21
|
+
formatJoeResult,
|
|
22
|
+
formatProjectsTable,
|
|
23
|
+
DEFAULT_BUDGET_MS,
|
|
24
|
+
type JoeCommand,
|
|
25
|
+
type ExecuteJoeOutcome,
|
|
26
|
+
} from "../lib/joe";
|
|
27
|
+
import {
|
|
28
|
+
resolveDblabInstanceId,
|
|
29
|
+
createClone,
|
|
30
|
+
listClones,
|
|
31
|
+
getClone,
|
|
32
|
+
resetClone,
|
|
33
|
+
destroyClone,
|
|
34
|
+
listBranches,
|
|
35
|
+
createBranch,
|
|
36
|
+
deleteBranch,
|
|
37
|
+
branchLog,
|
|
38
|
+
listSnapshots,
|
|
39
|
+
createSnapshot,
|
|
40
|
+
destroySnapshot,
|
|
41
|
+
} from "../lib/dblab";
|
|
16
42
|
import { resolveBaseUrls } from "../lib/util";
|
|
17
43
|
import { registerAasCollection, parseVcpus } from "../lib/aas-onboard";
|
|
18
44
|
import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "../lib/storage";
|
|
@@ -2443,9 +2469,44 @@ interface MonitoringRegistration {
|
|
|
2443
2469
|
*
|
|
2444
2470
|
* Never throws — registration is best-effort; returns null on failure.
|
|
2445
2471
|
*/
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* Classify how `mon local-install` should register the monitoring instance,
|
|
2475
|
+
* given the raw `--project` value and the resolved instance id.
|
|
2476
|
+
*
|
|
2477
|
+
* - With an instance id: ADOPT the provisioned instance. No project name is
|
|
2478
|
+
* required (the platform returns the real project); a provided name is
|
|
2479
|
+
* normalized and carried through for messaging/fallback.
|
|
2480
|
+
* - No instance id and no project name: ERROR. The hardcoded
|
|
2481
|
+
* "postgres-ai-monitoring" default was removed, and a nameless legacy
|
|
2482
|
+
* self-registration is rejected by v1.monitoring_instance_register (PT400).
|
|
2483
|
+
* - No instance id but a project name: legacy SELF-REGISTER.
|
|
2484
|
+
*
|
|
2485
|
+
* Pure (no I/O) so the decision is unit-testable independently of the large
|
|
2486
|
+
* install command. `projectName` is the trimmed value, or undefined when empty.
|
|
2487
|
+
*/
|
|
2488
|
+
type MonRegistrationPlan =
|
|
2489
|
+
| { kind: "adopt"; projectName: string | undefined }
|
|
2490
|
+
| { kind: "self-register"; projectName: string }
|
|
2491
|
+
| { kind: "error-missing-project"; projectName: undefined };
|
|
2492
|
+
|
|
2493
|
+
function planMonitoringRegistration(args: {
|
|
2494
|
+
project?: string;
|
|
2495
|
+
instanceId?: string;
|
|
2496
|
+
}): MonRegistrationPlan {
|
|
2497
|
+
const projectName = args.project?.trim() || undefined;
|
|
2498
|
+
if (args.instanceId) {
|
|
2499
|
+
return { kind: "adopt", projectName };
|
|
2500
|
+
}
|
|
2501
|
+
if (!projectName) {
|
|
2502
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
2503
|
+
}
|
|
2504
|
+
return { kind: "self-register", projectName };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2446
2507
|
async function registerMonitoringInstance(
|
|
2447
2508
|
apiKey: string,
|
|
2448
|
-
projectName: string,
|
|
2509
|
+
projectName: string | undefined,
|
|
2449
2510
|
opts?: { apiBaseUrl?: string; debug?: boolean; instanceId?: string; retries?: number; retryDelayMs?: number }
|
|
2450
2511
|
): Promise<MonitoringRegistration | null> {
|
|
2451
2512
|
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
@@ -2457,16 +2518,25 @@ async function registerMonitoringInstance(
|
|
|
2457
2518
|
// moment to recover; skipped before the first attempt. Tests pass 0.
|
|
2458
2519
|
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
2459
2520
|
|
|
2521
|
+
// Omit project_name entirely when empty: the adopt path (instance_id present)
|
|
2522
|
+
// relies on the platform returning the real project, and a nameless legacy
|
|
2523
|
+
// self-registration is rejected by v1.monitoring_instance_register (PT400).
|
|
2524
|
+
const hasProjectName = !!(projectName && projectName.trim());
|
|
2525
|
+
|
|
2460
2526
|
if (debug) {
|
|
2461
2527
|
console.error(`\nDebug: Registering monitoring instance...`);
|
|
2462
2528
|
console.error(`Debug: POST ${url}`);
|
|
2463
|
-
console.error(
|
|
2529
|
+
console.error(
|
|
2530
|
+
`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`
|
|
2531
|
+
);
|
|
2464
2532
|
}
|
|
2465
2533
|
|
|
2466
2534
|
const requestBody: Record<string, string> = {
|
|
2467
2535
|
api_token: apiKey,
|
|
2468
|
-
project_name: projectName,
|
|
2469
2536
|
};
|
|
2537
|
+
if (hasProjectName) {
|
|
2538
|
+
requestBody.project_name = projectName as string;
|
|
2539
|
+
}
|
|
2470
2540
|
if (instanceId) {
|
|
2471
2541
|
requestBody.instance_id = instanceId;
|
|
2472
2542
|
}
|
|
@@ -3137,8 +3207,11 @@ mon
|
|
|
3137
3207
|
// and persisted; the legacy self-registration stays fire-and-forget
|
|
3138
3208
|
// (issue platform-all#311).
|
|
3139
3209
|
if (apiKey && !opts.demo) {
|
|
3140
|
-
const projectName = opts.project || "postgres-ai-monitoring";
|
|
3141
3210
|
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
3211
|
+
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
3212
|
+
const projectName = plan.projectName;
|
|
3213
|
+
// `instanceId` truthy ⟺ plan.kind === "adopt"; branch on it directly so
|
|
3214
|
+
// TypeScript narrows instanceId to a defined string in the adopt path.
|
|
3142
3215
|
if (instanceId) {
|
|
3143
3216
|
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
3144
3217
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -3160,11 +3233,17 @@ mon
|
|
|
3160
3233
|
// Request succeeded but carried no usable project field — don't claim
|
|
3161
3234
|
// adoption, but don't report a hard failure either (no re-run needed).
|
|
3162
3235
|
console.error(
|
|
3163
|
-
`⚠ Adopted provisioned instance ${instanceId} but the platform returned no project
|
|
3236
|
+
`⚠ Adopted provisioned instance ${instanceId} but the platform returned no project` +
|
|
3237
|
+
(projectName
|
|
3238
|
+
? ` — reports will use project '${projectName}'`
|
|
3239
|
+
: ` — reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`)
|
|
3164
3240
|
);
|
|
3165
3241
|
} else {
|
|
3166
3242
|
console.error(
|
|
3167
|
-
`⚠ Could not adopt provisioned instance ${instanceId}
|
|
3243
|
+
`⚠ Could not adopt provisioned instance ${instanceId}` +
|
|
3244
|
+
(projectName
|
|
3245
|
+
? ` — reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`
|
|
3246
|
+
: ` — reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`)
|
|
3168
3247
|
);
|
|
3169
3248
|
}
|
|
3170
3249
|
|
|
@@ -3188,6 +3267,17 @@ mon
|
|
|
3188
3267
|
`⚠ AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'\n`
|
|
3189
3268
|
);
|
|
3190
3269
|
}
|
|
3270
|
+
} else if (plan.kind === "error-missing-project") {
|
|
3271
|
+
// Legacy self-registration (no --instance-id) now requires a project
|
|
3272
|
+
// name: the hardcoded "postgres-ai-monitoring" default was removed, and
|
|
3273
|
+
// v1.monitoring_instance_register raises PT400 for a nameless legacy
|
|
3274
|
+
// registration. Console-provisioned installs should adopt with
|
|
3275
|
+
// --instance-id instead.
|
|
3276
|
+
console.error(
|
|
3277
|
+
"✗ A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " +
|
|
3278
|
+
"Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>."
|
|
3279
|
+
);
|
|
3280
|
+
process.exitCode = 1;
|
|
3191
3281
|
} else {
|
|
3192
3282
|
void registerMonitoringInstance(apiKey, projectName, {
|
|
3193
3283
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -5312,6 +5402,695 @@ function tryParseJson(s: string): unknown {
|
|
|
5312
5402
|
try { return JSON.parse(s); } catch { return s; }
|
|
5313
5403
|
}
|
|
5314
5404
|
|
|
5405
|
+
// ---------------------------------------------------------------------------
|
|
5406
|
+
// Joe API v2 command surface (SPEC §6). Every verb is a client-side
|
|
5407
|
+
// submit-then-poll one-shot over `v1.joe_command_submit`; `--project` accepts a
|
|
5408
|
+
// numeric id OR an alias/name (id-or-alias resolution). `plan` is plan-only
|
|
5409
|
+
// (EXPLAIN, no execution); `explain` (EXPLAIN ANALYZE) and `exec` execute on the
|
|
5410
|
+
// hardened, ephemeral DBLab clone. The backend rpcs are mocked in tests.
|
|
5411
|
+
// ---------------------------------------------------------------------------
|
|
5412
|
+
|
|
5413
|
+
interface JoeCliOpts {
|
|
5414
|
+
project?: string;
|
|
5415
|
+
session?: string;
|
|
5416
|
+
newSession?: boolean;
|
|
5417
|
+
budget?: number;
|
|
5418
|
+
query?: string;
|
|
5419
|
+
variant?: string;
|
|
5420
|
+
debug?: boolean;
|
|
5421
|
+
json?: boolean;
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
// ============================================================================
|
|
5425
|
+
// DBLab companion command surface (Joe API v2 · SPEC §8)
|
|
5426
|
+
//
|
|
5427
|
+
// `pgai dblab clone|branch|snapshot …` proxy the SAME Platform DBLab API the Console
|
|
5428
|
+
// already drives (v1.dblab_api_call). Every verb is `--project <id|alias>`
|
|
5429
|
+
// scoped: the project's single DBLab instance is resolved server-side listing,
|
|
5430
|
+
// then the verb is proxied. The DESTRUCTIVE verbs — the HTTP DELETEs: clone
|
|
5431
|
+
// destroy, branch delete, snapshot destroy — require the `joe:admin` token
|
|
5432
|
+
// scope (user-approved tightening, !612); clone reset is a POST and is NOT
|
|
5433
|
+
// gated. Read/list/create verbs stay at org-token + project-allowlist level.
|
|
5434
|
+
// All enforced backend-side — a PT403 surfaces here as an HTTP 403.
|
|
5435
|
+
//
|
|
5436
|
+
// NOTE: kept in its own region and its own lib (cli/lib/dblab.ts) so it does not
|
|
5437
|
+
// collide with the parallel Joe-commands work.
|
|
5438
|
+
// ============================================================================
|
|
5439
|
+
|
|
5440
|
+
interface DblabCmdOpts {
|
|
5441
|
+
project?: string;
|
|
5442
|
+
debug?: boolean;
|
|
5443
|
+
json?: boolean;
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
function inferCommandFromResult(r: { command?: JoeCommand; plan_json?: unknown; plan_text?: string | null; row_count?: number | null; result_rows?: unknown[] | null; hypo_plan?: unknown; hypo_used?: boolean | null; terminated?: boolean | null; pid?: number | null; reset?: boolean | null; snapshot?: unknown }): JoeCommand | null {
|
|
5447
|
+
if (r.command) return r.command;
|
|
5448
|
+
if (r.plan_text !== undefined || r.plan_json !== undefined) return "plan";
|
|
5449
|
+
if (r.row_count !== undefined || r.result_rows !== undefined) return "exec";
|
|
5450
|
+
if (r.hypo_used !== undefined) return "hypo";
|
|
5451
|
+
if (r.terminated !== undefined) return "terminate";
|
|
5452
|
+
if (r.reset !== undefined) return "reset";
|
|
5453
|
+
if (r.snapshot !== undefined) return "activity";
|
|
5454
|
+
return null;
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
function printJoeOutcome(command: JoeCommand, outcome: ExecuteJoeOutcome, json: boolean, budgetMs?: number): void {
|
|
5458
|
+
// The expiry hint reports the ACTUAL effective budget (--budget when given,
|
|
5459
|
+
// the default otherwise) — not a hardcoded DEFAULT_BUDGET_MS.
|
|
5460
|
+
const effectiveBudgetMs =
|
|
5461
|
+
typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
5462
|
+
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
5463
|
+
// One-shot budget reached before a terminal state — hand back a resume handle.
|
|
5464
|
+
// This is expected (a cold clone), NOT a failure: exit 0.
|
|
5465
|
+
if (outcome.budgetExpired) {
|
|
5466
|
+
if (json) {
|
|
5467
|
+
console.log(
|
|
5468
|
+
JSON.stringify(
|
|
5469
|
+
{
|
|
5470
|
+
command_id: outcome.commandId,
|
|
5471
|
+
status: outcome.status,
|
|
5472
|
+
session_id: outcome.sessionId,
|
|
5473
|
+
budget_expired: true,
|
|
5474
|
+
resume: `pgai joe result ${outcome.commandId}`,
|
|
5475
|
+
},
|
|
5476
|
+
null,
|
|
5477
|
+
2
|
|
5478
|
+
)
|
|
5479
|
+
);
|
|
5480
|
+
} else {
|
|
5481
|
+
console.log(
|
|
5482
|
+
`submitted ${outcome.commandId} · ${outcome.status} · budget ${budgetSeconds}s reached — resume: pgai joe result ${outcome.commandId}`
|
|
5483
|
+
);
|
|
5484
|
+
}
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
const result = outcome.result;
|
|
5489
|
+
if (outcome.status === "done" && result) {
|
|
5490
|
+
if (json) {
|
|
5491
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5492
|
+
} else {
|
|
5493
|
+
console.log(`submitted ${outcome.commandId} · done`);
|
|
5494
|
+
const body = formatJoeResult(command, result);
|
|
5495
|
+
if (body) console.log(body);
|
|
5496
|
+
}
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
if (outcome.status === "error") {
|
|
5501
|
+
const msg = result?.error ?? "command failed";
|
|
5502
|
+
console.error(`command ${outcome.commandId} error: ${msg}`);
|
|
5503
|
+
process.exitCode = 1;
|
|
5504
|
+
return;
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
// Server-side timed_out (the dispatcher gave up). The genuine reply may still
|
|
5508
|
+
// land; suggest a resume, but signal failure with a non-zero exit.
|
|
5509
|
+
console.error(
|
|
5510
|
+
`command ${outcome.commandId} timed out on the server — resume: pgai joe result ${outcome.commandId}`
|
|
5511
|
+
);
|
|
5512
|
+
process.exitCode = 1;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
async function runJoeCli(
|
|
5516
|
+
command: JoeCommand,
|
|
5517
|
+
sql: string | null,
|
|
5518
|
+
args: Record<string, unknown> | null,
|
|
5519
|
+
opts: JoeCliOpts
|
|
5520
|
+
): Promise<void> {
|
|
5521
|
+
try {
|
|
5522
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5523
|
+
const cfg = config.readConfig();
|
|
5524
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5525
|
+
if (!apiKey) {
|
|
5526
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5527
|
+
process.exitCode = 1;
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
5531
|
+
if (!projectRef) {
|
|
5532
|
+
console.error("Project is required. Pass --project <id|alias>.");
|
|
5533
|
+
process.exitCode = 1;
|
|
5534
|
+
return;
|
|
5535
|
+
}
|
|
5536
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5537
|
+
const budgetMs =
|
|
5538
|
+
typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
|
|
5539
|
+
|
|
5540
|
+
const outcome = await executeJoeCommand({
|
|
5541
|
+
apiKey,
|
|
5542
|
+
apiBaseUrl,
|
|
5543
|
+
command,
|
|
5544
|
+
project: projectRef,
|
|
5545
|
+
sql,
|
|
5546
|
+
args,
|
|
5547
|
+
session: opts.session ?? null,
|
|
5548
|
+
newSession: !!opts.newSession,
|
|
5549
|
+
orgId: cfg.orgId ?? undefined,
|
|
5550
|
+
budgetMs,
|
|
5551
|
+
debug: !!opts.debug,
|
|
5552
|
+
});
|
|
5553
|
+
|
|
5554
|
+
printJoeOutcome(command, outcome, !!opts.json, budgetMs);
|
|
5555
|
+
} catch (err) {
|
|
5556
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5557
|
+
console.error(message);
|
|
5558
|
+
process.exitCode = 1;
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
|
|
5562
|
+
/** Attach the shared Joe options (--project / --session / --new-session / --budget). */
|
|
5563
|
+
function withJoeOptions(cmd: import("commander").Command): import("commander").Command {
|
|
5564
|
+
return cmd
|
|
5565
|
+
.option("--project <id|alias>", "target project by numeric id OR alias/name")
|
|
5566
|
+
.option("--session <id>", "run on a specific session's clone")
|
|
5567
|
+
.option("--new-session", "start a fresh session/clone (null session)")
|
|
5568
|
+
.option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseInt(v, 10))
|
|
5569
|
+
.option("--debug", "enable debug output")
|
|
5570
|
+
.option("--json", "output raw JSON");
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
// ---------------------------------------------------------------------------
|
|
5574
|
+
// Joe command group — every Joe verb lives under `pgai joe <verb>` (grouped CLI,
|
|
5575
|
+
// USER-AUTHORIZED restructure). The MCP tool names stay flat/unchanged; only the
|
|
5576
|
+
// CLI invocation path is grouped. `projects` stays top-level (org-level, SPEC §6).
|
|
5577
|
+
// ---------------------------------------------------------------------------
|
|
5578
|
+
const joe = program
|
|
5579
|
+
.command("joe")
|
|
5580
|
+
.description("Joe API v2 — plan/EXPLAIN/exec queries on ephemeral DBLab clones");
|
|
5581
|
+
|
|
5582
|
+
withJoeOptions(
|
|
5583
|
+
joe
|
|
5584
|
+
.command("plan <sql>")
|
|
5585
|
+
.description("plan a query (EXPLAIN, plan-only — no execution; the fast/safe default)")
|
|
5586
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5587
|
+
await runJoeCli("plan", sql, null, opts);
|
|
5588
|
+
});
|
|
5589
|
+
|
|
5590
|
+
withJoeOptions(
|
|
5591
|
+
joe
|
|
5592
|
+
.command("explain <sql>")
|
|
5593
|
+
.description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")
|
|
5594
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5595
|
+
await runJoeCli("explain", sql, null, opts);
|
|
5596
|
+
});
|
|
5597
|
+
|
|
5598
|
+
withJoeOptions(
|
|
5599
|
+
joe
|
|
5600
|
+
.command("exec <sql>")
|
|
5601
|
+
.description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")
|
|
5602
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5603
|
+
await runJoeCli("exec", sql, null, opts);
|
|
5604
|
+
});
|
|
5605
|
+
|
|
5606
|
+
withJoeOptions(
|
|
5607
|
+
joe
|
|
5608
|
+
.command("hypo <indexSql>")
|
|
5609
|
+
.description("HypoPG hypothetical index + re-plan (no real index, no data change)")
|
|
5610
|
+
.requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")
|
|
5611
|
+
).action(async (indexSql: string, opts: JoeCliOpts) => {
|
|
5612
|
+
await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
|
|
5613
|
+
});
|
|
5614
|
+
|
|
5615
|
+
withJoeOptions(
|
|
5616
|
+
joe
|
|
5617
|
+
.command("activity")
|
|
5618
|
+
.description("running-activity snapshot (pg_stat_activity; query text redacted)")
|
|
5619
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5620
|
+
await runJoeCli("activity", null, null, opts);
|
|
5621
|
+
});
|
|
5622
|
+
|
|
5623
|
+
withJoeOptions(
|
|
5624
|
+
joe
|
|
5625
|
+
.command("terminate <pid>")
|
|
5626
|
+
.description("pg_terminate_backend(pid) on the clone")
|
|
5627
|
+
).action(async (pid: string, opts: JoeCliOpts) => {
|
|
5628
|
+
// A pid must be a bare non-negative integer. parseInt() silently accepts
|
|
5629
|
+
// trailing garbage ("12x"→12), a sign ("-5"→-5), decimals ("1.5"→1) and hex
|
|
5630
|
+
// ("0x10"→0) — any of which would submit a terminate against the WRONG
|
|
5631
|
+
// backend pid. Require a fully-numeric token so a mistyped pid is a clean,
|
|
5632
|
+
// typed rejection that never reaches the submit rpc.
|
|
5633
|
+
const pidStr = (pid ?? "").trim();
|
|
5634
|
+
if (!/^[0-9]+$/.test(pidStr)) {
|
|
5635
|
+
console.error("pid must be a number");
|
|
5636
|
+
process.exitCode = 1;
|
|
5637
|
+
return;
|
|
5638
|
+
}
|
|
5639
|
+
await runJoeCli("terminate", null, { pid: parseInt(pidStr, 10) }, opts);
|
|
5640
|
+
});
|
|
5641
|
+
|
|
5642
|
+
withJoeOptions(
|
|
5643
|
+
joe
|
|
5644
|
+
.command("reset")
|
|
5645
|
+
.description("reset/recreate the session's thin clone")
|
|
5646
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5647
|
+
await runJoeCli("reset", null, null, opts);
|
|
5648
|
+
});
|
|
5649
|
+
|
|
5650
|
+
withJoeOptions(
|
|
5651
|
+
joe
|
|
5652
|
+
.command("describe <object>")
|
|
5653
|
+
.description("\\d-family schema/relation/index metadata")
|
|
5654
|
+
.option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")
|
|
5655
|
+
).action(async (object: string, opts: JoeCliOpts) => {
|
|
5656
|
+
const args: Record<string, unknown> = { object };
|
|
5657
|
+
if (opts.variant) args.variant = opts.variant;
|
|
5658
|
+
await runJoeCli("describe", null, args, opts);
|
|
5659
|
+
});
|
|
5660
|
+
|
|
5661
|
+
// history search is roadmapped for M1b; the search backend is not deployed yet.
|
|
5662
|
+
// Ship a recognized subcommand so the documented `pgai joe history …` example
|
|
5663
|
+
// degrades cleanly (an informative "not yet available" line + exit 1) instead of
|
|
5664
|
+
// Commander's generic "unknown command 'history'". Wire it to the rpc when M1b lands.
|
|
5665
|
+
withJoeOptions(
|
|
5666
|
+
joe
|
|
5667
|
+
.command("history <terms>")
|
|
5668
|
+
.description("search prior Joe analyses, metadata-only (M1b — not yet available)")
|
|
5669
|
+
).action(async (_terms: string, _opts: JoeCliOpts) => {
|
|
5670
|
+
console.error(
|
|
5671
|
+
"Joe history search is not available yet — it is planned for M1b. " +
|
|
5672
|
+
"No history-search backend is deployed; this command is a placeholder until then.",
|
|
5673
|
+
);
|
|
5674
|
+
process.exitCode = 1;
|
|
5675
|
+
});
|
|
5676
|
+
|
|
5677
|
+
// Org-level discovery — a general postgresai command, NOT a Joe endpoint (SPEC §6).
|
|
5678
|
+
program
|
|
5679
|
+
.command("projects")
|
|
5680
|
+
.description("list the org's projects (shows which have Joe ready) — org-level, not a Joe endpoint")
|
|
5681
|
+
.option("--debug", "enable debug output")
|
|
5682
|
+
.option("--json", "output raw JSON")
|
|
5683
|
+
.action(async (opts: { debug?: boolean; json?: boolean }) => {
|
|
5684
|
+
try {
|
|
5685
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5686
|
+
const cfg = config.readConfig();
|
|
5687
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5688
|
+
if (!apiKey) {
|
|
5689
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5690
|
+
process.exitCode = 1;
|
|
5691
|
+
return;
|
|
5692
|
+
}
|
|
5693
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5694
|
+
const projects = await listProjects({
|
|
5695
|
+
apiKey,
|
|
5696
|
+
apiBaseUrl,
|
|
5697
|
+
orgId: cfg.orgId ?? undefined,
|
|
5698
|
+
debug: !!opts.debug,
|
|
5699
|
+
});
|
|
5700
|
+
if (opts.json) {
|
|
5701
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
5702
|
+
} else {
|
|
5703
|
+
console.log(formatProjectsTable(projects));
|
|
5704
|
+
}
|
|
5705
|
+
} catch (err) {
|
|
5706
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5707
|
+
console.error(message);
|
|
5708
|
+
process.exitCode = 1;
|
|
5709
|
+
}
|
|
5710
|
+
});
|
|
5711
|
+
|
|
5712
|
+
/**
|
|
5713
|
+
* Resolve the shared inputs every DBLab verb needs: the api key, the api base
|
|
5714
|
+
* url, and the project's single DBLab `instance_id`. Throws (caught by each
|
|
5715
|
+
* action → exit 1) when the api key or `--project` is missing, or when no DBLab
|
|
5716
|
+
* instance can be resolved for the project.
|
|
5717
|
+
*/
|
|
5718
|
+
async function resolveDblabTarget(
|
|
5719
|
+
project: string | undefined,
|
|
5720
|
+
debug: boolean
|
|
5721
|
+
): Promise<{ apiKey: string; apiBaseUrl: string; orgId?: number; instanceId: string }> {
|
|
5722
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5723
|
+
const cfg = config.readConfig();
|
|
5724
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5725
|
+
if (!apiKey) {
|
|
5726
|
+
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5727
|
+
}
|
|
5728
|
+
const ref = (project ?? "").trim();
|
|
5729
|
+
if (!ref) {
|
|
5730
|
+
throw new Error("--project <id|alias> is required");
|
|
5731
|
+
}
|
|
5732
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5733
|
+
const orgId = cfg.orgId ?? undefined;
|
|
5734
|
+
const instanceId = await resolveDblabInstanceId({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
5735
|
+
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5738
|
+
// ---------------------------------------------------------------------------
|
|
5739
|
+
// DBLab command group — every DBLab verb lives under
|
|
5740
|
+
// `pgai dblab <clone|branch|snapshot> …` (grouped CLI, USER-AUTHORIZED restructure).
|
|
5741
|
+
// MCP tool names (clone_create/…) stay flat/unchanged; only the CLI path is grouped.
|
|
5742
|
+
// ---------------------------------------------------------------------------
|
|
5743
|
+
const dblab = program
|
|
5744
|
+
.command("dblab")
|
|
5745
|
+
.description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
|
|
5746
|
+
|
|
5747
|
+
// ---- clone ----------------------------------------------------------------
|
|
5748
|
+
|
|
5749
|
+
const clone = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
5750
|
+
|
|
5751
|
+
clone
|
|
5752
|
+
.command("create")
|
|
5753
|
+
.description("create a thin clone of the project's database")
|
|
5754
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5755
|
+
.option("--branch <branch>", "branch to clone from")
|
|
5756
|
+
.option("--snapshot <id>", "snapshot id to clone from")
|
|
5757
|
+
.option("--id <id>", "clone id (DBLab generates one when omitted)")
|
|
5758
|
+
.option("--db-user <user>", "clone DB user")
|
|
5759
|
+
.option("--db-password <password>", "clone DB password")
|
|
5760
|
+
.option("--protected", "protect the clone from auto-deletion")
|
|
5761
|
+
.option("--debug", "enable debug output")
|
|
5762
|
+
.option("--json", "output raw JSON")
|
|
5763
|
+
.action(async (opts: DblabCmdOpts & { branch?: string; snapshot?: string; id?: string; dbUser?: string; dbPassword?: string; protected?: boolean }) => {
|
|
5764
|
+
try {
|
|
5765
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5766
|
+
const result = await createClone({
|
|
5767
|
+
apiKey, apiBaseUrl, instanceId,
|
|
5768
|
+
cloneId: opts.id,
|
|
5769
|
+
branch: opts.branch,
|
|
5770
|
+
snapshotId: opts.snapshot,
|
|
5771
|
+
dbUser: opts.dbUser,
|
|
5772
|
+
dbPassword: opts.dbPassword,
|
|
5773
|
+
isProtected: !!opts.protected,
|
|
5774
|
+
debug: !!opts.debug,
|
|
5775
|
+
});
|
|
5776
|
+
printResult(result, opts.json);
|
|
5777
|
+
} catch (err) {
|
|
5778
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5779
|
+
process.exitCode = 1;
|
|
5780
|
+
}
|
|
5781
|
+
});
|
|
5782
|
+
|
|
5783
|
+
// Resume / inspect a previously submitted command by id. Part of the `joe` group
|
|
5784
|
+
// (`pgai joe status <commandId>`) — distinct from `pgai dblab clone status <cloneId>`.
|
|
5785
|
+
joe
|
|
5786
|
+
.command("status <commandId>")
|
|
5787
|
+
.description("show a Joe command's status (metadata only)")
|
|
5788
|
+
.option("--debug", "enable debug output")
|
|
5789
|
+
.option("--json", "output raw JSON")
|
|
5790
|
+
.action(async (commandId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
5791
|
+
try {
|
|
5792
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5793
|
+
const cfg = config.readConfig();
|
|
5794
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5795
|
+
if (!apiKey) {
|
|
5796
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5797
|
+
process.exitCode = 1;
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5801
|
+
const status = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
5802
|
+
if (opts.json) {
|
|
5803
|
+
console.log(JSON.stringify(status, null, 2));
|
|
5804
|
+
} else {
|
|
5805
|
+
console.log(`command ${status.command_id} · ${status.status}${status.error ? ` · ${status.error}` : ""}`);
|
|
5806
|
+
}
|
|
5807
|
+
} catch (err) {
|
|
5808
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5809
|
+
console.error(message);
|
|
5810
|
+
process.exitCode = 1;
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
5813
|
+
|
|
5814
|
+
clone
|
|
5815
|
+
.command("list")
|
|
5816
|
+
.description("list the project's thin clones")
|
|
5817
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5818
|
+
.option("--debug", "enable debug output")
|
|
5819
|
+
.option("--json", "output raw JSON")
|
|
5820
|
+
.action(async (opts: DblabCmdOpts) => {
|
|
5821
|
+
try {
|
|
5822
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5823
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
5824
|
+
printResult(result, opts.json);
|
|
5825
|
+
} catch (err) {
|
|
5826
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5827
|
+
process.exitCode = 1;
|
|
5828
|
+
}
|
|
5829
|
+
});
|
|
5830
|
+
|
|
5831
|
+
joe
|
|
5832
|
+
.command("result <commandId>")
|
|
5833
|
+
.description("fetch a Joe command's result by id (resume a timed-out one-shot)")
|
|
5834
|
+
.option("--debug", "enable debug output")
|
|
5835
|
+
.option("--json", "output raw JSON")
|
|
5836
|
+
.action(async (commandId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
5837
|
+
try {
|
|
5838
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5839
|
+
const cfg = config.readConfig();
|
|
5840
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5841
|
+
if (!apiKey) {
|
|
5842
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5843
|
+
process.exitCode = 1;
|
|
5844
|
+
return;
|
|
5845
|
+
}
|
|
5846
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5847
|
+
const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
5848
|
+
if (opts.json) {
|
|
5849
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5850
|
+
// Same exit contract as the human output and the one-shot path
|
|
5851
|
+
// (printJoeOutcome): a terminal failure is a non-result — scripts
|
|
5852
|
+
// consuming --json must not proceed on it.
|
|
5853
|
+
if (result.status === "error" || result.status === "timed_out") {
|
|
5854
|
+
process.exitCode = 1;
|
|
5855
|
+
}
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
const inferred = inferCommandFromResult(result);
|
|
5859
|
+
if (result.status === "done" && inferred) {
|
|
5860
|
+
console.log(`command ${result.command_id} · done`);
|
|
5861
|
+
const body = formatJoeResult(inferred, result);
|
|
5862
|
+
if (body) console.log(body);
|
|
5863
|
+
} else if (result.status === "error") {
|
|
5864
|
+
console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
|
|
5865
|
+
process.exitCode = 1;
|
|
5866
|
+
} else if (result.status === "timed_out") {
|
|
5867
|
+
// Terminal server-side timeout — a non-result. Match the one-shot path
|
|
5868
|
+
// (printJoeOutcome): report and exit non-zero so `pgai joe result $id
|
|
5869
|
+
// && next-step` scripts do not proceed.
|
|
5870
|
+
console.error(`command ${result.command_id} timed out on the server`);
|
|
5871
|
+
process.exitCode = 1;
|
|
5872
|
+
} else {
|
|
5873
|
+
console.log(`command ${result.command_id} · ${result.status}`);
|
|
5874
|
+
}
|
|
5875
|
+
} catch (err) {
|
|
5876
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5877
|
+
console.error(message);
|
|
5878
|
+
process.exitCode = 1;
|
|
5879
|
+
}
|
|
5880
|
+
});
|
|
5881
|
+
|
|
5882
|
+
clone
|
|
5883
|
+
.command("status <cloneId>")
|
|
5884
|
+
.description("show a clone's status")
|
|
5885
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5886
|
+
.option("--debug", "enable debug output")
|
|
5887
|
+
.option("--json", "output raw JSON")
|
|
5888
|
+
.action(async (cloneId: string, opts: DblabCmdOpts) => {
|
|
5889
|
+
try {
|
|
5890
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5891
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
5892
|
+
printResult(result, opts.json);
|
|
5893
|
+
} catch (err) {
|
|
5894
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5895
|
+
process.exitCode = 1;
|
|
5896
|
+
}
|
|
5897
|
+
});
|
|
5898
|
+
|
|
5899
|
+
clone
|
|
5900
|
+
.command("reset <cloneId>")
|
|
5901
|
+
.description("reset a clone to a pristine snapshot")
|
|
5902
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5903
|
+
.option("--snapshot <id>", "snapshot id to reset to (default: latest)")
|
|
5904
|
+
.option("--latest", "reset to the latest snapshot")
|
|
5905
|
+
.option("--debug", "enable debug output")
|
|
5906
|
+
.option("--json", "output raw JSON")
|
|
5907
|
+
.action(async (cloneId: string, opts: DblabCmdOpts & { snapshot?: string; latest?: boolean }) => {
|
|
5908
|
+
try {
|
|
5909
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5910
|
+
const result = await resetClone({
|
|
5911
|
+
apiKey, apiBaseUrl, instanceId, cloneId,
|
|
5912
|
+
snapshotId: opts.snapshot,
|
|
5913
|
+
latest: opts.latest,
|
|
5914
|
+
debug: !!opts.debug,
|
|
5915
|
+
});
|
|
5916
|
+
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
5917
|
+
} catch (err) {
|
|
5918
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5919
|
+
process.exitCode = 1;
|
|
5920
|
+
}
|
|
5921
|
+
});
|
|
5922
|
+
|
|
5923
|
+
clone
|
|
5924
|
+
.command("destroy <cloneId>")
|
|
5925
|
+
.description("destroy a clone (requires joe:admin)")
|
|
5926
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5927
|
+
.option("--debug", "enable debug output")
|
|
5928
|
+
.option("--json", "output raw JSON")
|
|
5929
|
+
.action(async (cloneId: string, opts: DblabCmdOpts) => {
|
|
5930
|
+
try {
|
|
5931
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5932
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
5933
|
+
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
5934
|
+
} catch (err) {
|
|
5935
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5936
|
+
process.exitCode = 1;
|
|
5937
|
+
}
|
|
5938
|
+
});
|
|
5939
|
+
|
|
5940
|
+
// ---- branch ---------------------------------------------------------------
|
|
5941
|
+
|
|
5942
|
+
const branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
5943
|
+
|
|
5944
|
+
branch
|
|
5945
|
+
.command("list")
|
|
5946
|
+
.description("list the project's branches")
|
|
5947
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5948
|
+
.option("--debug", "enable debug output")
|
|
5949
|
+
.option("--json", "output raw JSON")
|
|
5950
|
+
.action(async (opts: DblabCmdOpts) => {
|
|
5951
|
+
try {
|
|
5952
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5953
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
5954
|
+
printResult(result, opts.json);
|
|
5955
|
+
} catch (err) {
|
|
5956
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5957
|
+
process.exitCode = 1;
|
|
5958
|
+
}
|
|
5959
|
+
});
|
|
5960
|
+
|
|
5961
|
+
branch
|
|
5962
|
+
.command("create <name>")
|
|
5963
|
+
.description("create a branch")
|
|
5964
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5965
|
+
.option("--snapshot <id>", "snapshot id to base the branch on")
|
|
5966
|
+
.option("--base-branch <branch>", "parent branch to fork from")
|
|
5967
|
+
.option("--debug", "enable debug output")
|
|
5968
|
+
.option("--json", "output raw JSON")
|
|
5969
|
+
.action(async (name: string, opts: DblabCmdOpts & { snapshot?: string; baseBranch?: string }) => {
|
|
5970
|
+
try {
|
|
5971
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5972
|
+
const result = await createBranch({
|
|
5973
|
+
apiKey, apiBaseUrl, instanceId,
|
|
5974
|
+
branchName: name,
|
|
5975
|
+
baseBranch: opts.baseBranch,
|
|
5976
|
+
snapshotId: opts.snapshot,
|
|
5977
|
+
debug: !!opts.debug,
|
|
5978
|
+
});
|
|
5979
|
+
printResult(result, opts.json);
|
|
5980
|
+
} catch (err) {
|
|
5981
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5982
|
+
process.exitCode = 1;
|
|
5983
|
+
}
|
|
5984
|
+
});
|
|
5985
|
+
|
|
5986
|
+
branch
|
|
5987
|
+
.command("delete <name>")
|
|
5988
|
+
.description("delete a branch (requires joe:admin)")
|
|
5989
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5990
|
+
.option("--debug", "enable debug output")
|
|
5991
|
+
.option("--json", "output raw JSON")
|
|
5992
|
+
.action(async (name: string, opts: DblabCmdOpts) => {
|
|
5993
|
+
try {
|
|
5994
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5995
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
5996
|
+
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
5997
|
+
} catch (err) {
|
|
5998
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5999
|
+
process.exitCode = 1;
|
|
6000
|
+
}
|
|
6001
|
+
});
|
|
6002
|
+
|
|
6003
|
+
branch
|
|
6004
|
+
.command("log <name>")
|
|
6005
|
+
.description("show a branch's snapshot log")
|
|
6006
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
6007
|
+
.option("--debug", "enable debug output")
|
|
6008
|
+
.option("--json", "output raw JSON")
|
|
6009
|
+
.action(async (name: string, opts: DblabCmdOpts) => {
|
|
6010
|
+
try {
|
|
6011
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6012
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
6013
|
+
printResult(result, opts.json);
|
|
6014
|
+
} catch (err) {
|
|
6015
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6016
|
+
process.exitCode = 1;
|
|
6017
|
+
}
|
|
6018
|
+
});
|
|
6019
|
+
|
|
6020
|
+
// ---- snapshot -------------------------------------------------------------
|
|
6021
|
+
|
|
6022
|
+
const snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
6023
|
+
|
|
6024
|
+
snapshot
|
|
6025
|
+
.command("list")
|
|
6026
|
+
.description("list the project's snapshots")
|
|
6027
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
6028
|
+
.option("--branch <branch>", "filter by branch")
|
|
6029
|
+
.option("--dataset <dataset>", "filter by dataset")
|
|
6030
|
+
.option("--debug", "enable debug output")
|
|
6031
|
+
.option("--json", "output raw JSON")
|
|
6032
|
+
.action(async (opts: DblabCmdOpts & { branch?: string; dataset?: string }) => {
|
|
6033
|
+
try {
|
|
6034
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6035
|
+
const result = await listSnapshots({
|
|
6036
|
+
apiKey, apiBaseUrl, instanceId,
|
|
6037
|
+
branchName: opts.branch,
|
|
6038
|
+
dataset: opts.dataset,
|
|
6039
|
+
debug: !!opts.debug,
|
|
6040
|
+
});
|
|
6041
|
+
printResult(result, opts.json);
|
|
6042
|
+
} catch (err) {
|
|
6043
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6044
|
+
process.exitCode = 1;
|
|
6045
|
+
}
|
|
6046
|
+
});
|
|
6047
|
+
|
|
6048
|
+
snapshot
|
|
6049
|
+
.command("create")
|
|
6050
|
+
.description("create a snapshot from a clone")
|
|
6051
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
6052
|
+
.requiredOption("--clone <id>", "clone id to snapshot")
|
|
6053
|
+
.option("--message <message>", "snapshot message")
|
|
6054
|
+
.option("--debug", "enable debug output")
|
|
6055
|
+
.option("--json", "output raw JSON")
|
|
6056
|
+
.action(async (opts: DblabCmdOpts & { clone?: string; message?: string }) => {
|
|
6057
|
+
try {
|
|
6058
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6059
|
+
const result = await createSnapshot({
|
|
6060
|
+
apiKey, apiBaseUrl, instanceId,
|
|
6061
|
+
cloneId: opts.clone as string,
|
|
6062
|
+
message: opts.message,
|
|
6063
|
+
debug: !!opts.debug,
|
|
6064
|
+
});
|
|
6065
|
+
printResult(result, opts.json);
|
|
6066
|
+
} catch (err) {
|
|
6067
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6068
|
+
process.exitCode = 1;
|
|
6069
|
+
}
|
|
6070
|
+
});
|
|
6071
|
+
|
|
6072
|
+
snapshot
|
|
6073
|
+
.command("destroy <snapshotId>")
|
|
6074
|
+
.description("destroy a snapshot (requires joe:admin)")
|
|
6075
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
6076
|
+
.option("--force", "force-delete even when dependent clones exist")
|
|
6077
|
+
.option("--debug", "enable debug output")
|
|
6078
|
+
.option("--json", "output raw JSON")
|
|
6079
|
+
.action(async (snapshotId: string, opts: DblabCmdOpts & { force?: boolean }) => {
|
|
6080
|
+
try {
|
|
6081
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6082
|
+
const result = await destroySnapshot({
|
|
6083
|
+
apiKey, apiBaseUrl, instanceId, snapshotId,
|
|
6084
|
+
force: !!opts.force,
|
|
6085
|
+
debug: !!opts.debug,
|
|
6086
|
+
});
|
|
6087
|
+
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
6088
|
+
} catch (err) {
|
|
6089
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6090
|
+
process.exitCode = 1;
|
|
6091
|
+
}
|
|
6092
|
+
});
|
|
6093
|
+
|
|
5315
6094
|
// MCP server
|
|
5316
6095
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
5317
6096
|
|
|
@@ -5488,3 +6267,7 @@ if (import.meta.main) {
|
|
|
5488
6267
|
// same functions used by the `mon` commands).
|
|
5489
6268
|
export { refreshBundledComposeIfStale, readDeployedTag, isValidComposeYaml };
|
|
5490
6269
|
export { registerMonitoringInstance, resolveAdoptedProject, type MonitoringRegistration };
|
|
6270
|
+
export { planMonitoringRegistration, type MonRegistrationPlan };
|
|
6271
|
+
// Joe: the `pgai joe result <id>` result-body -> command inference (unit-tested
|
|
6272
|
+
// table-driven; the shapes are the per-command contract matrix in CONTRACT_DECISIONS).
|
|
6273
|
+
export { inferCommandFromResult };
|