#!/bin/bash # # OurDigital Skills Installer # Portable installation tool for OurDigital Claude Skills package # # Usage: # ./install.sh # Interactive install # ./install.sh --update # Update existing installation # ./install.sh --uninstall # Remove installation # ./install.sh --commands # Only install/update global slash commands # ./install.sh --help # Show help # set -e # Colors for output 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 # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILLS_DIR="$(dirname "$SCRIPT_DIR")" REPO_DIR="$(dirname "$SKILLS_DIR")" ENV_FILE="$HOME/.env.ourdigital" CONFIG_DIR="$HOME/.ourdigital" VENV_DIR="$SKILLS_DIR/.venv-ourdigital" CLAUDE_COMMANDS_DIR="$HOME/.claude/commands" REPO_COMMANDS_DIR="$REPO_DIR/.claude/commands" # Print banner print_banner() { echo -e "${CYAN}" echo "╔═══════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ OurDigital Skills Installer ║" echo "║ 사람, 디지털, 그리고 문화 ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════╝" echo -e "${NC}" } # Print colored messages info() { echo -e "${BLUE}[INFO]${NC} $1"; } success() { echo -e "${GREEN}[OK]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } # Check prerequisites check_prerequisites() { info "Checking prerequisites..." local missing=() # Check Python 3.11+ if command -v python3 &> /dev/null; then PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') if [[ $(echo "$PYTHON_VERSION >= 3.11" | bc -l) -eq 1 ]]; then success "Python $PYTHON_VERSION" else warn "Python $PYTHON_VERSION (3.11+ recommended)" fi else missing+=("python3") fi # Check git if command -v git &> /dev/null; then success "Git $(git --version | cut -d' ' -f3)" else missing+=("git") fi # Check Claude Code CLI if command -v claude &> /dev/null; then success "Claude Code CLI available" else warn "Claude Code CLI not found (slash commands require it)" fi # Check 1Password CLI (optional) if command -v op &> /dev/null; then success "1Password CLI available" HAS_1PASSWORD=true else warn "1Password CLI not found (credentials will need manual entry)" HAS_1PASSWORD=false fi if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required tools: ${missing[*]}" exit 1 fi echo "" } # Create directory structure setup_directories() { info "Setting up directories..." # Create config directory mkdir -p "$CONFIG_DIR/credentials" chmod 700 "$CONFIG_DIR" chmod 700 "$CONFIG_DIR/credentials" success "Created $CONFIG_DIR/" # Create logs directory mkdir -p "$CONFIG_DIR/logs" success "Created $CONFIG_DIR/logs/" # Create Claude commands directory mkdir -p "$CLAUDE_COMMANDS_DIR" success "Created $CLAUDE_COMMANDS_DIR/" echo "" } # Install global slash commands via symlinks setup_global_commands() { info "Setting up global slash commands..." if [[ ! -d "$REPO_COMMANDS_DIR" ]]; then error "Commands directory not found: $REPO_COMMANDS_DIR" echo " Make sure you're running from the our-claude-skills repository." return 1 fi mkdir -p "$CLAUDE_COMMANDS_DIR" local linked=0 local skipped=0 local updated=0 for cmd_file in "$REPO_COMMANDS_DIR"/*.md; do [[ -f "$cmd_file" ]] || continue local filename filename=$(basename "$cmd_file") local target="$CLAUDE_COMMANDS_DIR/$filename" if [[ -L "$target" ]]; then local current_link current_link=$(readlink "$target") if [[ "$current_link" == "$cmd_file" ]]; then ((skipped++)) || true else rm "$target" ln -s "$cmd_file" "$target" ((updated++)) || true fi elif [[ -f "$target" ]]; then warn "Skipping $filename (non-symlink file exists at $target)" ((skipped++)) || true else ln -s "$cmd_file" "$target" ((linked++)) || true fi done [[ $linked -gt 0 ]] && success "Linked $linked new commands" [[ $updated -gt 0 ]] && success "Updated $updated existing symlinks" [[ $skipped -gt 0 ]] && info "Skipped $skipped (already current)" # Count total symlinks pointing to our repo local total total=$(find "$CLAUDE_COMMANDS_DIR" -maxdepth 1 -type l -lname "$REPO_COMMANDS_DIR/*" 2>/dev/null | wc -l | tr -d ' ') success "Total global commands from this repo: $total" echo "" } # Remove global slash command symlinks remove_global_commands() { info "Removing global slash command symlinks..." if [[ ! -d "$CLAUDE_COMMANDS_DIR" ]]; then info "No commands directory found, nothing to remove" return fi local removed=0 for link in "$CLAUDE_COMMANDS_DIR"/*.md; do [[ -L "$link" ]] || continue local link_target link_target=$(readlink "$link") if [[ "$link_target" == "$REPO_COMMANDS_DIR/"* ]]; then rm "$link" ((removed++)) || true fi done if [[ $removed -gt 0 ]]; then success "Removed $removed command symlinks" else info "No symlinks from this repo found" fi echo "" } # Setup environment file setup_environment() { info "Setting up environment file..." if [[ -f "$ENV_FILE" ]]; then warn "$ENV_FILE already exists" read -p "Overwrite? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then info "Keeping existing environment file" return fi cp "$ENV_FILE" "$ENV_FILE.backup.$(date +%Y%m%d%H%M%S)" success "Backed up existing file" fi cp "$SCRIPT_DIR/.env.ourdigital.template" "$ENV_FILE" chmod 600 "$ENV_FILE" success "Created $ENV_FILE" echo "" } # Setup credentials from 1Password or manual setup_credentials() { info "Setting up credentials..." if [[ "$HAS_1PASSWORD" == true ]]; then read -p "Fetch credentials from 1Password? (Y/n): " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then fetch_from_1password return fi fi echo -e "${YELLOW}Manual credential setup required.${NC}" echo "Edit $ENV_FILE with your API keys:" echo " - NOTION_API_TOKEN" echo " - GHOST_BLOG_ADMIN_KEY" echo " - GHOST_JOURNAL_ADMIN_KEY" echo " - FIGMA_ACCESS_TOKEN" echo "" } # Fetch credentials from 1Password fetch_from_1password() { info "Fetching credentials from 1Password..." # Check if signed in if ! op account list &> /dev/null; then warn "Please sign in to 1Password CLI first" echo "Run: eval \$(op signin)" return fi # Fetch each credential local credentials=( "NOTION_API_TOKEN:OurDigital Notion:api_token" "GHOST_BLOG_ADMIN_KEY:OurDigital Ghost Blog:api_key" "GHOST_JOURNAL_ADMIN_KEY:OurDigital Ghost Journal:api_key" "FIGMA_ACCESS_TOKEN:OurDigital Figma:api_token" ) for cred in "${credentials[@]}"; do IFS=':' read -r env_var item_name field <<< "$cred" if value=$(op item get "$item_name" --fields "$field" 2>/dev/null); then sed -i '' "s|^$env_var=.*|$env_var=$value|" "$ENV_FILE" 2>/dev/null || \ sed -i "s|^$env_var=.*|$env_var=$value|" "$ENV_FILE" success "Fetched $env_var" else warn "Could not fetch $env_var from 1Password" fi done echo "" } # Setup Python virtual environment setup_python_env() { info "Setting up Python virtual environment..." if [[ -d "$VENV_DIR" ]]; then warn "Virtual environment already exists" read -p "Recreate? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then rm -rf "$VENV_DIR" else info "Using existing virtual environment" return fi fi python3 -m venv "$VENV_DIR" success "Created virtual environment" # Activate and install dependencies source "$VENV_DIR/bin/activate" pip install --upgrade pip -q pip install -r "$SCRIPT_DIR/requirements/base.txt" -q success "Installed base dependencies" # Install code-specific if requested read -p "Install Claude Code dependencies? (Y/n): " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then pip install -r "$SCRIPT_DIR/requirements/code.txt" -q success "Installed Code dependencies" fi deactivate echo "" } # Copy config file setup_config() { info "Setting up configuration..." if [[ ! -f "$CONFIG_DIR/config.yaml" ]]; then cp "$SCRIPT_DIR/config/ourdigital.yaml" "$CONFIG_DIR/config.yaml" success "Created $CONFIG_DIR/config.yaml" else info "Config file already exists, skipping" fi echo "" } # Validate installation validate_installation() { info "Validating installation..." local checks_passed=0 local checks_total=7 # Check directories [[ -d "$CONFIG_DIR" ]] && { ((checks_passed++)) || true; success "Config directory exists"; } || warn "Config directory missing" [[ -d "$CONFIG_DIR/credentials" ]] && { ((checks_passed++)) || true; success "Credentials directory exists"; } || warn "Credentials directory missing" # Check files [[ -f "$ENV_FILE" ]] && { ((checks_passed++)) || true; success "Environment file exists"; } || warn "Environment file missing" [[ -f "$CONFIG_DIR/config.yaml" ]] && { ((checks_passed++)) || true; success "Config file exists"; } || warn "Config file missing" # Check virtual environment [[ -d "$VENV_DIR" ]] && { ((checks_passed++)) || true; success "Virtual environment exists"; } || warn "Virtual environment missing" # Check skills directory [[ -d "$SKILLS_DIR/01-ourdigital-brand-guide" ]] && { ((checks_passed++)) || true; success "Skills directory valid"; } || warn "Skills not found" # Check global commands local cmd_count cmd_count=$(find "$CLAUDE_COMMANDS_DIR" -maxdepth 1 -type l -lname "$REPO_COMMANDS_DIR/*" 2>/dev/null | wc -l | tr -d ' ') if [[ $cmd_count -gt 0 ]]; then ((checks_passed++)) || true success "Global slash commands installed ($cmd_count commands)" else warn "No global slash commands found (run with --commands to install)" fi echo "" echo -e "${CYAN}Validation: $checks_passed/$checks_total checks passed${NC}" echo "" } # Show installed skills show_skills() { info "Installed Skills:" echo "" local categories=( "OurDigital Core:0{1,2,3,4,5,6,7,8,9}-ourdigital-*,10-ourdigital-*" "SEO Tools:1{1,2,3,4,5,6,7,8,9}-seo-*,2{0,1,2,3,4,5,6,7,8,9}-seo-*,3{0,1,2,3,4}-seo-*" "Jamie Clinic:4{0,1,2,3,4,5}-jamie-*" "NotebookLM:5{0,1,2,3}-notebooklm-*" "GTM/GA:6{0,1,2}-gtm-*" "Notion:3{1,2}-notion-*" "Reference & Multi-Agent:9{0,1}-*" ) for category in "${categories[@]}"; do IFS=':' read -r label patterns <<< "$category" local count=0 local names=() IFS=',' read -ra pattern_arr <<< "$patterns" for pattern in "${pattern_arr[@]}"; do for dir in "$SKILLS_DIR"/$pattern; do if [[ -d "$dir" ]]; then ((count++)) || true names+=("$(basename "$dir")") fi done done if [[ $count -gt 0 ]]; then echo -e " ${CYAN}$label${NC} ($count skills)" for name in "${names[@]}"; do echo -e " ${GREEN}✓${NC} $name" done echo "" fi done } # Show global command summary show_global_commands() { info "Global Slash Commands (available from any project):" echo "" if [[ ! -d "$CLAUDE_COMMANDS_DIR" ]]; then warn "No commands directory found" return fi local cmd_count=0 local categories=() for link in "$CLAUDE_COMMANDS_DIR"/*.md; do [[ -L "$link" ]] || continue local link_target link_target=$(readlink "$link") if [[ "$link_target" == "$REPO_COMMANDS_DIR/"* ]]; then ((cmd_count++)) || true local name name=$(basename "$link" .md) categories+=("$name") fi done if [[ $cmd_count -eq 0 ]]; then warn "No commands symlinked from this repo" return fi # Group by prefix local seo=0 gtm=0 jamie=0 notebooklm=0 notion=0 ourdigital=0 other=0 for name in "${categories[@]}"; do case "$name" in seo-*) ((seo++)) || true ;; gtm-*) ((gtm++)) || true ;; jamie-*) ((jamie++)) || true ;; notebooklm-*) ((notebooklm++)) || true ;; notion-*) ((notion++)) || true ;; ourdigital-*) ((ourdigital++)) || true ;; *) ((other++)) || true ;; esac done [[ $seo -gt 0 ]] && echo -e " ${GREEN}SEO${NC}: $seo commands (/seo-technical, /seo-keyword-strategy, ...)" [[ $gtm -gt 0 ]] && echo -e " ${GREEN}GTM${NC}: $gtm commands (/gtm-audit, /gtm-manager)" [[ $jamie -gt 0 ]] && echo -e " ${GREEN}Jamie${NC}: $jamie commands (/jamie-editor, /jamie-audit)" [[ $notebooklm -gt 0 ]] && echo -e " ${GREEN}NotebookLM${NC}: $notebooklm commands (/notebooklm-agent, ...)" [[ $notion -gt 0 ]] && echo -e " ${GREEN}Notion${NC}: $notion commands (/notion-writer, /notion-organizer)" [[ $ourdigital -gt 0 ]] && echo -e " ${GREEN}OurDigital${NC}: $ourdigital commands (/ourdigital-research, ...)" [[ $other -gt 0 ]] && echo -e " ${GREEN}Other${NC}: $other commands" echo "" echo -e " ${CYAN}Total: $cmd_count global commands${NC}" echo "" } # Print usage instructions print_usage() { echo -e "${CYAN}Usage Instructions:${NC}" echo "" echo "1. Activate the virtual environment:" echo " source $VENV_DIR/bin/activate" echo "" echo "2. Edit credentials if needed:" echo " nano $ENV_FILE" echo "" echo "3. Use slash commands from any project (globally installed):" echo " /seo-technical https://example.com" echo " /reference-curator \"Claude Code best practices\"" echo " /ourdigital-research \"topic\"" echo " /notebooklm-agent" echo "" echo "4. Update commands after pulling repo changes:" echo " ./install.sh --commands" echo "" } # Uninstall uninstall() { warn "This will remove OurDigital configuration and global commands (not skill source files)." read -p "Continue? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then info "Cancelled" exit 0 fi # Remove global command symlinks remove_global_commands # Backup before removing if [[ -f "$ENV_FILE" ]]; then cp "$ENV_FILE" "$ENV_FILE.uninstall.backup" rm "$ENV_FILE" success "Removed $ENV_FILE (backup saved)" fi if [[ -d "$VENV_DIR" ]]; then rm -rf "$VENV_DIR" success "Removed virtual environment" fi info "Config directory preserved at $CONFIG_DIR" info "To fully remove: rm -rf $CONFIG_DIR" success "Uninstall complete" } # Show help show_help() { echo "OurDigital Skills Installer" echo "" echo "Usage: ./install.sh [OPTIONS]" echo "" echo "Options:" echo " --help, -h Show this help message" echo " --update, -u Update existing installation" echo " --uninstall Remove installation (preserves skill source files)" echo " --validate Only run validation checks" echo " --commands Only install/update global slash commands" echo " --skip-creds Skip credentials setup" echo " --skip-venv Skip virtual environment setup" echo "" echo "Examples:" echo " ./install.sh # Full interactive install" echo " ./install.sh --update # Update existing setup" echo " ./install.sh --commands # Just install/refresh global commands" echo " ./install.sh --skip-creds # Install without credentials" echo "" echo "Global Commands:" echo " Slash commands are symlinked to ~/.claude/commands/ so they" echo " work from any project directory in Claude Code." echo "" } # Main installation flow main_install() { print_banner check_prerequisites setup_directories setup_global_commands setup_environment if [[ "$SKIP_CREDS" != true ]]; then setup_credentials fi if [[ "$SKIP_VENV" != true ]]; then setup_python_env fi setup_config validate_installation show_skills show_global_commands print_usage echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Installation Complete! ║${NC}" echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}" } # Parse arguments SKIP_CREDS=false SKIP_VENV=false while [[ $# -gt 0 ]]; do case $1 in --help|-h) show_help exit 0 ;; --uninstall) uninstall exit 0 ;; --validate) validate_installation show_global_commands exit 0 ;; --commands) info "Installing/updating global slash commands..." setup_global_commands show_global_commands exit 0 ;; --update|-u) info "Updating installation..." setup_global_commands setup_python_env validate_installation show_global_commands exit 0 ;; --skip-creds) SKIP_CREDS=true shift ;; --skip-venv) SKIP_VENV=true shift ;; *) error "Unknown option: $1" show_help exit 1 ;; esac done # Run main install main_install