Compare commits
6 Commits
0f568e69de
...
f7a8ff6a3c
| Author | SHA1 | Date | |
|---|---|---|---|
| f7a8ff6a3c | |||
| 3b3820f95f | |||
| fa077ff8f5 | |||
| ea1b09dc8e | |||
| e3e4419933 | |||
| f7d876776b |
|
|
@ -36,7 +36,9 @@ 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
|
||||||
|
|
@ -60,7 +62,7 @@ src/
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Each module has a single responsibility. No classes — only exported async functions.
|
Each module has a single responsibility. No classes — only exported functions (async when they do I/O, sync when pure).
|
||||||
|
|
||||||
**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>`
|
||||||
|
|
@ -79,7 +81,7 @@ Each module has a single responsibility. No classes — only exported async func
|
||||||
|
|
||||||
- Image: `ubuntu:24.04`
|
- Image: `ubuntu:24.04`
|
||||||
- User: `ubuntu`
|
- User: `ubuntu`
|
||||||
- Memory limit: 4G
|
- Memory limit: configurable via `sandlot config memory` (default 16G)
|
||||||
- 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/…`)
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -45,3 +45,17 @@ 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) |
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ 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()
|
||||||
|
|
||||||
|
|
@ -185,6 +186,12 @@ 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")
|
||||||
|
|
|
||||||
32
src/commands/config.ts
Normal file
32
src/commands/config.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
55
src/config.ts
Normal file
55
src/config.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ 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"
|
||||||
|
|
@ -66,7 +67,9 @@ 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)
|
||||||
const args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", "32G"]
|
let memory: string
|
||||||
|
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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user