Files
ccs/lib/ccs
T

474 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Version (updated by scripts/bump-version.sh)
CCS_VERSION="3.0.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
# --- Color/Format Functions ---
setup_colors() {
# Enable colors if: FORCE_COLOR set OR (TTY detected AND NO_COLOR not set) OR (TERM supports colors AND NO_COLOR not set)
if [[ -n "${FORCE_COLOR:-}" ]] || \
([[ -t 1 || -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]) || \
([[ -n "${TERM:-}" && "${TERM}" != "dumb" ]] && [[ -z "${NO_COLOR:-}" ]]); then
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED='' YELLOW='' CYAN='' BOLD='' RESET=''
fi
}
msg_error() {
echo "" >&2
echo -e "${RED}${BOLD}╔═════════════════════════════════════════════╗${RESET}" >&2
echo -e "${RED}${BOLD}║ ERROR ║${RESET}" >&2
echo -e "${RED}${BOLD}╚═════════════════════════════════════════════╝${RESET}" >&2
echo "" >&2
echo -e "${RED}$1${RESET}" >&2
echo "" >&2
}
show_help() {
echo -e "${BOLD}CCS (Claude Code Switch) - Instant profile switching for Claude CLI${RESET}"
echo ""
echo -e "${CYAN}Usage:${RESET}"
echo -e " ${YELLOW}ccs${RESET} [profile] [claude-args...]"
echo -e " ${YELLOW}ccs${RESET} [flags]"
echo ""
echo -e "${CYAN}Description:${RESET}"
echo -e " Switch between Claude models instantly. Stop hitting rate limits."
echo -e " Maps profile names to Claude settings files via ~/.ccs/config.json"
echo ""
echo -e "${CYAN}Profile Switching:${RESET}"
echo -e " ${YELLOW}ccs${RESET} Use default profile"
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM profile"
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi profile"
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Switch to GLM and run command"
echo -e " ${YELLOW}ccs kimi${RESET} \"write tests\" Switch to Kimi and run command"
echo -e " ${YELLOW}ccs glm${RESET} --verbose Switch to GLM with Claude flags"
echo -e " ${YELLOW}ccs kimi${RESET} --verbose Switch to Kimi with Claude flags"
echo ""
echo -e "${CYAN}Flags:${RESET}"
echo -e " ${YELLOW}-h, --help${RESET} Show this help message"
echo -e " ${YELLOW}-v, --version${RESET} Show version and installation info"
echo ""
echo -e "${CYAN}Configuration:${RESET}"
echo -e " Config File: ~/.ccs/config.json"
echo -e " Settings: ~/.ccs/*.settings.json"
echo -e " Environment: CCS_CONFIG (override config path)"
echo ""
echo -e "${CYAN}Examples:${RESET}"
echo -e " # Use default Claude subscription"
echo -e " ${YELLOW}ccs${RESET} \"Review this architecture\""
echo ""
echo -e " # Switch to GLM for cost-effective tasks"
echo -e " ${YELLOW}ccs glm${RESET} \"Write unit tests\""
echo ""
echo -e " # Switch to Kimi for alternative option"
echo -e " ${YELLOW}ccs kimi${RESET} \"Write integration tests\""
echo ""
echo -e " # Use with verbose output"
echo -e " ${YELLOW}ccs glm${RESET} --verbose \"Debug error\""
echo -e " ${YELLOW}ccs kimi${RESET} --verbose \"Review code\""
echo ""
echo -e "${YELLOW}Uninstall:${RESET}"
echo -e " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash"
echo -e " Windows: irm ccs.kaitran.ca/uninstall | iex"
echo -e " npm: npm uninstall -g @kaitranntt/ccs"
echo ""
echo -e "${CYAN}Documentation:${RESET}"
echo -e " GitHub: ${CYAN}https://github.com/kaitranntt/ccs${RESET}"
echo -e " Docs: https://github.com/kaitranntt/ccs/blob/main/README.md"
echo -e " Issues: https://github.com/kaitranntt/ccs/issues"
echo ""
echo -e "${CYAN}License:${RESET} MIT"
}
setup_colors
# Check dependencies early
command -v jq &>/dev/null || {
msg_error "jq required but not installed. Install: brew install jq (macOS) or apt install jq (Ubuntu)"
exit 1
}
# --- Claude CLI Detection Logic ---
detect_claude_cli() {
echo "${CCS_CLAUDE_PATH:-claude}"
}
show_claude_not_found_error() {
msg_error "Claude CLI not found in PATH
CCS requires Claude CLI to be installed and available in your PATH.
Solutions:
1. Install Claude CLI:
https://docs.claude.com/en/docs/claude-code/installation
2. Verify installation:
command -v claude
3. If installed but not in PATH, add it:
# Find Claude installation
which claude
# Or set custom path
export CCS_CLAUDE_PATH='/path/to/claude'
Restart your terminal after installation."
}
# WIP: .claude/ integration testing incomplete
# Feature disabled until testing complete
# install_commands_and_skills() {
: <<'COMMENTED_OUT'
# Try both possible locations for .claude directory
local source_dir=""
local possible_dirs=(
"$SCRIPT_DIR/.claude" # Development: tools/ccs/.claude
"$HOME/.ccs/.claude" # Installed: ~/.ccs/.claude
)
for dir in "${possible_dirs[@]}"; do
if [[ -d "$dir" ]]; then
source_dir="$dir"
break
fi
done
local target_dir="$HOME/.claude"
echo "┌─ Installing CCS Commands & Skills"
echo "│ Source: $source_dir"
echo "│ Target: $target_dir"
echo "│"
# Check if source directory exists
if [[ ! -d "$source_dir" ]]; then
echo "|"
msg_error "Source directory not found.
Checked locations:
- $SCRIPT_DIR/.claude (development)
- $HOME/.ccs/.claude (installed)
Solution:
1. If developing: Ensure you're in the CCS repository
2. If installed: Reinstall CCS with: curl -fsSL ccs.kaitran.ca/install | bash"
return 1
fi
# Create target directories if they don't exist
mkdir -p "$target_dir/commands"
mkdir -p "$target_dir/skills"
local installed_count=0
local skipped_count=0
# Install commands
if [[ -d "$source_dir/commands" ]]; then
echo "│ Installing commands..."
for cmd_file in "$source_dir/commands"/*.md; do
if [[ -f "$cmd_file" ]]; then
local cmd_name=$(basename "$cmd_file" .md)
local target_file="$target_dir/commands/$cmd_name.md"
if [[ -f "$target_file" ]]; then
echo "| | [i] Skipping existing command: $cmd_name.md"
skipped_count=$((skipped_count + 1))
else
if cp "$cmd_file" "$target_file"; then
echo "| | [OK] Installed command: $cmd_name.md"
installed_count=$((installed_count + 1))
else
echo "| | [X] Failed to install command: $cmd_name.md"
fi
fi
fi
done
else
echo "| [i] No commands directory found"
fi
echo "|"
# Install skills
if [[ -d "$source_dir/skills" ]]; then
echo "| Installing skills..."
for skill_dir in "$source_dir/skills"/*; do
if [[ -d "$skill_dir" ]]; then
local skill_name=$(basename "$skill_dir")
local target_skill_dir="$target_dir/skills/$skill_name"
if [[ -d "$target_skill_dir" ]]; then
echo "| | [i] Skipping existing skill: $skill_name"
skipped_count=$((skipped_count + 1))
else
if cp -r "$skill_dir" "$target_skill_dir"; then
echo "| | [OK] Installed skill: $skill_name"
installed_count=$((installed_count + 1))
else
echo "| | [X] Failed to install skill: $skill_name"
fi
fi
fi
done
else
echo "| [i] No skills directory found"
fi
echo "└─"
echo ""
echo "[OK] Installation complete!"
echo " Installed: $installed_count items"
echo " Skipped: $skipped_count items (already exist)"
echo ""
echo "You can now use the /ccs command in Claude CLI for task delegation."
echo "Example: /ccs glm /plan 'add user authentication'"
COMMENTED_OUT
# }
# WIP: .claude/ integration testing incomplete
# Feature disabled until testing complete
# uninstall_commands_and_skills() {
: <<'COMMENTED_OUT'
local target_dir="$HOME/.claude"
local removed_count=0
local not_found_count=0
echo "┌─ Uninstalling CCS Commands & Skills"
echo "│ Target: $target_dir"
echo "│"
# Check if target directory exists
if [[ ! -d "$target_dir" ]]; then
echo "|"
echo "│ [i] Claude directory not found: $target_dir"
echo "│ Nothing to uninstall."
echo "└─"
echo ""
echo "[OK] Uninstall complete!"
echo " Removed: 0 items (nothing was installed)"
return 0
fi
# Remove commands
local commands_dir="$target_dir/commands"
if [[ -d "$commands_dir" ]]; then
echo "│ Removing commands..."
for cmd_file in "$commands_dir"/ccs.md; do
if [[ -f "$cmd_file" ]]; then
local cmd_name=$(basename "$cmd_file" .md)
if rm "$cmd_file"; then
echo "| | [OK] Removed command: $cmd_name.md"
removed_count=$((removed_count + 1))
else
echo "| | [X] Failed to remove command: $cmd_name.md"
fi
else
echo "| | [i] CCS command not found"
not_found_count=$((not_found_count + 1))
fi
done
else
echo "│ [i] Commands directory not found"
not_found_count=$((not_found_count + 1))
fi
echo "|"
# Remove skills
local skills_dir="$target_dir/skills"
if [[ -d "$skills_dir" ]]; then
echo "| Removing skills..."
for skill_dir in "$skills_dir"/ccs-delegation; do
if [[ -d "$skill_dir" ]]; then
local skill_name=$(basename "$skill_dir")
if rm -rf "$skill_dir"; then
echo "| | [OK] Removed skill: $skill_name"
removed_count=$((removed_count + 1))
else
echo "| | [X] Failed to remove skill: $skill_name"
fi
else
echo "| | [i] CCS skill not found"
not_found_count=$((not_found_count + 1))
fi
done
else
echo "│ [i] Skills directory not found"
not_found_count=$((not_found_count + 1))
fi
echo "└─"
echo ""
echo "[OK] Uninstall complete!"
echo " Removed: $removed_count items"
echo " Not found: $not_found_count items (already removed)"
echo ""
echo "The /ccs command is no longer available in Claude CLI."
echo "To reinstall: ccs --install"
COMMENTED_OUT
# }
show_version() {
echo -e "${BOLD}CCS (Claude Code Switch) v${CCS_VERSION}${RESET}"
echo ""
echo -e "${CYAN}Installation:${RESET}"
# Simple location - just show what 'command -v' returns
local location=$(command -v ccs 2>/dev/null || echo "(not installed)")
echo -e " ${CYAN}Location:${RESET} ${location}"
# Simple config display
local config="${CCS_CONFIG:-$HOME/.ccs/config.json}"
echo -e " ${CYAN}Config:${RESET} ${config}"
echo ""
echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
echo -e "${CYAN}License:${RESET} MIT"
echo ""
echo -e "${YELLOW}Run 'ccs --help' for usage information${RESET}"
}
# Special case: version command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "version" || "${1}" == "--version" || "${1}" == "-v" ]]; then
show_version
exit 0
fi
# Special case: help command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "--help" || "${1}" == "-h" || "${1}" == "help" ]]; then
setup_colors
show_help
exit 0
fi
# Special case: install command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "--install" ]]; then
echo ""
echo "Feature not available"
echo ""
echo "The --install flag is currently under development."
echo ".claude/ integration testing is not complete."
echo ""
echo "For updates: https://github.com/kaitranntt/ccs/issues"
echo ""
exit 0
fi
# Special case: uninstall command (check BEFORE profile detection)
if [[ $# -gt 0 ]] && [[ "${1}" == "--uninstall" ]]; then
echo ""
echo "Feature not available"
echo ""
echo "The --uninstall flag is currently under development."
echo ".claude/ integration testing is not complete."
echo ""
echo "For updates: https://github.com/kaitranntt/ccs/issues"
echo ""
exit 0
fi
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
# No args or first arg is a flag → use default profile
PROFILE="default"
else
# First arg doesn't start with '-' → treat as profile name
PROFILE="${1}"
fi
# Check config exists
if [[ ! -f "$CONFIG_FILE" ]]; then
msg_error "Config file not found: $CONFIG_FILE
Solutions:
1. Reinstall CCS:
curl -fsSL ccs.kaitran.ca/install | bash
2. Or create config manually:
mkdir -p ~/.ccs
cat > ~/.ccs/config.json << 'EOF'
{
\"profiles\": {
\"glm\": \"~/.ccs/glm.settings.json\",
\"kimi\": \"~/.ccs/kimi.settings.json\",
\"default\": \"~/.claude/settings.json\"
}
}
EOF"
exit 1
fi
# Validate profile name (alphanumeric, dash, underscore only)
if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then
msg_error "Invalid profile name: $PROFILE
Use only alphanumeric characters, dash, or underscore."
exit 1
fi
# Single check gets profile path, validates JSON + structure in one step
SETTINGS_PATH=$(jq -r --arg profile "$PROFILE" '.profiles[$profile] // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -z "$SETTINGS_PATH" ]]; then
# Could be: invalid JSON, no profiles object, or profile not found
# Show helpful error based on what we can detect
if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
msg_error "Invalid JSON in $CONFIG_FILE
Fix the JSON syntax or reinstall:
curl -fsSL ccs.kaitran.ca/install | bash"
elif ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then
msg_error "Config must have 'profiles' object
See config/config.example.json for correct format
Or reinstall:
curl -fsSL ccs.kaitran.ca/install | bash"
else
AVAILABLE_PROFILES=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /')
msg_error "Profile '$PROFILE' not found in $CONFIG_FILE
Available profiles:
$AVAILABLE_PROFILES"
fi
exit 1
fi
# Expand ~ in path
SETTINGS_PATH="${SETTINGS_PATH/#\~/$HOME}"
# Validate settings file exists
if [[ ! -f "$SETTINGS_PATH" ]]; then
msg_error "Settings file not found: $SETTINGS_PATH
Solutions:
1. Create the settings file for profile '$PROFILE'
2. Update the path in $CONFIG_FILE
3. Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
exit 1
fi
# Shift profile arg only if first arg was NOT a flag
if [[ $# -gt 0 ]] && [[ ! "${1}" =~ ^- ]]; then
shift
fi
# Detect Claude CLI executable
CLAUDE_CLI=$(detect_claude_cli)
# Execute Claude with the profile settings
# If claude is not found, exec will fail and show an error
if ! exec "$CLAUDE_CLI" --settings "$SETTINGS_PATH" "$@"; then
show_claude_not_found_error
exit 1
fi