Implements all 7 commands: new, save, push, list, open, stop, rm. Uses Commander for CLI parsing, Anthropic SDK for AI commit messages and merge conflict resolution, and Apple container for VMs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
6.1 KiB
Markdown
227 lines
6.1 KiB
Markdown
# sandlot
|
|
|
|
A CLI for branch-based development using git worktrees and Apple containers. Each branch gets its own worktree and isolated VM. Commits are auto-summarized by Claude. Merging cleans everything up.
|
|
|
|
## Concepts
|
|
|
|
**Sandlot** is a thin workflow layer over three things: git worktrees, [Apple containers](https://github.com/apple/container), and the Claude API. The idea is that spinning up a branch should give you a fully isolated environment — filesystem and runtime — with zero setup. When you're done, you merge and everything tears down.
|
|
|
|
A sandlot **session** is a (worktree, VM) pair tied to a branch. Sessions are created with `sandlot <branch>` and destroyed on merge.
|
|
|
|
## Tech Stack
|
|
|
|
- [Bun](https://bun.sh) runtime
|
|
- [Commander](https://github.com/tj/commander.js) for CLI parsing
|
|
- [Apple container](https://github.com/apple/container) for VMs
|
|
|
|
## Setup
|
|
|
|
### Prerequisites
|
|
|
|
- macOS on Apple Silicon
|
|
- [Bun](https://bun.sh) installed
|
|
- Git installed
|
|
- [container](https://github.com/apple/container) installed and available on PATH
|
|
|
|
### Install
|
|
|
|
```bash
|
|
git clone https://github.com/your/sandlot
|
|
cd sandlot
|
|
bun install
|
|
bun link
|
|
```
|
|
|
|
This makes `sandlot` available globally.
|
|
|
|
### Configure
|
|
|
|
Set your Anthropic API key:
|
|
|
|
```bash
|
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
```
|
|
|
|
Add to your shell profile (`.zshrc`, `.bashrc`, etc.) to persist it.
|
|
|
|
## CLI
|
|
|
|
### `sandlot new <branch>`
|
|
|
|
Create a new session. This:
|
|
|
|
1. Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD
|
|
2. Creates a git worktree at `.sandlot/<branch>/` (relative to the repo root)
|
|
3. Boots an Apple container VM mapped to that worktree
|
|
4. Drops you into the VM shell
|
|
|
|
```
|
|
$ sandlot new fix-POST
|
|
Creating worktree at .sandlot/fix-POST/
|
|
Booting VM...
|
|
root@fix-POST:~#
|
|
```
|
|
|
|
### `sandlot save [message]`
|
|
|
|
Stage and commit all changes. If a message is provided, use it. If not, generate one with Claude.
|
|
|
|
1. Runs `git add .` in the worktree
|
|
2. If no message provided: diffs staged changes against the last commit, sends the diff to Claude API (claude-sonnet-4-20250514) to generate a commit message
|
|
3. Commits with the message
|
|
4. Pushes the branch to origin
|
|
|
|
If there are no changes, does nothing.
|
|
|
|
The AI-generated commit message is a single-line summary (≤72 chars) followed by an optional body with more detail if the diff is substantial.
|
|
|
|
```
|
|
$ sandlot save
|
|
Staged 3 files
|
|
Commit: Fix POST handler to validate request body before processing
|
|
Pushed fix-POST → origin/fix-POST
|
|
|
|
$ sandlot save "wip: rough cut of validation"
|
|
Staged 3 files
|
|
Commit: wip: rough cut of validation
|
|
Pushed fix-POST → origin/fix-POST
|
|
```
|
|
|
|
### `sandlot push <target>`
|
|
|
|
Push the current session's branch into `<target>`, then tear everything down.
|
|
|
|
1. Checks out `<target>` in the main working tree
|
|
2. Merges the session branch (fast-forward if possible, merge commit otherwise)
|
|
3. Pushes `<target>` to origin
|
|
4. Stops and removes the VM
|
|
5. Removes the worktree
|
|
6. Deletes the local branch
|
|
7. Deletes the remote branch
|
|
|
|
If there are uncommitted changes in the worktree, prompts to `sandlot save` first.
|
|
|
|
If the merge has conflicts:
|
|
|
|
1. Collects all conflicted files
|
|
2. For each file, sends the full conflict diff (ours, theirs, and base) to the Claude API
|
|
3. Claude resolves the conflict and returns the merged file
|
|
4. Writes the resolved file and stages it
|
|
5. Shows a summary of what Claude chose and why
|
|
6. Prompts for confirmation before committing the merge
|
|
|
|
If you reject Claude's resolution, drops you into the main working tree to resolve manually. Run `sandlot push <target>` again to complete cleanup.
|
|
|
|
```
|
|
$ sandlot push main
|
|
Pushing fix-POST → main...
|
|
Pushed main → origin/main
|
|
Stopped VM fix-POST
|
|
Removed worktree .sandlot/fix-POST/
|
|
Deleted branch fix-POST (local + remote)
|
|
```
|
|
|
|
With conflicts:
|
|
|
|
```
|
|
$ sandlot push main
|
|
Pushing fix-POST → main...
|
|
2 conflicts found. Resolving with Claude...
|
|
|
|
src/handlers/post.ts
|
|
✓ Kept the new validation logic from fix-POST,
|
|
preserved the logging added in main
|
|
|
|
src/routes.ts
|
|
✓ Combined route definitions from both branches
|
|
|
|
Accept Claude's resolutions? [Y/n] y
|
|
Committed merge
|
|
Pushed main → origin/main
|
|
Stopped VM fix-POST
|
|
Removed worktree .sandlot/fix-POST/
|
|
Deleted branch fix-POST (local + remote)
|
|
```
|
|
|
|
### `sandlot list`
|
|
|
|
Show all active sessions.
|
|
|
|
```
|
|
$ sandlot list
|
|
BRANCH VM STATUS WORKTREE
|
|
fix-POST running .sandlot/fix-POST/
|
|
refactor-auth stopped .sandlot/refactor-auth/
|
|
```
|
|
|
|
### `sandlot open <branch>`
|
|
|
|
Re-enter an existing session's VM. If the VM is stopped, boots it first.
|
|
|
|
```
|
|
$ sandlot open fix-POST
|
|
Booting VM...
|
|
root@fix-POST:~#
|
|
```
|
|
|
|
### `sandlot stop <branch>`
|
|
|
|
Stop a session's VM without destroying it. The worktree and branch remain.
|
|
|
|
### `sandlot rm <branch>`
|
|
|
|
Tear down a session without merging. Stops the VM, removes the worktree, deletes the local branch. Does not touch the remote branch.
|
|
|
|
## Configuration
|
|
|
|
Optional `sandlot.json` at the repo root:
|
|
|
|
```json
|
|
{
|
|
"vm": {
|
|
"cpus": 4,
|
|
"memory": "8GB",
|
|
"image": "ubuntu:24.04",
|
|
"mounts": {
|
|
"/path/to/shared/deps": "/deps"
|
|
}
|
|
},
|
|
"ai": {
|
|
"model": "claude-sonnet-4-20250514"
|
|
}
|
|
}
|
|
```
|
|
|
|
## State
|
|
|
|
Sandlot tracks sessions in `.sandlot/state.json` at the repo root:
|
|
|
|
```json
|
|
{
|
|
"sessions": {
|
|
"fix-POST": {
|
|
"branch": "fix-POST",
|
|
"worktree": ".sandlot/fix-POST",
|
|
"vm_id": "container-abc123",
|
|
"created_at": "2026-02-16T10:30:00Z",
|
|
"status": "running"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`.sandlot/` should be added to `.gitignore`.
|
|
|
|
## Edge Cases
|
|
|
|
- **Nested sandlot calls**: `sandlot save` and `sandlot push` detect the current session from the working directory or a `SANDLOT_BRANCH` env var set when entering the VM.
|
|
- **Stale VMs**: If a VM crashes, `sandlot open` should detect the dead VM and reboot it.
|
|
- **Multiple repos**: State is per-repo. No global daemon.
|
|
- **Branch name conflicts**: If `.sandlot/<branch>/` already exists as a directory but the session state is missing, prompt to clean up or recover.
|
|
|
|
## Non-Goals
|
|
|
|
- Not a CI/CD tool. No pipelines, no test runners.
|
|
- Not a replacement for git. All git state lives in the real repo. Sandlot is sugar on top.
|
|
- No multi-user collaboration features. This is a single-developer workflow tool.
|