Add "cua get" cmd to CLI

This commit is contained in:
Dillon DuPont
2025-12-01 11:28:31 -05:00
parent 5b37015fd2
commit 77d8ecd5f3
4 changed files with 290 additions and 3 deletions

View File

@@ -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 <name>
# With additional options
cua get <name> --json
cua get <name> --show-passwords
cua get <name> --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
<Callout type="info">
The computer server status is only checked for running sandboxes. Stopped or suspended sandboxes will not show computer server information.
</Callout>
### `cua start`
Start a stopped sandbox.

View File

@@ -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 <name>` 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 <name>` delete a sandbox
- `start <name>` start a stopped sandbox
- `stop <name>` stop a running sandbox
- `restart <name>` restart a sandbox
- `suspend <name>` suspend a sandbox (preserves memory state)
- `vnc <name>` (alias: `open`) open VNC desktop in your browser
## Auth Flow (Dynamic Callback Port)

View File

@@ -14,9 +14,10 @@ export async function runCli() {
' env Export API key to .env file\n' +
' logout Clear stored credentials\n' +
'\n' +
' cua sb <command> Create and manage cloud sandboxes\n' +
' cua sb <command> 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' +

View File

@@ -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<string, unknown>) => {
const token = await ensureApiKeyInteractive();
@@ -254,6 +374,49 @@ const openHandler = async (argv: Record<string, unknown>) => {
await openInBrowser(url);
};
const getHandler = async (argv: Record<string, unknown>) => {
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 <command> or cua sb <command> (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 <name>',
'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 <name>',
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;