From 93cb8e8511203a3d5d2ee53bcd919222de69f2f9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:30:22 +0000 Subject: [PATCH 1/5] feat: add git-wt worktree management to developer plugin Bundle the git-wt script from vojtabiberle/git-wt as a new skill and slash command in the developer plugin, enabling Claude to manage git worktrees for parallel development workflows. - Add git-worktree skill with SKILL.md and bundled git-wt script (v1.0.0) - Add /worktree slash command (add/rm/ls/help subcommands) - Allow git worktree and git wt bash permissions in settings template - Update plugin structure docs in README - Bump developer plugin to 1.8.0, marketplace to 1.7.0 Co-Authored-By: Martin Vasko --- .claude-plugin/marketplace.json | 4 +- plugins/developer/.claude-plugin/plugin.json | 2 +- plugins/developer/README.md | 38 +- plugins/developer/commands/worktree.md | 36 ++ .../developer/skills/git-worktree/SKILL.md | 159 +++++ .../skills/git-worktree/scripts/git-wt | 556 ++++++++++++++++++ plugins/developer/templates/settings.json | 2 + 7 files changed, 790 insertions(+), 7 deletions(-) create mode 100644 plugins/developer/commands/worktree.md create mode 100644 plugins/developer/skills/git-worktree/SKILL.md create mode 100755 plugins/developer/skills/git-worktree/scripts/git-wt diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3644ced..6460c49 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "keboola-claude-kit", - "version": "1.6.0", + "version": "1.7.0", "metadata": { "description": "Comprehensive Claude Kit marketplace for Keboola workflows including developer tools, component development, and data app building with specialized agents and best practices" }, @@ -20,7 +20,7 @@ { "name": "developer", "description": "Developer toolkit with specialized agents for code review, security analysis, and GitHub PR review processing", - "version": "1.7.1", + "version": "1.8.0", "source": "./plugins/developer", "category": "development" }, diff --git a/plugins/developer/.claude-plugin/plugin.json b/plugins/developer/.claude-plugin/plugin.json index 34550ef..5746143 100644 --- a/plugins/developer/.claude-plugin/plugin.json +++ b/plugins/developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "developer", - "version": "1.7.1", + "version": "1.8.0", "description": "Developer toolkit with specialized agents for code review and security analysis", "author": { "name": "Keboola :(){:|:&};: s.r.o.", diff --git a/plugins/developer/README.md b/plugins/developer/README.md index 91715e5..f8388d6 100644 --- a/plugins/developer/README.md +++ b/plugins/developer/README.md @@ -136,6 +136,31 @@ Analyzes your changes and creates a pull request with AI-generated title and des --- +### Worktree +**Command**: `/worktree [args]` + +Manage git worktrees using the bundled [git-wt](https://github.com/vojtabiberle/git-wt) helper script. + +**Subcommands:** +- `add [source]` — Create a worktree for a branch +- `rm [branch]` — Remove a worktree +- `ls` — List all worktrees +- `help` — Show help + +**Usage:** +```bash +# Create worktree for a feature branch from master +/worktree add feature/login master + +# List all worktrees +/worktree ls + +# Remove a worktree +/worktree rm feature/login +``` + +--- + ### Handle Conflicts **Command**: `/handle-conflicts` @@ -375,14 +400,19 @@ plugins/developer/ ├── commands/ │ ├── add-task.md # Slash command for quick task creation │ ├── create-pr.md # Slash command for PR creation -│ └── handle-conflicts.md # Slash command for merge conflicts +│ ├── handle-conflicts.md # Slash command for merge conflicts +│ └── worktree.md # Slash command for git worktree management ├── scripts/ │ └── context-progressbar.sh # Composable context window progress bar ├── skills/ -│ └── gh-process-review/ # GitHub PR review processing skill +│ ├── gh-process-review/ # GitHub PR review processing skill +│ │ ├── SKILL.md +│ │ └── scripts/ +│ │ └── review.sh # Single CLI entry point (fetch/list/get/reply/mark) +│ └── git-worktree/ # Git worktree management skill (git-wt) │ ├── SKILL.md │ └── scripts/ -│ └── review.sh # Single CLI entry point (fetch/list/get/reply/mark) +│ └── git-wt # Bundled git-wt script from vojtabiberle/git-wt └── README.md # This file ``` @@ -427,6 +457,6 @@ To add or improve agents: --- -**Version**: 1.6.0 +**Version**: 1.8.0 **Maintainer**: Keboola :(){:|:&};: s.r.o. **License**: MIT diff --git a/plugins/developer/commands/worktree.md b/plugins/developer/commands/worktree.md new file mode 100644 index 0000000..d558ac2 --- /dev/null +++ b/plugins/developer/commands/worktree.md @@ -0,0 +1,36 @@ +--- +name: worktree +description: Manage git worktrees using git-wt. Usage - /worktree add [source], /worktree rm [branch], /worktree ls, /worktree help +allowed-tools: Bash, Read, Write +--- + +# Git Worktree Management + +Manage git worktrees using the bundled git-wt script. + +## Instructions + +1. Validate we are inside a git repository: + ```bash + git rev-parse --is-inside-work-tree + ``` + +2. Determine the skill script path. The git-wt script is located at: + ``` + /skills/git-worktree/scripts/git-wt + ``` + Find it relative to this command file's plugin directory. + +3. Parse the user's arguments from `$ARGUMENTS`: + - `add [source]` — create a worktree + - `rm [branch]` — remove a worktree + - `ls` — list worktrees + - `help` — show help + +4. Run the git-wt script with `--non-interactive` flag to avoid interactive prompts: + ```bash + "$SKILL_DIR/scripts/git-wt" --non-interactive $ARGUMENTS + ``` + Where `$SKILL_DIR` is the `skills/git-worktree` directory within this plugin. + +5. Report the result to the user. For `add`, highlight the worktree path (last line of output). diff --git a/plugins/developer/skills/git-worktree/SKILL.md b/plugins/developer/skills/git-worktree/SKILL.md new file mode 100644 index 0000000..34b2709 --- /dev/null +++ b/plugins/developer/skills/git-worktree/SKILL.md @@ -0,0 +1,159 @@ +--- +name: git-worktree +description: Manage git worktrees using the git-wt helper script. Use when user asks to create, list, remove, or navigate worktrees, work in parallel on multiple branches, or set up isolated development environments. Triggers on phrases like "create worktree", "new worktree", "worktree for branch", "/worktree", "wt add", "wt rm", "wt ls". +--- + +# Git Worktree Management (git-wt) + +Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) helper script, bundled in this skill. + +## Working Directory Context + +**CRITICAL: All commands MUST be run from the user's project root directory, NOT from the skill directory.** + +- The user will be in THEIR project directory when invoking this skill +- All script calls use `$SKILL_DIR/scripts/git-wt` as the executable +- The script auto-detects the repo root from the current directory +- **DO NOT `cd` into the skill directory** + +`SKILL_DIR` = directory containing this SKILL.md (automatically resolved by Claude) + +## Setup + +The `git-wt` script is bundled in this skill. To use it, call it directly: + +```bash +"$SKILL_DIR/scripts/git-wt" [args...] +``` + +For non-interactive (CI/automated) use, pass `--non-interactive` or `--yes` to suppress prompts: + +```bash +"$SKILL_DIR/scripts/git-wt" --non-interactive [args...] +``` + +## Commands + +### Create a Worktree + +```bash +"$SKILL_DIR/scripts/git-wt" --non-interactive add [source-branch] +``` + +- Creates a worktree for ``, optionally from `[source-branch]` +- If the branch exists locally, checks it out into a new worktree +- If it exists only on `origin`, uses the remote branch (auto-confirmed with `--non-interactive`) +- If it doesn't exist anywhere, creates a new branch from HEAD or `[source-branch]` +- Runs any configured setup commands from `worktree.conf` / `worktree.conf.local` + +**Examples:** +```bash +# Create worktree for existing branch +"$SKILL_DIR/scripts/git-wt" --non-interactive add feature/login + +# Create worktree for new branch from master +"$SKILL_DIR/scripts/git-wt" --non-interactive add feature/new-feature master + +# Create worktree for new branch from specific base +"$SKILL_DIR/scripts/git-wt" --non-interactive add bugfix/fix-123 release/v2 +``` + +### List Worktrees + +```bash +"$SKILL_DIR/scripts/git-wt" ls +``` + +Lists all worktrees (`git worktree list`). + +### Remove a Worktree + +```bash +"$SKILL_DIR/scripts/git-wt" --non-interactive rm +"$SKILL_DIR/scripts/git-wt" --non-interactive rm # Removes current worktree (auto-detected) +"$SKILL_DIR/scripts/git-wt" --non-interactive rm --force # Force removal +``` + +- Runs any configured teardown commands before removal +- Without args, removes the current worktree if in one + +### Show Worktree Path + +```bash +"$SKILL_DIR/scripts/git-wt" cd +``` + +Prints the filesystem path of the worktree for ``. Useful for scripting. + +### Initialize Configuration + +```bash +"$SKILL_DIR/scripts/git-wt" init +``` + +Interactively creates `worktree.conf.local` in the repo root. + +### Help + +```bash +"$SKILL_DIR/scripts/git-wt" help +``` + +## Configuration + +The script uses two shell-sourceable config files in each project's repo root: + +**`worktree.conf`** (committed project defaults): +```bash +WORKTREE_DIR=".." +WORKTREE_PREFIX="myapp" +WORKTREE_SETUP=("./setup.sh") +WORKTREE_TEARDOWN=("./teardown.sh") +``` + +**`worktree.conf.local`** (personal overrides, gitignored): +```bash +WORKTREE_DIR="/home/me/worktrees" +WORKTREE_SETUP=("./setup.sh" "direnv allow") +WORKTREE_TEARDOWN=("docker compose down") +``` + +### Variables + +| Variable | Default | Description | +|---|---|---| +| `WORKTREE_DIR` | `..` | Base directory for worktrees (relative to repo root) | +| `WORKTREE_PREFIX` | repo name | Prefix for worktree directory names | +| `WORKTREE_SETUP` | `()` | Commands to run after creating a worktree | +| `WORKTREE_TEARDOWN` | `()` | Commands to run before removing a worktree | + +### Directory Naming + +Format: `/-` + +Branch `feature/login` with prefix `myapp` and base `..`: +``` +../myapp-feature-login +``` + +Sanitization: `/` becomes `-`, everything lowercased. + +## Typical Workflow + +1. **Create worktree** for a feature branch: + ```bash + "$SKILL_DIR/scripts/git-wt" --non-interactive add feature/my-branch master + ``` +2. **Work in the worktree** directory (printed as the last line of output) +3. **List worktrees** to see all active ones: + ```bash + "$SKILL_DIR/scripts/git-wt" ls + ``` +4. **Clean up** when done: + ```bash + "$SKILL_DIR/scripts/git-wt" --non-interactive rm feature/my-branch + ``` + +## Source + +Bundled from [vojtabiberle/git-wt](https://github.com/vojtabiberle/git-wt) (v1.0.0). diff --git a/plugins/developer/skills/git-worktree/scripts/git-wt b/plugins/developer/skills/git-worktree/scripts/git-wt new file mode 100755 index 0000000..65656f0 --- /dev/null +++ b/plugins/developer/skills/git-worktree/scripts/git-wt @@ -0,0 +1,556 @@ +#!/usr/bin/env bash +set -euo pipefail + +# git-wt: Git worktree helper +# Add this directory to PATH, or: git config --global alias.wt '!git-wt' +# For cd support, source git-wt.sh in your .zshrc / .bashrc + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly VERSION_FILE="$SCRIPT_DIR/VERSION" +if [[ -f "$VERSION_FILE" ]]; then + readonly VERSION="$(tr -d '[:space:]' < "$VERSION_FILE")" +else + readonly VERSION="unknown" + echo "Warning: Could not read version from $VERSION_FILE" >&2 +fi + +readonly UPDATE_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/git-wt/update-check" +readonly UPDATE_CHECK_INTERVAL=86400 # 24 hours +readonly VERSION_URL="https://raw.githubusercontent.com/vojtabiberle/git-wt/master/VERSION" + +# Colors (stderr only) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# Global flags +NON_INTERACTIVE=false +YES=false + +info() { echo -e "${GREEN}[wt]${NC} $*" >&2; } +warn() { echo -e "${YELLOW}[wt]${NC} $*" >&2; } +error() { echo -e "${RED}[wt]${NC} $*" >&2; } + +confirm_default_yes() { + local prompt="$1" + if [[ "$NON_INTERACTIVE" == true ]]; then + return 0 + else + read -rp "$prompt [Y/n] " answer + [[ "$answer" != [nN] ]] + fi +} + +confirm_default_no() { + local prompt="$1" + if [[ "$NON_INTERACTIVE" == true ]]; then + return 1 + else + read -rp "$prompt [y/N] " answer + [[ "$answer" == [yY] ]] + fi +} + +# --------------------------------------------------------------------------- +# Resolve repo root (works from worktrees too) +# --------------------------------------------------------------------------- +repo_root() { + git rev-parse --show-toplevel 2>/dev/null \ + || { error "Not inside a git repository"; exit 1; } +} + +main_repo_root() { + local common_dir + common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" + if [[ "$common_dir" == ".git" ]]; then + repo_root + else + dirname "$common_dir" + fi +} + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +load_config() { + local root + root="$(main_repo_root)" + + # Defaults + WORKTREE_DIR=".." + WORKTREE_PREFIX="" + WORKTREE_SETUP=() + WORKTREE_TEARDOWN=() + + # Auto-detect prefix from repo directory name + local repo_name + repo_name="$(basename "$root")" + WORKTREE_PREFIX="$repo_name" + + # Source committed config + if [[ -f "$root/worktree.conf" ]]; then + # shellcheck disable=SC1091 + source "$root/worktree.conf" + fi + + # Source local overrides + if [[ -f "$root/worktree.conf.local" ]]; then + # shellcheck disable=SC1091 + source "$root/worktree.conf.local" + fi + + # Resolve WORKTREE_DIR relative to main repo root + if [[ "$WORKTREE_DIR" != /* ]]; then + WORKTREE_DIR="$(cd "$root" && cd "$WORKTREE_DIR" && pwd)" + fi +} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +sanitize_branch() { + local branch="$1" + echo "$branch" | tr '/' '-' | tr '[:upper:]' '[:lower:]' +} + +worktree_path_for() { + local branch="$1" + local sanitized + sanitized="$(sanitize_branch "$branch")" + + local dirname + if [[ -n "$WORKTREE_PREFIX" ]]; then + dirname="${WORKTREE_PREFIX}-${sanitized}" + else + dirname="$sanitized" + fi + + echo "${WORKTREE_DIR}/${dirname}" +} + +branch_exists() { + git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null +} + +find_worktree_path() { + local branch="$1" + git worktree list --porcelain | awk -v branch="$branch" ' + /^worktree / { path = substr($0, 10) } + /^branch refs\/heads\// { + sub(/^branch refs\/heads\//, "") + if ($0 == branch) { print path; exit } + } + ' +} + +current_worktree_branch() { + local git_dir + git_dir="$(git rev-parse --git-dir 2>/dev/null)" + if [[ "$git_dir" != *".git/worktrees/"* ]]; then + return 1 + fi + git rev-parse --abbrev-ref HEAD 2>/dev/null +} + +# --------------------------------------------------------------------------- +# Subcommands +# --------------------------------------------------------------------------- +cmd_add() { + if [[ $# -lt 1 ]]; then + error "Usage: git wt add [source-branch]" + exit 1 + fi + + local branch="$1" + local source="${2:-}" + + load_config + + local wt_path + wt_path="$(worktree_path_for "$branch")" + + # Already exists? + # Use git worktree list to verify it's actually a registered worktree + local existing_path + existing_path="$(find_worktree_path "$branch")" + if [[ -n "$existing_path" ]]; then + info "Worktree already exists at: $existing_path" + echo "$existing_path" + return 0 + fi + + # Check if directory exists but is not a worktree + if [[ -d "$wt_path" ]]; then + error "Directory '$wt_path' exists but is not a registered worktree." + error "You may want to remove it manually or use 'git wt rm' to clean up." + exit 1 + fi + + # Create worktree + if branch_exists "$branch"; then + info "Branch '$branch' exists, checking out into worktree..." + git worktree add "$wt_path" "$branch" + else + # Check if branch exists on origin when no source was specified + if [[ -z "$source" ]] && git show-ref --verify --quiet "refs/remotes/origin/$branch" 2>/dev/null; then + warn "Branch '$branch' not found locally, but exists on ${CYAN}origin${NC}." + if ! confirm_default_yes "Use origin/$branch?"; then + source="" + else + source="origin/$branch" + fi + fi + + if [[ -n "$source" ]]; then + info "Creating branch '$branch' from '$source'..." + git worktree add -b "$branch" "$wt_path" "$source" + else + info "Creating branch '$branch' from HEAD..." + git worktree add -b "$branch" "$wt_path" + fi + fi + + # Run setup commands + if [[ ${#WORKTREE_SETUP[@]} -gt 0 ]]; then + info "Running setup commands..." + for cmd in "${WORKTREE_SETUP[@]}"; do + info " -> $cmd" + (cd "$wt_path" && eval "$cmd") + done + fi + + info "Worktree ready at: $wt_path" + # Print path as last line to stdout (for shell wrapper to capture) + echo "$wt_path" +} + +cmd_rm() { + local branch="" + local force=false + + while [[ $# -gt 0 ]]; do + case "$1" in + --force|-f) force=true; shift ;; + *) branch="$1"; shift ;; + esac + done + + if [[ -z "$branch" ]]; then + # Try to auto-detect from current worktree + branch="$(current_worktree_branch)" \ + || { error "Not in a worktree. Usage: git wt rm "; exit 1; } + info "Auto-detected current worktree branch: $branch" + fi + + local wt_path + wt_path="$(find_worktree_path "$branch")" + + if [[ -z "$wt_path" ]]; then + error "No worktree found for branch '$branch'" + exit 1 + fi + + load_config + + if [[ ${#WORKTREE_TEARDOWN[@]} -gt 0 ]]; then + info "Running teardown commands..." + for cmd in "${WORKTREE_TEARDOWN[@]}"; do + info " -> $cmd" + (cd "$wt_path" && eval "$cmd") + done + fi + + info "Removing worktree at: $wt_path" + if $force; then + git worktree remove --force "$wt_path" + else + git worktree remove "$wt_path" + fi + info "Worktree removed" +} + +cmd_cd() { + if [[ $# -lt 1 ]]; then + error "Usage: git wt cd " + exit 1 + fi + + local branch="$1" + local wt_path + wt_path="$(find_worktree_path "$branch")" + + if [[ -z "$wt_path" ]]; then + error "No worktree found for branch '$branch'" + exit 1 + fi + + # Print only the path — shell wrapper does the actual cd + echo "$wt_path" +} + +cmd_ls() { + git worktree list +} + +cmd_init() { + local root + root="$(main_repo_root)" + local conf_local="$root/worktree.conf.local" + + load_config + + if [[ -f "$conf_local" ]]; then + warn "Existing worktree.conf.local:" + cat "$conf_local" >&2 + echo >&2 + if ! confirm_default_no "Overwrite?"; then + info "Aborted" + return 0 + fi + fi + + local default_dir="$WORKTREE_DIR" + if ! confirm_default_yes "Use default directory for worktrees [$default_dir]?"; then + read -rp "Base directory for worktrees [$default_dir]: " input_dir + input_dir="${input_dir:-$default_dir}" + else + input_dir="$default_dir" + fi + + echo >&2 + info "Enter extra setup commands (one per line, empty line to finish)." + info "Default setup: ${WORKTREE_SETUP[*]:-}" + local extra_cmds=() + while true; do + if ! confirm_default_no "Add command?"; then + break + fi + read -rp " command> " cmd + [[ -z "$cmd" ]] && break + extra_cmds+=("$cmd") + done + + # Build setup array + local all_setup=() + if [[ ${#WORKTREE_SETUP[@]} -gt 0 ]]; then + all_setup=("${WORKTREE_SETUP[@]}") + fi + for cmd in "${extra_cmds[@]}"; do + all_setup+=("$cmd") + done + + echo >&2 + info "Enter teardown commands to run before worktree removal (empty line to finish)." + info "Default teardown: ${WORKTREE_TEARDOWN[*]:-}" + local extra_teardown_cmds=() + while true; do + if ! confirm_default_no "Add command?"; then + break + fi + read -rp " command> " cmd + [[ -z "$cmd" ]] && break + extra_teardown_cmds+=("$cmd") + done + + # Build teardown array + local all_teardown=() + if [[ ${#WORKTREE_TEARDOWN[@]} -gt 0 ]]; then + all_teardown=("${WORKTREE_TEARDOWN[@]}") + fi + for cmd in "${extra_teardown_cmds[@]}"; do + all_teardown+=("$cmd") + done + + # Write config + { + echo "# worktree.conf.local — personal overrides (gitignored)" + echo "# Generated by: git wt init" + echo "" + if [[ "$input_dir" != "$default_dir" ]]; then + echo "WORKTREE_DIR=\"$input_dir\"" + else + echo "# WORKTREE_DIR=\"$input_dir\"" + fi + echo "" + if [[ ${#all_setup[@]} -gt 0 ]]; then + echo "WORKTREE_SETUP=(" + for cmd in "${all_setup[@]}"; do + echo " \"$cmd\"" + done + echo ")" + fi + if [[ ${#all_teardown[@]} -gt 0 ]]; then + echo "" + echo "WORKTREE_TEARDOWN=(" + for cmd in "${all_teardown[@]}"; do + echo " \"$cmd\"" + done + echo ")" + fi + } > "$conf_local" + + info "Written: $conf_local" + cat "$conf_local" >&2 +} + +cmd_help() { + cat >&2 <<'HELP' +git-wt — Git worktree helper + +USAGE + git wt [args...] + +COMMANDS + add [source] Create a worktree for . + If [source] is given, the new branch starts there. + rm [branch] Remove a worktree. Without args, removes the + current worktree (auto-detected). + cd Print the worktree path (use shell wrapper for cd). + ls List all worktrees (git worktree list). + init Interactively create worktree.conf.local. + help, --help Show this help. + +CONFIGURATION + Per-project config files (in repo root): + worktree.conf Committed project defaults. + worktree.conf.local Personal overrides, gitignored. + + Variables (set in config files): + WORKTREE_DIR="..." Base directory (default: "..") + WORKTREE_PREFIX="..." Directory name prefix (default: repo name) + WORKTREE_SETUP=(...) Commands to run after creating a worktree + WORKTREE_TEARDOWN=(...) Commands to run before removing a worktree + + Directory naming: /- + Example: ../myapp-feature-login + + Sanitization: / becomes -, everything lowercased. + +INSTALLATION + # Add to PATH: + export PATH="$HOME/Projects/git-wt:$PATH" + + # Source shell wrapper for cd support: + source ~/Projects/git-wt/git-wt.sh + + # Or use a git alias (no cd support): + git config --global alias.wt '!git-wt' + +SHELL WRAPPER (for cd support) + Add to .zshrc or .bashrc: + source ~/Projects/git-wt/git-wt.sh + + This defines the wt function that wraps git wt and adds cd support. + +AUTOMATION + --non-interactive or --yes: Suppress prompts, use default behavior. + --force: Force overwrite or removal without confirmation. + +EXAMPLES + git wt add feature/login master + git wt ls + git wt rm feature/login + git wt init + + # With shell wrapper: + wt add feature/login master # creates + cd's into worktree + wt cd feature/login # cd into existing worktree +HELP +} + +# --------------------------------------------------------------------------- +# Update check +# --------------------------------------------------------------------------- +version_gt() { + [[ "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" == "$1" && "$1" != "$2" ]] +} + +check_for_update() { + mkdir -p "$(dirname "$UPDATE_CACHE")" + + # Skip if checked recently + if [[ -f "$UPDATE_CACHE" ]]; then + local last_check now + last_check="$(stat -c %Y "$UPDATE_CACHE" 2>/dev/null || stat -f %m "$UPDATE_CACHE" 2>/dev/null || echo 0)" + now="$(date +%s)" + if (( now - last_check < UPDATE_CHECK_INTERVAL )); then + return + fi + fi + + # Fetch remote version in background, update cache + ( + remote_version="$(curl -fsSL --max-time 5 "$VERSION_URL" 2>/dev/null | tr -d '[:space:]')" || exit 0 + if [[ -n "$remote_version" ]]; then + echo "$remote_version" > "$UPDATE_CACHE" + fi + ) &>/dev/null & + disown 2>/dev/null +} + +show_update_notice() { + if [[ -f "$UPDATE_CACHE" ]]; then + local remote_version + remote_version="$(tr -d '[:space:]' < "$UPDATE_CACHE")" + if version_gt "$remote_version" "$VERSION"; then + warn "Update available: ${BOLD}${VERSION}${NC} → ${GREEN}${remote_version}${NC} (${CYAN}${VERSION_URL%/VERSION}${NC})" + fi + fi +} + +# --------------------------------------------------------------------------- +# Main dispatch +# --------------------------------------------------------------------------- +main() { + show_update_notice + check_for_update + + local cmd="" + local args=() + + # Parse all arguments, extracting global flags from any position + while [[ $# -gt 0 ]]; do + case "$1" in + --non-interactive|--yes) + NON_INTERACTIVE=true + YES=true + ;; + --help|-h) + cmd="help" + ;; + -*) + # Pass unknown flags through to subcommand + args+=("$1") + ;; + *) + if [[ -z "$cmd" ]]; then + cmd="$1" + else + args+=("$1") + fi + ;; + esac + shift + done + + cmd="${cmd:-help}" + + case "$cmd" in + add) cmd_add ${args[@]+"${args[@]}"} ;; + rm) cmd_rm ${args[@]+"${args[@]}"} ;; + cd) cmd_cd ${args[@]+"${args[@]}"} ;; + ls) cmd_ls ${args[@]+"${args[@]}"} ;; + init) cmd_init ${args[@]+"${args[@]}"} ;; + help) cmd_help ;; + *) + error "Unknown command: $cmd" + cmd_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/plugins/developer/templates/settings.json b/plugins/developer/templates/settings.json index 74e6000..e0f89c7 100644 --- a/plugins/developer/templates/settings.json +++ b/plugins/developer/templates/settings.json @@ -14,6 +14,8 @@ "Bash(git status:*)", "Bash(git remote:*)", "Bash(git tag:*)", + "Bash(git worktree:*)", + "Bash(git wt:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(ls:*)", From 55e37709a3655cc53f7165f352bd770333c6c4d3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:31:33 +0000 Subject: [PATCH 2/5] feat: add VERSION file and LICENSE attribution for bundled git-wt Co-Authored-By: Martin Vasko --- plugins/developer/skills/git-worktree/LICENSE | 23 +++++++++++++++++++ .../skills/git-worktree/scripts/VERSION | 1 + 2 files changed, 24 insertions(+) create mode 100644 plugins/developer/skills/git-worktree/LICENSE create mode 100644 plugins/developer/skills/git-worktree/scripts/VERSION diff --git a/plugins/developer/skills/git-worktree/LICENSE b/plugins/developer/skills/git-worktree/LICENSE new file mode 100644 index 0000000..55b0563 --- /dev/null +++ b/plugins/developer/skills/git-worktree/LICENSE @@ -0,0 +1,23 @@ +Bundled from https://github.com/vojtabiberle/git-wt (v1.0.0) + +MIT License + +Copyright (c) vojtabiberle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/developer/skills/git-worktree/scripts/VERSION b/plugins/developer/skills/git-worktree/scripts/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/plugins/developer/skills/git-worktree/scripts/VERSION @@ -0,0 +1 @@ +1.0.0 From ca5ba79a7212f04011644c780b966ae2862ae9d6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:37:37 +0000 Subject: [PATCH 3/5] refactor: fetch git-wt from upstream instead of bundling it Replace the bundled git-wt script with an ensure-git-wt.sh setup script that clones/pulls vojtabiberle/git-wt to ~/.local/share/git-wt on each use. This keeps the tool always up-to-date with upstream. Co-Authored-By: Martin Vasko --- plugins/developer/README.md | 2 +- plugins/developer/commands/worktree.md | 15 +- plugins/developer/skills/git-worktree/LICENSE | 23 - .../developer/skills/git-worktree/SKILL.md | 59 +- .../skills/git-worktree/scripts/VERSION | 1 - .../git-worktree/scripts/ensure-git-wt.sh | 17 + .../skills/git-worktree/scripts/git-wt | 556 ------------------ 7 files changed, 57 insertions(+), 616 deletions(-) delete mode 100644 plugins/developer/skills/git-worktree/LICENSE delete mode 100644 plugins/developer/skills/git-worktree/scripts/VERSION create mode 100755 plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh delete mode 100755 plugins/developer/skills/git-worktree/scripts/git-wt diff --git a/plugins/developer/README.md b/plugins/developer/README.md index f8388d6..0286a79 100644 --- a/plugins/developer/README.md +++ b/plugins/developer/README.md @@ -412,7 +412,7 @@ plugins/developer/ │ └── git-worktree/ # Git worktree management skill (git-wt) │ ├── SKILL.md │ └── scripts/ -│ └── git-wt # Bundled git-wt script from vojtabiberle/git-wt +│ └── ensure-git-wt.sh # Clones/pulls vojtabiberle/git-wt before use └── README.md # This file ``` diff --git a/plugins/developer/commands/worktree.md b/plugins/developer/commands/worktree.md index d558ac2..c60da01 100644 --- a/plugins/developer/commands/worktree.md +++ b/plugins/developer/commands/worktree.md @@ -6,7 +6,7 @@ allowed-tools: Bash, Read, Write # Git Worktree Management -Manage git worktrees using the bundled git-wt script. +Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) script, fetched from upstream. ## Instructions @@ -15,11 +15,15 @@ Manage git worktrees using the bundled git-wt script. git rev-parse --is-inside-work-tree ``` -2. Determine the skill script path. The git-wt script is located at: +2. Fetch or update the git-wt script. The setup script is located at: ``` - /skills/git-worktree/scripts/git-wt + /skills/git-worktree/scripts/ensure-git-wt.sh ``` - Find it relative to this command file's plugin directory. + Run it to clone/pull the repo and get the executable path: + ```bash + GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" + ``` + Where `$SKILL_DIR` is the `skills/git-worktree` directory within this plugin. 3. Parse the user's arguments from `$ARGUMENTS`: - `add [source]` — create a worktree @@ -29,8 +33,7 @@ Manage git worktrees using the bundled git-wt script. 4. Run the git-wt script with `--non-interactive` flag to avoid interactive prompts: ```bash - "$SKILL_DIR/scripts/git-wt" --non-interactive $ARGUMENTS + "$GIT_WT" --non-interactive $ARGUMENTS ``` - Where `$SKILL_DIR` is the `skills/git-worktree` directory within this plugin. 5. Report the result to the user. For `add`, highlight the worktree path (last line of output). diff --git a/plugins/developer/skills/git-worktree/LICENSE b/plugins/developer/skills/git-worktree/LICENSE deleted file mode 100644 index 55b0563..0000000 --- a/plugins/developer/skills/git-worktree/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Bundled from https://github.com/vojtabiberle/git-wt (v1.0.0) - -MIT License - -Copyright (c) vojtabiberle - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/plugins/developer/skills/git-worktree/SKILL.md b/plugins/developer/skills/git-worktree/SKILL.md index 34b2709..cace515 100644 --- a/plugins/developer/skills/git-worktree/SKILL.md +++ b/plugins/developer/skills/git-worktree/SKILL.md @@ -5,39 +5,36 @@ description: Manage git worktrees using the git-wt helper script. Use when user # Git Worktree Management (git-wt) -Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) helper script, bundled in this skill. +Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) helper script, fetched from upstream on each use. ## Working Directory Context **CRITICAL: All commands MUST be run from the user's project root directory, NOT from the skill directory.** - The user will be in THEIR project directory when invoking this skill -- All script calls use `$SKILL_DIR/scripts/git-wt` as the executable -- The script auto-detects the repo root from the current directory +- The setup script clones/pulls git-wt to `~/.local/share/git-wt` and prints the executable path - **DO NOT `cd` into the skill directory** `SKILL_DIR` = directory containing this SKILL.md (automatically resolved by Claude) -## Setup +## Setup (run once per session) -The `git-wt` script is bundled in this skill. To use it, call it directly: +Before using git-wt, fetch or update it: ```bash -"$SKILL_DIR/scripts/git-wt" [args...] +GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" ``` -For non-interactive (CI/automated) use, pass `--non-interactive` or `--yes` to suppress prompts: +This clones `vojtabiberle/git-wt` to `~/.local/share/git-wt` (or pulls latest if already cloned). The script prints the path to the `git-wt` executable on stdout. -```bash -"$SKILL_DIR/scripts/git-wt" --non-interactive [args...] -``` +All subsequent commands use `$GIT_WT`. Always pass `--non-interactive` to suppress prompts. ## Commands ### Create a Worktree ```bash -"$SKILL_DIR/scripts/git-wt" --non-interactive add [source-branch] +"$GIT_WT" --non-interactive add [source-branch] ``` - Creates a worktree for ``, optionally from `[source-branch]` @@ -49,19 +46,19 @@ For non-interactive (CI/automated) use, pass `--non-interactive` or `--yes` to s **Examples:** ```bash # Create worktree for existing branch -"$SKILL_DIR/scripts/git-wt" --non-interactive add feature/login +"$GIT_WT" --non-interactive add feature/login # Create worktree for new branch from master -"$SKILL_DIR/scripts/git-wt" --non-interactive add feature/new-feature master +"$GIT_WT" --non-interactive add feature/new-feature master # Create worktree for new branch from specific base -"$SKILL_DIR/scripts/git-wt" --non-interactive add bugfix/fix-123 release/v2 +"$GIT_WT" --non-interactive add bugfix/fix-123 release/v2 ``` ### List Worktrees ```bash -"$SKILL_DIR/scripts/git-wt" ls +"$GIT_WT" ls ``` Lists all worktrees (`git worktree list`). @@ -69,9 +66,9 @@ Lists all worktrees (`git worktree list`). ### Remove a Worktree ```bash -"$SKILL_DIR/scripts/git-wt" --non-interactive rm -"$SKILL_DIR/scripts/git-wt" --non-interactive rm # Removes current worktree (auto-detected) -"$SKILL_DIR/scripts/git-wt" --non-interactive rm --force # Force removal +"$GIT_WT" --non-interactive rm +"$GIT_WT" --non-interactive rm # Removes current worktree (auto-detected) +"$GIT_WT" --non-interactive rm --force # Force removal ``` - Runs any configured teardown commands before removal @@ -80,7 +77,7 @@ Lists all worktrees (`git worktree list`). ### Show Worktree Path ```bash -"$SKILL_DIR/scripts/git-wt" cd +"$GIT_WT" cd ``` Prints the filesystem path of the worktree for ``. Useful for scripting. @@ -88,7 +85,7 @@ Prints the filesystem path of the worktree for ``. Useful for scripting. ### Initialize Configuration ```bash -"$SKILL_DIR/scripts/git-wt" init +"$GIT_WT" init ``` Interactively creates `worktree.conf.local` in the repo root. @@ -96,7 +93,7 @@ Interactively creates `worktree.conf.local` in the repo root. ### Help ```bash -"$SKILL_DIR/scripts/git-wt" help +"$GIT_WT" help ``` ## Configuration @@ -140,20 +137,24 @@ Sanitization: `/` becomes `-`, everything lowercased. ## Typical Workflow -1. **Create worktree** for a feature branch: +1. **Fetch git-wt** (once per session): + ```bash + GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" + ``` +2. **Create worktree** for a feature branch: ```bash - "$SKILL_DIR/scripts/git-wt" --non-interactive add feature/my-branch master + "$GIT_WT" --non-interactive add feature/my-branch master ``` -2. **Work in the worktree** directory (printed as the last line of output) -3. **List worktrees** to see all active ones: +3. **Work in the worktree** directory (printed as the last line of output) +4. **List worktrees** to see all active ones: ```bash - "$SKILL_DIR/scripts/git-wt" ls + "$GIT_WT" ls ``` -4. **Clean up** when done: +5. **Clean up** when done: ```bash - "$SKILL_DIR/scripts/git-wt" --non-interactive rm feature/my-branch + "$GIT_WT" --non-interactive rm feature/my-branch ``` ## Source -Bundled from [vojtabiberle/git-wt](https://github.com/vojtabiberle/git-wt) (v1.0.0). +Fetched from [vojtabiberle/git-wt](https://github.com/vojtabiberle/git-wt) (MIT License). diff --git a/plugins/developer/skills/git-worktree/scripts/VERSION b/plugins/developer/skills/git-worktree/scripts/VERSION deleted file mode 100644 index 3eefcb9..0000000 --- a/plugins/developer/skills/git-worktree/scripts/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0.0 diff --git a/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh b/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh new file mode 100755 index 0000000..93401cd --- /dev/null +++ b/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ensure-git-wt.sh — Clone or pull the git-wt repo, then print its path. +# Usage: eval "$(./ensure-git-wt.sh)" or GIT_WT="$(./ensure-git-wt.sh)" + +readonly GIT_WT_REPO="https://github.com/vojtabiberle/git-wt.git" +readonly GIT_WT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/git-wt" + +if [[ -d "$GIT_WT_DIR/.git" ]]; then + git -C "$GIT_WT_DIR" pull --quiet >&2 +else + git clone --quiet "$GIT_WT_REPO" "$GIT_WT_DIR" >&2 +fi + +# Print the path to the git-wt executable on stdout +echo "$GIT_WT_DIR/git-wt" diff --git a/plugins/developer/skills/git-worktree/scripts/git-wt b/plugins/developer/skills/git-worktree/scripts/git-wt deleted file mode 100755 index 65656f0..0000000 --- a/plugins/developer/skills/git-worktree/scripts/git-wt +++ /dev/null @@ -1,556 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# git-wt: Git worktree helper -# Add this directory to PATH, or: git config --global alias.wt '!git-wt' -# For cd support, source git-wt.sh in your .zshrc / .bashrc - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly VERSION_FILE="$SCRIPT_DIR/VERSION" -if [[ -f "$VERSION_FILE" ]]; then - readonly VERSION="$(tr -d '[:space:]' < "$VERSION_FILE")" -else - readonly VERSION="unknown" - echo "Warning: Could not read version from $VERSION_FILE" >&2 -fi - -readonly UPDATE_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/git-wt/update-check" -readonly UPDATE_CHECK_INTERVAL=86400 # 24 hours -readonly VERSION_URL="https://raw.githubusercontent.com/vojtabiberle/git-wt/master/VERSION" - -# Colors (stderr only) -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -# Global flags -NON_INTERACTIVE=false -YES=false - -info() { echo -e "${GREEN}[wt]${NC} $*" >&2; } -warn() { echo -e "${YELLOW}[wt]${NC} $*" >&2; } -error() { echo -e "${RED}[wt]${NC} $*" >&2; } - -confirm_default_yes() { - local prompt="$1" - if [[ "$NON_INTERACTIVE" == true ]]; then - return 0 - else - read -rp "$prompt [Y/n] " answer - [[ "$answer" != [nN] ]] - fi -} - -confirm_default_no() { - local prompt="$1" - if [[ "$NON_INTERACTIVE" == true ]]; then - return 1 - else - read -rp "$prompt [y/N] " answer - [[ "$answer" == [yY] ]] - fi -} - -# --------------------------------------------------------------------------- -# Resolve repo root (works from worktrees too) -# --------------------------------------------------------------------------- -repo_root() { - git rev-parse --show-toplevel 2>/dev/null \ - || { error "Not inside a git repository"; exit 1; } -} - -main_repo_root() { - local common_dir - common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" - if [[ "$common_dir" == ".git" ]]; then - repo_root - else - dirname "$common_dir" - fi -} - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -load_config() { - local root - root="$(main_repo_root)" - - # Defaults - WORKTREE_DIR=".." - WORKTREE_PREFIX="" - WORKTREE_SETUP=() - WORKTREE_TEARDOWN=() - - # Auto-detect prefix from repo directory name - local repo_name - repo_name="$(basename "$root")" - WORKTREE_PREFIX="$repo_name" - - # Source committed config - if [[ -f "$root/worktree.conf" ]]; then - # shellcheck disable=SC1091 - source "$root/worktree.conf" - fi - - # Source local overrides - if [[ -f "$root/worktree.conf.local" ]]; then - # shellcheck disable=SC1091 - source "$root/worktree.conf.local" - fi - - # Resolve WORKTREE_DIR relative to main repo root - if [[ "$WORKTREE_DIR" != /* ]]; then - WORKTREE_DIR="$(cd "$root" && cd "$WORKTREE_DIR" && pwd)" - fi -} - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- -sanitize_branch() { - local branch="$1" - echo "$branch" | tr '/' '-' | tr '[:upper:]' '[:lower:]' -} - -worktree_path_for() { - local branch="$1" - local sanitized - sanitized="$(sanitize_branch "$branch")" - - local dirname - if [[ -n "$WORKTREE_PREFIX" ]]; then - dirname="${WORKTREE_PREFIX}-${sanitized}" - else - dirname="$sanitized" - fi - - echo "${WORKTREE_DIR}/${dirname}" -} - -branch_exists() { - git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null -} - -find_worktree_path() { - local branch="$1" - git worktree list --porcelain | awk -v branch="$branch" ' - /^worktree / { path = substr($0, 10) } - /^branch refs\/heads\// { - sub(/^branch refs\/heads\//, "") - if ($0 == branch) { print path; exit } - } - ' -} - -current_worktree_branch() { - local git_dir - git_dir="$(git rev-parse --git-dir 2>/dev/null)" - if [[ "$git_dir" != *".git/worktrees/"* ]]; then - return 1 - fi - git rev-parse --abbrev-ref HEAD 2>/dev/null -} - -# --------------------------------------------------------------------------- -# Subcommands -# --------------------------------------------------------------------------- -cmd_add() { - if [[ $# -lt 1 ]]; then - error "Usage: git wt add [source-branch]" - exit 1 - fi - - local branch="$1" - local source="${2:-}" - - load_config - - local wt_path - wt_path="$(worktree_path_for "$branch")" - - # Already exists? - # Use git worktree list to verify it's actually a registered worktree - local existing_path - existing_path="$(find_worktree_path "$branch")" - if [[ -n "$existing_path" ]]; then - info "Worktree already exists at: $existing_path" - echo "$existing_path" - return 0 - fi - - # Check if directory exists but is not a worktree - if [[ -d "$wt_path" ]]; then - error "Directory '$wt_path' exists but is not a registered worktree." - error "You may want to remove it manually or use 'git wt rm' to clean up." - exit 1 - fi - - # Create worktree - if branch_exists "$branch"; then - info "Branch '$branch' exists, checking out into worktree..." - git worktree add "$wt_path" "$branch" - else - # Check if branch exists on origin when no source was specified - if [[ -z "$source" ]] && git show-ref --verify --quiet "refs/remotes/origin/$branch" 2>/dev/null; then - warn "Branch '$branch' not found locally, but exists on ${CYAN}origin${NC}." - if ! confirm_default_yes "Use origin/$branch?"; then - source="" - else - source="origin/$branch" - fi - fi - - if [[ -n "$source" ]]; then - info "Creating branch '$branch' from '$source'..." - git worktree add -b "$branch" "$wt_path" "$source" - else - info "Creating branch '$branch' from HEAD..." - git worktree add -b "$branch" "$wt_path" - fi - fi - - # Run setup commands - if [[ ${#WORKTREE_SETUP[@]} -gt 0 ]]; then - info "Running setup commands..." - for cmd in "${WORKTREE_SETUP[@]}"; do - info " -> $cmd" - (cd "$wt_path" && eval "$cmd") - done - fi - - info "Worktree ready at: $wt_path" - # Print path as last line to stdout (for shell wrapper to capture) - echo "$wt_path" -} - -cmd_rm() { - local branch="" - local force=false - - while [[ $# -gt 0 ]]; do - case "$1" in - --force|-f) force=true; shift ;; - *) branch="$1"; shift ;; - esac - done - - if [[ -z "$branch" ]]; then - # Try to auto-detect from current worktree - branch="$(current_worktree_branch)" \ - || { error "Not in a worktree. Usage: git wt rm "; exit 1; } - info "Auto-detected current worktree branch: $branch" - fi - - local wt_path - wt_path="$(find_worktree_path "$branch")" - - if [[ -z "$wt_path" ]]; then - error "No worktree found for branch '$branch'" - exit 1 - fi - - load_config - - if [[ ${#WORKTREE_TEARDOWN[@]} -gt 0 ]]; then - info "Running teardown commands..." - for cmd in "${WORKTREE_TEARDOWN[@]}"; do - info " -> $cmd" - (cd "$wt_path" && eval "$cmd") - done - fi - - info "Removing worktree at: $wt_path" - if $force; then - git worktree remove --force "$wt_path" - else - git worktree remove "$wt_path" - fi - info "Worktree removed" -} - -cmd_cd() { - if [[ $# -lt 1 ]]; then - error "Usage: git wt cd " - exit 1 - fi - - local branch="$1" - local wt_path - wt_path="$(find_worktree_path "$branch")" - - if [[ -z "$wt_path" ]]; then - error "No worktree found for branch '$branch'" - exit 1 - fi - - # Print only the path — shell wrapper does the actual cd - echo "$wt_path" -} - -cmd_ls() { - git worktree list -} - -cmd_init() { - local root - root="$(main_repo_root)" - local conf_local="$root/worktree.conf.local" - - load_config - - if [[ -f "$conf_local" ]]; then - warn "Existing worktree.conf.local:" - cat "$conf_local" >&2 - echo >&2 - if ! confirm_default_no "Overwrite?"; then - info "Aborted" - return 0 - fi - fi - - local default_dir="$WORKTREE_DIR" - if ! confirm_default_yes "Use default directory for worktrees [$default_dir]?"; then - read -rp "Base directory for worktrees [$default_dir]: " input_dir - input_dir="${input_dir:-$default_dir}" - else - input_dir="$default_dir" - fi - - echo >&2 - info "Enter extra setup commands (one per line, empty line to finish)." - info "Default setup: ${WORKTREE_SETUP[*]:-}" - local extra_cmds=() - while true; do - if ! confirm_default_no "Add command?"; then - break - fi - read -rp " command> " cmd - [[ -z "$cmd" ]] && break - extra_cmds+=("$cmd") - done - - # Build setup array - local all_setup=() - if [[ ${#WORKTREE_SETUP[@]} -gt 0 ]]; then - all_setup=("${WORKTREE_SETUP[@]}") - fi - for cmd in "${extra_cmds[@]}"; do - all_setup+=("$cmd") - done - - echo >&2 - info "Enter teardown commands to run before worktree removal (empty line to finish)." - info "Default teardown: ${WORKTREE_TEARDOWN[*]:-}" - local extra_teardown_cmds=() - while true; do - if ! confirm_default_no "Add command?"; then - break - fi - read -rp " command> " cmd - [[ -z "$cmd" ]] && break - extra_teardown_cmds+=("$cmd") - done - - # Build teardown array - local all_teardown=() - if [[ ${#WORKTREE_TEARDOWN[@]} -gt 0 ]]; then - all_teardown=("${WORKTREE_TEARDOWN[@]}") - fi - for cmd in "${extra_teardown_cmds[@]}"; do - all_teardown+=("$cmd") - done - - # Write config - { - echo "# worktree.conf.local — personal overrides (gitignored)" - echo "# Generated by: git wt init" - echo "" - if [[ "$input_dir" != "$default_dir" ]]; then - echo "WORKTREE_DIR=\"$input_dir\"" - else - echo "# WORKTREE_DIR=\"$input_dir\"" - fi - echo "" - if [[ ${#all_setup[@]} -gt 0 ]]; then - echo "WORKTREE_SETUP=(" - for cmd in "${all_setup[@]}"; do - echo " \"$cmd\"" - done - echo ")" - fi - if [[ ${#all_teardown[@]} -gt 0 ]]; then - echo "" - echo "WORKTREE_TEARDOWN=(" - for cmd in "${all_teardown[@]}"; do - echo " \"$cmd\"" - done - echo ")" - fi - } > "$conf_local" - - info "Written: $conf_local" - cat "$conf_local" >&2 -} - -cmd_help() { - cat >&2 <<'HELP' -git-wt — Git worktree helper - -USAGE - git wt [args...] - -COMMANDS - add [source] Create a worktree for . - If [source] is given, the new branch starts there. - rm [branch] Remove a worktree. Without args, removes the - current worktree (auto-detected). - cd Print the worktree path (use shell wrapper for cd). - ls List all worktrees (git worktree list). - init Interactively create worktree.conf.local. - help, --help Show this help. - -CONFIGURATION - Per-project config files (in repo root): - worktree.conf Committed project defaults. - worktree.conf.local Personal overrides, gitignored. - - Variables (set in config files): - WORKTREE_DIR="..." Base directory (default: "..") - WORKTREE_PREFIX="..." Directory name prefix (default: repo name) - WORKTREE_SETUP=(...) Commands to run after creating a worktree - WORKTREE_TEARDOWN=(...) Commands to run before removing a worktree - - Directory naming: /- - Example: ../myapp-feature-login - - Sanitization: / becomes -, everything lowercased. - -INSTALLATION - # Add to PATH: - export PATH="$HOME/Projects/git-wt:$PATH" - - # Source shell wrapper for cd support: - source ~/Projects/git-wt/git-wt.sh - - # Or use a git alias (no cd support): - git config --global alias.wt '!git-wt' - -SHELL WRAPPER (for cd support) - Add to .zshrc or .bashrc: - source ~/Projects/git-wt/git-wt.sh - - This defines the wt function that wraps git wt and adds cd support. - -AUTOMATION - --non-interactive or --yes: Suppress prompts, use default behavior. - --force: Force overwrite or removal without confirmation. - -EXAMPLES - git wt add feature/login master - git wt ls - git wt rm feature/login - git wt init - - # With shell wrapper: - wt add feature/login master # creates + cd's into worktree - wt cd feature/login # cd into existing worktree -HELP -} - -# --------------------------------------------------------------------------- -# Update check -# --------------------------------------------------------------------------- -version_gt() { - [[ "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" == "$1" && "$1" != "$2" ]] -} - -check_for_update() { - mkdir -p "$(dirname "$UPDATE_CACHE")" - - # Skip if checked recently - if [[ -f "$UPDATE_CACHE" ]]; then - local last_check now - last_check="$(stat -c %Y "$UPDATE_CACHE" 2>/dev/null || stat -f %m "$UPDATE_CACHE" 2>/dev/null || echo 0)" - now="$(date +%s)" - if (( now - last_check < UPDATE_CHECK_INTERVAL )); then - return - fi - fi - - # Fetch remote version in background, update cache - ( - remote_version="$(curl -fsSL --max-time 5 "$VERSION_URL" 2>/dev/null | tr -d '[:space:]')" || exit 0 - if [[ -n "$remote_version" ]]; then - echo "$remote_version" > "$UPDATE_CACHE" - fi - ) &>/dev/null & - disown 2>/dev/null -} - -show_update_notice() { - if [[ -f "$UPDATE_CACHE" ]]; then - local remote_version - remote_version="$(tr -d '[:space:]' < "$UPDATE_CACHE")" - if version_gt "$remote_version" "$VERSION"; then - warn "Update available: ${BOLD}${VERSION}${NC} → ${GREEN}${remote_version}${NC} (${CYAN}${VERSION_URL%/VERSION}${NC})" - fi - fi -} - -# --------------------------------------------------------------------------- -# Main dispatch -# --------------------------------------------------------------------------- -main() { - show_update_notice - check_for_update - - local cmd="" - local args=() - - # Parse all arguments, extracting global flags from any position - while [[ $# -gt 0 ]]; do - case "$1" in - --non-interactive|--yes) - NON_INTERACTIVE=true - YES=true - ;; - --help|-h) - cmd="help" - ;; - -*) - # Pass unknown flags through to subcommand - args+=("$1") - ;; - *) - if [[ -z "$cmd" ]]; then - cmd="$1" - else - args+=("$1") - fi - ;; - esac - shift - done - - cmd="${cmd:-help}" - - case "$cmd" in - add) cmd_add ${args[@]+"${args[@]}"} ;; - rm) cmd_rm ${args[@]+"${args[@]}"} ;; - cd) cmd_cd ${args[@]+"${args[@]}"} ;; - ls) cmd_ls ${args[@]+"${args[@]}"} ;; - init) cmd_init ${args[@]+"${args[@]}"} ;; - help) cmd_help ;; - *) - error "Unknown command: $cmd" - cmd_help - exit 1 - ;; - esac -} - -main "$@" From a983971e6b1f71d926c60e025fcd3df4c3bd6946 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:38:31 +0000 Subject: [PATCH 4/5] fix: update README wording from bundled to fetched Co-Authored-By: Martin Vasko --- plugins/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/developer/README.md b/plugins/developer/README.md index 0286a79..1606d36 100644 --- a/plugins/developer/README.md +++ b/plugins/developer/README.md @@ -139,7 +139,7 @@ Analyzes your changes and creates a pull request with AI-generated title and des ### Worktree **Command**: `/worktree [args]` -Manage git worktrees using the bundled [git-wt](https://github.com/vojtabiberle/git-wt) helper script. +Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) helper script, fetched from upstream at runtime. **Subcommands:** - `add [source]` — Create a worktree for a branch From f0090765af886aec2fd3c3b247b87e7ca7949401 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 06:30:11 +0000 Subject: [PATCH 5/5] fix: address Copilot review feedback - Pin ensure-git-wt.sh to specific commit (f44fbfe) for supply-chain safety - Use fetch+checkout instead of pull for resilient updates - Add error handling with fallback for network failures - Verify git-wt executable exists after clone - Remove unused Bash(git wt:*) permission from settings.json - Fix slash command: remove dependency, use inline clone logic - Fix slash command: validate subcommands and safely pass arguments - Clarify init command should not use --non-interactive - Fix SKILL.md wording about session/invocation behavior Co-Authored-By: Martin Vasko --- plugins/developer/commands/worktree.md | 34 +++++++++++++------ .../developer/skills/git-worktree/SKILL.md | 10 +++--- .../git-worktree/scripts/ensure-git-wt.sh | 25 +++++++++++--- plugins/developer/templates/settings.json | 1 - 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/plugins/developer/commands/worktree.md b/plugins/developer/commands/worktree.md index c60da01..d51829f 100644 --- a/plugins/developer/commands/worktree.md +++ b/plugins/developer/commands/worktree.md @@ -15,25 +15,39 @@ Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) git rev-parse --is-inside-work-tree ``` -2. Fetch or update the git-wt script. The setup script is located at: - ``` - /skills/git-worktree/scripts/ensure-git-wt.sh - ``` - Run it to clone/pull the repo and get the executable path: +2. Fetch or update the git-wt script directly from upstream: ```bash - GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" + GIT_WT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/git-wt" + if [[ -d "$GIT_WT_DIR/.git" ]]; then + git -C "$GIT_WT_DIR" fetch --quiet 2>/dev/null || true + else + git clone --quiet https://github.com/vojtabiberle/git-wt.git "$GIT_WT_DIR" + fi + GIT_WT="$GIT_WT_DIR/git-wt" ``` - Where `$SKILL_DIR` is the `skills/git-worktree` directory within this plugin. -3. Parse the user's arguments from `$ARGUMENTS`: +3. Parse the user's arguments from `$ARGUMENTS`. Only accept known subcommands: - `add [source]` — create a worktree - `rm [branch]` — remove a worktree - `ls` — list worktrees - `help` — show help -4. Run the git-wt script with `--non-interactive` flag to avoid interactive prompts: +4. Run the git-wt script with `--non-interactive` flag, validating and safely passing arguments: ```bash - "$GIT_WT" --non-interactive $ARGUMENTS + # Parse arguments into positional parameters + set -- $ARGUMENTS + subcommand="$1" + shift || true + + case "$subcommand" in + add|rm|ls|help) + "$GIT_WT" --non-interactive "$subcommand" "$@" + ;; + *) + echo "Error: unknown subcommand: $subcommand" >&2 + echo "Usage: /worktree [args...]" >&2 + ;; + esac ``` 5. Report the result to the user. For `add`, highlight the worktree path (last line of output). diff --git a/plugins/developer/skills/git-worktree/SKILL.md b/plugins/developer/skills/git-worktree/SKILL.md index cace515..a7eef1b 100644 --- a/plugins/developer/skills/git-worktree/SKILL.md +++ b/plugins/developer/skills/git-worktree/SKILL.md @@ -17,7 +17,7 @@ Manage git worktrees using the [git-wt](https://github.com/vojtabiberle/git-wt) `SKILL_DIR` = directory containing this SKILL.md (automatically resolved by Claude) -## Setup (run once per session) +## Setup Before using git-wt, fetch or update it: @@ -25,9 +25,9 @@ Before using git-wt, fetch or update it: GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" ``` -This clones `vojtabiberle/git-wt` to `~/.local/share/git-wt` (or pulls latest if already cloned). The script prints the path to the `git-wt` executable on stdout. +This clones `vojtabiberle/git-wt` to `~/.local/share/git-wt` (pinned to a known-good commit) or updates the existing clone. The script prints the path to the `git-wt` executable on stdout. If the network is unavailable, it falls back to the existing local version. -All subsequent commands use `$GIT_WT`. Always pass `--non-interactive` to suppress prompts. +All subsequent commands use `$GIT_WT`. Pass `--non-interactive` to suppress prompts for commands that may prompt (e.g. `add`, `rm`). Do **not** use `--non-interactive` with `init`, which requires interactive input. ## Commands @@ -88,7 +88,7 @@ Prints the filesystem path of the worktree for ``. Useful for scripting. "$GIT_WT" init ``` -Interactively creates `worktree.conf.local` in the repo root. +Interactively creates `worktree.conf.local` in the repo root. **Note:** Do not use `--non-interactive` with this command — it requires user input to configure directories and setup commands. ### Help @@ -137,7 +137,7 @@ Sanitization: `/` becomes `-`, everything lowercased. ## Typical Workflow -1. **Fetch git-wt** (once per session): +1. **Fetch git-wt**: ```bash GIT_WT="$("$SKILL_DIR/scripts/ensure-git-wt.sh")" ``` diff --git a/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh b/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh index 93401cd..7335d51 100755 --- a/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh +++ b/plugins/developer/skills/git-worktree/scripts/ensure-git-wt.sh @@ -1,16 +1,33 @@ #!/usr/bin/env bash set -euo pipefail -# ensure-git-wt.sh — Clone or pull the git-wt repo, then print its path. -# Usage: eval "$(./ensure-git-wt.sh)" or GIT_WT="$(./ensure-git-wt.sh)" +# ensure-git-wt.sh — Clone or update the git-wt repo, then print its path. +# Usage: GIT_WT="$(./ensure-git-wt.sh)" readonly GIT_WT_REPO="https://github.com/vojtabiberle/git-wt.git" +readonly GIT_WT_PINNED_COMMIT="f44fbfe042b6f454e0016d5e844d773073600074" readonly GIT_WT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/git-wt" if [[ -d "$GIT_WT_DIR/.git" ]]; then - git -C "$GIT_WT_DIR" pull --quiet >&2 + # Try to update existing clone; on failure, warn and keep using current version. + if ! git -C "$GIT_WT_DIR" fetch --quiet >&2 || \ + ! git -C "$GIT_WT_DIR" checkout --quiet "$GIT_WT_PINNED_COMMIT" >&2; then + echo "Warning: Failed to update git-wt in '$GIT_WT_DIR'; using existing version." >&2 + fi else - git clone --quiet "$GIT_WT_REPO" "$GIT_WT_DIR" >&2 + # Clone fresh; on failure, provide a helpful error and exit. + if ! git clone --quiet "$GIT_WT_REPO" "$GIT_WT_DIR" >&2; then + echo "Error: Failed to clone git-wt from '$GIT_WT_REPO' into '$GIT_WT_DIR'." >&2 + echo "Please check your network connection and repository access, then try again." >&2 + exit 1 + fi + git -C "$GIT_WT_DIR" checkout --quiet "$GIT_WT_PINNED_COMMIT" >&2 || true +fi + +# Verify the executable exists +if [[ ! -f "$GIT_WT_DIR/git-wt" ]]; then + echo "ensure-git-wt.sh: expected git-wt executable not found at '$GIT_WT_DIR/git-wt'" >&2 + exit 1 fi # Print the path to the git-wt executable on stdout diff --git a/plugins/developer/templates/settings.json b/plugins/developer/templates/settings.json index e0f89c7..6ef3273 100644 --- a/plugins/developer/templates/settings.json +++ b/plugins/developer/templates/settings.json @@ -15,7 +15,6 @@ "Bash(git remote:*)", "Bash(git tag:*)", "Bash(git worktree:*)", - "Bash(git wt:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(ls:*)",