Compare commits
3 Commits
c4c0703bee
...
c68c9b5e0d
| Author | SHA1 | Date | |
|---|---|---|---|
| c68c9b5e0d | |||
| b8c9603c71 | |||
| 3c6cd05ef7 |
59
src/vm.ts
59
src/vm.ts
|
|
@ -9,7 +9,13 @@ const DEBUG = !!process.env.DEBUG
|
|||
const CONTAINER_NAME = "sandlot"
|
||||
const USER = "ubuntu"
|
||||
const CLAUDE_BIN = `/home/${USER}/.local/bin/claude`
|
||||
const CONTAINER_PATH = `/home/${USER}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
|
||||
const CONTAINER_PATH = `/sandlot/.cargo/bin:/sandlot/.go/bin:/sandlot/.gopath/bin:/home/${USER}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
|
||||
const CONTAINER_ENV = {
|
||||
RUSTUP_HOME: "/sandlot/.rustup",
|
||||
CARGO_HOME: "/sandlot/.cargo",
|
||||
GOROOT: "/sandlot/.go",
|
||||
GOPATH: "/sandlot/.gopath",
|
||||
}
|
||||
|
||||
/** Translate a host path to its corresponding container path. */
|
||||
export function containerPath(hostPath: string): string {
|
||||
|
|
@ -74,8 +80,8 @@ async function createContainer(home: string): Promise<void> {
|
|||
/** Install base system packages (as root). */
|
||||
async function installPackages(cached: boolean): Promise<void> {
|
||||
const packages = cached
|
||||
? "curl git fish"
|
||||
: "curl git fish unzip"
|
||||
? "curl git fish make"
|
||||
: "curl git fish unzip make"
|
||||
await run(
|
||||
$`container exec ${CONTAINER_NAME} bash -c ${`apt update && apt install -y ${packages}`}`,
|
||||
"Package installation")
|
||||
|
|
@ -142,6 +148,41 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro
|
|||
|
||||
// Cache binaries for next time
|
||||
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp ~/.local/bin/bun ~/.local/bin/claude ~/.local/bin/neofetch /sandlot/.cache/ && cp /tmp/nvim.tar.gz /sandlot/.cache/nvim.tar.gz"}`.nothrow().quiet()
|
||||
|
||||
await installPersistentTooling(log)
|
||||
}
|
||||
|
||||
/** Install Rust and Go to /sandlot/ so they persist across container recreates. */
|
||||
async function installPersistentTooling(log?: (msg: string) => void): Promise<void> {
|
||||
// Rust — skip if already installed on the persistent mount
|
||||
const hasRust = await $`container exec --user ${USER} ${CONTAINER_NAME} test -x /sandlot/.cargo/bin/rustc`.nothrow().quiet()
|
||||
if (hasRust.exitCode !== 0) {
|
||||
log?.("Installing Rust")
|
||||
await run(
|
||||
$`container exec --user ${USER} ${CONTAINER_NAME} env ${`RUSTUP_HOME=${CONTAINER_ENV.RUSTUP_HOME}`} ${`CARGO_HOME=${CONTAINER_ENV.CARGO_HOME}`} bash -c ${"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"}`,
|
||||
"Rust installation")
|
||||
// Add musl target so Rust can link without a system C linker
|
||||
await run(
|
||||
$`container exec --user ${USER} ${CONTAINER_NAME} env ${`RUSTUP_HOME=${CONTAINER_ENV.RUSTUP_HOME}`} ${`CARGO_HOME=${CONTAINER_ENV.CARGO_HOME}`} ${`PATH=${CONTAINER_ENV.CARGO_HOME}/bin:$PATH`} rustup target add aarch64-unknown-linux-musl`,
|
||||
"Rust musl target")
|
||||
}
|
||||
|
||||
// Ensure cargo config exists for musl target (even if Rust was already installed)
|
||||
const hasCargoConfig = await $`container exec --user ${USER} ${CONTAINER_NAME} test -f /sandlot/.cargo/config.toml`.nothrow().quiet()
|
||||
if (hasCargoConfig.exitCode !== 0) {
|
||||
const cargoConfig = `[target.aarch64-unknown-linux-musl]\nlinker = "rust-lld"\n\n[build]\ntarget = "aarch64-unknown-linux-musl"\n`
|
||||
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${`echo -e '${cargoConfig}' > /sandlot/.cargo/config.toml`}`.quiet()
|
||||
}
|
||||
|
||||
// Go — skip if already installed on the persistent mount
|
||||
const hasGo = await $`container exec --user ${USER} ${CONTAINER_NAME} test -x /sandlot/.go/bin/go`.nothrow().quiet()
|
||||
if (hasGo.exitCode !== 0) {
|
||||
log?.("Installing Go")
|
||||
await run(
|
||||
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p /sandlot/.go && curl -fsSL https://go.dev/dl/go1.24.1.linux-arm64.tar.gz | tar xz -C /sandlot/.go --strip-components=1"}`,
|
||||
"Go installation")
|
||||
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p /sandlot/.gopath"}`.nothrow().quiet()
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a script to a temp file, copy it into ~/.local/bin/ in the container, then clean up. */
|
||||
|
|
@ -273,6 +314,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
|
|||
systemPromptLines.push(
|
||||
"The host's ~/.sandlot is mounted at /sandlot.",
|
||||
"Bun is installed at ~/.local/bin/bun. Use bun instead of node/npm.",
|
||||
"Rust (cargo/rustc) is installed at /sandlot/.cargo/. Go is installed at /sandlot/.go/.",
|
||||
)
|
||||
if (opts?.print) {
|
||||
systemPromptLines.push("IMPORTANT: Do not use plan mode. Do not call the EnterPlanMode tool. Proceed directly with the task.")
|
||||
|
|
@ -280,7 +322,8 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
|
|||
const systemPrompt = systemPromptLines.join("\n")
|
||||
|
||||
const term = process.env.TERM || "xterm-256color"
|
||||
const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", `TERM=${term}`, `PATH=${CONTAINER_PATH}`, CLAUDE_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--append-system-prompt", systemPrompt]
|
||||
const envArgs = [`TERM=${term}`, `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)]
|
||||
const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", ...envArgs, CLAUDE_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--append-system-prompt", systemPrompt]
|
||||
if (opts?.continue) args.push("--continue")
|
||||
if (opts?.print) args.push("-p", opts.print)
|
||||
else if (opts?.prompt) args.push(opts.prompt)
|
||||
|
|
@ -309,7 +352,8 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
|
|||
export async function shell(workdir?: string): Promise<void> {
|
||||
const args = ["container", "exec", "-it", "--user", USER]
|
||||
if (workdir) args.push("--workdir", containerPath(workdir))
|
||||
args.push(CONTAINER_NAME, "env", "TERM=xterm-256color", `PATH=${CONTAINER_PATH}`, "fish", "--login")
|
||||
const envArgs = ["TERM=xterm-256color", `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)]
|
||||
args.push(CONTAINER_NAME, "env", ...envArgs, "fish", "--login")
|
||||
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "inherit", stderr: "inherit" })
|
||||
await proc.exited
|
||||
}
|
||||
|
|
@ -317,7 +361,7 @@ export async function shell(workdir?: string): Promise<void> {
|
|||
/** Run neofetch in the container. */
|
||||
export async function neofetch(): Promise<void> {
|
||||
const proc = Bun.spawn(
|
||||
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, "neofetch"],
|
||||
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`), "neofetch"],
|
||||
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
||||
)
|
||||
await proc.exited
|
||||
|
|
@ -325,7 +369,8 @@ export async function neofetch(): Promise<void> {
|
|||
|
||||
/** Run a bash command in the container at the given workdir, capturing output. */
|
||||
export async function exec(workdir: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
const result = await $`container exec --user ${USER} --workdir ${containerPath(workdir)} ${CONTAINER_NAME} bash -c ${"export PATH=$HOME/.local/bin:$PATH; " + command}`.nothrow().quiet()
|
||||
const envExports = Object.entries(CONTAINER_ENV).map(([k, v]) => `export ${k}=${v}`).join("; ")
|
||||
const result = await $`container exec --user ${USER} --workdir ${containerPath(workdir)} ${CONTAINER_NAME} bash -c ${`export PATH=$HOME/.local/bin:$PATH; ${envExports}; ` + command}`.nothrow().quiet()
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString().trim(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user