Files
2026-07-15 08:58:25 +00:00

747 lines
24 KiB
Bash
Executable File

#!/bin/sh
# Omnigent uninstaller. POSIX sh by design so it still works when the Python
# wheel is wedged or PATH is broken.
set -u
MARKER_BEGIN="# >>> Omnigent installer >>>"
MARKER_END="# <<< Omnigent installer <<<"
TAB=$(printf '\t')
TARGETS=""
DRY_RUN=false
YES=false
JSON=false
FORCE=false
PURGE=false
NO_BACKUP=false
PURGE_WORKSPACE=false
MODIFY_EXTERNAL_CONFIG=false
ASSUME_INFERRED=false
DESTRUCTIVE_FLAG=false
EXPLICIT_TARGET=false
DONE=0
SKIPPED=0
FAILED=0
REPORTED=0
EXIT_CODE=0
ACTIONS_FILE="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-actions.XXXXXX")" || exit 1
BACKUPS_FILE="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-backups.XXXXXX")" || exit 1
cleanup() {
rm -f "$ACTIONS_FILE" "$BACKUPS_FILE"
}
trap cleanup EXIT HUP INT TERM
usage() {
cat <<'EOF'
Usage: uninstall_oss.sh [cli|state|desktop-data|all ...] [flags]
Flags:
--purge Remove state data (backs up first)
--purge-workspace With --purge, also remove ~/omnigent non-interactively
--dry-run Print planned actions only
--yes Non-interactive for auto-removable artifacts
--json Emit a machine-readable summary
--force SIGKILL stubborn processes and override refusals
--modify-external-config Allow marker-scoped third-party config edits
--no-backup Skip purge backup creation
--assume-inferred Allow inferred entries when combined with their gates
EOF
}
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g'
}
record_action() {
artifact="$1"
path="$2"
planned="$3"
status="$4"
gate="$5"
detail="$6"
case "$status" in
done) DONE=$((DONE + 1)) ;;
skipped) SKIPPED=$((SKIPPED + 1)) ;;
failed) FAILED=$((FAILED + 1)); EXIT_CODE=1 ;;
reported) REPORTED=$((REPORTED + 1)) ;;
esac
printf '{"artifact":"%s","path":"%s","planned":"%s","status":"%s","gate":%s,"detail":"%s"}\n' \
"$(json_escape "$artifact")" \
"$(json_escape "$path")" \
"$(json_escape "$planned")" \
"$(json_escape "$status")" \
"$(if [ -n "$gate" ]; then printf '"%s"' "$(json_escape "$gate")"; else printf 'null'; fi)" \
"$(json_escape "$detail")" >>"$ACTIONS_FILE"
if [ "$JSON" != true ]; then
printf '%s: %s (%s)\n' "$status" "$artifact${path:+ $path}" "$detail"
fi
}
has_target() {
case " $TARGETS " in
*" $1 "*) return 0 ;;
*) return 1 ;;
esac
}
add_target() {
case "$1" in
all)
add_target cli
add_target state
add_target desktop-data
;;
cli | state | desktop-data)
if ! has_target "$1"; then
TARGETS="${TARGETS}${TARGETS:+ }$1"
fi
;;
*)
usage >&2
exit 3
;;
esac
}
while [ "$#" -gt 0 ]; do
case "$1" in
--purge) PURGE=true; DESTRUCTIVE_FLAG=true; add_target state ;;
--purge-workspace) PURGE_WORKSPACE=true; DESTRUCTIVE_FLAG=true ;;
--dry-run) DRY_RUN=true ;;
--yes) YES=true; DESTRUCTIVE_FLAG=true ;;
--json) JSON=true ;;
--force) FORCE=true; DESTRUCTIVE_FLAG=true ;;
--modify-external-config) MODIFY_EXTERNAL_CONFIG=true; DESTRUCTIVE_FLAG=true ;;
--no-backup) NO_BACKUP=true; DESTRUCTIVE_FLAG=true ;;
--assume-inferred) ASSUME_INFERRED=true; DESTRUCTIVE_FLAG=true ;;
-h | --help) usage; exit 0 ;;
--*) usage >&2; exit 3 ;;
*) EXPLICIT_TARGET=true; add_target "$1" ;;
esac
shift
done
if [ "$EXPLICIT_TARGET" != true ]; then
add_target cli
fi
if [ "$DESTRUCTIVE_FLAG" != true ]; then
DRY_RUN=true
fi
state_home() {
if [ -n "${OMNIGENT_DATA_DIR:-}" ]; then
printf '%s\n' "$OMNIGENT_DATA_DIR"
else
printf '%s/.omnigent\n' "$HOME"
fi
}
is_pid_alive() {
pid="$1"
[ -n "$pid" ] || return 1
kill -0 "$pid" 2>/dev/null || return 1
state="$(ps -o stat= -p "$pid" 2>/dev/null | awk 'NR == 1 { print $1 }')"
case "$state" in
Z*) return 1 ;;
esac
return 0
}
stop_pid() {
pid="$1"
origin="$2"
if ! is_pid_alive "$pid"; then
record_action process "$origin" stop done "" "already stopped"
return 0
fi
if [ "$DRY_RUN" = true ]; then
record_action process "$origin" stop reported "" "would SIGTERM pid $pid"
return 0
fi
kill -TERM "$pid" 2>/dev/null || true
i=0
while [ "$i" -lt 50 ]; do
if ! is_pid_alive "$pid"; then
record_action process "$origin" stop done "" "stopped pid $pid"
return 0
fi
sleep 0.1
i=$((i + 1))
done
if [ "$FORCE" = true ]; then
kill -KILL "$pid" 2>/dev/null || true
record_action process "$origin" stop done "" "SIGKILL sent to pid $pid"
return 0
fi
record_action process "$origin" stop failed "--force" "pid $pid did not stop"
EXIT_CODE=2
return 1
}
stop_processes() {
home_dir="$(state_home)"
pidfiles="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-pidfiles.XXXXXX")" || return 1
: >"$pidfiles"
if [ -d "$home_dir" ]; then
for root in "$home_dir/run" "$home_dir/daemons" "$home_dir/runners" "$home_dir/local_server"; do
[ -d "$root" ] || continue
find "$root" -type f \( -name '*.pid' -o -name 'local_server.pid' \) 2>/dev/null >>"$pidfiles" || true
done
for pidfile in "$home_dir/local_server.pid" "$home_dir/host.pid"; do
[ -f "$pidfile" ] && printf '%s\n' "$pidfile" >>"$pidfiles"
done
while IFS= read -r pidfile; do
[ -n "$pidfile" ] || continue
pid="$(awk 'NR == 1 { sub(/[^0-9].*$/, ""); print; exit }' "$pidfile" 2>/dev/null || true)"
[ -n "$pid" ] && stop_pid "$pid" "$pidfile" || true
done <"$pidfiles"
fi
if command -v tmux >/dev/null 2>&1; then
sessions_file="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-tmux.XXXXXX")" || return 1
tmux list-sessions -F '#S' >"$sessions_file" 2>/dev/null || true
while IFS= read -r session; do
case "$session" in
omnigent:*)
if [ "$DRY_RUN" = true ]; then
record_action tmux "$session" stop reported "" "would kill tmux session"
elif tmux kill-session -t "$session" 2>/dev/null; then
record_action tmux "$session" stop done "" "killed tmux session"
else
record_action tmux "$session" stop failed "" "failed to kill tmux session"
fi
;;
esac
done <"$sessions_file"
rm -f "$sessions_file"
fi
rm -f "$pidfiles"
if [ "$EXIT_CODE" = 2 ]; then
return 1
fi
}
unload_launch_agents() {
[ -n "${OMNIGENT_UNINSTALL_LEDGER_MANIFEST:-}" ] && [ -f "$OMNIGENT_UNINSTALL_LEDGER_MANIFEST" ] || return 0
while IFS="$TAB" read -r artifact kind unit_path label source confidence rest; do
[ "$artifact" = launch_agent ] || continue
if [ "$DRY_RUN" = true ]; then
record_action launch_agent "$unit_path" unload reported "" "would unload $kind $label"
continue
fi
case "$kind" in
launchd)
if command -v launchctl >/dev/null 2>&1; then
launchctl bootout "gui/$(id -u)" "$unit_path" >/dev/null 2>&1 || launchctl unload "$unit_path" >/dev/null 2>&1 || true
record_action launch_agent "$unit_path" unload done "" "requested launchd unload for $label"
else
record_action launch_agent "$unit_path" unload skipped "" "launchctl not found"
fi
;;
systemd_user)
if command -v systemctl >/dev/null 2>&1; then
systemctl --user disable --now "$label" >/dev/null 2>&1 || systemctl --user stop "$label" >/dev/null 2>&1 || true
record_action launch_agent "$unit_path" unload done "" "requested systemd user stop for $label"
else
record_action launch_agent "$unit_path" unload skipped "" "systemctl not found"
fi
;;
*)
record_action launch_agent "$unit_path" unload skipped "" "unknown launch agent kind $kind"
;;
esac
if [ -e "$unit_path" ]; then
if rm -f "$unit_path"; then
record_action launch_agent "$unit_path" remove done "" "removed unit file"
else
record_action launch_agent "$unit_path" remove failed "" "failed to remove unit file"
fi
else
record_action launch_agent "$unit_path" remove skipped "" "unit file already absent"
fi
done <"$OMNIGENT_UNINSTALL_LEDGER_MANIFEST"
}
profile_candidates() {
printf '%s\n' \
"$HOME/.zprofile" \
"$HOME/.zshrc" \
"$HOME/.bash_profile" \
"$HOME/.bashrc" \
"$HOME/.profile" \
"$HOME/.config/fish/config.fish"
if [ -d "$HOME/.config/fish/conf.d" ]; then
find "$HOME/.config/fish/conf.d" -type f -name '*.fish' 2>/dev/null
fi
}
has_shell_install_signal() {
[ -n "${OMNIGENT_UNINSTALL_LEDGER_SOURCE:-}" ] && [ "$OMNIGENT_UNINSTALL_LEDGER_SOURCE" != unknown ] && return 0
[ -f "$(state_home)/installation_id" ] && return 0
command -v omnigent >/dev/null 2>&1 && return 0
command -v omni >/dev/null 2>&1 && return 0
profiles_file="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-anchor-profiles.XXXXXX")" || return 1
profile_candidates >"$profiles_file"
while IFS= read -r profile; do
if profile_has_block "$profile"; then
rm -f "$profiles_file"
return 0
fi
done <"$profiles_file"
rm -f "$profiles_file"
return 1
}
profile_has_block() {
[ -f "$1" ] || return 1
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
$0 == begin { found_begin=1 }
found_begin && $0 == end { found_end=1 }
END { exit(found_begin && found_end ? 0 : 1) }
' "$1"
}
sha256_file() {
file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$file" | awk '{print $NF}'
else
return 1
fi
}
write_profile_block() {
profile="$1"
output="$2"
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
$0 == begin { printing=1 }
printing { print }
printing && $0 == end { exit }
' "$profile" >"$output"
}
remove_profile_block() {
profile="$1"
expected_sha="${2:-}"
if ! profile_has_block "$profile"; then
record_action profile_block "$profile" remove skipped "" "marker block absent"
return 0
fi
line_range="$(awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
$0 == begin { start=NR }
start && $0 == end { print start "-" NR; exit }
' "$profile")"
if [ "$DRY_RUN" = true ]; then
record_action profile_block "$profile" remove reported "" "would remove lines $line_range"
return 0
fi
if [ -n "$expected_sha" ]; then
block_file="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-block.XXXXXX")" || return 1
write_profile_block "$profile" "$block_file"
actual_sha="$(sha256_file "$block_file" 2>/dev/null || true)"
rm -f "$block_file"
if [ -z "$actual_sha" ]; then
record_action profile_block "$profile" remove failed "" "could not verify block hash"
return 1
fi
if [ "$actual_sha" != "$expected_sha" ] && [ "$FORCE" != true ]; then
record_action profile_block "$profile" remove failed "--force" "marker block hash mismatch; refusing tampered block"
EXIT_CODE=3
return 1
fi
fi
backup="$profile.omnigent.bak.$(date -u +%Y%m%dT%H%M%SZ)"
tmp="$(mktemp "$profile.omnigent.tmp.XXXXXX")" || return 1
if ! cp "$profile" "$backup"; then
record_action profile_block "$profile" remove failed "" "failed to write backup"
return 1
fi
if awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
$0 == begin { skipping=1; found=1; next }
skipping && $0 == end { skipping=0; next }
!skipping { print }
END { exit(found ? 0 : 42) }
' "$profile" >"$tmp" && mv "$tmp" "$profile"; then
record_action profile_block "$profile" remove done "" "block removed, backup at $backup"
else
rm -f "$tmp"
record_action profile_block "$profile" remove failed "" "failed to remove block"
fi
}
cleanup_profiles() {
if [ -n "${OMNIGENT_UNINSTALL_LEDGER_MANIFEST:-}" ] && [ -f "$OMNIGENT_UNINSTALL_LEDGER_MANIFEST" ]; then
while IFS="$TAB" read -r artifact profile expected_sha source confidence rest; do
[ "$artifact" = profile_block ] || continue
remove_profile_block "$profile" "$expected_sha"
[ "$EXIT_CODE" = 3 ] && return 1
done <"$OMNIGENT_UNINSTALL_LEDGER_MANIFEST"
else
profiles_file="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-profiles.XXXXXX")" || return 1
profile_candidates >"$profiles_file"
while IFS= read -r profile; do
[ -n "$profile" ] || continue
if profile_has_block "$profile"; then
remove_profile_block "$profile" ""
if [ "$EXIT_CODE" = 3 ]; then
rm -f "$profiles_file"
return 1
fi
fi
done <"$profiles_file"
rm -f "$profiles_file"
fi
}
remove_delimited_external_config() {
path="$1"
marker="$2"
[ -f "$path" ] || { record_action external_config "$path" remove skipped "" "already absent"; return 0; }
if ! grep -F "$marker" "$path" >/dev/null 2>&1; then
record_action external_config "$path" remove skipped "" "marker absent"
return 0
fi
if [ "$DRY_RUN" = true ]; then
record_action external_config "$path" remove reported "" "would remove marker block $marker"
return 0
fi
backup="$path.omnigent.bak.$(date -u +%Y%m%dT%H%M%SZ)"
tmp="$(mktemp "$path.omnigent.tmp.XXXXXX")" || return 1
cp "$path" "$backup" || { record_action external_config "$path" remove failed "" "failed to write backup"; return 1; }
if awk -v marker="$marker" '
index($0, marker) && !skipping { skipping=1; found=1; next }
index($0, marker) && skipping { skipping=0; next }
!skipping { print }
END { exit(found && !skipping ? 0 : 42) }
' "$path" >"$tmp" && mv "$tmp" "$path"; then
record_action external_config "$path" remove done "" "removed marker block, backup at $backup"
else
rm -f "$tmp"
record_action external_config "$path" remove failed "" "failed to remove marker block"
fi
}
remove_keyed_external_config() {
path="$1"
marker="$2"
format="$3"
[ -f "$path" ] || { record_action external_config "$path" remove skipped "" "already absent"; return 0; }
if [ "$DRY_RUN" = true ]; then
record_action external_config "$path" remove reported "" "would remove $marker from $format config"
return 0
fi
if [ "$format" = toml ]; then
backup="$path.omnigent.bak.$(date -u +%Y%m%dT%H%M%SZ)"
tmp="$(mktemp "$path.omnigent.tmp.XXXXXX")" || return 1
cp "$path" "$backup" || { record_action external_config "$path" remove failed "" "failed to write backup"; return 1; }
if awk -v marker="$marker" '
$0 == "[" marker "]" || $0 ~ "^\\[" marker "\\." { skipping=1; found=1; next }
skipping && /^\[/ { skipping=0 }
!skipping { print }
END { exit(found ? 0 : 42) }
' "$backup" >"$tmp" && mv "$tmp" "$path"; then
record_action external_config "$path" remove done "" "removed TOML table $marker, backup at $backup"
else
rm -f "$tmp"
record_action external_config "$path" remove skipped "" "TOML table $marker absent"
fi
return 0
fi
py="$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)"
if [ -z "$py" ]; then
record_action external_config "$path" remove skipped "" "python not found for structured config edit"
return 0
fi
backup="$path.omnigent.bak.$(date -u +%Y%m%dT%H%M%SZ)"
cp "$path" "$backup" || { record_action external_config "$path" remove failed "" "failed to write backup"; return 1; }
if "$py" - "$path" "$marker" "$format" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
marker = sys.argv[2]
fmt = sys.argv[3]
if fmt != "json":
raise SystemExit(42)
original = path.read_text()
data = json.loads(original)
current = data
parts = marker.split(".")
for part in parts[:-1]:
current = current.get(part, {}) if isinstance(current, dict) else {}
if not isinstance(current, dict) or parts[-1] not in current:
raise SystemExit(42)
current.pop(parts[-1])
indent = 2 if "\n \"" in original else None
path.write_text(json.dumps(data, indent=indent, sort_keys=False) + "\n")
PY
then
record_action external_config "$path" remove done "" "removed $marker, backup at $backup"
else
record_action external_config "$path" remove skipped "" "unsupported structured format; backup kept at $backup"
fi
}
cleanup_external_configs() {
[ -n "${OMNIGENT_UNINSTALL_LEDGER_MANIFEST:-}" ] && [ -f "$OMNIGENT_UNINSTALL_LEDGER_MANIFEST" ] || return 0
while IFS="$TAB" read -r artifact path marker format expected_sha source confidence rest; do
[ "$artifact" = external_config ] || continue
if [ "$MODIFY_EXTERNAL_CONFIG" != true ]; then
record_action external_config "$path" remove skipped "--modify-external-config" "gate not provided"
continue
fi
if { [ "$source" = inferred ] || [ "$confidence" = low ] || [ "$confidence" = medium ]; } && [ "$ASSUME_INFERRED" != true ]; then
record_action external_config "$path" remove skipped "--assume-inferred" "confidence gate not provided"
continue
fi
if [ "$format" = delimited_block ]; then
remove_delimited_external_config "$path" "$marker"
else
remove_keyed_external_config "$path" "$marker" "$format"
fi
done <"$OMNIGENT_UNINSTALL_LEDGER_MANIFEST"
}
archive_path_for() {
target="$1"
if [ -n "${XDG_STATE_HOME:-}" ]; then
backup_root="$XDG_STATE_HOME/omnigent-backups"
else
backup_root="$HOME/.omnigent-backups"
fi
mkdir -p "$backup_root" || return 1
ts="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
base="$(printf '%s' "$target" | sed 's/[^A-Za-z0-9._-]/_/g; s/^_*//; s/_*$//')"
if command -v zstd >/dev/null 2>&1; then
printf '%s/%s-%s-%s.tar.zst\n' "$backup_root" "$ts" "$base" "$$"
else
printf '%s/%s-%s-%s.tar.gz\n' "$backup_root" "$ts" "$base" "$$"
fi
}
backup_path() {
target="$1"
[ -e "$target" ] || return 0
if [ "$NO_BACKUP" = true ]; then
return 0
fi
archive="$(archive_path_for "$target")" || return 1
parent="$(dirname "$target")"
base="$(basename "$target")"
case "$archive" in
*.tar.zst)
tar_tmp="$(mktemp "$archive.tar.XXXXXX")" || return 1
if ! tar -cf "$tar_tmp" -C "$parent" "$base"; then
rm -f "$tar_tmp" "$archive"
return 1
fi
if ! zstd -q -o "$archive" "$tar_tmp"; then
rm -f "$tar_tmp" "$archive"
return 1
fi
rm -f "$tar_tmp"
;;
*.tar.gz)
tar -czf "$archive" -C "$parent" "$base" || return 1
;;
esac
printf '%s\n' "$archive" >>"$BACKUPS_FILE"
record_action backup "$target" archive done "" "restore with: tar -xf $archive -C $parent"
}
remove_tree() {
artifact="$1"
path="$2"
gate="$3"
if [ ! -e "$path" ]; then
record_action "$artifact" "$path" remove skipped "" "already absent"
return 0
fi
if [ -n "$gate" ]; then
record_action "$artifact" "$path" remove skipped "$gate" "gate not provided"
return 0
fi
if [ "$DRY_RUN" = true ]; then
size="$(du -sk "$path" 2>/dev/null | awk '{print $1 " KiB"}' || printf unknown)"
record_action "$artifact" "$path" remove reported "" "would remove $size"
return 0
fi
if backup_path "$path" && rm -rf "$path"; then
record_action "$artifact" "$path" remove done "" "removed"
else
record_action "$artifact" "$path" remove failed "" "failed to remove"
fi
}
desktop_paths() {
case "$(uname -s)" in
Darwin)
printf '%s\n' \
"$HOME/Library/Application Support/Omnigent" \
"$HOME/Library/Caches/Omnigent" \
"$HOME/Library/Logs/Omnigent"
;;
*)
printf '%s\n' \
"${XDG_CONFIG_HOME:-$HOME/.config}/Omnigent" \
"${XDG_CACHE_HOME:-$HOME/.cache}/Omnigent" \
"${XDG_STATE_HOME:-$HOME/.local/state}/Omnigent"
;;
esac
}
purge_state() {
remove_tree state "$(state_home)" ""
workspace="$HOME/omnigent"
if [ -e "$workspace" ]; then
if [ "$PURGE_WORKSPACE" = true ]; then
remove_tree workspace "$workspace" ""
elif [ "$YES" = true ]; then
record_action workspace "$workspace" remove skipped "--purge-workspace" "workspace kept"
elif [ "$DRY_RUN" = true ]; then
record_action workspace "$workspace" remove reported "--purge-workspace" "would prompt separately"
else
printf 'Remove workspace %s? [y/N] ' "$workspace" >/dev/tty 2>/dev/null || true
answer=
IFS= read -r answer </dev/tty 2>/dev/null || true
case "$answer" in
y | Y | yes | YES | Yes) remove_tree workspace "$workspace" "" ;;
*) record_action workspace "$workspace" remove skipped "--purge-workspace" "workspace kept" ;;
esac
fi
fi
}
purge_desktop_data() {
desktop_file="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-desktop.XXXXXX")" || return 1
desktop_paths >"$desktop_file"
while IFS= read -r path; do
[ -n "$path" ] || continue
remove_tree desktop_data "$path" ""
done <"$desktop_file"
rm -f "$desktop_file"
}
report_shared_deps() {
for dep in uv node npm tmux bwrap; do
dep_path="$(command -v "$dep" 2>/dev/null || true)"
if [ -n "$dep_path" ]; then
record_action shared_dep "$dep" report reported "" "present at $dep_path; not removed"
fi
done
}
uninstall_wheel() {
if [ "$DRY_RUN" = true ]; then
record_action wheel omnigent remove reported "" "would run uv tool uninstall omnigent"
return 0
fi
if ! command -v uv >/dev/null 2>&1; then
if ! command -v omnigent >/dev/null 2>&1 && ! command -v omni >/dev/null 2>&1; then
record_action wheel omnigent remove skipped "" "already absent"
return 0
fi
record_action wheel omnigent remove failed "" "uv not found; remove the tool manually"
return 1
fi
uv_output="$(mktemp "${TMPDIR:-/tmp}/omnigent-uninstall-uv.XXXXXX")" || return 1
if uv tool uninstall omnigent >"$uv_output" 2>&1; then
rm -f "$uv_output"
record_action wheel omnigent remove done "" "uv tool uninstall omnigent"
else
output="$(cat "$uv_output" 2>/dev/null || true)"
rm -f "$uv_output"
case "$output" in
*'not installed'* | *'No tool'* | *'not found'*)
record_action wheel omnigent remove skipped "" "already absent"
;;
*)
record_action wheel omnigent remove failed "" "uv tool uninstall failed: $output"
;;
esac
fi
}
emit_json() {
printf '{\n'
printf ' "schema_version": 1,\n'
printf ' "dry_run": %s,\n' "$DRY_RUN"
printf ' "ledger_source": "%s",\n' "${OMNIGENT_UNINSTALL_LEDGER_SOURCE:-unknown}"
printf ' "actions": [\n'
first=true
if [ -f "$ACTIONS_FILE" ]; then
while IFS= read -r line; do
if [ "$first" = true ]; then first=false; else printf ',\n'; fi
printf ' %s' "$line"
done <"$ACTIONS_FILE"
fi
printf '\n ],\n'
printf ' "backups": ['
first=true
if [ -f "$BACKUPS_FILE" ]; then
while IFS= read -r line; do
if [ "$first" = true ]; then first=false; else printf ','; fi
printf '"%s"' "$(json_escape "$line")"
done <"$BACKUPS_FILE"
fi
printf '],\n'
printf ' "summary": {"done": %s, "skipped": %s, "failed": %s, "reported": %s},\n' "$DONE" "$SKIPPED" "$FAILED" "$REPORTED"
printf ' "exit_code": %s\n' "$EXIT_CODE"
printf '}\n'
}
if ! has_shell_install_signal; then
record_action anchor omnigent detect failed "" "no Omnigent install detected"
EXIT_CODE=3
[ "$JSON" = true ] && emit_json
exit "$EXIT_CODE"
fi
unload_launch_agents
if ! stop_processes; then
[ "$JSON" = true ] && emit_json
exit "$EXIT_CODE"
fi
if has_target cli; then
cleanup_profiles
if [ "$EXIT_CODE" = 3 ]; then
[ "$JSON" = true ] && emit_json
exit "$EXIT_CODE"
fi
cleanup_external_configs
fi
if has_target state; then
if [ "$PURGE" = true ]; then
purge_state
else
record_action state "$(state_home)" remove skipped "--purge" "state preserved"
fi
fi
if has_target desktop-data; then
purge_desktop_data
fi
report_shared_deps
if has_target cli; then
uninstall_wheel
fi
if [ "$JSON" = true ]; then
emit_json
elif [ "$DRY_RUN" = true ]; then
printf 'Preview only — nothing was changed. Re-run with --yes to apply CLI cleanup, or --purge --yes to remove state.\n'
elif [ -f "$BACKUPS_FILE" ]; then
while IFS= read -r backup; do
printf 'backup: %s\n' "$backup"
done <"$BACKUPS_FILE"
fi
exit "$EXIT_CODE"