#!/bin/sh set -eu PACKAGE_NAME="free-claude-code" FCC_HOME_DIRNAME=".fcc" FCC_COMMANDS="fcc-server fcc-claude fcc-codex fcc-pi fcc-init free-claude-code" dry_run=0 uv_tool_bin="" show_usage() { cat <<'USAGE' Usage: uninstall.sh [options] Removes the Free Claude Code uv tool and deletes ~/.fcc/ after removal is verified. Does not remove uv, Claude Code, Codex, Pi, the uv-managed Python runtime, or shared PATH entries. Options: --dry-run Print commands without running them. --help Show this help text. USAGE } fail() { printf 'error: %s\n' "$*" >&2 exit 1 } step() { printf '\n==> %s\n' "$1" } quote_arg() { case "$1" in *[!A-Za-z0-9_./:@%+=,-]*|"") escaped=$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g') printf '"%s"' "$escaped" ;; *) printf '%s' "$1" ;; esac } print_command() { printf '+' for arg in "$@"; do printf ' ' quote_arg "$arg" done printf '\n' } run() { print_command "$@" if [ "$dry_run" -eq 1 ]; then return 0 fi if "$@"; then return 0 else status=$? fi fail "Command failed with exit code $status: $1" } is_missing_uv_tool_error() { normalized=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') case "$normalized" in *"$PACKAGE_NAME"*"is not installed"*) return 0 ;; *) return 1 ;; esac } add_path_entry() { [ -n "$1" ] || return 0 case ":$PATH:" in *":$1:"*) ;; *) PATH="$1:$PATH" ;; esac } add_known_uv_paths() { if [ -n "${XDG_BIN_HOME:-}" ]; then add_path_entry "$XDG_BIN_HOME" fi add_path_entry "$HOME/.local/bin" add_path_entry "$HOME/.cargo/bin" export PATH hash -r 2>/dev/null || true } is_fcc_command_running() { command_name=$1 if command -v pgrep >/dev/null 2>&1; then if pgrep -x "$command_name" >/dev/null 2>&1; then return 0 fi if pgrep -f "(^|/)${command_name}( |$)" >/dev/null 2>&1; then return 0 fi return 1 fi ps -A -o comm= 2>/dev/null | grep -qx "$command_name" } assert_no_fcc_processes_running() { running="" for command_name in $FCC_COMMANDS; do if is_fcc_command_running "$command_name"; then running="${running} ${command_name}" fi done if [ -n "$running" ]; then fail "Free Claude Code is still running (${running# }). Stop those processes, then rerun uninstall." fi } initialize_uv_context() { add_known_uv_paths if [ "$dry_run" -eq 1 ]; then print_command uv tool dir --bin return 0 fi if ! command -v uv >/dev/null 2>&1; then fail "uv is required to remove the Free Claude Code tool. Install uv, then rerun this uninstaller; ~/.fcc was not deleted." fi print_command uv tool dir --bin if uv_tool_bin=$(uv tool dir --bin); then : else status=$? fail "Could not determine the uv tool bin directory (exit code $status); ~/.fcc was not deleted." fi [ -n "$uv_tool_bin" ] || fail "uv returned an empty tool bin directory; ~/.fcc was not deleted." } uninstall_free_claude_code() { print_command uv tool uninstall "$PACKAGE_NAME" if [ "$dry_run" -eq 1 ]; then return 0 fi if output=$(uv tool uninstall "$PACKAGE_NAME" 2>&1); then if [ -n "$output" ]; then printf '%s\n' "$output" fi return 0 else status=$? fi if is_missing_uv_tool_error "$output"; then printf 'Free Claude Code uv tool is already absent; verifying its entry points.\n' return 0 fi if [ -n "$output" ]; then printf '%s\n' "$output" >&2 fi fail "uv tool uninstall $PACKAGE_NAME failed with exit code $status; ~/.fcc was not deleted." } verify_fcc_commands_removed() { if [ "$dry_run" -eq 1 ]; then printf '+ verify all Free Claude Code entry points are absent from the uv tool bin directory\n' return 0 fi remaining="" for command_name in $FCC_COMMANDS; do command_path="$uv_tool_bin/$command_name" if [ -e "$command_path" ] || [ -L "$command_path" ]; then remaining="${remaining} ${command_path}" fi done if [ -n "$remaining" ]; then fail "Free Claude Code entry points remain after uv uninstall:${remaining}; ~/.fcc was not deleted." fi } purge_fcc_home() { fcc_home="$HOME/$FCC_HOME_DIRNAME" if [ ! -e "$fcc_home" ]; then printf 'No FCC config directory at %s; skipping purge.\n' "$fcc_home" return 0 fi run rm -rf "$fcc_home" if [ "$dry_run" -eq 0 ] && [ -e "$fcc_home" ]; then fail "FCC config directory still exists after deletion: $fcc_home" fi } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --dry-run) dry_run=1 ;; --help|-h) show_usage exit 0 ;; *) show_usage >&2 fail "unknown option: $1" ;; esac shift done } parse_args "$@" [ -n "${HOME:-}" ] || fail "HOME is not set; cannot locate Free Claude Code data." step "Checking for running Free Claude Code processes" assert_no_fcc_processes_running step "Locating the uv-managed Free Claude Code installation" initialize_uv_context step "Removing the Free Claude Code uv tool" uninstall_free_claude_code step "Verifying Free Claude Code entry points were removed" verify_fcc_commands_removed step "Purging FCC config and data from ~/.fcc" purge_fcc_home if [ "$dry_run" -eq 1 ]; then printf '\nDry run complete. No changes were made.\n' else printf '\nFree Claude Code has been removed and verified.\n' printf 'uv, Claude Code, Codex, Pi, the uv-managed Python runtime, and shared PATH entries were left installed.\n' fi