alishahryar1--free-claude-code
d98a6b0ca0
## Problem Free Claude Code currently has to remain attached to a terminal, so closing that window stops the proxy and users have no native way to reopen Admin or control the background server. The contributed Windows wrapper also would have introduced a second bundled server lifecycle instead of reusing FCC's cleanup and restart ownership. Fixes #1147. ## Changes | Before | After | | --- | --- | | Users keep `fcc-server` running in a terminal. | Windows and macOS users can launch a console-free FCC Desktop host from a desktop/application shortcut and control it from the tray or menu bar. | | A wrapper would need to spawn and terminate a child server. | The terminal and desktop paths share one in-process supervisor, one graceful runtime shutdown path, and an OS-held singleton lock. | | Installers manage only command entry points. | Windows installs desktop and Start-menu shortcuts; macOS installs a per-user app bundle and owned desktop link; uninstallers remove only those FCC artifacts. | | Desktop behavior had no contract coverage. | Lifecycle, duplicate launch, restart/quit, GUI packaging, Windows shortcuts, macOS bundle creation, quoting, and ownership boundaries are covered alongside the full CI suite. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR adds a native FCC desktop launcher for Windows and macOS. The main changes are: - A shared server supervisor for terminal and desktop launches. - A singleton desktop host with tray or menu-bar controls. - Windows shortcuts and a per-user macOS app bundle. - Ownership checks for launcher installation and removal. - Tests for lifecycle, packaging, shortcuts, and uninstall behavior. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. Startup restart requests are reserved before the worker starts. macOS bundle operations verify ownership before modifying or deleting files. Windows shortcut operations verify their targets before replacement or removal. No blocking issues were found in the updated code. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - Compared the pre-change contract test results against the parent commit f81af55630aa1adb518b748b9e6985c73c4c4775 and observed 3 failures and 4 passes, indicating the missing scheduled-startup lifecycle contract. - Executed the after-state contract validation with uv run pytest -n 0 tests/cli/test\_desktop.py -q and confirmed the run finished with 7 passes and an exit code of 0. - Verified that no real proxy or native GUI dependency was started during the after-state run. - Inspected the two log artifacts that accompany the proof to corroborate the test outcomes. <a href="https://app.greptile.com/trex/runs/15235588/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/cli/commands.py | Adds the shared server supervisor and scheduled-run state for startup restart requests. | | src/free_claude_code/cli/desktop.py | Adds singleton locking and coordinates the tray loop with the server worker. | | scripts/install.sh | Creates the macOS app bundle only when an existing bundle is FCC-owned. | | scripts/uninstall.sh | Removes the macOS launcher only on macOS and only with the expected ownership marker. | | scripts/install.ps1 | Creates Windows shortcuts while preserving shortcuts with unrelated targets. | | scripts/uninstall.ps1 | Removes Windows shortcuts only when their targets match an FCC desktop entry point. | </details> <sub>Reviews (3): Last reviewed commit: ["fix: coalesce desktop startup restarts"](https://github.com/alishahryar1/free-claude-code/commit/b9554729770e08a58818d677de39757c751e760e) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=45925660)</sub> <!-- /greptile_comment -->
652 行
18 KiB
Bash
652 行
18 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
REPO_ARCHIVE_URL="https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"
|
|
PYTHON_VERSION="3.14.0"
|
|
MIN_UV_VERSION="0.11.16"
|
|
CLAUDE_INSTALL_URL="https://claude.ai/install.sh"
|
|
CODEX_INSTALL_URL="https://chatgpt.com/codex/install.sh"
|
|
PI_INSTALL_URL="https://pi.dev/install.sh"
|
|
UV_INSTALL_URL="https://astral.sh/uv/install.sh"
|
|
FCC_MACOS_BUNDLE_ID="io.github.alishahryar1.free-claude-code"
|
|
FCC_MACOS_OWNER_FILE=".free-claude-code-owner"
|
|
# Include retired entry points so updates reject older FCC processes before replacement.
|
|
FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-init free-claude-code"
|
|
|
|
dry_run=0
|
|
voice_nim=0
|
|
voice_local=0
|
|
voice_all=0
|
|
torch_backend=""
|
|
temporary_script=""
|
|
tool_bin=""
|
|
|
|
show_usage() {
|
|
cat <<'USAGE'
|
|
Usage: install.sh [options]
|
|
|
|
Installs Claude Code, Codex, and Pi if missing, ensures a compatible uv, and installs or updates Free Claude Code.
|
|
|
|
Options:
|
|
--voice-nim Install NVIDIA NIM voice transcription support.
|
|
--voice-local Install local Whisper voice transcription support.
|
|
--voice-all Install all voice transcription backends.
|
|
--torch-backend VALUE Use a uv PyTorch backend, such as cu130. Requires local voice.
|
|
--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"
|
|
}
|
|
|
|
cleanup() {
|
|
if [ -n "$temporary_script" ] && [ -e "$temporary_script" ]; then
|
|
rm -f "$temporary_script"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' HUP TERM
|
|
|
|
add_path_entry() {
|
|
[ -n "$1" ] || return 0
|
|
case ":$PATH:" in
|
|
*":$1:"*) ;;
|
|
*) PATH="$1:$PATH" ;;
|
|
esac
|
|
}
|
|
|
|
add_known_bin_directories() {
|
|
if [ -n "${XDG_BIN_HOME:-}" ]; then
|
|
add_path_entry "$XDG_BIN_HOME"
|
|
fi
|
|
|
|
if [ -n "${HOME:-}" ]; then
|
|
add_path_entry "$HOME/.local/bin"
|
|
add_path_entry "$HOME/.cargo/bin"
|
|
add_path_entry "${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin"
|
|
fi
|
|
|
|
export PATH
|
|
hash -r 2>/dev/null || true
|
|
}
|
|
|
|
add_pi_bin_directories() {
|
|
[ "$dry_run" -eq 0 ] || return 0
|
|
add_known_bin_directories
|
|
if command -v npm >/dev/null 2>&1; then
|
|
pi_npm_prefix=$(npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null || true)
|
|
if [ -n "$pi_npm_prefix" ]; then
|
|
add_path_entry "$pi_npm_prefix/bin"
|
|
export PATH
|
|
hash -r 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
fcc_process_ids() {
|
|
command_name=$1
|
|
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
{
|
|
pgrep -x "$command_name" 2>/dev/null || true
|
|
pgrep -f "(^|/)${command_name}([[:space:]]|$)" 2>/dev/null || true
|
|
} | sort -nu
|
|
return 0
|
|
fi
|
|
|
|
ps -A -o pid= -o args= 2>/dev/null |
|
|
awk -v command_name="$command_name" '
|
|
BEGIN {
|
|
pattern = "(^|/)" command_name "([[:space:]]|$)"
|
|
}
|
|
{
|
|
process_id = $1
|
|
sub(/^[[:space:]]*[0-9]+[[:space:]]+/, "")
|
|
if ($0 ~ pattern) {
|
|
print process_id
|
|
}
|
|
}
|
|
' || true
|
|
}
|
|
|
|
assert_no_fcc_processes_running() {
|
|
running=""
|
|
for command_name in $FCC_COMMANDS; do
|
|
process_ids=$(fcc_process_ids "$command_name")
|
|
[ -n "$process_ids" ] || continue
|
|
|
|
for process_id in $process_ids; do
|
|
process="$command_name (PID $process_id)"
|
|
if [ -n "$running" ]; then
|
|
running="$running, $process"
|
|
else
|
|
running=$process
|
|
fi
|
|
done
|
|
done
|
|
|
|
if [ -n "$running" ]; then
|
|
fail "Free Claude Code is still running ($running). Stop those processes, then rerun the installer."
|
|
fi
|
|
}
|
|
|
|
require_command() {
|
|
if [ "$dry_run" -eq 0 ] && ! command -v "$1" >/dev/null 2>&1; then
|
|
fail "$1 is required. Install it first, then rerun this installer."
|
|
fi
|
|
}
|
|
|
|
download_and_run() {
|
|
url=$1
|
|
interpreter=$2
|
|
label=$3
|
|
non_interactive=${4:-0}
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
print_command curl -fsSL "$url" -o "<temporary-script>"
|
|
if [ "$non_interactive" -eq 1 ]; then
|
|
printf '+ CODEX_NON_INTERACTIVE=1 '
|
|
quote_arg "$interpreter"
|
|
printf ' <temporary-script>\n'
|
|
else
|
|
print_command "$interpreter" "<temporary-script>"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
temporary_script=$(mktemp "${TMPDIR:-/tmp}/fcc-install.XXXXXX") || fail "Unable to create a temporary file for $label."
|
|
print_command curl -fsSL "$url" -o "$temporary_script"
|
|
if curl -fsSL "$url" -o "$temporary_script"; then
|
|
:
|
|
else
|
|
status=$?
|
|
fail "Could not download the $label installer (curl exit code $status)."
|
|
fi
|
|
|
|
if [ ! -s "$temporary_script" ]; then
|
|
fail "The downloaded $label installer was empty."
|
|
fi
|
|
|
|
if [ "$non_interactive" -eq 1 ]; then
|
|
printf '+ CODEX_NON_INTERACTIVE=1 '
|
|
quote_arg "$interpreter"
|
|
printf ' '
|
|
quote_arg "$temporary_script"
|
|
printf '\n'
|
|
if CODEX_NON_INTERACTIVE=1 "$interpreter" "$temporary_script"; then
|
|
:
|
|
else
|
|
status=$?
|
|
fail "$label installation failed with exit code $status."
|
|
fi
|
|
else
|
|
print_command "$interpreter" "$temporary_script"
|
|
if "$interpreter" "$temporary_script"; then
|
|
:
|
|
else
|
|
status=$?
|
|
fail "$label installation failed with exit code $status."
|
|
fi
|
|
fi
|
|
|
|
rm -f "$temporary_script"
|
|
temporary_script=""
|
|
}
|
|
|
|
verify_command() {
|
|
command_name=$1
|
|
display_name=$2
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
print_command "$command_name" --version
|
|
return 0
|
|
fi
|
|
|
|
command_path=$(command -v "$command_name" 2>/dev/null) || fail "$display_name was installed, but '$command_name' is not available on PATH."
|
|
run "$command_path" --version
|
|
}
|
|
|
|
pi_command_is_compatible() {
|
|
pi_command_path=$(command -v pi 2>/dev/null) || return 1
|
|
pi_help=$("$pi_command_path" --help 2>/dev/null) || return 1
|
|
case "$pi_help" in
|
|
*--extension*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$pi_help" in
|
|
*--models*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
verify_pi_command() {
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
printf '+ pi --help (verify --extension and --models support)\n'
|
|
print_command pi --version
|
|
return 0
|
|
fi
|
|
|
|
pi_command_path=$(command -v pi 2>/dev/null) || fail "Pi was installed, but 'pi' is not available on PATH."
|
|
pi_command_is_compatible || fail "The 'pi' command at $pi_command_path is not a compatible Pi Coding Agent."
|
|
run "$pi_command_path" --version
|
|
}
|
|
|
|
ensure_claude() {
|
|
if command -v claude >/dev/null 2>&1; then
|
|
printf 'Claude Code already found on PATH; verifying it.\n'
|
|
else
|
|
download_and_run "$CLAUDE_INSTALL_URL" bash "Claude Code"
|
|
add_known_bin_directories
|
|
fi
|
|
|
|
verify_command claude "Claude Code"
|
|
}
|
|
|
|
ensure_codex() {
|
|
if command -v codex >/dev/null 2>&1; then
|
|
printf 'Codex already found on PATH; verifying it.\n'
|
|
else
|
|
download_and_run "$CODEX_INSTALL_URL" sh "Codex" 1
|
|
add_known_bin_directories
|
|
fi
|
|
|
|
verify_command codex "Codex"
|
|
}
|
|
|
|
ensure_pi() {
|
|
if [ "$dry_run" -eq 1 ] && command -v pi >/dev/null 2>&1; then
|
|
printf 'Pi already found on PATH; verifying it.\n'
|
|
elif pi_command_is_compatible; then
|
|
printf 'Pi already found on PATH; verifying it.\n'
|
|
else
|
|
if existing_pi_path=$(command -v pi 2>/dev/null); then
|
|
printf "The existing 'pi' command at %s is not Pi Coding Agent; installing Pi.\n" "$existing_pi_path"
|
|
fi
|
|
download_and_run "$PI_INSTALL_URL" sh "Pi"
|
|
add_pi_bin_directories
|
|
fi
|
|
|
|
verify_pi_command
|
|
}
|
|
|
|
current_uv_version() {
|
|
if output=$(uv --version); then
|
|
:
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
case "$output" in
|
|
uv\ *) version=${output#uv } ;;
|
|
*) version=$output ;;
|
|
esac
|
|
version=${version%% *}
|
|
|
|
case "$version" in
|
|
[0-9]*.[0-9]*.[0-9]*) printf '%s\n' "$version" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
uv_version_is_supported() {
|
|
case "$1" in
|
|
*-*) return 1 ;;
|
|
esac
|
|
|
|
current=${1%%+*}
|
|
minimum=${2%%+*}
|
|
|
|
old_ifs=$IFS
|
|
IFS=.
|
|
set -- $current
|
|
current_major=${1:-0}
|
|
current_minor=${2:-0}
|
|
current_patch=${3:-0}
|
|
set -- $minimum
|
|
minimum_major=${1:-0}
|
|
minimum_minor=${2:-0}
|
|
minimum_patch=${3:-0}
|
|
IFS=$old_ifs
|
|
|
|
case "$current_major$current_minor$current_patch$minimum_major$minimum_minor$minimum_patch" in
|
|
*[!0-9]*) return 1 ;;
|
|
esac
|
|
|
|
[ "$current_major" -gt "$minimum_major" ] && return 0
|
|
[ "$current_major" -lt "$minimum_major" ] && return 1
|
|
[ "$current_minor" -gt "$minimum_minor" ] && return 0
|
|
[ "$current_minor" -lt "$minimum_minor" ] && return 1
|
|
[ "$current_patch" -ge "$minimum_patch" ]
|
|
}
|
|
|
|
verify_uv() {
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
print_command uv --version
|
|
return 0
|
|
fi
|
|
|
|
command -v uv >/dev/null 2>&1 || fail "uv was installed, but it is not available on PATH."
|
|
version=$(current_uv_version) || fail "uv is present, but 'uv --version' did not return a valid version."
|
|
if ! uv_version_is_supported "$version" "$MIN_UV_VERSION"; then
|
|
fail "Stable uv $MIN_UV_VERSION or newer is required; found uv $version after installation."
|
|
fi
|
|
|
|
printf 'Verified uv %s.\n' "$version"
|
|
}
|
|
|
|
ensure_uv() {
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
if command -v uv >/dev/null 2>&1; then
|
|
print_command uv --version
|
|
printf 'A compatible existing uv will be left unchanged; an obsolete one will be replaced by the standalone installer.\n'
|
|
else
|
|
printf 'uv is not installed; the current standalone uv would be installed.\n'
|
|
download_and_run "$UV_INSTALL_URL" sh "uv"
|
|
verify_uv
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
if command -v uv >/dev/null 2>&1; then
|
|
version=$(current_uv_version) || fail "uv is present, but 'uv --version' did not return a valid version."
|
|
if uv_version_is_supported "$version" "$MIN_UV_VERSION"; then
|
|
printf 'uv %s already satisfies >=%s; leaving it unchanged.\n' "$version" "$MIN_UV_VERSION"
|
|
return 0
|
|
fi
|
|
printf 'uv %s does not satisfy stable >=%s; installing the current standalone uv.\n' "$version" "$MIN_UV_VERSION"
|
|
else
|
|
printf 'uv is not installed; installing the current standalone uv.\n'
|
|
fi
|
|
|
|
download_and_run "$UV_INSTALL_URL" sh "uv"
|
|
add_known_bin_directories
|
|
verify_uv
|
|
}
|
|
|
|
parse_args() {
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--voice-nim)
|
|
voice_nim=1
|
|
;;
|
|
--voice-local)
|
|
voice_local=1
|
|
;;
|
|
--voice-all)
|
|
voice_all=1
|
|
;;
|
|
--torch-backend)
|
|
shift
|
|
[ "$#" -gt 0 ] || fail "--torch-backend requires a value."
|
|
torch_backend=$1
|
|
[ -n "$torch_backend" ] || fail "--torch-backend requires a non-empty value."
|
|
;;
|
|
--torch-backend=*)
|
|
torch_backend=${1#*=}
|
|
[ -n "$torch_backend" ] || fail "--torch-backend requires a non-empty value."
|
|
;;
|
|
--dry-run)
|
|
dry_run=1
|
|
;;
|
|
--help|-h)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
show_usage >&2
|
|
fail "unknown option: $1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
validate_args() {
|
|
include_local=$voice_local
|
|
if [ "$voice_all" -eq 1 ]; then
|
|
include_local=1
|
|
fi
|
|
|
|
if [ -n "$torch_backend" ] && [ "$include_local" -ne 1 ]; then
|
|
fail "--torch-backend requires --voice-local or --voice-all."
|
|
fi
|
|
}
|
|
|
|
package_spec() {
|
|
include_nim=$voice_nim
|
|
include_local=$voice_local
|
|
|
|
if [ "$voice_all" -eq 1 ]; then
|
|
include_nim=1
|
|
include_local=1
|
|
fi
|
|
|
|
if [ "$include_nim" -eq 1 ] && [ "$include_local" -eq 1 ]; then
|
|
printf 'free-claude-code[voice,voice_local] @ %s' "$REPO_ARCHIVE_URL"
|
|
elif [ "$include_nim" -eq 1 ]; then
|
|
printf 'free-claude-code[voice] @ %s' "$REPO_ARCHIVE_URL"
|
|
elif [ "$include_local" -eq 1 ]; then
|
|
printf 'free-claude-code[voice_local] @ %s' "$REPO_ARCHIVE_URL"
|
|
else
|
|
printf 'free-claude-code @ %s' "$REPO_ARCHIVE_URL"
|
|
fi
|
|
}
|
|
|
|
install_free_claude_code() {
|
|
assert_no_fcc_processes_running
|
|
spec=$(package_spec)
|
|
|
|
if [ -n "$torch_backend" ]; then
|
|
run uv tool install --force --refresh-package free-claude-code --python "$PYTHON_VERSION" --torch-backend "$torch_backend" "$spec"
|
|
else
|
|
run uv tool install --force --refresh-package free-claude-code --python "$PYTHON_VERSION" "$spec"
|
|
fi
|
|
}
|
|
|
|
configure_and_verify_free_claude_code() {
|
|
run uv tool update-shell
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
print_command uv tool dir --bin
|
|
printf '+ verify fcc-desktop, fcc-server, fcc-claude, fcc-codex, and fcc-pi in the uv tool bin directory\n'
|
|
print_command fcc-server --version
|
|
return 0
|
|
fi
|
|
|
|
print_command uv tool dir --bin
|
|
if tool_bin=$(uv tool dir --bin); then
|
|
:
|
|
else
|
|
status=$?
|
|
fail "Could not determine the uv tool bin directory (exit code $status)."
|
|
fi
|
|
[ -n "$tool_bin" ] || fail "uv returned an empty tool bin directory."
|
|
|
|
add_path_entry "$tool_bin"
|
|
export PATH
|
|
hash -r 2>/dev/null || true
|
|
|
|
for command_name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi; do
|
|
[ -x "$tool_bin/$command_name" ] || fail "Free Claude Code installation did not create $tool_bin/$command_name."
|
|
done
|
|
|
|
run "$tool_bin/fcc-server" --version
|
|
}
|
|
|
|
shell_quote() {
|
|
escaped=$(printf '%s' "$1" | sed "s/'/'\\\\''/g")
|
|
printf "'%s'" "$escaped"
|
|
}
|
|
|
|
macos_app_is_fcc_owned() {
|
|
app_dir=$1
|
|
owner_file="$app_dir/Contents/$FCC_MACOS_OWNER_FILE"
|
|
[ -d "$app_dir" ] &&
|
|
[ ! -L "$app_dir" ] &&
|
|
[ -f "$owner_file" ] &&
|
|
[ "$(cat "$owner_file")" = "$FCC_MACOS_BUNDLE_ID" ]
|
|
}
|
|
|
|
install_macos_desktop_app() {
|
|
[ "$(uname -s)" = "Darwin" ] || return 0
|
|
|
|
app_dir="$HOME/Applications/Free Claude Code.app"
|
|
contents_dir="$app_dir/Contents"
|
|
owner_file="$contents_dir/$FCC_MACOS_OWNER_FILE"
|
|
executable_dir="$contents_dir/MacOS"
|
|
executable_path="$executable_dir/fcc-desktop"
|
|
desktop_dir="$HOME/Desktop"
|
|
desktop_link="$desktop_dir/Free Claude Code.app"
|
|
|
|
if [ -e "$app_dir" ] || [ -L "$app_dir" ]; then
|
|
macos_app_is_fcc_owned "$app_dir" ||
|
|
fail "An app not managed by Free Claude Code already exists at $app_dir. Move it, then rerun the installer."
|
|
fi
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
print_command mkdir -p "$executable_dir" "$desktop_dir"
|
|
printf '+ write %s, %s, and %s\n' "$owner_file" "$contents_dir/Info.plist" "$executable_path"
|
|
print_command ln -s "$app_dir" "$desktop_link"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$executable_dir" "$desktop_dir"
|
|
printf '%s\n' "$FCC_MACOS_BUNDLE_ID" > "$owner_file"
|
|
cat > "$contents_dir/Info.plist" <<'PLIST'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CFBundleDisplayName</key>
|
|
<string>Free Claude Code</string>
|
|
<key>CFBundleExecutable</key>
|
|
<string>fcc-desktop</string>
|
|
<key>CFBundleIdentifier</key>
|
|
<string>io.github.alishahryar1.free-claude-code</string>
|
|
<key>CFBundleName</key>
|
|
<string>Free Claude Code</string>
|
|
<key>CFBundlePackageType</key>
|
|
<string>APPL</string>
|
|
<key>LSMultipleInstancesProhibited</key>
|
|
<true/>
|
|
<key>LSUIElement</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
desktop_command=$(shell_quote "$tool_bin/fcc-desktop")
|
|
{
|
|
printf '%s\n' '#!/bin/sh'
|
|
printf 'exec %s\n' "$desktop_command"
|
|
} > "$executable_path"
|
|
chmod +x "$executable_path"
|
|
|
|
if [ -L "$desktop_link" ]; then
|
|
if [ "$(readlink "$desktop_link")" = "$app_dir" ]; then
|
|
rm -f "$desktop_link"
|
|
else
|
|
printf 'A non-FCC link already exists at %s; leaving it unchanged.\n' "$desktop_link"
|
|
return 0
|
|
fi
|
|
elif [ -e "$desktop_link" ]; then
|
|
printf 'A non-FCC item already exists at %s; leaving it unchanged.\n' "$desktop_link"
|
|
return 0
|
|
fi
|
|
ln -s "$app_dir" "$desktop_link"
|
|
}
|
|
|
|
parse_args "$@"
|
|
validate_args
|
|
add_known_bin_directories
|
|
|
|
step "Checking for running Free Claude Code processes"
|
|
assert_no_fcc_processes_running
|
|
|
|
step "Checking installation prerequisites"
|
|
require_command curl
|
|
require_command bash
|
|
require_command sh
|
|
require_command mktemp
|
|
|
|
step "Ensuring Claude Code is installed"
|
|
ensure_claude
|
|
|
|
step "Ensuring Codex is installed"
|
|
ensure_codex
|
|
|
|
step "Ensuring Pi is installed"
|
|
ensure_pi
|
|
|
|
step "Ensuring uv $MIN_UV_VERSION or newer is installed"
|
|
ensure_uv
|
|
|
|
step "Installing or updating Free Claude Code"
|
|
install_free_claude_code
|
|
|
|
step "Configuring PATH and verifying Free Claude Code"
|
|
configure_and_verify_free_claude_code
|
|
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
step "Installing the Free Claude Code desktop launcher"
|
|
install_macos_desktop_app
|
|
fi
|
|
|
|
if [ "$dry_run" -eq 1 ]; then
|
|
printf '\nDry run complete. No changes were made.\n'
|
|
else
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
printf '\nFree Claude Code is installed and verified. Open Free Claude Code from Applications or the desktop to run it in the background.\n'
|
|
printf 'For terminal use, start the proxy with: fcc-server\n'
|
|
else
|
|
printf '\nFree Claude Code is installed and verified. Start the proxy with: fcc-server\n'
|
|
fi
|
|
printf 'Run Claude Code with: fcc-claude\n'
|
|
printf 'Run Codex with: fcc-codex\n'
|
|
printf 'Run Pi with: fcc-pi\n'
|
|
fi
|