feat: add opencode-worktree skill for isolated sessions
- Adds skills/opencode-worktree/ with SKILL.md and opencode-worktree.sh - Creates unique git worktree per session (e.g. session-20260327-a1b2c3-refactor-auth) - Cleans up stale worktrees on every launch - Branch always based on main - User can source directly or copy to PATH
This commit is contained in:
215
skills/opencode-worktree/SKILL.md
Normal file
215
skills/opencode-worktree/SKILL.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# opencode-worktree
|
||||||
|
|
||||||
|
Isolated OpenCode sessions via git worktrees.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each OpenCode session gets its own git worktree with a unique branch. This prevents:
|
||||||
|
- Clashes with parallel sessions on the same repo
|
||||||
|
- Accidental overwrites from multiple agents
|
||||||
|
- Confusion from work-in-progress across contexts
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Git
|
||||||
|
- opencode installed and configured
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Source directly (Recommended)
|
||||||
|
```bash
|
||||||
|
. skills/opencode-worktree/opencode-worktree.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Copy to PATH
|
||||||
|
```bash
|
||||||
|
cp skills/opencode-worktree/opencode-worktree.sh ~/.local/bin/opencode-worktree
|
||||||
|
chmod +x ~/.local/bin/opencode-worktree
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Create new session
|
||||||
|
```bash
|
||||||
|
. opencode-worktree.sh # session-20260327-a1b2c3
|
||||||
|
. opencode-worktree.sh refactor-auth # session-20260327-a1b2c3-refactor-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
```bash
|
||||||
|
. opencode-worktree.sh --cleanup # remove all session-* worktrees
|
||||||
|
. opencode-worktree.sh --cleanup <name> # remove specific worktree
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Cleanup** - On every launch, removes all stale `session-*` worktrees and their branches
|
||||||
|
2. **Create** - Creates new worktree based on `main` with unique name: `session-{timestamp}-{random6}[-{purpose}]`
|
||||||
|
3. **Launch** - Changes into worktree and launches opencode
|
||||||
|
4. **Exit** - When opencode exits, you return to your original directory (worktree remains for review)
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start session for refactoring auth
|
||||||
|
. opencode-worktree.sh refactor-auth
|
||||||
|
|
||||||
|
# ... do work in opencode ...
|
||||||
|
|
||||||
|
# Exit opencode (worktree with your changes still exists)
|
||||||
|
# Later, resume or cleanup
|
||||||
|
. opencode-worktree.sh --cleanup session-20260327-a1b2c3-refactor-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Script Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# opencode-worktree - Isolated OpenCode sessions via git worktrees
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORKTREE_BASE="$PWD/.git/worktrees"
|
||||||
|
PURPOSE=""
|
||||||
|
CLEANUP_ONLY=false
|
||||||
|
CLEANUP_NAME=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [purpose] [--cleanup [name]]
|
||||||
|
|
||||||
|
Create isolated OpenCode sessions via git worktrees.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
purpose Purpose string for the session (optional)
|
||||||
|
--cleanup Remove all session-* worktrees
|
||||||
|
--cleanup <name> Remove specific worktree by name
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") # session-20260327-a1b2c3
|
||||||
|
$(basename "$0") refactor-auth # session-20260327-a1b2c3-refactor-auth
|
||||||
|
$(basename "$0") --cleanup # remove all session-* worktrees
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cleanup)
|
||||||
|
CLEANUP_ONLY=true
|
||||||
|
if [[ $# -gt 1 && ! "$2" =~ ^-- ]]; then
|
||||||
|
CLEANUP_NAME="$2"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$PURPOSE" ]]; then
|
||||||
|
PURPOSE="$1"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stale() {
|
||||||
|
if [[ ! -d "$WORKTREE_BASE" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for wt in "$WORKTREE_BASE"/session-*; do
|
||||||
|
[[ -d "$wt" ]] || continue
|
||||||
|
|
||||||
|
branch=$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null) || continue
|
||||||
|
|
||||||
|
echo "Removing stale worktree: $(basename "$wt")"
|
||||||
|
git worktree remove "$wt" --force 2>/dev/null || true
|
||||||
|
if [[ -n "$branch" && "$branch" != "HEAD" ]]; then
|
||||||
|
git branch -D "$branch" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_single() {
|
||||||
|
local name="$1"
|
||||||
|
local wt_path="$WORKTREE_BASE/$name"
|
||||||
|
|
||||||
|
if [[ ! -d "$wt_path" ]]; then
|
||||||
|
echo "Worktree '$name' not found"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch=""
|
||||||
|
|
||||||
|
echo "Removing worktree: $name"
|
||||||
|
git worktree remove "$wt_path" --force 2>/dev/null || true
|
||||||
|
if [[ -n "$branch" && "$branch" != "HEAD" ]]; then
|
||||||
|
git branch -D "$branch" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_worktree() {
|
||||||
|
local timestamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
local random=$(head -c 3 /dev/urandom | xxd -p | head -c 6)
|
||||||
|
local worktree_name="session-${timestamp}-${random}"
|
||||||
|
local branch_name="$worktree_name"
|
||||||
|
|
||||||
|
if [[ -n "$PURPOSE" ]]; then
|
||||||
|
worktree_name="${worktree_name}-${PURPOSE}"
|
||||||
|
branch_name="$worktree_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local worktree_path="$WORKTREE_BASE/$worktree_name"
|
||||||
|
|
||||||
|
# Cleanup any existing with same name
|
||||||
|
if [[ -d "$worktree_path" ]]; then
|
||||||
|
echo "Removing existing worktree: $worktree_name"
|
||||||
|
git worktree remove "$worktree_path" --force 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure main exists and is up to date
|
||||||
|
if ! git show-ref --quiet refs/heads/main 2>/dev/null; then
|
||||||
|
echo "Error: 'main' branch does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create worktree from main
|
||||||
|
echo "Creating worktree: $worktree_name"
|
||||||
|
git worktree add -b "$branch_name" "$worktree_path" main
|
||||||
|
|
||||||
|
# Launch opencode in worktree
|
||||||
|
echo "Entering worktree and launching opencode..."
|
||||||
|
cd "$worktree_path"
|
||||||
|
exec opencode
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Verify we're in a git repo
|
||||||
|
if ! git rev-parse --is-inside-work-tree 2>/dev/null; then
|
||||||
|
echo "Error: Must be run inside a git repository"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CLEANUP_ONLY" == true ]]; then
|
||||||
|
if [[ -n "$CLEANUP_NAME" ]]; then
|
||||||
|
cleanup_single "$CLEANUP_NAME"
|
||||||
|
else
|
||||||
|
cleanup_stale
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_stale
|
||||||
|
create_worktree
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args "$@"
|
||||||
|
main
|
||||||
|
```
|
||||||
146
skills/opencode-worktree/opencode-worktree.sh
Normal file
146
skills/opencode-worktree/opencode-worktree.sh
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# opencode-worktree - Isolated OpenCode sessions via git worktrees
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORKTREE_BASE="$PWD/.git/worktrees"
|
||||||
|
PURPOSE=""
|
||||||
|
CLEANUP_ONLY=false
|
||||||
|
CLEANUP_NAME=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [purpose] [--cleanup [name]]
|
||||||
|
|
||||||
|
Create isolated OpenCode sessions via git worktrees.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
purpose Purpose string for the session (optional)
|
||||||
|
--cleanup Remove all session-* worktrees
|
||||||
|
--cleanup <name> Remove specific worktree by name
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") # session-20260327-a1b2c3
|
||||||
|
$(basename "$0") refactor-auth # session-20260327-a1b2c3-refactor-auth
|
||||||
|
$(basename "$0") --cleanup # remove all session-* worktrees
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--cleanup)
|
||||||
|
CLEANUP_ONLY=true
|
||||||
|
if [[ $# -gt 1 && ! "$2" =~ ^-- ]]; then
|
||||||
|
CLEANUP_NAME="$2"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$PURPOSE" ]]; then
|
||||||
|
PURPOSE="$1"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stale() {
|
||||||
|
if [[ ! -d "$WORKTREE_BASE" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for wt in "$WORKTREE_BASE"/session-*; do
|
||||||
|
[[ -d "$wt" ]] || continue
|
||||||
|
|
||||||
|
branch=$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null) || continue
|
||||||
|
|
||||||
|
echo "Removing stale worktree: $(basename "$wt")"
|
||||||
|
git worktree remove "$wt" --force 2>/dev/null || true
|
||||||
|
if [[ -n "$branch" && "$branch" != "HEAD" ]]; then
|
||||||
|
git branch -D "$branch" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_single() {
|
||||||
|
local name="$1"
|
||||||
|
local wt_path="$WORKTREE_BASE/$name"
|
||||||
|
|
||||||
|
if [[ ! -d "$wt_path" ]]; then
|
||||||
|
echo "Worktree '$name' not found"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch=""
|
||||||
|
|
||||||
|
echo "Removing worktree: $name"
|
||||||
|
git worktree remove "$wt_path" --force 2>/dev/null || true
|
||||||
|
if [[ -n "$branch" && "$branch" != "HEAD" ]]; then
|
||||||
|
git branch -D "$branch" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_worktree() {
|
||||||
|
local timestamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
local random=$(head -c 3 /dev/urandom | xxd -p | head -c 6)
|
||||||
|
local worktree_name="session-${timestamp}-${random}"
|
||||||
|
local branch_name="$worktree_name"
|
||||||
|
|
||||||
|
if [[ -n "$PURPOSE" ]]; then
|
||||||
|
worktree_name="${worktree_name}-${PURPOSE}"
|
||||||
|
branch_name="$worktree_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local worktree_path="$WORKTREE_BASE/$worktree_name"
|
||||||
|
|
||||||
|
# Cleanup any existing with same name
|
||||||
|
if [[ -d "$worktree_path" ]]; then
|
||||||
|
echo "Removing existing worktree: $worktree_name"
|
||||||
|
git worktree remove "$worktree_path" --force 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure main exists and is up to date
|
||||||
|
if ! git show-ref --quiet refs/heads/main 2>/dev/null; then
|
||||||
|
echo "Error: 'main' branch does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create worktree from main
|
||||||
|
echo "Creating worktree: $worktree_name"
|
||||||
|
git worktree add -b "$branch_name" "$worktree_path" main
|
||||||
|
|
||||||
|
# Launch opencode in worktree
|
||||||
|
echo "Entering worktree and launching opencode..."
|
||||||
|
cd "$worktree_path"
|
||||||
|
exec opencode
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Verify we're in a git repo
|
||||||
|
if ! git rev-parse --is-inside-work-tree 2>/dev/null; then
|
||||||
|
echo "Error: Must be run inside a git repository"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CLEANUP_ONLY" == true ]]; then
|
||||||
|
if [[ -n "$CLEANUP_NAME" ]]; then
|
||||||
|
cleanup_single "$CLEANUP_NAME"
|
||||||
|
else
|
||||||
|
cleanup_stale
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_stale
|
||||||
|
create_worktree
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args "$@"
|
||||||
|
main
|
||||||
Reference in New Issue
Block a user