diff --git a/src/cli.ts b/src/cli.ts index 7a03bdd..56c14f5 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -117,6 +117,14 @@ program const vmCmd = program.command("vm").description("Manage the sandlot VM") +vmCmd + .command("shell") + .description("Open a shell in the VM") + .action(async () => { + await vm.ensure() + await vm.shell() + }) + vmCmd .command("status") .description("Show VM status") diff --git a/src/vm.ts b/src/vm.ts index 32abcd2..d9a356b 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -19,7 +19,10 @@ export async function ensure(): Promise { await $`limactl start ${VM_NAME}`.quiet() // Provision - await $`limactl shell ${VM_NAME} -- sudo bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs && npm install -g @anthropic-ai/claude-code"`.quiet() + await $`limactl shell ${VM_NAME} -- bash -c "curl -fsSL https://claude.ai/install.sh | bash"`.quiet() + + // Replace ~/.claude and ~/.claude.json with symlinks to host credentials + await $`limactl shell ${VM_NAME} -- bash -c "rm -rf ~/.claude ~/.claude.json && ln -sf ${home}/.claude ~/.claude && ln -sf ${home}/.claude.json ~/.claude.json"`.quiet() } /** Check VM status. */ @@ -41,10 +44,33 @@ export async function status(): Promise<"running" | "stopped" | "missing"> { return "missing" } +/** Load env vars from ~/.env */ +async function loadEnv(): Promise> { + const envFile = Bun.file(`${homedir()}/.env`) + if (!(await envFile.exists())) return {} + const vars: Record = {} + for (const line of (await envFile.text()).split("\n")) { + const match = line.match(/^([A-Z_]+)=(.+)$/) + if (match) vars[match[1]] = match[2] + } + return vars +} + /** Launch claude in the VM at the given workdir. */ export async function claude(workdir: string): Promise { + const env = await loadEnv() + const envArgs = Object.entries(env).map(([k, v]) => `${k}=${v}`) const proc = Bun.spawn( - ["limactl", "shell", `--workdir=${workdir}`, VM_NAME, "claude", "--dangerously-skip-permissions"], + ["limactl", "shell", `--workdir=${workdir}`, VM_NAME, "env", ...envArgs, "claude", "--dangerously-skip-permissions"], + { stdin: "inherit", stdout: "inherit", stderr: "inherit" }, + ) + await proc.exited +} + +/** Open an interactive shell in the VM. */ +export async function shell(): Promise { + const proc = Bun.spawn( + ["limactl", "shell", VM_NAME], { stdin: "inherit", stdout: "inherit", stderr: "inherit" }, ) await proc.exited