#!/bin/bash # ClaudeTools Bidirectional Sync Script # Ensures proper pull BEFORE push on all machines # Prints incoming/outgoing change summary with author attribution set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # Machine + timestamp if [ -n "$COMPUTERNAME" ]; then MACHINE="$COMPUTERNAME" else MACHINE=$(hostname) fi TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") echo -e "${GREEN}[OK]${NC} Starting ClaudeTools sync from $MACHINE at $TIMESTAMP" # Navigate to ClaudeTools directory (check common locations) for candidate in "$HOME/ClaudeTools" "/d/ClaudeTools" "D:/ClaudeTools" "/d/claudetools" "D:/claudetools" "C:/claudetools" "/c/claudetools"; do if [ -d "$candidate" ]; then cd "$candidate" break fi done if [ ! -d ".git" ]; then echo -e "${RED}[ERROR]${NC} Not in a git working tree" exit 1 fi echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)" # Detect Python interpreter — verify it actually runs (Windows Store stub passes command -v but fails to execute) PYTHON="" for candidate in py python3 python; do if command -v "$candidate" >/dev/null 2>&1; then if "$candidate" -c "import sys; sys.exit(0)" >/dev/null 2>&1; then PYTHON="$candidate" break fi fi done if [ -z "$PYTHON" ]; then echo -e "${RED}[ERROR]${NC} No Python interpreter found (tried: py, python3, python)" exit 1 fi # Load user identity USER_DISPLAY="unknown" USER_GITEA="" if [ -f ".claude/identity.json" ]; then USER_DISPLAY=$($PYTHON -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('full_name', d.get('user','unknown')))" 2>/dev/null || echo "unknown") USER_GITEA=$($PYTHON -c "import json,sys; d=json.load(open('.claude/identity.json')); print(d.get('user',''))" 2>/dev/null || echo "") fi echo -e "${GREEN}[OK]${NC} Syncing as: $USER_DISPLAY (machine: $MACHINE)" # Phase 1a: Update submodules to latest remote echo "" echo "=== Phase 1a: Submodule update ===" if [ -f ".gitmodules" ]; then # Temporarily disable errexit — submodule ops emit non-fatal warnings that # would otherwise kill the script under set -e. set +e git submodule foreach --quiet ' git fetch origin --quiet 2>/dev/null git checkout main --quiet 2>/dev/null || git checkout master --quiet 2>/dev/null git merge --ff-only origin/main --quiet 2>/dev/null || \ git merge --ff-only origin/master --quiet 2>/dev/null ' 2>/dev/null set -e echo -e "${GREEN}[OK]${NC} Submodules updated to latest remote (on branch)." else echo -e "${GREEN}[OK]${NC} No submodules." fi # Phase 1: Local changes echo "" echo "=== Phase 1: Local changes ===" if ! git diff-index --quiet HEAD -- 2>/dev/null; then echo -e "${YELLOW}[INFO]${NC} Local changes detected:" git status --short echo "" echo -e "${GREEN}[OK]${NC} Staging all changes..." git add -A # Commit message (Co-Authored-By uses local git user if configured) COMMIT_MSG="sync: auto-sync from $MACHINE at $TIMESTAMP Author: $USER_DISPLAY Machine: $MACHINE Timestamp: $TIMESTAMP" if git diff-index --quiet --cached HEAD -- 2>/dev/null; then echo -e "${GREEN}[OK]${NC} No stageable changes (submodule internal changes skipped)." else git commit -m "$COMMIT_MSG" echo -e "${GREEN}[OK]${NC} Committed." fi else echo -e "${GREEN}[OK]${NC} No local changes to commit." fi # Phase 2: Remote sync echo "" echo "=== Phase 2: Fetch + inspect ===" LOCAL_BEFORE=$(git rev-parse HEAD) echo -e "${GREEN}[OK]${NC} Fetching from origin..." git fetch origin --quiet LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null || echo "$LOCAL") REMOTE_BRANCH="origin/main" if ! git rev-parse origin/main >/dev/null 2>&1; then REMOTE_BRANCH="origin/master" fi # Count and show incoming INCOMING_COUNT=$(git rev-list --count HEAD..$REMOTE_BRANCH 2>/dev/null || echo 0) OUTGOING_COUNT=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) if [ "$INCOMING_COUNT" -gt 0 ]; then echo "" echo -e "${CYAN}--- Incoming: $INCOMING_COUNT commits from remote ---${NC}" git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' HEAD..$REMOTE_BRANCH | head -30 echo "" echo -e "${CYAN}--- Files touched by incoming commits ---${NC}" git diff --stat HEAD..$REMOTE_BRANCH | tail -20 else echo -e "${GREEN}[OK]${NC} No incoming changes." fi if [ "$OUTGOING_COUNT" -gt 0 ]; then echo "" echo -e "${CYAN}--- Outgoing: $OUTGOING_COUNT commits to remote ---${NC}" git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' $REMOTE_BRANCH..HEAD | head -30 fi # Phase 3: Pull (if needed) if [ "$INCOMING_COUNT" -gt 0 ]; then echo "" echo "=== Phase 3: Pull (rebase) ===" if git pull origin main --rebase; then echo -e "${GREEN}[OK]${NC} Pulled successfully." else echo -e "${RED}[ERROR]${NC} Pull failed (likely conflicts). Resolve and re-run sync." exit 1 fi fi # Phase 4: Push (if needed) OUTGOING_AFTER_PULL=$(git rev-list --count $REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then echo "" echo "=== Phase 4: Push ===" if git push origin main; then echo -e "${GREEN}[OK]${NC} Pushed successfully." else echo -e "${RED}[ERROR]${NC} Push failed. Check auth / network." exit 1 fi else echo -e "${GREEN}[OK]${NC} Nothing to push." fi # Phase 5: Scan pulled session logs for cross-user messages # Look for "## Note for" or "## Message for" sections in any session log # touched by incoming commits. Print them prominently so they aren't missed. if [ "$INCOMING_COUNT" -gt 0 ] && [ -n "$LOCAL_BEFORE" ]; then CHANGED_LOGS=$(git diff --name-only "$LOCAL_BEFORE"..HEAD -- '**/session-logs/*.md' 'session-logs/*.md' 2>/dev/null || true) if [ -n "$CHANGED_LOGS" ]; then NOTES_FOUND=0 for LOG_FILE in $CHANGED_LOGS; do if [ -f "$LOG_FILE" ]; then # Extract author from "## User" block and any "## Note for" / "## Message for" sections NOTE_CONTENT=$(awk ' /^## (Note|Message) for /{ in_note=1; header=$0; next } in_note && /^## /{ in_note=0 } in_note{ buf=buf"\n"$0 } END{ if(buf) print header buf } ' "$LOG_FILE") if [ -n "$NOTE_CONTENT" ]; then if [ "$NOTES_FOUND" -eq 0 ]; then echo "" echo -e "${YELLOW}============================================================${NC}" echo -e "${YELLOW} MESSAGES FROM OTHER TEAM MEMBERS${NC}" echo -e "${YELLOW}============================================================${NC}" NOTES_FOUND=1 fi LOG_AUTHOR=$(awk '/^- \*\*User:\*\*/{print; exit}' "$LOG_FILE" | sed 's/.*\*\*User:\*\* //') LOG_DATE=$(basename "$LOG_FILE" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1) echo "" echo -e "${YELLOW} From: ${LOG_AUTHOR:-unknown} | ${LOG_DATE:-unknown date} | ${LOG_FILE}${NC}" echo -e "${YELLOW}------------------------------------------------------------${NC}" echo "$NOTE_CONTENT" fi fi done if [ "$NOTES_FOUND" -gt 0 ]; then echo "" echo -e "${YELLOW}============================================================${NC}" echo -e "${YELLOW} Address the above before continuing with other work.${NC}" echo -e "${YELLOW}============================================================${NC}" echo "" fi fi fi # Phase 6: Vault sync echo "" echo "=== Phase 6: Vault sync ===" VAULT_PATH="" if [ -f ".claude/identity.json" ]; then VAULT_PATH=$($PYTHON -c "import json; d=json.load(open('.claude/identity.json')); print(d.get('vault_path',''))" 2>/dev/null || echo "") fi if [ -z "$VAULT_PATH" ]; then echo -e "${YELLOW}[INFO]${NC} vault_path not set in identity.json — skipping vault sync." elif [ ! -d "$VAULT_PATH/.git" ]; then echo -e "${YELLOW}[WARNING]${NC} Vault path '$VAULT_PATH' is not a git repo — skipping." else CLAUDETOOLS_DIR=$(pwd) cd "$VAULT_PATH" echo -e "${GREEN}[OK]${NC} Vault: $VAULT_PATH" # Commit any local vault changes if ! git diff-index --quiet HEAD -- 2>/dev/null; then echo -e "${YELLOW}[INFO]${NC} Local vault changes detected — committing..." git add -A git commit -m "sync: auto-sync vault from $MACHINE at $TIMESTAMP" echo -e "${GREEN}[OK]${NC} Vault committed." else echo -e "${GREEN}[OK]${NC} No local vault changes." fi VAULT_LOCAL_BEFORE=$(git rev-parse HEAD) git fetch origin --quiet VAULT_REMOTE_BRANCH="origin/main" if ! git rev-parse origin/main >/dev/null 2>&1; then VAULT_REMOTE_BRANCH="origin/master" fi VAULT_INCOMING=$(git rev-list --count HEAD..$VAULT_REMOTE_BRANCH 2>/dev/null || echo 0) VAULT_OUTGOING=$(git rev-list --count $VAULT_REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) if [ "$VAULT_INCOMING" -gt 0 ]; then echo -e "${CYAN}--- Vault: $VAULT_INCOMING incoming commit(s) ---${NC}" git log --oneline --format=' %C(yellow)%h%Creset %C(cyan)%an%Creset %s %C(dim)(%ar)%Creset' HEAD..$VAULT_REMOTE_BRANCH | head -10 if git pull origin main --rebase; then echo -e "${GREEN}[OK]${NC} Vault pulled." else echo -e "${RED}[ERROR]${NC} Vault pull failed — resolve conflicts manually in $VAULT_PATH." fi else echo -e "${GREEN}[OK]${NC} Vault: no incoming changes." fi VAULT_OUTGOING_AFTER=$(git rev-list --count $VAULT_REMOTE_BRANCH..HEAD 2>/dev/null || echo 0) if [ "$VAULT_OUTGOING_AFTER" -gt 0 ]; then if git push origin main; then echo -e "${GREEN}[OK]${NC} Vault pushed ($VAULT_OUTGOING_AFTER commit(s))." else echo -e "${RED}[ERROR]${NC} Vault push failed." fi else echo -e "${GREEN}[OK]${NC} Vault: nothing to push." fi cd "$CLAUDETOOLS_DIR" fi # Phase 7: Summary echo "" echo "=== Sync Summary ===" if [ "$INCOMING_COUNT" -gt 0 ]; then INCOMING_AUTHORS=$(git log --format='%an' $LOCAL_BEFORE..HEAD 2>/dev/null | sort | uniq -c | sort -rn | awk '{printf "%s (%s), ", substr($0, index($0,$2)), $1}' | sed 's/, $//') echo -e "${CYAN}Pulled in:${NC} $INCOMING_COUNT commit(s) — authors: ${INCOMING_AUTHORS:-unknown}" fi if [ "$OUTGOING_AFTER_PULL" -gt 0 ]; then echo -e "${CYAN}Pushed out:${NC} $OUTGOING_AFTER_PULL commit(s) by $USER_DISPLAY" fi if [ "$INCOMING_COUNT" -eq 0 ] && [ "$OUTGOING_AFTER_PULL" -eq 0 ]; then echo -e "${GREEN}Already in sync — no commits moved in either direction.${NC}" fi echo -e "${GREEN}[OK]${NC} HEAD: $(git log -1 --oneline)" echo -e "${GREEN}[OK]${NC} Status: $(git status -sb | head -1)" echo "" echo -e "${GREEN}[SUCCESS]${NC} Sync complete."