Skip to content

Instantly share code, notes, and snippets.

@PaulKinlan
Forked from bfollington/worktree.sh
Last active January 11, 2026 22:25
Show Gist options
  • Select an option

  • Save PaulKinlan/c1a82bd8882dc1e72b9df6cabc3484ae to your computer and use it in GitHub Desktop.

Select an option

Save PaulKinlan/c1a82bd8882dc1e72b9df6cabc3484ae to your computer and use it in GitHub Desktop.
Worktree convenience aliases
. "/Users/paulkinlan/.deno/env"
# --- Configuration ---
# TODO: Paste your Z-AI API Key here
export ZAI_API_KEY=""
# --- Tmux Workflow Commands ---
c-begin() {
local name=$1
if [ -z "$name" ]; then
# Generate a rememberable name if none provided
local adjs=("azure" "crimson" "golden" "silent" "brave" "calm" "swift" "wise")
local nouns=("falcon" "tiger" "river" "ocean" "mountain" "wolf" "eagle" "bear")
local rand_adj=${adjs[$RANDOM % ${#adjs[@]}]}
local rand_noun=${nouns[$RANDOM % ${#nouns[@]}]}
name="${rand_adj}-${rand_noun}"
fi
# Check if session exists; if so, attach, otherwise create
if tmux has-session -t "$name" 2>/dev/null; then
echo "Session '$name' already exists. Attaching..."
tmux attach-session -t "$name"
else
tmux new-session -s "$name"
fi
}
c-resume() {
local name=$1
if [ -z "$name" ]; then
# If no name, attach to the most recently used session
tmux attach-session
else
tmux attach-session -t "$name"
fi
}
c-toggle-billing() {
if [ -n "$ANTHROPIC_API_KEY" ]; then
# Backup the key and unset it to force subscription usage
export ANTHROPIC_API_KEY_BACKUP="$ANTHROPIC_API_KEY"
unset ANTHROPIC_API_KEY
echo "๐Ÿ’ธ ANTHROPIC_API_KEY unset. Using Subscription billing."
elif [ -n "$ANTHROPIC_API_KEY_BACKUP" ]; then
# Restore the key from backup to use API billing
export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY_BACKUP"
unset ANTHROPIC_API_KEY_BACKUP
echo "๐Ÿ’ณ ANTHROPIC_API_KEY restored. Using API billing."
else
echo "โŒ No ANTHROPIC_API_KEY or backup found to toggle."
fi
}
c-toggle-ai() {
if [ -z "$ZAI_API_KEY" ]; then
echo "โŒ ZAI_API_KEY is not set. Please edit your .profile and set it."
return 1
fi
if ! command -v node >/dev/null 2>&1; then
echo "โŒ 'node' is required to update settings.json but was not found."
return 1
fi
local SETTINGS_FILE="$HOME/.claude/settings.json"
# Check if we are currently using Z-AI by looking for the URL in the file
if grep -q "api.z.ai" "$SETTINGS_FILE" 2>/dev/null; then
# Switch to Anthropic (Remove Z-AI config)
node -e "
const fs = require('fs');
const p = '$SETTINGS_FILE';
try {
let d = JSON.parse(fs.readFileSync(p, 'utf8'));
if(d.env) {
delete d.env.ANTHROPIC_AUTH_TOKEN;
delete d.env.ANTHROPIC_BASE_URL;
delete d.env.API_TIMEOUT_MS;
}
fs.writeFileSync(p, JSON.stringify(d, null, 2));
console.log('๐Ÿ”„ Switched to Anthropic (Default).');
} catch(e) { console.error('Error updating settings:', e); }
"
else
# Switch to Z-AI (Inject Z-AI config)
# Pass key via environment variable to node for safety
ZAI_KEY="$ZAI_API_KEY" node -e "
const fs = require('fs');
const p = '$SETTINGS_FILE';
try {
let d = {};
if (fs.existsSync(p)) { d = JSON.parse(fs.readFileSync(p, 'utf8')); }
d.env = d.env || {};
d.env.ANTHROPIC_AUTH_TOKEN = process.env.ZAI_KEY;
d.env.ANTHROPIC_BASE_URL = 'https://api.z.ai/api/anthropic';
d.env.API_TIMEOUT_MS = '3000000';
fs.writeFileSync(p, JSON.stringify(d, null, 2));
console.log('๐Ÿš€ Switched to Z-AI.');
} catch(e) { console.error('Error updating settings:', e); }
"
fi
}
c-do() {
local branch="$1"
local prompt="$2"
if [ -z "$TMUX" ]; then
echo "โš ๏ธ You need to be in a tmux session to use c-do."
echo "Run 'c-begin' to start one, or 'c-resume' to join an existing one."
return 1
fi
if [ -z "$branch" ]; then
echo "Usage: c-do <branch-name> <prompt>"
return 1
fi
# Check if a *window* already exists (tab style)
# If found, we switch to it rather than splitting the current view
if tmux list-windows -F '#{window_name}' | grep -q "^$branch$"; then
echo "Window '$branch' already exists. Switching to it..."
tmux select-window -t "$branch"
return 0
fi
# 1. Split the current window horizontally (columns)
# -h: horizontal split (side-by-side)
# -c "$PWD": start in current directory
tmux split-window -h -c "$PWD"
# 2. Rebalance layout to equal columns
tmux select-layout even-horizontal
# 3. Prepare arguments for the new shell
# Escape characters that might break the quoted string
local safe_prompt="${prompt//\"/\\\"}" # Escape double quotes
safe_prompt="${safe_prompt//\$/\\\$}" # Escape dollar signs
safe_prompt="${safe_prompt//\`/\\\`}" # Escape backticks
# 4. Send keys to the new pane
tmux send-keys "source ~/.profile" C-m
tmux send-keys "clear" C-m
tmux send-keys "c-start \"$branch\" \"$safe_prompt\"" C-m
}
# --- Core Logic ---
c-start() {
local branch=$1
local prompt=$2
# Create/Switch to worktree FIRST
wt-new "$branch" || return 1
# Run npm install INSIDE the new worktree
if [ -f ./package.json ]; then
echo "Installing dependencies in new worktree..."
npm i
fi
# Pass $PWD to --add-dir
claude --permission-mode=acceptEdits --model=opus --chrome --add-dir="$PWD" "$prompt"
}
# --- Git Worktree Helpers ---
wt-new() {
local branch=$1
if [ -z "$branch" ]; then echo "Branch name required"; return 1; fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: Not inside a git repository"
return 1
fi
git worktree add ".worktrees/$branch" -b "$branch" && cd ".worktrees/$branch"
git config core.hooksPath "$(git rev-parse --show-toplevel)/.githooks"
}
wt-checkout() {
local branch=$1
if [ -z "$branch" ]; then echo "Branch name required"; return 1; fi
# Try to add worktree from local branch, otherwise create tracking branch
git worktree add ".worktrees/$branch" "$branch" 2>/dev/null || \
git worktree add ".worktrees/$branch" -b "$branch" --track "origin/$branch"
cd ".worktrees/$branch"
git config core.hooksPath "$(git rev-parse --show-toplevel)/.githooks"
}
wt-sync() {
git fetch origin && git rebase origin/main
}
wt-merge() {
local target=${1:-main}
local current
current=$(git branch --show-current)
if [ -z "$current" ]; then
echo "Error: Not on a branch."
return 1
fi
if [ "$current" = "$target" ]; then
echo "You are already on '$target'."
return 1
fi
# Find path of target branch using porcelain output for safety
# Looks for: branch refs/heads/<target>
# And grabs the associated 'worktree' path from the stanza (usually 2 lines up)
local target_path
target_path=$(git worktree list --porcelain | grep -B 2 -F "refs/heads/$target" | grep "^worktree " | cut -d ' ' -f 2-)
if [ -z "$target_path" ]; then
echo "Error: Branch '$target' is not checked out in any worktree."
echo "Cannot merge into a branch that isn't checked out locally."
return 1
fi
echo "Merging '$current' into '$target' (at $target_path)..."
# Perform the merge in the target's directory
# We use a subshell so we don't actually change our current shell's directory
(
cd "$target_path" && \
git merge "$current"
)
}
wt-remove() {
local branch=$1
if [ -z "$branch" ]; then echo "Branch name required"; return 1; fi
git worktree remove ".worktrees/$branch"
}
wt-list() {
git worktree list
}
wt-done() {
local branch
branch=$(git branch --show-current)
# Cannot remove current working directory, so move up (to repo root) first
# Assuming standard .worktrees/<branch> structure
cd ../..
git worktree remove ".worktrees/$branch"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment