postgresai 0.16.0-dev.1 → 0.16.0-dev.11
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/CHANGELOG.md +11 -0
- package/bin/postgres-ai.ts +408 -25
- package/dist/bin/postgres-ai.js +940 -90
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/checkup-summary.ts +25 -0
- package/lib/checkup.ts +88 -2
- package/lib/init.ts +29 -8
- package/lib/joe.ts +703 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +241 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/auth.test.ts +30 -1
- package/test/checkup.integration.test.ts +31 -21
- package/test/checkup.test.ts +48 -3
- package/test/init.integration.test.ts +9 -79
- package/test/init.test.ts +35 -0
- package/test/joe.cli.test.ts +628 -0
- package/test/joe.test.ts +855 -0
- package/test/monitoring.test.ts +54 -3
- package/test/schema-validation.test.ts +40 -0
- package/test/test-utils.ts +5 -0
- package/test/util.test.ts +227 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `checkup --markdown` previously performed server-side conversion and sent the
|
|
8
|
+
full report JSON to the PostgresAI API even when `--no-upload` was set. The
|
|
9
|
+
flags are now mutually exclusive, and `--no-upload` prevents report data from
|
|
10
|
+
being sent to the PostgresAI API. Use `--json` or `--output` for local-only
|
|
11
|
+
output.
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -13,6 +13,16 @@ 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
|
+
getCommandOutput,
|
|
20
|
+
formatJoeOutput,
|
|
21
|
+
formatProjectsTable,
|
|
22
|
+
DEFAULT_BUDGET_MS,
|
|
23
|
+
type JoeCommand,
|
|
24
|
+
type ExecuteJoeOutcome,
|
|
25
|
+
} from "../lib/joe";
|
|
16
26
|
import { resolveBaseUrls } from "../lib/util";
|
|
17
27
|
import { registerAasCollection, parseVcpus } from "../lib/aas-onboard";
|
|
18
28
|
import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "../lib/storage";
|
|
@@ -335,12 +345,18 @@ function prepareUploadConfig(
|
|
|
335
345
|
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
336
346
|
return null; // Signal to exit
|
|
337
347
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
348
|
+
if (opts.markdown) {
|
|
349
|
+
console.error("Notice: no API key configured — regular report upload is disabled.");
|
|
350
|
+
console.error(" The full report JSON will still be sent to the PostgresAI API for markdown conversion.");
|
|
351
|
+
console.error(" To avoid sending report data, replace --markdown with --no-upload and --json or --output.");
|
|
352
|
+
} else {
|
|
353
|
+
// No credentials and upload not explicitly requested: fall back to
|
|
354
|
+
// local-only mode, but say so prominently — skipping the upload silently
|
|
355
|
+
// hides the fact that results never reach the Console.
|
|
356
|
+
console.error("Notice: no API key configured — results will NOT be uploaded to PostgresAI.");
|
|
357
|
+
console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
|
|
358
|
+
console.error(" To run locally without this notice, pass --no-upload.");
|
|
359
|
+
}
|
|
344
360
|
return undefined; // Skip upload, run checks locally
|
|
345
361
|
}
|
|
346
362
|
|
|
@@ -1996,7 +2012,7 @@ program
|
|
|
1996
2012
|
"project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
|
|
1997
2013
|
)
|
|
1998
2014
|
.option("--json", "output JSON to stdout")
|
|
1999
|
-
.option("--markdown", "output markdown
|
|
2015
|
+
.option("--markdown", "output markdown via PostgresAI API (transmits the full report JSON)")
|
|
2000
2016
|
.addHelpText(
|
|
2001
2017
|
"after",
|
|
2002
2018
|
[
|
|
@@ -2010,7 +2026,7 @@ program
|
|
|
2010
2026
|
" postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
|
|
2011
2027
|
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
2012
2028
|
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
|
|
2013
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --
|
|
2029
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --markdown",
|
|
2014
2030
|
].join("\n")
|
|
2015
2031
|
)
|
|
2016
2032
|
.action(async (checkIdOrConn: string | undefined, connArg: string | undefined, opts: CheckupOptions, cmd: Command) => {
|
|
@@ -2060,9 +2076,14 @@ program
|
|
|
2060
2076
|
process.exitCode = 1;
|
|
2061
2077
|
return;
|
|
2062
2078
|
}
|
|
2063
|
-
// Note: --json, --markdown and --upload/--no-upload are independent flags.
|
|
2064
|
-
// Use --no-upload to explicitly disable upload when using --json or --markdown.
|
|
2065
2079
|
const uploadExplicitlyDisabled = opts.upload === false;
|
|
2080
|
+
if (uploadExplicitlyDisabled && shouldConvertMarkdown) {
|
|
2081
|
+
console.error("Error: --no-upload and --markdown are mutually exclusive");
|
|
2082
|
+
console.error("Markdown conversion is performed by the PostgresAI API and transmits the full report JSON.");
|
|
2083
|
+
console.error("Drop --no-upload to allow transmission, or use --json or --output for local-only output.");
|
|
2084
|
+
process.exitCode = 1;
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2066
2087
|
let shouldUpload = !uploadExplicitlyDisabled;
|
|
2067
2088
|
|
|
2068
2089
|
// Preflight: validate/create output directory BEFORE connecting / running checks.
|
|
@@ -2308,7 +2329,9 @@ program
|
|
|
2308
2329
|
|
|
2309
2330
|
console.log('\nFor details:');
|
|
2310
2331
|
console.log(' --json Output JSON');
|
|
2311
|
-
|
|
2332
|
+
if (!uploadExplicitlyDisabled) {
|
|
2333
|
+
console.log(' --markdown Output markdown via PostgresAI API');
|
|
2334
|
+
}
|
|
2312
2335
|
console.log(' --output <dir> Save to directory');
|
|
2313
2336
|
}
|
|
2314
2337
|
} catch (error) {
|
|
@@ -2443,9 +2466,44 @@ interface MonitoringRegistration {
|
|
|
2443
2466
|
*
|
|
2444
2467
|
* Never throws — registration is best-effort; returns null on failure.
|
|
2445
2468
|
*/
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Classify how `mon local-install` should register the monitoring instance,
|
|
2472
|
+
* given the raw `--project` value and the resolved instance id.
|
|
2473
|
+
*
|
|
2474
|
+
* - With an instance id: ADOPT the provisioned instance. No project name is
|
|
2475
|
+
* required (the platform returns the real project); a provided name is
|
|
2476
|
+
* normalized and carried through for messaging/fallback.
|
|
2477
|
+
* - No instance id and no project name: ERROR. The hardcoded
|
|
2478
|
+
* "postgres-ai-monitoring" default was removed, and a nameless legacy
|
|
2479
|
+
* self-registration is rejected by v1.monitoring_instance_register (PT400).
|
|
2480
|
+
* - No instance id but a project name: legacy SELF-REGISTER.
|
|
2481
|
+
*
|
|
2482
|
+
* Pure (no I/O) so the decision is unit-testable independently of the large
|
|
2483
|
+
* install command. `projectName` is the trimmed value, or undefined when empty.
|
|
2484
|
+
*/
|
|
2485
|
+
type MonRegistrationPlan =
|
|
2486
|
+
| { kind: "adopt"; projectName: string | undefined }
|
|
2487
|
+
| { kind: "self-register"; projectName: string }
|
|
2488
|
+
| { kind: "error-missing-project"; projectName: undefined };
|
|
2489
|
+
|
|
2490
|
+
function planMonitoringRegistration(args: {
|
|
2491
|
+
project?: string;
|
|
2492
|
+
instanceId?: string;
|
|
2493
|
+
}): MonRegistrationPlan {
|
|
2494
|
+
const projectName = args.project?.trim() || undefined;
|
|
2495
|
+
if (args.instanceId) {
|
|
2496
|
+
return { kind: "adopt", projectName };
|
|
2497
|
+
}
|
|
2498
|
+
if (!projectName) {
|
|
2499
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
2500
|
+
}
|
|
2501
|
+
return { kind: "self-register", projectName };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2446
2504
|
async function registerMonitoringInstance(
|
|
2447
2505
|
apiKey: string,
|
|
2448
|
-
projectName: string,
|
|
2506
|
+
projectName: string | undefined,
|
|
2449
2507
|
opts?: { apiBaseUrl?: string; debug?: boolean; instanceId?: string; retries?: number; retryDelayMs?: number }
|
|
2450
2508
|
): Promise<MonitoringRegistration | null> {
|
|
2451
2509
|
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
@@ -2457,16 +2515,25 @@ async function registerMonitoringInstance(
|
|
|
2457
2515
|
// moment to recover; skipped before the first attempt. Tests pass 0.
|
|
2458
2516
|
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
2459
2517
|
|
|
2518
|
+
// Omit project_name entirely when empty: the adopt path (instance_id present)
|
|
2519
|
+
// relies on the platform returning the real project, and a nameless legacy
|
|
2520
|
+
// self-registration is rejected by v1.monitoring_instance_register (PT400).
|
|
2521
|
+
const hasProjectName = !!(projectName && projectName.trim());
|
|
2522
|
+
|
|
2460
2523
|
if (debug) {
|
|
2461
2524
|
console.error(`\nDebug: Registering monitoring instance...`);
|
|
2462
2525
|
console.error(`Debug: POST ${url}`);
|
|
2463
|
-
console.error(
|
|
2526
|
+
console.error(
|
|
2527
|
+
`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`
|
|
2528
|
+
);
|
|
2464
2529
|
}
|
|
2465
2530
|
|
|
2466
2531
|
const requestBody: Record<string, string> = {
|
|
2467
2532
|
api_token: apiKey,
|
|
2468
|
-
project_name: projectName,
|
|
2469
2533
|
};
|
|
2534
|
+
if (hasProjectName) {
|
|
2535
|
+
requestBody.project_name = projectName as string;
|
|
2536
|
+
}
|
|
2470
2537
|
if (instanceId) {
|
|
2471
2538
|
requestBody.instance_id = instanceId;
|
|
2472
2539
|
}
|
|
@@ -3137,8 +3204,11 @@ mon
|
|
|
3137
3204
|
// and persisted; the legacy self-registration stays fire-and-forget
|
|
3138
3205
|
// (issue platform-all#311).
|
|
3139
3206
|
if (apiKey && !opts.demo) {
|
|
3140
|
-
const projectName = opts.project || "postgres-ai-monitoring";
|
|
3141
3207
|
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
3208
|
+
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
3209
|
+
const projectName = plan.projectName;
|
|
3210
|
+
// `instanceId` truthy ⟺ plan.kind === "adopt"; branch on it directly so
|
|
3211
|
+
// TypeScript narrows instanceId to a defined string in the adopt path.
|
|
3142
3212
|
if (instanceId) {
|
|
3143
3213
|
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
3144
3214
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -3160,11 +3230,17 @@ mon
|
|
|
3160
3230
|
// Request succeeded but carried no usable project field — don't claim
|
|
3161
3231
|
// adoption, but don't report a hard failure either (no re-run needed).
|
|
3162
3232
|
console.error(
|
|
3163
|
-
`⚠ Adopted provisioned instance ${instanceId} but the platform returned no project
|
|
3233
|
+
`⚠ Adopted provisioned instance ${instanceId} but the platform returned no project` +
|
|
3234
|
+
(projectName
|
|
3235
|
+
? ` — reports will use project '${projectName}'`
|
|
3236
|
+
: ` — reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`)
|
|
3164
3237
|
);
|
|
3165
3238
|
} else {
|
|
3166
3239
|
console.error(
|
|
3167
|
-
`⚠ Could not adopt provisioned instance ${instanceId}
|
|
3240
|
+
`⚠ Could not adopt provisioned instance ${instanceId}` +
|
|
3241
|
+
(projectName
|
|
3242
|
+
? ` — reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`
|
|
3243
|
+
: ` — reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`)
|
|
3168
3244
|
);
|
|
3169
3245
|
}
|
|
3170
3246
|
|
|
@@ -3188,6 +3264,17 @@ mon
|
|
|
3188
3264
|
`⚠ AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'\n`
|
|
3189
3265
|
);
|
|
3190
3266
|
}
|
|
3267
|
+
} else if (plan.kind === "error-missing-project") {
|
|
3268
|
+
// Legacy self-registration (no --instance-id) now requires a project
|
|
3269
|
+
// name: the hardcoded "postgres-ai-monitoring" default was removed, and
|
|
3270
|
+
// v1.monitoring_instance_register raises PT400 for a nameless legacy
|
|
3271
|
+
// registration. Console-provisioned installs should adopt with
|
|
3272
|
+
// --instance-id instead.
|
|
3273
|
+
console.error(
|
|
3274
|
+
"✗ A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " +
|
|
3275
|
+
"Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>."
|
|
3276
|
+
);
|
|
3277
|
+
process.exitCode = 1;
|
|
3191
3278
|
} else {
|
|
3192
3279
|
void registerMonitoringInstance(apiKey, projectName, {
|
|
3193
3280
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
@@ -3867,13 +3954,9 @@ targets
|
|
|
3867
3954
|
// Authentication and API key management
|
|
3868
3955
|
const auth = program.command("auth").description("authentication and API key management");
|
|
3869
3956
|
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
.option("--set-key <key>", "store API key directly without OAuth flow")
|
|
3874
|
-
.option("--port <port>", "local callback server port (default: random)", parseInt)
|
|
3875
|
-
.option("--debug", "enable debug output")
|
|
3876
|
-
.action(async (opts: { setKey?: string; port?: number; debug?: boolean }) => {
|
|
3957
|
+
type AuthLoginOptions = { setKey?: string; port?: number; debug?: boolean };
|
|
3958
|
+
|
|
3959
|
+
async function runAuthLogin(opts: AuthLoginOptions) {
|
|
3877
3960
|
// If --set-key is provided, store it directly without OAuth
|
|
3878
3961
|
if (opts.setKey) {
|
|
3879
3962
|
const trimmedKey = opts.setKey.trim();
|
|
@@ -4127,7 +4210,21 @@ auth
|
|
|
4127
4210
|
console.error(`Authentication error: ${message}`);
|
|
4128
4211
|
process.exit(1);
|
|
4129
4212
|
}
|
|
4130
|
-
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
function configureLoginCommand(command: Command): Command {
|
|
4216
|
+
return command
|
|
4217
|
+
.description("authenticate via browser (OAuth) or store API key directly")
|
|
4218
|
+
.option("--set-key <key>", "store API key directly without OAuth flow")
|
|
4219
|
+
.option("--port <port>", "local callback server port (default: random)", parseInt)
|
|
4220
|
+
.option("--debug", "enable debug output");
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
configureLoginCommand(auth.command("login", { isDefault: true }))
|
|
4224
|
+
.action(runAuthLogin);
|
|
4225
|
+
|
|
4226
|
+
configureLoginCommand(program.command("login"))
|
|
4227
|
+
.action(runAuthLogin);
|
|
4131
4228
|
|
|
4132
4229
|
auth
|
|
4133
4230
|
.command("show-key")
|
|
@@ -5312,6 +5409,291 @@ function tryParseJson(s: string): unknown {
|
|
|
5312
5409
|
try { return JSON.parse(s); } catch { return s; }
|
|
5313
5410
|
}
|
|
5314
5411
|
|
|
5412
|
+
// ---------------------------------------------------------------------------
|
|
5413
|
+
// Joe command surface (Joe API v2 CLI v1, issue #438). Every verb builds the
|
|
5414
|
+
// RAW command text Joe's /webui/command dispatches (`plan <sql>`, `\d users`),
|
|
5415
|
+
// runs it via the synchronous `v1.joe_command_run`, and polls
|
|
5416
|
+
// `v1.joe_command_output` until `ok`/`error` within the one-shot budget.
|
|
5417
|
+
// `--project` accepts a numeric id OR an alias/name/label. The backend rpcs
|
|
5418
|
+
// are mocked in tests.
|
|
5419
|
+
// ---------------------------------------------------------------------------
|
|
5420
|
+
|
|
5421
|
+
interface JoeCliOpts {
|
|
5422
|
+
project?: string;
|
|
5423
|
+
instanceId?: string;
|
|
5424
|
+
budget?: number;
|
|
5425
|
+
variant?: string;
|
|
5426
|
+
debug?: boolean;
|
|
5427
|
+
json?: boolean;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
function printJoeOutcome(outcome: ExecuteJoeOutcome, json: boolean, budgetMs?: number): void {
|
|
5431
|
+
// The expiry hint reports the ACTUAL effective budget (--budget when given,
|
|
5432
|
+
// the default otherwise) — not a hardcoded DEFAULT_BUDGET_MS.
|
|
5433
|
+
const effectiveBudgetMs =
|
|
5434
|
+
typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
5435
|
+
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
5436
|
+
// One-shot budget reached before a terminal state — hand back a resume handle.
|
|
5437
|
+
// This is expected (a cold clone), NOT a failure: exit 0.
|
|
5438
|
+
if (outcome.budgetExpired) {
|
|
5439
|
+
if (json) {
|
|
5440
|
+
console.log(
|
|
5441
|
+
JSON.stringify(
|
|
5442
|
+
{
|
|
5443
|
+
command_id: outcome.commandId,
|
|
5444
|
+
status: outcome.status,
|
|
5445
|
+
budget_expired: true,
|
|
5446
|
+
resume: `pgai joe result ${outcome.commandId}`,
|
|
5447
|
+
},
|
|
5448
|
+
null,
|
|
5449
|
+
2
|
|
5450
|
+
)
|
|
5451
|
+
);
|
|
5452
|
+
} else {
|
|
5453
|
+
console.log(
|
|
5454
|
+
`started ${outcome.commandId} · ${outcome.status} · budget ${budgetSeconds}s reached — resume: pgai joe result ${outcome.commandId}`
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
return;
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
const output = outcome.output;
|
|
5461
|
+
if (outcome.status === "ok") {
|
|
5462
|
+
if (!output) {
|
|
5463
|
+
console.error(`command ${outcome.commandId} ok but output is empty`);
|
|
5464
|
+
process.exitCode = 1;
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5467
|
+
if (json) console.log(JSON.stringify(output, null, 2));
|
|
5468
|
+
else {
|
|
5469
|
+
console.log(`command ${outcome.commandId} · ok`);
|
|
5470
|
+
const body = formatJoeOutput(output);
|
|
5471
|
+
if (body) console.log(body);
|
|
5472
|
+
}
|
|
5473
|
+
return;
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// Terminal error.
|
|
5477
|
+
if (json && output) console.log(JSON.stringify(output, null, 2));
|
|
5478
|
+
console.error(`command ${outcome.commandId} error: ${output?.error ?? "command failed"}`);
|
|
5479
|
+
process.exitCode = 1;
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
async function runJoeCli(
|
|
5483
|
+
command: JoeCommand,
|
|
5484
|
+
arg: string | null,
|
|
5485
|
+
opts: JoeCliOpts
|
|
5486
|
+
): Promise<void> {
|
|
5487
|
+
try {
|
|
5488
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5489
|
+
const cfg = config.readConfig();
|
|
5490
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5491
|
+
if (!apiKey) {
|
|
5492
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5493
|
+
process.exitCode = 1;
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
|
|
5497
|
+
const instanceRef = (opts.instanceId ?? "").toString().trim();
|
|
5498
|
+
if (!projectRef && !instanceRef) {
|
|
5499
|
+
console.error(
|
|
5500
|
+
"Specify --instance-id <id> (or --project <id|alias> once projects_list is available)."
|
|
5501
|
+
);
|
|
5502
|
+
process.exitCode = 1;
|
|
5503
|
+
return;
|
|
5504
|
+
}
|
|
5505
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5506
|
+
if (typeof opts.budget === "number" && (!Number.isFinite(opts.budget) || opts.budget < 0)) {
|
|
5507
|
+
throw new Error("--budget must be a non-negative number of seconds");
|
|
5508
|
+
}
|
|
5509
|
+
const budgetMs = typeof opts.budget === "number" ? opts.budget * 1000 : undefined;
|
|
5510
|
+
|
|
5511
|
+
const outcome = await executeJoeCommand({
|
|
5512
|
+
apiKey,
|
|
5513
|
+
apiBaseUrl,
|
|
5514
|
+
command,
|
|
5515
|
+
project: projectRef || undefined,
|
|
5516
|
+
instanceId: instanceRef || undefined,
|
|
5517
|
+
input: { arg, variant: opts.variant ?? null },
|
|
5518
|
+
orgId: cfg.orgId ?? undefined,
|
|
5519
|
+
budgetMs,
|
|
5520
|
+
debug: !!opts.debug,
|
|
5521
|
+
});
|
|
5522
|
+
|
|
5523
|
+
printJoeOutcome(outcome, !!opts.json, budgetMs);
|
|
5524
|
+
} catch (err) {
|
|
5525
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5526
|
+
console.error(message);
|
|
5527
|
+
process.exitCode = 1;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
/** Attach the shared Joe options (--instance-id / --project / --budget / --debug / --json). */
|
|
5532
|
+
function withJoeOptions(cmd: import("commander").Command): import("commander").Command {
|
|
5533
|
+
return cmd
|
|
5534
|
+
.option(
|
|
5535
|
+
"--instance-id <id>",
|
|
5536
|
+
"target the Joe instance id directly (skips --project resolution; the v1 path while projects_list is unavailable)"
|
|
5537
|
+
)
|
|
5538
|
+
.option("--project <id|alias>", "target project by numeric id OR alias/name (requires projects_list)")
|
|
5539
|
+
.option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseFloat(v))
|
|
5540
|
+
.option("--debug", "enable debug output")
|
|
5541
|
+
.option("--json", "output raw JSON");
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
const joe = program
|
|
5545
|
+
.command("joe")
|
|
5546
|
+
.description("Joe — plan/EXPLAIN/exec queries on ephemeral DBLab clones");
|
|
5547
|
+
|
|
5548
|
+
withJoeOptions(
|
|
5549
|
+
joe
|
|
5550
|
+
.command("plan <sql>")
|
|
5551
|
+
.description("plan a query (EXPLAIN, plan-only — no execution; the fast/safe default)")
|
|
5552
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5553
|
+
await runJoeCli("plan", sql, opts);
|
|
5554
|
+
});
|
|
5555
|
+
|
|
5556
|
+
withJoeOptions(
|
|
5557
|
+
joe
|
|
5558
|
+
.command("explain <sql>")
|
|
5559
|
+
.description("EXPLAIN + EXPLAIN ANALYZE a query (EXECUTES on the ephemeral clone)")
|
|
5560
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5561
|
+
await runJoeCli("explain", sql, opts);
|
|
5562
|
+
});
|
|
5563
|
+
|
|
5564
|
+
withJoeOptions(
|
|
5565
|
+
joe
|
|
5566
|
+
.command("exec <sql>")
|
|
5567
|
+
.description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")
|
|
5568
|
+
).action(async (sql: string, opts: JoeCliOpts) => {
|
|
5569
|
+
await runJoeCli("exec", sql, opts);
|
|
5570
|
+
});
|
|
5571
|
+
|
|
5572
|
+
withJoeOptions(
|
|
5573
|
+
joe
|
|
5574
|
+
.command("hypo <args>")
|
|
5575
|
+
.description("HypoPG hypothetical indexes (e.g. `hypo create index on users (email)`, `hypo desc`, `hypo reset`)")
|
|
5576
|
+
).action(async (args: string, opts: JoeCliOpts) => {
|
|
5577
|
+
await runJoeCli("hypo", args, opts);
|
|
5578
|
+
});
|
|
5579
|
+
|
|
5580
|
+
withJoeOptions(
|
|
5581
|
+
joe
|
|
5582
|
+
.command("activity")
|
|
5583
|
+
.description("running-activity snapshot (pg_stat_activity) on the clone")
|
|
5584
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5585
|
+
await runJoeCli("activity", null, opts);
|
|
5586
|
+
});
|
|
5587
|
+
|
|
5588
|
+
withJoeOptions(
|
|
5589
|
+
joe
|
|
5590
|
+
.command("terminate <pid>")
|
|
5591
|
+
.description("pg_terminate_backend(pid) on the clone")
|
|
5592
|
+
).action(async (pid: string, opts: JoeCliOpts) => {
|
|
5593
|
+
// The bare-positive-integer pid guard lives in buildJoeCommandText, which
|
|
5594
|
+
// runs before any network call — a mistyped pid never reaches an rpc.
|
|
5595
|
+
await runJoeCli("terminate", pid, opts);
|
|
5596
|
+
});
|
|
5597
|
+
|
|
5598
|
+
withJoeOptions(
|
|
5599
|
+
joe
|
|
5600
|
+
.command("reset")
|
|
5601
|
+
.description("reset/recreate the session's thin clone")
|
|
5602
|
+
).action(async (opts: JoeCliOpts) => {
|
|
5603
|
+
await runJoeCli("reset", null, opts);
|
|
5604
|
+
});
|
|
5605
|
+
|
|
5606
|
+
withJoeOptions(
|
|
5607
|
+
joe
|
|
5608
|
+
.command("describe <object>")
|
|
5609
|
+
.description("\\d-family schema/relation/index metadata")
|
|
5610
|
+
.option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")
|
|
5611
|
+
).action(async (object: string, opts: JoeCliOpts) => {
|
|
5612
|
+
await runJoeCli("describe", object, opts);
|
|
5613
|
+
});
|
|
5614
|
+
|
|
5615
|
+
// Resume / inspect a previously started command by id (the budget-expiry
|
|
5616
|
+
// resume handle printed by the one-shot verbs).
|
|
5617
|
+
joe
|
|
5618
|
+
.command("result <commandId>")
|
|
5619
|
+
.description("fetch a Joe command's output by id (resume a budget-expired one-shot)")
|
|
5620
|
+
.option("--debug", "enable debug output")
|
|
5621
|
+
.option("--json", "output raw JSON")
|
|
5622
|
+
.action(async (commandId: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
5623
|
+
try {
|
|
5624
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5625
|
+
const cfg = config.readConfig();
|
|
5626
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5627
|
+
if (!apiKey) {
|
|
5628
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5629
|
+
process.exitCode = 1;
|
|
5630
|
+
return;
|
|
5631
|
+
}
|
|
5632
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5633
|
+
const output = await getCommandOutput({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
|
|
5634
|
+
if (opts.json) {
|
|
5635
|
+
console.log(JSON.stringify(output, null, 2));
|
|
5636
|
+
// Same exit contract as the human output and the one-shot path
|
|
5637
|
+
// (printJoeOutcome): only `ok` is a result — scripts consuming --json
|
|
5638
|
+
// must not proceed on `pending`/`error`.
|
|
5639
|
+
if (output.status !== "ok") {
|
|
5640
|
+
process.exitCode = 1;
|
|
5641
|
+
}
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
if (output.status === "ok") {
|
|
5645
|
+
console.log(`command ${output.command_id} · ok`);
|
|
5646
|
+
const body = formatJoeOutput(output);
|
|
5647
|
+
if (body) console.log(body);
|
|
5648
|
+
} else if (output.status === "error") {
|
|
5649
|
+
console.error(`command ${output.command_id} error: ${output.error ?? "command failed"}`);
|
|
5650
|
+
process.exitCode = 1;
|
|
5651
|
+
} else {
|
|
5652
|
+
console.error(`command ${output.command_id} · ${output.status} — result is not ready`);
|
|
5653
|
+
process.exitCode = 1;
|
|
5654
|
+
}
|
|
5655
|
+
} catch (err) {
|
|
5656
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5657
|
+
console.error(message);
|
|
5658
|
+
process.exitCode = 1;
|
|
5659
|
+
}
|
|
5660
|
+
});
|
|
5661
|
+
|
|
5662
|
+
// Org-level discovery — a general postgresai command, NOT a Joe endpoint.
|
|
5663
|
+
program
|
|
5664
|
+
.command("projects")
|
|
5665
|
+
.description("list the org's projects (shows which have Joe ready) — org-level, not a Joe endpoint")
|
|
5666
|
+
.option("--debug", "enable debug output")
|
|
5667
|
+
.option("--json", "output raw JSON")
|
|
5668
|
+
.action(async (opts: { debug?: boolean; json?: boolean }) => {
|
|
5669
|
+
try {
|
|
5670
|
+
const rootOpts = program.opts<CliOptions>();
|
|
5671
|
+
const cfg = config.readConfig();
|
|
5672
|
+
const { apiKey } = getConfig(rootOpts);
|
|
5673
|
+
if (!apiKey) {
|
|
5674
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
5675
|
+
process.exitCode = 1;
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
5679
|
+
const projects = await listProjects({
|
|
5680
|
+
apiKey,
|
|
5681
|
+
apiBaseUrl,
|
|
5682
|
+
orgId: cfg.orgId ?? undefined,
|
|
5683
|
+
debug: !!opts.debug,
|
|
5684
|
+
});
|
|
5685
|
+
if (opts.json) {
|
|
5686
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
5687
|
+
} else {
|
|
5688
|
+
console.log(formatProjectsTable(projects));
|
|
5689
|
+
}
|
|
5690
|
+
} catch (err) {
|
|
5691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5692
|
+
console.error(message);
|
|
5693
|
+
process.exitCode = 1;
|
|
5694
|
+
}
|
|
5695
|
+
});
|
|
5696
|
+
|
|
5315
5697
|
// MCP server
|
|
5316
5698
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
5317
5699
|
|
|
@@ -5488,3 +5870,4 @@ if (import.meta.main) {
|
|
|
5488
5870
|
// same functions used by the `mon` commands).
|
|
5489
5871
|
export { refreshBundledComposeIfStale, readDeployedTag, isValidComposeYaml };
|
|
5490
5872
|
export { registerMonitoringInstance, resolveAdoptedProject, type MonitoringRegistration };
|
|
5873
|
+
export { planMonitoringRegistration, type MonRegistrationPlan };
|