Add init and cd shell integration commands

This commit is contained in:
Chris Wanstrath 2026-03-11 14:27:19 -07:00
parent 4a465f7787
commit 83787ab868
5 changed files with 139 additions and 17 deletions

View File

@ -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 cleanup vm set -l __sandlot_cmds new list open review shell close rm merge save diff show log dir cd init 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,12 +31,14 @@ 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 for cmd in open review shell close rm merge save diff show log dir cd
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
@ -71,5 +73,9 @@ 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'

View File

@ -24,6 +24,8 @@ 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()
@ -172,6 +174,12 @@ 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:")
@ -208,6 +216,12 @@ 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) {

7
src/commands/cd.ts Normal file
View File

@ -0,0 +1,7 @@
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,22 +1,9 @@
import type { Command, Option } from "commander" import type { Command, Option } from "commander"
/** Generate fish completions dynamically from the Commander program tree. */ /** Generate fish completion lines from the Commander program tree. */
export function action(program: Command, opts: { install?: boolean } = {}) { export function generateFishCompletions(program: Command): string[] {
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",
"", "",
@ -65,6 +52,26 @@ export function action(program: Command, opts: { install?: boolean } = {}) {
} }
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")
} }

88
src/commands/init.ts Normal file
View File

@ -0,0 +1,88 @@
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 }
}