#!/usr/bin/env node /* eslint-disable local/structured-log-format */ /** * oppi CLI * * Commands: * init Interactive first-time setup * serve Start the server * pair [name] Pair iOS client with server owner token * status Show server status * doctor Run security + environment diagnostics * token Rotate owner bearer token * config Show/get/set/validate server config */ import * as c from "./ansi.js"; import { renderTerminal as renderQR } from "./qr.js"; import { readFileSync, existsSync, statSync } from "node:fs"; import { execSync } from "node:child_process"; import { createInterface } from "node:readline"; import { join } from "node:path"; import { hostname as osHostname, networkInterfaces } from "node:os"; import { Storage } from "./storage.js"; import { Server } from "./server.js"; import { applyHostEnv, resolveExecutableOnPath, resolveHostEnv } from "./host-env.js"; import { ensureIdentityMaterial, identityConfigForDataDir } from "./security.js"; import type { APNsConfig } from "./push.js"; import { readCertificateExpiryMs, readCertificateFingerprint, resolveTlsConfig, tlsSchemeForConfig, } from "./tls.js"; import type { ServerConfig } from "./types.js"; import { generateInvite } from "./invite.js"; function loadAPNsConfig(storage: Storage): APNsConfig | undefined { const dataDir = storage.getDataDir(); const apnsConfigPath = join(dataDir, "apns.json"); if (!existsSync(apnsConfigPath)) return undefined; try { const raw = JSON.parse(readFileSync(apnsConfigPath, "utf-8")); if (!raw.keyPath || !raw.keyId || !raw.teamId || !raw.bundleId) { console.log(c.yellow(" ⚠️ apns.json incomplete — need keyPath, keyId, teamId, bundleId")); return undefined; } return raw as APNsConfig; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); console.log(c.yellow(` ⚠️ apns.json parse error: ${message}`)); return undefined; } } function printHeader(): void { console.log(""); console.log(c.boldMagenta(" ╭─────────────────────────────────────╮")); console.log( c.boldMagenta(" │") + c.bold(" π oppi ") + c.boldMagenta("│"), ); console.log(c.boldMagenta(" ╰─────────────────────────────────────╯")); console.log(""); } function getTailscaleHostname(): string | null { try { const result = execSync("tailscale status --json", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); const status = JSON.parse(result); if (status.Self?.DNSName) { return status.Self.DNSName.replace(/\.$/, ""); } } catch {} return null; } function getTailscaleIp(): string | null { try { return execSync("tailscale ip -4", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }) .trim() .split("\n")[0]; } catch {} return null; } function getLocalHostname(): string | null { try { const localHostName = execSync("scutil --get LocalHostName", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }).trim(); if (localHostName) { return `${localHostName}.local`; } } catch {} try { const host = osHostname().trim(); if (!host) return null; if (host.endsWith(".local")) return host; return `${host.split(".")[0]}.local`; } catch {} return null; } function getLocalIp(): string | null { const nets = networkInterfaces(); for (const iface of Object.values(nets)) { if (!iface) continue; for (const addr of iface) { if (addr.family !== "IPv4") continue; if (addr.internal) continue; if (addr.address.startsWith("169.254.")) continue; // Link-local fallback return addr.address; } } return null; } function resolveInviteHost(config: ServerConfig, hostOverride?: string): string | null { if (hostOverride?.trim()) return hostOverride.trim(); if (config.tls?.mode === "tailscale") { return getTailscaleHostname(); } // Prefer local network; fall back to Tailscale if no LAN host found. return getLocalHostname() || getLocalIp() || getTailscaleHostname() || getTailscaleIp(); } function shortHostLabel(host: string): string { // Keep IPs as-is, trim FQDNs to first label. if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return host; return host.split(".")[0] || host; } // ─── Commands ─── async function cmdServe(storage: Storage, pairHost?: string): Promise { printHeader(); // Auto-init: generate owner token + identity keys if this is a fresh install. if (!storage.isPaired()) { storage.rotateToken(); console.log(c.green(" ✓ First run — owner token generated")); } ensureIdentityMaterial(identityConfigForDataDir(storage.getDataDir())); const config = storage.getConfig(); // Apply runtime environment from config.json (explicit configuration only). applyHostEnv(config); const tailscaleHostname = getTailscaleHostname(); const tailscaleIp = getTailscaleIp(); const localHostname = getLocalHostname(); const localIp = getLocalIp(); if (tailscaleHostname || tailscaleIp) { console.log(c.dim(" Tailscale detected — remote access available")); console.log(""); } // Load APNs config from config file if present const apnsConfig = loadAPNsConfig(storage); const server = new Server(storage, apnsConfig); let shuttingDown = false; async function shutdown(code: number, reason?: string): Promise { if (shuttingDown) { return; } shuttingDown = true; if (reason) { console.log(`\n${reason}`); } await server.stop().catch((err: unknown) => { console.error(c.red("Shutdown error:"), err); }); process.exit(code); } process.on("SIGINT", () => { void shutdown(0, "\nShutting down..."); }); process.on("SIGTERM", () => { void shutdown(0); }); process.on("uncaughtException", (err) => { console.error(c.red("Uncaught exception:"), err); void shutdown(1); }); process.on("unhandledRejection", (reason) => { console.error(c.red("Unhandled rejection:"), reason); void shutdown(1); }); await server.start(); console.log(""); const scheme = server.scheme; const displayPort = server.port; if (localHostname) { console.log(` Local: ${c.cyan(`${scheme}://${localHostname}:${displayPort}`)}`); } if (localIp) { console.log(` LAN IP: ${c.dim(`${scheme}://${localIp}:${displayPort}`)}`); } if (tailscaleHostname) { console.log(` Tailscale: ${c.dim(`${scheme}://${tailscaleHostname}:${displayPort}`)}`); } if (tailscaleIp) { console.log(` Tail IP: ${c.dim(`${scheme}://${tailscaleIp}:${displayPort}`)}`); } console.log(` Data: ${c.dim(storage.getDataDir())}`); console.log(""); if (storage.isPaired()) { console.log(c.green(" ✓ Paired")); console.log(""); console.log(c.green(" Waiting for connections...")); console.log(c.dim(" Press Ctrl+C to stop")); console.log(c.dim(" Run 'oppi pair' to re-pair or add devices.")); console.log(""); } else { // First run: show pairing QR inline so user doesn't need a separate command. console.log(""); showPairingQR(storage, undefined, pairHost); console.log(c.green(" Server is running. Scan QR above, then Ctrl+C when done.")); console.log(""); } } /** * Show the pairing QR code + deep link. Reusable by both `pair` and `serve`. * Returns true if QR was shown, false if host detection failed. */ function showPairingQR( storage: Storage, requestedName?: string, hostOverride?: string, showToken = false, ): boolean { let invite; try { invite = generateInvite( storage, (override) => resolveInviteHost(storage.getConfig(), override), shortHostLabel, { hostOverride, requestedName }, ); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); console.log(c.red(` Error: ${message}`)); console.log(""); return false; } if (hostOverride?.trim()) { console.log(c.dim(` (using host override: ${invite.host})`)); } else { console.log(c.dim(` (auto-detected host: ${invite.host})`)); } console.log(` 📱 Pair with ${c.bold(shortHostLabel(invite.host))}`); console.log(c.dim(` Transport: ${invite.scheme.toUpperCase()} (${invite.host}:${invite.port})`)); if (invite.tlsCertFingerprint) { console.log(c.dim(` Cert pin: ${invite.tlsCertFingerprint}`)); } console.log(""); console.log(" Scan this QR code in Oppi:"); console.log(""); // Reconstruct v3 JSON for QR encoding (generateInvite returns the URL, we need raw JSON for QR) const invitePayloadJson = JSON.stringify({ v: 3, host: invite.host, port: invite.port, scheme: invite.scheme, token: "", pairingToken: invite.pairingToken, name: invite.name, tlsCertFingerprint: invite.tlsCertFingerprint, fingerprint: invite.fingerprint, }); const qr = renderQR(invitePayloadJson); console.log( qr .split("\n") .map((line) => " " + line) .join("\n"), ); console.log(""); console.log(" Or share this link:"); console.log(` ${c.cyan(invite.inviteURL)}`); console.log(""); if (showToken) { console.log(c.yellow(" ⚠️ Manual token display enabled (--show-token)")); console.log(c.dim(" Owner token:")); console.log(` ${c.dim(storage.getToken() ?? "(none)")}`); console.log(""); } return true; } async function cmdPair( storage: Storage, requestedName: string | undefined, hostOverride?: string, showToken = false, jsonOutput = false, ): Promise { if (jsonOutput) { try { const invite = generateInvite( storage, (override) => resolveInviteHost(storage.getConfig(), override), shortHostLabel, { hostOverride, requestedName }, ); process.stdout.write(JSON.stringify(invite, null, 2) + "\n"); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); process.stderr.write(`Error: ${message}\n`); process.exit(1); } return; } printHeader(); if (!showPairingQR(storage, requestedName, hostOverride, showToken)) { process.exit(1); } } function cmdStatus(storage: Storage): void { printHeader(); const config = storage.getConfig(); const hostname = getTailscaleHostname(); const ip = getTailscaleIp(); const localHostname = getLocalHostname(); const localIp = getLocalIp(); console.log(" " + c.bold("Server Configuration")); console.log(""); const tlsMode = config.tls?.mode ?? "disabled"; const transportScheme = tlsSchemeForConfig(config); console.log(` Port: ${config.port}`); console.log(` Transport: ${transportScheme.toUpperCase()} (${tlsMode})`); console.log(` Data: ${c.dim(storage.getDataDir())}`); console.log(""); console.log(" " + c.bold("Local Network")); console.log(""); if (localHostname || localIp) { console.log(` Hostname: ${localHostname || c.dim("unknown")}`); console.log(` IP: ${localIp || c.dim("unknown")}`); } else { console.log(` Status: ${c.yellow("No active LAN interface detected")}`); } console.log(""); console.log(" " + c.bold("Tailscale")); console.log(""); if (hostname) { console.log(` Hostname: ${c.green(hostname)}`); console.log(` IP: ${ip || c.dim("unknown")}`); } else { console.log(` Status: ${c.dim("Not connected")}`); } console.log(""); console.log(" " + c.bold("Pairing")); console.log(""); if (!storage.isPaired()) { console.log(c.dim(" Not paired")); console.log(c.dim(" Run 'oppi pair'")); } else { const sessions = storage.listSessions(); console.log(` Status: ${c.green("Paired")}`); console.log(` Sessions: ${sessions.length}`); } console.log(""); } function isLoopbackHost(host: string): boolean { const normalized = host.trim().toLowerCase(); return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1"; } function cmdDoctor(storage: Storage): void { printHeader(); type CheckLevel = "pass" | "warn" | "fail"; type Check = { level: CheckLevel; message: string }; const checks: Check[] = []; const config = storage.getConfig(); const host = config.host; const loopback = isLoopbackHost(host); if (!loopback && !config.token) { checks.push({ level: "fail", message: `non-loopback bind (${host}) without token configured`, }); } else if (config.token) { checks.push({ level: "pass", message: "auth token configured" }); } else { checks.push({ level: "warn", message: "no token configured (loopback-only bind)" }); } try { const mode = statSync(storage.getConfigPath()).mode & 0o777; if ((mode & 0o077) !== 0) { checks.push({ level: "warn", message: `config file permissions are ${mode.toString(8)} (recommend 600)`, }); } else { checks.push({ level: "pass", message: "config file permissions are private" }); } } catch { checks.push({ level: "warn", message: "could not inspect config file permissions" }); } try { const mode = statSync(storage.getDataDir()).mode & 0o777; if ((mode & 0o077) !== 0) { checks.push({ level: "warn", message: `data dir permissions are ${mode.toString(8)} (recommend 700)`, }); } else { checks.push({ level: "pass", message: "data dir permissions are private" }); } } catch { checks.push({ level: "warn", message: "could not inspect data directory permissions" }); } const runtimeEnv = resolveHostEnv(config); const runtimePath = runtimeEnv.env.PATH || ""; const runtimePathEntries = runtimePath.split(":").filter(Boolean).length; checks.push({ level: runtimePathEntries > 0 ? "pass" : "warn", message: runtimePathEntries > 0 ? `runtime PATH has ${runtimePathEntries} configured entries` : "runtime PATH is empty (configure runtimePathEntries in config)", }); const piPath = resolveExecutableOnPath("pi", runtimePath); if (piPath) { checks.push({ level: "pass", message: `pi executable found (${piPath})` }); } else { checks.push({ level: "warn", message: "pi executable not found in runtime PATH" }); } const tls = resolveTlsConfig(config, storage.getDataDir()); if (!tls.enabled) { checks.push({ level: loopback ? "pass" : "warn", message: loopback ? "TLS disabled (loopback-only bind)" : `TLS disabled while binding to ${config.host}`, }); } else { checks.push({ level: "pass", message: `TLS mode configured (${tls.mode})` }); if (tls.mode === "tailscale") { const tailscaleHostname = getTailscaleHostname(); if (tailscaleHostname) { checks.push({ level: "pass", message: `Tailscale hostname detected (${tailscaleHostname})`, }); } else { checks.push({ level: "fail", message: "Tailscale hostname not detected (tailscale status --json)", }); } } if (!tls.certPath) { checks.push({ level: "fail", message: "tls.certPath is not configured" }); } else if (!existsSync(tls.certPath)) { checks.push({ level: "fail", message: `TLS cert missing: ${tls.certPath}` }); } else { checks.push({ level: "pass", message: `TLS cert found (${tls.certPath})` }); try { const expiresAt = readCertificateExpiryMs(tls.certPath); const msRemaining = expiresAt - Date.now(); const daysRemaining = Math.floor(msRemaining / (24 * 60 * 60 * 1000)); if (msRemaining <= 0) { checks.push({ level: "fail", message: "TLS certificate is expired" }); } else if (daysRemaining <= 14) { checks.push({ level: "warn", message: `TLS certificate expires in ${daysRemaining} day(s)`, }); } else { checks.push({ level: "pass", message: `TLS certificate valid for ${daysRemaining} more day(s)`, }); } } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); checks.push({ level: "warn", message: `could not read TLS certificate expiry (${message})`, }); } try { const fingerprint = readCertificateFingerprint(tls.certPath); checks.push({ level: "pass", message: `TLS cert fingerprint ${fingerprint}` }); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); checks.push({ level: "warn", message: `could not read TLS cert fingerprint (${message})` }); } } if (!tls.keyPath) { checks.push({ level: "fail", message: "tls.keyPath is not configured" }); } else if (!existsSync(tls.keyPath)) { checks.push({ level: "fail", message: `TLS key missing: ${tls.keyPath}` }); } else { checks.push({ level: "pass", message: `TLS key found (${tls.keyPath})` }); } if (tls.mode === "self-signed") { if (!tls.caPath) { checks.push({ level: "fail", message: "tls.caPath is not configured for self-signed mode", }); } else if (!existsSync(tls.caPath)) { checks.push({ level: "fail", message: `TLS CA missing: ${tls.caPath}` }); } else { checks.push({ level: "pass", message: `TLS CA found (${tls.caPath})` }); } } } let criticalFailures = 0; for (const check of checks) { if (check.level === "pass") { console.log(` ${c.green("✓")} ${check.message}`); } else if (check.level === "warn") { console.log(` ${c.yellow("!")} ${check.message}`); } else { criticalFailures++; console.log(` ${c.red("✗")} ${check.message}`); } } console.log(""); if (criticalFailures > 0) { console.log(c.red(` Doctor failed: ${criticalFailures} critical issue(s)`)); console.log(""); process.exit(1); } console.log(c.green(" Doctor passed (no critical issues)")); console.log(""); } function cmdToken(storage: Storage, action: string | undefined): void { printHeader(); const mode = action || "help"; if (mode === "rotate") { if (!storage.isPaired()) { console.log(c.red(" Error: server is not paired yet.")); console.log(c.dim(" Run 'oppi pair' first to generate owner credentials.")); console.log(""); process.exit(1); } storage.rotateToken(); console.log(c.green(" ✓ Bearer token rotated.")); console.log(""); console.log(c.yellow(" Existing clients will be unauthorized until re-paired.")); console.log(c.dim(" Next step: run 'oppi pair' to issue a fresh invite.")); console.log(""); return; } console.log(c.red(` Unknown token action: ${mode}`)); console.log(c.dim(" Usage: oppi token rotate")); console.log(""); process.exit(1); } // ─── Prompt Helper ─── function prompt(question: string, defaultValue?: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout }); const suffix = defaultValue ? c.dim(` [${defaultValue}]`) : ""; return new Promise((resolve) => { rl.question(` ${question}${suffix}: `, (answer) => { rl.close(); resolve(answer.trim() || defaultValue || ""); }); }); } // ─── Init Command ─── async function cmdInit(flags: Record): Promise { printHeader(); console.log(c.bold(" First-time setup")); console.log(""); const { homedir } = await import("node:os"); const dataDir = flags["data-dir"] || join(homedir(), ".config", "oppi"); const alreadyExists = existsSync(join(dataDir, "config.json")); const nonInteractive = flags.yes === "true" || flags.y === "true" || !process.stdin.isTTY; if (alreadyExists && flags.force !== "true") { console.log(c.yellow(` Config already exists at ${dataDir}/config.json`)); console.log(c.dim(" Use --force to re-initialize (keeps existing data).")); console.log(""); if (nonInteractive) { process.exit(1); } const answer = await prompt("Continue anyway? (y/N)", "n"); if (answer.toLowerCase() !== "y") { console.log(""); return; } console.log(""); } let port: number; let defaultModel: string; let maxSessionsGlobal: number; if (nonInteractive) { // Non-interactive: use flags or defaults port = parseInt(flags.port || "7749") || 7749; defaultModel = flags.model || "openai-codex/gpt-5.3-codex"; maxSessionsGlobal = parseInt(flags["max-sessions"] || "40") || 40; console.log(c.dim(` Port: ${port}`)); console.log(c.dim(` Model: ${defaultModel}`)); console.log(c.dim(` Max sessions: ${maxSessionsGlobal}`)); console.log(""); } else { // Interactive prompts const portStr = await prompt("Port", "7749"); port = parseInt(portStr) || 7749; console.log(""); console.log(c.dim(" Popular models:")); console.log(c.dim(" openai-codex/gpt-5.3-codex")); console.log(c.dim(" anthropic/claude-opus-4-6")); console.log(c.dim(" anthropic/claude-sonnet-4-20250514")); console.log(""); defaultModel = await prompt("Default model", "openai-codex/gpt-5.3-codex"); const maxSessionsStr = await prompt("Max concurrent sessions", "40"); maxSessionsGlobal = parseInt(maxSessionsStr) || 40; } // Create storage (auto-creates dirs + default config) const storage = new Storage(dataDir); // Apply user choices + generate owner token so `oppi serve` can bind to 0.0.0.0. // Default to self-signed TLS so first `oppi serve` boots HTTPS/WSS out of the box. storage.updateConfig({ port, defaultModel, maxSessionsGlobal, tls: { mode: "self-signed" }, }); storage.rotateToken(); console.log(""); console.log(c.green(" ✓ Config written to ") + c.dim(storage.getConfigPath())); console.log(c.green(" ✓ Owner token generated")); console.log(c.green(" ✓ TLS mode set to self-signed (cert generated on first serve)")); // 4. Generate identity keys ensureIdentityMaterial(identityConfigForDataDir(storage.getDataDir())); console.log(c.green(" ✓ Identity keys generated")); // 5. Summary console.log(""); console.log(c.bold(" Next steps:")); console.log(""); console.log( ` ${c.cyan("1.")} oppi serve ${c.dim("Start the server (HTTPS/WSS)")}`, ); console.log( ` ${c.cyan("2.")} oppi pair ${c.dim('"YourName"')} ${c.dim("Generate pairing QR")}`, ); console.log(` ${c.cyan("3.")} Scan QR in Oppi app ${c.dim("Connect your phone")}`); console.log(""); } // ─── Config Command ─── /** Settable config keys and their types for `oppi config set`. */ const SETTABLE_KEYS: Record< string, { type: "number" | "string" | "boolean" | "json"; desc: string } > = { port: { type: "number", desc: "Server port" }, host: { type: "string", desc: "Bind address" }, defaultModel: { type: "string", desc: "Default model for new sessions" }, maxSessionsGlobal: { type: "number", desc: "Max concurrent sessions" }, maxSessionsPerWorkspace: { type: "number", desc: "Max sessions per workspace" }, sessionIdleTimeoutMs: { type: "number", desc: "Session idle timeout (ms)" }, workspaceIdleTimeoutMs: { type: "number", desc: "Workspace idle timeout (ms)" }, approvalTimeoutMs: { type: "number", desc: "Permission approval timeout (ms)" }, runtimePathEntries: { type: "json", desc: "Runtime PATH entries JSON array" }, runtimeEnv: { type: "json", desc: "Runtime env JSON object" }, tls: { type: "json", desc: "TLS config JSON (mode/certPath/keyPath/caPath)" }, subagents: { type: "json", desc: "Subagent config JSON (maxDepth/autoStopWhenDone/startupGraceMs/defaultWaitTimeoutMs)", }, }; function coerceValue(raw: string, type: "number" | "string" | "boolean" | "json"): unknown { switch (type) { case "number": { const n = Number(raw); if (isNaN(n)) throw new Error(`"${raw}" is not a valid number`); return n; } case "boolean": { const lower = raw.toLowerCase(); if (["true", "1", "yes", "on"].includes(lower)) return true; if (["false", "0", "no", "off"].includes(lower)) return false; throw new Error(`"${raw}" is not a valid boolean`); } case "string": return raw; case "json": { try { return JSON.parse(raw); } catch { throw new Error(`"${raw}" is not valid JSON`); } } } } function cmdConfig( storage: Storage, action: string | undefined, positional: string[], flags: Record, ): void { const mode = action || "show"; // `get` is machine-readable — no header if (mode === "get") { const key = positional[0]; if (!key) { console.log(c.red(" Usage: oppi config get ")); console.log(""); process.exit(1); } const config = storage.getConfig() as unknown as Record; const value = config[key]; if (value === undefined) { console.error(`Unknown key: ${key}`); process.exit(1); } if (typeof value === "object") { console.log(JSON.stringify(value, null, 2)); } else { console.log(String(value)); } return; } printHeader(); if (mode === "show") { const showDefault = flags.default === "true"; const config = showDefault ? Storage.getDefaultConfig(storage.getDataDir()) : storage.getConfig(); console.log(` ${c.bold(showDefault ? "Default config" : "Current config")}`); console.log(""); const pretty = JSON.stringify(config, null, 2) .split("\n") .map((line) => ` ${line}`) .join("\n"); console.log(pretty); console.log(""); return; } if (mode === "validate") { const target = flags["config-file"] || storage.getConfigPath(); const result = Storage.validateConfigFile(target); if (!result.valid) { console.log(c.red(` ✗ Config validation failed: ${target}`)); console.log(""); for (const err of result.errors) { console.log(c.red(` - ${err}`)); } console.log(""); process.exit(1); } console.log(c.green(` ✓ Config valid: ${target}`)); if (result.warnings.length > 0) { console.log(""); for (const warning of result.warnings) { console.log(c.yellow(` ! ${warning}`)); } } console.log(""); return; } if (mode === "set") { const key = positional[0]; const value = positional[1]; if (!key || value === undefined) { console.log(c.red(" Usage: oppi config set ")); console.log(""); console.log(c.bold(" Available keys:")); console.log(""); for (const [k, meta] of Object.entries(SETTABLE_KEYS)) { const current = (storage.getConfig() as unknown as Record)[k]; console.log(` ${c.cyan(k.padEnd(28))} ${c.dim(meta.desc)}`); console.log(` ${"".padEnd(28)} ${c.dim("current:")} ${current}`); } console.log(""); process.exit(1); } const meta = SETTABLE_KEYS[key]; if (!meta) { console.log(c.red(` Unknown config key: ${key}`)); console.log(c.dim(` Available: ${Object.keys(SETTABLE_KEYS).join(", ")}`)); console.log(""); process.exit(1); } try { const coerced = coerceValue(value, meta.type); storage.updateConfig({ [key]: coerced } as Partial); console.log(c.green(` ✓ ${key} = ${coerced}`)); console.log(c.dim(` Saved to ${storage.getConfigPath()}`)); console.log(""); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); console.log(c.red(` ✗ ${message}`)); console.log(""); process.exit(1); } return; } console.log(c.red(` Unknown config action: ${mode}`)); console.log(c.dim(" Usage: oppi config [show|get|set|validate]")); console.log(""); process.exit(1); } function cmdHelp(): void { printHeader(); console.log(" " + c.bold("Getting Started:")); console.log(""); console.log(` ${c.cyan("init")} Interactive first-time setup`); console.log(` ${c.cyan("serve")} Start the server`); console.log(` ${c.cyan("pair")} Generate pairing QR for server owner`); console.log(""); console.log(" " + c.bold("Server:")); console.log(""); console.log(` ${c.cyan("status")} Show server status`); console.log(` ${c.cyan("doctor")} Security + environment diagnostics`); console.log(` ${c.cyan("token rotate")} Rotate owner bearer token`); console.log(""); console.log(" " + c.bold("Configuration:")); console.log(""); console.log(` ${c.cyan("config show")} Show current config`); console.log(` ${c.cyan("config set")} Update a config value`); console.log(` ${c.cyan("config get")} Get a config value`); console.log(` ${c.cyan("config validate")} Validate config file`); console.log(""); console.log(" " + c.bold("Options:")); console.log(""); console.log(` ${c.dim("--host ")} Hostname/IP encoded in pairing QR`); console.log(` ${c.dim("--json")} Output invite as JSON (pair command)`); console.log(` ${c.dim("--show-token")} Print owner token in pair output (unsafe)`); console.log(` ${c.dim("--config-file

