mirror of
https://github.com/trycua/computer.git
synced 2026-02-21 13:59:34 -06:00
run uv run pre-commit run --all-files
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <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?...
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env bun
|
||||
import { runCli } from "./src/cli";
|
||||
import { runCli } from './src/cli';
|
||||
|
||||
runCli().catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user