Files
ccs/installers/install.sh
T
semantic-release-botandkaitranntt bd46c8de12 fix(tests): migrate test suite from mocha to bun test runner
- Replace before()/after() with beforeAll()/afterAll()
- Remove this.timeout() calls (unsupported by bun)
- Update package.json scripts to use bun test
- Fix error message regex for cross-runtime compatibility
- Skip integration tests requiring network/child process mocking
- Format source files with prettier
2025-12-03 21:43:29 -05:00

928 lines
28 KiB
Bash
Executable File

#!/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.4.1-dev.1"
# 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 ""