747 lines
24 KiB
Bash
Executable File
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"
|