import { mkdirSync } from "node:fs"; import { hostname } from "node:os"; import { resolve } from "node:path"; import type { CommandModule } from "yargs"; import { generateToken, validateToken, TOKEN_REQUIREMENTS } from "../../shared/token"; import { readConfig, rememberedRoot, writeConfig } from "../config"; import { launchServeDaemon } from "../daemonize"; import { isGitRepo } from "../git"; import { scaffoldCodehost } from "../init"; import { confirmRiskyRoot } from "./serve"; import { resolveCodeBinary } from "../vscode-install"; import { announceConnect } from "../open-url"; import { DEFAULT_SIGNAL_URL } from "./serve"; import { hasGhCli, hasGitCredentials } from "../../provision/git-auth"; interface SetupArgs { dir?: string; token?: string; newToken: boolean; name?: string; signal: string; port?: number; approve: string; allow: string[]; } export const setupCommand: CommandModule<{}, SetupArgs> = { command: "setup [dir]", describe: "One-shot: pick a token, ensure VS Code, and start a daemonized server", builder: (y) => y .positional("dir", { describe: "Directory to serve (default: a git cwd serves itself, else the remembered root)", type: "string", }) .option("token", { alias: "t", describe: "Room token (generated + saved if omitted)", type: "string", }) .option("new-token", { describe: "Generate a fresh token even if one is saved", type: "boolean", default: false, }) .option("name", { describe: "Display name for this server (defaults to hostname)", type: "string" }) .option("signal", { describe: "Signaling server URL", type: "string", default: DEFAULT_SIGNAL_URL }) .option("port", { describe: "Fixed port for the local VS Code server", type: "number" }) .option("approve", { describe: "Client admission: 'auto' (anyone with the token) or 'confirm' (approve via --allow; the daemon has no terminal)", type: "string", choices: ["auto", "confirm"], default: "auto", }) .option("allow", { describe: "Under --approve confirm, auto-approve clients whose label matches (repeatable)", type: "string", array: true, default: [], }) as any, handler: async (argv) => { // Explicit dir > a git cwd (serve THIS repo) > remembered root. No // implicit ~/ws fallback: with nothing remembered and cwd not a git // repo, the dir must be given explicitly, once. let dir: string; if (argv.dir) { dir = resolve(process.cwd(), argv.dir); } else if (isGitRepo(process.cwd())) { dir = process.cwd(); } else { const remembered = rememberedRoot(); if (!remembered) { console.error("[codehost] no workspace root configured."); console.error("[codehost] run: codehost setup /path/to/workspace"); console.error("[codehost] this path will be remembered for future `codehost setup` calls."); process.exit(1); } dir = remembered; mkdirSync(dir, { recursive: true }); console.log(`[codehost] no dir given — using remembered workspace root ${dir}`); } if (!(await confirmRiskyRoot(dir))) { console.error("[codehost] aborted"); process.exit(1); } const host = hostname(); // 1. Resolve the room token: validate an explicit one, otherwise reuse the // saved token (stable room URL) or mint and persist a strong new one. const token = resolveToken(argv); console.log(`[codehost] room token: ${token}`); // 2. Make sure a working VS Code is available, installing/upgrading the // managed CLI if the system `code` is missing or broken. Doing it here // surfaces download progress before we hand off to the daemon. console.log("[codehost] checking VS Code…"); const codeBin = await resolveCodeBinary(); console.log(`[codehost] using VS Code: ${codeBin}`); // 3. Batteries included: a workspace root gets its `.codehost/` scaffold // (config.yaml + clone/worktree setup hook) so /// links // provision on demand out of the box. Existing files are never touched; // no new trust — the room token already grants code execution. const root = !isGitRepo(dir); if (root) { const written = scaffoldCodehost(dir); if (written.length > 0) { console.log(`[codehost] scaffolded ${dir}/.codehost (config.yaml + setup hook — edit freely)`); } // Remember an explicitly chosen root so future bare runs reuse it. if (argv.dir) writeConfig({ ...readConfig(), root: dir }); } // 4. Start the WebRTC + VS Code server under oxmgr. A git repo is a single // workspace (`dev`); anything else is treated as a root (`serve`). const { ok, name } = await launchServeDaemon({ command: root ? "serve" : "dev", dir, token, signal: argv.signal, name: argv.name, port: argv.port, host, approve: argv.approve, allow: argv.allow, }); if (!ok) process.exit(1); // 5. Close with what the user actually got and what to try first, and open // the browser straight at the token-carrying URL so VS Code loads // without typing the token in. console.log(""); console.log(`[codehost] ✓ server "${name}" is live, serving ${dir}`); announceConnect(token); console.log(""); console.log("[codehost] try it:"); console.log("[codehost] · on that page this machine appears as a host — Connect opens VS Code"); console.log("[codehost] · paste any github.com repo URL there — it clones into your workspace and opens"); console.log("[codehost] · another machine? run the same installer there, then “+ Add” its token on the page"); // Private-repo readiness: provisioning clones over plain https, so without // a credential helper those clones fail (with a hint, but better to say it // now than after the first broken open). if (hasGitCredentials()) { console.log("[codehost] ✓ GitHub auth: ready — private repos will clone"); } else { console.log("[codehost] ⚠ GitHub auth: none found — private repos won't clone until you run:"); console.log( hasGhCli() ? "[codehost] gh auth login && gh auth setup-git" : "[codehost] gh auth login && gh auth setup-git (install gh first: https://cli.github.com)", ); } console.log(`[codehost] manage: codehost list · codehost stop ${name}`); }, }; function resolveToken(argv: SetupArgs): string { if (argv.token) { const t = argv.token.trim(); const check = validateToken(t); if (!check.ok) { console.error(`[codehost] ${check.reason}`); console.error(`[codehost] room token requires: ${TOKEN_REQUIREMENTS}`); process.exit(1); } // Persist an explicit token too, so later `setup` runs reuse the same room. writeConfig({ ...readConfig(), token: t }); return t; } const config = readConfig(); if (config.token && !argv.newToken) return config.token; const token = generateToken(); writeConfig({ ...config, token }); console.log("[codehost] generated a new room token (saved to ~/.codehost/config.json)"); return token; }