From e3e4419933e27dd6f083d72d1edaf0729acb0348 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 20 Mar 2026 10:24:57 -0700 Subject: [PATCH] Refactor config command and fix default memory limit Move defaults and normalization into KEYS metadata, validate config file shape on load, and lower default container memory from 32G to 16G. Co-Authored-By: Claude Opus 4.6 --- README.md | 14 ++++++++++++++ src/commands/config.ts | 33 ++++++++++++++++++--------------- src/config.ts | 14 ++++++++------ src/vm.ts | 2 +- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d39fcca..adc9ee5 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,17 @@ sandlot rm # tear down session (container, worktree, local branch) ``` Use git directly for commits, pushes, merges, etc. The worktree is a normal git checkout. + +### Configuration + +```bash +sandlot config # show all options and current values +sandlot config memory # show current memory setting +sandlot config memory 32G # set container memory limit +``` + +Config is stored in `~/.config/sandlot/config.json`. + +| Key | Default | Description | +|-----|---------|-------------| +| `memory` | `16G` | Container memory limit (e.g. 4G, 16G, 32G, 64G) | diff --git a/src/commands/config.ts b/src/commands/config.ts index 24610dc..f53b390 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,38 +1,41 @@ import { die } from "../fmt.ts" import * as config from "../config.ts" -const KEYS: Record string | null }> = { +const KEYS: Record string; validate: (v: string) => string | null }> = { memory: { + default: "16G", description: "Container memory limit (e.g. 4G, 16G, 32G, 64G)", - validate: (v) => /^\d+[GMgm]$/.test(v) ? null : "Must be a number followed by G or M (e.g. 16G)", + normalize: (v) => v.toUpperCase(), + validate: (v) => /^[1-9]\d*[GMgm]$/.test(v) ? null : "Must be a number followed by G or M (e.g. 16G)", }, } export async function action(args: string[]) { if (args.length === 0) { const cfg = await config.load() - const defaults: Record = { memory: "32G" } for (const [key, meta] of Object.entries(KEYS)) { - const val = (cfg as any)[key] - const display = val ?? `${defaults[key]} (default)` + const val = cfg[key as keyof config.Config] + const display = val ?? `${meta.default} (default)` console.log(`${key} = ${display}`) + console.log(` ${meta.description}`) } return } - if (args.length === 1) { - const key = args[0] - if (!(key in KEYS)) die(`Unknown config key: ${key}\nAvailable keys: ${Object.keys(KEYS).join(", ")}`) + const [key, ...rest] = args + if (!(key in KEYS)) die(`Unknown config key: ${key}\nAvailable keys: ${Object.keys(KEYS).join(", ")}`) + + if (rest.length === 0) { const val = await config.get(key as keyof config.Config) - const defaults: Record = { memory: "32G" } - console.log(val ?? `${defaults[key]} (default)`) + console.log(val ?? `${KEYS[key].default} (default)`) return } - const [key, value] = args - if (!(key in KEYS)) die(`Unknown config key: ${key}\nAvailable keys: ${Object.keys(KEYS).join(", ")}`) - const error = KEYS[key].validate(value) + if (rest.length > 1) die(`Too many arguments. Usage: sandlot config ${key} `) + const meta = KEYS[key] + const error = meta.validate(rest[0]) if (error) die(error) - await config.set(key as keyof config.Config, value.toUpperCase()) - console.log(`${key} = ${value.toUpperCase()}`) + const normalized = meta.normalize(rest[0]) + await config.set(key as keyof config.Config, normalized) + console.log(`${key} = ${normalized}`) } diff --git a/src/config.ts b/src/config.ts index 69584b9..d547247 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import { mkdir } from "fs/promises" import { homedir } from "os" import { dirname, join } from "path" @@ -8,15 +9,16 @@ export interface Config { } export async function load(): Promise { - try { - return await Bun.file(CONFIG_PATH).json() - } catch { - return {} - } + 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 {} + const cfg: Config = {} + if (typeof raw.memory === "string") cfg.memory = raw.memory + return cfg } export async function save(config: Config): Promise { - const { mkdir } = await import("fs/promises") await mkdir(dirname(CONFIG_PATH), { recursive: true }) await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n") } diff --git a/src/vm.ts b/src/vm.ts index 9b2b6d2..3475bcf 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -67,7 +67,7 @@ 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")) ?? "32G" + const memory = (await getConfig("memory")) ?? "16G" 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`)