Compare commits

..

No commits in common. "f7a8ff6a3c283bbb08621eb3a9a7c48263d63eeb" and "0f568e69de011bab917c00fc62a8791fb56db530" have entirely different histories.

6 changed files with 3 additions and 116 deletions

View File

@ -36,9 +36,7 @@ src/
fmt.ts # ANSI escape codes, die/success/info helpers, pager fmt.ts # ANSI escape codes, die/success/info helpers, pager
markdown.ts # Terminal markdown renderer (headings, bold, links, code blocks) markdown.ts # Terminal markdown renderer (headings, bold, links, code blocks)
spinner.ts # CLI progress spinner (braille frames) spinner.ts # CLI progress spinner (braille frames)
config.ts # Per-user config (~/.config/sandlot/config.json): memory, etc.
commands/ commands/
config.ts # Get/set config values (e.g. `sandlot config memory 16G`)
new.ts # Create session, derive branch name from prompt new.ts # Create session, derive branch name from prompt
open.ts # Re-enter existing session (always uses --continue) open.ts # Re-enter existing session (always uses --continue)
close.ts # Remove worktree and clean up session close.ts # Remove worktree and clean up session
@ -62,7 +60,7 @@ src/
## Architecture ## Architecture
Each module has a single responsibility. No classes — only exported functions (async when they do I/O, sync when pure). Each module has a single responsibility. No classes — only exported async functions.
**Session flow for `sandlot new <branch>`:** **Session flow for `sandlot new <branch>`:**
1. `git.createWorktree()` → creates worktree at `~/.sandlot/<repo>/<branch>` 1. `git.createWorktree()` → creates worktree at `~/.sandlot/<repo>/<branch>`
@ -81,7 +79,7 @@ Each module has a single responsibility. No classes — only exported functions
- Image: `ubuntu:24.04` - Image: `ubuntu:24.04`
- User: `ubuntu` - User: `ubuntu`
- Memory limit: configurable via `sandlot config memory` (default 16G) - Memory limit: 4G
- Mounts: `~/dev` **read-only** at `/host`, `~/.sandlot` read-write at `/sandlot` - 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 - 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/…`) - `containerPath()` in `vm.ts` translates host paths to container paths (`~/.sandlot/…` → `/sandlot/…`, `~/dev/…``/host/…`)

View File

@ -45,17 +45,3 @@ sandlot rm <branch> # tear down session (container, worktree, local branch)
``` ```
Use git directly for commits, pushes, merges, etc. The worktree is a normal git checkout. 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) |

View File

@ -26,7 +26,6 @@ import { register as registerVmCommands } from "./commands/vm.ts"
import { action as completionsAction } from "./commands/completions.ts" import { action as completionsAction } from "./commands/completions.ts"
import { action as initAction } from "./commands/init.ts" import { action as initAction } from "./commands/init.ts"
import { action as cdAction } from "./commands/cd.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() const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
@ -186,12 +185,6 @@ program
program.commandsGroup("Admin Commands:") 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 program
.command("cleanup") .command("cleanup")
.description("Remove stale sessions whose worktrees no longer exist") .description("Remove stale sessions whose worktrees no longer exist")

View File

@ -1,32 +0,0 @@
import { die } from "../fmt.ts"
import * as config from "../config.ts"
const VALID_KEYS = Object.keys(config.DEFAULTS) as config.Key[]
export async function action(args: string[]) {
if (args.length === 0) {
const cfg = await config.load()
for (const key of VALID_KEYS) {
const val = cfg[key]
const display = val ?? `${config.DEFAULTS[key]} (default)`
console.log(`${key} = ${display}`)
}
return
}
const [key, ...rest] = args
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 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>`)
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(key as config.Key, normalized)
console.log(`${key} = ${normalized}`)
}

View File

@ -1,55 +0,0 @@
import { mkdir } from "fs/promises"
import { homedir } from "os"
import { join } from "path"
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()
}
export async function load(): Promise<Config> {
const file = Bun.file(CONFIG_PATH)
if (!(await file.exists())) return {}
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> {
await mkdir(CONFIG_DIR, { 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)
}

View File

@ -4,7 +4,6 @@ import { homedir } from "os"
import { dirname, join } from "path" import { dirname, join } from "path"
import { requireApiKey } from "./env.ts" import { requireApiKey } from "./env.ts"
import { info } from "./fmt.ts" import { info } from "./fmt.ts"
import { get as getConfig, DEFAULTS, validateMemory } from "./config.ts"
const DEBUG = !!process.env.DEBUG const DEBUG = !!process.env.DEBUG
const CONTAINER_NAME = "sandlot" const CONTAINER_NAME = "sandlot"
@ -67,9 +66,7 @@ function hostMounts(home: string): { dev: boolean; code: boolean } {
/** Pull the image and start the container in detached mode. */ /** Pull the image and start the container in detached mode. */
async function createContainer(home: string): Promise<void> { async function createContainer(home: string): Promise<void> {
const mounts = hostMounts(home) const mounts = hostMounts(home)
let memory: string const args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", "32G"]
try { memory = validateMemory((await getConfig("memory")) ?? DEFAULTS.memory) } catch (e) { info(`Invalid memory config, using default: ${e instanceof Error ? e.message : e}`); 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.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`) 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") args.push("-v", `${home}/.sandlot:/sandlot`, "ubuntu:24.04", "sleep", "infinity")