#!/bin/sh # Foxora Studio recovery. Read-only unless an explicit repair option is supplied. # No project/database/pack or OS-vault credential is deleted or inspected. set -eu umask 077 mode=check case "${1:-}" in ''|--check) ;; --repair-signin) mode=signin ;; --repair-view) mode=view ;; --help) printf '%s\n' 'Usage: sh repair.sh [--check|--repair-signin|--repair-view]' 'Quit Studio first. Sign-in recovery preserves old credentials and requires a new login.' 'View recovery moves only the app webview profile to a private backup; preferences reset.' 'Projects, tasks, browser packs and model packs are never removed.'; exit 0 ;; *) printf '%s\n' 'Unknown option. Use --help.' >&2; exit 2 ;; esac [ "$#" -le 1 ] || { echo 'Too many arguments.' >&2; exit 2; } os=$(uname -s) case "$os" in Darwin|Linux) ;; *) echo 'Use repair-windows.ps1 on Windows.' >&2; exit 2;; esac [ "$(id -u)" -ne 0 ] || { echo 'Run as the affected desktop user, without sudo.' >&2; exit 2; } data=${FOXORA_APP_DATA:-"$HOME/.foxora"} case "$data" in /*) ;; *) echo 'FOXORA_APP_DATA must be absolute.' >&2; exit 2;; esac # Newlines/tabs would make the backup manifest ambiguous. case "$data$HOME" in *" "*|*"$(printf '\t')"*) echo 'Unsupported control character in path.' >&2; exit 2;; esac [ ! -L "$data" ] || { echo 'Data home is a symlink. Use its real path in FOXORA_APP_DATA.' >&2; exit 2; } [ -d "$data" ] || { echo 'No existing Foxora data folder. No recovery needed; start Studio normally.'; exit 0; } data=$(cd "$data" && pwd -P) [ "$data" != / ] && [ "$data" != "$HOME" ] || { echo 'Refusing an unsafe data-home path.' >&2; exit 2; } if [ "$os" = Darwin ]; then view="$HOME/Library/WebKit/ai.foxora.desktop" # Tauri's WKWebView profile is app-scoped; the engine lives elsewhere. else view="${XDG_DATA_HOME:-$HOME/.local/share}/ai.foxora.desktop" fi case "$view" in /*) ;; *) echo 'The webview data directory must be absolute.' >&2; exit 2;; esac if [ "$mode" = view ] || { [ "$mode" = signin ] && [ "$os" = Linux ]; }; then case "$data/" in "$view/"*) echo 'Data home must not be inside the webview profile.' >&2; exit 2;; esac fi printf 'Foxora recovery check (%s)\n' "$os" for item in studio.db device-session.json auth-storage workspaces browser/packs; do if [ -e "$data/$item" ]; then printf 'Present: %s\n' "$item"; fi done [ ! -e "$view" ] || echo 'Present: app webview profile' echo 'Generated fox names are normal for new tasks; existing folders are not evidence of another account.' echo 'This tool cannot repair a server authorization problem or patch a compiled OAuth callback.' [ "$mode" != check ] || exit 0 command -v pgrep >/dev/null 2>&1 || { echo 'pgrep is required to verify Studio is closed.' >&2; exit 2; } for process in foxora-desktop foxora-engine 'Foxora Studio'; do if pgrep -u "$(id -u)" -x "$process" >/dev/null 2>&1; then echo 'Quit Foxora Studio from its tray/menu and wait for its engine to exit, then retry. No processes were stopped.' >&2; exit 3 fi done [ ! -L "$data/recovery" ] || { echo 'Recovery folder must not be a symlink.' >&2; exit 2; } mkdir -p "$data/recovery" chmod 700 "$data/recovery" lock="$data/recovery/.repair-lock" mkdir "$lock" 2>/dev/null || { echo 'Another recovery may be active. No change made.' >&2; exit 3; } trap 'rmdir "$lock" 2>/dev/null || :' EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM backup=$(mktemp -d "$data/recovery/repair-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX") printf 'Private recovery backup: %s\n' "$backup" : > "$backup/moves.tsv" move_aside() { src=$1; label=$2 [ ! -L "$src" ] || { echo 'Refusing a symlink recovery target.' >&2; exit 2; } if [ -e "$src" ]; then printf '%s\t%s\n' "$label" "$src" >> "$backup/moves.tsv" mv "$src" "$backup/$label" fi } if [ "$mode" = signin ] && [ "$os" = Darwin ]; then # Must match auth_storage.rs storage_name and auth_storage_scope.rs marker. home_hash=$(printf %s "$data" | shasum -a 256 | awk '{print $1}') profile="ai.foxora.desktop:$home_hash:production:lcskvopauvdhjxpjdhra" marker_hash=$(printf %s "$profile" | shasum -a 256 | awk '{print $1}') [ ! -L "$data/auth-storage" ] || { echo 'Auth storage must not be a symlink.' >&2; exit 2; } mkdir -p "$data/auth-storage" marker="$data/auth-storage/$marker_hash.generation" generation=$(uuidgen) || exit 2 move_aside "$marker" auth-generation printf '%s\n' "$marker" > "$backup/new-marker.txt" pending=$(mktemp "$data/auth-storage/.repair-XXXXXX") printf %s "$generation" | tr '[:upper:]' '[:lower:]' > "$pending" mv "$pending" "$marker" move_aside "$data/device-session.json" device-session.json else # Linux has no native auth vault bridge in this release. Its sign-in is in # the app webview profile. Do not touch a user's ordinary browser profile. move_aside "$view" webview if [ "$mode" = signin ]; then move_aside "$data/device-session.json" device-session.json; fi fi cp "$0" "$backup/recovery-script.sh" 2>/dev/null || : printf '%s\n' 'Backup contains private local state. Do not upload it or send it to support.' > "$backup/PRIVATE.txt" printf 'Recovery prepared. Backup: %s\n' "$backup" echo 'Reopen Studio. For sign-in, close old login tabs and start ONE new sign-in attempt.' echo 'If PKCE fails again, stop retrying and report the exact error. An app update may be required.' echo 'Project/task databases and all project files and packs are unchanged.' echo 'To undo: quit Studio; preserve newly created target files separately, then move each backup item to the original path listed in moves.tsv. Remove only the new marker listed in new-marker.txt before restoring auth-generation, if present.'