mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 04:19:49 +00:00
1919 lines
55 KiB
Bash
Executable File
1919 lines
55 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Version (updated by scripts/bump-version.sh)
|
|
CCS_VERSION="4.5.0"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
|
readonly INSTANCES_DIR="$HOME/.ccs/instances"
|
|
|
|
# Determine dependency location (git vs installed)
|
|
# Git: lib/ccs and dependencies are in same dir (lib/)
|
|
# Installed: lib/ccs is symlinked from ~/.local/bin/ccs, dependencies in ~/.ccs/lib/
|
|
if [[ -f "$SCRIPT_DIR/error-codes.sh" ]]; then
|
|
# Git install - files in same directory
|
|
DEP_DIR="$SCRIPT_DIR"
|
|
else
|
|
# Standalone install - files in ~/.ccs/lib/
|
|
DEP_DIR="$HOME/.ccs/lib"
|
|
fi
|
|
|
|
# Source error codes
|
|
source "$DEP_DIR/error-codes.sh"
|
|
|
|
# Source progress indicators
|
|
source "$DEP_DIR/progress-indicator.sh"
|
|
|
|
# Source interactive prompts
|
|
source "$DEP_DIR/prompt.sh"
|
|
|
|
# --- 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'
|
|
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_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
|
|
}
|
|
|
|
# Enhanced error message with error codes
|
|
show_enhanced_error() {
|
|
local error_code="$1"
|
|
local short_msg="$2"
|
|
local context="${3:-}"
|
|
local suggestions="${4:-}"
|
|
|
|
echo "" >&2
|
|
echo -e "${RED}[X] $short_msg${RESET}" >&2
|
|
echo "" >&2
|
|
|
|
[[ -n "$context" ]] && {
|
|
echo -e "$context" >&2
|
|
echo "" >&2
|
|
}
|
|
|
|
[[ -n "$suggestions" ]] && {
|
|
echo -e "${YELLOW}Solutions:${RESET}" >&2
|
|
echo -e "$suggestions" >&2
|
|
echo "" >&2
|
|
}
|
|
|
|
echo -e "${YELLOW}Error: $error_code${RESET}" >&2
|
|
echo -e "${YELLOW}$(get_error_doc_url "$error_code")${RESET}" >&2
|
|
echo "" >&2
|
|
}
|
|
|
|
# Calculate Levenshtein distance between two strings
|
|
levenshtein_distance() {
|
|
local a="$1"
|
|
local b="$2"
|
|
local len_a=${#a}
|
|
local len_b=${#b}
|
|
|
|
# Return early for empty strings
|
|
[[ $len_a -eq 0 ]] && { echo "$len_b"; return; }
|
|
[[ $len_b -eq 0 ]] && { echo "$len_a"; return; }
|
|
|
|
# Initialize matrix using associative array
|
|
declare -A matrix
|
|
|
|
# Initialize first row and column
|
|
for ((j=0; j<=len_a; j++)); do
|
|
matrix[0,$j]=$j
|
|
done
|
|
for ((i=0; i<=len_b; i++)); do
|
|
matrix[$i,0]=$i
|
|
done
|
|
|
|
# Fill matrix
|
|
for ((i=1; i<=len_b; i++)); do
|
|
for ((j=1; j<=len_a; j++)); do
|
|
if [[ "${a:j-1:1}" == "${b:i-1:1}" ]]; then
|
|
matrix[$i,$j]=${matrix[$((i-1)),$((j-1))]}
|
|
else
|
|
local sub=${matrix[$((i-1)),$((j-1))]}
|
|
local ins=${matrix[$i,$((j-1))]}
|
|
local del=${matrix[$((i-1)),$j]}
|
|
local min=$sub
|
|
[[ $ins -lt $min ]] && min=$ins
|
|
[[ $del -lt $min ]] && min=$del
|
|
matrix[$i,$j]=$((min + 1))
|
|
fi
|
|
done
|
|
done
|
|
|
|
echo "${matrix[$len_b,$len_a]}"
|
|
}
|
|
|
|
# Find similar strings using fuzzy matching
|
|
find_similar_strings() {
|
|
local target="$1"
|
|
shift
|
|
local candidates=("$@")
|
|
local max_distance=2
|
|
local target_lower="${target,,}"
|
|
|
|
declare -A distances
|
|
local matches=()
|
|
|
|
# Calculate distances
|
|
for candidate in "${candidates[@]}"; do
|
|
local candidate_lower="${candidate,,}"
|
|
local dist=$(levenshtein_distance "$target_lower" "$candidate_lower")
|
|
if [[ $dist -le $max_distance && $dist -gt 0 ]]; then
|
|
distances["$candidate"]=$dist
|
|
matches+=("$candidate")
|
|
fi
|
|
done
|
|
|
|
# Sort by distance (simple bubble sort for small arrays)
|
|
for ((i=0; i<${#matches[@]}; i++)); do
|
|
for ((j=i+1; j<${#matches[@]}; j++)); do
|
|
if [[ ${distances[${matches[i]}]} -gt ${distances[${matches[j]}]} ]]; then
|
|
local temp="${matches[i]}"
|
|
matches[i]="${matches[j]}"
|
|
matches[j]="$temp"
|
|
fi
|
|
done
|
|
done
|
|
|
|
# Return first 3 matches
|
|
for ((i=0; i<${#matches[@]} && i<3; i++)); do
|
|
echo "${matches[i]}"
|
|
done
|
|
}
|
|
|
|
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 multiple Claude accounts and alternative models"
|
|
echo -e " (GLM, Kimi) instantly. Run different Claude CLI sessions concurrently"
|
|
echo -e " with auto-recovery. Zero downtime."
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Model Switching:${RESET}"
|
|
echo -e " ${YELLOW}ccs${RESET} Use default Claude account"
|
|
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM 4.6 model"
|
|
echo -e " ${YELLOW}ccs glmt${RESET} Switch to GLM with thinking mode"
|
|
echo -e " ${YELLOW}ccs glmt --verbose${RESET} Enable debug logging"
|
|
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi for Coding"
|
|
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Use GLM and run command"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Account Management:${RESET}"
|
|
echo -e " ${YELLOW}ccs auth --help${RESET} Run multiple Claude accounts concurrently"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Delegation (inside Claude Code CLI):${RESET}"
|
|
echo -e " ${YELLOW}/ccs \"task\"${RESET} Delegate task (auto-selects best profile)"
|
|
echo -e " ${YELLOW}/ccs --glm \"task\"${RESET} Force GLM-4.6 for simple tasks"
|
|
echo -e " ${YELLOW}/ccs --kimi \"task\"${RESET} Force Kimi for long context"
|
|
echo -e " ${YELLOW}/ccs:continue \"follow-up\"${RESET} Continue last delegation session"
|
|
echo -e " Save tokens by delegating simple tasks to cost-optimized models"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Diagnostics:${RESET}"
|
|
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
|
|
echo -e " ${YELLOW}ccs sync${RESET} Sync delegation commands and skills"
|
|
echo -e " ${YELLOW}ccs update${RESET} Update CCS to latest version"
|
|
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 -e " ${YELLOW}-sc, --shell-completion${RESET} Install shell auto-completion"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Configuration:${RESET}"
|
|
echo -e " Config File: ~/.ccs/config.json"
|
|
echo -e " Profiles: ~/.ccs/profiles.json"
|
|
echo -e " Instances: ~/.ccs/instances/"
|
|
echo -e " Settings: ~/.ccs/*.settings.json"
|
|
echo -e " Environment: CCS_CONFIG (override config path)"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Shared Data:${RESET}"
|
|
echo -e " Commands: ~/.ccs/shared/commands/"
|
|
echo -e " Skills: ~/.ccs/shared/skills/"
|
|
echo -e " Agents: ~/.ccs/shared/agents/"
|
|
echo -e " Plugins: ~/.ccs/shared/plugins/"
|
|
echo -e " Note: Commands, skills, agents, and plugins are symlinked across all profiles"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}Examples:${RESET}"
|
|
echo -e " ${YELLOW}\$ ccs${RESET} # Use default account"
|
|
echo -e " ${YELLOW}\$ ccs glm \"implement API\"${RESET} # Cost-optimized model"
|
|
echo ""
|
|
echo -e " For more: ${CYAN}https://github.com/kaitranntt/ccs/blob/main/README.md${RESET}"
|
|
echo ""
|
|
|
|
echo -e "${YELLOW}Uninstall:${RESET}"
|
|
echo -e " npm: npm uninstall -g @kaitranntt/ccs"
|
|
echo -e " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash"
|
|
echo -e " Windows: irm ccs.kaitran.ca/uninstall | iex"
|
|
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
|
|
}
|
|
|
|
# --- Auto-Recovery Functions ---
|
|
|
|
ensure_ccs_directory() {
|
|
[[ -d "$HOME/.ccs" ]] && return 0
|
|
|
|
mkdir -p "$HOME/.ccs" 2>/dev/null || {
|
|
msg_error "Cannot create ~/.ccs/ directory. Check permissions."
|
|
return 1
|
|
}
|
|
|
|
echo "[i] Auto-recovery: Created ~/.ccs/ directory"
|
|
return 0
|
|
}
|
|
|
|
ensure_config_json() {
|
|
local config_file="$HOME/.ccs/config.json"
|
|
|
|
# Check if exists and valid
|
|
if [[ -f "$config_file" ]]; then
|
|
jq empty "$config_file" 2>/dev/null && return 0
|
|
|
|
# Corrupted - backup and recreate
|
|
local backup_file="${config_file}.backup.$(date +%s)"
|
|
mv "$config_file" "$backup_file" 2>/dev/null
|
|
echo "[i] Auto-recovery: Backed up corrupted config.json"
|
|
fi
|
|
|
|
# Create default config
|
|
cat > "$config_file" <<'EOF'
|
|
{
|
|
"profiles": {
|
|
"glm": "~/.ccs/glm.settings.json",
|
|
"kimi": "~/.ccs/kimi.settings.json",
|
|
"default": "~/.claude/settings.json"
|
|
}
|
|
}
|
|
EOF
|
|
|
|
echo "[i] Auto-recovery: Created ~/.ccs/config.json"
|
|
return 0
|
|
}
|
|
|
|
ensure_claude_settings() {
|
|
local claude_dir="$HOME/.claude"
|
|
local settings_file="$claude_dir/settings.json"
|
|
|
|
# Create ~/.claude/ if missing
|
|
if [[ ! -d "$claude_dir" ]]; then
|
|
mkdir -p "$claude_dir" 2>/dev/null || return 1
|
|
echo "[i] Auto-recovery: Created ~/.claude/ directory"
|
|
fi
|
|
|
|
# Create settings.json if missing
|
|
if [[ ! -f "$settings_file" ]]; then
|
|
echo '{}' > "$settings_file" 2>/dev/null || return 1
|
|
echo "[i] Auto-recovery: Created ~/.claude/settings.json"
|
|
echo "[i] Next step: Run 'claude /login' to authenticate"
|
|
return 0
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Run auto-recovery
|
|
auto_recover() {
|
|
ensure_ccs_directory || return 1
|
|
ensure_config_json || return 1
|
|
ensure_claude_settings || return 1
|
|
return 0
|
|
}
|
|
|
|
# --- Doctor Command ---
|
|
|
|
doctor_check() {
|
|
local check_name="$1"
|
|
local status="$2" # success, warning, error
|
|
local message="${3:-}"
|
|
|
|
case "$status" in
|
|
success)
|
|
echo -e "${GREEN}[OK]${RESET} $check_name"
|
|
;;
|
|
warning)
|
|
echo -e "${YELLOW}[!]${RESET} $check_name${message:+: $message}"
|
|
;;
|
|
error)
|
|
echo -e "${RED}[X]${RESET} $check_name: $message"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
doctor_run() {
|
|
echo -e "${CYAN}Running CCS Health Check...${RESET}"
|
|
echo ""
|
|
|
|
local has_errors=false
|
|
local total_checks=9
|
|
local current_check=0
|
|
|
|
# Check Claude CLI
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking Claude CLI"
|
|
if command -v "$(detect_claude_cli)" &>/dev/null; then
|
|
clear_progress
|
|
doctor_check "Claude CLI" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "Claude CLI" "error" "Not found in PATH"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Check ~/.ccs/
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking CCS directory"
|
|
if [[ -d "$HOME/.ccs" ]]; then
|
|
clear_progress
|
|
doctor_check "CCS Directory" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "CCS Directory" "error" "~/.ccs/ not found"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Check config.json
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking config.json"
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
if jq empty "$CONFIG_FILE" 2>/dev/null; then
|
|
clear_progress
|
|
doctor_check "config.json" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "config.json" "error" "Invalid JSON"
|
|
has_errors=true
|
|
fi
|
|
else
|
|
clear_progress
|
|
doctor_check "config.json" "error" "Not found"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Check glm.settings.json
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking glm.settings.json"
|
|
local glm_file="$HOME/.ccs/glm.settings.json"
|
|
if [[ -f "$glm_file" ]]; then
|
|
if jq empty "$glm_file" 2>/dev/null; then
|
|
clear_progress
|
|
doctor_check "glm.settings.json" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "glm.settings.json" "error" "Invalid JSON"
|
|
has_errors=true
|
|
fi
|
|
else
|
|
clear_progress
|
|
doctor_check "glm.settings.json" "error" "Not found"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Check kimi.settings.json
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking kimi.settings.json"
|
|
local kimi_file="$HOME/.ccs/kimi.settings.json"
|
|
if [[ -f "$kimi_file" ]]; then
|
|
if jq empty "$kimi_file" 2>/dev/null; then
|
|
clear_progress
|
|
doctor_check "kimi.settings.json" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "kimi.settings.json" "error" "Invalid JSON"
|
|
has_errors=true
|
|
fi
|
|
else
|
|
clear_progress
|
|
doctor_check "kimi.settings.json" "error" "Not found"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Check ~/.claude/settings.json
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking Claude settings"
|
|
if [[ -f "$HOME/.claude/settings.json" ]]; then
|
|
if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then
|
|
clear_progress
|
|
doctor_check "Claude Settings" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "Claude Settings" "warning" "Invalid JSON"
|
|
fi
|
|
else
|
|
clear_progress
|
|
doctor_check "Claude Settings" "warning" "Not found - run 'claude /login'"
|
|
fi
|
|
|
|
# Check profiles
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking profiles"
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
local profile_count=$(jq -r '.profiles | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
|
|
clear_progress
|
|
doctor_check "Profiles" "success" "($profile_count configured)"
|
|
fi
|
|
|
|
# Check instances
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking instances"
|
|
if [[ -d "$INSTANCES_DIR" ]]; then
|
|
local instance_count=$(find "$INSTANCES_DIR" -maxdepth 1 -type d 2>/dev/null | wc -l)
|
|
instance_count=$((instance_count - 1)) # Exclude parent dir
|
|
clear_progress
|
|
doctor_check "Instances" "success" "($instance_count account profiles)"
|
|
else
|
|
clear_progress
|
|
doctor_check "Instances" "success" "(no account profiles)"
|
|
fi
|
|
|
|
# Check permissions
|
|
current_check=$((current_check + 1))
|
|
show_progress_step $current_check $total_checks "Checking permissions"
|
|
local test_file="$HOME/.ccs/.permission-test"
|
|
if echo "test" > "$test_file" 2>/dev/null; then
|
|
rm -f "$test_file" 2>/dev/null
|
|
clear_progress
|
|
doctor_check "Permissions" "success"
|
|
else
|
|
clear_progress
|
|
doctor_check "Permissions" "error" "Cannot write to ~/.ccs/"
|
|
has_errors=true
|
|
fi
|
|
|
|
# Summary
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════${RESET}"
|
|
if $has_errors; then
|
|
echo -e "${RED}Status: Installation has errors${RESET}"
|
|
echo "Run: npm install -g @kaitranntt/ccs --force"
|
|
else
|
|
echo -e "${GREEN}✓ All checks passed!${RESET}"
|
|
fi
|
|
echo ""
|
|
|
|
$has_errors && exit 1 || exit 0
|
|
}
|
|
|
|
# --- Sync Command ---
|
|
|
|
sync_run() {
|
|
local ccs_claude_dir="$HOME/.ccs/.claude"
|
|
local user_claude_dir="$HOME/.claude"
|
|
|
|
echo -e "${CYAN}Syncing delegation commands and skills to ~/.claude/...${RESET}"
|
|
echo ""
|
|
|
|
# Check if source directory exists
|
|
if [[ ! -d "$ccs_claude_dir" ]]; then
|
|
msg_error "CCS .claude/ directory not found at $ccs_claude_dir"
|
|
echo "Reinstall CCS: npm install -g @kaitranntt/ccs --force"
|
|
exit 1
|
|
fi
|
|
|
|
# Create ~/.claude/ if missing
|
|
if [[ ! -d "$user_claude_dir" ]]; then
|
|
echo -e "${CYAN}[i]${RESET} Creating ~/.claude/ directory"
|
|
mkdir -p "$user_claude_dir"
|
|
chmod 700 "$user_claude_dir"
|
|
fi
|
|
|
|
# Items to symlink (source:target:type)
|
|
local items=(
|
|
"commands/ccs:commands/ccs:dir"
|
|
"skills/ccs-delegation:skills/ccs-delegation:dir"
|
|
"agents/ccs-delegator.md:agents/ccs-delegator.md:file"
|
|
)
|
|
|
|
local installed=0
|
|
local skipped=0
|
|
|
|
for item in "${items[@]}"; do
|
|
IFS=':' read -r source target type <<< "$item"
|
|
local source_path="$ccs_claude_dir/$source"
|
|
local target_path="$user_claude_dir/$target"
|
|
local target_dir="$(dirname "$target_path")"
|
|
|
|
# Check source exists
|
|
if [[ ! -e "$source_path" ]]; then
|
|
echo -e "${YELLOW}[!]${RESET} Source not found: $source, skipping"
|
|
continue
|
|
fi
|
|
|
|
# Create parent directory if needed
|
|
if [[ ! -d "$target_dir" ]]; then
|
|
mkdir -p "$target_dir"
|
|
chmod 700 "$target_dir"
|
|
fi
|
|
|
|
# Check if already correct symlink
|
|
if [[ -L "$target_path" ]]; then
|
|
local link_target="$(readlink "$target_path")"
|
|
local resolved_target="$(cd "$(dirname "$target_path")" && cd "$(dirname "$link_target")" && pwd)/$(basename "$link_target")"
|
|
|
|
if [[ "$resolved_target" == "$source_path" ]]; then
|
|
((skipped++))
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# Backup existing file/directory
|
|
if [[ -e "$target_path" ]]; then
|
|
local timestamp="$(date +%Y-%m-%d)"
|
|
local backup_path="${target_path}.backup-${timestamp}"
|
|
local counter=1
|
|
|
|
while [[ -e "$backup_path" ]]; do
|
|
backup_path="${target_path}.backup-${timestamp}-${counter}"
|
|
((counter++))
|
|
done
|
|
|
|
mv "$target_path" "$backup_path"
|
|
echo -e "${CYAN}[i]${RESET} Backed up existing to $(basename "$backup_path")"
|
|
fi
|
|
|
|
# Create symlink
|
|
ln -s "$source_path" "$target_path" 2>/dev/null
|
|
if [[ $? -eq 0 ]]; then
|
|
echo -e "${GREEN}[OK]${RESET} Installed $target"
|
|
((installed++))
|
|
else
|
|
echo -e "${RED}[X]${RESET} Failed to install $target"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${GREEN}✓ Update complete${RESET}"
|
|
echo " Installed: $installed"
|
|
echo " Already up-to-date: $skipped"
|
|
echo ""
|
|
}
|
|
|
|
# --- Update Command ---
|
|
|
|
update_run() {
|
|
echo ""
|
|
echo -e "${CYAN}Checking for updates...${RESET}"
|
|
echo ""
|
|
|
|
# Detect installation method
|
|
local install_method="direct"
|
|
if command -v npm &>/dev/null && npm list -g @kaitranntt/ccs &>/dev/null 2>&1; then
|
|
install_method="npm"
|
|
fi
|
|
|
|
# Fetch latest version from appropriate source
|
|
local latest_version=""
|
|
if command -v curl &>/dev/null; then
|
|
if [[ "$install_method" == "npm" ]]; then
|
|
# Check npm registry for npm installations
|
|
latest_version=$(curl -fsSL https://registry.npmjs.org/@kaitranntt/ccs/latest 2>/dev/null | \
|
|
grep '"version"' | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9.]+)".*/\1/')
|
|
else
|
|
# Check GitHub releases for direct installations
|
|
latest_version=$(curl -fsSL https://api.github.com/repos/kaitranntt/ccs/releases/latest 2>/dev/null | \
|
|
grep '"tag_name"' | sed -E 's/.*"v?([0-9.]+)".*/\1/')
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$latest_version" ]]; then
|
|
echo -e "${YELLOW}[!] Unable to check for updates${RESET}"
|
|
echo ""
|
|
echo "Try manually:"
|
|
if [[ "$install_method" == "npm" ]]; then
|
|
echo -e " ${YELLOW}npm install -g @kaitranntt/ccs@latest${RESET}"
|
|
else
|
|
echo -e " ${YELLOW}curl -fsSL ccs.kaitran.ca/install | bash${RESET}"
|
|
fi
|
|
echo ""
|
|
exit 1
|
|
fi
|
|
|
|
# Compare versions
|
|
if [[ "$latest_version" == "$CCS_VERSION" ]]; then
|
|
echo -e "${GREEN}[OK] You are already on the latest version (${CCS_VERSION})${RESET}"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
# Check if update available
|
|
local current_major=$(echo "$CCS_VERSION" | cut -d. -f1)
|
|
local current_minor=$(echo "$CCS_VERSION" | cut -d. -f2)
|
|
local current_patch=$(echo "$CCS_VERSION" | cut -d. -f3)
|
|
|
|
local latest_major=$(echo "$latest_version" | cut -d. -f1)
|
|
local latest_minor=$(echo "$latest_version" | cut -d. -f2)
|
|
local latest_patch=$(echo "$latest_version" | cut -d. -f3)
|
|
|
|
local is_newer=0
|
|
if [[ $latest_major -gt $current_major ]]; then
|
|
is_newer=1
|
|
elif [[ $latest_major -eq $current_major ]] && [[ $latest_minor -gt $current_minor ]]; then
|
|
is_newer=1
|
|
elif [[ $latest_major -eq $current_major ]] && [[ $latest_minor -eq $current_minor ]] && [[ $latest_patch -gt $current_patch ]]; then
|
|
is_newer=1
|
|
fi
|
|
|
|
if [[ $is_newer -eq 0 ]]; then
|
|
echo -e "${GREEN}[OK] You are on version ${CCS_VERSION} (latest is ${latest_version})${RESET}"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
echo -e "${YELLOW}[i] Update available: ${CCS_VERSION} → ${latest_version}${RESET}"
|
|
echo ""
|
|
|
|
# Perform update based on installation method
|
|
if [[ "$install_method" == "npm" ]]; then
|
|
echo -e "${CYAN}Updating via npm...${RESET}"
|
|
echo ""
|
|
|
|
# Clear npm cache to ensure fresh download
|
|
echo -e "${CYAN}Clearing package cache...${RESET}"
|
|
if ! npm cache clean --force 2>/dev/null; then
|
|
echo -e "${YELLOW}[!] Cache clearing failed, proceeding anyway...${RESET}"
|
|
fi
|
|
echo ""
|
|
|
|
if npm install -g @kaitranntt/ccs@latest; then
|
|
echo ""
|
|
echo -e "${GREEN}[OK] Update successful!${RESET}"
|
|
echo ""
|
|
echo -e "Run ${YELLOW}ccs --version${RESET} to verify"
|
|
echo ""
|
|
exit 0
|
|
else
|
|
echo ""
|
|
echo -e "${RED}[X] Update failed${RESET}"
|
|
echo ""
|
|
echo "Try manually:"
|
|
echo -e " ${YELLOW}npm cache clean --force && npm install -g @kaitranntt/ccs@latest${RESET}"
|
|
echo ""
|
|
exit 1
|
|
fi
|
|
else
|
|
echo -e "${CYAN}Updating via installer...${RESET}"
|
|
echo ""
|
|
|
|
if curl -fsSL ccs.kaitran.ca/install | bash; then
|
|
echo ""
|
|
echo -e "${GREEN}[OK] Update successful!${RESET}"
|
|
echo ""
|
|
echo -e "Run ${YELLOW}ccs --version${RESET} to verify"
|
|
echo ""
|
|
exit 0
|
|
else
|
|
echo ""
|
|
echo -e "${RED}[X] Update failed${RESET}"
|
|
echo ""
|
|
echo "Try manually:"
|
|
echo -e " ${YELLOW}curl -fsSL ccs.kaitran.ca/install | bash${RESET}"
|
|
echo ""
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# --- 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."
|
|
}
|
|
|
|
show_version() {
|
|
# Title
|
|
echo -e "${BOLD}CCS (Claude Code Switch) v${CCS_VERSION}${RESET}"
|
|
echo ""
|
|
|
|
# Installation section with table-like formatting
|
|
echo -e "${CYAN}Installation:${RESET}"
|
|
|
|
# Location - prioritize script location over command location
|
|
local script_location="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
local command_location=$(command -v ccs 2>/dev/null || echo "(not found)")
|
|
|
|
# Show script location if running from source
|
|
if [[ "$script_location" == *"ccs"* ]]; then
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Location:" "${script_location}"
|
|
else
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Location:" "${command_location}"
|
|
fi
|
|
|
|
# .ccs/ directory location
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "CCS Directory:" "$HOME/.ccs/"
|
|
|
|
# Config path
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Config:" "${CONFIG_FILE}"
|
|
|
|
# Profiles.json location
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Profiles:" "${PROFILES_JSON}"
|
|
|
|
# Delegation status - check multiple indicators
|
|
local delegation_configured=false
|
|
local ready_profiles=()
|
|
|
|
# Check for delegation-sessions.json (primary indicator)
|
|
local delegation_sessions="$HOME/.ccs/delegation-sessions.json"
|
|
if [[ -f "$delegation_sessions" ]]; then
|
|
delegation_configured=true
|
|
fi
|
|
|
|
# Check for profiles with valid API keys (secondary indicator)
|
|
for profile in glm kimi; do
|
|
local settings_file="$HOME/.ccs/$profile.settings.json"
|
|
if [[ -f "$settings_file" ]]; then
|
|
# Check if API key is configured (not a placeholder)
|
|
local api_key=$(jq -r '.env.ANTHROPIC_AUTH_TOKEN // empty' "$settings_file" 2>/dev/null)
|
|
if [[ -n "$api_key" ]] && [[ ! "$api_key" =~ YOUR_.*_API_KEY_HERE ]] && [[ ! "$api_key" =~ sk-test.* ]]; then
|
|
ready_profiles+=("$profile")
|
|
delegation_configured=true
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if $delegation_configured; then
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Delegation:" "Enabled"
|
|
else
|
|
printf " ${CYAN}%-16s${RESET} %s\n" "Delegation:" "Not configured"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Ready Profiles section - make it more prominent
|
|
if [[ ${#ready_profiles[@]} -gt 0 ]]; then
|
|
echo -e "${CYAN}Delegation Ready:${RESET}"
|
|
echo " ${YELLOW}✓${RESET} ${ready_profiles[*]} profiles are ready for delegation"
|
|
echo ""
|
|
elif $delegation_configured; then
|
|
echo -e "${CYAN}Delegation Ready:${RESET}"
|
|
echo " ${YELLOW}!${RESET} Delegation configured but no valid API keys found"
|
|
echo ""
|
|
fi
|
|
|
|
# Documentation
|
|
echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
|
|
echo -e "${CYAN}License:${RESET} MIT"
|
|
echo ""
|
|
|
|
# Help hint
|
|
echo -e "${YELLOW}Run 'ccs --help' for usage information${RESET}"
|
|
}
|
|
|
|
# --- Profile Registry Functions (Phase 4) ---
|
|
|
|
# Initialize empty registry if missing
|
|
init_profiles_json() {
|
|
[[ -f "$PROFILES_JSON" ]] && return 0
|
|
|
|
local init_data='{
|
|
"version": "2.0.0",
|
|
"profiles": {},
|
|
"default": null
|
|
}'
|
|
|
|
echo "$init_data" > "$PROFILES_JSON"
|
|
chmod 0600 "$PROFILES_JSON"
|
|
}
|
|
|
|
# Read entire profiles.json
|
|
read_profiles_json() {
|
|
init_profiles_json
|
|
cat "$PROFILES_JSON"
|
|
}
|
|
|
|
# Write entire profiles.json (atomic)
|
|
write_profiles_json() {
|
|
local content="$1"
|
|
local temp_file="$PROFILES_JSON.tmp"
|
|
|
|
echo "$content" > "$temp_file" || {
|
|
msg_error "Failed to write profiles registry"
|
|
return 1
|
|
}
|
|
|
|
chmod 0600 "$temp_file"
|
|
mv "$temp_file" "$PROFILES_JSON" || {
|
|
rm -f "$temp_file"
|
|
msg_error "Failed to update profiles registry"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# Check if profile exists
|
|
profile_exists() {
|
|
local profile_name="$1"
|
|
init_profiles_json
|
|
|
|
local exists=$(jq -r ".profiles.\"$profile_name\" // empty" "$PROFILES_JSON")
|
|
[[ -n "$exists" ]]
|
|
}
|
|
|
|
# Create new profile
|
|
register_profile() {
|
|
local profile_name="$1"
|
|
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
|
|
init_profiles_json
|
|
|
|
# Check if exists
|
|
profile_exists "$profile_name" && {
|
|
msg_error "Profile already exists: $profile_name"
|
|
return 1
|
|
}
|
|
|
|
# Read current data
|
|
local data=$(read_profiles_json)
|
|
|
|
# Add new profile
|
|
data=$(echo "$data" | jq \
|
|
--arg name "$profile_name" \
|
|
--arg timestamp "$timestamp" \
|
|
'.profiles[$name] = {
|
|
"type": "account",
|
|
"created": $timestamp,
|
|
"last_used": null
|
|
}')
|
|
|
|
# Note: No longer auto-set as default
|
|
# Users must explicitly run: ccs auth default <profile>
|
|
# Default always stays on implicit 'default' profile (uses ~/.claude/)
|
|
|
|
write_profiles_json "$data"
|
|
}
|
|
|
|
# Delete profile
|
|
unregister_profile() {
|
|
local profile_name="$1"
|
|
|
|
init_profiles_json
|
|
|
|
profile_exists "$profile_name" || return 0 # Idempotent
|
|
|
|
local data=$(read_profiles_json)
|
|
|
|
# Remove profile
|
|
data=$(echo "$data" | jq --arg name "$profile_name" 'del(.profiles[$name])')
|
|
|
|
# Update default if it was the deleted profile
|
|
local current_default=$(echo "$data" | jq -r '.default // empty')
|
|
if [[ "$current_default" == "$profile_name" ]]; then
|
|
# Set to first remaining profile or null
|
|
local first_profile=$(echo "$data" | jq -r '.profiles | keys[0] // empty')
|
|
data=$(echo "$data" | jq --arg first "$first_profile" '
|
|
.default = if $first != "" then $first else null end
|
|
')
|
|
fi
|
|
|
|
write_profiles_json "$data"
|
|
}
|
|
|
|
# Update last_used timestamp
|
|
touch_profile() {
|
|
local profile_name="$1"
|
|
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
|
|
profile_exists "$profile_name" || return 0 # Silent fail if not exists
|
|
|
|
local data=$(read_profiles_json)
|
|
|
|
data=$(echo "$data" | jq \
|
|
--arg name "$profile_name" \
|
|
--arg timestamp "$timestamp" \
|
|
'.profiles[$name].last_used = $timestamp')
|
|
|
|
write_profiles_json "$data"
|
|
}
|
|
|
|
# Get default profile
|
|
get_default_profile() {
|
|
init_profiles_json
|
|
jq -r '.default // empty' "$PROFILES_JSON"
|
|
}
|
|
|
|
# Set default profile
|
|
set_default_profile() {
|
|
local profile_name="$1"
|
|
|
|
profile_exists "$profile_name" || {
|
|
msg_error "Profile not found: $profile_name"
|
|
return 1
|
|
}
|
|
|
|
local data=$(read_profiles_json)
|
|
data=$(echo "$data" | jq --arg name "$profile_name" '.default = $name')
|
|
|
|
write_profiles_json "$data"
|
|
}
|
|
|
|
# --- Instance Management Functions (Phase 2) ---
|
|
|
|
# Sanitize profile name for filesystem
|
|
sanitize_profile_name() {
|
|
local name="$1"
|
|
# Replace unsafe chars with dash, lowercase
|
|
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g'
|
|
}
|
|
|
|
# Link shared directories (Phase 1: Shared Global Data)
|
|
link_shared_directories() {
|
|
local instance_path="$1"
|
|
local shared_dir="$HOME/.ccs/shared"
|
|
|
|
# Ensure shared directories exist
|
|
mkdir -p "$shared_dir"/{commands,skills,agents,plugins}
|
|
|
|
# Create symlinks (remove existing first if present)
|
|
for dir in commands skills agents plugins; do
|
|
local link_path="$instance_path/$dir"
|
|
local target_path="$shared_dir/$dir"
|
|
|
|
# Remove existing directory/link
|
|
[[ -e "$link_path" ]] && rm -rf "$link_path"
|
|
|
|
# Create symlink
|
|
ln -sf "$target_path" "$link_path"
|
|
done
|
|
}
|
|
|
|
# Migrate to shared structure (Phase 1: Auto-migration)
|
|
migrate_to_shared_structure() {
|
|
local shared_dir="$HOME/.ccs/shared"
|
|
|
|
# Check if migration is needed (shared dirs exist but are empty)
|
|
if [[ -d "$shared_dir" ]]; then
|
|
local needs_migration=false
|
|
for dir in commands skills agents plugins; do
|
|
if [[ ! -d "$shared_dir/$dir" ]] || [[ -z "$(ls -A "$shared_dir/$dir" 2>/dev/null)" ]]; then
|
|
needs_migration=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
[[ "$needs_migration" == "false" ]] && return 0
|
|
fi
|
|
|
|
# Create shared directory
|
|
mkdir -p "$shared_dir"/{commands,skills,agents,plugins}
|
|
|
|
# Copy from ~/.claude/ (actual Claude CLI directory)
|
|
local claude_dir="$HOME/.claude"
|
|
|
|
if [[ -d "$claude_dir" ]]; then
|
|
# Copy commands to shared (if exists)
|
|
[[ -d "$claude_dir/commands" ]] && \
|
|
cp -r "$claude_dir/commands"/* "$shared_dir/commands/" 2>/dev/null || true
|
|
|
|
# Copy skills to shared (if exists)
|
|
[[ -d "$claude_dir/skills" ]] && \
|
|
cp -r "$claude_dir/skills"/* "$shared_dir/skills/" 2>/dev/null || true
|
|
|
|
# Copy agents to shared (if exists)
|
|
[[ -d "$claude_dir/agents" ]] && \
|
|
cp -r "$claude_dir/agents"/* "$shared_dir/agents/" 2>/dev/null || true
|
|
|
|
# Copy plugins to shared (if exists)
|
|
[[ -d "$claude_dir/plugins" ]] && \
|
|
cp -r "$claude_dir/plugins"/* "$shared_dir/plugins/" 2>/dev/null || true
|
|
fi
|
|
|
|
# Update all instances to use symlinks
|
|
for instance_path in "$INSTANCES_DIR"/*; do
|
|
[[ -d "$instance_path" ]] || continue
|
|
link_shared_directories "$instance_path"
|
|
done
|
|
|
|
echo "[OK] Migrated to shared structure"
|
|
}
|
|
|
|
# Initialize new instance directory
|
|
initialize_instance() {
|
|
local instance_path="$1"
|
|
|
|
# Create base directory
|
|
mkdir -m 0700 -p "$instance_path"
|
|
|
|
# Create subdirectories (profile-specific only)
|
|
local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic)
|
|
for dir in "${subdirs[@]}"; do
|
|
mkdir -m 0700 -p "$instance_path/$dir"
|
|
done
|
|
|
|
# Symlink shared directories
|
|
link_shared_directories "$instance_path"
|
|
|
|
# Copy global configs (optional)
|
|
copy_global_configs "$instance_path"
|
|
}
|
|
|
|
# Validate instance structure (auto-repair)
|
|
validate_instance() {
|
|
local instance_path="$1"
|
|
local required_dirs=(session-env todos logs file-history shell-snapshots debug .anthropic)
|
|
|
|
for dir in "${required_dirs[@]}"; do
|
|
if [[ ! -d "$instance_path/$dir" ]]; then
|
|
mkdir -m 0700 -p "$instance_path/$dir"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Copy global Claude configs to instance
|
|
copy_global_configs() {
|
|
local instance_path="$1"
|
|
local global_claude="$HOME/.claude"
|
|
|
|
# Copy settings.json only (commands/skills are now symlinked to shared/)
|
|
[[ -f "$global_claude/settings.json" ]] && \
|
|
cp "$global_claude/settings.json" "$instance_path/settings.json" 2>/dev/null || true
|
|
}
|
|
|
|
# Ensure instance exists (lazy initialization)
|
|
ensure_instance() {
|
|
local profile_name="$1"
|
|
local safe_name=$(sanitize_profile_name "$profile_name")
|
|
local instance_path="$INSTANCES_DIR/$safe_name"
|
|
|
|
# Create if missing
|
|
if [[ ! -d "$instance_path" ]]; then
|
|
initialize_instance "$instance_path"
|
|
fi
|
|
|
|
# Validate structure
|
|
validate_instance "$instance_path"
|
|
|
|
echo "$instance_path"
|
|
}
|
|
|
|
# --- Profile Detection Logic (Phase 1) ---
|
|
|
|
# List available profiles for error messages
|
|
# Get all profile names (for fuzzy matching)
|
|
get_all_profile_names() {
|
|
local names=()
|
|
|
|
# Settings-based profiles
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
while IFS= read -r name; do
|
|
names+=("$name")
|
|
done < <(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null || true)
|
|
fi
|
|
|
|
# Account-based profiles
|
|
if [[ -f "$PROFILES_JSON" ]]; then
|
|
while IFS= read -r name; do
|
|
names+=("$name")
|
|
done < <(jq -r '.profiles | keys[]' "$PROFILES_JSON" 2>/dev/null || true)
|
|
fi
|
|
|
|
printf '%s\n' "${names[@]}"
|
|
}
|
|
|
|
list_available_profiles() {
|
|
local lines=()
|
|
|
|
# Settings-based profiles
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
local settings_profiles=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null || true)
|
|
if [[ -n "$settings_profiles" ]]; then
|
|
lines+=("Settings-based profiles (GLM, Kimi, etc.):")
|
|
while IFS= read -r name; do
|
|
lines+=(" - $name")
|
|
done <<< "$settings_profiles"
|
|
fi
|
|
fi
|
|
|
|
# Account-based profiles
|
|
if [[ -f "$PROFILES_JSON" ]]; then
|
|
local account_profiles=$(jq -r '.profiles | keys[]' "$PROFILES_JSON" 2>/dev/null || true)
|
|
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
|
|
if [[ -n "$account_profiles" ]]; then
|
|
lines+=("Account-based profiles:")
|
|
while IFS= read -r name; do
|
|
local is_default=""
|
|
[[ "$name" == "$default_profile" ]] && is_default=" [DEFAULT]"
|
|
lines+=(" - $name$is_default")
|
|
done <<< "$account_profiles"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
echo " (no profiles configured)"
|
|
echo " Run \"ccs auth create <profile>\" to create your first account profile."
|
|
else
|
|
printf '%s\n' "${lines[@]}"
|
|
fi
|
|
}
|
|
|
|
# Detect profile type and return info
|
|
# Sets global variables: PROFILE_TYPE, PROFILE_PATH/INSTANCE_PATH
|
|
detect_profile_type() {
|
|
local profile_name="$1"
|
|
|
|
# Special case: 'default' resolves to default profile
|
|
if [[ "$profile_name" == "default" ]]; then
|
|
# Check account-based default first
|
|
if [[ -f "$PROFILES_JSON" ]]; then
|
|
local default_account=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
if [[ -n "$default_account" ]] && profile_exists "$default_account"; then
|
|
PROFILE_TYPE="account"
|
|
PROFILE_NAME="$default_account"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Check settings-based default
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
local default_settings=$(jq -r '.profiles.default // empty' "$CONFIG_FILE" 2>/dev/null || true)
|
|
if [[ -n "$default_settings" ]]; then
|
|
PROFILE_TYPE="settings"
|
|
PROFILE_PATH="$default_settings"
|
|
PROFILE_NAME="default"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# No default configured, use Claude's defaults
|
|
PROFILE_TYPE="default"
|
|
PROFILE_NAME="default"
|
|
return 0
|
|
fi
|
|
|
|
# Priority 1: Check settings-based profiles (backward compatibility)
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
local settings_path=$(jq -r ".profiles.\"$profile_name\" // empty" "$CONFIG_FILE" 2>/dev/null || true)
|
|
if [[ -n "$settings_path" ]]; then
|
|
PROFILE_TYPE="settings"
|
|
PROFILE_PATH="$settings_path"
|
|
PROFILE_NAME="$profile_name"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Priority 2: Check account-based profiles
|
|
if [[ -f "$PROFILES_JSON" ]] && profile_exists "$profile_name"; then
|
|
PROFILE_TYPE="account"
|
|
PROFILE_NAME="$profile_name"
|
|
return 0
|
|
fi
|
|
|
|
# Not found
|
|
PROFILE_TYPE="error"
|
|
return 1
|
|
}
|
|
|
|
# --- Auth Commands (Phase 3) ---
|
|
|
|
auth_help() {
|
|
echo -e "${BOLD}CCS Concurrent Account Management${RESET}"
|
|
echo ""
|
|
echo -e "${CYAN}Usage:${RESET}"
|
|
echo -e " ${YELLOW}ccs auth${RESET} <command> [options]"
|
|
echo ""
|
|
echo -e "${CYAN}Commands:${RESET}"
|
|
echo -e " ${YELLOW}create <profile>${RESET} Create new profile and login"
|
|
echo -e " ${YELLOW}list${RESET} List all saved profiles"
|
|
echo -e " ${YELLOW}show <profile>${RESET} Show profile details"
|
|
echo -e " ${YELLOW}remove <profile>${RESET} Remove saved profile"
|
|
echo -e " ${YELLOW}default <profile>${RESET} Set default profile"
|
|
echo ""
|
|
echo -e "${CYAN}Examples:${RESET}"
|
|
echo -e " ${YELLOW}ccs auth create work${RESET} # Create & login to work profile"
|
|
echo -e " ${YELLOW}ccs auth default work${RESET} # Set work as default"
|
|
echo -e " ${YELLOW}ccs auth list${RESET} # List all profiles"
|
|
echo -e " ${YELLOW}ccs work \"review code\"${RESET} # Use work profile"
|
|
echo -e " ${YELLOW}ccs \"review code\"${RESET} # Use default profile"
|
|
echo ""
|
|
echo -e "${CYAN}Options:${RESET}"
|
|
echo -e " ${YELLOW}--force${RESET} Allow overwriting existing profile (create)"
|
|
echo -e " ${YELLOW}--yes, -y${RESET} Skip confirmation prompts (remove)"
|
|
echo -e " ${YELLOW}--json${RESET} Output in JSON format (list, show)"
|
|
echo -e " ${YELLOW}--verbose${RESET} Show additional details (list)"
|
|
echo ""
|
|
echo -e "${CYAN}Note:${RESET}"
|
|
echo -e " By default, ${YELLOW}ccs${RESET} uses Claude CLI defaults from ~/.claude/"
|
|
echo -e " Use ${YELLOW}ccs auth default <profile>${RESET} to change the default profile."
|
|
echo ""
|
|
}
|
|
|
|
auth_create() {
|
|
local profile_name=""
|
|
local force=false
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--force) force=true ;;
|
|
-*) msg_error "Unknown option: $1"; return 1 ;;
|
|
*) profile_name="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Validate profile name
|
|
[[ -z "$profile_name" ]] && {
|
|
msg_error "Profile name is required"
|
|
echo ""
|
|
echo "Usage: ${YELLOW}ccs auth create <profile> [--force]${RESET}"
|
|
return 1
|
|
}
|
|
|
|
# Check if exists
|
|
if ! $force && profile_exists "$profile_name"; then
|
|
msg_error "Profile already exists: $profile_name"
|
|
echo "Use ${YELLOW}--force${RESET} to overwrite"
|
|
return 1
|
|
fi
|
|
|
|
# Create instance
|
|
echo "[i] Creating profile: $profile_name"
|
|
local instance_path=$(ensure_instance "$profile_name")
|
|
echo "[i] Instance directory: $instance_path"
|
|
echo ""
|
|
|
|
# Register profile
|
|
register_profile "$profile_name"
|
|
|
|
# Launch Claude for login
|
|
echo -e "${YELLOW}[i] Starting Claude in isolated instance...${RESET}"
|
|
echo -e "${YELLOW}[i] You will be prompted to login with your account.${RESET}"
|
|
echo ""
|
|
|
|
CLAUDE_CONFIG_DIR="$instance_path" $(detect_claude_cli) || {
|
|
msg_error "Login failed or cancelled"
|
|
echo "To retry: ${YELLOW}ccs auth create $profile_name --force${RESET}"
|
|
return 1
|
|
}
|
|
|
|
echo ""
|
|
echo -e "${GREEN}[OK] Profile created successfully${RESET}"
|
|
echo ""
|
|
echo " Profile: $profile_name"
|
|
echo " Instance: $instance_path"
|
|
echo ""
|
|
echo "Usage:"
|
|
echo " ${YELLOW}ccs $profile_name \"your prompt here\"${RESET} # Use this specific profile"
|
|
echo ""
|
|
echo "To set as default (so you can use just \"ccs\"):"
|
|
echo " ${YELLOW}ccs auth default $profile_name${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
auth_list() {
|
|
local verbose=false
|
|
local json=false
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--verbose) verbose=true ;;
|
|
--json) json=true ;;
|
|
*) ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Read profiles.json
|
|
[[ ! -f "$PROFILES_JSON" ]] && {
|
|
if $json; then
|
|
echo "{\"version\":\"$CCS_VERSION\",\"profiles\":[]}"
|
|
return 0
|
|
fi
|
|
echo -e "${YELLOW}No account profiles found${RESET}"
|
|
echo ""
|
|
echo "To create your first profile:"
|
|
echo " ${YELLOW}ccs auth create <profile>${RESET}"
|
|
return 0
|
|
}
|
|
|
|
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
|
|
# JSON output mode
|
|
if $json; then
|
|
jq -n \
|
|
--arg version "$CCS_VERSION" \
|
|
--arg default "$default_profile" \
|
|
--argjson data "$(cat "$PROFILES_JSON")" \
|
|
'{
|
|
version: $version,
|
|
profiles: [
|
|
$data.profiles | to_entries[] | {
|
|
name: .key,
|
|
type: (.value.type // "account"),
|
|
is_default: (.key == $default),
|
|
created: .value.created,
|
|
last_used: (.value.last_used // null),
|
|
instance_path: ($ENV.INSTANCES_DIR + "/" + .key)
|
|
}
|
|
]
|
|
}'
|
|
return 0
|
|
fi
|
|
|
|
# Human-readable output
|
|
local profiles=$(jq -r '.profiles | keys[]' "$PROFILES_JSON" 2>/dev/null || true)
|
|
|
|
[[ -z "$profiles" ]] && {
|
|
echo -e "${YELLOW}No account profiles found${RESET}"
|
|
return 0
|
|
}
|
|
|
|
echo -e "${BOLD}Saved Account Profiles:${RESET}"
|
|
echo ""
|
|
|
|
# Display profiles
|
|
while IFS= read -r profile; do
|
|
local is_default=false
|
|
[[ "$profile" == "$default_profile" ]] && is_default=true
|
|
|
|
if $is_default; then
|
|
echo -e "${GREEN}[*] ${CYAN}$profile${GREEN} (default)${RESET}"
|
|
else
|
|
echo -e "[ ] ${CYAN}$profile${RESET}"
|
|
fi
|
|
|
|
local type=$(jq -r ".profiles.\"$profile\".type // \"account\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
echo " Type: $type"
|
|
|
|
if $verbose; then
|
|
local created=$(jq -r ".profiles.\"$profile\".created" "$PROFILES_JSON" 2>/dev/null || true)
|
|
local last_used=$(jq -r ".profiles.\"$profile\".last_used // \"Never\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
echo " Created: $created"
|
|
echo " Last used: $last_used"
|
|
fi
|
|
|
|
echo ""
|
|
done <<< "$profiles"
|
|
}
|
|
|
|
auth_show() {
|
|
local profile_name=""
|
|
local json=false
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--json) json=true ;;
|
|
-*) msg_error "Unknown option: $1"; return 1 ;;
|
|
*) profile_name="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[[ -z "$profile_name" ]] && {
|
|
msg_error "Profile name is required"
|
|
echo "Usage: ${YELLOW}ccs auth show <profile> [--json]${RESET}"
|
|
return 1
|
|
}
|
|
|
|
# Check if exists
|
|
profile_exists "$profile_name" || {
|
|
msg_error "Profile not found: $profile_name"
|
|
return 1
|
|
}
|
|
|
|
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
local is_default=false
|
|
[[ "$profile_name" == "$default_profile" ]] && is_default=true
|
|
|
|
local type=$(jq -r ".profiles.\"$profile_name\".type // \"account\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
local created=$(jq -r ".profiles.\"$profile_name\".created" "$PROFILES_JSON" 2>/dev/null || true)
|
|
local last_used=$(jq -r ".profiles.\"$profile_name\".last_used // null" "$PROFILES_JSON" 2>/dev/null || true)
|
|
local instance_path="$INSTANCES_DIR/$(sanitize_profile_name "$profile_name")"
|
|
|
|
# Count sessions
|
|
local session_count=0
|
|
if [[ -d "$instance_path/session-env" ]]; then
|
|
session_count=$(find "$instance_path/session-env" -name "*.json" 2>/dev/null | wc -l | tr -d ' ')
|
|
fi
|
|
|
|
# JSON output mode
|
|
if $json; then
|
|
jq -n \
|
|
--arg name "$profile_name" \
|
|
--arg type "$type" \
|
|
--argjson is_default "$is_default" \
|
|
--arg created "$created" \
|
|
--arg last_used "$last_used" \
|
|
--arg instance_path "$instance_path" \
|
|
--argjson session_count "$session_count" \
|
|
'{
|
|
name: $name,
|
|
type: $type,
|
|
is_default: $is_default,
|
|
created: $created,
|
|
last_used: $last_used,
|
|
instance_path: $instance_path,
|
|
session_count: $session_count
|
|
}'
|
|
return 0
|
|
fi
|
|
|
|
# Human-readable output
|
|
echo -e "${BOLD}Profile: $profile_name${RESET}"
|
|
echo ""
|
|
|
|
echo " Type: $type"
|
|
echo " Default: $($is_default && echo "Yes" || echo "No")"
|
|
echo " Instance: $instance_path"
|
|
echo " Created: $created"
|
|
[[ "$last_used" != "null" ]] && echo " Last used: $last_used" || echo " Last used: Never"
|
|
echo ""
|
|
}
|
|
|
|
auth_remove() {
|
|
local profile_name=""
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--yes|-y) export CCS_YES=1 ;; # Auto-confirm (export for confirm_action)
|
|
-*) msg_error "Unknown option: $1"; return 1 ;;
|
|
*) profile_name="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[[ -z "$profile_name" ]] && {
|
|
msg_error "Profile name is required"
|
|
echo ""
|
|
echo "Usage: ${YELLOW}ccs auth remove <profile> [--yes]${RESET}"
|
|
return 1
|
|
}
|
|
|
|
profile_exists "$profile_name" || {
|
|
msg_error "Profile not found: $profile_name"
|
|
return 1
|
|
}
|
|
|
|
# Get instance path and session count for impact display
|
|
local instance_path="$INSTANCES_DIR/$(sanitize_profile_name "$profile_name")"
|
|
local session_count=0
|
|
|
|
if [[ -d "$instance_path/session-env" ]]; then
|
|
session_count=$(find "$instance_path/session-env" -name "*.json" 2>/dev/null | wc -l | tr -d ' ')
|
|
fi
|
|
|
|
# Display impact
|
|
echo ""
|
|
echo "Profile '${CYAN}$profile_name${RESET}' will be permanently deleted."
|
|
echo " Instance path: $instance_path"
|
|
echo " Sessions: $session_count conversation$([ "$session_count" -ne 1 ] && echo "s" || echo "")"
|
|
echo ""
|
|
|
|
# Interactive confirmation (or --yes flag)
|
|
if ! confirm_action "Delete this profile?" "no"; then
|
|
echo "[i] Cancelled"
|
|
return 0
|
|
fi
|
|
|
|
# Delete instance directory
|
|
rm -rf "$instance_path"
|
|
|
|
# Remove from registry
|
|
unregister_profile "$profile_name"
|
|
|
|
echo -e "${GREEN}[OK] Profile removed successfully${RESET}"
|
|
echo " Profile: $profile_name"
|
|
echo ""
|
|
}
|
|
|
|
auth_default() {
|
|
local profile_name="${1:-}"
|
|
|
|
[[ -z "$profile_name" ]] && {
|
|
msg_error "Profile name is required"
|
|
echo "Usage: ${YELLOW}ccs auth default <profile>${RESET}"
|
|
return 1
|
|
}
|
|
|
|
profile_exists "$profile_name" || {
|
|
msg_error "Profile not found: $profile_name"
|
|
return 1
|
|
}
|
|
|
|
set_default_profile "$profile_name"
|
|
|
|
echo -e "${GREEN}[OK] Default profile set${RESET}"
|
|
echo " Profile: $profile_name"
|
|
echo ""
|
|
echo "Now you can use:"
|
|
echo " ${YELLOW}ccs \"your prompt\"${RESET} # Uses $profile_name profile"
|
|
echo ""
|
|
}
|
|
|
|
handle_auth_commands() {
|
|
shift # Remove 'auth'
|
|
|
|
local subcommand="${1:-}"
|
|
shift || true
|
|
|
|
case "$subcommand" in
|
|
create) auth_create "$@" ;;
|
|
list) auth_list "$@" ;;
|
|
show) auth_show "$@" ;;
|
|
remove) auth_remove "$@" ;;
|
|
default) auth_default "$@" ;;
|
|
*) auth_help ;;
|
|
esac
|
|
}
|
|
|
|
# --- Shell Completion Installer ---
|
|
|
|
install_shell_completion() {
|
|
shift # Remove --shell-completion
|
|
|
|
echo -e "${BOLD}Shell Completion Installer${RESET}"
|
|
echo ""
|
|
|
|
# Parse flags for manual shell selection
|
|
local target_shell=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--bash) target_shell="bash" ;;
|
|
--zsh) target_shell="zsh" ;;
|
|
--fish) target_shell="fish" ;;
|
|
*) ;;
|
|
esac
|
|
done
|
|
|
|
# Auto-detect shell if not specified
|
|
if [[ -z "$target_shell" ]]; then
|
|
if [[ -n "$BASH_VERSION" ]]; then
|
|
target_shell="bash"
|
|
elif [[ -n "$ZSH_VERSION" ]]; then
|
|
target_shell="zsh"
|
|
elif [[ -n "$FISH_VERSION" ]]; then
|
|
target_shell="fish"
|
|
else
|
|
echo -e "${RED}[X] Could not detect shell${RESET}" >&2
|
|
echo "" >&2
|
|
echo -e "${YELLOW}Usage:${RESET}" >&2
|
|
echo " ccs --shell-completion # Auto-detect shell" >&2
|
|
echo " ccs --shell-completion --bash # Install for bash" >&2
|
|
echo " ccs --shell-completion --zsh # Install for zsh" >&2
|
|
echo " ccs --shell-completion --fish # Install for fish" >&2
|
|
echo "" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Ensure completion files exist in ~/.ccs/completions/
|
|
local completions_dir="$HOME/.ccs/completions"
|
|
if [[ ! -d "$completions_dir" ]]; then
|
|
mkdir -p "$completions_dir"
|
|
fi
|
|
|
|
# Copy from scripts if not present
|
|
local script_dir="$(dirname "$0")"
|
|
if [[ -f "$script_dir/../scripts/completion/ccs.bash" ]]; then
|
|
cp "$script_dir/../scripts/completion/ccs.bash" "$completions_dir/" 2>/dev/null || true
|
|
cp "$script_dir/../scripts/completion/ccs.zsh" "$completions_dir/" 2>/dev/null || true
|
|
cp "$script_dir/../scripts/completion/ccs.fish" "$completions_dir/" 2>/dev/null || true
|
|
fi
|
|
|
|
# Install based on target shell
|
|
case "$target_shell" in
|
|
bash)
|
|
local rc_file="$HOME/.bashrc"
|
|
local completion_file="$completions_dir/ccs.bash"
|
|
local marker="# CCS shell completion"
|
|
|
|
if [[ ! -f "$completion_file" ]]; then
|
|
echo -e "${RED}[X] Completion file not found: $completion_file${RESET}" >&2
|
|
echo " Please reinstall CCS." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Check if already installed
|
|
if grep -q "$marker" "$rc_file" 2>/dev/null; then
|
|
echo -e "${GREEN}[OK] Shell completion already installed${RESET}"
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
# Append to .bashrc
|
|
{
|
|
echo ""
|
|
echo "$marker"
|
|
echo "source \"$completion_file\""
|
|
} >> "$rc_file"
|
|
|
|
echo -e "${GREEN}[OK] Shell completion installed successfully!${RESET}"
|
|
echo ""
|
|
echo "Added to $rc_file"
|
|
echo ""
|
|
echo -e "${CYAN}To activate:${RESET}"
|
|
echo " source ~/.bashrc"
|
|
echo ""
|
|
echo -e "${CYAN}Then test:${RESET}"
|
|
echo " ccs <TAB> # See available profiles"
|
|
echo " ccs auth <TAB> # See auth subcommands"
|
|
echo ""
|
|
;;
|
|
|
|
zsh)
|
|
local rc_file="$HOME/.zshrc"
|
|
local completion_dir="$HOME/.zsh/completion"
|
|
local completion_file="$completions_dir/ccs.zsh"
|
|
local marker="# CCS shell completion"
|
|
|
|
if [[ ! -f "$completion_file" ]]; then
|
|
echo -e "${RED}[X] Completion file not found: $completion_file${RESET}" >&2
|
|
echo " Please reinstall CCS." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Create zsh completion directory
|
|
mkdir -p "$completion_dir"
|
|
|
|
# Copy to zsh completion directory
|
|
cp "$completion_file" "$completion_dir/_ccs"
|
|
|
|
# Check if already installed
|
|
if grep -q "$marker" "$rc_file" 2>/dev/null; then
|
|
echo -e "${GREEN}[OK] Shell completion already installed${RESET}"
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
# Append to .zshrc
|
|
{
|
|
echo ""
|
|
echo "$marker"
|
|
echo "fpath=(~/.zsh/completion \$fpath)"
|
|
echo "autoload -Uz compinit && compinit"
|
|
} >> "$rc_file"
|
|
|
|
echo -e "${GREEN}[OK] Shell completion installed successfully!${RESET}"
|
|
echo ""
|
|
echo "Added to $rc_file"
|
|
echo ""
|
|
echo -e "${CYAN}To activate:${RESET}"
|
|
echo " source ~/.zshrc"
|
|
echo ""
|
|
echo -e "${CYAN}Then test:${RESET}"
|
|
echo " ccs <TAB> # See available profiles"
|
|
echo " ccs auth <TAB> # See auth subcommands"
|
|
echo ""
|
|
;;
|
|
|
|
fish)
|
|
local fish_dir="$HOME/.config/fish/completions"
|
|
local completion_file="$completions_dir/ccs.fish"
|
|
|
|
if [[ ! -f "$completion_file" ]]; then
|
|
echo -e "${RED}[X] Completion file not found: $completion_file${RESET}" >&2
|
|
echo " Please reinstall CCS." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Create fish completion directory
|
|
mkdir -p "$fish_dir"
|
|
|
|
# Copy to fish completion directory (fish auto-loads from here)
|
|
cp "$completion_file" "$fish_dir/ccs.fish"
|
|
|
|
echo -e "${GREEN}[OK] Shell completion installed successfully!${RESET}"
|
|
echo ""
|
|
echo "Installed to $fish_dir/ccs.fish"
|
|
echo ""
|
|
echo -e "${CYAN}To activate:${RESET}"
|
|
echo " Fish auto-loads completions (no reload needed)"
|
|
echo ""
|
|
echo -e "${CYAN}Then test:${RESET}"
|
|
echo " ccs <TAB> # See available profiles"
|
|
echo " ccs auth <TAB> # See auth subcommands"
|
|
echo ""
|
|
;;
|
|
|
|
*)
|
|
echo -e "${RED}[X] Unsupported shell: $target_shell${RESET}" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
return 0
|
|
}
|
|
|
|
# --- Main Execution Logic ---
|
|
|
|
# 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
|
|
show_help
|
|
exit 0
|
|
fi
|
|
|
|
# Special case: auth commands
|
|
if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then
|
|
handle_auth_commands "$@"
|
|
exit $?
|
|
fi
|
|
|
|
# Special case: shell completion installer
|
|
if [[ $# -gt 0 ]] && [[ "${1}" == "--shell-completion" || "${1}" == "-sc" ]]; then
|
|
install_shell_completion "$@"
|
|
exit $?
|
|
fi
|
|
|
|
# Special case: doctor command
|
|
if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then
|
|
doctor_run
|
|
exit $?
|
|
fi
|
|
|
|
# Special case: sync command
|
|
if [[ $# -gt 0 ]] && [[ "${1}" == "sync" || "${1}" == "--sync" ]]; then
|
|
sync_run
|
|
exit $?
|
|
fi
|
|
|
|
# Special case: update command
|
|
if [[ $# -gt 0 ]] && [[ "${1}" == "update" || "${1}" == "--update" ]]; then
|
|
update_run
|
|
exit $?
|
|
fi
|
|
|
|
# Run auto-recovery before main logic
|
|
auto_recover || {
|
|
msg_error "Auto-recovery failed. Check permissions."
|
|
exit 1
|
|
}
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# Detect profile type
|
|
if ! detect_profile_type "$PROFILE"; then
|
|
# Get suggestions using fuzzy matching
|
|
mapfile -t all_profiles < <(get_all_profile_names)
|
|
mapfile -t suggestions < <(find_similar_strings "$PROFILE" "${all_profiles[@]}")
|
|
|
|
echo "" >&2
|
|
echo -e "${RED}[X] Profile '$PROFILE' not found${RESET}" >&2
|
|
echo "" >&2
|
|
|
|
# Show suggestions if any
|
|
if [[ ${#suggestions[@]} -gt 0 ]]; then
|
|
echo -e "${YELLOW}Did you mean:${RESET}" >&2
|
|
for suggestion in "${suggestions[@]}"; do
|
|
echo " $suggestion" >&2
|
|
done
|
|
echo "" >&2
|
|
fi
|
|
|
|
echo -e "${CYAN}Available profiles:${RESET}" >&2
|
|
list_available_profiles >&2
|
|
echo "" >&2
|
|
echo -e "${YELLOW}Solutions:${RESET}" >&2
|
|
echo " # Use existing profile" >&2
|
|
echo " ccs <profile> \"your prompt\"" >&2
|
|
echo "" >&2
|
|
echo " # Create new account profile" >&2
|
|
echo " ccs auth create <name>" >&2
|
|
echo "" >&2
|
|
echo -e "${YELLOW}Error: $E_PROFILE_NOT_FOUND${RESET}" >&2
|
|
echo -e "${YELLOW}$(get_error_doc_url "$E_PROFILE_NOT_FOUND")${RESET}" >&2
|
|
echo "" >&2
|
|
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 based on profile type (Phase 5)
|
|
case "$PROFILE_TYPE" in
|
|
account)
|
|
# Account-based profile: use CLAUDE_CONFIG_DIR
|
|
INSTANCE_PATH=$(ensure_instance "$PROFILE_NAME")
|
|
touch_profile "$PROFILE_NAME" # Update last_used
|
|
|
|
# Execute Claude with isolated config
|
|
CLAUDE_CONFIG_DIR="$INSTANCE_PATH" exec "$CLAUDE_CLI" "$@" || {
|
|
show_claude_not_found_error
|
|
exit 1
|
|
}
|
|
;;
|
|
|
|
settings)
|
|
# Settings-based profile: use --settings flag
|
|
SETTINGS_PATH="${PROFILE_PATH/#\~/$HOME}"
|
|
|
|
[[ ! -f "$SETTINGS_PATH" ]] && {
|
|
msg_error "Settings file not found: $SETTINGS_PATH"
|
|
exit 1
|
|
}
|
|
|
|
exec "$CLAUDE_CLI" --settings "$SETTINGS_PATH" "$@" || {
|
|
show_claude_not_found_error
|
|
exit 1
|
|
}
|
|
;;
|
|
|
|
default)
|
|
# Default: no special handling
|
|
exec "$CLAUDE_CLI" "$@" || {
|
|
show_claude_not_found_error
|
|
exit 1
|
|
}
|
|
;;
|
|
|
|
*)
|
|
msg_error "Unknown profile type: $PROFILE_TYPE"
|
|
exit 1
|
|
;;
|
|
esac
|