Compare commits
No commits in common. "83787ab8687de7423decd4e6e198e6fe75a999cd" and "003c42bc0318849710a96e8888f07f2f1524d657" have entirely different histories.
83787ab868
...
003c42bc03
|
|
@ -11,7 +11,7 @@ function __sandlot_sessions
|
||||||
end
|
end
|
||||||
|
|
||||||
# All top-level subcommands (used for __fish_seen_subcommand_from checks)
|
# 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
|
set -l __sandlot_vm_cmds create start shell status info stop destroy
|
||||||
|
|
||||||
# Disable file completions
|
# 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 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 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 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 cleanup -d 'Remove stale sessions'
|
||||||
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a vm -d 'Manage the sandlot VM'
|
complete -c sandlot -n "not __fish_seen_subcommand_from $__sandlot_cmds" -a vm -d 'Manage the sandlot VM'
|
||||||
|
|
||||||
# ── Session branch completions ───────────────────────────────────────
|
# ── 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)'
|
complete -c sandlot -n "__fish_seen_subcommand_from $cmd; and not __fish_seen_subcommand_from vm" -a '(__sandlot_sessions)'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -73,9 +71,5 @@ complete -c sandlot -n "__fish_seen_subcommand_from vm; and not __fish_seen_subc
|
||||||
|
|
||||||
# ── Global options ───────────────────────────────────────────────────
|
# ── 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 h -l help -d 'Show help'
|
||||||
complete -c sandlot -s V -l version -d 'Show version'
|
complete -c sandlot -s V -l version -d 'Show version'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@because/sandlot",
|
"name": "@because/sandlot",
|
||||||
"version": "0.0.25",
|
"version": "0.0.24",
|
||||||
"description": "Sandboxed, branch-based development with Claude",
|
"description": "Sandboxed, branch-based development with Claude",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
14
src/cli.ts
14
src/cli.ts
|
|
@ -24,8 +24,6 @@ import { action as editAction } from "./commands/edit.ts"
|
||||||
import { action as cleanupAction } from "./commands/cleanup.ts"
|
import { action as cleanupAction } from "./commands/cleanup.ts"
|
||||||
import { register as registerVmCommands } from "./commands/vm.ts"
|
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 cdAction } from "./commands/cd.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()
|
||||||
|
|
||||||
|
|
@ -174,12 +172,6 @@ program
|
||||||
.description("Print the worktree path for a session")
|
.description("Print the worktree path for a session")
|
||||||
.action(dirAction)
|
.action(dirAction)
|
||||||
|
|
||||||
program
|
|
||||||
.command("cd")
|
|
||||||
.argument("<branch>", "branch name")
|
|
||||||
.description("Change to a branch's worktree directory")
|
|
||||||
.action(cdAction)
|
|
||||||
|
|
||||||
// ── Admin ───────────────────────────────────────────────────────────
|
// ── Admin ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
program.commandsGroup("Admin Commands:")
|
program.commandsGroup("Admin Commands:")
|
||||||
|
|
@ -216,12 +208,6 @@ program
|
||||||
.description("Output fish shell completions")
|
.description("Output fish shell completions")
|
||||||
.action((opts: { install?: boolean }) => completionsAction(program, opts))
|
.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` ─────────────────────────────
|
// ── Default: `sandlot` → `sandlot list` ─────────────────────────────
|
||||||
|
|
||||||
if (process.argv.length === 2) {
|
if (process.argv.length === 2) {
|
||||||
|
|
|
||||||
|
|
@ -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)"`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
import type { Command, Option } from "commander"
|
import type { Command, Option } from "commander"
|
||||||
|
|
||||||
/** Generate fish completion lines from the Commander program tree. */
|
/** Generate fish completions dynamically from the Commander program tree. */
|
||||||
export function generateFishCompletions(program: Command): string[] {
|
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[] = [
|
const lines: string[] = [
|
||||||
"# Fish completions for sandlot (auto-generated)",
|
"# Fish completions for sandlot (auto-generated)",
|
||||||
|
"# Install: sandlot completions > ~/.config/fish/completions/sandlot.fish",
|
||||||
"",
|
"",
|
||||||
"complete -c sandlot -f",
|
"complete -c sandlot -f",
|
||||||
"",
|
"",
|
||||||
|
|
@ -52,26 +65,6 @@ export function generateFishCompletions(program: Command): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("")
|
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")
|
process.stdout.write(lines.join("\n") + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
|
|
@ -46,6 +46,7 @@ export async function action(opts: { json?: boolean }) {
|
||||||
// Determine status for each session in parallel
|
// Determine status for each session in parallel
|
||||||
const statusEntries = await Promise.all(
|
const statusEntries = await Promise.all(
|
||||||
sessions.map(async (s): Promise<[string, string]> => {
|
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"]
|
if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"]
|
||||||
const dirty = await git.isDirty(s.worktree)
|
const dirty = await git.isDirty(s.worktree)
|
||||||
if (dirty) return [s.branch, "dirty"]
|
if (dirty) return [s.branch, "dirty"]
|
||||||
|
|
@ -61,8 +62,8 @@ export async function action(opts: { json?: boolean }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons: Record<string, string> = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}` }
|
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 }
|
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 branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
|
||||||
const cols = process.stdout.columns || 80
|
const cols = process.stdout.columns || 80
|
||||||
const prefixWidth = branchWidth + 4
|
const prefixWidth = branchWidth + 4
|
||||||
|
|
@ -70,7 +71,7 @@ export async function action(opts: { json?: boolean }) {
|
||||||
console.log(` ${dim}${"BRANCH".padEnd(branchWidth)} PROMPT${reset}`)
|
console.log(` ${dim}${"BRANCH".padEnd(branchWidth)} PROMPT${reset}`)
|
||||||
|
|
||||||
for (const s of sessions) {
|
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 status = statuses[s.branch]
|
||||||
const icon = icons[status]
|
const icon = icons[status]
|
||||||
const bc = branchColors[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(`${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") {
|
if ((await vm.status()) !== "running") {
|
||||||
console.log(`\n${red}VM is not running.${reset}`)
|
console.log(`\n${red}VM is not running.${reset}`)
|
||||||
|
|
|
||||||
|
|
@ -29,39 +29,6 @@ Description of problem.
|
||||||
Tell each agent: Run \`git diff main...HEAD\` and focus on the "+" lines, not the "-" lines.
|
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.
|
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
|
if (extra) prompt += "\n\n" + extra
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,12 @@ export async function rebaseAbort(cwd: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a worktree has uncommitted changes. */
|
/** 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> {
|
export async function isDirty(worktreePath: string): Promise<boolean> {
|
||||||
const result = await $`git -C ${worktreePath} status --porcelain`.nothrow().quiet()
|
const result = await $`git -C ${worktreePath} status --porcelain`.nothrow().quiet()
|
||||||
if (result.exitCode !== 0) return false
|
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. */
|
/** Check if a branch has commits beyond main. */
|
||||||
export async function hasNewCommits(worktreePath: string): Promise<boolean> {
|
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()
|
const result = await $`git -C ${worktreePath} rev-list ${main}..HEAD --count`.nothrow().quiet()
|
||||||
if (result.exitCode !== 0) return false
|
if (result.exitCode !== 0) return false
|
||||||
return parseInt(result.text().trim(), 10) > 0
|
return parseInt(result.text().trim(), 10) > 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user