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.
@@ -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