Compare commits

..

No commits in common. "83787ab8687de7423decd4e6e198e6fe75a999cd" and "003c42bc0318849710a96e8888f07f2f1524d657" have entirely different histories.

9 changed files with 33 additions and 178 deletions

View File

@ -11,7 +11,7 @@ function __sandlot_sessions
end
# All top-level subcommands (used for __fish_seen_subcommand_from checks)
set -l __sandlot_cmds new list open review shell close rm merge save diff show log dir cd init cleanup vm
set -l __sandlot_cmds new list open review shell close rm merge save diff show log dir cleanup vm
set -l __sandlot_vm_cmds create start shell status info stop destroy
# Disable file completions
@ -31,14 +31,12 @@ complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a diff
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a show -d 'Show prompt and diff for a branch'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a log -d 'Show commits not on main'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a dir -d 'Print the worktree path'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a cd -d 'Change to a branch worktree directory'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a init -d 'Print shell init script'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a cleanup -d 'Remove stale sessions'
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a vm -d 'Manage the sandlot VM'
# ── Session branch completions ───────────────────────────────────────
for cmd in open review shell close rm merge save diff show log dir cd
for cmd in open review shell close rm merge save diff show log dir
complete -c sandlot -n "__fish_seen_subcommand_from $cmd; and not __fish_seen_subcommand_from vm" -a '(__sandlot_sessions)'
end
@ -73,9 +71,5 @@ complete -c sandlot -n "__fish_seen_subcommand_from vm; and not __fish_seen_subc
# ── Global options ───────────────────────────────────────────────────
# ── init shell completions ───────────────────────────────────────
complete -c sandlot -n '__fish_seen_subcommand_from init' -a 'fish bash zsh' -d 'Shell type'
complete -c sandlot -s h -l help -d 'Show help'
complete -c sandlot -s V -l version -d 'Show version'

View File

