fix: improve install scripts robustness and edge cases

install.sh:
- Fix NodeSource GPG key overwrite issue (rm before dearmor)
- Fix systemd ExecStart missing node binary prefix
- Add SUSE/openSUSE support (zypper-based)
- Add local changes check before git reset --hard
- Add curl/wget detection with fallback support
- Use shallow clone (--depth 1) for faster install
- Update Node.js target to v22 (current LTS)
- Add CLAUDEMAN_NODE_VERSION env var for customization
- Add run_as_root/ensure_sudo helpers for consistent privilege handling
- Add cleanup trap with helpful failure message
- Add Windows detection with WSL guidance
- Fix PATH substring check (was using grep -qx incorrectly)
- Add stdin TTY check for non-interactive piped input
- Update Claude CLI install to official curl installer
- Add armv7 architecture support (Raspberry Pi)
- Quieter apt/dnf output with -qq flags

scripts/postinstall.js:
- Fix command detection for Windows (where vs command -v)
- Add Fedora/Arch/Alpine install instructions for screen
- Add Windows/WSL guidance
- Show Claude CLI path when found
- Refactor with clear sections and helper functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-26 03:48:03 +01:00
co-authored by Claude Opus 4.5
parent 34e32ad999
commit 7b2440a49d
2 changed files with 466 additions and 177 deletions
+305 -125
View File
@@ -8,6 +8,7 @@
# CLAUDEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation) # CLAUDEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation)
# CLAUDEMAN_INSTALL_DIR - Custom install directory (default: ~/.claudeman/app) # CLAUDEMAN_INSTALL_DIR - Custom install directory (default: ~/.claudeman/app)
# CLAUDEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt # CLAUDEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt
# CLAUDEMAN_NODE_VERSION - Node.js major version to install (default: 22)
set -euo pipefail set -euo pipefail
@@ -18,6 +19,7 @@ set -euo pipefail
INSTALL_DIR="${CLAUDEMAN_INSTALL_DIR:-$HOME/.claudeman/app}" INSTALL_DIR="${CLAUDEMAN_INSTALL_DIR:-$HOME/.claudeman/app}"
REPO_URL="https://github.com/Ark0N/claudeman.git" REPO_URL="https://github.com/Ark0N/claudeman.git"
MIN_NODE_VERSION=18 MIN_NODE_VERSION=18
TARGET_NODE_VERSION="${CLAUDEMAN_NODE_VERSION:-22}"
NONINTERACTIVE="${CLAUDEMAN_NONINTERACTIVE:-0}" NONINTERACTIVE="${CLAUDEMAN_NONINTERACTIVE:-0}"
SKIP_SYSTEMD="${CLAUDEMAN_SKIP_SYSTEMD:-0}" SKIP_SYSTEMD="${CLAUDEMAN_SKIP_SYSTEMD:-0}"
@@ -34,25 +36,29 @@ CLAUDE_SEARCH_PATHS=(
# Color Output (from scripts/screen-manager.sh pattern) # Color Output (from scripts/screen-manager.sh pattern)
# ============================================================================ # ============================================================================
# Check if terminal supports colors setup_colors() {
if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && command -v tput &>/dev/null; then # Check if terminal supports colors
ncolors=$(tput colors 2>/dev/null || echo 0) if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && command -v tput &>/dev/null; then
if [[ "$ncolors" -ge 8 ]]; then local ncolors
RED='\033[0;31m' ncolors=$(tput colors 2>/dev/null || echo 0)
GREEN='\033[0;32m' if [[ "$ncolors" -ge 8 ]]; then
YELLOW='\033[1;33m' RED='\033[0;31m'
BLUE='\033[0;34m' GREEN='\033[0;32m'
CYAN='\033[0;36m' YELLOW='\033[1;33m'
MAGENTA='\033[0;35m' BLUE='\033[0;34m'
BOLD='\033[1m' CYAN='\033[0;36m'
DIM='\033[2m' MAGENTA='\033[0;35m'
NC='\033[0m' BOLD='\033[1m'
else DIM='\033[2m'
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC='' NC='\033[0m'
return
fi
fi fi
else # No color support
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC='' RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC=''
fi }
setup_colors
# ============================================================================ # ============================================================================
# Output Helpers # Output Helpers
@@ -67,7 +73,7 @@ success() {
} }
warn() { warn() {
echo -e "${YELLOW}Warning:${NC} $1" echo -e "${YELLOW}Warning:${NC} $1" >&2
} }
error() { error() {
@@ -79,6 +85,20 @@ die() {
exit 1 exit 1
} }
# ============================================================================
# Cleanup on Failure
# ============================================================================
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
error "Installation failed. Partial installation may remain at $INSTALL_DIR"
error "To retry, run the installer again or remove the directory manually."
fi
}
trap cleanup EXIT
# ============================================================================ # ============================================================================
# System Detection # System Detection
# ============================================================================ # ============================================================================
@@ -89,6 +109,9 @@ detect_os() {
case "$os" in case "$os" in
Darwin) echo "macos" ;; Darwin) echo "macos" ;;
Linux) echo "linux" ;; Linux) echo "linux" ;;
MINGW*|MSYS*|CYGWIN*)
die "Windows is not supported directly. Please use WSL (Windows Subsystem for Linux)."
;;
*) die "Unsupported operating system: $os" ;; *) die "Unsupported operating system: $os" ;;
esac esac
} }
@@ -99,13 +122,25 @@ detect_arch() {
case "$arch" in case "$arch" in
x86_64|amd64) echo "x64" ;; x86_64|amd64) echo "x64" ;;
aarch64|arm64) echo "arm64" ;; aarch64|arm64) echo "arm64" ;;
armv7l) echo "armv7" ;;
*) die "Unsupported architecture: $arch" ;; *) die "Unsupported architecture: $arch" ;;
esac esac
} }
detect_linux_distro() { detect_linux_distro() {
if [[ ! -f /etc/os-release ]]; then if [[ ! -f /etc/os-release ]]; then
echo "unknown" # Fallback detection for older systems
if [[ -f /etc/debian_version ]]; then
echo "debian"
elif [[ -f /etc/redhat-release ]]; then
echo "fedora"
elif [[ -f /etc/arch-release ]]; then
echo "arch"
elif [[ -f /etc/alpine-release ]]; then
echo "alpine"
else
echo "unknown"
fi
return return
fi fi
@@ -114,16 +149,16 @@ detect_linux_distro() {
source /etc/os-release source /etc/os-release
case "${ID:-}" in case "${ID:-}" in
debian|ubuntu|linuxmint|pop|elementary|zorin|kali) debian|ubuntu|linuxmint|pop|elementary|zorin|kali|raspbian)
echo "debian" echo "debian"
;; ;;
fedora|rhel|centos|rocky|alma|ol) fedora|rhel|centos|rocky|alma|ol|amzn)
echo "fedora" echo "fedora"
;; ;;
arch|manjaro|endeavouros|garuda) arch|manjaro|endeavouros|garuda|artix)
echo "arch" echo "arch"
;; ;;
opensuse*|suse*) opensuse*|sles|suse)
echo "suse" echo "suse"
;; ;;
alpine) alpine)
@@ -142,6 +177,42 @@ detect_linux_distro() {
esac esac
} }
# ============================================================================
# Prerequisite Checks
# ============================================================================
check_curl_or_wget() {
if command -v curl &>/dev/null; then
DOWNLOADER="curl"
return 0
elif command -v wget &>/dev/null; then
DOWNLOADER="wget"
return 0
fi
return 1
}
download() {
local url="$1"
local output="$2"
if [[ "$DOWNLOADER" == "curl" ]]; then
curl -fsSL "$url" -o "$output"
else
wget -q "$url" -O "$output"
fi
}
download_to_stdout() {
local url="$1"
if [[ "$DOWNLOADER" == "curl" ]]; then
curl -fsSL "$url"
else
wget -qO- "$url"
fi
}
# ============================================================================ # ============================================================================
# Dependency Checks # Dependency Checks
# ============================================================================ # ============================================================================
@@ -206,16 +277,39 @@ get_claude_path() {
# Dependency Installation # Dependency Installation
# ============================================================================ # ============================================================================
ensure_sudo() {
if [[ $EUID -eq 0 ]]; then
return 0
fi
if ! command -v sudo &>/dev/null; then
die "sudo is required but not installed. Please install packages manually or run as root."
fi
# Validate sudo access
if ! sudo -v 2>/dev/null; then
die "Failed to obtain sudo privileges."
fi
}
run_as_root() {
if [[ $EUID -eq 0 ]]; then
"$@"
else
sudo "$@"
fi
}
install_node_macos() { install_node_macos() {
info "Installing Node.js via Homebrew..." info "Installing Node.js via Homebrew..."
if ! command -v brew &>/dev/null; then if ! command -v brew &>/dev/null; then
info "Installing Homebrew first..." info "Installing Homebrew first..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" /bin/bash -c "$(download_to_stdout https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for Apple Silicon # Add Homebrew to PATH for Apple Silicon
if [[ -f /opt/homebrew/bin/brew ]]; then if [[ -f /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)" eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -f /usr/local/bin/brew ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi fi
fi fi
@@ -223,89 +317,120 @@ install_node_macos() {
} }
install_node_debian() { install_node_debian() {
info "Installing Node.js via NodeSource..." info "Installing Node.js v$TARGET_NODE_VERSION via NodeSource..."
# Check if we can use sudo ensure_sudo
if ! command -v sudo &>/dev/null; then
die "sudo is required to install Node.js. Please install sudo or Node.js manually."
fi
# Install prerequisites # Install prerequisites
sudo apt-get update run_as_root apt-get update -qq
sudo apt-get install -y ca-certificates curl gnupg run_as_root apt-get install -y -qq ca-certificates curl gnupg
# Add NodeSource repository # Setup NodeSource repository (new method)
sudo mkdir -p /etc/apt/keyrings run_as_root mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20 # Remove old key if exists to avoid conflicts
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list run_as_root rm -f /etc/apt/keyrings/nodesource.gpg
sudo apt-get update download_to_stdout https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | run_as_root gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
sudo apt-get install -y nodejs
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$TARGET_NODE_VERSION.x nodistro main" | run_as_root tee /etc/apt/sources.list.d/nodesource.list > /dev/null
run_as_root apt-get update -qq
run_as_root apt-get install -y -qq nodejs
} }
install_node_fedora() { install_node_fedora() {
info "Installing Node.js via dnf..." info "Installing Node.js v$TARGET_NODE_VERSION via NodeSource..."
if ! command -v sudo &>/dev/null; then ensure_sudo
die "sudo is required to install Node.js. Please install sudo or Node.js manually."
fi
sudo dnf install -y nodejs npm # Use NodeSource for consistent version across Fedora versions
download_to_stdout "https://rpm.nodesource.com/setup_$TARGET_NODE_VERSION.x" | run_as_root bash -
run_as_root dnf install -y nodejs
} }
install_node_arch() { install_node_arch() {
info "Installing Node.js via pacman..." info "Installing Node.js via pacman..."
if ! command -v sudo &>/dev/null; then ensure_sudo
die "sudo is required to install Node.js. Please install sudo or Node.js manually." run_as_root pacman -Sy --noconfirm nodejs npm
fi
sudo pacman -Sy --noconfirm nodejs npm # Verify version is sufficient
local version
version=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
if [[ "$version" -lt "$MIN_NODE_VERSION" ]]; then
warn "Arch package nodejs is v$version, which is older than required v$MIN_NODE_VERSION"
warn "Consider using nvm or the nodejs-lts-* package instead"
fi
} }
install_node_alpine() { install_node_alpine() {
info "Installing Node.js via apk..." info "Installing Node.js via apk..."
if [[ $EUID -eq 0 ]]; then run_as_root apk add --no-cache nodejs npm
apk add --no-cache nodejs npm
else # Verify version
if ! command -v sudo &>/dev/null; then local version
die "sudo is required to install Node.js. Please install sudo or Node.js manually." version=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
fi if [[ "$version" -lt "$MIN_NODE_VERSION" ]]; then
sudo apk add --no-cache nodejs npm warn "Alpine package nodejs is v$version, which is older than required v$MIN_NODE_VERSION"
warn "Consider using a newer Alpine version or building from source"
fi fi
} }
install_node_suse() {
info "Installing Node.js v$TARGET_NODE_VERSION via NodeSource..."
ensure_sudo
# Use NodeSource for openSUSE
download_to_stdout "https://rpm.nodesource.com/setup_$TARGET_NODE_VERSION.x" | run_as_root bash -
run_as_root zypper install -y nodejs
}
install_screen_macos() { install_screen_macos() {
info "Installing GNU Screen via Homebrew..." info "Installing GNU Screen via Homebrew..."
# macOS has a built-in screen but it's very outdated
if command -v screen &>/dev/null; then
local builtin_version
builtin_version=$(screen --version 2>&1 | head -1 || echo "unknown")
if [[ "$builtin_version" == *"Apple"* ]] || [[ ! "$builtin_version" == *"GNU"* ]]; then
info "Upgrading from macOS built-in screen to GNU Screen..."
fi
fi
brew install screen brew install screen
} }
install_screen_debian() { install_screen_debian() {
info "Installing GNU Screen via apt..." info "Installing GNU Screen via apt..."
sudo apt-get update ensure_sudo
sudo apt-get install -y screen run_as_root apt-get update -qq
run_as_root apt-get install -y -qq screen
} }
install_screen_fedora() { install_screen_fedora() {
info "Installing GNU Screen via dnf..." info "Installing GNU Screen via dnf..."
sudo dnf install -y screen ensure_sudo
run_as_root dnf install -y screen
} }
install_screen_arch() { install_screen_arch() {
info "Installing GNU Screen via pacman..." info "Installing GNU Screen via pacman..."
sudo pacman -Sy --noconfirm screen ensure_sudo
run_as_root pacman -Sy --noconfirm screen
} }
install_screen_alpine() { install_screen_alpine() {
info "Installing GNU Screen via apk..." info "Installing GNU Screen via apk..."
if [[ $EUID -eq 0 ]]; then run_as_root apk add --no-cache screen
apk add --no-cache screen }
else
sudo apk add --no-cache screen install_screen_suse() {
fi info "Installing GNU Screen via zypper..."
ensure_sudo
run_as_root zypper install -y screen
} }
install_git_macos() { install_git_macos() {
@@ -315,27 +440,32 @@ install_git_macos() {
install_git_debian() { install_git_debian() {
info "Installing Git via apt..." info "Installing Git via apt..."
sudo apt-get update ensure_sudo
sudo apt-get install -y git run_as_root apt-get update -qq
run_as_root apt-get install -y -qq git
} }
install_git_fedora() { install_git_fedora() {
info "Installing Git via dnf..." info "Installing Git via dnf..."
sudo dnf install -y git ensure_sudo
run_as_root dnf install -y git
} }
install_git_arch() { install_git_arch() {
info "Installing Git via pacman..." info "Installing Git via pacman..."
sudo pacman -Sy --noconfirm git ensure_sudo
run_as_root pacman -Sy --noconfirm git
} }
install_git_alpine() { install_git_alpine() {
info "Installing Git via apk..." info "Installing Git via apk..."
if [[ $EUID -eq 0 ]]; then run_as_root apk add --no-cache git
apk add --no-cache git }
else
sudo apk add --no-cache git install_git_suse() {
fi info "Installing Git via zypper..."
ensure_sudo
run_as_root zypper install -y git
} }
# ============================================================================ # ============================================================================
@@ -351,6 +481,13 @@ prompt_yes_no() {
return return
fi fi
# Check if stdin is a terminal
if [[ ! -t 0 ]]; then
# Non-interactive, use default
[[ "$default" == "y" ]]
return
fi
local yn_hint local yn_hint
if [[ "$default" == "y" ]]; then if [[ "$default" == "y" ]]; then
yn_hint="[Y/n]" yn_hint="[Y/n]"
@@ -359,13 +496,13 @@ prompt_yes_no() {
fi fi
while true; do while true; do
echo -en "${CYAN}$prompt${NC} $yn_hint " echo -en "${CYAN}$prompt${NC} $yn_hint " >&2
read -r answer read -r answer
answer="${answer:-$default}" answer="${answer:-$default}"
case "$answer" in case "$answer" in
[Yy]|[Yy][Ee][Ss]) return 0 ;; [Yy]|[Yy][Ee][Ss]) return 0 ;;
[Nn]|[Nn][Oo]) return 1 ;; [Nn]|[Nn][Oo]) return 1 ;;
*) echo "Please answer yes or no." ;; *) echo "Please answer yes or no." >&2 ;;
esac esac
done done
} }
@@ -387,12 +524,21 @@ detect_shell_profile() {
fi fi
;; ;;
bash) bash)
if [[ -f "$HOME/.bashrc" ]]; then # macOS uses .bash_profile, Linux typically uses .bashrc
echo "$HOME/.bashrc" if [[ "$(uname -s)" == "Darwin" ]]; then
elif [[ -f "$HOME/.bash_profile" ]]; then if [[ -f "$HOME/.bash_profile" ]]; then
echo "$HOME/.bash_profile" echo "$HOME/.bash_profile"
else
echo "$HOME/.profile"
fi
else else
echo "$HOME/.profile" if [[ -f "$HOME/.bashrc" ]]; then
echo "$HOME/.bashrc"
elif [[ -f "$HOME/.bash_profile" ]]; then
echo "$HOME/.bash_profile"
else
echo "$HOME/.profile"
fi
fi fi
;; ;;
fish) fish)
@@ -410,13 +556,13 @@ add_to_path() {
profile=$(detect_shell_profile) profile=$(detect_shell_profile)
# Check if already in PATH # Check if already in PATH
if echo "$PATH" | tr ':' '\n' | grep -qx "$bin_dir"; then if [[ ":$PATH:" == *":$bin_dir:"* ]]; then
info "PATH already includes $bin_dir" info "PATH already includes $bin_dir"
return 0 return 0
fi fi
# Check if already in profile # Check if already in profile
if [[ -f "$profile" ]] && grep -q "$bin_dir" "$profile" 2>/dev/null; then if [[ -f "$profile" ]] && grep -qF "$bin_dir" "$profile" 2>/dev/null; then
info "PATH export already in $profile" info "PATH export already in $profile"
return 0 return 0
fi fi
@@ -430,6 +576,8 @@ add_to_path() {
shell_name="$(basename "${SHELL:-/bin/bash}")" shell_name="$(basename "${SHELL:-/bin/bash}")"
if [[ "$shell_name" == "fish" ]]; then if [[ "$shell_name" == "fish" ]]; then
echo "" >> "$profile"
echo "# Added by Claudeman installer" >> "$profile"
echo "fish_add_path $bin_dir" >> "$profile" echo "fish_add_path $bin_dir" >> "$profile"
else else
echo "" >> "$profile" echo "" >> "$profile"
@@ -452,6 +600,10 @@ setup_systemd_service() {
mkdir -p "$service_dir" mkdir -p "$service_dir"
# Find node binary path
local node_path
node_path=$(command -v node)
# Create service file # Create service file
cat > "$service_file" << EOF cat > "$service_file" << EOF
[Unit] [Unit]
@@ -460,11 +612,12 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
ExecStart=$INSTALL_DIR/dist/index.js web --https ExecStart=$node_path $INSTALL_DIR/dist/index.js web --https
WorkingDirectory=$HOME WorkingDirectory=$HOME
Restart=always Restart=always
RestartSec=10 RestartSec=10
Environment=NODE_ENV=production Environment=NODE_ENV=production
Environment=PATH=$PATH
[Install] [Install]
WantedBy=default.target WantedBy=default.target
@@ -474,7 +627,7 @@ EOF
systemctl --user daemon-reload systemctl --user daemon-reload
# Enable service # Enable service
systemctl --user enable claudeman-web.service systemctl --user enable claudeman-web.service 2>/dev/null || true
# Enable lingering (allows service to run after logout) # Enable lingering (allows service to run after logout)
if command -v loginctl &>/dev/null; then if command -v loginctl &>/dev/null; then
@@ -485,18 +638,40 @@ EOF
info "Start with: systemctl --user start claudeman-web" info "Start with: systemctl --user start claudeman-web"
} }
# ============================================================================
# Installation Helpers
# ============================================================================
install_dependency() {
local dep_name="$1"
local os="$2"
local distro="$3"
local install_func="install_${dep_name}_${distro:-$os}"
# Try distro-specific first, then OS-level
if [[ "$os" == "macos" ]]; then
install_func="install_${dep_name}_macos"
elif ! declare -f "$install_func" &>/dev/null; then
die "Don't know how to install $dep_name on $distro. Please install it manually."
fi
"$install_func"
}
# ============================================================================ # ============================================================================
# Main Installation # Main Installation
# ============================================================================ # ============================================================================
print_banner() { print_banner() {
echo -e "${CYAN}${BOLD}" echo -e "${CYAN}${BOLD}"
echo " ____ _ _ " cat << 'EOF'
echo " / ___|| | __ _ _ _ __| | ___ _ __ ___ __ _ _ __ " ____ _ _
echo "| | | |/ _\` | | | |/ _\` |/ _ \\ '_ \` _ \\ / _\` | '_ \\ " / ___| | __ _ _ _ __| | ___ _ __ ___ __ _ _ __
echo "| |____| | (_| | |_| | (_| | __/ | | | | | (_| | | | | " | | | |/ _` | | | |/ _` |/ _ \ '_ ` _ \ / _` | '_ \
echo " \\_____|_|\\__,_|\\__,_|\\__,_|\\___|_| |_| |_|\\__,_|_| |_| " | |___| | (_| | |_| | (_| | __/ | | | | | (_| | | | |
echo "" \____|_|\__,_|\__,_|\__,_|\___|_| |_| |_|\__,_|_| |_|
EOF
echo -e "${NC}${DIM} The missing control plane for Claude Code${NC}" echo -e "${NC}${DIM} The missing control plane for Claude Code${NC}"
echo "" echo ""
} }
@@ -504,6 +679,11 @@ print_banner() {
main() { main() {
print_banner print_banner
# Check for curl/wget first
if ! check_curl_or_wget; then
die "curl or wget is required but neither is installed. Please install one first."
fi
# Detect system # Detect system
local os arch distro="" local os arch distro=""
os=$(detect_os) os=$(detect_os)
@@ -524,14 +704,7 @@ main() {
info "Checking Git..." info "Checking Git..."
if ! check_git; then if ! check_git; then
if prompt_yes_no "Git is not installed. Install it now?"; then if prompt_yes_no "Git is not installed. Install it now?"; then
case "$os-$distro" in install_dependency "git" "$os" "$distro"
macos-*) install_git_macos ;;
linux-debian) install_git_debian ;;
linux-fedora) install_git_fedora ;;
linux-arch) install_git_arch ;;
linux-alpine) install_git_alpine ;;
*) die "Don't know how to install Git on this system. Please install it manually." ;;
esac
else else
die "Git is required to install Claudeman." die "Git is required to install Claudeman."
fi fi
@@ -548,15 +721,11 @@ main() {
warn "Node.js $node_version is installed but version $MIN_NODE_VERSION+ is required." warn "Node.js $node_version is installed but version $MIN_NODE_VERSION+ is required."
fi fi
if prompt_yes_no "Install Node.js $MIN_NODE_VERSION+?"; then if prompt_yes_no "Install Node.js v$TARGET_NODE_VERSION?"; then
case "$os-$distro" in install_dependency "node" "$os" "$distro"
macos-*) install_node_macos ;;
linux-debian) install_node_debian ;; # Rehash to pick up new node
linux-fedora) install_node_fedora ;; hash -r 2>/dev/null || true
linux-arch) install_node_arch ;;
linux-alpine) install_node_alpine ;;
*) die "Don't know how to install Node.js on this system. Please install it manually." ;;
esac
else else
die "Node.js $MIN_NODE_VERSION+ is required to run Claudeman." die "Node.js $MIN_NODE_VERSION+ is required to run Claudeman."
fi fi
@@ -575,14 +744,7 @@ main() {
info "Checking GNU Screen..." info "Checking GNU Screen..."
if ! check_screen; then if ! check_screen; then
if prompt_yes_no "GNU Screen is not installed. Install it now?"; then if prompt_yes_no "GNU Screen is not installed. Install it now?"; then
case "$os-$distro" in install_dependency "screen" "$os" "$distro"
macos-*) install_screen_macos ;;
linux-debian) install_screen_debian ;;
linux-fedora) install_screen_fedora ;;
linux-arch) install_screen_arch ;;
linux-alpine) install_screen_alpine ;;
*) die "Don't know how to install GNU Screen on this system. Please install it manually." ;;
esac
else else
die "GNU Screen is required for session persistence." die "GNU Screen is required for session persistence."
fi fi
@@ -598,7 +760,7 @@ main() {
echo -e " ${DIM}Claudeman requires Claude CLI to manage AI sessions.${NC}" echo -e " ${DIM}Claudeman requires Claude CLI to manage AI sessions.${NC}"
echo -e " ${DIM}Install it with:${NC}" echo -e " ${DIM}Install it with:${NC}"
echo "" echo ""
echo -e " ${CYAN}npm install -g @anthropic-ai/claude-code${NC}" echo -e " ${CYAN}curl -fsSL https://claude.ai/install.sh | bash${NC}"
echo "" echo ""
echo -e " ${DIM}Or see: https://docs.anthropic.com/en/docs/claude-code${NC}" echo -e " ${DIM}Or see: https://docs.anthropic.com/en/docs/claude-code${NC}"
echo "" echo ""
@@ -619,14 +781,26 @@ main() {
if [[ -d "$INSTALL_DIR/.git" ]]; then if [[ -d "$INSTALL_DIR/.git" ]]; then
info "Existing installation found, updating..." info "Existing installation found, updating..."
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
git fetch --quiet origin
git reset --hard origin/master --quiet # Check for local changes
if ! git diff --quiet 2>/dev/null || ! git diff --staged --quiet 2>/dev/null; then
warn "Local changes detected in $INSTALL_DIR"
if prompt_yes_no "Discard local changes and update?" "n"; then
git fetch --quiet origin
git reset --hard origin/master --quiet
else
info "Keeping existing installation, skipping update"
fi
else
git fetch --quiet origin
git reset --hard origin/master --quiet
fi
else else
# Create parent directory # Create parent directory
mkdir -p "$(dirname "$INSTALL_DIR")" mkdir -p "$(dirname "$INSTALL_DIR")"
# Clone repository # Clone repository (shallow for speed)
git clone --quiet "$REPO_URL" "$INSTALL_DIR" git clone --quiet --depth 1 "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
fi fi
@@ -637,10 +811,10 @@ main() {
# ======================================================================== # ========================================================================
info "Installing dependencies..." info "Installing dependencies..."
npm install --quiet --no-fund --no-audit npm install --quiet --no-fund --no-audit 2>/dev/null || npm install --no-fund --no-audit
info "Building..." info "Building..."
npm run build --quiet npm run build --quiet 2>/dev/null || npm run build
success "Build complete" success "Build complete"
@@ -653,9 +827,15 @@ main() {
# Create symlink in a common PATH location if possible # Create symlink in a common PATH location if possible
local symlink_dir="$HOME/.local/bin" local symlink_dir="$HOME/.local/bin"
if [[ -d "$symlink_dir" ]] || mkdir -p "$symlink_dir" 2>/dev/null; then mkdir -p "$symlink_dir" 2>/dev/null || true
if [[ -d "$symlink_dir" ]]; then
ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/claudeman" ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/claudeman"
info "Created symlink: $symlink_dir/claudeman" info "Created symlink: $symlink_dir/claudeman"
# Add ~/.local/bin to PATH if not already there
if [[ ":$PATH:" != *":$symlink_dir:"* ]]; then
add_to_path "$symlink_dir"
fi
fi fi
# ======================================================================== # ========================================================================
@@ -664,7 +844,7 @@ main() {
if [[ "$os" == "linux" ]] && [[ "$SKIP_SYSTEMD" != "1" ]] && command -v systemctl &>/dev/null; then if [[ "$os" == "linux" ]] && [[ "$SKIP_SYSTEMD" != "1" ]] && command -v systemctl &>/dev/null; then
echo "" echo ""
if prompt_yes_no "Set up systemd service for auto-start?"; then if prompt_yes_no "Set up systemd service for auto-start?" "n"; then
setup_systemd_service setup_systemd_service
fi fi
fi fi
@@ -705,14 +885,14 @@ main() {
if ! check_claude; then if ! check_claude; then
echo -e " ${YELLOW}${BOLD}Reminder:${NC} Install Claude CLI to start using Claudeman:" echo -e " ${YELLOW}${BOLD}Reminder:${NC} Install Claude CLI to start using Claudeman:"
echo -e " ${CYAN}npm install -g @anthropic-ai/claude-code${NC}" echo -e " ${CYAN}curl -fsSL https://claude.ai/install.sh | bash${NC}"
echo "" echo ""
fi fi
# Remind to reload shell if PATH was modified # Remind to reload shell if PATH was modified
local profile local profile
profile=$(detect_shell_profile) profile=$(detect_shell_profile)
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$bin_dir"; then if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
echo -e " ${DIM}Restart your shell or run: source $profile${NC}" echo -e " ${DIM}Restart your shell or run: source $profile${NC}"
echo "" echo ""
fi fi
+161 -52
View File
@@ -1,87 +1,196 @@
#!/usr/bin/env node #!/usr/bin/env node
/**
* Claudeman postinstall verification script
* Runs after `npm install` to check environment readiness
*/
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { homedir } from 'os'; import { homedir, platform } from 'os';
import { join } from 'path'; import { join } from 'path';
// Colors (with fallback for no-color environments) // ============================================================================
const useColor = process.stdout.isTTY && !process.env.NO_COLOR; // Configuration
const green = (s) => useColor ? `\x1b[32m${s}\x1b[0m` : s; // ============================================================================
const yellow = (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s;
const red = (s) => useColor ? `\x1b[31m${s}\x1b[0m` : s;
const bold = (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s;
console.log(bold('Claudeman postinstall check...')); const MIN_NODE_VERSION = 18;
// Claude CLI search paths (must match src/session.ts)
const home = homedir();
const CLAUDE_SEARCH_PATHS = [
join(home, '.local/bin/claude'),
join(home, '.claude/local/claude'),
'/usr/local/bin/claude',
join(home, '.npm-global/bin/claude'),
join(home, 'bin/claude'),
];
// ============================================================================
// Colors (with fallback for no-color environments)
// ============================================================================
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
const colors = {
green: (s) => useColor ? `\x1b[32m${s}\x1b[0m` : s,
yellow: (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s,
red: (s) => useColor ? `\x1b[31m${s}\x1b[0m` : s,
cyan: (s) => useColor ? `\x1b[36m${s}\x1b[0m` : s,
bold: (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s,
dim: (s) => useColor ? `\x1b[2m${s}\x1b[0m` : s,
};
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Check if a command exists in PATH
* Works on Unix and Windows
*/
function commandExists(cmd) {
try {
const checkCmd = platform() === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
execSync(checkCmd, { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
/**
* Get install instructions for screen based on platform
*/
function getScreenInstallInstructions() {
const os = platform();
if (os === 'darwin') {
return [
' macOS: brew install screen',
];
}
if (os === 'linux') {
return [
' Ubuntu/Debian: sudo apt install screen',
' Fedora/RHEL: sudo dnf install screen',
' Arch Linux: sudo pacman -S screen',
' Alpine: sudo apk add screen',
];
}
if (os === 'win32') {
return [
' Windows: Use WSL (Windows Subsystem for Linux)',
' GNU Screen is not available on native Windows',
];
}
return [' Please install GNU Screen for your platform'];
}
// ============================================================================
// Main Checks
// ============================================================================
console.log(colors.bold('Claudeman postinstall check...'));
console.log('');
let hasWarnings = false; let hasWarnings = false;
let hasErrors = false;
// ----------------------------------------------------------------------------
// 1. Check Node.js version >= 18 // 1. Check Node.js version >= 18
// ----------------------------------------------------------------------------
const nodeVersion = process.versions.node; const nodeVersion = process.versions.node;
const majorVersion = parseInt(nodeVersion.split('.')[0], 10); const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
if (majorVersion < 18) { if (majorVersion < MIN_NODE_VERSION) {
console.log(red(`✗ Node.js v${nodeVersion} is too old (requires >=18)`)); console.log(colors.red(`✗ Node.js v${nodeVersion} is too old`));
process.exit(1); console.log(colors.dim(` Minimum required: v${MIN_NODE_VERSION}`));
console.log('');
hasErrors = true;
} else { } else {
console.log(green(`✓ Node.js v${nodeVersion}`) + ' (meets >=18 requirement)'); console.log(colors.green(`✓ Node.js v${nodeVersion}`) + colors.dim(` (meets >=v${MIN_NODE_VERSION} requirement)`));
} }
// ----------------------------------------------------------------------------
// 2. Check if GNU Screen is installed // 2. Check if GNU Screen is installed
let screenFound = false; // ----------------------------------------------------------------------------
try {
execSync('which screen', { stdio: 'pipe' }); if (commandExists('screen')) {
screenFound = true; console.log(colors.green('✓ GNU Screen found'));
console.log(green('✓ GNU Screen found')); } else {
} catch { hasWarnings = true;
hasWarnings = true; console.log(colors.yellow('⚠ GNU Screen not found'));
console.log(yellow('⚠ GNU Screen not found')); console.log(colors.dim(' Screen is required for session persistence.'));
console.log(' Install:'); console.log(colors.dim(' Install:'));
console.log(' Ubuntu/Debian: sudo apt install screen'); for (const instruction of getScreenInstallInstructions()) {
console.log(' macOS: brew install screen'); console.log(colors.dim(instruction));
}
} }
// ----------------------------------------------------------------------------
// 3. Check if Claude CLI is found // 3. Check if Claude CLI is found
const home = homedir(); // ----------------------------------------------------------------------------
const claudePaths = [
join(home, '.local/bin/claude'),
join(home, '.claude/local/claude'),
'/usr/local/bin/claude',
join(home, '.npm-global/bin/claude'),
join(home, 'bin/claude'),
];
let claudeFound = false; let claudeFound = false;
let claudePath = null;
// First try `which claude` // First try PATH lookup
try { if (commandExists('claude')) {
execSync('which claude', { stdio: 'pipe' }); claudeFound = true;
claudeFound = true; try {
} catch { const checkCmd = platform() === 'win32' ? 'where claude' : 'command -v claude';
// Check known paths claudePath = execSync(checkCmd, { stdio: 'pipe', encoding: 'utf-8' }).trim().split('\n')[0];
for (const p of claudePaths) { } catch {
if (existsSync(p)) { // Ignore, we know it exists
claudeFound = true; }
break; }
// Check known paths if not found in PATH
if (!claudeFound) {
for (const p of CLAUDE_SEARCH_PATHS) {
if (existsSync(p)) {
claudeFound = true;
claudePath = p;
break;
}
} }
}
} }
if (claudeFound) { if (claudeFound) {
console.log(green('✓ Claude CLI found')); const pathInfo = claudePath ? colors.dim(` (${claudePath})`) : '';
console.log(colors.green('✓ Claude CLI found') + pathInfo);
} else { } else {
hasWarnings = true; hasWarnings = true;
console.log(yellow('⚠ Claude CLI not found')); console.log(colors.yellow('⚠ Claude CLI not found'));
console.log(' Install: curl -fsSL https://claude.ai/install.sh | bash'); console.log(colors.dim(' Claude CLI is required to run AI sessions.'));
console.log(colors.dim(' Install:'));
console.log(colors.cyan(' curl -fsSL https://claude.ai/install.sh | bash'));
} }
// Print next steps // ----------------------------------------------------------------------------
// Print Summary and Next Steps
// ----------------------------------------------------------------------------
console.log(''); console.log('');
console.log(bold('Next steps:'));
console.log(' npm run build'); if (hasErrors) {
console.log(' claudeman web'); console.log(colors.red(colors.bold('Installation cannot proceed due to errors above.')));
process.exit(1);
}
console.log(colors.bold('Next steps:'));
console.log(colors.dim(' 1. Build: ') + colors.cyan('npm run build'));
console.log(colors.dim(' 2. Start: ') + colors.cyan('claudeman web --https'));
console.log(colors.dim(' 3. Open: ') + colors.cyan('https://localhost:3000'));
if (hasWarnings) { if (hasWarnings) {
console.log(''); console.log('');
console.log(yellow('Note: Resolve warnings above for full functionality.')); console.log(colors.yellow('Note: Resolve warnings above for full functionality.'));
} }
console.log('');