#!/usr/bin/env bash # cc-sync installer # Usage: curl -fsSL https://raw.githubusercontent.com/ikook-wang/cc-sync/main/install.sh | bash # https://github.com/ikook-wang/cc-sync set -euo pipefail CLAUDE_DIR="$HOME/.claude" REPO_URL="https://raw.githubusercontent.com/ikook-wang/cc-sync/main" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' print_banner() { echo "" echo -e "${CYAN} ╭──────────────────────────────────────╮${NC}" echo -e "${CYAN} │ │${NC}" echo -e "${CYAN} │ ${BOLD}cc-sync${NC}${CYAN} │${NC}" echo -e "${CYAN} │ Claude Code Config Sync │${NC}" echo -e "${CYAN} │ │${NC}" echo -e "${CYAN} │ Seamlessly sync your Claude Code │${NC}" echo -e "${CYAN} │ sessions across machines. │${NC}" echo -e "${CYAN} │ │${NC}" echo -e "${CYAN} ╰──────────────────────────────────────╯${NC}" echo "" } info() { echo -e "${BLUE}[info]${NC} $*"; } success() { echo -e "${GREEN}[done]${NC} $*"; } warn() { echo -e "${YELLOW}[warn]${NC} $*"; } error() { echo -e "${RED}[error]${NC} $*"; exit 1; } # ─── Step 1: Check prerequisites ─── check_prerequisites() { info "Checking prerequisites..." if ! command -v git &>/dev/null; then error "git is not installed. Please install git first." fi if [ ! -d "$CLAUDE_DIR" ]; then error "~/.claude directory not found. Please install and run Claude Code first." fi if ! command -v python3 &>/dev/null; then error "python3 is not installed. Required for JSON patching." fi # Check if already installed if [ -f "$CLAUDE_DIR/sync.sh" ] && [ -f "$CLAUDE_DIR/sync.conf" ]; then warn "cc-sync is already installed." echo -n " Reinstall/upgrade? [y/N] " read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo "Aborted." exit 0 fi fi success "Prerequisites OK." } # ─── Step 2: Get user input ─── get_user_input() { echo "" info "Configuration" echo "" # GitHub repo URL echo -e " ${BOLD}GitHub repository URL${NC}" echo " Create a private repo on GitHub, then paste the URL here." echo " Supports SSH (git@github.com:user/repo.git) or HTTPS." echo "" # Check if already has a remote if git -C "$CLAUDE_DIR" remote get-url origin &>/dev/null 2>&1; then existing_remote=$(git -C "$CLAUDE_DIR" remote get-url origin) echo -e " Current remote: ${CYAN}$existing_remote${NC}" echo -n " Keep this remote? [Y/n] " read -r keep_remote if [[ "$keep_remote" =~ ^[Nn]$ ]]; then echo -n " New repo URL: " read -r REMOTE_URL else REMOTE_URL="$existing_remote" fi else echo -n " Repo URL: " read -r REMOTE_URL fi if [ -z "$REMOTE_URL" ]; then error "Repository URL is required." fi echo "" # Sessions to keep echo -e " ${BOLD}Sessions to keep per project${NC} (for claude --resume)" echo -n " Number of recent sessions to sync [3]: " read -r sessions_input KEEP_SESSIONS="${sessions_input:-3}" # Validate number if ! [[ "$KEEP_SESSIONS" =~ ^[0-9]+$ ]]; then error "Invalid number: $KEEP_SESSIONS" fi echo "" success "Configuration complete." } # ─── Step 3: Initialize git repo ─── init_git_repo() { info "Setting up git repository..." cd "$CLAUDE_DIR" if [ ! -d ".git" ]; then git init --quiet git remote add origin "$REMOTE_URL" success "Initialized git repo with remote: $REMOTE_URL" else # Update remote if changed current_remote=$(git remote get-url origin 2>/dev/null || echo "") if [ "$current_remote" != "$REMOTE_URL" ]; then if [ -n "$current_remote" ]; then git remote set-url origin "$REMOTE_URL" else git remote add origin "$REMOTE_URL" fi success "Updated remote to: $REMOTE_URL" else success "Git repo already configured." fi fi } # ─── Step 4: Deploy files ─── deploy_files() { info "Deploying sync files..." # Download and install sync.sh if command -v curl &>/dev/null; then curl -fsSL "$REPO_URL/sync.sh" -o "$CLAUDE_DIR/sync.sh" curl -fsSL "$REPO_URL/uninstall.sh" -o "$CLAUDE_DIR/uninstall.sh" elif command -v wget &>/dev/null; then wget -qO "$CLAUDE_DIR/sync.sh" "$REPO_URL/sync.sh" wget -qO "$CLAUDE_DIR/uninstall.sh" "$REPO_URL/uninstall.sh" else error "Neither curl nor wget found." fi chmod +x "$CLAUDE_DIR/sync.sh" "$CLAUDE_DIR/uninstall.sh" # Write config cat > "$CLAUDE_DIR/sync.conf" << EOF # cc-sync configuration # https://github.com/ikook-wang/cc-sync KEEP_SESSIONS=$KEEP_SESSIONS EOF # Deploy .gitignore (merge with existing) if [ -f "$CLAUDE_DIR/.gitignore" ]; then # Download template if command -v curl &>/dev/null; then curl -fsSL "$REPO_URL/gitignore.template" -o /tmp/cc-sync-gitignore else wget -qO /tmp/cc-sync-gitignore "$REPO_URL/gitignore.template" fi # Check if already contains cc-sync marker if grep -q "cc-sync" "$CLAUDE_DIR/.gitignore" 2>/dev/null; then # Replace existing cc-sync gitignore cp /tmp/cc-sync-gitignore "$CLAUDE_DIR/.gitignore" else # Backup and replace (template covers all common cases) cp "$CLAUDE_DIR/.gitignore" "$CLAUDE_DIR/.gitignore.bak" cp /tmp/cc-sync-gitignore "$CLAUDE_DIR/.gitignore" warn "Backed up existing .gitignore to .gitignore.bak" fi rm -f /tmp/cc-sync-gitignore else if command -v curl &>/dev/null; then curl -fsSL "$REPO_URL/gitignore.template" -o "$CLAUDE_DIR/.gitignore" else wget -qO "$CLAUDE_DIR/.gitignore" "$REPO_URL/gitignore.template" fi fi success "Files deployed." } # ─── Step 5: Patch settings.json ─── patch_settings() { info "Configuring Claude Code hooks..." local settings_file="$CLAUDE_DIR/settings.json" # Create settings.json if it doesn't exist if [ ! -f "$settings_file" ]; then echo '{}' > "$settings_file" fi # Use python3 to safely patch JSON python3 << 'PYTHON_SCRIPT' import json import sys import os settings_file = os.path.expanduser("~/.claude/settings.json") try: with open(settings_file, "r") as f: settings = json.load(f) except (json.JSONDecodeError, FileNotFoundError): settings = {} # Ensure hooks object exists if "hooks" not in settings: settings["hooks"] = {} # Add SessionStart hook (only if not present) hook_command = ( "git -C ~/.claude pull --no-rebase --autostash --quiet 2>/dev/null || " "(cd ~/.claude && git checkout --ours . && git add -A && " "git commit -m 'sync: auto-resolve' --quiet) 2>/dev/null || true" ) session_start_hooks = settings["hooks"].get("SessionStart", []) # Check if cc-sync hook already exists has_cc_sync = False for hook_group in session_start_hooks: for hook in hook_group.get("hooks", []): if "git -C ~/.claude pull" in hook.get("command", ""): has_cc_sync = True break if not has_cc_sync: session_start_hooks.append({ "hooks": [{ "type": "command", "command": hook_command }] }) settings["hooks"]["SessionStart"] = session_start_hooks with open(settings_file, "w") as f: json.dump(settings, f, indent=2, ensure_ascii=False) f.write("\n") print("OK") PYTHON_SCRIPT success "SessionStart hook configured." } # ─── Step 6: Patch shell rc ─── patch_shell_rc() { info "Configuring shell wrapper..." # Detect shell local shell_name shell_name=$(basename "$SHELL") local rc_file case "$shell_name" in zsh) rc_file="$HOME/.zshrc" ;; bash) rc_file="$HOME/.bashrc" ;; *) warn "Unsupported shell: $shell_name. Please manually add the wrapper." warn "See: https://github.com/ikook-wang/cc-sync#manual-shell-setup" return ;; esac # Check if already patched if grep -q "cc-sync" "$rc_file" 2>/dev/null; then success "Shell wrapper already configured in $rc_file" return fi # Append wrapper function cat >> "$rc_file" << 'SHELL_WRAPPER' # cc-sync: auto-sync Claude Code config on exit # https://github.com/ikook-wang/cc-sync claude() { command claude "$@" bash ~/.claude/sync.sh &>/dev/null & } SHELL_WRAPPER success "Shell wrapper added to $rc_file" } # ─── Step 7: Initial sync ─── initial_sync() { info "Running initial sync..." cd "$CLAUDE_DIR" # Try to pull first (repo might already have content) git pull --no-rebase --quiet 2>/dev/null || true # Run sync bash "$CLAUDE_DIR/sync.sh" --verbose 2>/dev/null || true success "Initial sync complete." } # ─── Step 8: Done ─── print_done() { echo "" echo -e "${GREEN}${BOLD} Installation complete!${NC}" echo "" echo " How it works:" echo -e " ${CYAN}Start claude${NC} → auto pull latest config" echo -e " ${CYAN}Exit claude${NC} → auto commit & push" echo "" echo " On another machine, run the same install command," echo " then use ${BOLD}claude --resume${NC} to continue sessions." echo "" echo " Commands:" echo -e " ${CYAN}bash ~/.claude/sync.sh${NC} Manual sync" echo -e " ${CYAN}bash ~/.claude/sync.sh -v${NC} Verbose sync" echo -e " ${CYAN}bash ~/.claude/uninstall.sh${NC} Uninstall cc-sync" echo "" echo -e " Config: ${CYAN}~/.claude/sync.conf${NC}" echo "" echo -e " ${YELLOW}Note: Restart your terminal for the shell wrapper to take effect.${NC}" echo "" } # ─── Main ─── main() { print_banner check_prerequisites get_user_input init_git_repo deploy_files patch_settings patch_shell_rc initial_sync print_done } main