Add per-user config system with sandlot config command
Allow users to configure container memory limit (default 32G) via `sandlot config memory <value>` instead of hardcoding it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0f568e69de
commit
f7d876776b
|
|
@ -36,7 +36,9 @@ src/
|
|||
fmt.ts # ANSI escape codes, die/success/info helpers, pager
|
||||
markdown.ts # Terminal markdown renderer (headings, bold, links, code blocks)
|
||||
spinner.ts # CLI progress spinner (braille frames)
|
||||
config.ts # Per-user config (~/.config/sandlot/config.json): memory, etc.
|
||||
commands/
|
||||
config.ts # Get/set config values (e.g. `sandlot config memory 16G`)
|
||||
new.ts # Create session, derive branch name from prompt
|
||||
open.ts # Re-enter existing session (always uses --continue)
|
||||
close.ts # Remove worktree and clean up session
|
||||
|
|
@ -79,7 +81,7 @@ Each module has a single responsibility. No classes — only exported async func
|
|||
|
||||
- Image: `ubuntu:24.04`
|
||||
- User: `ubuntu`
|
||||
- Memory limit: 4G
|
||||
- Memory limit: configurable via `sandlot config memory` (default 32G)
|
||||
- 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/…`)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { register as registerVmCommands } from "./commands/vm.ts"
|
|||
import { action as completionsAction } from "./commands/completions.ts"
|
||||
import { action as initAction } from "./commands/init.ts"
|
||||
import { action as cdAction } from "./commands/cd.ts"
|
||||
import { action as configAction } from "./commands/config.ts"
|
||||
|
||||
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
|
||||
|
||||
|
|
@ -185,6 +186,12 @@ program
|
|||
|
||||
program.commandsGroup("Admin Commands:")
|
||||
|
||||
program
|
||||
.command("config")
|
||||
.argument("[args...]", "key [value]")
|
||||
.description("Get or set configuration (e.g. sandlot config memory 16G)")
|
||||
.action(configAction)
|
||||
|
||||
program
|
||||
.command("cleanup")
|
||||
.description("Remove stale sessions whose worktrees no longer exist")
|
||||
|
|
|
|||
38
src/commands/config.ts
Normal file
38
src/commands/config.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { die } from "../fmt.ts"
|
||||
import * as config from "../config.ts"
|
||||
|
||||
const KEYS: Record<string, { description: string; validate: (v: string) => string | null }> = {
|
||||
memory: {
|
||||
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)",
|
||||
},
|
||||
}
|
||||
|
||||
export async function action(args: string[]) {
|
||||
if (args.length === 0) {
|
||||
const cfg = await config.load()
|
||||
const defaults: Record<string, string> = { memory: "32G" }
|
||||
for (const [key, meta] of Object.entries(KEYS)) {
|
||||
const val = (cfg as any)[key]
|
||||
const display = val ?? `${defaults[key]} (default)`
|
||||
console.log(`${key} = ${display}`)
|
||||
}
|
||||
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 val = await config.get(key as keyof config.Config)
|
||||
const defaults: Record<string, string> = { memory: "32G" }
|
||||
console.log(val ?? `${defaults[key]} (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 (error) die(error)
|
||||
await config.set(key as keyof config.Config, value.toUpperCase())
|
||||
console.log(`${key} = ${value.toUpperCase()}`)
|
||||
}
|
||||
33
src/config.ts
Normal file
33
src/config.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { homedir } from "os"
|
||||
import { dirname, join } from "path"
|
||||
|
||||
const CONFIG_PATH = join(homedir(), ".config", "sandlot", "config.json")
|
||||
|
||||
export interface Config {
|
||||
memory?: string
|
||||
}
|
||||
|
||||
export async function load(): Promise<Config> {
|
||||
try {
|
||||
return await Bun.file(CONFIG_PATH).json()
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function save(config: Config): Promise<void> {
|
||||
const { mkdir } = await import("fs/promises")
|
||||
await mkdir(dirname(CONFIG_PATH), { recursive: true })
|
||||
await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n")
|
||||
}
|
||||
|
||||
export async function get<K extends keyof Config>(key: K): Promise<Config[K]> {
|
||||
const config = await load()
|
||||
return config[key]
|
||||
}
|
||||
|
||||
export async function set<K extends keyof Config>(key: K, value: Config[K]): Promise<void> {
|
||||
const config = await load()
|
||||
config[key] = value
|
||||
await save(config)
|
||||
}
|
||||
|
|
@ -4,6 +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 } from "./config.ts"
|
||||
|
||||
const DEBUG = !!process.env.DEBUG
|
||||
const CONTAINER_NAME = "sandlot"
|
||||
|
|
@ -66,7 +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 args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", "32G"]
|
||||
const memory = (await getConfig("memory")) ?? "32G"
|
||||
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`)
|
||||
args.push("-v", `${home}/.sandlot:/sandlot`, "ubuntu:24.04", "sleep", "infinity")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user