")} Config path for 'config validate'`); console.log(""); console.log(" " + c.bold("Examples:")); console.log(""); console.log(` ${c.dim("oppi init")}`); console.log(` ${c.dim("oppi serve")}`); console.log(` ${c.dim('oppi config set defaultModel "openai-codex/gpt-5.3-codex"')}`); console.log(` ${c.dim("oppi config set port 8080")}`); console.log(` ${c.dim('oppi config set tls \'{"mode":"self-signed"}\'')}`); console.log(""); } // ─── Main ─── async function main(): Promise { const args = process.argv.slice(2); const command = args[0] || "help"; // Parse flags const flags: Record = {}; const positional: string[] = []; for (let i = 1; i < args.length; i++) { if (args[i].startsWith("--")) { const key = args[i].slice(2); const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true"; flags[key] = value; } else { positional.push(args[i]); } } // These commands run before Storage to avoid creating default config prematurely if (command === "init") { await cmdInit(flags); return; } if (command === "help" || command === "--help" || command === "-h") { cmdHelp(); return; } const storage = new Storage(process.env.OPPI_DATA_DIR || undefined); switch (command) { case "serve": case "start": await cmdServe(storage, flags.host); break; case "pair": await cmdPair( storage, positional[0], flags.host, flags["show-token"] === "true", flags.json === "true", ); break; case "status": cmdStatus(storage); break; case "doctor": cmdDoctor(storage); break; case "token": cmdToken(storage, positional[0]); break; case "config": cmdConfig(storage, positional[0], positional.slice(1), flags); break; default: console.log(c.red(`Unknown command: ${command}`)); console.log(c.dim("Run 'oppi help' for usage.")); process.exit(1); } } main().catch((err) => { console.error(c.red("Fatal error:"), err); process.exit(1); });