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