diff --git a/VERSION b/VERSION index a95f2884..b1cbc1fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.4 +4.1.5 diff --git a/bin/auth/auth-commands.js b/bin/auth/auth-commands.js index ee89ec5c..6547d0af 100644 --- a/bin/auth/auth-commands.js +++ b/bin/auth/auth-commands.js @@ -29,7 +29,7 @@ class AuthCommands { * Show help for auth commands */ showHelp() { - console.log(colored('CCS Account Management', 'bold')); + console.log(colored('CCS Concurrent Account Management', 'bold')); console.log(''); console.log(colored('Usage:', 'cyan')); console.log(` ${colored('ccs auth', 'yellow')} [options]`); diff --git a/bin/ccs.js b/bin/ccs.js index 1aeedf4e..f189b5dc 100755 --- a/bin/ccs.js +++ b/bin/ccs.js @@ -63,45 +63,69 @@ function handleVersionCommand() { console.log(colored(`CCS (Claude Code Switch) v${CCS_VERSION}`, 'bold')); console.log(''); - // Installation section + // Installation section with table-like formatting console.log(colored('Installation:', 'cyan')); // Location const installLocation = process.argv[1] || '(not found)'; - console.log(` ${colored('Location:', 'cyan')} ${installLocation}`); + console.log(` ${colored('Location:'.padEnd(17), 'cyan')} ${installLocation}`); + + // .ccs/ directory location + const ccsDir = path.join(os.homedir(), '.ccs'); + console.log(` ${colored('CCS Directory:'.padEnd(17), 'cyan')} ${ccsDir}`); // Config path const configPath = getConfigPath(); - console.log(` ${colored('Config:', 'cyan')} ${configPath}`); + console.log(` ${colored('Config:'.padEnd(17), 'cyan')} ${configPath}`); - // Delegation status - const delegationRulesPath = path.join(os.homedir(), '.ccs', 'delegation-rules.json'); - const delegationEnabled = fs.existsSync(delegationRulesPath); + // Profiles.json location + const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json'); + console.log(` ${colored('Profiles:'.padEnd(17), 'cyan')} ${profilesJson}`); - if (delegationEnabled) { - console.log(` ${colored('Delegation:', 'cyan')} Enabled`); + // Delegation status - check multiple indicators + const delegationSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json'); + const delegationConfigured = fs.existsSync(delegationSessionsPath); - // Check which profiles are delegation-ready - const readyProfiles = []; - const { DelegationValidator } = require('./utils/delegation-validator'); + let readyProfiles = []; - for (const profile of ['glm', 'kimi']) { - const validation = DelegationValidator.validate(profile); - if (validation.valid) { - readyProfiles.push(profile); + // Check for profiles with valid API keys + for (const profile of ['glm', 'kimi']) { + const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`); + if (fs.existsSync(settingsPath)) { + try { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN; + if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) { + readyProfiles.push(profile); + } + } catch (error) { + // Invalid JSON, skip } } - - if (readyProfiles.length > 0) { - console.log(` ${colored('Ready:', 'cyan')} ${readyProfiles.join(', ')}`); - } else { - console.log(` ${colored('Ready:', 'cyan')} None (configure profiles first)`); - } - } else { - console.log(` ${colored('Delegation:', 'cyan')} Not configured`); } + + const hasValidApiKeys = readyProfiles.length > 0; + const delegationEnabled = delegationConfigured || hasValidApiKeys; + + if (delegationEnabled) { + console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Enabled`); + } else { + console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Not configured`); + } + console.log(''); + // Ready Profiles section - make it more prominent + if (readyProfiles.length > 0) { + console.log(colored('Delegation Ready:', 'cyan')); + console.log(` ${colored('✓', 'yellow')} ${readyProfiles.join(', ')} profiles are ready for delegation`); + console.log(''); + } else if (delegationEnabled) { + console.log(colored('Delegation Ready:', 'cyan')); + console.log(` ${colored('!', 'yellow')} Delegation configured but no valid API keys found`); + console.log(''); + } + // Documentation console.log(`${colored('Documentation:', 'cyan')} https://github.com/kaitranntt/ccs`); console.log(`${colored('License:', 'cyan')} MIT`); @@ -143,7 +167,7 @@ function handleHelpCommand() { // Account Management console.log(colored('Account Management:', 'cyan')); - console.log(` ${colored('ccs auth --help', 'yellow')} Manage multiple Claude accounts`); + console.log(` ${colored('ccs auth --help', 'yellow')} Run multiple Claude accounts concurrently`); console.log(''); // Delegation (inside Claude Code CLI) @@ -156,14 +180,14 @@ function handleHelpCommand() { // Diagnostics console.log(colored('Diagnostics:', 'cyan')); console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`); - console.log(` ${colored('ccs update', 'yellow')} Re-install CCS items to ~/.claude/`); + console.log(` ${colored('ccs sync', 'yellow')} Sync delegation commands and skills`); console.log(''); // Flags console.log(colored('Flags:', 'cyan')); console.log(` ${colored('-h, --help', 'yellow')} Show this help message`); console.log(` ${colored('-v, --version', 'yellow')} Show version and installation info`); - console.log(` ${colored('--shell-completion', 'yellow')} Install shell auto-completion`); + console.log(` ${colored('-sc, --shell-completion', 'yellow')} Install shell auto-completion`); console.log(''); // Configuration @@ -188,7 +212,7 @@ function handleHelpCommand() { console.log(` ${colored('$ ccs', 'yellow')} # Use default account`); console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # Cost-optimized model`); console.log(''); - console.log(` For more: ${colored('https://github.com/kaitranntt/ccs#usage', 'cyan')}`); + console.log(` For more: ${colored('https://github.com/kaitranntt/ccs/blob/main/README.md', 'cyan')}`); console.log(''); // Uninstall @@ -245,7 +269,7 @@ async function handleDoctorCommand() { process.exit(doctor.results.isHealthy() ? 0 : 1); } -async function handleUpdateCommand() { +async function handleSyncCommand() { // First, copy .claude/ directory from package to ~/.ccs/.claude/ const ClaudeDirInstaller = require('./utils/claude-dir-installer'); const installer = new ClaudeDirInstaller(); @@ -255,8 +279,8 @@ async function handleUpdateCommand() { const ClaudeSymlinkManager = require('./utils/claude-symlink-manager'); const manager = new ClaudeSymlinkManager(); - console.log('[i] Updating CCS items in ~/.claude/...'); - manager.update(); + console.log('[i] Syncing delegation commands and skills to ~/.claude/...'); + manager.sync(); process.exit(0); } @@ -487,7 +511,7 @@ async function main() { } // Special case: shell completion installer - if (firstArg === '--shell-completion') { + if (firstArg === '--shell-completion' || firstArg === '-sc') { await handleShellCompletionCommand(args.slice(1)); return; } @@ -498,9 +522,9 @@ async function main() { return; } - // Special case: update command (re-install CCS symlinks) - if (firstArg === 'update' || firstArg === '--update') { - await handleUpdateCommand(); + // Special case: sync command (sync delegation commands and skills to ~/.claude/) + if (firstArg === 'sync' || firstArg === '--sync') { + await handleSyncCommand(); return; } diff --git a/bin/utils/claude-symlink-manager.js b/bin/utils/claude-symlink-manager.js index 34ba3aef..933d2bad 100644 --- a/bin/utils/claude-symlink-manager.js +++ b/bin/utils/claude-symlink-manager.js @@ -53,7 +53,7 @@ class ClaudeSymlinkManager { this._installItem(item); } - console.log('[OK] CCS items installed to ~/.claude/'); + console.log('[OK] Delegation commands and skills installed to ~/.claude/'); } /** @@ -178,9 +178,9 @@ class ClaudeSymlinkManager { } if (removed > 0) { - console.log(`[OK] Removed ${removed} CCS items from ~/.claude/`); + console.log(`[OK] Removed ${removed} delegation commands and skills from ~/.claude/`); } else { - console.log('[i] No CCS items to remove'); + console.log('[i] No delegation commands or skills to remove'); } } @@ -226,11 +226,11 @@ class ClaudeSymlinkManager { } /** - * Re-install symlinks (used by 'ccs update' command) - * Same as install() but with explicit re-installation message + * Sync delegation commands and skills to ~/.claude/ (used by 'ccs sync' command) + * Same as install() but with explicit sync message */ - update() { - console.log('[i] Updating CCS items in ~/.claude/...'); + sync() { + console.log('[i] Syncing delegation commands and skills to ~/.claude/...'); this.install(); } } diff --git a/installers/install.ps1 b/installers/install.ps1 index ae24b4e0..7d189b37 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or ( # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (irm | iex) # For git installations, VERSION file is read if available -$CcsVersion = "4.1.4" +$CcsVersion = "4.1.5" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index 959de55d..ec5acc28 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -32,7 +32,7 @@ fi # 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="4.1.4" +CCS_VERSION="4.1.5" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/lib/ccs b/lib/ccs index 63700176..5b5de655 100755 --- a/lib/ccs +++ b/lib/ccs @@ -2,7 +2,7 @@ set -euo pipefail # Version (updated by scripts/bump-version.sh) -CCS_VERSION="4.1.4" +CCS_VERSION="4.1.5" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}" readonly PROFILES_JSON="$HOME/.ccs/profiles.json" @@ -165,65 +165,82 @@ find_similar_strings() { 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 auth${RESET} [options]" 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} Manage multiple Claude accounts" + 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:glm \"task\"${RESET} Delegate to GLM-4.6 for simple tasks" echo -e " ${YELLOW}/ccs:kimi \"task\"${RESET} Delegate to Kimi for long context" 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 "" + 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}--shell-completion${RESET} Install shell auto-completion" + echo -e " ${YELLOW}-sc, --shell-completion${RESET} Install shell auto-completion" echo "" + echo -e "${CYAN}Configuration:${RESET}" - echo -e " Config: ~/.ccs/config.json" - echo -e " Profiles: ~/.ccs/profiles.json" - echo -e " Instances: ~/.ccs/instances/" - echo -e " Settings: ~/.ccs/*.settings.json" + 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 " Commands: ~/.ccs/shared/commands/" + echo -e " Skills: ~/.ccs/shared/skills/" + echo -e " Agents: ~/.ccs/shared/agents/" echo -e " Note: Commands, skills, and agents 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#usage${RESET}" + echo -e " For more: ${CYAN}https://github.com/kaitranntt/ccs/blob/main/README.md${RESET}" echo "" + echo -e "${YELLOW}Uninstall:${RESET}" - echo " npm: npm uninstall -g @kaitranntt/ccs" - echo " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash" - echo " Windows: irm ccs.kaitran.ca/uninstall | iex" + 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" } @@ -480,6 +497,100 @@ doctor_run() { $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 "" +} + # --- Claude CLI Detection Logic --- detect_claude_cli() { @@ -509,49 +620,81 @@ 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}" - # 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}" + # 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)") - # Simple config display - local config="${CCS_CONFIG:-$HOME/.ccs/config.json}" - echo -e " ${CYAN}Config:${RESET} ${config}" - - # Delegation status - local delegation_rules="$HOME/.ccs/delegation-rules.json" - if [[ -f "$delegation_rules" ]]; then - echo -e " ${CYAN}Delegation:${RESET} Enabled" - - # Check which profiles are delegation-ready - local ready_profiles=() - for profile in glm kimi; do - local settings_file="$HOME/.ccs/profiles/$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 ]]; then - ready_profiles+=("$profile") - fi - fi - done - - if [[ ${#ready_profiles[@]} -gt 0 ]]; then - echo -e " ${CYAN}Ready:${RESET} ${ready_profiles[*]}" - else - echo -e " ${CYAN}Ready:${RESET} None (configure profiles first)" - fi + # Show script location if running from source + if [[ "$script_location" == *"ccs"* ]]; then + printf " ${CYAN}%-16s${RESET} %s\n" "Location:" "${script_location}" else - echo -e " ${CYAN}Delegation:${RESET} Not configured" + 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}" } @@ -957,7 +1100,7 @@ detect_profile_type() { # --- Auth Commands (Phase 3) --- auth_help() { - echo -e "${BOLD}CCS Account Management${RESET}" + echo -e "${BOLD}CCS Concurrent Account Management${RESET}" echo "" echo -e "${CYAN}Usage:${RESET}" echo -e " ${YELLOW}ccs auth${RESET} [options]" @@ -1509,7 +1652,7 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then fi # Special case: shell completion installer -if [[ $# -gt 0 ]] && [[ "${1}" == "--shell-completion" ]]; then +if [[ $# -gt 0 ]] && [[ "${1}" == "--shell-completion" || "${1}" == "-sc" ]]; then install_shell_completion "$@" exit $? fi @@ -1520,6 +1663,12 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then exit $? fi +# Special case: sync command +if [[ $# -gt 0 ]] && [[ "${1}" == "sync" || "${1}" == "--sync" ]]; then + sync_run + exit $? +fi + # Run auto-recovery before main logic auto_recover || { msg_error "Auto-recovery failed. Check permissions." diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index 72bf770b..75a62b87 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -12,7 +12,7 @@ param( $ErrorActionPreference = "Stop" # Version (updated by scripts/bump-version.sh) -$CcsVersion = "4.1.4" +$CcsVersion = "4.1.5" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" } $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json" @@ -212,66 +212,105 @@ function Show-Help { Write-ColorLine "CCS (Claude Code Switch) - Instant profile switching for Claude CLI" "White" Write-Host "" + Write-ColorLine "Usage:" "Cyan" Write-ColorLine " ccs [profile] [claude-args...]" "Yellow" - Write-ColorLine " ccs auth [options]" "Yellow" Write-ColorLine " ccs [flags]" "Yellow" Write-Host "" + Write-ColorLine "Description:" "Cyan" Write-Host " Switch between multiple Claude accounts and alternative models" Write-Host " (GLM, Kimi) instantly. Run different Claude CLI sessions concurrently" Write-Host " with auto-recovery. Zero downtime." Write-Host "" + Write-ColorLine "Model Switching:" "Cyan" Write-ColorLine " ccs Use default Claude account" "Yellow" Write-ColorLine " ccs glm Switch to GLM 4.6 model" "Yellow" Write-ColorLine " ccs glmt Switch to GLM with thinking mode" "Yellow" + Write-ColorLine " ccs glmt --verbose Enable debug logging" "Yellow" Write-ColorLine " ccs kimi Switch to Kimi for Coding" "Yellow" Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow" Write-Host "" - Write-ColorLine "Examples:" "Cyan" - Write-ColorLine " `$ ccs" "Yellow" -NoNewline - Write-Host " # Use default account" - Write-ColorLine " `$ ccs glm `"implement API`"" "Yellow" -NoNewline - Write-Host " # Cost-optimized model" - Write-Host "" + Write-ColorLine "Account Management:" "Cyan" - Write-ColorLine " ccs auth --help Manage multiple Claude accounts" "Yellow" + Write-ColorLine " ccs auth --help Run multiple Claude accounts concurrently" "Yellow" Write-Host "" + Write-ColorLine "Delegation (inside Claude Code CLI):" "Cyan" Write-ColorLine " /ccs:glm `"task`" Delegate to GLM-4.6 for simple tasks" "Yellow" Write-ColorLine " /ccs:kimi `"task`" Delegate to Kimi for long context" "Yellow" Write-Host " Save tokens by delegating simple tasks to cost-optimized models" Write-Host "" + Write-ColorLine "Diagnostics:" "Cyan" Write-ColorLine " ccs doctor Run health check and diagnostics" "Yellow" + Write-ColorLine " ccs sync Sync delegation commands and skills" "Yellow" Write-Host "" + Write-ColorLine "Flags:" "Cyan" Write-ColorLine " -h, --help Show this help message" "Yellow" Write-ColorLine " -v, --version Show version and installation info" "Yellow" - Write-ColorLine " --shell-completion Install shell auto-completion" "Yellow" + Write-ColorLine " -sc, --shell-completion Install shell auto-completion" "Yellow" Write-Host "" + Write-ColorLine "Configuration:" "Cyan" - Write-Host " Config: ~/.ccs/config.json" - Write-Host " Profiles: ~/.ccs/profiles.json" - Write-Host " Instances: ~/.ccs/instances/" - Write-Host " Settings: ~/.ccs/*.settings.json" + Write-Host " Config File: ~/.ccs/config.json" + Write-Host " Profiles: ~/.ccs/profiles.json" + Write-Host " Instances: ~/.ccs/instances/" + Write-Host " Settings: ~/.ccs/*.settings.json" + Write-Host " Environment: CCS_CONFIG (override config path)" Write-Host "" + Write-ColorLine "Shared Data:" "Cyan" - Write-Host " Commands: ~/.ccs/shared/commands/" - Write-Host " Skills: ~/.ccs/shared/skills/" + Write-Host " Commands: ~/.ccs/shared/commands/" + Write-Host " Skills: ~/.ccs/shared/skills/" + Write-Host " Agents: ~/.ccs/shared/agents/" Write-Host " Note: Commands, skills, and agents are symlinked across all profiles" Write-Host "" + + Write-ColorLine "Examples:" "Cyan" + Write-ColorLine " `$ ccs # Use default account" "Yellow" + Write-ColorLine " `$ ccs glm `"implement API`" # Cost-optimized model" "Yellow" + Write-Host "" + Write-ColorLine " For more: https://github.com/kaitranntt/ccs/blob/main/README.md" "Cyan" + Write-Host "" + + Write-ColorLine "Uninstall:" "Yellow" + Write-Host " npm: npm uninstall -g @kaitranntt/ccs" + Write-Host " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash" + Write-Host " Windows: irm ccs.kaitran.ca/uninstall | iex" + Write-Host "" + Write-ColorLine "Documentation:" "Cyan" Write-Host " GitHub: https://github.com/kaitranntt/ccs" Write-Host " Docs: https://github.com/kaitranntt/ccs/blob/main/README.md" + Write-Host " Issues: https://github.com/kaitranntt/ccs/issues" Write-Host "" + Write-ColorLine "License: MIT" "Cyan" } function Show-Version { $UseColors = $env:FORCE_COLOR -or ([Console]::IsOutputRedirected -eq $false -and -not $env:NO_COLOR) + # Helper for aligned output + function Write-TableLine { + param( + [string]$Label, + [string]$Value, + [string]$Color = "Cyan" + ) + if ($UseColors) { + $PaddedLabel = $Label.PadRight(17) + Write-Host " $PaddedLabel " -ForegroundColor $Color -NoNewline + Write-Host $Value + } else { + $PaddedLabel = $Label.PadRight(17) + Write-Host " $PaddedLabel $Value" + } + } + # Title if ($UseColors) { Write-Host "CCS (Claude Code Switch) v$CcsVersion" -ForegroundColor White @@ -280,38 +319,91 @@ function Show-Version { } Write-Host "" - # Installation + # Installation section with table-like formatting if ($UseColors) { Write-Host "Installation:" -ForegroundColor Cyan } else { Write-Host "Installation:" } - # Location + # Location - prioritize script location over command location + $ScriptLocation = $MyInvocation.MyCommand.Path $InstallLocation = (Get-Command ccs -ErrorAction SilentlyContinue).Source - if ($InstallLocation) { - if ($UseColors) { - Write-Host " Location: " -ForegroundColor Cyan -NoNewline - Write-Host $InstallLocation - } else { - Write-Host " Location: $InstallLocation" - } + + # Show script location if running from source + if ($ScriptLocation -and (Test-Path $ScriptLocation)) { + Write-TableLine "Location:" $ScriptLocation + } elseif ($InstallLocation) { + Write-TableLine "Location:" $InstallLocation } else { - if ($UseColors) { - Write-Host " Location: " -ForegroundColor Cyan -NoNewline - Write-Host "(not found - run from current directory)" -ForegroundColor Gray - } else { - Write-Host " Location: (not found - run from current directory)" + Write-TableLine "Location:" "(not found - run from current directory)" + } + + # .ccs/ directory location + Write-TableLine "CCS Directory:" "$env:USERPROFILE\.ccs\" + + # Config path + Write-TableLine "Config:" $ConfigFile + + # Profiles.json location + Write-TableLine "Profiles:" $ProfilesJson + + # Delegation status - check multiple indicators + $DelegationConfigured = $false + $ReadyProfiles = @() + + # Check for delegation-sessions.json (primary indicator) + $DelegationSessions = "$env:USERPROFILE\.ccs\delegation-sessions.json" + if (Test-Path $DelegationSessions) { + $DelegationConfigured = $true + } + + # Check for profiles with valid API keys (secondary indicator) + foreach ($profile in @("glm", "kimi")) { + $SettingsFile = "$env:USERPROFILE\.ccs\$profile.settings.json" + if (Test-Path $SettingsFile) { + try { + $Settings = Get-Content $SettingsFile -Raw | ConvertFrom-Json + $ApiKey = $Settings.env.ANTHROPIC_AUTH_TOKEN + if ($ApiKey -and $ApiKey -notmatch "YOUR_.*_API_KEY_HERE" -and $ApiKey -notmatch "sk-test.*") { + $ReadyProfiles += $profile + $DelegationConfigured = $true + } + } catch { } } } - # Config - if ($UseColors) { - Write-Host " Config: " -ForegroundColor Cyan -NoNewline - Write-Host $ConfigFile + if ($DelegationConfigured) { + Write-TableLine "Delegation:" "Enabled" } else { - Write-Host " Config: $ConfigFile" + Write-TableLine "Delegation:" "Not configured" } Write-Host "" + # Ready Profiles section - make it more prominent + if ($ReadyProfiles.Count -gt 0) { + if ($UseColors) { Write-Host "Delegation Ready:" -ForegroundColor Cyan } + else { Write-Host "Delegation Ready:" } + + $ReadyProfilesStr = $ReadyProfiles -join ", " + if ($UseColors) { + Write-Host " ✓ " -ForegroundColor Yellow -NoNewline + Write-Host "$ReadyProfilesStr profiles are ready for delegation" + } else { + Write-Host " ! $ReadyProfilesStr profiles are ready for delegation" + } + Write-Host "" + } elseif ($DelegationConfigured) { + if ($UseColors) { Write-Host "Delegation Ready:" -ForegroundColor Cyan } + else { Write-Host "Delegation Ready:" } + + if ($UseColors) { + Write-Host " ! " -ForegroundColor Yellow -NoNewline + Write-Host "Delegation configured but no valid API keys found" + } else { + Write-Host " ! Delegation configured but no valid API keys found" + } + Write-Host "" + } + # Documentation if ($UseColors) { Write-Host "Documentation: https://github.com/kaitranntt/ccs" -ForegroundColor Cyan @@ -845,11 +937,105 @@ function Get-ProfileType { } } +# --- Sync Command --- + +function Sync-Run { + $CcsClaudeDir = "$env:USERPROFILE\.ccs\.claude" + $UserClaudeDir = "$env:USERPROFILE\.claude" + + Write-Host "Syncing delegation commands and skills to ~/.claude/..." -ForegroundColor Cyan + Write-Host "" + + # Check if source directory exists + if (-not (Test-Path $CcsClaudeDir)) { + Write-Host "[X] CCS .claude/ directory not found at $CcsClaudeDir" -ForegroundColor Red + Write-Host "Reinstall CCS: npm install -g @kaitranntt/ccs --force" + exit 1 + } + + # Create ~/.claude/ if missing + if (-not (Test-Path $UserClaudeDir)) { + Write-Host "[i] Creating ~/.claude/ directory" -ForegroundColor Cyan + New-Item -ItemType Directory -Path $UserClaudeDir -Force | Out-Null + } + + # Items to symlink + $Items = @( + @{ Source = "commands\ccs"; Target = "commands\ccs"; Type = "Directory" } + @{ Source = "skills\ccs-delegation"; Target = "skills\ccs-delegation"; Type = "Directory" } + @{ Source = "agents\ccs-delegator.md"; Target = "agents\ccs-delegator.md"; Type = "File" } + ) + + $Installed = 0 + $Skipped = 0 + + foreach ($Item in $Items) { + $SourcePath = Join-Path $CcsClaudeDir $Item.Source + $TargetPath = Join-Path $UserClaudeDir $Item.Target + $TargetDir = Split-Path -Parent $TargetPath + + # Check source exists + if (-not (Test-Path $SourcePath)) { + Write-Host "[!] Source not found: $($Item.Source), skipping" -ForegroundColor Yellow + continue + } + + # Create parent directory if needed + if (-not (Test-Path $TargetDir)) { + New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null + } + + # Check if already correct symlink + if (Test-Path $TargetPath) { + $ItemInfo = Get-Item $TargetPath -Force + if ($ItemInfo.LinkType -eq "SymbolicLink") { + $LinkTarget = $ItemInfo.Target + if ($LinkTarget -eq $SourcePath) { + $Skipped++ + continue + } + } + + # Backup existing file/directory + $Timestamp = Get-Date -Format "yyyy-MM-dd" + $BackupPath = "$TargetPath.backup-$Timestamp" + $Counter = 1 + + while (Test-Path $BackupPath) { + $BackupPath = "$TargetPath.backup-$Timestamp-$Counter" + $Counter++ + } + + Move-Item -Path $TargetPath -Destination $BackupPath -Force + Write-Host "[i] Backed up existing to $(Split-Path -Leaf $BackupPath)" -ForegroundColor Cyan + } + + # Create symlink + try { + $SymlinkType = if ($Item.Type -eq "Directory") { "Junction" } else { "SymbolicLink" } + New-Item -ItemType $SymlinkType -Path $TargetPath -Target $SourcePath -Force -ErrorAction Stop | Out-Null + Write-Host "[OK] Installed $($Item.Target)" -ForegroundColor Green + $Installed++ + } catch { + Write-Host "[X] Failed to install $($Item.Target): $($_.Exception.Message)" -ForegroundColor Red + if ($_.Exception.Message -match "privilege") { + Write-Host "[i] Run PowerShell as Administrator or enable Developer Mode" -ForegroundColor Yellow + } + } + } + + Write-Host "" + Write-Host "✓ Update complete" -ForegroundColor Green + Write-Host " Installed: $Installed" + Write-Host " Already up-to-date: $Skipped" + Write-Host "" +} + # --- Auth Commands (Phase 3) --- function Show-AuthHelp { Write-Host "" - Write-Host "CCS Account Management" -ForegroundColor White + Write-Host "CCS Concurrent Account Management" -ForegroundColor White Write-Host "" Write-Host "Usage:" -ForegroundColor Cyan Write-Host " ccs auth [options]" -ForegroundColor Yellow @@ -1314,7 +1500,7 @@ if ($Help) { } # Special case: shell completion installer -if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "--shell-completion") { +if ($RemainingArgs.Count -gt 0 -and ($RemainingArgs[0] -eq "--shell-completion" -or $RemainingArgs[0] -eq "-sc")) { $CompletionArgs = if ($RemainingArgs.Count -gt 1) { $RemainingArgs[1..($RemainingArgs.Count-1)] } else { @() } $Result = Install-ShellCompletion $CompletionArgs exit $Result @@ -1327,6 +1513,12 @@ if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "auth") { exit $LASTEXITCODE } +# Special case: sync command +if ($RemainingArgs.Count -gt 0 -and ($RemainingArgs[0] -eq "sync" -or $RemainingArgs[0] -eq "--sync")) { + Sync-Run + exit 0 +} + # Run auto-recovery before main logic if (-not (Invoke-AutoRecovery)) { Write-ErrorMsg "Auto-recovery failed. Check permissions." diff --git a/package.json b/package.json index c9114536..3a76e072 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "4.1.4", + "version": "4.1.5", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/scripts/completion/ccs.bash b/scripts/completion/ccs.bash index 39fbcd67..027791ae 100644 --- a/scripts/completion/ccs.bash +++ b/scripts/completion/ccs.bash @@ -18,8 +18,8 @@ _ccs_completion() { # Top-level completion (first argument) if [[ ${COMP_CWORD} -eq 1 ]]; then - local commands="auth doctor" - local flags="--help --version --shell-completion -h -v" + local commands="auth doctor sync" + local flags="--help --version --shell-completion -h -v -sc" local profiles="" # Add profiles from config.json (settings-based profiles) diff --git a/scripts/completion/ccs.fish b/scripts/completion/ccs.fish index c43e6dde..aab2eb70 100644 --- a/scripts/completion/ccs.fish +++ b/scripts/completion/ccs.fish @@ -70,29 +70,30 @@ complete -c ccs -f # Top-level flags complete -c ccs -s h -l help -d 'Show help message' complete -c ccs -s v -l version -d 'Show version information' -complete -c ccs -l shell-completion -d 'Install shell completion' +complete -c ccs -s sc -l shell-completion -d 'Install shell completion' # Top-level commands (blue color for commands) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'auth' -d (set_color blue)'Manage multiple Claude accounts'(set_color normal) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'doctor' -d (set_color blue)'Run health check and diagnostics'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'auth' -d (set_color blue)'Manage multiple Claude accounts'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'doctor' -d (set_color blue)'Run health check and diagnostics'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'sync' -d (set_color blue)'Sync delegation commands and skills'(set_color normal) # Top-level known settings profiles (green color for model profiles) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'default' -d (set_color green)'Default Claude Sonnet 4.5'(set_color normal) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'glm' -d (set_color green)'GLM-4.6 (cost-optimized)'(set_color normal) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'glmt' -d (set_color green)'GLM-4.6 with thinking mode'(set_color normal) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a 'kimi' -d (set_color green)'Kimi for Coding (long-context)'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'default' -d (set_color green)'Default Claude Sonnet 4.5'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'glm' -d (set_color green)'GLM-4.6 (cost-optimized)'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'glmt' -d (set_color green)'GLM-4.6 with thinking mode'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a 'kimi' -d (set_color green)'Kimi for Coding (long-context)'(set_color normal) # Top-level custom settings profiles (dynamic, with generic description in green) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a '(__fish_ccs_get_custom_settings_profiles)' -d (set_color green)'Settings-based profile'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a '(__fish_ccs_get_custom_settings_profiles)' -d (set_color green)'Settings-based profile'(set_color normal) # Top-level account profiles (dynamic, yellow color for account profiles) -complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor' -a '(__fish_ccs_get_account_profiles)' -d (set_color yellow)'Account profile'(set_color normal) +complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync' -a '(__fish_ccs_get_account_profiles)' -d (set_color yellow)'Account profile'(set_color normal) # shell-completion subflags -complete -c ccs -n '__fish_seen_argument -l shell-completion' -l bash -d 'Install for bash' -complete -c ccs -n '__fish_seen_argument -l shell-completion' -l zsh -d 'Install for zsh' -complete -c ccs -n '__fish_seen_argument -l shell-completion' -l fish -d 'Install for fish' -complete -c ccs -n '__fish_seen_argument -l shell-completion' -l powershell -d 'Install for PowerShell' +complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l bash -d 'Install for bash' +complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l zsh -d 'Install for zsh' +complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l fish -d 'Install for fish' +complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l powershell -d 'Install for PowerShell' # auth subcommands complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'create' -d 'Create new profile and login' diff --git a/scripts/completion/ccs.ps1 b/scripts/completion/ccs.ps1 index 75bd4646..1fec0d9f 100644 --- a/scripts/completion/ccs.ps1 +++ b/scripts/completion/ccs.ps1 @@ -12,7 +12,7 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock { param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters) - $commands = @('auth', 'doctor', '--help', '--version', '--shell-completion', '-h', '-v') + $commands = @('auth', 'doctor', 'sync', '--help', '--version', '--shell-completion', '-h', '-v', '-sc') $authCommands = @('create', 'list', 'show', 'remove', 'default', '--help', '-h') $shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell') $listFlags = @('--verbose', '--json') @@ -69,7 +69,7 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock { } # shell-completion flag completion - if ($words[1] -eq '--shell-completion') { + if ($words[1] -eq '--shell-completion' -or $words[1] -eq '-sc') { if ($position -eq 3) { $shellCompletionFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( diff --git a/scripts/completion/ccs.zsh b/scripts/completion/ccs.zsh index 3e1102e4..c31129a9 100644 --- a/scripts/completion/ccs.zsh +++ b/scripts/completion/ccs.zsh @@ -16,7 +16,7 @@ # Color codes: 0;34=blue, 0;32=green, 0;33=yellow, 2;37=dim white # Pattern format: =(#b)(group1)(group2)==color_for_group1=color_for_group2 # The leading '=' means no color for whole match, then each '=' assigns to each group -zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|doctor)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37' +zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|doctor|sync)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37' zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37' zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37' zstyle ':completion:*:*:ccs:*' group-name '' @@ -34,6 +34,7 @@ _ccs() { commands=( 'auth:Manage multiple Claude accounts' 'doctor:Run health check and diagnostics' + 'sync:Sync delegation commands and skills' ) # Define known settings profiles with descriptions (consistent padding) @@ -71,7 +72,7 @@ _ccs() { _arguments -C \ '(- *)'{-h,--help}'[Show help message]' \ '(- *)'{-v,--version}'[Show version information]' \ - '(- *)--shell-completion[Install shell completion]' \ + '(- *)'{-sc,--shell-completion}'[Install shell completion]' \ '1: :->command' \ '*:: :->args'