Derive config keys from DEFAULTS and enforce minimum memory of 512M

The config command previously maintained a separate key list and used a
switch statement that would need updating for each new key. Deriving
VALID_KEYS from config.DEFAULTS keeps them in sync automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Wanstrath 2026-03-20 10:40:22 -07:00
parent fa077ff8f5
commit 3b3820f95f
3 changed files with 21 additions and 21 deletions

View File

@ -1,8 +1,7 @@
import { die } from "../fmt.ts"
import * as config from "../config.ts"
const VALID_KEYS = ["memory"] as const
type Key = (typeof VALID_KEYS)[number]
const VALID_KEYS = Object.keys(config.DEFAULTS) as config.Key[]
export async function action(args: string[]) {
if (args.length === 0) {
@ -16,25 +15,18 @@ export async function action(args: string[]) {
}
const [key, ...rest] = args
if (!VALID_KEYS.includes(key as Key)) die(`Unknown config key: ${key}\nAvailable keys: ${VALID_KEYS.join(", ")}`)
if (!VALID_KEYS.includes(key as config.Key)) die(`Unknown config key: ${key}\nAvailable keys: ${VALID_KEYS.join(", ")}`)
if (rest.length === 0) {
const val = await config.get(key as Key)
console.log(val ?? `${config.DEFAULTS[key as Key]} (default)`)
const val = await config.get(key as config.Key)
console.log(val ?? `${config.DEFAULTS[key as config.Key]} (default)`)
return
}
if (rest.length > 1) die(`Too many arguments. Usage: sandlot config ${key} <value>`)
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}`)
}
let normalized: string
try { normalized = config.validateMemory(rest[0]) } catch { return die("Must be a number followed by G or M, minimum 512M (e.g. 16G)") }
await config.set("memory", normalized)
console.log(`memory = ${normalized}`)
}

View File

@ -1,19 +1,28 @@
import { mkdir } from "fs/promises"
import { homedir } from "os"
import { dirname, join } from "path"
import { join } from "path"
const CONFIG_PATH = join(homedir(), ".config", "sandlot", "config.json")
const CONFIG_DIR = join(homedir(), ".config", "sandlot")
const CONFIG_PATH = join(CONFIG_DIR, "config.json")
export const DEFAULTS = {
memory: "16G",
} as const
export type Key = keyof typeof DEFAULTS
export interface Config {
memory?: string
}
const MIN_MEMORY_MB = 512
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)`)
const num = parseInt(v)
const unit = v.slice(-1).toUpperCase()
const mb = unit === "G" ? num * 1024 : num
if (mb < MIN_MEMORY_MB) throw new Error(`Memory too low: ${v} (minimum ${MIN_MEMORY_MB}M)`)
return v.toUpperCase()
}
@ -30,7 +39,7 @@ export async function load(): Promise<Config> {
}
export async function save(config: Config): Promise<void> {
await mkdir(dirname(CONFIG_PATH), { recursive: true })
await mkdir(CONFIG_DIR, { recursive: true })
await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n")
}

View File

@ -67,9 +67,8 @@ 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 raw = (await getConfig("memory")) ?? DEFAULTS.memory
let memory: string
try { memory = validateMemory(raw) } catch { memory = DEFAULTS.memory }
try { memory = validateMemory((await getConfig("memory")) ?? DEFAULTS.memory) } 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`)