Add Charm CLI (gum) support for premium installer experience

- Detect gum and offer to install for enhanced UX
- Add wrapper functions with graceful bash fallback:
  - gum_choose: Interactive option selection
  - gum_confirm: Yes/no prompts
  - gum_input: Text input
  - gum_password: Hidden input
  - gum_spin: Animated spinners
  - gum_style_box: Styled boxes
- Works perfectly without gum, beautiful with it
- Supports macOS (Homebrew), Debian/Ubuntu (apt), and Go install

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Bob Roberts 2025-12-05 21:04:45 -06:00
parent e98ecbcf14
commit 8e2c4cc1d0

View file

@ -46,6 +46,161 @@ BOX_BR="╯"
BOX_H="─"
BOX_V="│"
# ═══════════════════════════════════════════════════════════════════════════
# GUM DETECTION & SETUP
# ═══════════════════════════════════════════════════════════════════════════
# Gum (https://github.com/charmbracelet/gum) provides beautiful interactive
# prompts. If available, we use it. Otherwise, we fall back to bash.
HAS_GUM=false
if command -v gum &> /dev/null; then
HAS_GUM=true
fi
# Offer to install gum for enhanced experience
offer_gum_install() {
if [[ "$HAS_GUM" == "true" ]]; then
return
fi
echo ""
echo -e "${CYAN}┌────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}│${NC} ${WHITE}✨ Enhanced installer available!${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${DIM}Install 'gum' for a more beautiful experience with${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${DIM}interactive menus and smooth animations.${NC} ${CYAN}│${NC}"
echo -e "${CYAN}└────────────────────────────────────────────────────────────────┘${NC}"
echo ""
echo -ne " ${WHITE}Install gum? [y/N]:${NC} "
read -r install_gum
if [[ "$install_gum" =~ ^[Yy]$ ]]; then
echo ""
info "Installing gum..."
if [[ "$OS" == "macos" ]]; then
brew install gum &>/dev/null &
spinner $! "Installing gum via Homebrew..."
elif [[ "$OS" == "linux" ]]; then
# Try various package managers
if [[ "$PKG_MANAGER" == "apt" ]]; then
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list > /dev/null
sudo apt update -qq && sudo apt install -y -qq gum &>/dev/null &
spinner $! "Installing gum via apt..."
elif command -v go &> /dev/null; then
go install github.com/charmbracelet/gum@latest &>/dev/null &
spinner $! "Installing gum via go..."
else
warn "Could not auto-install gum. Install manually: https://github.com/charmbracelet/gum"
return
fi
fi
if command -v gum &> /dev/null; then
HAS_GUM=true
success "Gum installed! You'll get the premium experience ✨"
else
warn "Gum installation may have failed. Continuing with standard installer."
fi
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# GUM WRAPPER FUNCTIONS (with bash fallback)
# ═══════════════════════════════════════════════════════════════════════════
# Choose from a list of options
# Usage: choice=$(gum_choose "Option 1" "Option 2" "Option 3")
gum_choose() {
if [[ "$HAS_GUM" == "true" ]]; then
gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" "$@"
else
# Bash fallback
local options=("$@")
local i=1
for opt in "${options[@]}"; do
echo -e " ${CYAN}[$i]${NC} $opt"
((i++))
done
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-${#options[@]}]:${NC} "
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then
echo "${options[$((choice-1))]}"
return
fi
echo -e " ${RED}Please enter a number between 1 and ${#options[@]}${NC}"
done
fi
}
# Confirm yes/no
# Usage: if gum_confirm "Are you sure?"; then ...
gum_confirm() {
local prompt="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum confirm --prompt.foreground="#FFFFFF" "$prompt"
else
echo -ne " ${WHITE}$prompt [y/N]:${NC} "
read -r response
[[ "$response" =~ ^[Yy]$ ]]
fi
}
# Get text input
# Usage: value=$(gum_input "Enter value" "placeholder")
gum_input() {
local prompt="$1"
local placeholder="$2"
if [[ "$HAS_GUM" == "true" ]]; then
gum input --placeholder "$placeholder" --prompt "$prompt: " --prompt.foreground="#00FFFF"
else
echo -ne " ${WHITE}$prompt:${NC} "
read -r value
echo "$value"
fi
}
# Get password input (hidden)
# Usage: value=$(gum_password "Enter password")
gum_password() {
local prompt="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum input --password --placeholder "••••••••••••••••" --prompt "$prompt: " --prompt.foreground="#00FFFF"
else
echo -ne " ${WHITE}$prompt:${NC} "
read -rs value
echo ""
echo "$value"
fi
}
# Show a spinner while a command runs
# Usage: gum_spin "Message" command arg1 arg2
gum_spin() {
local message="$1"
shift
if [[ "$HAS_GUM" == "true" ]]; then
gum spin --spinner dot --title "$message" -- "$@"
else
"$@" &
spinner $! "$message"
fi
}
# Style text in a box
gum_style_box() {
local text="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" "$text"
else
print_box "$text"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════
@ -174,32 +329,60 @@ EOF
# ═══════════════════════════════════════════════════════════════════════════
select_provider() {
print_box "Choose Your Setup"
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "How would you like to power Claude OS?")"
echo ""
echo -e " ${WHITE}How would you like to power Claude OS?${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}🏠 Local (Ollama)${NC}"
echo -e " ${DIM}Free, private, runs on your machine${NC}"
echo -e " ${DIM}Best for: Privacy-focused users, offline use${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}☁️ Cloud (OpenAI)${NC}"
echo -e " ${DIM}Fast, no local resources needed${NC}"
echo -e " ${DIM}Best for: Quick setup, Linux servers${NC}"
echo ""
echo -e " ${CYAN}[3]${NC} ${MAGENTA}🔧 Custom${NC}"
echo -e " ${DIM}I'll configure it myself${NC}"
echo ""
local choice=$(gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" \
"🏠 Local (Ollama) - Free, private, runs on your machine" \
"☁️ Cloud (OpenAI) - Fast, no local resources needed" \
"🔧 Custom - I'll configure it myself")
while true; do
echo -ne " ${WHITE}Enter choice [1-3]:${NC} "
read -r choice
case $choice in
1) PROVIDER="local"; break ;;
2) PROVIDER="openai"; break ;;
3) PROVIDER="custom"; break ;;
*) echo -e " ${RED}Please enter 1, 2, or 3${NC}" ;;
case "$choice" in
*"Local"*) PROVIDER="local" ;;
*"Cloud"*) PROVIDER="openai" ;;
*"Custom"*) PROVIDER="custom" ;;
esac
done
else
# Bash fallback
print_box "Choose Your Setup"
echo -e " ${WHITE}How would you like to power Claude OS?${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}🏠 Local (Ollama)${NC}"
echo -e " ${DIM}Free, private, runs on your machine${NC}"
echo -e " ${DIM}Best for: Privacy-focused users, offline use${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}☁️ Cloud (OpenAI)${NC}"
echo -e " ${DIM}Fast, no local resources needed${NC}"
echo -e " ${DIM}Best for: Quick setup, Linux servers${NC}"
echo ""
echo -e " ${CYAN}[3]${NC} ${MAGENTA}🔧 Custom${NC}"
echo -e " ${DIM}I'll configure it myself${NC}"
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-3]:${NC} "
read -r choice
case $choice in
1) PROVIDER="local"; break ;;
2) PROVIDER="openai"; break ;;
3) PROVIDER="custom"; break ;;
*) echo -e " ${RED}Please enter 1, 2, or 3${NC}" ;;
esac
done
fi
# Show selection confirmation
echo ""
case "$PROVIDER" in
local) success "Selected: Local (Ollama)" ;;
openai) success "Selected: Cloud (OpenAI)" ;;
custom) success "Selected: Custom configuration" ;;
esac
}
# ═══════════════════════════════════════════════════════════════════════════
@ -207,30 +390,52 @@ select_provider() {
# ═══════════════════════════════════════════════════════════════════════════
select_model_size() {
print_box "Choose Model Size"
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "Choose your model size")"
echo ""
echo -e " ${WHITE}Select your local model:${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}💨 Lite${NC} ${DIM}(Recommended)${NC}"
echo -e " ${WHITE}llama3.2:3b${NC} - 2GB download, ~4GB RAM"
echo -e " ${DIM}Fast download, works on most machines${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}🚀 Full${NC}"
echo -e " ${WHITE}llama3.1:8b${NC} - 4.7GB download, ~8GB RAM"
echo -e " ${DIM}Better quality, needs more resources${NC}"
echo ""
local choice=$(gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" \
"💨 Lite (Recommended) - llama3.2:3b - 2GB download, ~4GB RAM" \
"🚀 Full - llama3.1:8b - 4.7GB download, ~8GB RAM")
while true; do
echo -ne " ${WHITE}Enter choice [1-2]:${NC} "
read -r choice
case $choice in
1) LLM_MODEL="$DEFAULT_LLM_MODEL"; break ;;
2) LLM_MODEL="$FULL_LLM_MODEL"; break ;;
*) echo -e " ${RED}Please enter 1 or 2${NC}" ;;
case "$choice" in
*"Lite"*) LLM_MODEL="$DEFAULT_LLM_MODEL" ;;
*"Full"*) LLM_MODEL="$FULL_LLM_MODEL" ;;
esac
done
else
# Bash fallback
print_box "Choose Model Size"
echo -e " ${WHITE}Select your local model:${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}💨 Lite${NC} ${DIM}(Recommended)${NC}"
echo -e " ${WHITE}llama3.2:3b${NC} - 2GB download, ~4GB RAM"
echo -e " ${DIM}Fast download, works on most machines${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}🚀 Full${NC}"
echo -e " ${WHITE}llama3.1:8b${NC} - 4.7GB download, ~8GB RAM"
echo -e " ${DIM}Better quality, needs more resources${NC}"
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-2]:${NC} "
read -r choice
case $choice in
1) LLM_MODEL="$DEFAULT_LLM_MODEL"; break ;;
2) LLM_MODEL="$FULL_LLM_MODEL"; break ;;
*) echo -e " ${RED}Please enter 1 or 2${NC}" ;;
esac
done
fi
EMBED_MODEL="$DEFAULT_EMBED_MODEL"
# Show selection confirmation
echo ""
success "Selected: $LLM_MODEL"
}
# ═══════════════════════════════════════════════════════════════════════════
@ -238,14 +443,27 @@ select_model_size() {
# ═══════════════════════════════════════════════════════════════════════════
configure_openai() {
print_box "OpenAI Configuration"
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "OpenAI Configuration")"
echo ""
echo -e " ${DIM}Get your API key at https://platform.openai.com/api-keys${NC}"
echo ""
echo -e " ${WHITE}Enter your OpenAI API key:${NC}"
echo -e " ${DIM}(Get one at https://platform.openai.com/api-keys)${NC}"
echo ""
echo -ne " ${WHITE}API Key:${NC} "
read -rs OPENAI_API_KEY
echo ""
OPENAI_API_KEY=$(gum input --password --placeholder "sk-..." --prompt "API Key: " --prompt.foreground="#00FFFF")
else
# Bash fallback
print_box "OpenAI Configuration"
echo -e " ${WHITE}Enter your OpenAI API key:${NC}"
echo -e " ${DIM}(Get one at https://platform.openai.com/api-keys)${NC}"
echo ""
echo -ne " ${WHITE}API Key:${NC} "
read -rs OPENAI_API_KEY
echo ""
fi
if [[ -z "$OPENAI_API_KEY" ]]; then
error "API key cannot be empty"
@ -586,16 +804,24 @@ EOF
show_completion() {
echo ""
echo -e "${GREEN}"
cat << 'EOF'
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum completion
gum style --border double --padding "1 4" --border-foreground "#00FF00" --foreground "#00FF00" \
"✨ Claude OS is ready! ✨"
else
echo -e "${GREEN}"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ ✨ Claude OS is ready! ✨ ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
echo -e "${NC}"
fi
echo ""
echo -e " ${WHITE}What was set up:${NC}"
echo ""
[[ -d "${CLAUDE_OS_DIR}/venv" ]] && echo -e " ${GREEN}✓${NC} Python environment"
@ -641,6 +867,9 @@ main() {
# Show banner
show_banner
# Offer to install gum for enhanced experience
offer_gum_install
# Provider selection
select_provider