diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..41e82e9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md + +## Project Overview + +**Sandlot** is a CLI tool for branch-based development using git worktrees and [Apple Container](https://github.com/apple/container). Each branch gets its own isolated worktree and container. The primary workflow is: create a session (`sandlot new `), do work with Claude Code inside the container, then merge and clean up. + +**Platform requirement**: macOS on Apple Silicon only. + +## Tech Stack + +- **Runtime**: [Bun](https://bun.sh) (not Node.js — use `bun` for everything) +- **Language**: TypeScript (strict, ESNext, bundler module resolution) +- **CLI parsing**: [Commander.js](https://github.com/tj/commander.js) 13.x +- **Containers**: Apple Container (`brew install container`) +- **Entry point**: `src/cli.ts` (shebang: `#!/usr/bin/env bun`, runs directly without compilation) + +## Commands + +```bash +bun install # install dependencies +bun link # make `sandlot` available globally +sandlot --help # list all commands +``` + +No build step. TypeScript runs directly via Bun. There are no tests. + +## Source Structure + +``` +src/ + cli.ts # CLI entry point, all command handlers (Commander.js) + git.ts # Git operations: worktrees, branches, merge + vm.ts # Container lifecycle: create, provision, exec, shell, claude + state.ts # Per-repo session persistence (.sandlot/state.json) + config.ts # Load optional sandlot.json config from repo root + spinner.ts # CLI progress spinner (braille frames) +``` + +## Architecture + +Each module has a single responsibility. No classes — only exported async functions. + +**Session flow for `sandlot new `:** +1. `git.createWorktree()` → creates worktree at `~/.sandlot//` +2. Creates symlink `/.sandlot/` → worktree path +3. `vm.ensure()` → start/create/provision the container +4. `state.setSession()` → write to `.sandlot/state.json` +5. `vm.claude()` → launch Claude Code in container at worktree path + +**Worktree location**: `~/.sandlot///` (outside the repo) +**Symlink in repo**: `/.sandlot/` → worktree +**State file**: `/.sandlot/state.json` +**Container name**: always `"sandlot"` (single shared container per machine) + +## Container Details + +- Image: `ubuntu:24.04` +- User: `ubuntu` +- Mounts: `~/dev` and `~/.sandlot` from host +- Provisioned once on first use: installs `curl git neofetch fish`, Claude Code, git identity, API key helper +- API key: read from `~/.env` on host (`ANTHROPIC_API_KEY=...`), written as `~/.claude/api-key-helper.sh` in the container (never passed as a process argument) +- Claude settings: `skipDangerousModePermissionPrompt: true` in container + +## Shell Command Pattern + +Uses Bun's `$` template literal for shell execution: + +```typescript +import { $ } from "bun" + +// Capture output +const text = await $`git rev-parse --show-toplevel`.cwd(dir).nothrow().quiet().text() + +// Suppress output, ignore failures +await $`git worktree prune`.cwd(cwd).nothrow().quiet() + +// Check exit code +const result = await $`git diff --staged --quiet`.nothrow().quiet() +if (result.exitCode === 0) { ... } +``` + +Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` to suppress stdout/stderr. + +## Configuration + +Optional `sandlot.json` at repo root (loaded by `config.ts`): + +```json +{ + "vm": { + "cpus": 4, + "memory": "8GB", + "image": "ubuntu:24.04", + "mounts": { "/host/path": "/container/path" } + } +} +``` + +Note: `config.ts` loads this but `vm.ts` does not yet use it — the container is hardcoded with `-m 4G` and `ubuntu:24.04`. + +## State Schema + +`.sandlot/state.json` (per repo, gitignored): + +```json +{ + "sessions": { + "branch-name": { + "branch": "branch-name", + "worktree": "/Users/you/.sandlot/repo/branch-name", + "created_at": "2026-02-16T10:30:00Z" + } + } +} +``` + +## Error Handling Conventions + +- Throw `Error` with descriptive messages from git/vm modules +- CLI commands catch and display errors, then `process.exit(1)` +- On `new` failure, roll back: remove worktree, delete branch, unlink symlink +- Non-fatal cleanup steps use `.catch(() => {})` to continue past failures + +## Key Implementation Notes + +- `vm.exec()` prepends `export PATH=$HOME/.local/bin:$PATH` so `claude` binary is found +- `vm.claude()` uses `Bun.spawn` with `stdin/stdout/stderr: "inherit"` for interactive TTY +- Branch creation in `createWorktree()` handles three cases: local branch, remote branch (tracks origin), new branch from HEAD +- `sandlot save` uses Claude (`claude -p "..."`) inside the container to generate commit messages +- `.sandlot/` should be in the repo's `.gitignore`