@ -1,6 +1,6 @@
{
"name": "@because/sandlot",
"version": "0.0.25",
"version": "0.0.24",
"description": "Sandboxed, branch-based development with Claude",
"type": "module",
"bin": {

View File

@ -24,8 +24,6 @@ import { action as editAction } from "./commands/edit.ts"
import { action as cleanupAction } from "./commands/cleanup.ts"
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"
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
@ -174,12 +172,6 @@ program
.description("Print the worktree path for a session")
.action(dirAction)
program
.command("cd")
.argument("<branch>", "branch name")
.description("Change to a branch's worktree directory")
.action(cdAction)
// ── Admin ───────────────────────────────────────────────────────────
program.commandsGroup("Admin Commands:")
@ -216,12 +208,6 @@ program
.description("Output fish shell completions")
.action((opts: { install?: boolean }) => completionsAction(program, opts))
program
.command("init")
.argument("<shell>", "shell type (fish, bash, zsh)")
.description("Print shell init script (eval in your shell config)")
.action((shell: string) => initAction(program, shell))
// ── Default: `sandlot` → `sandlot list` ─────────────────────────────
if (process.argv.length === 2) {

View File

@ -1,7 +0,0 @@
import { die } from "../fmt.ts"
export function action(): never {
die(
`"sandlot cd" requires shell integration.\n\nAdd one of these to your shell config:\n\n Fish (~/.config/fish/config.fish):\n sandlot init fish | source\n\n Bash (~/.bashrc):\n eval "$(sandlot init bash)"\n\n Zsh (~/.zshrc):\n eval "$(sandlot init zsh)"`
)
}

View File

@ -1,9 +1,22 @@
import type { Command, Option } from "commander"
/** Generate fish completion lines from the Commander program tree. */
export function generateFishCompletions(program: Command): string[] {
/** Generate fish completions dynamically from the Commander program tree. */
export function action(program: Command, opts: { install?: boolean } = {}) {
if (opts.install) {
const dest = "~/.config/fish/completions/sandlot.fish"
const lines = [
"#!/bin/sh",
`mkdir -p ~/.config/fish/completions`,
`sandlot completions > ${dest}`,
`echo "Installed fish completions to ${dest}"`,
]
process.stdout.write(lines.join("\n") + "\n")
return
}
const lines: string[] = [
"# Fish completions for sandlot (auto-generated)",
"# Install: sandlot completions > ~/.config/fish/completions/sandlot.fish",
"",
"complete -c sandlot -f",
"",
@ -52,26 +65,6 @@ export function generateFishCompletions(program: Command): string[] {
}
lines.push("")
return lines
}
/** Generate fish completions dynamically from the Commander program tree. */
export function action(program: Command, opts: { install?: boolean } = {}) {
if (opts.install) {
const dest = "~/.config/fish/completions/sandlot.fish"
const lines = [
"#!/bin/sh",
`mkdir -p ~/.config/fish/completions`,
`sandlot completions > ${dest}`,
`echo "Installed fish completions to ${dest}"`,
]
process.stdout.write(lines.join("\n") + "\n")
return
}
const lines = generateFishCompletions(program)
// Add install hint at the top
lines.splice(1, 0, "# Install: sandlot completions > ~/.config/fish/completions/sandlot.fish")
process.stdout.write(lines.join("\n") + "\n")
}

View File

@ -1,88 +0,0 @@
import type { Command } from "commander"
import { die } from "../fmt.ts"
import { generateFishCompletions } from "./completions.ts"
export function action(program: Command, shell: string) {
switch (shell) {
case "fish":
return emitFish(program)
case "bash":
return emitBash(program)
case "zsh":
return emitZsh(program)
default:
die(`Unsupported shell: ${shell}. Supported shells: fish, bash, zsh`)
}
}
function emitFish(program: Command) {
const lines: string[] = [
"function sandlot --wraps sandlot --description 'Sandlot CLI wrapper'",
" if test (count $argv) -ge 1; and test \"$argv[1]\" = cd",
" set -l dir (command sandlot dir $argv[2..])",
" and cd $dir",
" else",
" command sandlot $argv",
" end",
"end",
"",
...generateFishCompletions(program),
]
process.stdout.write(lines.join("\n") + "\n")
}
function emitBash(program: Command) {
const { subcommands, branchCommands } = collectCommands(program)
const lines: string[] = [
"sandlot() {",
' if [ "$#" -ge 1 ] && [ "$1" = "cd" ]; then',
" local dir",
' dir="$(command sandlot dir "${@:2}")" && cd "$dir"',
" else",
' command sandlot "$@"',
" fi",
"}",
"",
"_sandlot_completions() {",
' local cur prev',
' cur="${COMP_WORDS[COMP_CWORD]}"',
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
"",
" if [ \"$COMP_CWORD\" -eq 1 ]; then",
` COMPREPLY=( $(compgen -W "${subcommands.join(" ")}" -- "$cur") )`,
" return",
" fi",
"",
` case "$prev" in`,
` ${branchCommands.join("|")})`,
' local branches',
' branches="$(command sandlot list --json 2>/dev/null | grep -o \'"branch": *"[^"]*"\' | sed \'s/.*"\\([^"]*\\)"$/\\1/\')"',
' COMPREPLY=( $(compgen -W "$branches" -- "$cur") )',
" return",
" ;;",
" esac",
"}",
"complete -F _sandlot_completions sandlot",
]
process.stdout.write(lines.join("\n") + "\n")
}
function emitZsh(program: Command) {
// Prepend bashcompinit setup, then emit the same bash script
process.stdout.write("autoload -Uz bashcompinit && bashcompinit\n")
emitBash(program)
}
function collectCommands(program: Command): { subcommands: string[]; branchCommands: string[] } {
const subcommands: string[] = []
const branchCommands: string[] = []
for (const cmd of program.commands) {
if ((cmd as any)._hidden) continue
subcommands.push(cmd.name())
const hasBranch = cmd.registeredArguments.some(a => a.name() === "branch")
if (hasBranch) branchCommands.push(cmd.name())
}
return { subcommands, branchCommands }
}

View File

@ -46,6 +46,7 @@ export async function action(opts: { json?: boolean }) {
// Determine status for each session in parallel
const statusEntries = await Promise.all(
sessions.map(async (s): Promise<[string, string]> => {
if (!(await git.isValidWorktree(s.worktree))) return [s.branch, "stale"]
if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"]
const dirty = await git.isDirty(s.worktree)
if (dirty) return [s.branch, "dirty"]
@ -61,8 +62,8 @@ export async function action(opts: { json?: boolean }) {
return
}
const icons: Record<string, string> = { idle: `${dim}${reset}`, active: `${cyan}${reset}`, dirty: `${yellow}${reset}`, saved: `${green}${reset}` }
const branchColors: Record<string, string> = { idle: dim, active: cyan, dirty: yellow, saved: green }
const icons: Record<string, string> = { idle: `${dim}${reset}`, active: `${cyan}${reset}`, dirty: `${yellow}${reset}`, saved: `${green}${reset}`, stale: `${red}${reset}` }
const branchColors: Record<string, string> = { idle: dim, active: cyan, dirty: yellow, saved: green, stale: red }
const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
const cols = process.stdout.columns || 80
const prefixWidth = branchWidth + 4
@ -70,7 +71,7 @@ export async function action(opts: { json?: boolean }) {
console.log(` ${dim}${"BRANCH".padEnd(branchWidth)} PROMPT${reset}`)
for (const s of sessions) {
const prompt = (s.prompt ?? "").split("\n")[0]
const prompt = status === "stale" ? "broken worktree — close with -f to clean up" : (s.prompt ?? "").split("\n")[0]
const status = statuses[s.branch]
const icon = icons[status]
const bc = branchColors[status]
@ -79,7 +80,9 @@ export async function action(opts: { json?: boolean }) {
console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`)
}
console.log(`\n${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`)
const hasStale = Object.values(statuses).includes("stale")
const legend = `${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`
console.log(`\n${legend}${hasStale ? ` · ${red}✖ stale${reset} (run ${dim}sandlot close -f <branch>${reset} to clean up)` : ""}`)
if ((await vm.status()) !== "running") {
console.log(`\n${red}VM is not running.${reset}`)

View File

@ -29,39 +29,6 @@ Description of problem.
Tell each agent: Run \`git diff main...HEAD\` and focus on the "+" lines, not the "-" lines.
Once the agents are done, look at all their suggestions and let me know what you think.
Give me your opinion in this format, with the :
<grumpySeniorDevResponse>
# {branch name} Review
**OK TO SHIP: yes or no**
# Showstoppers
1. BUG: Bug that both bug hunters found.
2. BUG: Bug that one of the hunters found.
3. COMPLIANCE: Describe CLAUDE.md compliance issue.
# Recommendations
4. BUG: Bug that both bug hunters found.
5. BUG: Bug that one of the hunters found.
6. COMPLIANCE: Describe CLAUDE.md compliance issue.
7. SIMPLIFY: Opportunities to simplify code.
# Optional
8. BUG: Bug that both bug hunters found.
9. BUG: Bug that one of the hunters found.
10. COMPLIANCE: Describe CLAUDE.md compliance issue.
11. SIMPLIFY: Opportunities to simplify code.
# Summary
Your thoughts, in brief.
</grumpySeniorDevResponse>
`
if (extra) prompt += "\n\n" + extra

View File

@ -189,6 +189,12 @@ export async function rebaseAbort(cwd: string): Promise<void> {
}
/** Check if a worktree has uncommitted changes. */
/** Check if a worktree path is a valid git directory. */
export async function isValidWorktree(worktreePath: string): Promise<boolean> {
const result = await $`git -C ${worktreePath} rev-parse --git-dir`.nothrow().quiet()
return result.exitCode === 0
}
export async function isDirty(worktreePath: string): Promise<boolean> {
const result = await $`git -C ${worktreePath} status --porcelain`.nothrow().quiet()
if (result.exitCode !== 0) return false
@ -227,7 +233,8 @@ export async function fileDiff(ref1: string, ref2: string, file: string, cwd: st
/** Check if a branch has commits beyond main. */
export async function hasNewCommits(worktreePath: string): Promise<boolean> {
const main = await mainBranch(worktreePath)
let main: string
try { main = await mainBranch(worktreePath) } catch { return false }
const result = await $`git -C ${worktreePath} rev-list ${main}..HEAD --count`.nothrow().quiet()
if (result.exitCode !== 0) return false
return parseInt(result.text().trim(), 10) > 0