Last active
August 15, 2025 17:31
-
-
Save saturnflyer/77d9ff81b7e14d1036f36471dcf2721d to your computer and use it in GitHub Desktop.
Create a git worktree for feature work with Agent OS support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| # Returns the port number for the current git branch | |
| # Used by Procfile.dev to start the server on the correct port | |
| require "yaml" | |
| require "fileutils" | |
| DEFAULT_PORT = 3000 | |
| CONFIG_PATH = File.join(Dir.pwd, ".branch-config.yml") | |
| def current_git_branch | |
| `git branch --show-current`.strip | |
| rescue | |
| "main" | |
| end | |
| def load_config | |
| return {} unless File.exist?(CONFIG_PATH) | |
| YAML.load_file(CONFIG_PATH) || {} | |
| rescue | |
| {} | |
| end | |
| def get_port_for_branch(branch) | |
| config = load_config | |
| # Return existing port if configured | |
| if config.dig("branches", branch, "port") | |
| return config.dig("branches", branch, "port") | |
| end | |
| # Default port for main branch | |
| return DEFAULT_PORT if branch == "main" | |
| # Find next available port for new branches | |
| used_ports = config.dig("branches")&.values&.map { |data| data["port"] } || [] | |
| (3001..9999).each do |port| | |
| unless used_ports.include?(port) | |
| # Save the new port assignment | |
| config["branches"] ||= {} | |
| config["branches"][branch] = { | |
| "port" => port, | |
| "last_used" => Time.now.iso8601 | |
| } | |
| File.write(CONFIG_PATH, config.to_yaml) | |
| return port | |
| end | |
| end | |
| # Fallback | |
| DEFAULT_PORT | |
| end | |
| # Output the port for the current branch | |
| branch = current_git_branch | |
| port = get_port_for_branch(branch) | |
| puts port |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Git Worktree Feature Command | |
| # Manages git worktrees for feature development with interactive interface | |
| # Now supports reading from Agent OS specs | |
| set -e | |
| # Color codes for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| CYAN='\033[0;36m' | |
| MAGENTA='\033[0;35m' | |
| NC='\033[0m' # No Color | |
| # Configuration | |
| # Create worktrees as siblings to the main directory | |
| WORKTREE_BASE="../" | |
| SPECS_BASE=".agent-os/specs" | |
| TEST_COMMAND="rails test" | |
| MAIN_BRANCH="main" | |
| VERBOSE=false | |
| # Helper functions | |
| error() { | |
| echo -e "${RED}Error: $1${NC}" >&2 | |
| exit 1 | |
| } | |
| success() { | |
| echo -e "${GREEN}✓ $1${NC}" | |
| } | |
| info() { | |
| echo -e "${BLUE}→ $1${NC}" | |
| } | |
| warning() { | |
| echo -e "${YELLOW}⚠ $1${NC}" | |
| } | |
| debug() { | |
| if [[ "$VERBOSE" == "true" ]]; then | |
| echo -e "${CYAN}[DEBUG] $1${NC}" >&2 | |
| fi | |
| } | |
| # Check if we're in a git repository | |
| check_git_repo() { | |
| debug "Checking if we're in a git repository" | |
| if ! git rev-parse --git-dir > /dev/null 2>&1; then | |
| error "Not in a git repository" | |
| fi | |
| debug "Git repository confirmed" | |
| } | |
| # Check if git worktree command is available | |
| check_worktree_support() { | |
| debug "Checking git worktree support" | |
| if ! git worktree list > /dev/null 2>&1; then | |
| error "Git worktree command not available. Please upgrade git to version 2.5 or higher" | |
| fi | |
| debug "Git worktree command available" | |
| } | |
| # Get the date in YYYY-MM-DD format | |
| get_date() { | |
| date +%Y-%m-%d | |
| } | |
| # Convert string to kebab-case | |
| to_kebab_case() { | |
| echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '-' | sed 's/[^a-z0-9-]//g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' | |
| } | |
| # Get port for current branch using bin/branch-port | |
| get_branch_port() { | |
| local original_dir=$(pwd) | |
| local worktree_dir="$1" | |
| # Change to worktree directory to get the correct branch context | |
| cd "$worktree_dir" 2>/dev/null || return 1 | |
| # Use bin/branch-port to get the port | |
| local port=$("$original_dir/bin/branch-port" 2>/dev/null) | |
| # Return to original directory | |
| cd "$original_dir" | |
| echo "$port" | |
| } | |
| # List available Agent OS specs | |
| list_specs() { | |
| local specs=() | |
| debug "Listing specs in $SPECS_BASE" | |
| if [[ -d "$SPECS_BASE" ]]; then | |
| for spec_dir in "$SPECS_BASE"/*; do | |
| if [[ -d "$spec_dir" && -f "$spec_dir/spec.md" ]]; then | |
| local spec_name=$(basename "$spec_dir") | |
| debug "Found spec: $spec_name" | |
| specs+=("$spec_name") | |
| fi | |
| done | |
| fi | |
| echo "${specs[@]}" | |
| } | |
| # Extract feature name from spec directory name | |
| extract_feature_from_spec() { | |
| local spec_dir="$1" | |
| # Remove date prefix (YYYY-MM-DD-) and return the rest | |
| echo "$spec_dir" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-//' | |
| } | |
| # Get spec overview from spec.md file | |
| get_spec_overview() { | |
| local spec_dir="$1" | |
| local spec_file="$SPECS_BASE/$spec_dir/spec.md" | |
| if [[ -f "$spec_file" ]]; then | |
| # Extract the overview section (first paragraph after ## Overview) | |
| awk '/^## Overview/{flag=1; next} /^##/{flag=0} flag && NF' "$spec_file" | head -n 3 | |
| else | |
| echo "No overview available" | |
| fi | |
| } | |
| # Check task completion status | |
| get_spec_task_status() { | |
| local spec_dir="$1" | |
| local tasks_file="$SPECS_BASE/$spec_dir/tasks.md" | |
| if [[ -f "$tasks_file" ]]; then | |
| local total=$(grep -c "^- \[.\]" "$tasks_file" 2>/dev/null || echo "0") | |
| local completed=$(grep -c "^- \[x\]" "$tasks_file" 2>/dev/null || echo "0") | |
| echo "$completed/$total tasks completed" | |
| else | |
| echo "No tasks defined" | |
| fi | |
| } | |
| # List existing worktrees in the worktrees directory | |
| list_worktrees() { | |
| local worktrees=() | |
| local branches=() | |
| local project_name=$(basename "$(pwd)") | |
| debug "Listing worktrees for project: $project_name" | |
| # Get all worktrees, looking for ones that match our project naming pattern | |
| while IFS= read -r line; do | |
| debug "Processing worktree line: $line" | |
| local path=$(echo "$line" | awk '{print $1}') | |
| local branch=$(echo "$line" | awk '{print $3}' | sed 's/\[//' | sed 's/\]//') | |
| # Check if this worktree is a sibling directory with our project prefix | |
| # Handle both relative and absolute paths | |
| if [[ "$path" == *"/${project_name}-"* ]] && [[ "$path" != *"/${project_name}/"* ]] && [[ -d "$path" ]]; then | |
| debug "Found worktree: $path (branch: $branch)" | |
| worktrees+=("$path") | |
| branches+=("$branch") | |
| fi | |
| done < <(git worktree list) | |
| echo "${worktrees[@]}" | |
| } | |
| # Interactive spec selection | |
| spec_selection_mode() { | |
| echo -e "${MAGENTA}Available Agent OS Specs${NC}" | |
| echo -e "${MAGENTA}========================${NC}" | |
| echo | |
| local specs=($(list_specs)) | |
| local worktrees=($(list_worktrees)) | |
| # Option to finish existing worktrees if any exist | |
| if [[ ${#worktrees[@]} -gt 0 ]]; then | |
| echo -e "${YELLOW}Existing worktrees to finish:${NC}" | |
| local i=1 | |
| for worktree in "${worktrees[@]}"; do | |
| local name=$(basename "$worktree") | |
| local branch=$(cd "$worktree" 2>/dev/null && git branch --show-current 2>/dev/null || echo "unknown") | |
| # Check if worktree has associated spec | |
| local associated_spec="" | |
| for spec in "${specs[@]}"; do | |
| local feature_name=$(extract_feature_from_spec "$spec") | |
| if [[ "$branch" == "$feature_name" ]] || [[ "$name" == *"$feature_name"* ]]; then | |
| associated_spec="$spec" | |
| break | |
| fi | |
| done | |
| if [[ -n "$associated_spec" ]]; then | |
| local task_status=$(get_spec_task_status "$associated_spec") | |
| echo -e " f$i) Finish: $name ${YELLOW}($task_status)${NC}" | |
| else | |
| echo -e " f$i) Finish: $name" | |
| fi | |
| ((i++)) | |
| done | |
| echo | |
| fi | |
| if [[ ${#specs[@]} -eq 0 ]]; then | |
| warning "No Agent OS specs found in $SPECS_BASE" | |
| echo | |
| read -p "Enter feature name manually: " feature_name | |
| handle_feature_creation "$feature_name" "" | |
| return | |
| fi | |
| echo -e "${CYAN}Work on spec:${NC}" | |
| echo | |
| local i=1 | |
| for spec in "${specs[@]}"; do | |
| local feature_name=$(extract_feature_from_spec "$spec") | |
| local task_status=$(get_spec_task_status "$spec") | |
| # Check if worktree already exists for this spec | |
| local project_name=$(basename "$(pwd)") | |
| local existing_worktree="" | |
| for worktree_path in $(list_worktrees); do | |
| if [[ "$worktree_path" == *"$feature_name"* ]]; then | |
| existing_worktree="$worktree_path" | |
| break | |
| fi | |
| done | |
| if [[ -n "$existing_worktree" ]]; then | |
| echo -e "${CYAN}$i)${NC} $feature_name ${YELLOW}($task_status)${NC} ${GREEN}[Worktree exists]${NC}" | |
| else | |
| echo -e "${CYAN}$i)${NC} $feature_name ${YELLOW}($task_status)${NC}" | |
| fi | |
| # Show overview if verbose | |
| if [[ "$VERBOSE" == "true" ]]; then | |
| local overview=$(get_spec_overview "$spec" | sed 's/^/ /') | |
| echo -e "${BLUE}$overview${NC}" | |
| echo | |
| fi | |
| ((i++)) | |
| done | |
| echo | |
| echo "0) Cancel" | |
| echo | |
| read -p "Select option: " choice | |
| if [[ "$choice" == "0" || -z "$choice" ]]; then | |
| exit 0 | |
| elif [[ "$choice" =~ ^f([0-9]+)$ ]]; then | |
| # Handle finish option | |
| local index=$((${BASH_REMATCH[1]} - 1)) | |
| if [[ $index -ge 0 && $index -lt ${#worktrees[@]} ]]; then | |
| finish_feature "${worktrees[$index]}" | |
| else | |
| warning "Invalid worktree number" | |
| exit 1 | |
| fi | |
| elif [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -ge 1 && $choice -le ${#specs[@]} ]]; then | |
| local index=$((choice - 1)) | |
| local selected_spec="${specs[$index]}" | |
| local feature_name=$(extract_feature_from_spec "$selected_spec") | |
| handle_feature_creation "$feature_name" "$selected_spec" | |
| else | |
| warning "Invalid selection" | |
| exit 1 | |
| fi | |
| } | |
| # Interactive menu for worktree management | |
| interactive_mode() { | |
| echo -e "${CYAN}Git Worktree Feature Manager${NC}" | |
| echo -e "${CYAN}=============================${NC}" | |
| echo | |
| local worktrees=($(list_worktrees)) | |
| if [[ ${#worktrees[@]} -eq 0 ]]; then | |
| echo "No existing feature worktrees found." | |
| echo | |
| echo "1) Work on existing spec" | |
| echo "2) Create new feature from spec" | |
| echo "3) Create new feature (manual)" | |
| echo "0) Exit" | |
| echo | |
| read -p "Select an option: " choice | |
| case $choice in | |
| 1) | |
| spec_selection_mode | |
| ;; | |
| 2) | |
| spec_selection_mode | |
| ;; | |
| 3) | |
| read -p "Enter feature name: " feature_name | |
| create_feature "$feature_name" | |
| ;; | |
| 0|"") | |
| exit 0 | |
| ;; | |
| *) | |
| warning "Invalid option" | |
| exit 1 | |
| ;; | |
| esac | |
| else | |
| echo "Existing feature worktrees:" | |
| echo | |
| local i=1 | |
| for worktree in "${worktrees[@]}"; do | |
| local name=$(basename "$worktree") | |
| echo "$i) $name" | |
| ((i++)) | |
| done | |
| echo | |
| echo "Actions:" | |
| echo "s#) Switch to worktree (e.g., s1)" | |
| echo "f#) Finish worktree (e.g., f1)" | |
| echo "w) Work on existing spec" | |
| echo "n) Create new feature from spec" | |
| echo "m) Create new feature (manual)" | |
| echo "0) Exit" | |
| echo | |
| read -p "Select an option: " choice | |
| if [[ "$choice" == "0" || -z "$choice" ]]; then | |
| exit 0 | |
| elif [[ "$choice" == "w" ]]; then | |
| spec_selection_mode | |
| elif [[ "$choice" == "n" ]]; then | |
| spec_selection_mode | |
| elif [[ "$choice" == "m" ]]; then | |
| read -p "Enter feature name: " feature_name | |
| create_feature "$feature_name" | |
| elif [[ "$choice" =~ ^s([0-9]+)$ ]]; then | |
| local index=$((${BASH_REMATCH[1]} - 1)) | |
| if [[ $index -ge 0 && $index -lt ${#worktrees[@]} ]]; then | |
| switch_to_worktree "${worktrees[$index]}" | |
| else | |
| warning "Invalid worktree number" | |
| exit 1 | |
| fi | |
| elif [[ "$choice" =~ ^f([0-9]+)$ ]]; then | |
| local index=$((${BASH_REMATCH[1]} - 1)) | |
| if [[ $index -ge 0 && $index -lt ${#worktrees[@]} ]]; then | |
| finish_feature "${worktrees[$index]}" | |
| else | |
| warning "Invalid worktree number" | |
| exit 1 | |
| fi | |
| else | |
| warning "Invalid option" | |
| exit 1 | |
| fi | |
| fi | |
| } | |
| # Handle feature creation or switching - unified function | |
| handle_feature_creation() { | |
| local feature_name="$1" | |
| local spec_dir="$2" | |
| # Convert to kebab case for consistency | |
| local kebab_name=$(to_kebab_case "$feature_name" | cut -d'-' -f1-5) | |
| local project_name=$(basename "$(pwd)") | |
| local branch_name="${kebab_name}" | |
| # Check if a worktree already exists for this feature | |
| local existing_worktree="" | |
| for worktree_path in $(list_worktrees); do | |
| if [[ "$worktree_path" == *"$kebab_name"* ]]; then | |
| existing_worktree="$worktree_path" | |
| break | |
| fi | |
| done | |
| if [[ -n "$existing_worktree" ]]; then | |
| info "Worktree already exists for feature: $kebab_name" | |
| info "Switching to existing worktree..." | |
| # Set spec reference if available | |
| if [[ -n "$spec_dir" ]]; then | |
| export AGENT_OS_SPEC_REF="$spec_dir" | |
| fi | |
| switch_to_worktree "$existing_worktree" | |
| else | |
| # Check if branch already exists (without worktree) | |
| if git show-ref --verify --quiet "refs/heads/$branch_name"; then | |
| info "Branch '$branch_name' already exists" | |
| # Check if a worktree already exists for this branch | |
| local date_prefix=$(get_date) | |
| local dir_name="${project_name}-${date_prefix}-${kebab_name}" | |
| local worktree_path="${WORKTREE_BASE}/${dir_name}" | |
| # Check if worktree directory exists | |
| if [[ -d "$worktree_path" ]]; then | |
| info "Worktree already exists at: $worktree_path" | |
| info "Switching to existing worktree..." | |
| # Set spec reference if available | |
| if [[ -n "$spec_dir" ]]; then | |
| export AGENT_OS_SPEC_REF="$spec_dir" | |
| fi | |
| switch_to_worktree "$worktree_path" | |
| else | |
| info "Creating worktree for existing branch..." | |
| # Set spec reference if available | |
| if [[ -n "$spec_dir" ]]; then | |
| export AGENT_OS_SPEC_REF="$spec_dir" | |
| fi | |
| # Create the worktree for the existing branch | |
| git worktree add "$worktree_path" "$branch_name" | |
| success "Worktree created for existing branch" | |
| # Switch to the worktree | |
| switch_to_worktree "$worktree_path" | |
| fi | |
| else | |
| info "Creating new worktree for feature: $kebab_name" | |
| # Set spec reference if available | |
| if [[ -n "$spec_dir" ]]; then | |
| export AGENT_OS_SPEC_REF="$spec_dir" | |
| fi | |
| create_feature "$feature_name" | |
| fi | |
| fi | |
| } | |
| # Switch to an existing worktree | |
| switch_to_worktree() { | |
| local worktree_path="$1" | |
| if [[ ! -d "$worktree_path" ]]; then | |
| error "Worktree directory does not exist: $worktree_path" | |
| fi | |
| info "Switching to worktree: $(basename "$worktree_path")" | |
| # Change to the worktree directory | |
| cd "$worktree_path" || error "Failed to change directory to $worktree_path" | |
| # Get port for this branch using bin/branch-port | |
| local port=$(bin/branch-port) | |
| info "Using port $port for this worktree" | |
| # Check if a Rails server is already running on this port | |
| if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then | |
| warning "Rails server already running on port $port" | |
| else | |
| # Start the Rails server in the background | |
| info "Starting Rails server on port $port..." | |
| rails server -p $port > /tmp/rails-server-$(basename "$worktree_path").log 2>&1 & | |
| local server_pid=$! | |
| sleep 2 # Give the server a moment to start | |
| if kill -0 $server_pid 2>/dev/null; then | |
| success "Rails server started (PID: $server_pid)" | |
| else | |
| warning "Rails server may have failed to start. Check /tmp/rails-server-$(basename "$worktree_path").log" | |
| fi | |
| fi | |
| echo | |
| success "Feature worktree ready!" | |
| echo -e "${CYAN}Visit your application at:${NC} ${GREEN}http://localhost:$port${NC}" | |
| echo | |
| info "Starting Claude with spec context..." | |
| echo | |
| # Start Claude command with spec reference if available | |
| if [[ -n "$AGENT_OS_SPEC_REF" ]]; then | |
| # Get the spec overview for better context | |
| local spec_overview=$(get_spec_overview "$AGENT_OS_SPEC_REF" | head -n 1) | |
| exec claude "I'm ready to work on the '$AGENT_OS_SPEC_REF' spec. The tasks are in @$SPECS_BASE/$AGENT_OS_SPEC_REF/tasks.md | |
| Please start by reviewing the tasks and begin with the first uncompleted task." | |
| else | |
| exec claude | |
| fi | |
| } | |
| # Create a new feature worktree | |
| create_feature() { | |
| local feature_name="$1" | |
| debug "Creating feature with name: $feature_name" | |
| if [[ -z "$feature_name" ]]; then | |
| error "Feature name is required" | |
| fi | |
| # Convert to kebab case and limit to 5 words | |
| local kebab_name=$(to_kebab_case "$feature_name" | cut -d'-' -f1-5) | |
| local date_prefix=$(get_date) | |
| local project_name=$(basename "$(pwd)") | |
| local dir_name="${project_name}-${date_prefix}-${kebab_name}" | |
| local branch_name="${kebab_name}" | |
| local worktree_path="${WORKTREE_BASE}/${dir_name}" | |
| debug "Kebab name: $kebab_name" | |
| debug "Directory name: $dir_name" | |
| debug "Branch name: $branch_name" | |
| debug "Worktree path: $worktree_path" | |
| info "Creating feature: $kebab_name" | |
| info "Directory: $worktree_path" | |
| info "Branch: $branch_name" | |
| # Check if branch already exists | |
| debug "Checking if branch '$branch_name' exists" | |
| if git show-ref --verify --quiet "refs/heads/$branch_name"; then | |
| warning "Branch '$branch_name' already exists - this shouldn't happen in create_feature" | |
| error "Branch '$branch_name' already exists. Use the interactive menu to work on existing specs." | |
| fi | |
| # Check if worktree already exists | |
| debug "Checking if worktree directory exists: $worktree_path" | |
| if [[ -d "$worktree_path" ]]; then | |
| info "Worktree already exists at: $worktree_path" | |
| info "Switching to existing worktree..." | |
| switch_to_worktree "$worktree_path" | |
| return # Exit the function after switching | |
| fi | |
| # Create the worktree and branch | |
| info "Creating worktree and branch..." | |
| debug "Running: git worktree add -b $branch_name $worktree_path $MAIN_BRANCH" | |
| git worktree add -b "$branch_name" "$worktree_path" "$MAIN_BRANCH" | |
| success "Feature worktree created successfully" | |
| # Change to the new worktree directory | |
| cd "$worktree_path" || error "Failed to change directory to $worktree_path" | |
| # Get port for this new branch using bin/branch-port (it will auto-assign) | |
| local port=$(bin/branch-port) | |
| info "Assigned port $port to branch $branch_name" | |
| # Start the Rails server in the background | |
| info "Starting Rails server on port $port..." | |
| rails server -p $port > /tmp/rails-server-$(basename "$worktree_path").log 2>&1 & | |
| local server_pid=$! | |
| sleep 2 # Give the server a moment to start | |
| if kill -0 $server_pid 2>/dev/null; then | |
| success "Rails server started (PID: $server_pid)" | |
| else | |
| warning "Rails server may have failed to start. Check /tmp/rails-server-$(basename "$worktree_path").log" | |
| fi | |
| echo | |
| success "Feature worktree ready!" | |
| echo -e "${CYAN}Visit your application at:${NC} ${GREEN}http://localhost:$port${NC}" | |
| echo | |
| info "Starting Claude with spec context..." | |
| echo | |
| # Start Claude command with spec reference if available | |
| if [[ -n "$AGENT_OS_SPEC_REF" ]]; then | |
| exec claude "I'm ready to work on the '$AGENT_OS_SPEC_REF' spec. The tasks are in @$SPECS_BASE/$AGENT_OS_SPEC_REF/tasks.md | |
| Please start by reviewing the tasks and begin with the first uncompleted task." | |
| else | |
| exec claude "I'm ready to work on this feature branch. What would you like me to help with?" | |
| fi | |
| } | |
| # Create a feature worktree from an Agent OS spec | |
| create_feature_from_spec() { | |
| local spec_dir="$1" | |
| local feature_name="$2" | |
| debug "Creating feature from spec: $spec_dir" | |
| debug "Feature name: $feature_name" | |
| # Display spec information first | |
| echo | |
| echo -e "${MAGENTA}Agent OS Spec Information${NC}" | |
| echo -e "${MAGENTA}========================${NC}" | |
| echo | |
| echo -e "${CYAN}Spec:${NC} $spec_dir" | |
| echo -e "${CYAN}Location:${NC} @$SPECS_BASE/$spec_dir/" | |
| local task_status=$(get_spec_task_status "$spec_dir") | |
| echo -e "${CYAN}Progress:${NC} $task_status" | |
| echo | |
| echo -e "${CYAN}Overview:${NC}" | |
| get_spec_overview "$spec_dir" | |
| echo | |
| info "When Claude starts, you can reference: @$SPECS_BASE/$spec_dir/tasks.md" | |
| echo | |
| # Store spec_dir in an environment variable to pass to the modified create_feature | |
| export AGENT_OS_SPEC_REF="$spec_dir" | |
| # Use the existing create_feature function (which will exec claude) | |
| create_feature "$feature_name" | |
| } | |
| # Finish a feature (rebase, test, merge, remove) | |
| finish_feature() { | |
| local worktree_path="$1" | |
| debug "Finishing feature at worktree: $worktree_path" | |
| local branch_name=$(cd "$worktree_path" && git branch --show-current) | |
| debug "Branch name: $branch_name" | |
| if [[ -z "$branch_name" ]]; then | |
| error "Could not determine branch name for worktree" | |
| fi | |
| # Save current directory | |
| local original_dir=$(pwd) | |
| # Get port for this branch before we lose access to it | |
| local port=$(get_branch_port "$worktree_path") | |
| debug "Branch is using port: $port" | |
| # Check for uncommitted changes in worktree | |
| cd "$worktree_path" | |
| if ! git diff-index --quiet HEAD -- 2>/dev/null; then | |
| warning "You have uncommitted changes in the worktree" | |
| git status --short | |
| echo | |
| read -p "Do you want to continue anyway? (y/N): " continue_with_changes | |
| if [[ "$continue_with_changes" != "y" && "$continue_with_changes" != "Y" ]]; then | |
| warning "Please commit or stash your changes first" | |
| exit 0 | |
| fi | |
| fi | |
| # Check if branch has any differences from main | |
| cd "$original_dir" | |
| git checkout "$MAIN_BRANCH" > /dev/null 2>&1 | |
| git pull origin "$MAIN_BRANCH" > /dev/null 2>&1 || true | |
| # Check if there are any commits difference between branch and main | |
| local commits_ahead=$(git rev-list --count "$MAIN_BRANCH".."$branch_name" 2>/dev/null || echo "0") | |
| local commits_behind=$(git rev-list --count "$branch_name".."$MAIN_BRANCH" 2>/dev/null || echo "0") | |
| if [[ "$commits_ahead" == "0" && "$commits_behind" == "0" ]]; then | |
| info "Branch '$branch_name' has no differences from $MAIN_BRANCH" | |
| # Check if the branch exists on remote | |
| if git ls-remote --heads origin "$branch_name" | grep -q .; then | |
| warning "Branch exists on remote but has no local changes" | |
| read -p "Remove the worktree and local branch? (y/N): " remove_empty | |
| if [[ "$remove_empty" == "y" || "$remove_empty" == "Y" ]]; then | |
| info "Removing empty worktree and branch..." | |
| git worktree remove "$worktree_path" --force 2>/dev/null || git worktree remove "$worktree_path" | |
| git branch -D "$branch_name" 2>/dev/null || true | |
| # Clean up branch port configuration | |
| if [[ -f ".branch-config.yml" ]]; then | |
| ruby -ryaml -e " | |
| config = YAML.load_file('.branch-config.yml') || {} | |
| config['branches']&.delete('$branch_name') | |
| File.write('.branch-config.yml', config.to_yaml) | |
| " 2>/dev/null || true | |
| fi | |
| success "Cleaned up empty worktree and branch" | |
| exit 0 | |
| fi | |
| else | |
| info "Removing local worktree with no changes..." | |
| git worktree remove "$worktree_path" --force 2>/dev/null || git worktree remove "$worktree_path" | |
| git branch -D "$branch_name" 2>/dev/null || true | |
| # Clean up branch port configuration | |
| if [[ -f ".branch-config.yml" ]]; then | |
| ruby -ryaml -e " | |
| config = YAML.load_file('.branch-config.yml') || {} | |
| config['branches']&.delete('$branch_name') | |
| File.write('.branch-config.yml', config.to_yaml) | |
| " 2>/dev/null || true | |
| fi | |
| success "Cleaned up empty worktree and branch" | |
| exit 0 | |
| fi | |
| fi | |
| # Try to find associated spec for this branch | |
| local kebab_name="${branch_name}" | |
| local associated_spec="" | |
| for spec in $(list_specs); do | |
| local feature_name=$(extract_feature_from_spec "$spec") | |
| if [[ "$feature_name" == "$kebab_name" ]]; then | |
| associated_spec="$spec" | |
| break | |
| fi | |
| done | |
| echo -e "${CYAN}Finishing feature: $branch_name${NC}" | |
| # Show spec task status if spec found | |
| if [[ -n "$associated_spec" ]]; then | |
| echo -e "${MAGENTA}Associated spec: $associated_spec${NC}" | |
| local task_status=$(get_spec_task_status "$associated_spec") | |
| echo -e "${YELLOW}Task status: $task_status${NC}" | |
| # Check if all tasks are completed | |
| local tasks_file="$SPECS_BASE/$associated_spec/tasks.md" | |
| if [[ -f "$tasks_file" ]]; then | |
| local incomplete=$(grep -c "^- \[ \]" "$tasks_file" 2>/dev/null || echo "0") | |
| if [[ $incomplete -gt 0 ]]; then | |
| warning "There are $incomplete incomplete tasks in the spec" | |
| read -p "Continue anyway? (y/N): " continue_incomplete | |
| if [[ "$continue_incomplete" != "y" && "$continue_incomplete" != "Y" ]]; then | |
| warning "Cancelled - please complete all tasks first" | |
| exit 0 | |
| fi | |
| fi | |
| fi | |
| fi | |
| echo | |
| read -p "This will rebase, test, merge, and remove the worktree. Continue? (y/N): " confirm | |
| if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then | |
| warning "Cancelled" | |
| exit 0 | |
| fi | |
| # Step 1: Update main branch | |
| info "Updating $MAIN_BRANCH branch..." | |
| cd "$original_dir" | |
| git checkout "$MAIN_BRANCH" | |
| git pull origin "$MAIN_BRANCH" || warning "Could not pull from origin" | |
| # Step 2: Rebase feature branch | |
| info "Rebasing feature branch..." | |
| cd "$worktree_path" | |
| debug "Changed directory to: $worktree_path" | |
| debug "Running: git rebase $MAIN_BRANCH" | |
| if ! git rebase "$MAIN_BRANCH"; then | |
| warning "Rebase failed due to conflicts" | |
| echo | |
| echo "Options:" | |
| echo "1) cd into worktree to resolve conflicts" | |
| echo "2) Abort and exit" | |
| echo | |
| read -p "Select option (1-2): " rebase_choice | |
| case "$rebase_choice" in | |
| 1) | |
| info "Changing to worktree directory: $worktree_path" | |
| echo | |
| echo "To resolve conflicts:" | |
| echo " 1. Fix the conflicted files" | |
| echo " 2. git add <resolved files>" | |
| echo " 3. git rebase --continue" | |
| echo " 4. Run 'bin/feature' again to finish" | |
| echo | |
| cd "$worktree_path" | |
| exec $SHELL | |
| ;; | |
| 2|*) | |
| error "Rebase failed. Please resolve conflicts and try again" | |
| ;; | |
| esac | |
| fi | |
| debug "Rebase successful" | |
| # Step 3: Run tests | |
| info "Running tests..." | |
| if command -v rails &> /dev/null; then | |
| if ! $TEST_COMMAND; then | |
| warning "Tests failed" | |
| echo | |
| echo "Options:" | |
| echo "1) cd into worktree to fix test failures" | |
| echo "2) Skip tests and continue (not recommended)" | |
| echo "3) Abort and exit" | |
| echo | |
| read -p "Select option (1-3): " test_choice | |
| case "$test_choice" in | |
| 1) | |
| info "Changing to worktree directory: $worktree_path" | |
| echo | |
| echo "To fix test failures:" | |
| echo " 1. Run '$TEST_COMMAND' to see failures" | |
| echo " 2. Fix the failing tests" | |
| echo " 3. Run 'bin/feature' again to finish" | |
| echo | |
| cd "$worktree_path" | |
| exec $SHELL | |
| ;; | |
| 2) | |
| warning "Skipping tests - proceeding with merge" | |
| ;; | |
| 3|*) | |
| error "Tests failed. Please fix the issues and try again" | |
| ;; | |
| esac | |
| fi | |
| else | |
| warning "Rails not found, skipping tests" | |
| read -p "Continue without running tests? (y/N): " skip_tests | |
| if [[ "$skip_tests" != "y" && "$skip_tests" != "Y" ]]; then | |
| error "Aborted due to missing test runner" | |
| fi | |
| fi | |
| # Step 4: Update spec status if needed | |
| if [[ -n "$associated_spec" ]]; then | |
| info "Updating spec status..." | |
| local spec_file="$SPECS_BASE/$associated_spec/spec.md" | |
| if [[ -f "$spec_file" ]]; then | |
| # Update status to Complete | |
| sed -i '' 's/> Status: .*/> Status: Complete/' "$spec_file" | |
| success "Spec status updated to Complete" | |
| fi | |
| fi | |
| # Step 5: Merge to main | |
| info "Merging to $MAIN_BRANCH..." | |
| cd "$original_dir" | |
| git checkout "$MAIN_BRANCH" | |
| if [[ -n "$associated_spec" ]]; then | |
| git merge --no-ff "$branch_name" -m "Merge feature '$branch_name' - Complete spec: $associated_spec" | |
| else | |
| git merge --no-ff "$branch_name" -m "Merge feature '$branch_name'" | |
| fi | |
| # Step 6: Shut down any running server for this branch | |
| if [[ -n "$port" ]]; then | |
| info "Shutting down server on port $port..." | |
| # Find and kill any process listening on this port | |
| local server_pids=$(lsof -Pi :$port -sTCP:LISTEN -t 2>/dev/null) | |
| if [[ -n "$server_pids" ]]; then | |
| debug "Found server PIDs: $server_pids" | |
| echo "$server_pids" | while read -r pid; do | |
| if [[ -n "$pid" ]]; then | |
| kill -TERM "$pid" 2>/dev/null || true | |
| debug "Sent TERM signal to PID $pid" | |
| fi | |
| done | |
| # Give processes a moment to shut down gracefully | |
| sleep 1 | |
| # Force kill if still running | |
| local remaining_pids=$(lsof -Pi :$port -sTCP:LISTEN -t 2>/dev/null) | |
| if [[ -n "$remaining_pids" ]]; then | |
| echo "$remaining_pids" | while read -r pid; do | |
| if [[ -n "$pid" ]]; then | |
| kill -KILL "$pid" 2>/dev/null || true | |
| debug "Force killed PID $pid" | |
| fi | |
| done | |
| fi | |
| success "Server on port $port shut down" | |
| else | |
| debug "No server found running on port $port" | |
| fi | |
| fi | |
| # Step 7: Remove worktree | |
| info "Removing worktree..." | |
| debug "Running: git worktree remove $worktree_path" | |
| # Use --force if the normal remove fails (e.g., due to uncommitted changes we already warned about) | |
| if ! git worktree remove "$worktree_path" 2>/dev/null; then | |
| debug "Normal remove failed, trying with --force" | |
| git worktree remove "$worktree_path" --force | |
| fi | |
| debug "Worktree removed" | |
| # Step 8: Delete branch | |
| info "Deleting branch..." | |
| debug "Running: git branch -d $branch_name" | |
| git branch -d "$branch_name" | |
| debug "Branch deleted" | |
| # Step 9: Clean up branch port configuration | |
| info "Cleaning up branch port configuration..." | |
| # Remove the branch from .branch-config.yml | |
| if [[ -f ".branch-config.yml" ]]; then | |
| ruby -ryaml -e " | |
| config = YAML.load_file('.branch-config.yml') || {} | |
| config['branches']&.delete('$branch_name') | |
| File.write('.branch-config.yml', config.to_yaml) | |
| " 2>/dev/null || warning "Could not clean up branch port config" | |
| fi | |
| success "Feature '$branch_name' completed and merged successfully!" | |
| echo | |
| info "Remember to push your changes:" | |
| echo -e "${GREEN}git push origin $MAIN_BRANCH${NC}" | |
| } | |
| # Show usage information | |
| show_usage() { | |
| cat << EOF | |
| Usage: $(basename "$0") [OPTIONS] [feature-name] | |
| Git Worktree Feature Manager with Agent OS Integration | |
| Creates worktrees as siblings to the main project directory with the naming | |
| pattern: projectname-YYYY-MM-DD-feature-name | |
| Options: | |
| (no args) Interactive mode - list and manage worktrees | |
| <feature-name> Create a new feature worktree directly | |
| --spec [name] Create feature from Agent OS spec (interactive if no name) | |
| -h, --help Show this help message | |
| -v, --verbose Enable verbose/debug output | |
| Interactive Mode Commands: | |
| s# Switch to worktree (e.g., s1 for first worktree) | |
| f# Finish worktree (rebase, test, merge, remove) | |
| n Create new feature from spec | |
| m Create new feature (manual) | |
| 0 Exit | |
| Examples: | |
| $(basename "$0") # Enter interactive mode | |
| $(basename "$0") user-auth # Create user-auth feature | |
| $(basename "$0") "Password Reset" # Create password-reset feature | |
| $(basename "$0") --spec # Select spec interactively | |
| $(basename "$0") --spec workout-formats # Create from specific spec | |
| $(basename "$0") -v --spec # Show spec overviews | |
| Note: Worktrees are created as siblings to the current project directory. | |
| EOF | |
| } | |
| # Main script logic | |
| main() { | |
| # Parse arguments for verbose flag and spec option | |
| local args=() | |
| local use_spec=false | |
| local spec_name="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -v|--verbose) | |
| VERBOSE=true | |
| debug "Verbose mode enabled" | |
| shift | |
| ;; | |
| --spec) | |
| use_spec=true | |
| shift | |
| if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then | |
| spec_name="$1" | |
| shift | |
| fi | |
| ;; | |
| -h|--help) | |
| show_usage | |
| exit 0 | |
| ;; | |
| *) | |
| args+=("$1") | |
| shift | |
| ;; | |
| esac | |
| done | |
| debug "Script started with arguments: $*" | |
| debug "Worktree base: $WORKTREE_BASE" | |
| debug "Specs base: $SPECS_BASE" | |
| debug "Main branch: $MAIN_BRANCH" | |
| debug "Use spec: $use_spec" | |
| debug "Spec name: $spec_name" | |
| # Check prerequisites | |
| check_git_repo | |
| check_worktree_support | |
| # Handle spec option | |
| if [[ "$use_spec" == "true" ]]; then | |
| if [[ -n "$spec_name" ]]; then | |
| # Try to find spec matching the name | |
| local specs=($(list_specs)) | |
| local found_spec="" | |
| for spec in "${specs[@]}"; do | |
| local feature=$(extract_feature_from_spec "$spec") | |
| if [[ "$feature" == "$spec_name" ]] || [[ "$spec" == *"$spec_name"* ]]; then | |
| found_spec="$spec" | |
| break | |
| fi | |
| done | |
| if [[ -n "$found_spec" ]]; then | |
| local feature_name=$(extract_feature_from_spec "$found_spec") | |
| info "Found spec: $found_spec" | |
| create_feature_from_spec "$found_spec" "$feature_name" | |
| else | |
| error "No spec found matching: $spec_name" | |
| fi | |
| else | |
| # Interactive spec selection | |
| spec_selection_mode | |
| fi | |
| elif [[ ${#args[@]} -eq 0 ]]; then | |
| interactive_mode | |
| else | |
| create_feature "${args[*]}" | |
| fi | |
| } | |
| # Run main function | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment