Last active
August 28, 2025 15:42
-
-
Save shoveller/dee67d5961fa73dd82d2ebbf00a757a7 to your computer and use it in GitHub Desktop.
Robust shell script to clone ccpm repository and setup Claude configuration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Claude Configuration Setup Script | |
| # | |
| # Description: Clones the ccpm repository and copies the .claude configuration | |
| # to the current directory for Claude IDE integration. | |
| # | |
| # Author: Automated script generation | |
| # Version: 1.0 | |
| # Created: $(date +%Y-%m-%d) | |
| # | |
| # Usage: ./setup-claude-config.sh [OPTIONS] | |
| # ./setup-claude-config.sh --help | |
| # | |
| # Exit Codes: | |
| # 0 - Success | |
| # 1 - General error | |
| # 2 - Missing dependencies | |
| # 3 - Network/clone error | |
| # 4 - File operation error | |
| # 5 - Permission error | |
| # 6 - Invalid arguments | |
| # | |
| # Enable strict error handling and security | |
| set -euo pipefail | |
| # Set secure umask | |
| umask 077 | |
| # Script configuration | |
| readonly SCRIPT_NAME=$(basename "$0") | |
| readonly SCRIPT_VERSION="1.0" | |
| readonly DEFAULT_REPO_URL="https://github.com/automazeio/ccpm.git" | |
| readonly DEFAULT_TEMP_DIR="/tmp/ccpm" | |
| readonly DEFAULT_SOURCE_PATH="/.claude" | |
| readonly DEFAULT_DEST_PATH="./" | |
| # Global variables | |
| REPO_URL="$DEFAULT_REPO_URL" | |
| TEMP_DIR="$DEFAULT_TEMP_DIR" | |
| SOURCE_PATH="$DEFAULT_SOURCE_PATH" | |
| DEST_PATH="$DEFAULT_DEST_PATH" | |
| VERBOSE=false | |
| QUIET=false | |
| FORCE=false | |
| BACKUP=false | |
| LOG_FILE="" | |
| # Color codes for output (if terminal supports it) | |
| if [[ -t 1 ]]; then | |
| readonly RED='\033[0;31m' | |
| readonly GREEN='\033[0;32m' | |
| readonly YELLOW='\033[1;33m' | |
| readonly BLUE='\033[0;34m' | |
| readonly NC='\033[0m' # No Color | |
| else | |
| readonly RED='' | |
| readonly GREEN='' | |
| readonly YELLOW='' | |
| readonly BLUE='' | |
| readonly NC='' | |
| fi | |
| # Logging functions | |
| log_info() { | |
| if [[ "$QUIET" != true ]]; then | |
| echo -e "${BLUE}[INFO]${NC} $1" >&2 | |
| [[ -n "$LOG_FILE" ]] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" | |
| fi | |
| } | |
| log_warn() { | |
| echo -e "${YELLOW}[WARN]${NC} $1" >&2 | |
| [[ -n "$LOG_FILE" ]] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARN: $1" >> "$LOG_FILE" | |
| } | |
| log_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" >&2 | |
| [[ -n "$LOG_FILE" ]] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" | |
| } | |
| log_success() { | |
| if [[ "$QUIET" != true ]]; then | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 | |
| [[ -n "$LOG_FILE" ]] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" | |
| fi | |
| } | |
| log_debug() { | |
| if [[ "$VERBOSE" == true ]]; then | |
| echo -e "${BLUE}[DEBUG]${NC} $1" >&2 | |
| [[ -n "$LOG_FILE" ]] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $1" >> "$LOG_FILE" | |
| fi | |
| } | |
| # Cleanup function | |
| cleanup_temp_dir() { | |
| if [[ -d "$TEMP_DIR" ]]; then | |
| # Safety check: ensure we're only removing directories under /tmp | |
| if [[ "$TEMP_DIR" == /tmp/* ]]; then | |
| log_debug "Cleaning up temporary directory: $TEMP_DIR" | |
| rm -rf "$TEMP_DIR" | |
| log_debug "Temporary directory cleaned up" | |
| else | |
| log_warn "Skipping cleanup of suspicious path: $TEMP_DIR" | |
| fi | |
| fi | |
| } | |
| # Exit handler | |
| cleanup_and_exit() { | |
| local exit_code=${1:-1} | |
| log_debug "Script exiting with code $exit_code" | |
| cleanup_temp_dir | |
| exit "$exit_code" | |
| } | |
| # Set up signal handlers | |
| trap 'cleanup_and_exit 1' INT TERM | |
| # Display help | |
| show_help() { | |
| cat << EOF | |
| $SCRIPT_NAME v$SCRIPT_VERSION | |
| USAGE: | |
| $SCRIPT_NAME [OPTIONS] | |
| DESCRIPTION: | |
| Clones the ccpm repository and copies the .claude configuration directory | |
| to the current directory for Claude IDE integration. | |
| OPTIONS: | |
| -r, --repo-url URL Repository URL to clone (default: $DEFAULT_REPO_URL) | |
| -t, --temp-dir DIR Temporary directory path (default: $DEFAULT_TEMP_DIR) | |
| -d, --dest-path DIR Destination path (default: $DEFAULT_DEST_PATH) | |
| -f, --force Force overwrite existing .claude directory | |
| -b, --backup Create backup of existing .claude directory | |
| -v, --verbose Enable verbose output | |
| -q, --quiet Suppress non-error output | |
| -l, --log-file FILE Log operations to specified file | |
| -h, --help Show this help message | |
| --version Show version information | |
| EXIT CODES: | |
| 0 Success | |
| 1 General error | |
| 2 Missing dependencies | |
| 3 Network/clone error | |
| 4 File operation error | |
| 5 Permission error | |
| 6 Invalid arguments | |
| EXAMPLES: | |
| $SCRIPT_NAME # Basic usage | |
| $SCRIPT_NAME --verbose # Verbose output | |
| $SCRIPT_NAME --backup --force # Backup existing and force overwrite | |
| $SCRIPT_NAME --repo-url https://github.com/user/repo.git # Custom repository | |
| REQUIREMENTS: | |
| - bash (version 4.0+) | |
| - git (any recent version) | |
| - cp, rm, mkdir (coreutils) | |
| - Network connectivity | |
| EOF | |
| } | |
| # Show version | |
| show_version() { | |
| echo "$SCRIPT_NAME v$SCRIPT_VERSION" | |
| } | |
| # Validate dependencies | |
| check_dependencies() { | |
| log_debug "Checking dependencies" | |
| local missing_deps=() | |
| # Check for required commands | |
| for cmd in git cp rm mkdir chmod; do | |
| if ! command -v "$cmd" &> /dev/null; then | |
| missing_deps+=("$cmd") | |
| fi | |
| done | |
| if [[ ${#missing_deps[@]} -gt 0 ]]; then | |
| log_error "Missing required dependencies: ${missing_deps[*]}" | |
| log_error "Please install the missing dependencies and try again" | |
| cleanup_and_exit 2 | |
| fi | |
| # Check bash version | |
| if [[ ${BASH_VERSION%%.*} -lt 4 ]]; then | |
| log_warn "Bash version ${BASH_VERSION} detected. Version 4.0+ recommended" | |
| fi | |
| log_debug "All dependencies satisfied" | |
| } | |
| # Validate input parameters | |
| validate_inputs() { | |
| log_debug "Validating input parameters" | |
| # Validate repository URL | |
| if [[ ! "$REPO_URL" =~ ^https?://|^git@ ]]; then | |
| log_error "Invalid repository URL format: $REPO_URL" | |
| cleanup_and_exit 6 | |
| fi | |
| # Validate temporary directory path | |
| if [[ ! "$TEMP_DIR" =~ ^/tmp/ ]]; then | |
| log_error "Temporary directory must be under /tmp for security: $TEMP_DIR" | |
| cleanup_and_exit 6 | |
| fi | |
| # Check if destination is writable | |
| if [[ ! -w "$(dirname "$DEST_PATH")" ]]; then | |
| log_error "Destination directory is not writable: $(dirname "$DEST_PATH")" | |
| cleanup_and_exit 5 | |
| fi | |
| # Check disk space (require at least 100MB free in /tmp) | |
| local available_space | |
| available_space=$(df /tmp | awk 'NR==2 {print $4}') | |
| if [[ $available_space -lt 100000 ]]; then | |
| log_warn "Low disk space in /tmp: ${available_space}KB available" | |
| fi | |
| log_debug "Input validation passed" | |
| } | |
| # Check network connectivity | |
| check_network() { | |
| log_debug "Checking network connectivity" | |
| # Extract hostname from repository URL | |
| local hostname | |
| if [[ "$REPO_URL" =~ ^https://([^/]+) ]]; then | |
| hostname="${BASH_REMATCH[1]}" | |
| elif [[ "$REPO_URL" =~ ^git@([^:]+): ]]; then | |
| hostname="${BASH_REMATCH[1]}" | |
| else | |
| log_warn "Could not extract hostname from repository URL for connectivity check" | |
| return 0 | |
| fi | |
| # Test connectivity (timeout after 10 seconds) | |
| if ! timeout 10 bash -c "</dev/tcp/$hostname/443" 2>/dev/null && | |
| ! timeout 10 bash -c "</dev/tcp/$hostname/22" 2>/dev/null; then | |
| log_error "Cannot connect to $hostname. Check network connectivity" | |
| cleanup_and_exit 3 | |
| fi | |
| log_debug "Network connectivity verified" | |
| } | |
| # Create backup of existing .claude directory | |
| create_backup() { | |
| local claude_dir="$DEST_PATH/.claude" | |
| if [[ -d "$claude_dir" ]]; then | |
| local backup_dir="$claude_dir.backup.$(date +%Y%m%d-%H%M%S)" | |
| log_info "Creating backup of existing .claude directory: $backup_dir" | |
| if ! cp -r "$claude_dir" "$backup_dir"; then | |
| log_error "Failed to create backup of existing .claude directory" | |
| cleanup_and_exit 4 | |
| fi | |
| log_success "Backup created: $backup_dir" | |
| fi | |
| } | |
| # Clone repository | |
| clone_repository() { | |
| log_info "Cloning repository: $REPO_URL" | |
| log_debug "Target directory: $TEMP_DIR" | |
| # Remove existing temporary directory if it exists | |
| if [[ -d "$TEMP_DIR" ]]; then | |
| log_warn "Temporary directory already exists: $TEMP_DIR" | |
| cleanup_temp_dir | |
| fi | |
| # Clone repository with progress output control | |
| local git_opts=() | |
| if [[ "$QUIET" == true ]]; then | |
| git_opts+=(--quiet) | |
| elif [[ "$VERBOSE" == true ]]; then | |
| git_opts+=(--verbose) | |
| fi | |
| # Perform the clone | |
| if ! git clone "${git_opts[@]}" "$REPO_URL" "$TEMP_DIR"; then | |
| log_error "Failed to clone repository from $REPO_URL" | |
| log_error "Check repository URL, network connectivity, and authentication" | |
| cleanup_and_exit 3 | |
| fi | |
| log_success "Repository cloned successfully" | |
| } | |
| # Copy .claude directory | |
| copy_claude_directory() { | |
| local source_dir="$TEMP_DIR$SOURCE_PATH" | |
| local dest_dir="$DEST_PATH/.claude" | |
| log_info "Copying .claude configuration" | |
| log_debug "Source: $source_dir" | |
| log_debug "Destination: $dest_dir" | |
| # Check if source directory exists | |
| if [[ ! -d "$source_dir" ]]; then | |
| log_error ".claude directory not found in cloned repository: $source_dir" | |
| log_error "Repository may not contain Claude configuration" | |
| cleanup_and_exit 4 | |
| fi | |
| # Handle existing destination directory | |
| if [[ -d "$dest_dir" ]]; then | |
| if [[ "$BACKUP" == true ]]; then | |
| create_backup | |
| fi | |
| if [[ "$FORCE" == true ]]; then | |
| log_warn "Removing existing .claude directory" | |
| rm -rf "$dest_dir" | |
| else | |
| log_error ".claude directory already exists: $dest_dir" | |
| log_error "Use --force to overwrite or --backup to create backup first" | |
| cleanup_and_exit 4 | |
| fi | |
| fi | |
| # Perform the copy | |
| if ! cp -r "$source_dir" "$dest_dir"; then | |
| log_error "Failed to copy .claude directory" | |
| cleanup_and_exit 4 | |
| fi | |
| # Set appropriate permissions | |
| chmod -R u+rw,go-rwx "$dest_dir" 2>/dev/null || { | |
| log_warn "Could not set restrictive permissions on .claude directory" | |
| } | |
| log_success ".claude directory copied successfully" | |
| } | |
| # Parse command line arguments | |
| parse_arguments() { | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -r|--repo-url) | |
| [[ -n "${2:-}" ]] || { log_error "Repository URL required for $1"; cleanup_and_exit 6; } | |
| REPO_URL="$2" | |
| shift 2 | |
| ;; | |
| -t|--temp-dir) | |
| [[ -n "${2:-}" ]] || { log_error "Temporary directory required for $1"; cleanup_and_exit 6; } | |
| TEMP_DIR="$2" | |
| shift 2 | |
| ;; | |
| -d|--dest-path) | |
| [[ -n "${2:-}" ]] || { log_error "Destination path required for $1"; cleanup_and_exit 6; } | |
| DEST_PATH="$2" | |
| shift 2 | |
| ;; | |
| -f|--force) | |
| FORCE=true | |
| shift | |
| ;; | |
| -b|--backup) | |
| BACKUP=true | |
| shift | |
| ;; | |
| -v|--verbose) | |
| VERBOSE=true | |
| shift | |
| ;; | |
| -q|--quiet) | |
| QUIET=true | |
| shift | |
| ;; | |
| -l|--log-file) | |
| [[ -n "${2:-}" ]] || { log_error "Log file path required for $1"; cleanup_and_exit 6; } | |
| LOG_FILE="$2" | |
| # Create log file and test write access | |
| if ! touch "$LOG_FILE" 2>/dev/null; then | |
| log_error "Cannot write to log file: $LOG_FILE" | |
| cleanup_and_exit 5 | |
| fi | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| show_help | |
| exit 0 | |
| ;; | |
| --version) | |
| show_version | |
| exit 0 | |
| ;; | |
| *) | |
| log_error "Unknown argument: $1" | |
| log_error "Use --help for usage information" | |
| cleanup_and_exit 6 | |
| ;; | |
| esac | |
| done | |
| # Validate conflicting options | |
| if [[ "$VERBOSE" == true && "$QUIET" == true ]]; then | |
| log_error "Cannot use --verbose and --quiet together" | |
| cleanup_and_exit 6 | |
| fi | |
| } | |
| # Main execution function | |
| main() { | |
| log_info "Starting Claude configuration setup" | |
| log_debug "Script version: $SCRIPT_VERSION" | |
| # Pre-execution checks | |
| check_dependencies | |
| validate_inputs | |
| check_network | |
| # Core operations | |
| clone_repository | |
| copy_claude_directory | |
| # Cleanup | |
| cleanup_temp_dir | |
| log_success "Claude configuration setup completed successfully" | |
| log_info ".claude directory is now available in: $(realpath "$DEST_PATH/.claude")" | |
| return 0 | |
| } | |
| # Script entry point | |
| if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then | |
| # Parse command line arguments | |
| parse_arguments "$@" | |
| # Execute main function | |
| main | |
| # Exit with success | |
| exit 0 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment