diff --git a/CLAUDE.md b/CLAUDE.md index 622589d..5b60dac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,7 +81,7 @@ Each module has a single responsibility. No classes — only exported async func - Image: `ubuntu:24.04` - User: `ubuntu` -- Memory limit: configurable via `sandlot config memory` (default 32G) +- Memory limit: configurable via `sandlot config memory` (default 16G) - Mounts: `~/dev` **read-only** at `/host`, `~/.sandlot` read-write at `/sandlot` - Host symlinks: creates `~/dev` → `/host` and `~/.sandlot` → `/sandlot` inside the container so host-absolute worktree paths resolve correctly - `containerPath()` in `vm.ts` translates host paths to container paths (`~/.sandlot/…` → `/sandlot/…`, `~/dev/…` → `/host/…`) diff --git a/src/commands/config.ts b/src/commands/config.ts index ad3471d..b084c7d 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -4,11 +4,6 @@ import * as config from "../config.ts" const VALID_KEYS = ["memory"] as const type Key = (typeof VALID_KEYS)[number] -function validateMemory(v: string): string { - if (!/^[1-9]\d*[GMgm]$/.test(v)) die("Must be a number followed by G or M (e.g. 16G)") - return v.toUpperCase() -} - export async function action(args: string[]) { if (args.length === 0) { const cfg = await config.load() @@ -31,9 +26,15 @@ export async function action(args: string[]) { if (rest.length > 1) die(`Too many arguments. Usage: sandlot config ${key} `) - if (key === "memory") { - const normalized = validateMemory(rest[0]) - await config.set("memory", normalized) - console.log(`memory = ${normalized}`) + switch (key) { + case "memory": { + let normalized: string + try { normalized = config.validateMemory(rest[0]) } catch { die("Must be a number followed by G or M (e.g. 16G)") } + await config.set("memory", normalized!) + console.log(`memory = ${normalized!}`) + break + } + default: + die(`Unhandled config key: ${key}`) } } diff --git a/src/config.ts b/src/config.ts index 437b9e9..6c75bef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,12 +12,21 @@ export interface Config { memory?: string } +export function validateMemory(v: string): string { + if (!/^[1-9]\d*[GMgm]$/.test(v)) throw new Error(`Invalid memory value: ${v} (must be a number followed by G or M, e.g. 16G)`) + return v.toUpperCase() +} + export async function load(): Promise { const file = Bun.file(CONFIG_PATH) if (!(await file.exists())) return {} - const raw = await file.json() - if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {} - return raw as Config + try { + const raw = await file.json() + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {} + return raw as Config + } catch { + return {} + } } export async function save(config: Config): Promise { diff --git a/src/vm.ts b/src/vm.ts index b284313..3302469 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -4,7 +4,7 @@ import { homedir } from "os" import { dirname, join } from "path" import { requireApiKey } from "./env.ts" import { info } from "./fmt.ts" -import { get as getConfig, DEFAULTS } from "./config.ts" +import { get as getConfig, DEFAULTS, validateMemory } from "./config.ts" const DEBUG = !!process.env.DEBUG const CONTAINER_NAME = "sandlot" @@ -67,7 +67,9 @@ function hostMounts(home: string): { dev: boolean; code: boolean } { /** Pull the image and start the container in detached mode. */ async function createContainer(home: string): Promise { const mounts = hostMounts(home) - const memory = (await getConfig("memory")) ?? DEFAULTS.memory + const raw = (await getConfig("memory")) ?? DEFAULTS.memory + let memory: string + try { memory = validateMemory(raw) } catch { memory = DEFAULTS.memory } const args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", memory] if (mounts.dev) args.push("--mount", `type=bind,source=${home}/dev,target=/host/dev,readonly`) if (mounts.code) args.push("--mount", `type=bind,source=${home}/code,target=/host/code,readonly`)