From fa077ff8f5fe8a0b3e0825c5e83c19627e3b50a9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 20 Mar 2026 10:36:48 -0700 Subject: [PATCH] Move validateMemory to config module and harden against bad values The validator is now reusable by both the CLI config command and the VM startup path, which falls back to the default if the stored value is invalid. Also lowers the default memory limit to 16G and makes config.load() resilient to malformed JSON. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- src/commands/config.ts | 19 ++++++++++--------- src/config.ts | 15 ++++++++++++--- src/vm.ts | 6 ++++-- 4 files changed, 27 insertions(+), 15 deletions(-) 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`)