chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"description": "Track file changes in a simple log. Records which files were modified and when for easy tracking of Claude Code activity.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo \"[$(date '+%Y-%m-%d %H:%M:%S')] File modified: $CLAUDE_TOOL_FILE_PATH\" >> ~/.claude/changes.log"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo \"[$(date '+%Y-%m-%d %H:%M:%S')] File created: $CLAUDE_TOOL_FILE_PATH\" >> ~/.claude/changes.log"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Log all Claude Code commands to a file for audit and debugging purposes. Simple logging that records tool usage with timestamps.",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo \"[$(date)] Tool: $CLAUDE_TOOL_NAME | File: $CLAUDE_TOOL_FILE_PATH\" >> ~/.claude/command-log.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"description": "Auto Debug Log Viewer. Opens a live-tailing debug log window when Claude Code starts with --debug or -d flag. The window closes automatically on session end. To keep the debug window open after session ends, set DEBUG_WINDOW_AUTO_CLOSE_DISABLE=1 in your settings.json. Tested on Intel Mac. Supports macOS, Linux, and Windows (Git Bash/Cygwin). Contributions from other platform users are welcome.",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/debug-window.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/debug-window.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# Session Hook: Auto Debug Log Viewer
|
||||
#
|
||||
# Opens a live-tailing debug log window on SessionStart and
|
||||
# closes it on SessionEnd when Claude Code runs with --debug or -d flag.
|
||||
#
|
||||
# Events:
|
||||
# - SessionStart: Opens terminal window with tail -f on debug log
|
||||
# - SessionEnd: Terminates tail process and closes window
|
||||
#
|
||||
# Requirements:
|
||||
# - Only runs when --debug or -d flag was used to start the session
|
||||
# - Debug log file must exist at ~/.claude/debug/{session_id}.txt
|
||||
#
|
||||
# Configuration (in ~/.claude/settings.json):
|
||||
# To keep debug window open after session ends:
|
||||
# { "env": { "DEBUG_WINDOW_AUTO_CLOSE_DISABLE": "1" } }
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# check_debug_flag: Traverse parent process tree to find --debug or -d flag
|
||||
#
|
||||
# Why: The hook script runs as a child process of Claude Code.
|
||||
# We walk up the process tree to check if Claude was started with
|
||||
# debug flag, since only then should we manage the debug window.
|
||||
#
|
||||
# Returns: 0 if debug flag found, 1 otherwise
|
||||
#-----------------------------------------------------------------------
|
||||
check_debug_flag() {
|
||||
local pid=$$
|
||||
while [[ $pid -ne 1 ]]; do
|
||||
local cmdline
|
||||
# Linux stores cmdline in /proc, macOS requires ps command
|
||||
if [[ -f "/proc/$pid/cmdline" ]]; then
|
||||
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
|
||||
else
|
||||
cmdline=$(ps -p "$pid" -o args= 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Match --debug or -d as standalone flags (not part of another word)
|
||||
[[ "$cmdline" =~ (^|[[:space:]])(--debug|-d)($|[[:space:]]) ]] && return 0
|
||||
|
||||
# Move to parent process
|
||||
local ppid
|
||||
ppid=$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ')
|
||||
[[ -z "$ppid" || "$ppid" == "$pid" ]] && break
|
||||
pid=$ppid
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Exit early if debug flag not found
|
||||
check_debug_flag || exit 0
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# Parse JSON input from stdin
|
||||
#
|
||||
# Claude Code passes session info via stdin as JSON:
|
||||
# {"session_id":"uuid","hook_event_name":"SessionStart|SessionEnd",...}
|
||||
#
|
||||
# We use jq if available, otherwise fall back to sed for portability
|
||||
#-----------------------------------------------------------------------
|
||||
INPUT=$(cat)
|
||||
|
||||
if command -v jq &> /dev/null; then
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
|
||||
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
|
||||
else
|
||||
SESSION_ID=$(echo "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
HOOK_EVENT=$(echo "$INPUT" | sed -n 's/.*"hook_event_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
fi
|
||||
|
||||
# Exit if session_id missing
|
||||
[[ -z "$SESSION_ID" || "$SESSION_ID" == "null" ]] && exit 1
|
||||
|
||||
DEBUG_LOG="$HOME/.claude/debug/${SESSION_ID}.txt"
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# SessionStart: Open debug log in a new terminal window
|
||||
#-----------------------------------------------------------------------
|
||||
open_debug_window() {
|
||||
# Wait for debug log file creation (max 5 seconds)
|
||||
# Why: Claude Code may not have created the file yet when hook runs
|
||||
for i in {1..5}; do
|
||||
[[ -f "$DEBUG_LOG" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ ! -f "$DEBUG_LOG" ]] && exit 1
|
||||
|
||||
# Platform-specific terminal window opening
|
||||
# Opens: tail -n 1000 -f $DEBUG_LOG (last 1000 lines + follow)
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
osascript -e "tell application \"Terminal\" to do script \"tail -n 1000 -f '$DEBUG_LOG'\""
|
||||
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
if command -v gnome-terminal &> /dev/null; then
|
||||
gnome-terminal -- tail -n 1000 -f "$DEBUG_LOG"
|
||||
elif command -v konsole &> /dev/null; then
|
||||
konsole -e tail -n 1000 -f "$DEBUG_LOG"
|
||||
elif command -v xterm &> /dev/null; then
|
||||
xterm -e tail -n 1000 -f "$DEBUG_LOG" &
|
||||
fi
|
||||
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
start cmd /c "tail -n 1000 -f '$DEBUG_LOG'"
|
||||
fi
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# SessionEnd: Close debug log window
|
||||
#-----------------------------------------------------------------------
|
||||
close_debug_window() {
|
||||
# Allow users to disable auto-close via environment variable
|
||||
[[ "$DEBUG_WINDOW_AUTO_CLOSE_DISABLE" == "1" ]] && exit 0
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# Find tail process by matching the exact debug log path
|
||||
# ps output: PID TTY COMMAND
|
||||
TAIL_INFO=$(ps -eo pid,tty,args | grep "tail.*${DEBUG_LOG}" | grep -v grep | head -1)
|
||||
TAIL_PID=$(echo "$TAIL_INFO" | awk '{print $1}')
|
||||
TAIL_TTY=$(echo "$TAIL_INFO" | awk '{print $2}')
|
||||
|
||||
[[ -z "$TAIL_TTY" || "$TAIL_TTY" == "??" ]] && exit 0
|
||||
|
||||
# Kill tail first to prevent "terminate running process?" dialog
|
||||
[[ -n "$TAIL_PID" ]] && kill "$TAIL_PID" 2>/dev/null && sleep 0.2
|
||||
|
||||
# Close Terminal window by matching TTY
|
||||
osascript -e "
|
||||
tell application \"Terminal\"
|
||||
repeat with w in windows
|
||||
repeat with t in tabs of w
|
||||
if tty of t is \"/dev/$TAIL_TTY\" then
|
||||
close w
|
||||
return
|
||||
end if
|
||||
end repeat
|
||||
end repeat
|
||||
end tell
|
||||
" 2>/dev/null
|
||||
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Killing tail process is usually sufficient on Linux
|
||||
pkill -f "tail.*${DEBUG_LOG}"
|
||||
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
# Note: taskkill /FI does not support COMMANDLINE filter
|
||||
# Use ps and kill instead
|
||||
TAIL_PID=$(ps -s | grep "tail.*${DEBUG_LOG}" | grep -v grep | awk '{print $1}' | head -1)
|
||||
[[ -n "$TAIL_PID" ]] && kill "$TAIL_PID" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# Event dispatcher
|
||||
#-----------------------------------------------------------------------
|
||||
case "$HOOK_EVENT" in
|
||||
SessionStart) open_debug_window ;;
|
||||
SessionEnd) close_debug_window ;;
|
||||
esac
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Log all file edits to a project-local audit file with timestamps. Records every Edit tool usage to .claude/edit-log.txt for tracking changes across sessions. Requires jq.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "jq -r '.tool_input.file_path' | xargs -I FILE sh -c 'echo \"$(date +%Y-%m-%dT%H:%M:%S): Edit FILE\" >> \"$CLAUDE_PROJECT_DIR/.claude/edit-log.txt\"'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Automatically backup files before editing. Creates timestamped backups in a .backups directory when files are modified.",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if [[ -n \"$CLAUDE_TOOL_FILE_PATH\" && -f \"$CLAUDE_TOOL_FILE_PATH\" ]]; then mkdir -p .backups && cp \"$CLAUDE_TOOL_FILE_PATH\" \".backups/$(basename \"$CLAUDE_TOOL_FILE_PATH\").$(date +%Y%m%d_%H%M%S).bak\"; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Automatically run linting tools after file modifications. Supports ESLint for JavaScript/TypeScript, Pylint for Python, and RuboCop for Ruby.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.js || \"$CLAUDE_TOOL_FILE_PATH\" == *.ts || \"$CLAUDE_TOOL_FILE_PATH\" == *.jsx || \"$CLAUDE_TOOL_FILE_PATH\" == *.tsx ]]; then npx eslint \"$CLAUDE_TOOL_FILE_PATH\" --fix 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.py ]]; then pylint \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.rb ]]; then rubocop \"$CLAUDE_TOOL_FILE_PATH\" --auto-correct 2>/dev/null || true; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"description": "Enforce Next.js best practices, proper file structure, component patterns, and TypeScript usage with automated code reviews and suggestions. Validates Next.js App Router conventions, Server/Client component patterns, proper imports, and TypeScript usage. Provides real-time feedback on code quality and adherence to Next.js best practices.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash -c 'input=$(cat); FILE_PATH=$(echo \"$input\" | jq -r \".tool_input.file_path // empty\"); SUCCESS=$(echo \"$input\" | jq -r \".tool_response.success // false\"); if [ \"$SUCCESS\" = \"true\" ] && [[ \"$FILE_PATH\" =~ \\.(js|jsx|ts|tsx)$ ]] && [[ ! \"$FILE_PATH\" =~ node_modules ]]; then echo \"🔍 Next.js Code Quality Enforcer: Reviewing $FILE_PATH...\"; ISSUES=0; if [ -f \"$FILE_PATH\" ]; then if [[ \"$FILE_PATH\" =~ app/.* ]]; then echo \"📁 App Router file detected: $FILE_PATH\"; if [[ \"$FILE_PATH\" =~ page\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"export default function\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"export default async function\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Page component must export default function\" >&2; ((ISSUES++)); fi; if [[ \"$FILE_PATH\" =~ layout\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"children\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Layout component should accept children prop\" >&2; ((ISSUES++)); fi; if [[ \"$FILE_PATH\" =~ page\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"Metadata\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"metadata\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Consider adding metadata export for SEO\"; fi; if grep -q \"use client\" \"$FILE_PATH\" 2>/dev/null; then echo \"🖥️ Client Component detected\"; if ! grep -E \"(useState|useEffect|onClick|onChange|onSubmit)\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Client component without interactivity - consider Server Component\"; fi; else echo \"🚀 Server Component (default)\"; if grep -E \"(useState|useEffect|onClick|onChange|onSubmit)\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Interactive features in Server Component - add \\\"use client\\\" directive\" >&2; ((ISSUES++)); fi; fi; fi; if [[ \"$FILE_PATH\" =~ \\.(jsx|tsx)$ ]]; then if ! grep -q \"import.*React\" \"$FILE_PATH\" 2>/dev/null && grep -q \"<\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ JSX without React import (Next.js 17+ handles this automatically)\"; fi; if ! grep -q \"FC\\|FunctionComponent\" \"$FILE_PATH\" 2>/dev/null && grep -q \"props\" \"$FILE_PATH\" 2>/dev/null && [[ \"$FILE_PATH\" =~ \\.tsx$ ]]; then echo \"💡 Consider using React.FC or explicit prop types for TypeScript\"; fi; fi; if [[ \"$FILE_PATH\" =~ \\.js$ ]] && [ -f \"tsconfig.json\" ]; then echo \"📝 JavaScript file in TypeScript project: $FILE_PATH\"; echo \"💡 Consider migrating to TypeScript for better type safety\"; fi; if grep -q \"next/image\" \"$FILE_PATH\" 2>/dev/null; then echo \"✅ Using next/image for optimized images\"; elif grep -q \"<img\" \"$FILE_PATH\" 2>/dev/null; then echo \"🖼️ Regular <img> tag detected\"; echo \"💡 Consider using next/image for better performance\"; fi; if grep -q \"next/link\" \"$FILE_PATH\" 2>/dev/null; then echo \"✅ Using next/link for navigation\"; elif grep -q \"<a href=\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"http\" \"$FILE_PATH\" 2>/dev/null; then echo \"🔗 Regular <a> tag for internal links detected\"; echo \"💡 Use next/link for internal navigation\"; fi; if grep -q \"getServerSideProps\\|getStaticProps\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Pages Router data fetching methods detected\"; echo \"💡 Consider migrating to App Router with Server Components\"; fi; if grep -q \"className=.*{\" \"$FILE_PATH\" 2>/dev/null; then echo \"🎨 Dynamic className detected\"; if ! grep -q \"clsx\\|classnames\\|cn(\" \"$FILE_PATH\" 2>/dev/null; then echo \"💡 Consider using clsx or similar utility for className concatenation\"; fi; fi; if [ $ISSUES -eq 0 ]; then echo \"✅ Code quality check passed for $FILE_PATH\"; else echo \"❌ Found $ISSUES code quality issues in $FILE_PATH\" >&2; exit 2; fi; else echo \"❌ File $FILE_PATH not found\"; fi; else echo \"ℹ️ Code quality check skipped (not a JavaScript/TypeScript file or failed operation)\"; fi'",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "Smart code formatting based on file type. Automatically formats code using Prettier, Black, gofmt, rustfmt, and other language-specific formatters.",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.js || \"$CLAUDE_TOOL_FILE_PATH\" == *.ts || \"$CLAUDE_TOOL_FILE_PATH\" == *.jsx || \"$CLAUDE_TOOL_FILE_PATH\" == *.tsx || \"$CLAUDE_TOOL_FILE_PATH\" == *.json || \"$CLAUDE_TOOL_FILE_PATH\" == *.css || \"$CLAUDE_TOOL_FILE_PATH\" == *.html ]]; then npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.py ]]; then black \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.go ]]; then gofmt -w \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.rs ]]; then rustfmt \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; elif [[ \"$CLAUDE_TOOL_FILE_PATH\" == *.php ]]; then php-cs-fixer fix \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"description": "Worktree Ghostty Layout. Opens a 3-panel Ghostty layout when creating worktrees: Claude Code (left) | lazygit (top-right) / yazi (bottom-right). Creates worktrees in a sibling directory (../worktrees/<repo>/<name>/) and cleans up on removal. macOS only. Requires: jq, Ghostty terminal, lazygit, yazi. Ghostty keybindings required: super+d = new_split:right, super+shift+d = new_split:down.",
|
||||
"hooks": {
|
||||
"WorktreeCreate": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-ghostty.sh",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"WorktreeRemove": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-ghostty.sh",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# Hook: Worktree Ghostty Layout
|
||||
#
|
||||
# Creates git worktrees in a sibling directory and opens a Ghostty terminal
|
||||
# layout with lazygit (top-right) and yazi (bottom-right).
|
||||
#
|
||||
# Events:
|
||||
# - WorktreeCreate: Creates worktree + opens Ghostty 3-panel layout
|
||||
# - WorktreeRemove: Removes worktree, branch, and empty directories
|
||||
#
|
||||
# Requirements:
|
||||
# - jq (JSON parsing)
|
||||
# - Ghostty terminal (macOS)
|
||||
# - lazygit (git TUI)
|
||||
# - yazi (file manager TUI)
|
||||
#
|
||||
# Ghostty keybindings required:
|
||||
# super+d = new_split:right
|
||||
# super+shift+d = new_split:down
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "jq is required but not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# WorktreeCreate: Create worktree in sibling dir + open Ghostty layout
|
||||
#-----------------------------------------------------------------------
|
||||
create_worktree() {
|
||||
local NAME
|
||||
NAME=$(echo "$INPUT" | jq -r '.name')
|
||||
|
||||
local REPO_NAME PARENT_DIR WORKTREE_DIR BRANCH_NAME
|
||||
REPO_NAME=$(basename "$CWD")
|
||||
PARENT_DIR=$(cd "$CWD/.." && pwd)
|
||||
WORKTREE_DIR="$PARENT_DIR/worktrees/$REPO_NAME/$NAME"
|
||||
BRANCH_NAME="worktree-$NAME"
|
||||
|
||||
# Detect default remote branch
|
||||
local DEFAULT_BRANCH
|
||||
DEFAULT_BRANCH=$(cd "$CWD" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
|
||||
: "${DEFAULT_BRANCH:=main}"
|
||||
|
||||
# Create git worktree in sibling directory
|
||||
mkdir -p "$PARENT_DIR/worktrees/$REPO_NAME" >&2
|
||||
cd "$CWD" || exit 1
|
||||
git fetch origin &>/dev/null || true
|
||||
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "origin/$DEFAULT_BRANCH" >&2 || {
|
||||
echo "Failed to create worktree: $NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Open Ghostty layout: Claude (left) | lazygit (top-right) / yazi (bottom-right)
|
||||
# Uses clipboard + Cmd+V for reliable text input (keystroke is unreliable for long paths)
|
||||
{
|
||||
sleep 1.5
|
||||
osascript <<APPLESCRIPT >/dev/null 2>&1
|
||||
-- Save current clipboard
|
||||
try
|
||||
set oldClip to the clipboard as text
|
||||
on error
|
||||
set oldClip to ""
|
||||
end try
|
||||
|
||||
tell application "System Events"
|
||||
tell process "Ghostty"
|
||||
-- Split right (Cmd+D)
|
||||
keystroke "d" using {command down}
|
||||
delay 1.0
|
||||
|
||||
-- cd + lazygit
|
||||
set the clipboard to "cd '${WORKTREE_DIR}' && lazygit"
|
||||
keystroke "v" using {command down}
|
||||
delay 0.3
|
||||
key code 36
|
||||
delay 2.0
|
||||
|
||||
-- Split down (Cmd+Shift+D)
|
||||
keystroke "d" using {command down, shift down}
|
||||
delay 1.0
|
||||
|
||||
-- cd + yazi
|
||||
set the clipboard to "cd '${WORKTREE_DIR}' && yazi"
|
||||
keystroke "v" using {command down}
|
||||
delay 0.3
|
||||
key code 36
|
||||
end tell
|
||||
end tell
|
||||
|
||||
-- Restore clipboard
|
||||
delay 0.5
|
||||
set the clipboard to oldClip
|
||||
APPLESCRIPT
|
||||
} &>/dev/null &
|
||||
|
||||
# Output the worktree path (the ONLY stdout Claude Code reads)
|
||||
echo "$WORKTREE_DIR"
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# WorktreeRemove: Clean up worktree, branch, and empty directories
|
||||
#-----------------------------------------------------------------------
|
||||
remove_worktree() {
|
||||
local WORKTREE_PATH
|
||||
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path')
|
||||
|
||||
[ ! -d "$WORKTREE_PATH" ] && exit 0
|
||||
|
||||
# Find main repo (first entry in worktree list)
|
||||
local MAIN_REPO BRANCH_NAME
|
||||
MAIN_REPO=$(git -C "$WORKTREE_PATH" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
|
||||
BRANCH_NAME="worktree-$(basename "$WORKTREE_PATH")"
|
||||
|
||||
# Remove worktree and branch
|
||||
cd "$MAIN_REPO" 2>/dev/null || exit 0
|
||||
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || rm -rf "$WORKTREE_PATH"
|
||||
git branch -D "$BRANCH_NAME" 2>/dev/null
|
||||
|
||||
# Clean up empty parent directories
|
||||
rmdir "$(dirname "$WORKTREE_PATH")" 2>/dev/null
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# Event dispatcher
|
||||
#-----------------------------------------------------------------------
|
||||
case "$HOOK_EVENT" in
|
||||
WorktreeCreate) create_worktree ;;
|
||||
WorktreeRemove) remove_worktree ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user