|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Claude Code WorktreeCreate hook |
| 5 | +# Input: JSON on stdin: { "name": "<slug>", "cwd": "<project-root>" } |
| 6 | +# Output: worktree path on stdout (last line) |
| 7 | + |
| 8 | +# --- Parse input --- |
| 9 | +INPUT=$(cat) |
| 10 | +NAME=$(echo "$INPUT" | jq -r '.name // empty') |
| 11 | +CWD=$(echo "$INPUT" | jq -r '.cwd // empty') |
| 12 | + |
| 13 | +if [ -z "$NAME" ]; then |
| 14 | + echo "Error: no name provided" >&2 |
| 15 | + exit 1 |
| 16 | +fi |
| 17 | + |
| 18 | +# Use CLAUDE_PROJECT_DIR if available, fall back to cwd, then git root |
| 19 | +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${CWD:-$(git rev-parse --show-toplevel)}}" |
| 20 | + |
| 21 | +# --- Derive identifiers --- |
| 22 | +# Directory slug: replace / with - |
| 23 | +DIR_SLUG=$(echo "$NAME" | tr '/' '-') |
| 24 | + |
| 25 | +# Branch name: if name contains /, use as-is (e.g. PR branch); otherwise prefix with feature/ |
| 26 | +if [[ "$NAME" == *"/"* ]]; then |
| 27 | + BRANCH_NAME="$NAME" |
| 28 | +else |
| 29 | + BRANCH_NAME="feature/$NAME" |
| 30 | +fi |
| 31 | + |
| 32 | +WORKTREE_PATH="$PROJECT_DIR/.claude/worktrees/$DIR_SLUG" |
| 33 | +DB_NAME="umbraco-mcp-editor-$DIR_SLUG" |
| 34 | + |
| 35 | +# --- Detect base branch --- |
| 36 | +git -C "$PROJECT_DIR" fetch origin 2>/dev/null || true |
| 37 | + |
| 38 | +BASE_BRANCH="" |
| 39 | +for candidate in dev main master; do |
| 40 | + if git -C "$PROJECT_DIR" rev-parse --verify "origin/$candidate" >/dev/null 2>&1; then |
| 41 | + BASE_BRANCH="origin/$candidate" |
| 42 | + break |
| 43 | + fi |
| 44 | +done |
| 45 | + |
| 46 | +if [ -z "$BASE_BRANCH" ]; then |
| 47 | + echo "Error: could not find base branch (dev, main, or master)" >&2 |
| 48 | + exit 1 |
| 49 | +fi |
| 50 | + |
| 51 | +echo "Base branch: $BASE_BRANCH" >&2 |
| 52 | + |
| 53 | +# --- Handle existing worktree --- |
| 54 | +if [ -d "$WORKTREE_PATH" ]; then |
| 55 | + echo "Worktree already exists at $WORKTREE_PATH" >&2 |
| 56 | + echo "$WORKTREE_PATH" |
| 57 | + exit 0 |
| 58 | +fi |
| 59 | + |
| 60 | +# --- Create worktree --- |
| 61 | +mkdir -p "$(dirname "$WORKTREE_PATH")" |
| 62 | + |
| 63 | +# Check if branch already exists locally |
| 64 | +if git -C "$PROJECT_DIR" rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then |
| 65 | + echo "Using existing local branch: $BRANCH_NAME" >&2 |
| 66 | + git -C "$PROJECT_DIR" worktree add "$WORKTREE_PATH" "$BRANCH_NAME" >&2 |
| 67 | +# Check if branch exists on remote |
| 68 | +elif git -C "$PROJECT_DIR" rev-parse --verify "origin/$BRANCH_NAME" >/dev/null 2>&1; then |
| 69 | + echo "Tracking remote branch: origin/$BRANCH_NAME" >&2 |
| 70 | + git -C "$PROJECT_DIR" worktree add "$WORKTREE_PATH" -b "$BRANCH_NAME" --track "origin/$BRANCH_NAME" >&2 |
| 71 | +else |
| 72 | + echo "Creating new branch: $BRANCH_NAME from $BASE_BRANCH" >&2 |
| 73 | + git -C "$PROJECT_DIR" worktree add "$WORKTREE_PATH" -b "$BRANCH_NAME" "$BASE_BRANCH" >&2 |
| 74 | +fi |
| 75 | + |
| 76 | +# --- Copy .env --- |
| 77 | +if [ -f "$PROJECT_DIR/.env" ]; then |
| 78 | + cp "$PROJECT_DIR/.env" "$WORKTREE_PATH/.env" |
| 79 | + echo "Copied: .env" >&2 |
| 80 | +fi |
| 81 | + |
| 82 | +# --- Copy demo-site (gitignored, so not in worktree by default) --- |
| 83 | +if [ -d "$PROJECT_DIR/demo-site" ]; then |
| 84 | + echo "Copying demo-site to worktree..." >&2 |
| 85 | + rsync -a \ |
| 86 | + --exclude='bin/' \ |
| 87 | + --exclude='obj/' \ |
| 88 | + --exclude='umbraco/Data/*.sqlite*' \ |
| 89 | + --exclude='umbraco/Logs/' \ |
| 90 | + --exclude='appsettings.local.json' \ |
| 91 | + "$PROJECT_DIR/demo-site/" "$WORKTREE_PATH/demo-site/" >&2 |
| 92 | + echo "Copied demo-site (excluding build artifacts and data)" >&2 |
| 93 | +fi |
| 94 | + |
| 95 | +# --- Create SQL Server database --- |
| 96 | +echo "Creating database: $DB_NAME" >&2 |
| 97 | + |
| 98 | +# Read SA password from main worktree's appsettings.local.json |
| 99 | +SA_PASSWORD="" |
| 100 | +if [ -f "$PROJECT_DIR/demo-site/appsettings.local.json" ]; then |
| 101 | + SA_PASSWORD=$(jq -r '.ConnectionStrings.umbracoDbDSN // ""' "$PROJECT_DIR/demo-site/appsettings.local.json" | sed -n 's/.*password=\([^;]*\).*/\1/p') |
| 102 | +fi |
| 103 | + |
| 104 | +if [ -z "$SA_PASSWORD" ]; then |
| 105 | + echo "Warning: Could not read SA password from demo-site/appsettings.local.json" >&2 |
| 106 | + echo "Skipping database creation — set up manually" >&2 |
| 107 | +else |
| 108 | + # Create database (ignore error if already exists) |
| 109 | + docker exec sql bash -c "/opt/mssql-tools*/bin/sqlcmd -S localhost -U sa -P '$SA_PASSWORD' -C -Q \"IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = '$DB_NAME') CREATE DATABASE [$DB_NAME]\"" 2>/dev/null || { |
| 110 | + echo "Warning: Could not create database (is Docker running?)" >&2 |
| 111 | + } |
| 112 | + |
| 113 | + # Write appsettings.local.json for the worktree |
| 114 | + mkdir -p "$WORKTREE_PATH/demo-site" |
| 115 | + cat > "$WORKTREE_PATH/demo-site/appsettings.local.json" <<JSONEOF |
| 116 | +{ |
| 117 | + "ConnectionStrings": { |
| 118 | + "umbracoDbDSN": "Server=localhost,1433;Database=$DB_NAME;User Id=sa;password=$SA_PASSWORD;TrustServerCertificate=True", |
| 119 | + "umbracoDbDSN_ProviderName": "Microsoft.Data.SqlClient" |
| 120 | + } |
| 121 | +} |
| 122 | +JSONEOF |
| 123 | + echo "Wrote demo-site/appsettings.local.json with database: $DB_NAME" >&2 |
| 124 | +fi |
| 125 | + |
| 126 | +# --- Rewrite launchSettings.json to use dynamic port --- |
| 127 | +LAUNCH_SETTINGS="$WORKTREE_PATH/demo-site/Properties/launchSettings.json" |
| 128 | +if [ -f "$LAUNCH_SETTINGS" ]; then |
| 129 | + # Use jq to rewrite the applicationUrl to port 0 |
| 130 | + jq ' |
| 131 | + .profiles["Umbraco.Web.UI"].applicationUrl = "https://127.0.0.1:0;http://127.0.0.1:0" | |
| 132 | + .iisSettings.iisExpress.sslPort = 0 | |
| 133 | + .iisSettings.iisExpress.applicationUrl = "http://127.0.0.1:0" |
| 134 | + ' "$LAUNCH_SETTINGS" > "$LAUNCH_SETTINGS.tmp" && mv "$LAUNCH_SETTINGS.tmp" "$LAUNCH_SETTINGS" |
| 135 | + echo "Rewrote launchSettings.json to use dynamic port" >&2 |
| 136 | +fi |
| 137 | + |
| 138 | +# --- Ensure .claude/worktrees/ is in .gitignore --- |
| 139 | +GITIGNORE="$PROJECT_DIR/.gitignore" |
| 140 | +if [ -f "$GITIGNORE" ] && ! grep -q '.claude/worktrees/' "$GITIGNORE"; then |
| 141 | + echo "" >> "$GITIGNORE" |
| 142 | + echo "# Worktree directories" >> "$GITIGNORE" |
| 143 | + echo ".claude/worktrees/" >> "$GITIGNORE" |
| 144 | + echo "Added .claude/worktrees/ to .gitignore" >&2 |
| 145 | +fi |
| 146 | + |
| 147 | +# --- Run npm install --- |
| 148 | +echo "Running npm install in worktree..." >&2 |
| 149 | +cd "$WORKTREE_PATH" |
| 150 | +npm install --silent >&2 2>&1 || { |
| 151 | + echo "Warning: npm install failed" >&2 |
| 152 | +} |
| 153 | + |
| 154 | +# --- Output worktree path (Claude Code reads the last line) --- |
| 155 | +echo "$WORKTREE_PATH" |
0 commit comments