From 77d8ecd5f375edcea045bb0ec05a8add86a10312 Mon Sep 17 00:00:00 2001 From: Dillon DuPont Date: Mon, 1 Dec 2025 11:28:31 -0500 Subject: [PATCH] Add "cua get" cmd to CLI --- .../docs/libraries/cua-cli/commands.mdx | 73 +++++- libs/typescript/cua-cli/README.md | 6 + libs/typescript/cua-cli/src/cli.ts | 3 +- .../cua-cli/src/commands/sandbox.ts | 211 +++++++++++++++++- 4 files changed, 290 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/libraries/cua-cli/commands.mdx b/docs/content/docs/libraries/cua-cli/commands.mdx index b425b9a4..12805d9d 100644 --- a/docs/content/docs/libraries/cua-cli/commands.mdx +++ b/docs/content/docs/libraries/cua-cli/commands.mdx @@ -35,7 +35,7 @@ Both styles work identically - use whichever you prefer! ### Available Commands - **Authentication** - `cua auth login`, `cua auth env`, `cua auth logout` (also available as flat commands: `cua login`, `cua env`, `cua logout`) -- **Sandbox Management** - `cua list`, `cua create`, `cua start`, `cua stop`, `cua restart`, `cua delete`, `cua vnc` +- **Sandbox Management** - `cua list`, `cua create`, `cua get`, `cua start`, `cua stop`, `cua restart`, `cua delete`, `cua vnc` ## Authentication Commands @@ -188,6 +188,77 @@ Job ID: job-xyz789 Use 'cua list' to monitor provisioning progress ``` +### `cua get` + +Get detailed information about a specific sandbox, including computer-server health status. + +```bash +cua get + +# With additional options +cua get --json +cua get --show-passwords +cua get --show-vnc-url +``` + +**Options:** + +- `--json` - Output all details in JSON format +- `--show-passwords` - Include password in output +- `--show-vnc-url` - Include computed NoVNC URL + +**Example Output (default):** + +```bash +$ cua get my-dev-sandbox +Name: my-dev-sandbox +Status: running +Host: my-dev-sandbox.containers.cloud.trycua.com +OS Type: linux +Computer Server Version: 0.1.30 +Computer Server Status: healthy +``` + +**Example Output (with --show-passwords and --show-vnc-url):** + +```bash +$ cua get my-dev-sandbox --show-passwords --show-vnc-url +Name: my-dev-sandbox +Status: running +Host: my-dev-sandbox.containers.cloud.trycua.com +Password: secure-pass-123 +OS Type: linux +Computer Server Version: 0.1.30 +Computer Server Status: healthy +VNC URL: https://my-dev-sandbox.containers.cloud.trycua.com/vnc.html?autoconnect=true&password=secure-pass-123 +``` + +**Example Output (JSON format):** + +```bash +$ cua get my-dev-sandbox --json +{ + "name": "my-dev-sandbox", + "status": "running", + "host": "my-dev-sandbox.containers.cloud.trycua.com", + "os_type": "linux", + "computer_server_version": "0.1.30", + "computer_server_status": "healthy" +} +``` + +**Computer Server Health Check:** + +The `cua get` command automatically probes the computer-server when the sandbox is running: +- Checks OS type via `https://{host}:8443/status` +- Checks version via `https://{host}:8443/cmd` +- Shows "Computer Server Status: healthy" when both probes succeed +- Uses a 3-second timeout for each probe + + + The computer server status is only checked for running sandboxes. Stopped or suspended sandboxes will not show computer server information. + + ### `cua start` Start a stopped sandbox. diff --git a/libs/typescript/cua-cli/README.md b/libs/typescript/cua-cli/README.md index 6edf5c5d..0c9538b4 100644 --- a/libs/typescript/cua-cli/README.md +++ b/libs/typescript/cua-cli/README.md @@ -58,14 +58,20 @@ bun run ./index.ts -- --help **Available Commands:** - `list` (aliases: `ls`, `ps`) – list all sandboxes + - `--show-passwords` – include passwords in output - `create` – create a new sandbox - `--os`: `linux`, `windows`, `macos` - `--size`: `small`, `medium`, `large` - `--region`: `north-america`, `europe`, `asia-pacific`, `south-america` + - `get ` – get detailed information about a specific sandbox + - `--json` – output in JSON format + - `--show-passwords` – include password in output + - `--show-vnc-url` – include computed NoVNC URL - `delete ` – delete a sandbox - `start ` – start a stopped sandbox - `stop ` – stop a running sandbox - `restart ` – restart a sandbox + - `suspend ` – suspend a sandbox (preserves memory state) - `vnc ` (alias: `open`) – open VNC desktop in your browser ## Auth Flow (Dynamic Callback Port) diff --git a/libs/typescript/cua-cli/src/cli.ts b/libs/typescript/cua-cli/src/cli.ts index 7ee7d080..f626da8b 100644 --- a/libs/typescript/cua-cli/src/cli.ts +++ b/libs/typescript/cua-cli/src/cli.ts @@ -14,9 +14,10 @@ export async function runCli() { ' env Export API key to .env file\n' + ' logout Clear stored credentials\n' + '\n' + - ' cua sb Create and manage cloud sandboxes\n' + + ' cua sb Create and manage cloud sandboxes\n' + ' list View all your sandboxes\n' + ' create Provision a new sandbox\n' + + ' get Get detailed info about a sandbox\n' + ' start Start or resume a sandbox\n' + ' stop Stop a sandbox (preserves disk)\n' + ' suspend Suspend a sandbox (preserves memory)\n' + diff --git a/libs/typescript/cua-cli/src/commands/sandbox.ts b/libs/typescript/cua-cli/src/commands/sandbox.ts index 5d2bde93..d1ace263 100644 --- a/libs/typescript/cua-cli/src/commands/sandbox.ts +++ b/libs/typescript/cua-cli/src/commands/sandbox.ts @@ -1,11 +1,131 @@ import type { Argv } from 'yargs'; import { ensureApiKeyInteractive } from '../auth'; -import { WEBSITE_URL } from '../config'; import { http } from '../http'; import { clearApiKey } from '../storage'; import type { SandboxItem } from '../util'; import { openInBrowser, printSandboxList } from '../util'; +// Helper function to fetch sandbox details with computer-server probes +async function fetchSandboxDetails( + name: string, + token: string, + options: { + showPasswords?: boolean; + showVncUrl?: boolean; + probeComputerServer?: boolean; + } = {} +) { + // Fetch sandbox list + const listRes = await http('/v1/vms', { token }); + if (listRes.status === 401) { + clearApiKey(); + console.error("Unauthorized. Try 'cua login' again."); + process.exit(1); + } + if (!listRes.ok) { + console.error(`Request failed: ${listRes.status}`); + process.exit(1); + } + + const sandboxes = (await listRes.json()) as SandboxItem[]; + const sandbox = sandboxes.find((s) => s.name === name); + + if (!sandbox) { + console.error('Sandbox not found'); + process.exit(1); + } + + // Build result object + const result: any = { + name: sandbox.name, + status: sandbox.status, + host: sandbox.host || `${sandbox.name}.sandbox.cua.ai`, + }; + + if (options.showPasswords) { + result.password = sandbox.password; + } + + // Compute VNC URL if requested + if (options.showVncUrl) { + const host = sandbox.host || `${sandbox.name}.sandbox.cua.ai`; + result.vnc_url = `https://${host}/vnc.html?autoconnect=true&password=${encodeURIComponent(sandbox.password)}`; + } + + // Probe computer-server if requested and sandbox is running + if (options.probeComputerServer && sandbox.status === 'running' && sandbox.host) { + let statusProbeSuccess = false; + let versionProbeSuccess = false; + + try { + // Probe OS type + const statusUrl = `https://${sandbox.host}:8443/status`; + const statusController = new AbortController(); + const statusTimeout = setTimeout(() => statusController.abort(), 3000); + + try { + const statusRes = await fetch(statusUrl, { + signal: statusController.signal, + }); + clearTimeout(statusTimeout); + + if (statusRes.ok) { + const statusData = await statusRes.json() as { status: string; os_type: string; features?: string[] }; + result.os_type = statusData.os_type; + statusProbeSuccess = true; + } + } catch (err) { + // Timeout or connection error - skip + } + + // Probe computer-server version + const versionUrl = `https://${sandbox.host}:8443/cmd`; + const versionController = new AbortController(); + const versionTimeout = setTimeout(() => versionController.abort(), 3000); + + try { + const versionRes = await fetch(versionUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Container-Name': sandbox.name, + 'X-API-Key': token, + }, + body: JSON.stringify({ + command: 'version', + params: {}, + }), + signal: versionController.signal, + }); + clearTimeout(versionTimeout); + + if (versionRes.ok) { + const versionDataRaw = await versionRes.text(); + if (versionDataRaw.startsWith('data: ')) { + const jsonStr = versionDataRaw.slice(6); + const versionData = JSON.parse(jsonStr) as { success: boolean; protocol: number; package: string }; + if (versionData.package) { + result.computer_server_version = versionData.package; + versionProbeSuccess = true; + } + } + } + } catch (err) { + // Timeout or connection error - skip + } + } catch (err) { + // General error - skip probing + } + + // Set computer server status based on probe results + if (statusProbeSuccess && versionProbeSuccess) { + result.computer_server_status = 'healthy'; + } + } + + return result; +} + // Command handlers const listHandler = async (argv: Record) => { const token = await ensureApiKeyInteractive(); @@ -254,6 +374,49 @@ const openHandler = async (argv: Record) => { await openInBrowser(url); }; +const getHandler = async (argv: Record) => { + const token = await ensureApiKeyInteractive(); + const name = String((argv as any).name); + const showPasswords = Boolean(argv['show-passwords']); + const showVncUrl = Boolean(argv['show-vnc-url']); + const json = Boolean(argv.json); + + const details = await fetchSandboxDetails(name, token, { + showPasswords, + showVncUrl, + probeComputerServer: true, + }); + + if (json) { + console.log(JSON.stringify(details, null, 2)); + } else { + // Pretty print the details + console.log(`Name: ${details.name}`); + console.log(`Status: ${details.status}`); + console.log(`Host: ${details.host}`); + + if (showPasswords) { + console.log(`Password: ${details.password}`); + } + + if (details.os_type) { + console.log(`OS Type: ${details.os_type}`); + } + + if (details.computer_server_version) { + console.log(`Computer Server Version: ${details.computer_server_version}`); + } + + if (details.computer_server_status) { + console.log(`Computer Server Status: ${details.computer_server_status}`); + } + + if (showVncUrl) { + console.log(`VNC URL: ${details.vnc_url}`); + } + } +}; + // Register commands in both flat and grouped structures export function registerSandboxCommands(y: Argv) { // Grouped structure: cua sandbox or cua sb (register first to appear first in help) @@ -345,6 +508,29 @@ export function registerSandboxCommands(y: Argv) { y.positional('name', { type: 'string', describe: 'Sandbox name' }), openHandler ) + .command( + 'get ', + 'Get detailed information about a specific sandbox', + (y) => + y + .positional('name', { type: 'string', describe: 'Sandbox name' }) + .option('json', { + type: 'boolean', + default: false, + describe: 'Output in JSON format', + }) + .option('show-passwords', { + type: 'boolean', + default: false, + describe: 'Include password in output', + }) + .option('show-vnc-url', { + type: 'boolean', + default: false, + describe: 'Include computed NoVNC URL in output', + }), + getHandler + ) .demandCommand(1, 'You must provide a sandbox command'); }, () => {} @@ -433,6 +619,29 @@ export function registerSandboxCommands(y: Argv) { builder: (y: Argv) => y.positional('name', { type: 'string', describe: 'Sandbox name' }), handler: openHandler, + } as any) + .command({ + command: 'get ', + describe: false as any, // Hide from help + builder: (y: Argv) => + y + .positional('name', { type: 'string', describe: 'Sandbox name' }) + .option('json', { + type: 'boolean', + default: false, + describe: 'Output in JSON format', + }) + .option('show-passwords', { + type: 'boolean', + default: false, + describe: 'Include password in output', + }) + .option('show-vnc-url', { + type: 'boolean', + default: false, + describe: 'Include computed NoVNC URL in output', + }), + handler: getHandler, } as any); return y;