diff --git a/libs/python/computer-server/computer_server/watchdog.py b/libs/python/computer-server/computer_server/watchdog.py index 460c51c6..50ade796 100644 --- a/libs/python/computer-server/computer_server/watchdog.py +++ b/libs/python/computer-server/computer_server/watchdog.py @@ -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}") diff --git a/libs/python/computer/computer/providers/cloud/provider.py b/libs/python/computer/computer/providers/cloud/provider.py index 8db233a2..5664f18b 100644 --- a/libs/python/computer/computer/providers/cloud/provider.py +++ b/libs/python/computer/computer/providers/cloud/provider.py @@ -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) diff --git a/libs/typescript/cua-cli/CLAUDE.md b/libs/typescript/cua-cli/CLAUDE.md index 1ee68904..5fa3f4d5 100644 --- a/libs/typescript/cua-cli/CLAUDE.md +++ b/libs/typescript/cua-cli/CLAUDE.md @@ -1,4 +1,3 @@ - Default to using Bun instead of Node.js. - Use `bun ` instead of `node ` or `ts-node ` diff --git a/libs/typescript/cua-cli/README.md b/libs/typescript/cua-cli/README.md index b5843428..3a8004f7 100644 --- a/libs/typescript/cua-cli/README.md +++ b/libs/typescript/cua-cli/README.md @@ -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?... ``` diff --git a/libs/typescript/cua-cli/index.ts b/libs/typescript/cua-cli/index.ts index b48c5715..ca291983 100755 --- a/libs/typescript/cua-cli/index.ts +++ b/libs/typescript/cua-cli/index.ts @@ -1,5 +1,5 @@ #! /usr/bin/env bun -import { runCli } from "./src/cli"; +import { runCli } from './src/cli'; runCli().catch((err) => { console.error(err); diff --git a/libs/typescript/cua-cli/src/auth.ts b/libs/typescript/cua-cli/src/auth.ts index e46f4e6b..28bef6df 100644 --- a/libs/typescript/cua-cli/src/auth.ts +++ b/libs/typescript/cua-cli/src/auth.ts @@ -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 { let resolveToken!: (v: string) => void; - const tokenPromise = new Promise((resolve) => { resolveToken = resolve; }); + const tokenPromise = new Promise((resolve) => { + resolveToken = resolve; + }); // dynamic port (0) -> OS chooses available port const server = Bun.serve({ @@ -23,12 +24,15 @@ export async function loginViaBrowser(): Promise { 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 { let timeoutId: ReturnType | undefined; const timeout = new Promise((_, 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 {} } } diff --git a/libs/typescript/cua-cli/src/cli.ts b/libs/typescript/cua-cli/src/cli.ts index 2cfa759d..b47280b8 100644 --- a/libs/typescript/cua-cli/src/cli.ts +++ b/libs/typescript/cua-cli/src/cli.ts @@ -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(); diff --git a/libs/typescript/cua-cli/src/commands/auth.ts b/libs/typescript/cua-cli/src/commands/auth.ts index e09beb03..8a9878c5 100644 --- a/libs/typescript/cua-cli/src/commands/auth.ts +++ b/libs/typescript/cua-cli/src/commands/auth.ts @@ -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) => { - 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) => { + 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) => { - const token = await ensureApiKeyInteractive(); - const out = await writeEnvFile(process.cwd(), token); - console.log(`Wrote ${out}`); - } - ) - .command( - "logout", - "Remove stored API key", - () => {}, - async (_argv: Record) => { - 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) => { + const token = await ensureApiKeyInteractive(); + const out = await writeEnvFile(process.cwd(), token); + console.log(`Wrote ${out}`); + } + ) + .command( + 'logout', + 'Remove stored API key', + () => {}, + async (_argv: Record) => { + clearApiKey(); + console.log('Logged out'); + } + ) + .demandCommand(1, 'Specify an auth subcommand') ); } diff --git a/libs/typescript/cua-cli/src/commands/vm.ts b/libs/typescript/cua-cli/src/commands/vm.ts index ded4f950..843a49dd 100644 --- a/libs/typescript/cua-cli/src/commands/vm.ts +++ b/libs/typescript/cua-cli/src/commands/vm.ts @@ -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) => { - 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) => { + 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) => { - 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) => { + 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 ", - "Delete a VM", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ", - "Start a VM", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ", - "Stop a VM", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ", - "Restart a VM", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ", - "Open NoVNC for a VM in your browser", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ', + 'Delete a VM', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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 ", - "Open CUA dashboard playground for a VM", - (y) => y.positional("name", { type: "string", describe: "VM name" }), - async (argv: Record) => { - 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 ', + 'Start a VM', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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 ', + 'Stop a VM', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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 ', + 'Restart a VM', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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 ', + 'Open NoVNC for a VM in your browser', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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 ', + 'Open CUA dashboard playground for a VM', + (y) => y.positional('name', { type: 'string', describe: 'VM name' }), + async (argv: Record) => { + 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') ); } diff --git a/libs/typescript/cua-cli/src/config.ts b/libs/typescript/cua-cli/src/config.ts index 16e93662..e8183065 100644 --- a/libs/typescript/cua-cli/src/config.ts +++ b/libs/typescript/cua-cli/src/config.ts @@ -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; } diff --git a/libs/typescript/cua-cli/src/http.ts b/libs/typescript/cua-cli/src/http.ts index 95ff39ea..57b7c408 100644 --- a/libs/typescript/cua-cli/src/http.ts +++ b/libs/typescript/cua-cli/src/http.ts @@ -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 { +export async function http( + path: string, + opts: { method?: string; token: string; body?: any } +): Promise { const url = `${API_BASE}${path}`; const headers: Record = { 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, }); diff --git a/libs/typescript/cua-cli/src/storage.ts b/libs/typescript/cua-cli/src/storage.ts index 15f68308..ea4a6d3a 100644 --- a/libs/typescript/cua-cli/src/storage.ts +++ b/libs/typescript/cua-cli/src/storage.ts @@ -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(); diff --git a/libs/typescript/cua-cli/src/util.ts b/libs/typescript/cua-cli/src/util.ts index 2d1a1578..16705b2e 100644 --- a/libs/typescript/cua-cli/src/util.ts +++ b/libs/typescript/cua-cli/src/util.ts @@ -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}`); + } }