1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 17:55:08 +00:00

Add dry-run support across destructive commands (#516)

* chore: update contributors [skip ci]

* Add dry-run support across destructive commands

Implement dry-run for uninstall, purge, installer, touchid, completion, and remove flows.\nGuard side effects in uninstall path (launchctl, defaults writes, kill/brew actions), update help/README, and add coverage in CLI/Bats tests.\n\nValidation: ./scripts/check.sh and ./scripts/test.sh (452 tests, 0 failures, 8 skipped).

* test(purge): keep dev-compatible purge coverage

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tw93 <hitw93@gmail.com>
This commit is contained in:
陳德生
2026-03-01 20:03:22 +08:00
committed by GitHub
parent adcd98096a
commit 05446e0847
18 changed files with 1021 additions and 684 deletions

View File

@@ -11,6 +11,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Batch uninstall with a single confirmation.
get_lsregister_path() {
echo "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
}
is_uninstall_dry_run() {
[[ "${MOLE_DRY_RUN:-0}" == "1" ]]
}
# High-performance sensitive data detection (pure Bash, no subprocess)
# Faster than grep for batch operations, especially when processing many apps
has_sensitive_data() {
@@ -77,6 +85,11 @@ stop_launch_services() {
local bundle_id="$1"
local has_system_files="${2:-false}"
if is_uninstall_dry_run; then
debug_log "[DRY RUN] Would unload launch services for bundle: $bundle_id"
return 0
fi
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
# Validate bundle_id format: must be reverse-DNS style (e.g., com.example.app)
@@ -152,6 +165,11 @@ remove_login_item() {
local app_name="$1"
local bundle_id="$2"
if is_uninstall_dry_run; then
debug_log "[DRY RUN] Would remove login item: ${app_name:-$bundle_id}"
return 0
fi
# Skip if no identifiers provided
[[ -z "$app_name" && -z "$bundle_id" ]] && return 0
@@ -201,7 +219,12 @@ remove_file_list() {
safe_remove_symlink "$file" "$use_sudo" && ((++count)) || true
else
if [[ "$use_sudo" == "true" ]]; then
safe_sudo_remove "$file" && ((++count)) || true
if is_uninstall_dry_run; then
debug_log "[DRY RUN] Would sudo remove: $file"
((++count))
else
safe_sudo_remove "$file" && ((++count)) || true
fi
else
safe_remove "$file" true && ((++count)) || true
fi
@@ -437,7 +460,7 @@ batch_uninstall_applications() {
export MOLE_UNINSTALL_MODE=1
# Request sudo if needed.
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
if [[ ${#sudo_apps[@]} -gt 0 && "${MOLE_DRY_RUN:-0}" != "1" ]]; then
if ! sudo -n true 2> /dev/null; then
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
echo ""
@@ -547,12 +570,18 @@ batch_uninstall_applications() {
fi
fi
else
local ret=0
safe_sudo_remove "$app_path" || ret=$?
if [[ $ret -ne 0 ]]; then
local diagnosis
diagnosis=$(diagnose_removal_failure "$ret" "$app_name")
IFS='|' read -r reason suggestion <<< "$diagnosis"
if is_uninstall_dry_run; then
if ! safe_remove "$app_path" true; then
reason="dry-run path validation failed"
fi
else
local ret=0
safe_sudo_remove "$app_path" || ret=$?
if [[ $ret -ne 0 ]]; then
local diagnosis
diagnosis=$(diagnose_removal_failure "$ret" "$app_name")
IFS='|' read -r reason suggestion <<< "$diagnosis"
fi
fi
fi
else
@@ -583,10 +612,14 @@ batch_uninstall_applications() {
remove_file_list "$system_all" "true" > /dev/null
fi
# Clean up macOS defaults (preference domains).
# Defaults writes are side effects that should never run in dry-run mode.
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
if defaults read "$bundle_id" &> /dev/null; then
defaults delete "$bundle_id" 2> /dev/null || true
if is_uninstall_dry_run; then
debug_log "[DRY RUN] Would clear defaults domain: $bundle_id"
else
if defaults read "$bundle_id" &> /dev/null; then
defaults delete "$bundle_id" 2> /dev/null || true
fi
fi
# ByHost preferences (machine-specific).
@@ -644,8 +677,15 @@ batch_uninstall_applications() {
local success_text="app"
[[ $success_count -gt 1 ]] && success_text="apps"
local success_line="Removed ${success_count} ${success_text}"
if is_uninstall_dry_run; then
success_line="Would remove ${success_count} ${success_text}"
fi
if [[ -n "$freed_display" ]]; then
success_line+=", freed ${GREEN}${freed_display}${NC}"
if is_uninstall_dry_run; then
success_line+=", would free ${GREEN}${freed_display}${NC}"
else
success_line+=", freed ${GREEN}${freed_display}${NC}"
fi
fi
# Format app list with max 3 per line.
@@ -730,24 +770,48 @@ batch_uninstall_applications() {
if [[ "$summary_status" == "warn" ]]; then
title="Uninstall incomplete"
fi
if is_uninstall_dry_run; then
title="Uninstall dry run complete"
fi
echo ""
print_summary_block "$title" "${summary_details[@]}"
printf '\n'
if [[ $success_count -gt 0 && ${#success_items[@]} -gt 0 ]]; then
# Kick off LaunchServices rebuild in background immediately after summary.
# The caller shows a 3s "Press Enter" prompt, giving the rebuild time to finish
# before the user returns to the app list — fixes stale Spotlight entries (#490).
(
refresh_launch_services_after_uninstall 2> /dev/null || true
remove_apps_from_dock "${success_items[@]}" 2> /dev/null || true
) > /dev/null 2>&1 &
# Auto-run brew autoremove if Homebrew casks were uninstalled
if [[ $brew_apps_removed -gt 0 ]]; then
if is_uninstall_dry_run; then
log_info "[DRY RUN] Would run brew autoremove"
else
# Show spinner while checking for orphaned dependencies
if [[ -t 1 ]]; then
start_inline_spinner "Checking brew dependencies..."
fi
local autoremove_output removed_count
autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2> /dev/null) || true
removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true)
removed_count=${removed_count:-0}
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ $removed_count -gt 0 ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Cleaned $removed_count orphaned brew dependencies"
echo ""
fi
fi
fi
# brew autoremove can be slow — run in background so the prompt returns quickly.
if [[ $brew_apps_removed -gt 0 ]]; then
(HOMEBREW_NO_ENV_HINTS=1 brew autoremove > /dev/null 2>&1 || true) &
# Clean up Dock entries for uninstalled apps.
if [[ $success_count -gt 0 && ${#success_items[@]} -gt 0 ]]; then
if is_uninstall_dry_run; then
log_info "[DRY RUN] Would refresh LaunchServices and update Dock entries"
else
remove_apps_from_dock "${success_items[@]}" 2> /dev/null || true
refresh_launch_services_after_uninstall 2> /dev/null || true
fi
fi
_cleanup_sudo_keepalive