postgresai 0.16.0-dev.1 → 0.16.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +728 -6
- package/dist/bin/postgres-ai.js +2496 -255
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +457 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +730 -0
- package/lib/mcp-server.ts +597 -0
- package/lib/supabase.ts +0 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +339 -0
- package/test/dblab.test.ts +408 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +298 -0
- package/test/joe.test.ts +726 -0
- package/test/monitoring.test.ts +54 -3
package/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,637 @@ 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 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
|
+
|
|
5315
6036
|
// MCP server
|
|
5316
6037
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
5317
6038
|
|
|
@@ -5488,3 +6209,4 @@ if (import.meta.main) {
|
|
|
5488
6209
|
// same functions used by the `mon` commands).
|
|
5489
6210
|
export { refreshBundledComposeIfStale, readDeployedTag, isValidComposeYaml };
|
|
5490
6211
|
export { registerMonitoringInstance, resolveAdoptedProject, type MonitoringRegistration };
|
|
6212
|
+
export { planMonitoringRegistration, type MonRegistrationPlan };
|