#!/bin/sh set -eu system=$(uname -s) machine=$(uname -m) case "$system-$machine" in Darwin-arm64) target=darwin-arm64 ;; Linux-x86_64) target=linux-x64 ;; *) echo "Unsupported platform: $system-$machine. Supported targets: darwin-arm64, win32-x64, linux-x64." >&2 exit 1 ;; esac if [ "$target" = linux-x64 ]; then default_data_dir=${XDG_DATA_HOME:-$HOME/.local/share}/farero else default_data_dir="$HOME/Library/Application Support/farero" fi channel=${FARERO_CHANNEL:-} data_dir=${FARERO_DATA_DIR:-$default_data_dir} uninstall=0 port=${FARERO_PORT:-} https_port=${FARERO_HTTPS_PORT:-} origin=${FARERO_UPDATE_ORIGIN:-https://updates.farero.ai} # The Linux tarball ships this script beside a bootstrap directory, so it installs offline. bootstrap=${FARERO_BOOTSTRAP_DIR:-} case "$0" in */*) script_dir=${0%/*} ;; *) script_dir=. ;; esac if [ -z "$bootstrap" ] && [ -f "$0" ] && [ -d "$script_dir/bootstrap" ]; then bootstrap=$(cd "$script_dir/bootstrap" && pwd) fi command_dir=$HOME/.local/bin # Desktop on Linux is one per-user app, whichever data directory the service uses. # The tarball carries it in a desktop directory beside this script. desktop_source= desktop_staging= if [ "$target" = linux-x64 ]; then desktop_id=ai.farero.Farero xdg_data=${XDG_DATA_HOME:-$HOME/.local/share} desktop_dir=$xdg_data/farero/desktop desktop_launcher=$xdg_data/applications/$desktop_id.desktop desktop_icon=$xdg_data/icons/hicolor/512x512/apps/$desktop_id.png # One graphical hook per installation provides display context to the # service's shared lifecycle policy; it never starts Electron directly. # Desktop 1.33.1 and earlier wrote its own tray login entry, which the hook replaces. legacy_autostart=${XDG_CONFIG_HOME:-$HOME/.config}/autostart/$desktop_id.desktop if [ -f "$0" ] && [ -x "$script_dir/desktop/farero-desktop" ]; then desktop_source=$(cd "$script_dir/desktop" && pwd) fi fi # Paths reach the process table resolved, so compare against the resolved executable. desktop_executable() { readlink -f "$desktop_dir/Farero" 2>/dev/null || printf '%s\n' "$desktop_dir/Farero" } desktop_running() { executable=$(desktop_executable) for process in /proc/[0-9]*; do if [ "$(readlink "$process/exe" 2>/dev/null)" = "$executable" ]; then return 0; fi done return 1 } # Ask a running Desktop to quit, as the macOS and Windows installers do, and wait # for it. The service keeps running. Succeeds when a Desktop was running. close_desktop() { desktop_running || return 1 executable=$(desktop_executable) for process in /proc/[0-9]*; do [ "$(readlink "$process/exe" 2>/dev/null)" = "$executable" ] || continue # Chromium helpers share the executable; the main process has no --type switch. if tr '\000' '\n' <"$process/cmdline" 2>/dev/null | grep -q '^--type='; then continue; fi kill -TERM "${process#/proc/}" 2>/dev/null || true done waited=0 while desktop_running && [ "$waited" -lt 10 ]; do sleep 1 waited=$((waited + 1)) done return 0 } launch_desktop() { nohup "$desktop_dir/farero-desktop" >/dev/null 2>&1 /dev/null 2>&1; then update-desktop-database -q "${desktop_launcher%/*}" >/dev/null 2>&1 || true fi } # Desktop Entry Exec quoting, then the string escape that doubles each backslash. desktop_exec_path() { printf '%s' "$1" | sed -e 's/[\\"`$]/\\&/g' -e 's/\\/\\\\/g' -e 's/%/%%/g' } # A launcher copied into autostart by hand has the same name but not this marker. remove_legacy_autostart() { if [ -f "$legacy_autostart" ] && grep -qx "X-Farero-Autostart=tray" "$legacy_autostart"; then rm -f "$legacy_autostart" fi } remove_desktop() { remove_legacy_autostart # Another data directory may use the same per-user Desktop binary. Remove # only this installation's hook, then retain the shared app while an owned # hook for another installation remains. if [ -f "$desktop_autostart" ] && grep -qx "X-Farero-Autostart=service-tray" "$desktop_autostart"; then rm -f "$desktop_autostart" fi for other_autostart in "${desktop_autostart%/*}"/ai.farero.*.desktop; do [ -f "$other_autostart" ] || continue if grep -qx "X-Farero-Autostart=service-tray" "$other_autostart"; then return; fi done rm -rf "$desktop_dir" "$desktop_dir".install.* "$desktop_dir".previous.* rmdir "${desktop_dir%/*}" 2>/dev/null || true rm -f "$desktop_launcher" "$desktop_icon" refresh_desktop_menu } while [ "$#" -gt 0 ]; do case "$1" in --channel|--data-dir|--port|--https-port) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; } case "$1" in --channel) channel=$2 ;; --data-dir) data_dir=$2 ;; --port) port=$2 ;; --https-port) https_port=$2 ;; esac shift 2 ;; --uninstall) uninstall=1; shift ;; *) echo "Unknown option: $1" >&2; exit 2 ;; esac done if [ "$target" = linux-x64 ]; then installation_id=$(printf '%s' "$data_dir" | cksum | sed 's/ .*//') desktop_autostart=${XDG_CONFIG_HOME:-$HOME/.config}/autostart/ai.farero.$installation_id.desktop fi # Removes the service, Desktop, its menu entry, icon and login entry, and the # command. Chats, worktrees and settings stay in the data directory. if [ "$uninstall" = 1 ]; then if [ "$target" != linux-x64 ]; then echo "--uninstall is for Linux. On macOS, run \`farero service uninstall\`." >&2 exit 2 fi farero_command=$data_dir/runtime/farero desktop_was_running=0 if close_desktop; then desktop_was_running=1; fi if [ -x "$farero_command" ]; then echo "Removing the Farero service..." # stdout can carry the private app URL. if ! "$farero_command" service uninstall --data-dir "$data_dir" >/dev/null; then if [ "$desktop_was_running" = 1 ]; then launch_desktop; fi echo "The Farero service could not be removed, so Farero Desktop and the command stay installed." >&2 exit 1 fi fi remove_desktop if [ -L "$command_dir/farero" ] && [ "$(readlink "$command_dir/farero")" = "$farero_command" ]; then rm -f "$command_dir/farero" fi echo "Farero was removed. Chats, worktrees and settings stay in $data_dir." exit 0 fi # A pinned `x.y.z` channel installs that release and holds it: the stored # channel resolves to the same manifest on every check, so the supervisor # finds nothing newer. `v1.2.3` is accepted and stored as `1.2.3`. case "$channel" in v*|V*) stripped=${channel#?} if printf '%s' "$stripped" | grep -q '^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$'; then channel=$stripped fi ;; esac # An existing installation keeps its channel unless one is given explicitly. if [ -z "$channel" ]; then settings="$data_dir/runtime/settings.json" if [ -f "$settings" ]; then stored=$(sed -n 's/.*"channel"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$settings") case "$stored" in stable|edge) channel=$stored ;; *) if printf '%s' "$stored" | grep -q '^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$'; then channel=$stored else channel=stable fi ;; esac else channel=stable fi fi case "$channel" in stable|edge) ;; *) if ! printf '%s' "$channel" | grep -q '^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$'; then echo "Channel must be stable, edge, or a version (x.y.z)." >&2 exit 2 fi ;; esac if ! command -v git >/dev/null 2>&1 || ! git --version >/dev/null 2>&1; then if [ "$target" = darwin-arm64 ]; then echo "Git is required. Opening the macOS Command Line Tools installer; rerun this command after it finishes." >&2 xcode-select --install >/dev/null 2>&1 || true else echo "Git is required. Install it with your distribution's package manager (for example apt install git or dnf install git), then rerun this command." >&2 fi exit 1 fi # Keep in step with MINIMUM_GIT_VERSION in src/git/version.ts. git_version=$(git --version | sed -n 's/^git version \([0-9][0-9]*\.[0-9][0-9]*\(\.[0-9][0-9]*\)\{0,1\}\).*/\1/p') git_major=${git_version%%.*} git_minor=${git_version#*.} git_minor=${git_minor%%.*} if [ -n "$git_version" ] && { [ "$git_major" -lt 2 ] || { [ "$git_major" -eq 2 ] && [ "$git_minor" -lt 38 ]; }; }; then if [ "$target" = darwin-arm64 ]; then echo "Farero needs Git 2.38 or later, and this computer has Git $git_version. Install the latest Command Line Tools from Software Update, or run \`brew install git\`, then rerun this command." >&2 else echo "Farero needs Git 2.38 or later, and this computer has Git $git_version. Update it with your distribution's package manager, then rerun this command." >&2 fi exit 1 fi if [ -z "$bootstrap" ] && ! command -v curl >/dev/null 2>&1; then echo "curl is required but was not found." >&2 exit 1 fi if [ "$target" = darwin-arm64 ]; then tar=/usr/bin/tar if [ ! -x "$tar" ]; then echo "The macOS system tar at /usr/bin/tar is required." >&2 exit 1 fi else tar=$(command -v tar || true) if [ -z "$tar" ]; then echo "tar is required but was not found." >&2 exit 1 fi fi if ! mkdir -p "$command_dir"; then echo "Could not create the command directory at $command_dir." >&2 exit 1 fi temporary=$(mktemp -d "${TMPDIR:-/tmp}/farero-install.XXXXXX") cleanup() { rm -rf "$temporary" if [ -n "$desktop_staging" ]; then rm -rf "$desktop_staging"; fi } trap cleanup EXIT HUP INT TERM # Copy Desktop beside its install directory before the service changes, so a # full disk refuses here and the swap after the service install is a rename. if [ -n "$desktop_source" ]; then mkdir -p "${desktop_dir%/*}" desktop_staging=$(mktemp -d "$desktop_dir.install.XXXXXX") if ! cp -R "$desktop_source/." "$desktop_staging/"; then echo "Could not copy Farero Desktop into ${desktop_dir%/*}." >&2 exit 1 fi fi one_artifact() { pattern=$1 set -- "$bootstrap"/$pattern if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then echo "Farero installer payload is incomplete: expected one $pattern in $bootstrap" >&2 exit 1 fi printf '%s\n' "$1" } if [ -n "$bootstrap" ]; then one_artifact manifest.json >/dev/null app=$(one_artifact 'farero-*.tgz') runtime_archive=$(one_artifact "farero-runtime-*-$target.tar.gz") artifacts=$bootstrap echo "Installing Farero for $target from $bootstrap..." else case "$channel" in stable|edge) base=${origin%/}/bootstrap/$channel ;; *) base=${origin%/}/bootstrap/v$channel ;; esac echo "Downloading Farero $channel for $target..." download() { case "$origin" in https://*) curl -fsSL --proto '=https' --tlsv1.2 "$1" -o "$2" ;; *) curl -fsSL "$1" -o "$2" ;; esac } download "$base/manifest.json" "$temporary/manifest.json" download "$base/app.tgz" "$temporary/app.tgz" download "$base/runtime-$target.tar.gz" "$temporary/runtime-$target.tar.gz" app=$temporary/app.tgz runtime_archive=$temporary/runtime-$target.tar.gz artifacts=$temporary fi # Keep the app below the runtime so Node resolves the separate dependency bundle. mkdir -p "$temporary/runtime/app" "$tar" -xzf "$runtime_archive" -C "$temporary/runtime" "$tar" -xzf "$app" -C "$temporary/runtime/app" set -- update install --artifacts "$artifacts" --channel "$channel" --data-dir "$data_dir" if [ -n "$port" ]; then set -- "$@" --port "$port"; fi if [ -n "$https_port" ]; then set -- "$@" --https-port "$https_port"; fi if [ -n "$desktop_staging" ]; then set -- "$@" --desktop-executable "$desktop_dir/Farero"; fi set +e echo "Installing components and starting the Farero service..." install_output=$("$temporary/runtime/Farero" "$temporary/runtime/app/package/bin/cli.js" "$@" 2>&1) install_status=$? set -e if [ "$install_status" -ne 0 ]; then printf '%s\n' "$install_output" >&2 exit "$install_status" fi ln -sfn "$data_dir/runtime/farero" "$command_dir/farero" if [ -n "$desktop_staging" ]; then close_desktop || true previous= if [ -e "$desktop_dir" ]; then previous=$desktop_dir.previous.$$ mv "$desktop_dir" "$previous" fi if ! mv "$desktop_staging" "$desktop_dir"; then if [ -n "$previous" ]; then mv "$previous" "$desktop_dir"; fi echo "Could not replace Farero Desktop in $desktop_dir." >&2 exit 1 fi desktop_staging= rm -rf "$desktop_dir".previous.* mkdir -p "${desktop_icon%/*}" "${desktop_launcher%/*}" cp "$desktop_dir/$desktop_id.png" "$desktop_icon.$$" mv "$desktop_icon.$$" "$desktop_icon" desktop_exec=$(desktop_exec_path "$desktop_dir/farero-desktop") cat >"$desktop_launcher.$$" <&2 else mkdir -p "${desktop_autostart%/*}" service_exec=$(desktop_exec_path "$data_dir/runtime/farero") service_data=$(desktop_exec_path "$data_dir") cat >"$desktop_autostart.$$" </dev/null 2>&1; then opened=1; fi elif [ -n "$full_url" ] && command -v xdg-open >/dev/null 2>&1; then if xdg-open "$full_url" >/dev/null 2>&1; then opened=1; fi fi fi if [ -n "$safe_url" ]; then echo "Farero is ready at $safe_url"; fi if [ "$opened" = desktop ]; then echo "Opening Farero Desktop." elif [ "$opened" -eq 0 ] && [ -n "$safe_url" ]; then echo "The browser did not open automatically. Run \`$command_dir/farero service status\` to print the local app URL, or rerun this installer." fi case ":$PATH:" in *":$command_dir:"*) echo "Command installed at $command_dir/farero." ;; *) echo "Command installed at $command_dir/farero. Add it to your PATH: export PATH=\"$command_dir:\$PATH\"." ;; esac echo "In Farero, open the computer connection menu to show the QR code for your phone."