run uv run pre-commit run --all-files

This commit is contained in:
Dillon DuPont
2025-11-11 17:18:18 -05:00
parent 554fb0a16d
commit ff957a7d04
13 changed files with 419 additions and 313 deletions

View File

@@ -77,10 +77,10 @@ class Watchdog:
"""
if not self.container_name:
return "ws://localhost:8000/ws"
# Try .sandbox.cua.ai first, fallback to .containers.cloud.trycua.com
return f"wss://{self.container_name}.sandbox.cua.ai:8443/ws"
@property
def ws_uri_fallback(self) -> str:
"""Get the fallback WebSocket URI using legacy hostname.
@@ -90,7 +90,7 @@ class Watchdog:
"""
if not self.container_name:
return "ws://localhost:8000/ws"
return f"wss://{self.container_name}.containers.cloud.trycua.com:8443/ws"
async def ping(self) -> bool:
@@ -102,7 +102,7 @@ class Watchdog:
"""
# Create a simple ping message
ping_message = {"command": "get_screen_size", "params": {}}
# Try primary URI first (.sandbox.cua.ai)
try:
async with websockets.connect(
@@ -120,12 +120,13 @@ class Watchdog:
return False
except Exception as e:
logger.debug(f"Primary URI ping failed: {e}")
# Try fallback URI (.containers.cloud.trycua.com)
if self.container_name:
try:
async with websockets.connect(
self.ws_uri_fallback, max_size=1024 * 1024 * 10 # 10MB limit to match server
self.ws_uri_fallback,
max_size=1024 * 1024 * 10, # 10MB limit to match server
) as websocket:
# Send ping message
await websocket.send(json.dumps(ping_message))
@@ -133,12 +134,16 @@ class Watchdog:
# Wait for any response or just close
try:
response = await asyncio.wait_for(websocket.recv(), timeout=5)
logger.debug(f"Ping response received from fallback URI: {response[:100]}...")
logger.debug(
f"Ping response received from fallback URI: {response[:100]}..."
)
return True
except asyncio.TimeoutError:
return False
except Exception as fallback_e:
logger.warning(f"Both primary and fallback ping failed. Primary: {e}, Fallback: {fallback_e}")
logger.warning(
f"Both primary and fallback ping failed. Primary: {e}, Fallback: {fallback_e}"
)
return False
else:
logger.warning(f"Ping failed: {e}")

View File

@@ -121,7 +121,7 @@ class CloudProvider(BaseVMProvider):
name = vm.get("name")
password = vm.get("password")
api_host = vm.get("host") # Read host from API response
if isinstance(name, str) and name:
# Use host from API if available, otherwise fallback to legacy format
if isinstance(api_host, str) and api_host:
@@ -133,7 +133,7 @@ class CloudProvider(BaseVMProvider):
host = f"{name}.containers.cloud.trycua.com"
# Cache the legacy host
self._host_cache[name] = host
# api_url: always set if missing
if not vm.get("api_url"):
vm["api_url"] = f"https://{host}:8443"
@@ -248,17 +248,17 @@ class CloudProvider(BaseVMProvider):
2. Try to refresh cache by calling list_vms
3. Try .sandbox.cua.ai format
4. Fallback to legacy .containers.cloud.trycua.com format
Args:
name: VM name
Returns:
Host string for the VM
"""
# Check cache first
if name in self._host_cache:
return self._host_cache[name]
# Try to refresh cache by calling list_vms
try:
await self.list_vms()
@@ -267,26 +267,26 @@ class CloudProvider(BaseVMProvider):
return self._host_cache[name]
except Exception as e:
logger.warning(f"Failed to refresh VM list for host lookup: {e}")
# Try .sandbox.cua.ai format first
sandbox_host = f"{name}.sandbox.cua.ai"
if await self._test_host_connectivity(sandbox_host):
self._host_cache[name] = sandbox_host
return sandbox_host
# Fallback to legacy format
legacy_host = f"{name}.containers.cloud.trycua.com"
# Cache the legacy host
self._host_cache[name] = legacy_host
return legacy_host
async def _test_host_connectivity(self, hostname: str) -> bool:
"""
Test if a host is reachable by trying to connect to its status endpoint.
Args:
hostname: Host to test
Returns:
True if host is reachable, False otherwise
"""
@@ -309,5 +309,5 @@ class CloudProvider(BaseVMProvider):
"""
if name is None:
raise ValueError("VM name is required for CloudProvider.get_ip")
return await self._get_host_for_vm(name)

View File

@@ -1,4 +1,3 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`

View File

@@ -72,5 +72,5 @@ export CUA_API_BASE=https://api.staging.cua.ai
export CUA_WEBSITE_URL=https://staging.cua.ai
cua auth login
cua vm chat my-vm # opens https://staging.cua.ai/dashboard/playground?...
cua vm chat my-vm # opens https://staging.cua.ai/dashboard/playground?...
```

View File

@@ -1,5 +1,5 @@
#! /usr/bin/env bun
import { runCli } from "./src/cli";
import { runCli } from './src/cli';
runCli().catch((err) => {
console.error(err);

View File

@@ -1,21 +1,22 @@
import { AUTH_PAGE, CALLBACK_HOST } from "./config";
import { setApiKey, getApiKey } from "./storage";
import { openInBrowser } from "./util";
import { AUTH_PAGE, CALLBACK_HOST } from './config';
import { setApiKey, getApiKey } from './storage';
import { openInBrowser } from './util';
const c = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
underline: "\x1b[4m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
underline: '\x1b[4m',
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
};
export async function loginViaBrowser(): Promise<string> {
let resolveToken!: (v: string) => void;
const tokenPromise = new Promise<string>((resolve) => { resolveToken = resolve; });
const tokenPromise = new Promise<string>((resolve) => {
resolveToken = resolve;
});
// dynamic port (0) -> OS chooses available port
const server = Bun.serve({
@@ -23,12 +24,15 @@ export async function loginViaBrowser(): Promise<string> {
port: 0,
fetch(req) {
const u = new URL(req.url);
if (u.pathname !== "/callback") return new Response("Not found", { status: 404 });
const token = u.searchParams.get("token");
if (!token) return new Response("Missing token", { status: 400 });
if (u.pathname !== '/callback') return new Response('Not found', { status: 404 });
const token = u.searchParams.get('token');
if (!token) return new Response('Missing token', { status: 400 });
resolveToken(token);
queueMicrotask(() => server.stop());
return new Response("CLI authorized. You can close this window.", { status: 200, headers: { "content-type": "text/plain" } });
return new Response('CLI authorized. You can close this window.', {
status: 200,
headers: { 'content-type': 'text/plain' },
});
},
});
@@ -41,14 +45,19 @@ export async function loginViaBrowser(): Promise<string> {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<string>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("Timed out waiting for authorization")), 2 * 60 * 1000);
timeoutId = setTimeout(
() => reject(new Error('Timed out waiting for authorization')),
2 * 60 * 1000
);
});
try {
const result = await Promise.race([tokenPromise, timeout]);
if (timeoutId) clearTimeout(timeoutId);
return result;
} finally {
try { server.stop(); } catch {}
try {
server.stop();
} catch {}
}
}

