Skip to content

Commit 3a166a8

Browse files
authored
Add worktree create/remove hook scripts (#57)
Registers WorktreeCreate and WorktreeRemove hooks in .claude/settings.json that shell out to scripts/worktree-create.sh and scripts/worktree-remove.sh. New worktrees branch off origin/dev (falling back to main/master), copy .env, and — when a demo-site/ is present — provision an isolated SQL Server database and rewrite launchSettings.json to use dynamic ports. Remove tears the database and worktree down. Adds .claude/worktrees/ to .gitignore and ships a .worktreeinclude stub listing files the create script copies in.
1 parent 03dbf61 commit 3a166a8

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

.claude/settings.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"hooks": {
3+
"WorktreeCreate": [
4+
{
5+
"hooks": [
6+
{
7+
"type": "command",
8+
"command": "bash \"$CLAUDE_PROJECT_DIR\"/scripts/worktree-create.sh",
9+
"timeout": 120
10+
}
11+
]
12+
}
13+
],
14+
"WorktreeRemove": [
15+
{
16+
"hooks": [
17+
{
18+
"type": "command",
19+
"command": "bash \"$CLAUDE_PROJECT_DIR\"/scripts/worktree-remove.sh",
20+
"timeout": 30
21+
}
22+
]
23+
}
24+
]
25+
}
26+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ junit.xml
1212
.DS_Store
1313
settings.local.json
1414
.claude/projects/
15+
.claude/worktrees/
1516
.worktrees/
1617
**/.wrangler/
1718
**/.dev.vars

.worktreeinclude

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Files to copy into new worktrees
2+
# Processed by the worktree-create.sh script
3+
4+
# Environment config (API credentials for MCP tools)
5+
.env

scripts/worktree-create.sh

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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"

scripts/worktree-remove.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Claude Code WorktreeRemove hook
5+
# Input: JSON on stdin: { "worktree_path": "<absolute-path>" }
6+
7+
# --- Parse input ---
8+
INPUT=$(cat)
9+
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path // empty')
10+
11+
if [ -z "$WORKTREE_PATH" ]; then
12+
echo "Error: no worktree_path provided" >&2
13+
exit 1
14+
fi
15+
16+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo "")}"
17+
18+
# --- Derive identifiers ---
19+
# Extract slug from path: .claude/worktrees/<slug>
20+
DIR_SLUG=$(basename "$WORKTREE_PATH")
21+
DB_NAME="umbraco-mcp-editor-$DIR_SLUG"
22+
23+
echo "Removing worktree: $DIR_SLUG" >&2
24+
25+
# --- Kill running demo-site process ---
26+
if pgrep -f "dotnet.*$WORKTREE_PATH/demo-site" >/dev/null 2>&1; then
27+
echo "Killing demo-site process..." >&2
28+
pkill -f "dotnet.*$WORKTREE_PATH/demo-site" 2>/dev/null || true
29+
sleep 1
30+
fi
31+
32+
# --- Drop database ---
33+
echo "Dropping database: $DB_NAME" >&2
34+
35+
SA_PASSWORD=""
36+
if [ -n "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/demo-site/appsettings.local.json" ]; then
37+
SA_PASSWORD=$(jq -r '.ConnectionStrings.umbracoDbDSN // ""' "$PROJECT_DIR/demo-site/appsettings.local.json" | sed -n 's/.*password=\([^;]*\).*/\1/p')
38+
fi
39+
40+
if [ -n "$SA_PASSWORD" ]; then
41+
docker exec sql bash -c "/opt/mssql-tools*/bin/sqlcmd -S localhost -U sa -P '$SA_PASSWORD' -C -Q \"
42+
IF EXISTS (SELECT name FROM sys.databases WHERE name = '$DB_NAME')
43+
BEGIN
44+
ALTER DATABASE [$DB_NAME] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
45+
DROP DATABASE [$DB_NAME];
46+
END
47+
\"" 2>/dev/null || {
48+
echo "Warning: Could not drop database $DB_NAME" >&2
49+
}
50+
else
51+
echo "Warning: Could not read SA password — skipping database drop" >&2
52+
fi
53+
54+
# --- Remove worktree ---
55+
if [ -n "$PROJECT_DIR" ]; then
56+
git -C "$PROJECT_DIR" worktree remove --force "$WORKTREE_PATH" 2>/dev/null || {
57+
echo "Force remove failed, trying prune + rm..." >&2
58+
git -C "$PROJECT_DIR" worktree prune 2>/dev/null || true
59+
rm -rf "$WORKTREE_PATH" 2>/dev/null || true
60+
}
61+
else
62+
rm -rf "$WORKTREE_PATH" 2>/dev/null || true
63+
fi
64+
65+
echo "Worktree $DIR_SLUG removed" >&2

0 commit comments

Comments
 (0)