postgresai 0.16.0-dev.2 → 0.16.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +657 -0
- package/dist/bin/postgres-ai.js +2472 -249
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +457 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +730 -0
- package/lib/mcp-server.ts +597 -0
- package/lib/supabase.ts +0 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +339 -0
- package/test/dblab.test.ts +408 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +298 -0
- package/test/joe.test.ts +726 -0
package/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";
|
|
@@ -5376,6 +5402,637 @@ function tryParseJson(s: string): unknown {
|
|
|
5376
5402
|
try { return JSON.parse(s); } catch { return s; }
|
|
5377
5403
|
}
|
|
5378
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 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. Destructive verbs (clone reset/destroy, branch
|
|
5431
|
+
// delete, snapshot destroy) require the `joe:admin` token scope; read/create
|
|
5432
|
+
// verbs require `joe:plan` — enforced backend-side (a PT403 surfaces here).
|
|
5433
|
+
//
|
|
5434
|
+
// NOTE: kept in its own region and its own lib (cli/lib/dblab.ts) so it does not
|
|
5435
|
+
// collide with the parallel Joe-commands work.
|
|
5436
|
+
// ============================================================================
|
|
5437
|
+
|
|
5438
|
+
interface DblabCmdOpts {
|
|
5439
|
+
project?: string;
|
|
5440
|
+
debug?: boolean;
|
|
5441
|
+
json?: boolean;
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
function inferCommandFromResult(r: { command?: JoeCommand; plan_json?: unknown; plan_text?: string | null; row_count?: number | null; result_rows?: unknown[] | null; hypo_used?: boolean | null; terminated?: boolean | null; reset?: boolean | null; snapshot?: unknown }): JoeCommand | null {
|
|
5445
|
+
if (r.command) return r.command;
|
|
5446
|
+
if (r.plan_text !== undefined || r.plan_json !== undefined) return "plan";
|
|
5447
|
+
if (r.row_count !== undefined || r.result_rows !== undefined) return "exec";
|
|
5448
|
+
if (r.hypo_used !== undefined) return "hypo";
|
|
5449
|
+
if (r.terminated !== undefined) return "terminate";
|
|
5450
|
+
if (r.reset !== undefined) return "reset";
|
|
5451
|
+
if (r.snapshot !== undefined) return "activity";
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
function printJoeOutcome(command: JoeCommand, outcome: ExecuteJoeOutcome, json: boolean): void {
|
|
5456
|
+
const budgetSeconds = Math.round(DEFAULT_BUDGET_MS / 1000);
|
|
5457
|
+
// One-shot budget reached before a terminal state — hand back a resume handle.
|
|
5458
|
+
// This is expected (a cold clone), NOT a failure: exit 0.
|
|
5459
|
+
if (outcome.budgetExpired) {
|
|
5460
|
+
if (json) {
|
|
5461
|
+
console.log(
|
|
5462
|
+
JSON.stringify(
|
|
5463
|
+
{
|
|
5464
|
+
command_id: outcome.commandId,
|
|
5465
|
+
status: outcome.status,
|
|
5466
|
+
session_id: outcome.sessionId,
|
|
5467
|
+
budget_expired: true,
|
|
5468
|
+
resume: `pgai result ${outcome.commandId}`,
|
|
5469
|
+
},
|
|
5470
|
+
null,
|
|
5471
|
+
2
|
|
5472
|
+
)
|
|
5473
|
+
);
|
|
5474
|
+
} else {
|
|
5475
|
+
console.log(
|
|
5476
|
+
`submitted ${outcome.commandId} · ${outcome.status} · budget ${budgetSeconds}s reached — resume: pgai result ${outcome.commandId}`
|
|
5477
|
+
);
|
|
5478
|
+
}
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
const result = outcome.result;
|
|
5483
|
+
if (outcome.status === "done" && result) {
|
|
5484
|
+
if (json) {
|
|
5485
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5486
|
+
} else {
|
|
5487
|
+
console.log(`submitted ${outcome.commandId} · done`);
|
|
5488
|
+
const body = formatJoeResult(command, result);
|
|
5489
|
+
if (body) console.log(body);
|
|
5490
|
+
}
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5494
|
+
if (outcome.status === "error") {
|
|
5495
|
+
const msg = result?.error ?? "command failed";
|
|
5496
|
+
console.error(`command ${outcome.commandId} error: ${msg}`);
|
|
5497
|
+
process.exitCode = 1;
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
// Server-side timed_out (the dispatcher gave up). The genuine reply may still
|
|
5502
|
+
// land; suggest a resume, but signal failure with a non-zero exit.
|
|
5503
|
+
console.error(
|
|
5504
|
+
`command ${outcome.commandId} timed out on the server — resume: pgai result ${outcome.commandId}`
|
|
5505
|
+
);
|
|
5506
|
+
process.exitCode = 1;
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
async function runJoeCli(
|
|
5510
|
+
command: JoeCommand,
|
|
5511
|
+
sql: string | null,
|
|
5512
|
+
args: Record<string, unknown> | null,
|
|
5513
|
+
opts: JoeCliOpts
|
|
5514
|
+
): Promise<void> {
|
|
5515
|
+
try {
|
|
5516
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5517
|
+
const cfg = config.readConfig();
|
|
5518
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5519
|
+
if (!apiKey) {
|
|
5520
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5521
|
+
process.exitCode = 1;
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
5524
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
5525
|
+
if (!projectRef) {
|
|
5526
|
+
console.error("Project is required. Pass --project <id|alias>.");
|
|
5527
|
+
process.exitCode = 1;
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5531
|
+
const budgetMs =
|
|
5532
|
+
typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
|
|
5533
|
+
|
|
5534
|
+
const outcome = await executeJoeCommand({
|
|
5535
|
+
apiKey,
|
|
5536
|
+
apiBaseUrl,
|
|
5537
|
+
command,
|
|
5538
|
+
project: projectRef,
|
|
5539
|
+
sql,
|
|
5540
|
+
args,
|
|
5541
|
+
session: opts.session ?? null,
|
|
5542
|
+
newSession: !!opts.newSession,
|
|
5543
|
+
orgId: cfg.orgId ?? undefined,
|
|
5544
|
+
budgetMs,
|
|
5545
|
+
debug: !!opts.debug,
|
|
5546
|
+
});
|
|
5547
|
+
|
|
5548
|
+
printJoeOutcome(command, outcome, !!opts.json);
|
|
5549
|
+
} catch (err) {
|
|
5550
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5551
|
+
console.error(message);
|
|
5552
|
+
process.exitCode = 1;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
/** Attach the shared Joe options (--project / --session / --new-session / --budget). */
|
|
5557
|
+
function withJoeOptions(cmd: import("commander").Command): import("commander").Command {
|
|
5558
|
+
return cmd
|
|
5559
|
+
.option("--project <id|alias>", "target project by numeric id OR alias/name")
|
|
5560
|
+
.option("--session <id>", "run on a specific session's clone")
|
|
5561
|
+
.option("--new-session", "start a fresh session/clone (null session)")
|
|
5562
|
+
.option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseInt(v, 10))
|
|
5563
|
+
.option("--debug", "enable debug output")
|
|
5564
|
+
.option("--json", "output raw JSON");
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
withJoeOptions(
|
|
5568
|
+
program
|
|
5569
|
+
.command("plan <sql>")
|
|
5570
|
+
.description("plan a query (EXPLAIN, plan-only — no execution; the fast/safe default)")
|
|
5571
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5572
|
+
await runJoeCli("plan", sql, null, opts);
|
|
5573
|
+
});
|
|
5574
|
+
|
|
5575
|
+
withJoeOptions(
|
|
5576
|
+
program
|
|
5577
|
+
.command("explain <sql>")
|
|
5578
|
+
.description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")
|
|
5579
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5580
|
+
await runJoeCli("explain", sql, null, opts);
|
|
5581
|
+
});
|
|
5582
|
+
|
|
5583
|
+
withJoeOptions(
|
|
5584
|
+
program
|
|
5585
|
+
.command("exec <sql>")
|
|
5586
|
+
.description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")
|
|
5587
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5588
|
+
await runJoeCli("exec", sql, null, opts);
|
|
5589
|
+
});
|
|
5590
|
+
|
|
5591
|
+
withJoeOptions(
|
|
5592
|
+
program
|
|
5593
|
+
.command("hypo <indexSql>")
|
|
5594
|
+
.description("HypoPG hypothetical index + re-plan (no real index, no data change)")
|
|
5595
|
+
.requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")
|
|
5596
|
+
).action(async (indexSql: string, opts: JoeCliOpts) => {
|
|
5597
|
+
await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
|
|
5598
|
+
});
|
|
5599
|
+
|
|
5600
|
+
withJoeOptions(
|
|
5601
|
+
program
|
|
5602
|
+
.command("activity")
|
|
5603
|
+
.description("running-activity snapshot (pg_stat_activity; query text redacted)")
|
|
5604
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5605
|
+
await runJoeCli("activity", null, null, opts);
|
|
5606
|
+
});
|
|
5607
|
+
|
|
5608
|
+
withJoeOptions(
|
|
5609
|
+
program
|
|
5610
|
+
.command("terminate <pid>")
|
|
5611
|
+
.description("pg_terminate_backend(pid) on the clone")
|
|
5612
|
+
).action(async (pid: string, opts: JoeCliOpts) => {
|
|
5613
|
+
const pidNum = parseInt(pid, 10);
|
|
5614
|
+
if (Number.isNaN(pidNum)) {
|
|
5615
|
+
console.error("pid must be a number");
|
|
5616
|
+
process.exitCode = 1;
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5619
|
+
await runJoeCli("terminate", null, { pid: pidNum }, opts);
|
|
5620
|
+
});
|
|
5621
|
+
|
|
5622
|
+
withJoeOptions(
|
|
5623
|
+
program
|
|
5624
|
+
.command("reset")
|
|
5625
|
+
.description("reset/recreate the session's thin clone")
|
|
5626
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5627
|
+
await runJoeCli("reset", null, null, opts);
|
|
5628
|
+
});
|
|
5629
|
+
|
|
5630
|
+
withJoeOptions(
|
|
5631
|
+
program
|
|
5632
|
+
.command("describe <object>")
|
|
5633
|
+
.description("\\d-family schema/relation/index metadata")
|
|
5634
|
+
.option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")
|
|
5635
|
+
).action(async (object: string, opts: JoeCliOpts) => {
|
|
5636
|
+
const args: Record<string, unknown> = { object };
|
|
5637
|
+
if (opts.variant) args.variant = opts.variant;
|
|
5638
|
+
await runJoeCli("describe", null, args, opts);
|
|
5639
|
+
});
|
|
5640
|
+
|
|
5641
|
+
// Org-level discovery — a general postgresai command, NOT a Joe endpoint (SPEC §6).
|
|
5642
|
+
program
|
|
5643
|
+
.command("projects")
|
|
5644
|
+
.description("list the org's projects (shows which have Joe ready) — org-level, not a Joe endpoint")
|
|
5645
|
+
.option("--debug", "enable debug output")
|
|
5646
|
+
.option("--json", "output raw JSON")
|
|
5647
|
+
.action(async (opts: { debug?: boolean; json?: boolean }) => {
|
|
5648
|
+
try {
|
|
5649
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5650
|
+
const cfg = config.readConfig();
|
|
5651
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5652
|
+
if (!apiKey) {
|
|
5653
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5654
|
+
process.exitCode = 1;
|
|
5655
|
+
return;
|
|
5656
|
+
}
|
|
5657
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5658
|
+
const projects = await listProjects({
|
|
5659
|
+
apiKey,
|
|
5660
|
+
apiBaseUrl,
|
|
5661
|
+
orgId: cfg.orgId ?? undefined,
|
|
5662
|
+
debug: !!opts.debug,
|
|
5663
|
+
});
|
|
5664
|
+
if (opts.json) {
|
|
5665
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
5666
|
+
} else {
|
|
5667
|
+
console.log(formatProjectsTable(projects));
|
|
5668
|
+
}
|
|
5669
|
+
} catch (err) {
|
|
5670
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5671
|
+
console.error(message);
|
|
5672
|
+
process.exitCode = 1;
|
|
5673
|
+
}
|
|
5674
|
+
});
|
|
5675
|
+
|
|
5676
|
+
/**
|
|
5677
|
+
* Resolve the shared inputs every DBLab verb needs: the api key, the api base
|
|
5678
|
+
* url, and the project's single DBLab `instance_id`. Throws (caught by each
|
|
5679
|
+
* action → exit 1) when the api key or `--project` is missing, or when no DBLab
|
|
5680
|
+
* instance can be resolved for the project.
|
|
5681
|
+
*/
|
|
5682
|
+
async function resolveDblabTarget(
|
|
5683
|
+
project: string | undefined,
|
|
5684
|
+
debug: boolean
|
|
5685
|
+
): Promise<{ apiKey: string; apiBaseUrl: string; orgId?: number; instanceId: string }> {
|
|
5686
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5687
|
+
const cfg = config.readConfig();
|
|
5688
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5689
|
+
if (!apiKey) {
|
|
5690
|
+
throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5691
|
+
}
|
|
5692
|
+
const ref = (project ?? "").trim();
|
|
5693
|
+
if (!ref) {
|
|
5694
|
+
throw new Error("--project <id|alias> is required");
|
|
5695
|
+
}
|
|
5696
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5697
|
+
const orgId = cfg.orgId ?? undefined;
|
|
5698
|
+
const instanceId = await resolveDblabInstanceId({ apiKey, apiBaseUrl, project: ref, orgId, debug });
|
|
5699
|
+
return { apiKey, apiBaseUrl, orgId, instanceId };
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
// ---- clone ----------------------------------------------------------------
|
|
5703
|
+
|
|
5704
|
+
const clone = program.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
|
|
5705
|
+
|
|
5706
|
+
clone
|
|
5707
|
+
.command("create")
|
|
5708
|
+
.description("create a thin clone of the project's database")
|
|
5709
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5710
|
+
.option("--branch <branch>", "branch to clone from")
|
|
5711
|
+
.option("--snapshot <id>", "snapshot id to clone from")
|
|
5712
|
+
.option("--id <id>", "clone id (DBLab generates one when omitted)")
|
|
5713
|
+
.option("--db-user <user>", "clone DB user")
|
|
5714
|
+
.option("--db-password <password>", "clone DB password")
|
|
5715
|
+
.option("--protected", "protect the clone from auto-deletion")
|
|
5716
|
+
.option("--debug", "enable debug output")
|
|
5717
|
+
.option("--json", "output raw JSON")
|
|
5718
|
+
.action(async (opts: DblabCmdOpts & { branch?: string; snapshot?: string; id?: string; dbUser?: string; dbPassword?: string; protected?: boolean }) => {
|
|
5719
|
+
try {
|
|
5720
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5721
|
+
const result = await createClone({
|
|
5722
|
+
apiKey, apiBaseUrl, instanceId,
|
|
5723
|
+
cloneId: opts.id,
|
|
5724
|
+
branch: opts.branch,
|
|
5725
|
+
snapshotId: opts.snapshot,
|
|
5726
|
+
dbUser: opts.dbUser,
|
|
5727
|
+
dbPassword: opts.dbPassword,
|
|
5728
|
+
isProtected: !!opts.protected,
|
|
5729
|
+
debug: !!opts.debug,
|
|
5730
|
+
});
|
|
5731
|
+
printResult(result, opts.json);
|
|
5732
|
+
} catch (err) {
|
|
5733
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5734
|
+
process.exitCode = 1;
|
|
5735
|
+
}
|
|
5736
|
+
});
|
|
5737
|
+
|
|
5738
|
+
// Resume / inspect a previously submitted command by id.
|
|
5739
|
+
program
|
|
5740
|
+
.command("status <commandId>")
|
|
5741
|
+
.description("show a Joe command's status (metadata only)")
|
|
5742
|
+
.option("--debug", "enable debug output")
|
|
5743
|
+
.option("--json", "output raw JSON")
|
|
5744
|
+
.action(async (commandId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
5745
|
+
try {
|
|
5746
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5747
|
+
const cfg = config.readConfig();
|
|
5748
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5749
|
+
if (!apiKey) {
|
|
5750
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5751
|
+
process.exitCode = 1;
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5754
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5755
|
+
const status = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
5756
|
+
if (opts.json) {
|
|
5757
|
+
console.log(JSON.stringify(status, null, 2));
|
|
5758
|
+
} else {
|
|
5759
|
+
console.log(`command ${status.command_id} · ${status.status}${status.error ? ` · ${status.error}` : ""}`);
|
|
5760
|
+
}
|
|
5761
|
+
} catch (err) {
|
|
5762
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5763
|
+
console.error(message);
|
|
5764
|
+
process.exitCode = 1;
|
|
5765
|
+
}
|
|
5766
|
+
});
|
|
5767
|
+
|
|
5768
|
+
clone
|
|
5769
|
+
.command("list")
|
|
5770
|
+
.description("list the project's thin clones")
|
|
5771
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5772
|
+
.option("--debug", "enable debug output")
|
|
5773
|
+
.option("--json", "output raw JSON")
|
|
5774
|
+
.action(async (opts: DblabCmdOpts) => {
|
|
5775
|
+
try {
|
|
5776
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5777
|
+
const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
5778
|
+
printResult(result, opts.json);
|
|
5779
|
+
} catch (err) {
|
|
5780
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5781
|
+
process.exitCode = 1;
|
|
5782
|
+
}
|
|
5783
|
+
});
|
|
5784
|
+
|
|
5785
|
+
program
|
|
5786
|
+
.command("result <commandId>")
|
|
5787
|
+
.description("fetch a Joe command's result by id (resume a timed-out one-shot)")
|
|
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 result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
5802
|
+
if (opts.json) {
|
|
5803
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5804
|
+
return;
|
|
5805
|
+
}
|
|
5806
|
+
const inferred = inferCommandFromResult(result);
|
|
5807
|
+
if (result.status === "done" && inferred) {
|
|
5808
|
+
console.log(`command ${result.command_id} · done`);
|
|
5809
|
+
const body = formatJoeResult(inferred, result);
|
|
5810
|
+
if (body) console.log(body);
|
|
5811
|
+
} else if (result.status === "error") {
|
|
5812
|
+
console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
|
|
5813
|
+
process.exitCode = 1;
|
|
5814
|
+
} else {
|
|
5815
|
+
console.log(`command ${result.command_id} · ${result.status}`);
|
|
5816
|
+
}
|
|
5817
|
+
} catch (err) {
|
|
5818
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5819
|
+
console.error(message);
|
|
5820
|
+
process.exitCode = 1;
|
|
5821
|
+
}
|
|
5822
|
+
});
|
|
5823
|
+
|
|
5824
|
+
clone
|
|
5825
|
+
.command("status <cloneId>")
|
|
5826
|
+
.description("show a clone's status")
|
|
5827
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5828
|
+
.option("--debug", "enable debug output")
|
|
5829
|
+
.option("--json", "output raw JSON")
|
|
5830
|
+
.action(async (cloneId: string, opts: DblabCmdOpts) => {
|
|
5831
|
+
try {
|
|
5832
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5833
|
+
const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
5834
|
+
printResult(result, opts.json);
|
|
5835
|
+
} catch (err) {
|
|
5836
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5837
|
+
process.exitCode = 1;
|
|
5838
|
+
}
|
|
5839
|
+
});
|
|
5840
|
+
|
|
5841
|
+
clone
|
|
5842
|
+
.command("reset <cloneId>")
|
|
5843
|
+
.description("reset a clone to a pristine snapshot (requires joe:admin)")
|
|
5844
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5845
|
+
.option("--snapshot <id>", "snapshot id to reset to (default: latest)")
|
|
5846
|
+
.option("--latest", "reset to the latest snapshot")
|
|
5847
|
+
.option("--debug", "enable debug output")
|
|
5848
|
+
.option("--json", "output raw JSON")
|
|
5849
|
+
.action(async (cloneId: string, opts: DblabCmdOpts & { snapshot?: string; latest?: boolean }) => {
|
|
5850
|
+
try {
|
|
5851
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5852
|
+
const result = await resetClone({
|
|
5853
|
+
apiKey, apiBaseUrl, instanceId, cloneId,
|
|
5854
|
+
snapshotId: opts.snapshot,
|
|
5855
|
+
latest: opts.latest,
|
|
5856
|
+
debug: !!opts.debug,
|
|
5857
|
+
});
|
|
5858
|
+
printResult(result ?? { reset: true, cloneId }, opts.json);
|
|
5859
|
+
} catch (err) {
|
|
5860
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5861
|
+
process.exitCode = 1;
|
|
5862
|
+
}
|
|
5863
|
+
});
|
|
5864
|
+
|
|
5865
|
+
clone
|
|
5866
|
+
.command("destroy <cloneId>")
|
|
5867
|
+
.description("destroy a clone (requires joe:admin)")
|
|
5868
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5869
|
+
.option("--debug", "enable debug output")
|
|
5870
|
+
.option("--json", "output raw JSON")
|
|
5871
|
+
.action(async (cloneId: string, opts: DblabCmdOpts) => {
|
|
5872
|
+
try {
|
|
5873
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5874
|
+
const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
|
|
5875
|
+
printResult(result ?? { destroyed: true, cloneId }, opts.json);
|
|
5876
|
+
} catch (err) {
|
|
5877
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5878
|
+
process.exitCode = 1;
|
|
5879
|
+
}
|
|
5880
|
+
});
|
|
5881
|
+
|
|
5882
|
+
// ---- branch ---------------------------------------------------------------
|
|
5883
|
+
|
|
5884
|
+
const branch = program.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
|
|
5885
|
+
|
|
5886
|
+
branch
|
|
5887
|
+
.command("list")
|
|
5888
|
+
.description("list the project's branches")
|
|
5889
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5890
|
+
.option("--debug", "enable debug output")
|
|
5891
|
+
.option("--json", "output raw JSON")
|
|
5892
|
+
.action(async (opts: DblabCmdOpts) => {
|
|
5893
|
+
try {
|
|
5894
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5895
|
+
const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
|
|
5896
|
+
printResult(result, opts.json);
|
|
5897
|
+
} catch (err) {
|
|
5898
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5899
|
+
process.exitCode = 1;
|
|
5900
|
+
}
|
|
5901
|
+
});
|
|
5902
|
+
|
|
5903
|
+
branch
|
|
5904
|
+
.command("create <name>")
|
|
5905
|
+
.description("create a branch")
|
|
5906
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5907
|
+
.option("--snapshot <id>", "snapshot id to base the branch on")
|
|
5908
|
+
.option("--base-branch <branch>", "parent branch to fork from")
|
|
5909
|
+
.option("--debug", "enable debug output")
|
|
5910
|
+
.option("--json", "output raw JSON")
|
|
5911
|
+
.action(async (name: string, opts: DblabCmdOpts & { snapshot?: string; baseBranch?: string }) => {
|
|
5912
|
+
try {
|
|
5913
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5914
|
+
const result = await createBranch({
|
|
5915
|
+
apiKey, apiBaseUrl, instanceId,
|
|
5916
|
+
branchName: name,
|
|
5917
|
+
baseBranch: opts.baseBranch,
|
|
5918
|
+
snapshotId: opts.snapshot,
|
|
5919
|
+
debug: !!opts.debug,
|
|
5920
|
+
});
|
|
5921
|
+
printResult(result, opts.json);
|
|
5922
|
+
} catch (err) {
|
|
5923
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5924
|
+
process.exitCode = 1;
|
|
5925
|
+
}
|
|
5926
|
+
});
|
|
5927
|
+
|
|
5928
|
+
branch
|
|
5929
|
+
.command("delete <name>")
|
|
5930
|
+
.description("delete a branch (requires joe:admin)")
|
|
5931
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5932
|
+
.option("--debug", "enable debug output")
|
|
5933
|
+
.option("--json", "output raw JSON")
|
|
5934
|
+
.action(async (name: string, opts: DblabCmdOpts) => {
|
|
5935
|
+
try {
|
|
5936
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5937
|
+
const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
5938
|
+
printResult(result ?? { deleted: true, branch: name }, opts.json);
|
|
5939
|
+
} catch (err) {
|
|
5940
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5941
|
+
process.exitCode = 1;
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
|
|
5945
|
+
branch
|
|
5946
|
+
.command("log <name>")
|
|
5947
|
+
.description("show a branch's snapshot log")
|
|
5948
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5949
|
+
.option("--debug", "enable debug output")
|
|
5950
|
+
.option("--json", "output raw JSON")
|
|
5951
|
+
.action(async (name: string, opts: DblabCmdOpts) => {
|
|
5952
|
+
try {
|
|
5953
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5954
|
+
const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
|
|
5955
|
+
printResult(result, opts.json);
|
|
5956
|
+
} catch (err) {
|
|
5957
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5958
|
+
process.exitCode = 1;
|
|
5959
|
+
}
|
|
5960
|
+
});
|
|
5961
|
+
|
|
5962
|
+
// ---- snapshot -------------------------------------------------------------
|
|
5963
|
+
|
|
5964
|
+
const snapshot = program.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
|
|
5965
|
+
|
|
5966
|
+
snapshot
|
|
5967
|
+
.command("list")
|
|
5968
|
+
.description("list the project's snapshots")
|
|
5969
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5970
|
+
.option("--branch <branch>", "filter by branch")
|
|
5971
|
+
.option("--dataset <dataset>", "filter by dataset")
|
|
5972
|
+
.option("--debug", "enable debug output")
|
|
5973
|
+
.option("--json", "output raw JSON")
|
|
5974
|
+
.action(async (opts: DblabCmdOpts & { branch?: string; dataset?: string }) => {
|
|
5975
|
+
try {
|
|
5976
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
5977
|
+
const result = await listSnapshots({
|
|
5978
|
+
apiKey, apiBaseUrl, instanceId,
|
|
5979
|
+
branchName: opts.branch,
|
|
5980
|
+
dataset: opts.dataset,
|
|
5981
|
+
debug: !!opts.debug,
|
|
5982
|
+
});
|
|
5983
|
+
printResult(result, opts.json);
|
|
5984
|
+
} catch (err) {
|
|
5985
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5986
|
+
process.exitCode = 1;
|
|
5987
|
+
}
|
|
5988
|
+
});
|
|
5989
|
+
|
|
5990
|
+
snapshot
|
|
5991
|
+
.command("create")
|
|
5992
|
+
.description("create a snapshot from a clone")
|
|
5993
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
5994
|
+
.requiredOption("--clone <id>", "clone id to snapshot")
|
|
5995
|
+
.option("--message <message>", "snapshot message")
|
|
5996
|
+
.option("--debug", "enable debug output")
|
|
5997
|
+
.option("--json", "output raw JSON")
|
|
5998
|
+
.action(async (opts: DblabCmdOpts & { clone?: string; message?: string }) => {
|
|
5999
|
+
try {
|
|
6000
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6001
|
+
const result = await createSnapshot({
|
|
6002
|
+
apiKey, apiBaseUrl, instanceId,
|
|
6003
|
+
cloneId: opts.clone as string,
|
|
6004
|
+
message: opts.message,
|
|
6005
|
+
debug: !!opts.debug,
|
|
6006
|
+
});
|
|
6007
|
+
printResult(result, opts.json);
|
|
6008
|
+
} catch (err) {
|
|
6009
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6010
|
+
process.exitCode = 1;
|
|
6011
|
+
}
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
snapshot
|
|
6015
|
+
.command("destroy <snapshotId>")
|
|
6016
|
+
.description("destroy a snapshot (requires joe:admin)")
|
|
6017
|
+
.requiredOption("--project <id|alias>", "project id or alias")
|
|
6018
|
+
.option("--force", "force-delete even when dependent clones exist")
|
|
6019
|
+
.option("--debug", "enable debug output")
|
|
6020
|
+
.option("--json", "output raw JSON")
|
|
6021
|
+
.action(async (snapshotId: string, opts: DblabCmdOpts & { force?: boolean }) => {
|
|
6022
|
+
try {
|
|
6023
|
+
const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
|
|
6024
|
+
const result = await destroySnapshot({
|
|
6025
|
+
apiKey, apiBaseUrl, instanceId, snapshotId,
|
|
6026
|
+
force: !!opts.force,
|
|
6027
|
+
debug: !!opts.debug,
|
|
6028
|
+
});
|
|
6029
|
+
printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
|
|
6030
|
+
} catch (err) {
|
|
6031
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
6032
|
+
process.exitCode = 1;
|
|
6033
|
+
}
|
|
6034
|
+
});
|
|
6035
|
+
|
|
5379
6036
|
// MCP server
|
|
5380
6037
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
5381
6038
|
|