added cua CLI

This commit is contained in:
Dillon DuPont
2025-10-20 12:12:14 -04:00
parent 517d641cc1
commit be98606cb5
15 changed files with 610 additions and 0 deletions

34
libs/typescript/cua-cli/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
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 <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

@@ -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:<port>/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 <api_key>`.

View File

@@ -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=="],
}
}

View File

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

View File

@@ -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"
}
}

View File

@@ -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<string> {
let resolveToken!: (v: string) => void;
const tokenPromise = new Promise<string>((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<string>((_, 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<string> {
const existing = getApiKey();
if (existing) return existing;
const token = await loginViaBrowser();
setApiKey(token);
return token;
}

View File

@@ -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();
}

View File

@@ -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<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");
}
)
.command(
"pull",
"Create or update .env with CUA_API_KEY (login if needed)",
() => {},
async (_argv: Record<string, unknown>) => {
const token = await ensureApiKeyInteractive();
const out = await writeEnvFile(process.cwd(), token);
console.log(`Wrote ${out}`);
}
)
.command(
"logout",
"Remove stored API key",
() => {},
async (_argv: Record<string, unknown>) => {
clearApiKey();
console.log("Logged out");
}
)
.demandCommand(1, "Specify an auth subcommand")
);
}

View File

@@ -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<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);
}
)
.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);
}
)
.demandCommand(1, "Specify a vm subcommand")
);
}

View File

@@ -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`;
}

View File

@@ -0,0 +1,12 @@
import { API_BASE } from "./config";
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";
return fetch(url, {
method: opts.method || "GET",
headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
}

View File

@@ -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();
}

View File

@@ -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}`); }
}

View File

@@ -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
}
}