#!/usr/bin/env bash set -euo pipefail # ============================================================================ # CCS Installation Script (v4.5.0) - DEPRECATED # DEPRECATED: This installer is deprecated. Use npm instead. # Bootstrap-based: Installs lightweight shell wrappers (LEGACY) # Requires: Node.js 14+ (npm recommended) # ============================================================================ # --- Deprecation Notice --- echo "" echo "=======================================================================" echo " " echo " [!] DEPRECATION NOTICE " echo " " echo " Native shell installers are deprecated and will be removed " echo " in a future version. Please use npm installation instead: " echo " " echo " npm install -g @kaitranntt/ccs " echo " " echo " Proceeding with legacy install (auto-runs npm if available)... " echo " " echo "=======================================================================" echo "" sleep 3 # Give users time to read # --- Auto-redirect to npm installation --- if command -v npm &> /dev/null; then echo "[i] Node.js detected, using npm installation (recommended)..." echo "" npm install -g @kaitranntt/ccs exit_code=$? if [ $exit_code -eq 0 ]; then echo "" echo "[OK] CCS installed via npm successfully!" echo "" echo "Quick start:" echo " ccs # Use Claude (default)" echo " ccs glm # Use GLM" echo " ccs --help # Show all commands" echo "" exit 0 else echo "" echo "[!] npm installation failed. Falling back to legacy install..." echo "" sleep 2 fi else echo "[!] npm not found. Falling back to legacy install..." echo "[!] Install Node.js from https://nodejs.org for the recommended method." echo "" sleep 2 fi # Continue with legacy bash installation... # --- Configuration --- INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" CCS_DIR="$HOME/.ccs" CLAUDE_DIR="$HOME/.claude" GLM_MODEL="glm-4.6" KIMI_MODEL="kimi-for-coding" # Resolve script directory (handles both file-based and piped execution) if [[ -n "${BASH_SOURCE[0]:-}" ]]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" else SCRIPT_DIR="$(cd "$(dirname "${0:-$PWD}")" && pwd)" fi # Detect installation method (git vs standalone) # Check if ccs executable exists in SCRIPT_DIR or parent (real git install) # Don't just check .git (user might run curl | bash inside their own git repo) if [[ -f "$SCRIPT_DIR/lib/ccs" ]] || [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then INSTALL_METHOD="git" else INSTALL_METHOD="standalone" fi # Version configuration # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (curl | bash) # For git installations, VERSION file is read if available CCS_VERSION="5.3.0-beta.4" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then CCS_VERSION="$(cat "$SCRIPT_DIR/VERSION" | tr -d '\n' | tr -d '\r')" elif [[ -f "$SCRIPT_DIR/../VERSION" ]]; then CCS_VERSION="$(cat "$SCRIPT_DIR/../VERSION" | tr -d '\n' | tr -d '\r')" fi # --- Platform Detection --- # Detect platform and redirect to Windows installer if needed detect_platform() { case "$OSTYPE" in msys*|mingw*|cygwin*|win32*) echo "windows" ;; *) echo "unix" ;; esac } PLATFORM=$(detect_platform) if [[ "$PLATFORM" == "windows" ]]; then echo "Windows detected. Using PowerShell installer..." if [[ -f "$SCRIPT_DIR/install.ps1" ]]; then powershell.exe -ExecutionPolicy Bypass -File "$SCRIPT_DIR/install.ps1" exit $? else echo "Error: install.ps1 not found." echo "Please download the full CCS package from:" echo " https://github.com/kaitranntt/ccs" exit 1 fi fi # Continue with Unix installation... # --- Helper Functions --- detect_current_provider() { local settings="$CLAUDE_DIR/settings.json" if [[ ! -f "$settings" ]]; then echo "unknown" return fi if grep -q "api.kimi.com\|kimi-for-coding" "$settings" 2>/dev/null; then echo "kimi" elif grep -q "api.z.ai\|glm-4" "$settings" 2>/dev/null; then echo "glm" elif grep -q "ANTHROPIC_BASE_URL" "$settings" 2>/dev/null && ! grep -q "api.z.ai\|api.kimi.com" "$settings" 2>/dev/null; then echo "custom" else echo "claude" fi } # --- Color/Format Functions (ANSI) --- setup_colors() { if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET='' fi } msg_critical() { echo "" >&2 echo -e "${RED}${BOLD}╔═════════════════════════════════════════════╗${RESET}" >&2 echo -e "${RED}${BOLD}║ ACTION REQUIRED ║${RESET}" >&2 echo -e "${RED}${BOLD}╚═════════════════════════════════════════════╝${RESET}" >&2 echo "" >&2 echo -e "${RED}$1${RESET}" >&2 echo "" >&2 } msg_warning() { echo "" >&2 echo -e "${YELLOW}${BOLD}[!] WARNING${RESET}" >&2 echo -e "${YELLOW}$1${RESET}" >&2 echo "" >&2 } msg_success() { echo -e "${GREEN}[OK] $1${RESET}" } msg_info() { echo -e "[i] $1" } msg_section() { echo "" echo -e "${BOLD}===== $1 =====${RESET}" echo "" } setup_colors # --- Node.js Detection (v4.5) --- check_nodejs() { if ! command -v node &> /dev/null; then msg_warning "Node.js not found CCS v4.5+ requires Node.js 14+ to run. The bootstrap scripts will check and install the npm package on first use. Install Node.js: https://nodejs.org (LTS recommended) Installation will continue, but 'ccs' will not work until Node.js is installed." return 1 fi local node_major node_major=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ $node_major -lt 14 ]]; then msg_warning "Node.js 14+ required (found: $(node -v)) CCS v4.5+ requires Node.js 14 or newer. Upgrade from: https://nodejs.org Installation will continue, but 'ccs' may not work correctly." return 1 fi msg_success "Node.js $(node -v) detected" return 0 } # --- Shell Profile Management --- detect_shell_profile() { # Safe extraction of shell name (no command substitution) local shell_path="${SHELL:-/bin/bash}" local shell_name="${shell_path##*/}" # Validate shell_name is alphanumeric (defense in depth) if [[ ! "$shell_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then shell_name="bash" fi case "$shell_name" in zsh) echo "$HOME/.zshrc" ;; bash) if [[ "$OSTYPE" == darwin* ]]; then # macOS prefers bash_profile [[ -f "$HOME/.bash_profile" ]] && echo "$HOME/.bash_profile" || echo "$HOME/.bashrc" else echo "$HOME/.bashrc" fi ;; fish) echo "$HOME/.config/fish/config.fish" ;; *) # Default to bashrc echo "$HOME/.bashrc" ;; esac } check_path_configured() { [[ ":$PATH:" == *":$HOME/.local/bin:"* ]] } add_to_path() { local profile_file="$1" local dir_to_add="$HOME/.local/bin" # Create profile file if doesn't exist if [[ ! -f "$profile_file" ]]; then local profile_dir="$(dirname "$profile_file")" if ! mkdir -p "$profile_dir" 2>/dev/null; then echo "[!] Failed to create directory: $profile_dir" >&2 return 1 fi if ! touch "$profile_file" 2>/dev/null; then echo "[!] Failed to create profile file: $profile_file" >&2 return 1 fi fi # Check if already in profile (avoid duplicates) if grep -q "# CCS: Added by Claude Code Switch installer" "$profile_file" 2>/dev/null; then return 0 # Already added fi # Check for fish shell (different syntax) if [[ "$profile_file" == *"config.fish" ]]; then cat >> "$profile_file" << 'EOF' # CCS: Added by Claude Code Switch installer set -gx PATH $HOME/.local/bin $PATH EOF else # Bash/Zsh syntax cat >> "$profile_file" << 'EOF' # CCS: Added by Claude Code Switch installer export PATH="$HOME/.local/bin:$PATH" EOF fi return 0 } configure_shell_path() { if check_path_configured; then msg_info "PATH already configured for ~/.local/bin" return 0 fi local profile_file=$(detect_shell_profile) echo "" msg_section "Configuring Shell PATH" msg_info "Detected shell profile: $profile_file" if add_to_path "$profile_file"; then msg_success "Added ~/.local/bin to PATH in $profile_file" echo "" # Show reload instructions msg_critical "Reload your shell to use 'ccs' command: Option 1 (current session): source $profile_file Option 2 (new session): Open a new terminal window Then verify: ccs --version" return 0 else msg_warning "Could not auto-configure PATH Manually add this line to $profile_file: export PATH=\"\$HOME/.local/bin:\$PATH\" Then reload: source $profile_file" return 1 fi } create_glm_template() { cat << EOF { "env": { "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic", "ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE", "ANTHROPIC_MODEL": "$GLM_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL": "$GLM_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL": "$GLM_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "$GLM_MODEL" } } EOF } create_kimi_template() { cat << EOF { "env": { "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/", "ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE", "ANTHROPIC_MODEL": "$KIMI_MODEL", "ANTHROPIC_SMALL_FAST_MODEL": "$KIMI_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL": "$KIMI_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL": "$KIMI_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "$KIMI_MODEL" }, "alwaysThinkingEnabled": true } EOF } atomic_mv() { local src="$1" local dest="$2" if mv "$src" "$dest" 2>/dev/null; then return 0 else rm -f "$src" echo " [X] Error: Failed to create $dest (check permissions)" exit 1 fi } download_file() { local url="$1" local dest="$2" if ! curl -fsSL "$url" -o "$dest"; then echo " [!] Failed to download: $(basename "$dest")" return 1 fi return 0 } install_claude_folder() { local source_dir="$1" local target_dir="$CCS_DIR/.claude" # Check if already exists if [[ -d "$target_dir" ]]; then echo "| [i] .claude/ folder already exists, skipping" return 0 fi mkdir -p "$target_dir/commands" "$target_dir/skills/ccs-delegation/references" if [[ "$INSTALL_METHOD" == "git" ]]; then # Copy from local git repo if [[ -d "$source_dir/.claude" ]]; then cp -r "$source_dir/.claude"/* "$target_dir/" 2>/dev/null || { echo "| [!] Failed to copy .claude/ folder" return 1 } echo "| [OK] Installed .claude/ folder" else echo "| [!] .claude/ folder not found in source" return 1 fi else # Standalone: download from GitHub local base_url="https://raw.githubusercontent.com/kaitranntt/ccs/main/.claude" download_file "$base_url/commands/ccs.md" "$target_dir/commands/ccs.md" || return 1 download_file "$base_url/skills/ccs-delegation/SKILL.md" "$target_dir/skills/ccs-delegation/SKILL.md" || return 1 download_file "$base_url/skills/ccs-delegation/references/delegation-patterns.md" "$target_dir/skills/ccs-delegation/references/delegation-patterns.md" || return 1 echo "| [OK] Downloaded .claude/ folder" fi return 0 } create_glm_profile() { local current_settings="$CLAUDE_DIR/settings.json" local glm_settings="$CCS_DIR/glm.settings.json" local provider="$1" if [[ "$provider" == "glm" ]]; then echo "[OK] Copying current GLM config to profile..." if command -v jq &> /dev/null; then if jq '.env |= (. // {}) + { "ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$GLM_MODEL"'", "ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$GLM_MODEL"'", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$GLM_MODEL"'" }' "$current_settings" > "$glm_settings.tmp" 2>/dev/null; then atomic_mv "$glm_settings.tmp" "$glm_settings" echo " Created: $glm_settings (with your existing API key + enhanced settings)" else rm -f "$glm_settings.tmp" cp "$current_settings" "$glm_settings" echo " Created: $glm_settings (copied as-is, jq enhancement failed)" fi else cp "$current_settings" "$glm_settings" echo " Created: $glm_settings (copied as-is, jq not available)" fi else echo "Creating GLM profile template at $glm_settings" if [[ -f "$current_settings" ]] && command -v jq &> /dev/null; then if jq '.env |= (. // {}) + { "ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic", "ANTHROPIC_AUTH_TOKEN": "YOUR_GLM_API_KEY_HERE", "ANTHROPIC_MODEL": "'"$GLM_MODEL"'", "ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$GLM_MODEL"'", "ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$GLM_MODEL"'", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$GLM_MODEL"'" }' "$current_settings" > "$glm_settings.tmp" 2>/dev/null; then atomic_mv "$glm_settings.tmp" "$glm_settings" else rm -f "$glm_settings.tmp" echo " [i] jq failed, using basic template" create_glm_template > "$glm_settings" fi else create_glm_template > "$glm_settings" fi echo " Created: $glm_settings" echo " [!] Edit this file and replace YOUR_GLM_API_KEY_HERE with your actual GLM API key" fi } create_kimi_profile() { local current_settings="$CLAUDE_DIR/settings.json" local kimi_settings="$CCS_DIR/kimi.settings.json" local provider="$1" if [[ "$provider" == "kimi" ]]; then echo "[OK] Copying current Kimi config to profile..." if command -v jq &> /dev/null; then if jq '.env |= (. // {}) + { "ANTHROPIC_SMALL_FAST_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$KIMI_MODEL"'" }' "$current_settings" > "$kimi_settings.tmp" 2>/dev/null; then atomic_mv "$kimi_settings.tmp" "$kimi_settings" echo " Created: $kimi_settings (with your existing API key + enhanced settings)" else rm -f "$kimi_settings.tmp" cp "$current_settings" "$kimi_settings" echo " Created: $kimi_settings (copied as-is, jq enhancement failed)" fi else cp "$current_settings" "$kimi_settings" echo " Created: $kimi_settings (copied as-is, jq not available)" fi else echo "Creating Kimi profile template at $kimi_settings" if [[ -f "$current_settings" ]] && command -v jq &> /dev/null; then if jq '.env |= (. // {}) + { "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/", "ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE", "ANTHROPIC_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_SMALL_FAST_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_OPUS_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_SONNET_MODEL": "'"$KIMI_MODEL"'", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "'"$KIMI_MODEL"'" } | . + {"alwaysThinkingEnabled": true}' "$current_settings" > "$kimi_settings.tmp" 2>/dev/null; then atomic_mv "$kimi_settings.tmp" "$kimi_settings" else rm -f "$kimi_settings.tmp" echo " [i] jq failed, using basic template" create_kimi_template > "$kimi_settings" fi else create_kimi_template > "$kimi_settings" fi echo " Created: $kimi_settings" echo " [!] Edit this file and replace YOUR_KIMI_API_KEY_HERE with your actual Kimi API key" fi } # --- Main Installation --- # Check Node.js requirement (warn if missing, continue anyway) check_nodejs || true echo "┌─ Installing CCS" # Create directories mkdir -p "$INSTALL_DIR" "$CCS_DIR" # Install main executable if [[ "$INSTALL_METHOD" == "standalone" ]]; then # Standalone install - download ccs from GitHub if ! command -v curl &> /dev/null; then echo "[X] Error: curl is required for standalone installation" exit 1 fi BASE_URL="https://raw.githubusercontent.com/kaitranntt/ccs/main" # Download main executable if curl -fsSL "$BASE_URL/lib/ccs" -o "$CCS_DIR/ccs"; then chmod +x "$CCS_DIR/ccs" ln -sf "$CCS_DIR/ccs" "$INSTALL_DIR/ccs" echo "| [OK] Downloaded executable" else echo "|" echo "[X] Error: Failed to download ccs from GitHub" exit 1 fi # Note: Shell dependencies (error-codes.sh, progress-indicator.sh, prompt.sh) no longer needed # Bootstrap delegates all functionality to Node.js via npx # Download shell completion files mkdir -p "$CCS_DIR/completions" if curl -fsSL "$BASE_URL/scripts/completion/ccs.bash" -o "$CCS_DIR/completions/ccs.bash" 2>/dev/null; then echo "| [OK] Downloaded completion files" fi curl -fsSL "$BASE_URL/scripts/completion/ccs.zsh" -o "$CCS_DIR/completions/ccs.zsh" 2>/dev/null || true curl -fsSL "$BASE_URL/scripts/completion/ccs.fish" -o "$CCS_DIR/completions/ccs.fish" 2>/dev/null || true else # Git install - use local ccs file # Handle both running from root or from installers/ subdirectory local LIB_DIR="" if [[ -f "$SCRIPT_DIR/lib/ccs" ]]; then chmod +x "$SCRIPT_DIR/lib/ccs" ln -sf "$SCRIPT_DIR/lib/ccs" "$INSTALL_DIR/ccs" LIB_DIR="$SCRIPT_DIR/lib" elif [[ -f "$SCRIPT_DIR/../lib/ccs" ]]; then chmod +x "$SCRIPT_DIR/../lib/ccs" ln -sf "$SCRIPT_DIR/../lib/ccs" "$INSTALL_DIR/ccs" LIB_DIR="$SCRIPT_DIR/../lib" else echo "|" echo "[X] Error: lib/ccs executable not found" exit 1 fi echo "| [OK] Installed executable" # Note: Shell dependencies (error-codes.sh, progress-indicator.sh, prompt.sh) no longer needed # Bootstrap delegates all functionality to Node.js via npx # Copy shell completion files mkdir -p "$CCS_DIR/completions" local COMPLETION_DIR="" if [[ -d "$SCRIPT_DIR/scripts/completion" ]]; then COMPLETION_DIR="$SCRIPT_DIR/scripts/completion" elif [[ -d "$SCRIPT_DIR/../scripts/completion" ]]; then COMPLETION_DIR="$SCRIPT_DIR/../scripts/completion" fi if [[ -n "$COMPLETION_DIR" ]]; then cp "$COMPLETION_DIR/ccs.bash" "$CCS_DIR/completions/ccs.bash" 2>/dev/null || true cp "$COMPLETION_DIR/ccs.zsh" "$CCS_DIR/completions/ccs.zsh" 2>/dev/null || true cp "$COMPLETION_DIR/ccs.fish" "$CCS_DIR/completions/ccs.fish" 2>/dev/null || true echo "| [OK] Copied completion files" fi fi if [[ ! -L "$INSTALL_DIR/ccs" ]]; then echo "|" echo "[X] Error: Failed to create symlink at $INSTALL_DIR/ccs" echo " Check directory permissions and try again." exit 1 fi # Install uninstall script (with idempotency check) if [[ -f "$SCRIPT_DIR/uninstall.sh" ]]; then # Only copy if source and destination are different if [[ "$SCRIPT_DIR/uninstall.sh" != "$CCS_DIR/uninstall.sh" ]]; then cp "$SCRIPT_DIR/uninstall.sh" "$CCS_DIR/uninstall.sh" fi chmod +x "$CCS_DIR/uninstall.sh" ln -sf "$CCS_DIR/uninstall.sh" "$INSTALL_DIR/ccs-uninstall" echo "| [OK] Installed uninstaller" elif [[ "$INSTALL_METHOD" == "standalone" ]] && command -v curl &> /dev/null; then if curl -fsSL https://raw.githubusercontent.com/kaitranntt/ccs/main/installers/uninstall.sh -o "$CCS_DIR/uninstall.sh"; then chmod +x "$CCS_DIR/uninstall.sh" ln -sf "$CCS_DIR/uninstall.sh" "$INSTALL_DIR/ccs-uninstall" echo "| [OK] Installed uninstaller" fi fi echo "| [OK] Created directories" # Install .claude/ folder if [[ "$INSTALL_METHOD" == "git" ]]; then install_claude_folder "$SCRIPT_DIR/.." || echo "| [!] Optional .claude/ installation skipped" else install_claude_folder "" || echo "| [!] Optional .claude/ installation skipped" fi echo "└─" echo "" # --- Profile Setup --- CURRENT_PROVIDER=$(detect_current_provider) GLM_SETTINGS="$CCS_DIR/glm.settings.json" KIMI_SETTINGS="$CCS_DIR/kimi.settings.json" # Build provider label PROVIDER_LABEL="" [[ "$CURRENT_PROVIDER" == "glm" ]] && PROVIDER_LABEL=" (detected: GLM)" [[ "$CURRENT_PROVIDER" == "kimi" ]] && PROVIDER_LABEL=" (detected: Kimi)" [[ "$CURRENT_PROVIDER" == "claude" ]] && PROVIDER_LABEL=" (detected: Claude)" [[ "$CURRENT_PROVIDER" == "custom" ]] && PROVIDER_LABEL=" (detected: custom)" echo "┌─ Configuring Profiles (v${CCS_VERSION})${PROVIDER_LABEL}" # Backup existing config if present (single backup, no timestamp) BACKUP_FILE="$CCS_DIR/config.json.backup" if [[ -f "$CCS_DIR/config.json" ]]; then cp "$CCS_DIR/config.json" "$BACKUP_FILE" fi # Track if GLM needs API key NEEDS_GLM_KEY=false # Create GLM profile if missing if [[ ! -f "$GLM_SETTINGS" ]]; then create_glm_profile "$CURRENT_PROVIDER" >/dev/null 2>&1 echo "| [OK] GLM profile -> ~/.ccs/glm.settings.json" [[ "$CURRENT_PROVIDER" != "glm" ]] && NEEDS_GLM_KEY=true fi # Track if Kimi needs API key NEEDS_KIMI_KEY=false # Create Kimi profile if missing if [[ ! -f "$KIMI_SETTINGS" ]]; then create_kimi_profile "$CURRENT_PROVIDER" >/dev/null 2>&1 echo "| [OK] Kimi profile -> ~/.ccs/kimi.settings.json" [[ "$CURRENT_PROVIDER" != "kimi" ]] && NEEDS_KIMI_KEY=true fi # Create config if missing if [[ ! -f "$CCS_DIR/config.json" ]]; then cat > "$CCS_DIR/config.json.tmp" << 'EOF' { "profiles": { "glm": "~/.ccs/glm.settings.json", "kimi": "~/.ccs/kimi.settings.json", "default": "~/.claude/settings.json" } } EOF atomic_mv "$CCS_DIR/config.json.tmp" "$CCS_DIR/config.json" echo "| [OK] Config -> ~/.ccs/config.json" fi # Validate config JSON if [[ -f "$CCS_DIR/config.json" ]]; then if command -v jq &> /dev/null; then if ! jq -e . "$CCS_DIR/config.json" &>/dev/null; then echo "| [!] Warning: Invalid JSON in config.json" if [[ -f "$BACKUP_FILE" ]]; then echo "| Restore from: $BACKUP_FILE" fi fi fi fi # Validate GLM settings JSON if [[ -f "$GLM_SETTINGS" ]]; then if command -v jq &> /dev/null; then if ! jq -e . "$GLM_SETTINGS" &>/dev/null; then echo "| [!] Warning: Invalid JSON in glm.settings.json" fi fi fi echo "└─" echo "" # Detect circular symlink detect_circular_symlink() { local target="$1" local link_path="$2" # Check if target exists and is symlink if [[ ! -L "$target" ]]; then return 1 # Not circular fi # Resolve target's link local target_link=$(readlink "$target" 2>/dev/null || echo "") local shared_dir="$HOME/.ccs/shared" # Check if target points back to our shared dir if [[ "$target_link" == "$shared_dir"* ]] || [[ "$target_link" == "$link_path" ]]; then echo "[!] Circular symlink detected: $target → $target_link" return 0 # Circular fi return 1 # Not circular } # Setup shared directories as symlinks to ~/.claude/ (v3.2.0) setup_shared_symlinks() { local shared_dir="$CCS_DIR/shared" local claude_dir="$HOME/.claude" # Create ~/.claude/ if missing if [[ ! -d "$claude_dir" ]]; then echo "[i] Creating ~/.claude/ directory structure" mkdir -p "$claude_dir"/{commands,skills,agents} fi # Create shared directory mkdir -p "$shared_dir" # Create symlinks ~/.ccs/shared/* → ~/.claude/* for dir in commands skills agents; do local claude_path="$claude_dir/$dir" local shared_path="$shared_dir/$dir" # Create directory in ~/.claude/ if missing if [[ ! -d "$claude_path" ]]; then mkdir -p "$claude_path" fi # Check for circular symlink if detect_circular_symlink "$claude_path" "$shared_path"; then echo "[!] Skipping $dir: circular symlink detected" continue fi # If already correct symlink, skip if [[ -L "$shared_path" ]]; then local current_target=$(readlink "$shared_path" 2>/dev/null || echo "") if [[ "$current_target" == "$claude_path" ]]; then continue # Already correct fi rm -rf "$shared_path" elif [[ -e "$shared_path" ]]; then # Backup existing data before replacing if [[ -d "$shared_path" ]] && [[ -n "$(ls -A "$shared_path" 2>/dev/null)" ]]; then echo "[i] Migrating existing $dir to ~/.claude/$dir" # Copy to claude dir (preserve user modifications) for item in "$shared_path"/*; do [[ -e "$item" ]] || continue local basename=$(basename "$item") if [[ ! -e "$claude_path/$basename" ]]; then cp -r "$item" "$claude_path/" 2>/dev/null fi done fi rm -rf "$shared_path" fi # Create symlink ln -s "$claude_path" "$shared_path" 2>/dev/null || { echo "[!] Failed to create symlink for $dir, copying instead" mkdir -p "$shared_path" if [[ -d "$claude_path" ]]; then cp -r "$claude_path"/* "$shared_path/" 2>/dev/null || true fi } done } echo "[i] Setting up shared directories..." setup_shared_symlinks echo "" # Install CCS items to ~/.claude/ via symlinks (v4.1.0) echo "[i] Installing CCS items to ~/.claude/..." if command -v node &> /dev/null; then # Check if .claude/ was successfully installed if [[ -d "$CCS_DIR/.claude" ]]; then # Download or copy claude-symlink-manager.js mkdir -p "$CCS_DIR/bin/utils" if [[ "$INSTALL_METHOD" == "git" ]]; then # Git install - copy from local repo if [[ -f "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" ]]; then cp "$SCRIPT_DIR/../bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js" elif [[ -f "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" ]]; then cp "$SCRIPT_DIR/bin/utils/claude-symlink-manager.js" "$CCS_DIR/bin/utils/claude-symlink-manager.js" fi else # Standalone install - download from GitHub if ! curl -fsSL "https://raw.githubusercontent.com/kaitranntt/ccs/main/bin/utils/claude-symlink-manager.js" -o "$CCS_DIR/bin/utils/claude-symlink-manager.js" 2>/dev/null; then echo "[!] Failed to download claude-symlink-manager.js" fi fi # Call ClaudeSymlinkManager if available if [[ -f "$CCS_DIR/bin/utils/claude-symlink-manager.js" ]]; then node -e " try { const ClaudeSymlinkManager = require('$CCS_DIR/bin/utils/claude-symlink-manager.js'); const manager = new ClaudeSymlinkManager(); manager.install(); } catch (err) { console.log('[!] CCS item installation warning: ' + err.message); console.log(' Run \"ccs sync\" to retry'); } " 2>/dev/null || echo "[!] CCS item installation skipped (run 'ccs sync' later)" else echo "[!] claude-symlink-manager.js not found, skipping" echo " Run 'ccs sync' after installation to complete setup" fi else echo "[!] .claude/ folder not found, skipping CCS item installation" fi else echo "[!] Node.js not found, skipping CCS item installation" echo " Install Node.js and run 'ccs sync' to complete setup" fi echo "" # Auto-configure PATH if needed (all Unix platforms) configure_shell_path # Show API key warning if needed if [[ "$NEEDS_GLM_KEY" == "true" ]]; then msg_critical "Configure GLM API Key: 1. Get API key from: https://api.z.ai 2. Edit: ~/.ccs/glm.settings.json 3. Replace: YOUR_GLM_API_KEY_HERE With your actual API key 4. Test: ccs glm --version" fi # Show API key warning for Kimi if needed if [[ "$NEEDS_KIMI_KEY" == "true" ]]; then msg_critical "Configure Kimi API Key: 1. Get API key from: https://www.kimi.com/coding 2. Edit: ~/.ccs/kimi.settings.json 3. Replace: YOUR_KIMI_API_KEY_HERE With your actual API key 4. Test: ccs kimi --version" fi msg_success "CCS installed successfully!" echo "" echo " Installed components:" echo " * ccs command -> ~/.local/bin/ccs" echo " * config -> ~/.ccs/config.json" echo " * glm profile -> ~/.ccs/glm.settings.json" echo " * kimi profile -> ~/.ccs/kimi.settings.json" echo " * .claude/ folder -> ~/.ccs/.claude/" echo "" echo " Requirements:" echo " * Node.js 14+ (detected: $(node -v 2>/dev/null || echo 'NOT FOUND'))" echo " * npm 5.2+ (for npx, comes with Node.js 8.2+)" echo "" echo " First Run:" echo " The first time you run 'ccs', it will automatically install" echo " the @kaitranntt/ccs npm package globally via npx." echo "" echo " Quick start:" echo " ccs # Use Claude subscription (default)" echo " ccs glm # Use GLM fallback" echo " ccs kimi # Use Kimi for Coding" echo "" echo " To uninstall: ccs-uninstall" echo ""