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`
- 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/…`)

View File

@ -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} <value>`)
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}`)
}
}

View File

@ -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<Config> {
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<void> {

View File

@ -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<void> {
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`)