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 <noreply@anthropic.com>
This commit is contained in:
Chris Wanstrath 2026-03-20 10:36:48 -07:00
parent ea1b09dc8e
commit fa077ff8f5
4 changed files with 27 additions and 15 deletions

View File

@ -81,7 +81,7 @@ Each module has a single responsibility. No classes — only exported async func
- Image: `ubuntu:24.04` - Image: `ubuntu:24.04`
- User: `ubuntu` - 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` - 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 - 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/…`) - `containerPath()` in `vm.ts` translates host paths to container paths (`~/.sandlot/…` → `/sandlot/…`, `~/dev/…``/host/…`)

View File

@ -4,11 +4,6 @@ import * as config from "../config.ts"
const VALID_KEYS = ["memory"] as const const VALID_KEYS = ["memory"] as const
type Key = (typeof VALID_KEYS)[number] 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[]) { export async function action(args: string[]) {
if (args.length === 0) { if (args.length === 0) {
const cfg = await config.load() 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} <value>`) if (rest.length > 1) die(`Too many arguments. Usage: sandlot config ${key} <value>`)
if (key === "memory") { switch (key) {
const normalized = validateMemory(rest[0]) case "memory": {
await config.set("memory", normalized) let normalized: string
console.log(`memory = ${normalized}`) 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}`)
} }
} }

View File

@ -12,12 +12,21 @@ export interface Config {
memory?: string 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<Config> { export async function load(): Promise<Config> {
const file = Bun.file(CONFIG_PATH) const file = Bun.file(CONFIG_PATH)
if (!(await file.exists())) return {} if (!(await file.exists())) return {}
const raw = await file.json() try {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {} const raw = await file.json()
return raw as Config if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {}
return raw as Config
} catch {
return {}
}
} }
export async function save(config: Config): Promise<void> { export async function save(config: Config): Promise<void> {

View File

@ -4,7 +4,7 @@ import { homedir } from "os"
import { dirname, join } from "path" import { dirname, join } from "path"
import { requireApiKey } from "./env.ts" import { requireApiKey } from "./env.ts"
import { info } from "./fmt.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 DEBUG = !!process.env.DEBUG
const CONTAINER_NAME = "sandlot" 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. */ /** Pull the image and start the container in detached mode. */
async function createContainer(home: string): Promise<void> { async function createContainer(home: string): Promise<void> {
const mounts = hostMounts(home) 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] 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.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`) if (mounts.code) args.push("--mount", `type=bind,source=${home}/code,target=/host/code,readonly`)