From be98606cb558e66c88f4b15aeaa94c0139e58d94 Mon Sep 17 00:00:00 2001 From: Dillon DuPont Date: Mon, 20 Oct 2025 12:12:14 -0400 Subject: [PATCH] added cua CLI --- libs/typescript/cua-cli/.gitignore | 34 ++++++ libs/typescript/cua-cli/CLAUDE.md | 106 +++++++++++++++++++ libs/typescript/cua-cli/README.md | 56 ++++++++++ libs/typescript/cua-cli/bun.lock | 63 +++++++++++ libs/typescript/cua-cli/index.ts | 7 ++ libs/typescript/cua-cli/package.json | 19 ++++ libs/typescript/cua-cli/src/auth.ts | 54 ++++++++++ libs/typescript/cua-cli/src/cli.ts | 11 ++ libs/typescript/cua-cli/src/commands/auth.ts | 49 +++++++++ libs/typescript/cua-cli/src/commands/vm.ts | 96 +++++++++++++++++ libs/typescript/cua-cli/src/config.ts | 16 +++ libs/typescript/cua-cli/src/http.ts | 12 +++ libs/typescript/cua-cli/src/storage.ts | 26 +++++ libs/typescript/cua-cli/src/util.ts | 32 ++++++ libs/typescript/cua-cli/tsconfig.json | 29 +++++ 15 files changed, 610 insertions(+) create mode 100644 libs/typescript/cua-cli/.gitignore create mode 100644 libs/typescript/cua-cli/CLAUDE.md create mode 100644 libs/typescript/cua-cli/README.md create mode 100644 libs/typescript/cua-cli/bun.lock create mode 100755 libs/typescript/cua-cli/index.ts create mode 100644 libs/typescript/cua-cli/package.json create mode 100644 libs/typescript/cua-cli/src/auth.ts create mode 100644 libs/typescript/cua-cli/src/cli.ts create mode 100644 libs/typescript/cua-cli/src/commands/auth.ts create mode 100644 libs/typescript/cua-cli/src/commands/vm.ts create mode 100644 libs/typescript/cua-cli/src/config.ts create mode 100644 libs/typescript/cua-cli/src/http.ts create mode 100644 libs/typescript/cua-cli/src/storage.ts create mode 100644 libs/typescript/cua-cli/src/util.ts create mode 100644 libs/typescript/cua-cli/tsconfig.json diff --git a/libs/typescript/cua-cli/.gitignore b/libs/typescript/cua-cli/.gitignore new file mode 100644 index 00000000..a14702c4 --- /dev/null +++ b/libs/typescript/cua-cli/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/libs/typescript/cua-cli/CLAUDE.md b/libs/typescript/cua-cli/CLAUDE.md new file mode 100644 index 00000000..1ee68904 --- /dev/null +++ b/libs/typescript/cua-cli/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/libs/typescript/cua-cli/README.md b/libs/typescript/cua-cli/README.md new file mode 100644 index 00000000..54d58f5e --- /dev/null +++ b/libs/typescript/cua-cli/README.md @@ -0,0 +1,56 @@ +# CUA CLI (Bun) + +## Install + +```bash +bun install +bun link # register package globally +bun link cua-cli # install the global binary `cua` +``` + +If you want to run without linking: + +```bash +bun run ./index.ts -- --help +``` + +## Commands + +- **Auth** + - `cua auth login` – opens browser to authorize; stores API key locally + - `cua auth login --api-key sk-...` – stores provided key directly + - `cua auth pull` – writes/updates `.env` with `CUA_API_KEY` + - `cua auth logout` – clears stored API key + +- **VMs** + - `cua vm list` + - `cua vm start NAME` + - `cua vm stop NAME` + - `cua vm restart NAME` + +## Auth Flow (Dynamic Callback Port) + +- CLI starts a small local HTTP server using `Bun.serve({ port: 0 })` which picks an available port. +- Browser is opened to `https://cua.ai/cli-auth?callback_url=http://127.0.0.1:/callback`. +- After you click "Authorize CLI", the browser redirects to the local server with `?token=...`. +- The CLI saves the API key in `~/.config/cua/cli.sqlite`. + +> Note: If the browser cannot be opened automatically, copy/paste the printed URL. + +## Project Structure + +- `index.ts` – entry point (shebang + start CLI) +- `src/cli.ts` – yargs bootstrapping +- `src/commands/auth.ts` – auth/login/pull/logout commands +- `src/commands/vm.ts` – vm list/start/stop/restart commands +- `src/auth.ts` – browser flow + local callback server (dynamic port) +- `src/http.ts` – HTTP helper +- `src/storage.ts` – SQLite-backed key-value storage +- `src/config.ts` – constants and paths +- `src/util.ts` – table printing, .env writer + +## Notes + +- Stored API key lives at `~/.config/cua/cli.sqlite` under `kv(api_key)`. +- Public API base: `https://api.cua.ai`. +- Authorization header: `Authorization: Bearer `. diff --git a/libs/typescript/cua-cli/bun.lock b/libs/typescript/cua-cli/bun.lock new file mode 100644 index 00000000..4de964c1 --- /dev/null +++ b/libs/typescript/cua-cli/bun.lock @@ -0,0 +1,63 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "cua-cli", + "dependencies": { + "yargs": "^18.0.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/yargs": "^17.0.33", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + + "@types/node": ["@types/node@24.9.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-MKNwXh3seSK8WurXF7erHPJ2AONmMwkI7zAMrXZDPIru8jRqkk6rGDBVbw4mLwfqA+ZZliiDPg05JQ3uW66tKQ=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + } +} diff --git a/libs/typescript/cua-cli/index.ts b/libs/typescript/cua-cli/index.ts new file mode 100755 index 00000000..b48c5715 --- /dev/null +++ b/libs/typescript/cua-cli/index.ts @@ -0,0 +1,7 @@ +#! /usr/bin/env bun +import { runCli } from "./src/cli"; + +runCli().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/libs/typescript/cua-cli/package.json b/libs/typescript/cua-cli/package.json new file mode 100644 index 00000000..0651e521 --- /dev/null +++ b/libs/typescript/cua-cli/package.json @@ -0,0 +1,19 @@ +{ + "name": "cua-cli", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "@types/yargs": "^17.0.33" + }, + "peerDependencies": { + "typescript": "^5" + }, + "bin": { + "cua": "./index.ts" + }, + "dependencies": { + "yargs": "^18.0.0" + } +} diff --git a/libs/typescript/cua-cli/src/auth.ts b/libs/typescript/cua-cli/src/auth.ts new file mode 100644 index 00000000..b3659b89 --- /dev/null +++ b/libs/typescript/cua-cli/src/auth.ts @@ -0,0 +1,54 @@ +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", +}; + +// openInBrowser is imported from util + +export async function loginViaBrowser(): Promise { + let resolveToken!: (v: string) => void; + const tokenPromise = new Promise((resolve) => { resolveToken = resolve; }); + + // dynamic port (0) -> OS chooses available port + const server = Bun.serve({ + hostname: CALLBACK_HOST, + 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 }); + resolveToken(token); + queueMicrotask(() => server.stop()); + return new Response("CLI authorized. You can close this window.", { status: 200, headers: { "content-type": "text/plain" } }); + }, + }); + + const callbackURL = `http://${CALLBACK_HOST}:${server.port}/callback`; + const url = `${AUTH_PAGE}?callback_url=${encodeURIComponent(callbackURL)}`; + console.log(`${c.cyan}${c.bold}Opening your default browser to authorize the CLI...${c.reset}`); + console.log(`${c.dim}If the browser does not open automatically, copy/paste this URL:${c.reset}`); + console.log(`${c.yellow}${c.underline}${url}${c.reset}`); + await openInBrowser(url); + + const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out waiting for authorization")), 2 * 60 * 1000)); + try { return await Promise.race([tokenPromise, timeout]); } + finally { try { server.stop(); } catch {} } +} + +export async function ensureApiKeyInteractive(): Promise { + const existing = getApiKey(); + if (existing) return existing; + const token = await loginViaBrowser(); + setApiKey(token); + return token; +} diff --git a/libs/typescript/cua-cli/src/cli.ts b/libs/typescript/cua-cli/src/cli.ts new file mode 100644 index 00000000..2cfa759d --- /dev/null +++ b/libs/typescript/cua-cli/src/cli.ts @@ -0,0 +1,11 @@ +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"); + 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 new file mode 100644 index 00000000..e09beb03 --- /dev/null +++ b/libs/typescript/cua-cli/src/commands/auth.ts @@ -0,0 +1,49 @@ +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"); + } + ) + .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 new file mode 100644 index 00000000..0fcb73be --- /dev/null +++ b/libs/typescript/cua-cli/src/commands/vm.ts @@ -0,0 +1,96 @@ +import type { Argv } from "yargs"; +import { ensureApiKeyInteractive } from "../auth"; +import { http } from "../http"; +import { printVmList, openInBrowser } from "../util"; +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); + } + ) + .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); + } + ) + .demandCommand(1, "Specify a vm subcommand") + ); +} diff --git a/libs/typescript/cua-cli/src/config.ts b/libs/typescript/cua-cli/src/config.ts new file mode 100644 index 00000000..3ef686c2 --- /dev/null +++ b/libs/typescript/cua-cli/src/config.ts @@ -0,0 +1,16 @@ +export const API_BASE = "https://api.cua.ai"; +export const AUTH_PAGE = "https://cua.ai/cli-auth"; +export const CALLBACK_HOST = "127.0.0.1"; + +export function getConfigDir(): string { + const home = Bun.env.HOME || Bun.env.USERPROFILE || "."; + const dir = `${home}/.cua/config`; + try { + Bun.spawnSync(["mkdir", "-p", dir]); + } catch {} + return dir; +} + +export function getDbPath(): string { + return `${getConfigDir()}/cli.sqlite`; +} diff --git a/libs/typescript/cua-cli/src/http.ts b/libs/typescript/cua-cli/src/http.ts new file mode 100644 index 00000000..95ff39ea --- /dev/null +++ b/libs/typescript/cua-cli/src/http.ts @@ -0,0 +1,12 @@ +import { API_BASE } from "./config"; + +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"; + return fetch(url, { + 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 new file mode 100644 index 00000000..d2c4fa56 --- /dev/null +++ b/libs/typescript/cua-cli/src/storage.ts @@ -0,0 +1,26 @@ +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);"); + return db; +} + +export function setApiKey(token: string) { + const db = getDb(); + const stmt = db.query("INSERT INTO kv (k, v) VALUES ('api_key', ?) ON CONFLICT(k) DO UPDATE SET v=excluded.v"); + stmt.run(token); +} + +export function getApiKey(): string | null { + const db = getDb(); + const row = db.query("SELECT v FROM kv WHERE k='api_key'").get() as { v: string } | undefined; + return row?.v ?? null; +} + +export function clearApiKey() { + const db = getDb(); + db.query("DELETE FROM kv WHERE k='api_key'").run(); +} diff --git a/libs/typescript/cua-cli/src/util.ts b/libs/typescript/cua-cli/src/util.ts new file mode 100644 index 00000000..503378b8 --- /dev/null +++ b/libs/typescript/cua-cli/src/util.ts @@ -0,0 +1,32 @@ +export async function writeEnvFile(cwd: string, key: string) { + const path = `${cwd}/.env`; + 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"); + return path; +} + +export type VmStatus = "pending" | "running" | "stopped" | "terminated" | "failed"; +export type VmItem = { name: string; password: string; status: VmStatus }; + +export function printVmList(items: VmItem[]) { + const rows: string[][] = [["NAME", "STATUS", "PASSWORD"], ...items.map(v => [v.name, String(v.status), v.password])]; + const widths: number[] = [0, 0, 0]; + for (const r of rows) for (let i = 0; i < 3; 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}`); } +} diff --git a/libs/typescript/cua-cli/tsconfig.json b/libs/typescript/cua-cli/tsconfig.json new file mode 100644 index 00000000..bfa0fead --- /dev/null +++ b/libs/typescript/cua-cli/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}