View File

@@ -1,10 +1,10 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { registerAuthCommands } from "./commands/auth";
import { registerVmCommands } from "./commands/vm";
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { registerAuthCommands } from './commands/auth';
import { registerVmCommands } from './commands/vm';
export async function runCli() {
let argv = yargs(hideBin(process.argv)).scriptName("cua");
let argv = yargs(hideBin(process.argv)).scriptName('cua');
argv = registerAuthCommands(argv);
argv = registerVmCommands(argv);
await argv.demandCommand(1).strict().help().parseAsync();

View File

@@ -1,49 +1,46 @@
import { setApiKey, clearApiKey } from "../storage";
import { ensureApiKeyInteractive, loginViaBrowser } from "../auth";
import { writeEnvFile } from "../util";
import type { Argv } from "yargs";
import { setApiKey, clearApiKey } from '../storage';
import { ensureApiKeyInteractive, loginViaBrowser } from '../auth';
import { writeEnvFile } from '../util';
import type { Argv } from 'yargs';
export function registerAuthCommands(y: Argv) {
return y.command(
"auth",
"Auth commands",
(ya) =>
ya
.command(
"login",
"Open browser to authorize and store API key",
(y) => y.option("api-key", { type: "string", describe: "API key to store directly" }),
async (argv: Record<string, unknown>) => {
if (argv["api-key"]) {
setApiKey(String(argv["api-key"]));
console.log("API key saved");
return;
}
console.log("Opening browser for CLI auth...");
const token = await loginViaBrowser();
setApiKey(token);
console.log("API key saved");
return y.command('auth', 'Auth commands', (ya) =>
ya
.command(
'login',
'Open browser to authorize and store API key',
(y) => y.option('api-key', { type: 'string', describe: 'API key to store directly' }),
async (argv: Record<string, unknown>) => {
if (argv['api-key']) {
setApiKey(String(argv['api-key']));
console.log('API key saved');
return;
}
)
.command(
"pull",
"Create or update .env with CUA_API_KEY (login if needed)",
() => {},
async (_argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const out = await writeEnvFile(process.cwd(), token);
console.log(`Wrote ${out}`);
}
)
.command(
"logout",
"Remove stored API key",
() => {},
async (_argv: Record<string, unknown>) => {
clearApiKey();
console.log("Logged out");
}
)
.demandCommand(1, "Specify an auth subcommand")
console.log('Opening browser for CLI auth...');
const token = await loginViaBrowser();
setApiKey(token);
console.log('API key saved');
}
)
.command(
'pull',
'Create or update .env with CUA_API_KEY (login if needed)',
() => {},
async (_argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const out = await writeEnvFile(process.cwd(), token);
console.log(`Wrote ${out}`);
}
)
.command(
'logout',
'Remove stored API key',
() => {},
async (_argv: Record<string, unknown>) => {
clearApiKey();
console.log('Logged out');
}
)
.demandCommand(1, 'Specify an auth subcommand')
);
}

View File

@@ -1,217 +1,291 @@
import type { Argv } from "yargs";
import { ensureApiKeyInteractive } from "../auth";
import { http } from "../http";
import { printVmList, openInBrowser } from "../util";
import { WEBSITE_URL } from "../config";
import type { VmItem } from "../util";
import { clearApiKey } from "../storage";
import type { Argv } from 'yargs';
import { ensureApiKeyInteractive } from '../auth';
import { http } from '../http';
import { printVmList, openInBrowser } from '../util';
import { WEBSITE_URL } from '../config';
import type { VmItem } from '../util';
import { clearApiKey } from '../storage';
export function registerVmCommands(y: Argv) {
return y.command(
"vm",
"VM commands",
(yv) =>
yv
.command(
"list",
"List VMs",
() => {},
async (_argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const res = await http("/v1/vms", { token });
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
if (!res.ok) {
console.error(`Request failed: ${res.status}`);
process.exit(1);
}
const data = (await res.json()) as VmItem[];
printVmList(data);
return y.command('vm', 'VM commands', (yv) =>
yv
.command(
'list',
'List VMs',
() => {},
async (_argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const res = await http('/v1/vms', { token });
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
)
.command(
"create",
"Create a new VM",
(y) => y
.option("os", {
type: "string",
choices: ["linux", "windows", "macos"],
if (!res.ok) {
console.error(`Request failed: ${res.status}`);
process.exit(1);
}
const data = (await res.json()) as VmItem[];
printVmList(data);
}
)
.command(
'create',
'Create a new VM',
(y) =>
y
.option('os', {
type: 'string',
choices: ['linux', 'windows', 'macos'],
demandOption: true,
describe: "Operating system"
describe: 'Operating system',
})
.option("configuration", {
type: "string",
choices: ["small", "medium", "large"],
.option('configuration', {
type: 'string',
choices: ['small', 'medium', 'large'],
demandOption: true,
describe: "VM size configuration"
describe: 'VM size configuration',
})
.option("region", {
type: "string",
choices: ["north-america", "europe", "asia-pacific", "south-america"],
.option('region', {
type: 'string',
choices: ['north-america', 'europe', 'asia-pacific', 'south-america'],
demandOption: true,
describe: "VM region"
describe: 'VM region',
}),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const { os, configuration, region } = argv as { os: string; configuration: string; region: string };
const res = await http("/v1/vms", {
token,
method: "POST",
body: { os, configuration, region }
});
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const { os, configuration, region } = argv as {
os: string;
configuration: string;
region: string;
};
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
const res = await http('/v1/vms', {
token,
method: 'POST',
body: { os, configuration, region },
});
if (res.status === 400) {
console.error("Invalid request or unsupported configuration");
process.exit(1);
}
if (res.status === 500) {
console.error("Internal server error");
process.exit(1);
}
if (res.status === 200) {
// VM ready immediately
const data = await res.json() as { status: string; name: string; password: string; host: string };
console.log(`VM created and ready: ${data.name}`);
console.log(`Password: ${data.password}`);
console.log(`Host: ${data.host}`);
return;
}
if (res.status === 202) {
// VM provisioning started
const data = await res.json() as { status: string; name: string; job_id: string };
console.log(`VM provisioning started: ${data.name}`);
console.log(`Job ID: ${data.job_id}`);
console.log("Use 'cua vm list' to monitor provisioning progress");
return;
}
console.error(`Unexpected status: ${res.status}`);
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
)
.command(
"delete <name>",
"Delete a VM",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}`, { token, method: "DELETE" });
if (res.status === 202) {
const body = await res.json().catch(() => ({})) as { status?: string };
console.log(`VM deletion initiated: ${body.status ?? "deleting"}`);
return;
}
if (res.status === 404) {
console.error("VM not found or not owned by you");
process.exit(1);
}
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
console.error(`Unexpected status: ${res.status}`);
if (res.status === 400) {
console.error('Invalid request or unsupported configuration');
process.exit(1);
}
)
.command(
"start <name>",
"Start a VM",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/start`, { token, method: "POST" });
if (res.status === 204) { console.log("Start accepted"); return; }
if (res.status === 404) { console.error("VM not found"); process.exit(1); }
if (res.status === 401) { clearApiKey(); console.error("Unauthorized. Try 'cua auth login' again."); process.exit(1); }
console.error(`Unexpected status: ${res.status}`); process.exit(1);
if (res.status === 500) {
console.error('Internal server error');
process.exit(1);
}
)
.command(
"stop <name>",
"Stop a VM",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/stop`, { token, method: "POST" });
if (res.status === 202) { const body = (await res.json().catch(() => ({}))) as { status?: string }; console.log(body.status ?? "stopping"); return; }
if (res.status === 404) { console.error("VM not found"); process.exit(1); }
if (res.status === 401) { clearApiKey(); console.error("Unauthorized. Try 'cua auth login' again."); process.exit(1); }
console.error(`Unexpected status: ${res.status}`); process.exit(1);
if (res.status === 200) {
// VM ready immediately
const data = (await res.json()) as {
status: string;
name: string;
password: string;
host: string;
};
console.log(`VM created and ready: ${data.name}`);
console.log(`Password: ${data.password}`);
console.log(`Host: ${data.host}`);
return;
}
)
.command(
"restart <name>",
"Restart a VM",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/restart`, { token, method: "POST" });
if (res.status === 202) { const body = (await res.json().catch(() => ({}))) as { status?: string }; console.log(body.status ?? "restarting"); return; }
if (res.status === 404) { console.error("VM not found"); process.exit(1); }
if (res.status === 401) { clearApiKey(); console.error("Unauthorized. Try 'cua auth login' again."); process.exit(1); }
console.error(`Unexpected status: ${res.status}`); process.exit(1);
if (res.status === 202) {
// VM provisioning started
const data = (await res.json()) as { status: string; name: string; job_id: string };
console.log(`VM provisioning started: ${data.name}`);
console.log(`Job ID: ${data.job_id}`);
console.log("Use 'cua vm list' to monitor provisioning progress");
return;
}
)
.command(
"vnc <name>",
"Open NoVNC for a VM in your browser",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const listRes = await http("/v1/vms", { token });
if (listRes.status === 401) { clearApiKey(); console.error("Unauthorized. Try 'cua auth login' again."); process.exit(1); }
if (!listRes.ok) { console.error(`Request failed: ${listRes.status}`); process.exit(1); }
const vms = (await listRes.json()) as VmItem[];
const vm = vms.find(v => v.name === name);
if (!vm) { console.error("VM not found"); process.exit(1); }
const url = `https://${vm.name}.containers.cloud.trycua.com/vnc.html?autoconnect=true&password=${encodeURIComponent(vm.password)}`;
console.log(`Opening NoVNC: ${url}`);
await openInBrowser(url);
console.error(`Unexpected status: ${res.status}`);
process.exit(1);
}
)
.command(
'delete <name>',
'Delete a VM',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}`, {
token,
method: 'DELETE',
});
if (res.status === 202) {
const body = (await res.json().catch(() => ({}))) as { status?: string };
console.log(`VM deletion initiated: ${body.status ?? 'deleting'}`);
return;
}
)
.command(
"chat <name>",
"Open CUA dashboard playground for a VM",
(y) => y.positional("name", { type: "string", describe: "VM name" }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const listRes = await http("/v1/vms", { token });
if (listRes.status === 401) { clearApiKey(); console.error("Unauthorized. Try 'cua auth login' again."); process.exit(1); }
if (!listRes.ok) { console.error(`Request failed: ${listRes.status}`); process.exit(1); }
const vms = (await listRes.json()) as VmItem[];
const vm = vms.find(v => v.name === name);
if (!vm) { console.error("VM not found"); process.exit(1); }
const host = `${vm.name}.containers.cloud.trycua.com`;
const base = WEBSITE_URL.replace(/\/$/, "");
const url = `${base}/dashboard/playground?host=${encodeURIComponent(host)}&id=${encodeURIComponent(vm.name)}&name=${encodeURIComponent(vm.name)}&vnc_password=${encodeURIComponent(vm.password)}&fullscreen=true`;
console.log(`Opening Playground: ${url}`);
await openInBrowser(url);
if (res.status === 404) {
console.error('VM not found or not owned by you');
process.exit(1);
}
)
.demandCommand(1, "Specify a vm subcommand")
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
console.error(`Unexpected status: ${res.status}`);
process.exit(1);
}
)
.command(
'start <name>',
'Start a VM',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/start`, {
token,
method: 'POST',
});
if (res.status === 204) {
console.log('Start accepted');
return;
}
if (res.status === 404) {
console.error('VM not found');
process.exit(1);
}
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
console.error(`Unexpected status: ${res.status}`);
process.exit(1);
}
)
.command(
'stop <name>',
'Stop a VM',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/stop`, {
token,
method: 'POST',
});
if (res.status === 202) {
const body = (await res.json().catch(() => ({}))) as { status?: string };
console.log(body.status ?? 'stopping');
return;
}
if (res.status === 404) {
console.error('VM not found');
process.exit(1);
}
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
console.error(`Unexpected status: ${res.status}`);
process.exit(1);
}
)
.command(
'restart <name>',
'Restart a VM',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const res = await http(`/v1/vms/${encodeURIComponent(name)}/restart`, {
token,
method: 'POST',
});
if (res.status === 202) {
const body = (await res.json().catch(() => ({}))) as { status?: string };
console.log(body.status ?? 'restarting');
return;
}
if (res.status === 404) {
console.error('VM not found');
process.exit(1);
}
if (res.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
console.error(`Unexpected status: ${res.status}`);
process.exit(1);
}
)
.command(
'vnc <name>',
'Open NoVNC for a VM in your browser',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const listRes = await http('/v1/vms', { token });
if (listRes.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
if (!listRes.ok) {
console.error(`Request failed: ${listRes.status}`);
process.exit(1);
}
const vms = (await listRes.json()) as VmItem[];
const vm = vms.find((v) => v.name === name);
if (!vm) {
console.error('VM not found');
process.exit(1);
}
const url = `https://${vm.name}.containers.cloud.trycua.com/vnc.html?autoconnect=true&password=${encodeURIComponent(vm.password)}`;
console.log(`Opening NoVNC: ${url}`);
await openInBrowser(url);
}
)
.command(
'chat <name>',
'Open CUA dashboard playground for a VM',
(y) => y.positional('name', { type: 'string', describe: 'VM name' }),
async (argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const name = String((argv as any).name);
const listRes = await http('/v1/vms', { token });
if (listRes.status === 401) {
clearApiKey();
console.error("Unauthorized. Try 'cua auth login' again.");
process.exit(1);
}
if (!listRes.ok) {
console.error(`Request failed: ${listRes.status}`);
process.exit(1);
}
const vms = (await listRes.json()) as VmItem[];
const vm = vms.find((v) => v.name === name);
if (!vm) {
console.error('VM not found');
process.exit(1);
}
const host = `${vm.name}.containers.cloud.trycua.com`;
const base = WEBSITE_URL.replace(/\/$/, '');
const url = `${base}/dashboard/playground?host=${encodeURIComponent(host)}&id=${encodeURIComponent(vm.name)}&name=${encodeURIComponent(vm.name)}&vnc_password=${encodeURIComponent(vm.password)}&fullscreen=true`;
console.log(`Opening Playground: ${url}`);
await openInBrowser(url);
}
)
.demandCommand(1, 'Specify a vm subcommand')
);
}

View File

@@ -1,13 +1,13 @@
export const WEBSITE_URL = Bun.env.CUA_WEBSITE_URL?.replace(/\/$/, "") || "https://cua.ai";
export const API_BASE = Bun.env.CUA_API_BASE?.replace(/\/$/, "") || "https://api.cua.ai";
export const WEBSITE_URL = Bun.env.CUA_WEBSITE_URL?.replace(/\/$/, '') || 'https://cua.ai';
export const API_BASE = Bun.env.CUA_API_BASE?.replace(/\/$/, '') || 'https://api.cua.ai';
export const AUTH_PAGE = `${WEBSITE_URL}/cli-auth`;
export const CALLBACK_HOST = "127.0.0.1";
export const CALLBACK_HOST = '127.0.0.1';
export function getConfigDir(): string {
const home = Bun.env.HOME || Bun.env.USERPROFILE || ".";
const home = Bun.env.HOME || Bun.env.USERPROFILE || '.';
const dir = `${home}/.cua`;
try {
Bun.spawnSync(["mkdir", "-p", dir]);
Bun.spawnSync(['mkdir', '-p', dir]);
} catch {}
return dir;
}

View File

@@ -1,11 +1,14 @@
import { API_BASE } from "./config";
import { API_BASE } from './config';
export async function http(path: string, opts: { method?: string; token: string; body?: any }): Promise<Response> {
export async function http(
path: string,
opts: { method?: string; token: string; body?: any }
): Promise<Response> {
const url = `${API_BASE}${path}`;
const headers: Record<string, string> = { Authorization: `Bearer ${opts.token}` };
if (opts.body) headers["content-type"] = "application/json";
if (opts.body) headers['content-type'] = 'application/json';
return fetch(url, {
method: opts.method || "GET",
method: opts.method || 'GET',
headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});

View File

@@ -1,17 +1,19 @@
import { Database } from "bun:sqlite";
import { getDbPath } from "./config";
import { Database } from 'bun:sqlite';
import { getDbPath } from './config';
function getDb(): Database {
const db = new Database(getDbPath());
db.exec("PRAGMA journal_mode = WAL;");
db.exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);");
db.exec('PRAGMA journal_mode = WAL;');
db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);');
return db;
}
export function setApiKey(token: string) {
const db = getDb();
try {
const stmt = db.query("INSERT INTO kv (k, v) VALUES ('api_key', ?) ON CONFLICT(k) DO UPDATE SET v=excluded.v");
const stmt = db.query(
"INSERT INTO kv (k, v) VALUES ('api_key', ?) ON CONFLICT(k) DO UPDATE SET v=excluded.v"
);
stmt.run(token);
} finally {
db.close();

View File

@@ -1,32 +1,49 @@
export async function writeEnvFile(cwd: string, key: string) {
const path = `${cwd}/.env`;
let content = "";
try { content = await Bun.file(path).text(); } catch {}
let content = '';
try {
content = await Bun.file(path).text();
} catch {}
const lines = content.split(/\r?\n/).filter(Boolean);
const idx = lines.findIndex((l) => l.startsWith("CUA_API_KEY="));
if (idx >= 0) lines[idx] = `CUA_API_KEY=${key}`; else lines.push(`CUA_API_KEY=${key}`);
await Bun.write(path, lines.join("\n") + "\n");
const idx = lines.findIndex((l) => l.startsWith('CUA_API_KEY='));
if (idx >= 0) lines[idx] = `CUA_API_KEY=${key}`;
else lines.push(`CUA_API_KEY=${key}`);
await Bun.write(path, lines.join('\n') + '\n');
return path;
}
export type VmStatus = "pending" | "running" | "stopped" | "terminated" | "failed";
export type VmStatus = 'pending' | 'running' | 'stopped' | 'terminated' | 'failed';
export type VmItem = { name: string; password: string; status: VmStatus; host?: string };
export function printVmList(items: VmItem[]) {
const rows: string[][] = [["NAME", "STATUS", "PASSWORD", "HOST"], ...items.map(v => [v.name, String(v.status), v.password, v.host || ""])];
const rows: string[][] = [
['NAME', 'STATUS', 'PASSWORD', 'HOST'],
...items.map((v) => [v.name, String(v.status), v.password, v.host || '']),
];
const widths: number[] = [0, 0, 0, 0];
for (const r of rows) for (let i = 0; i < 4; i++) widths[i] = Math.max(widths[i] ?? 0, (r[i] ?? "").length);
for (const r of rows) console.log(r.map((c, i) => (c ?? "").padEnd(widths[i] ?? 0)).join(" "));
if (items.length === 0) console.log("No VMs found");
for (const r of rows)
for (let i = 0; i < 4; i++) widths[i] = Math.max(widths[i] ?? 0, (r[i] ?? '').length);
for (const r of rows) console.log(r.map((c, i) => (c ?? '').padEnd(widths[i] ?? 0)).join(' '));
if (items.length === 0) console.log('No VMs found');
}
export async function openInBrowser(url: string) {
const platform = process.platform;
let cmd: string;
let args: string[] = [];
if (platform === "darwin") { cmd = "open"; args = [url]; }
else if (platform === "win32") { cmd = "cmd"; args = ["/c", "start", "", url]; }
else { cmd = "xdg-open"; args = [url]; }
try { await Bun.spawn({ cmd: [cmd, ...args] }).exited; }
catch { console.error(`Failed to open browser. Please visit: ${url}`); }
if (platform === 'darwin') {
cmd = 'open';
args = [url];
} else if (platform === 'win32') {
cmd = 'cmd';
args = ['/c', 'start', '', url];
} else {
cmd = 'xdg-open';
args = [url];
}
try {
await Bun.spawn({ cmd: [cmd, ...args] }).exited;
} catch {
console.error(`Failed to open browser. Please visit: ${url}`);
}
}