feat: implement v3.0.1 - fix silent postinstall failures with auto-recovery

- Add auto-recovery mechanisms for missing/corrupted configuration files
- Implement comprehensive health check command (`ccs doctor`)
- Enhance error messages with context-aware diagnostics and recovery commands
- Fix silent postinstall failures - now exits with proper error codes
- Add RecoveryManager class for automatic config restoration
- Add ErrorManager class for structured, helpful error messages
- Update postinstall script to validate created files and auto-create ~/.claude/settings.json
- Add doctor command support to bash and npm implementations
- Implement atomic file operations to prevent corruption
- Add comprehensive testing scenarios and validation

Fixes critical issue where npm install succeeded but CCS failed on first run.
Enhances user experience with automatic recovery and clear error guidance.

BREAKING CHANGE: Postinstall now exits with error code 1 on critical failures
This commit is contained in:
kaitranntt committed 2025-11-10 17:39:17 -05:00
1 parent f034c25a61
commit f65b87c123
13 files changed
+1198 -53

No files matched your search

+231 -8
View File
@@ -2,7 +2,7 @@
set -euo pipefail
# Version (updated by scripts/bump-version.sh)
CCS_VERSION="3.0.0"
CCS_VERSION="3.0.1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
@@ -44,14 +44,15 @@ show_help() {
echo -e " ${YELLOW}ccs${RESET} [flags]"
echo ""
echo -e "${CYAN}Description:${RESET}"
echo -e " Switch between Claude accounts and models instantly."
echo -e " Supports concurrent multi-account sessions."
echo -e " Switch between multiple Claude accounts (work, personal, team) and"
echo -e " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
echo -e " auto-recovery. Zero downtime."
echo ""
echo -e "${CYAN}Profile Switching:${RESET}"
echo -e " ${YELLOW}ccs${RESET} Use default profile"
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM profile"
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
echo -e " ${YELLOW}ccs work${RESET} \"debug code\" Run command with work account"
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 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 create <profile>${RESET} Create new account profile"
@@ -59,6 +60,12 @@ show_help() {
echo -e " ${YELLOW}ccs auth show <profile>${RESET} Show profile details"
echo -e " ${YELLOW}ccs auth remove <profile>${RESET} Remove profile (requires --force)"
echo -e " ${YELLOW}ccs auth default <profile>${RESET} Set default profile"
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account"
echo -e " ${YELLOW}ccs work${RESET} \"review code\" Run command with work account"
echo ""
echo -e "${CYAN}Diagnostics:${RESET}"
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
echo ""
echo -e "${CYAN}Flags:${RESET}"
echo -e " ${YELLOW}-h, --help${RESET} Show this help message"
@@ -94,6 +101,210 @@ command -v jq &>/dev/null || {
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
# Check Claude CLI
if command -v "$(detect_claude_cli)" &>/dev/null; then
doctor_check "Claude CLI" "success"
else
doctor_check "Claude CLI" "error" "Not found in PATH"
has_errors=true
fi
# Check ~/.ccs/
if [[ -d "$HOME/.ccs" ]]; then
doctor_check "CCS Directory" "success"
else
doctor_check "CCS Directory" "error" "~/.ccs/ not found"
has_errors=true
fi
# Check config.json
if [[ -f "$CONFIG_FILE" ]]; then
if jq empty "$CONFIG_FILE" 2>/dev/null; then
doctor_check "config.json" "success"
else
doctor_check "config.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "config.json" "error" "Not found"
has_errors=true
fi
# Check 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
doctor_check "glm.settings.json" "success"
else
doctor_check "glm.settings.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "glm.settings.json" "error" "Not found"
has_errors=true
fi
# Check 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
doctor_check "kimi.settings.json" "success"
else
doctor_check "kimi.settings.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "kimi.settings.json" "error" "Not found"
has_errors=true
fi
# Check ~/.claude/settings.json
if [[ -f "$HOME/.claude/settings.json" ]]; then
if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then
doctor_check "Claude Settings" "success"
else
doctor_check "Claude Settings" "warning" "Invalid JSON"
fi
else
doctor_check "Claude Settings" "warning" "Not found - run 'claude /login'"
fi
# Check profiles
if [[ -f "$CONFIG_FILE" ]]; then
local profile_count=$(jq -r '.profiles | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
doctor_check "Profiles" "success" "($profile_count configured)"
fi
# Check 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
doctor_check "Instances" "success" "($instance_count account profiles)"
else
doctor_check "Instances" "success" "(no account profiles)"
fi
# Check permissions
local test_file="$HOME/.ccs/.permission-test"
if echo "test" > "$test_file" 2>/dev/null; then
rm -f "$test_file" 2>/dev/null
doctor_check "Permissions" "success"
else
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
}
# --- Claude CLI Detection Logic ---
detect_claude_cli() {
@@ -727,6 +938,18 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then
exit $?
fi
# Special case: doctor command
if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then
doctor_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
+94 -8
View File
@@ -12,7 +12,7 @@ param(
$ErrorActionPreference = "Stop"
# Version (updated by scripts/bump-version.sh)
$CcsVersion = "3.0.0"
$CcsVersion = "3.0.1"
$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"
@@ -100,14 +100,15 @@ function Show-Help {
Write-ColorLine " ccs [flags]" "Yellow"
Write-Host ""
Write-ColorLine "Description:" "Cyan"
Write-Host " Switch between Claude accounts and models instantly."
Write-Host " Supports concurrent multi-account sessions."
Write-Host " Switch between multiple Claude accounts (work, personal, team) and"
Write-Host " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
Write-Host " auto-recovery. Zero downtime."
Write-Host ""
Write-ColorLine "Profile Switching:" "Cyan"
Write-ColorLine " ccs Use default profile" "Yellow"
Write-ColorLine " ccs glm Switch to GLM profile" "Yellow"
Write-ColorLine " ccs work Switch to work account" "Yellow"
Write-ColorLine " ccs work 'debug code' Run command with work account" "Yellow"
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 kimi Switch to Kimi for Coding" "Yellow"
Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow"
Write-Host ""
Write-ColorLine "Account Management:" "Cyan"
Write-ColorLine " ccs auth create <profile> Create new account profile" "Yellow"
@@ -115,6 +116,12 @@ function Show-Help {
Write-ColorLine " ccs auth show <profile> Show profile details" "Yellow"
Write-ColorLine " ccs auth remove <profile> Remove profile (requires --force)" "Yellow"
Write-ColorLine " ccs auth default <profile> Set default profile" "Yellow"
Write-ColorLine " ccs work Switch to work account" "Yellow"
Write-ColorLine " ccs personal Switch to personal account" "Yellow"
Write-ColorLine " ccs work 'review code' Run command with work account" "Yellow"
Write-Host ""
Write-ColorLine "Diagnostics:" "Cyan"
Write-ColorLine " ccs doctor Run health check and diagnostics" "Yellow"
Write-Host ""
Write-ColorLine "Flags:" "Cyan"
Write-ColorLine " -h, --help Show this help message" "Yellow"
@@ -202,6 +209,79 @@ function Show-Version {
}
}
# --- Auto-Recovery Functions ---
function Ensure-CcsDirectory {
if (Test-Path "$env:USERPROFILE\.ccs") { return $true }
try {
New-Item -ItemType Directory -Path "$env:USERPROFILE\.ccs" -Force | Out-Null
Write-Host "[i] Auto-recovery: Created ~/.ccs/ directory"
return $true
} catch {
Write-ErrorMsg "Cannot create ~/.ccs/ directory. Check permissions."
return $false
}
}
function Ensure-ConfigJson {
$ConfigFile = "$env:USERPROFILE\.ccs\config.json"
# Check if exists and valid
if (Test-Path $ConfigFile) {
try {
Get-Content $ConfigFile -Raw | ConvertFrom-Json | Out-Null
return $true
} catch {
# Corrupted - backup and recreate
$BackupFile = "$ConfigFile.backup.$(Get-Date -Format 'yyyyMMddHHmmss')"
Move-Item $ConfigFile $BackupFile -Force
Write-Host "[i] Auto-recovery: Backed up corrupted config.json"
}
}
# Create default config
$DefaultConfig = @{
profiles = @{
glm = "~/.ccs/glm.settings.json"
kimi = "~/.ccs/kimi.settings.json"
default = "~/.claude/settings.json"
}
}
$DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
Write-Host "[i] Auto-recovery: Created ~/.ccs/config.json"
return $true
}
function Ensure-ClaudeSettings {
$ClaudeDir = "$env:USERPROFILE\.claude"
$SettingsFile = "$ClaudeDir\settings.json"
# Create ~/.claude/ if missing
if (-not (Test-Path $ClaudeDir)) {
New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
Write-Host "[i] Auto-recovery: Created ~/.claude/ directory"
}
# Create settings.json if missing
if (-not (Test-Path $SettingsFile)) {
'{}' | Set-Content $SettingsFile
Write-Host "[i] Auto-recovery: Created ~/.claude/settings.json"
Write-Host "[i] Next step: Run 'claude /login' to authenticate"
return $true
}
return $false
}
function Invoke-AutoRecovery {
if (-not (Ensure-CcsDirectory)) { return $false }
if (-not (Ensure-ConfigJson)) { return $false }
Ensure-ClaudeSettings | Out-Null
return $true
}
# --- Profile Registry Functions (Phase 4) ---
function Initialize-ProfilesJson {
@@ -830,6 +910,12 @@ if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "auth") {
exit $LASTEXITCODE
}
# Run auto-recovery before main logic
if (-not (Invoke-AutoRecovery)) {
Write-ErrorMsg "Auto-recovery failed. Check permissions."
exit 1
}
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
# No args or first arg is a flag → use default profile