Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ Key coverage areas include:
- bundle ID boundary matching and malformed-ID rejection (`tests/uninstall_safety.bats`)
- bash 3.2 empty-array nounset compatibility (`tests/uninstall_scan_bash32.bats`)

Spotlight user search caches are protected from default `mo clean` removal. The explicit `mo optimize --spotlight-deep-reset` path is separate from normal cleanup and optimization, requires typed confirmation, and is limited to current-user Spotlight/CoreSpotlight cache targets before restarting indexing for `/`.

## Known Limitations and Future Work

- Cleanup is destructive. Most cleanup flows do not provide undo.
Expand Down
17 changes: 17 additions & 0 deletions bin/completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ for entry in "${MOLE_COMMANDS[@]}"; do
done
command_words="${command_names[*]}"
clean_option_words="--dry-run -n --external --whitelist --debug --help -h"
optimize_option_words="--dry-run --spotlight-deep-reset --whitelist --debug --help -h"
analyze_option_words="--json --help -h"
purge_option_words="--paths --dry-run -n --include-empty --debug --help -h"

Expand All @@ -35,6 +36,11 @@ emit_fish_completions() {
printf 'complete -f -c %s -n "__fish_seen_subcommand_from clean" -l whitelist -d "Manage protected paths"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from clean" -l debug -d "Show detailed logs"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from clean" -l help -s h -d "Show help"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from optimize optimise" -l dry-run -d "Preview optimization without making changes"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from optimize optimise" -l spotlight-deep-reset -d "Rebuild Spotlight user search caches after confirmation"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from optimize optimise" -l whitelist -d "Manage protected items"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from optimize optimise" -l debug -d "Show detailed logs"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from optimize optimise" -l help -s h -d "Show help"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from analyze analyse" -l json -d "Output analysis as JSON"\n' "$cmd"
printf 'complete -f -c %s -n "__fish_seen_subcommand_from analyze analyse" -l help -s h -d "Show help"\n' "$cmd"
printf 'complete -c %s -n "__fish_seen_subcommand_from analyze analyse; and not __fish_seen_argument -l json -l help -s h" -a "(__fish_complete_directories)" -d "Path to analyze"\n' "$cmd"
Expand Down Expand Up @@ -338,6 +344,9 @@ _mole_completions()
purge)
COMPREPLY=( \$(compgen -W "$purge_option_words" -- "\$cur_word") )
;;
optimize|optimise)
COMPREPLY=( \$(compgen -W "$optimize_option_words" -- "\$cur_word") )
;;
completion)
COMPREPLY=( \$(compgen -W "bash zsh fish" -- "\$cur_word") )
;;
Expand Down Expand Up @@ -387,6 +396,14 @@ EOF
printf " '--debug[Show detailed logs]' \\\\\n"
printf " '(-h --help)'{-h,--help}'[Show help]'\n"
printf ' ;;\n'
printf ' optimize|optimise)\n'
printf ' _arguments \\\n'
printf " '--dry-run[Preview optimization without making changes]' \\\\\n"
printf " '--spotlight-deep-reset[Rebuild Spotlight user search caches after confirmation]' \\\\\n"
printf " '--whitelist[Manage protected items]' \\\\\n"
printf " '--debug[Show detailed logs]' \\\\\n"
printf " '(-h --help)'{-h,--help}'[Show help]'\n"
printf ' ;;\n'
printf ' completion)\n'
printf " _arguments '1:shell:(bash zsh fish)'\n"
printf ' ;;\n'
Expand Down
18 changes: 17 additions & 1 deletion bin/optimize.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ main() {
export MOLE_CURRENT_COMMAND="optimize"

local health_json
local spotlight_deep_reset="false"
for arg in "$@"; do
case "$arg" in
"--help" | "-h")
Expand All @@ -203,6 +204,9 @@ main() {
"--dry-run")
export MOLE_DRY_RUN=1
;;
"--spotlight-deep-reset")
spotlight_deep_reset="true"
;;
"--whitelist")
manage_whitelist "optimize"
exit 0
Expand Down Expand Up @@ -230,6 +234,19 @@ main() {
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No files will be modified\n"
fi

load_whitelist "optimize"
if [[ "$spotlight_deep_reset" == "true" ]]; then
if command -v is_whitelisted > /dev/null && is_whitelisted "spotlight_deep_reset"; then
opt_msg "Skipped (whitelisted): Spotlight Deep Reset"
printf '\n'
return 0
fi
announce_action "Spotlight Deep Reset" "Disable indexing, clear user search caches, and rebuild Spotlight" "confirm"
opt_spotlight_deep_reset
printf '\n'
return 0
fi

if ! command -v bc > /dev/null 2>&1; then
echo -e "${YELLOW}${ICON_ERROR}${NC} Missing dependency: bc"
echo -e "${GRAY}Install with: ${GREEN}brew install bc${NC}"
Expand Down Expand Up @@ -263,7 +280,6 @@ main() {
stop_inline_spinner
fi

load_whitelist "optimize"
if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then
local count=${#CURRENT_WHITELIST_PATTERNS[@]}
if [[ $count -le 3 ]]; then
Expand Down
2 changes: 2 additions & 0 deletions lib/core/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ show_optimize_help() {
echo ""
echo "Options:"
echo " --dry-run Preview optimization without making changes"
echo " --spotlight-deep-reset"
echo " Rebuild Spotlight by clearing user search caches after confirmation"
echo " --whitelist Manage protected items"
echo " --debug Show detailed operation logs"
echo " -h, --help Show this help message"
Expand Down
1 change: 1 addition & 0 deletions lib/manage/whitelist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ Memory Optimization|memory_pressure_relief|optimize_task
Network Stack Refresh|network_stack_optimize|optimize_task
Permission Repair|disk_permissions_repair|optimize_task
Spotlight Optimization|spotlight_index_optimize|optimize_task
Spotlight Deep Reset|spotlight_deep_reset|optimize_task
Periodic Maintenance|periodic_maintenance|optimize_task
Shared File Lists|shared_file_list_repair|optimize_task
Disk Health|disk_verify|optimize_task
Expand Down
150 changes: 150 additions & 0 deletions lib/optimize/tasks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,155 @@ opt_spotlight_index_optimize() {
fi
}

_opt_spotlight_deep_reset_collect_targets() {
local -a patterns=(
"$HOME/Library/Metadata/CoreSpotlight"
"$HOME/Library/Caches/com.apple.Spotlight"*
"$HOME/Library/Caches/com.apple.spotlight"*
)

local target
for target in "${patterns[@]}"; do
[[ -e "$target" || -L "$target" ]] || continue
printf '%s\n' "$target"
done
}

_opt_spotlight_deep_reset_target_allowed() {
local target="$1"
[[ -n "$target" ]] || return 1
[[ "$target" == "$HOME/"* ]] || return 1
[[ ! -L "$target" ]] || return 1

case "$target" in
"$HOME/Library/Metadata/CoreSpotlight") return 0 ;;
"$HOME/Library/Caches/com.apple.Spotlight"*) return 0 ;;
"$HOME/Library/Caches/com.apple.spotlight"*) return 0 ;;
esac

return 1
}

_opt_spotlight_deep_reset_remove_target() {
local target="$1"

if ! _opt_spotlight_deep_reset_target_allowed "$target"; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Refusing unexpected Spotlight target: $target"
return 1
fi

if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Would remove $target"
log_operation "${MOLE_CURRENT_COMMAND:-optimize}" "DRY_RUN" "$target" "spotlight deep reset"
return 0
fi

if rm -rf "$target"; then
log_operation "${MOLE_CURRENT_COMMAND:-optimize}" "REMOVED" "$target" "spotlight deep reset"
return 0
fi

log_operation "${MOLE_CURRENT_COMMAND:-optimize}" "FAILED" "$target" "spotlight deep reset"
return 1
}

_opt_spotlight_deep_reset_confirmed() {
local expected="RESET SPOTLIGHT"
local typed="${MOLE_SPOTLIGHT_DEEP_RESET_CONFIRM:-}"

if [[ -z "$typed" ]]; then
echo -ne " ${PURPLE}${ICON_ARROW}${NC} Type ${YELLOW}${expected}${NC} to continue: "
IFS= read -r typed || typed=""
fi

[[ "$typed" == "$expected" ]]
}

opt_spotlight_deep_reset() {
local -a targets=()
local target
while IFS= read -r target; do
[[ -n "$target" ]] || continue
targets+=("$target")
done < <(_opt_spotlight_deep_reset_collect_targets)

echo -e " ${YELLOW}${ICON_WARNING}${NC} Spotlight deep reset will rebuild user search caches"
echo -e " ${GRAY}${ICON_LIST}${NC} Search results may be incomplete while macOS rebuilds the index"

if [[ ${#targets[@]} -eq 0 ]]; then
echo -e " ${GRAY}${ICON_EMPTY}${NC} No user Spotlight cache targets found"
else
echo -e " ${GRAY}${ICON_LIST}${NC} Targets:"
for target in "${targets[@]}"; do
echo -e " ${GRAY}-${NC} $target"
done
fi

if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} DRY RUN: would run mdutil off/on and rebuild /"
for target in "${targets[@]}"; do
_opt_spotlight_deep_reset_remove_target "$target" || true
done
opt_msg "Spotlight deep reset preview complete"
return 0
fi

if ! _opt_spotlight_deep_reset_confirmed; then
echo -e " ${GRAY}${ICON_EMPTY}${NC} Spotlight deep reset cancelled"
return 0
fi

if ! ensure_sudo_session "Spotlight deep reset requires admin access"; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Spotlight deep reset skipped · admin access required"
return 0
fi

local indexing_disabled=false
if sudo mdutil -i off / > /dev/null 2>&1; then
indexing_disabled=true
opt_msg "Spotlight indexing paused"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to pause Spotlight indexing"
return 1
fi

if pgrep -x corespotlightd > /dev/null 2>&1; then
if killall corespotlightd > /dev/null 2>&1; then
opt_msg "CoreSpotlight service restarted"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to restart CoreSpotlight service"
fi
fi

local failed=0
for target in "${targets[@]}"; do
if _opt_spotlight_deep_reset_remove_target "$target"; then
opt_msg "Removed $target"
else
failed=$((failed + 1))
fi
done

if [[ "$indexing_disabled" == "true" ]]; then
if sudo mdutil -i on / > /dev/null 2>&1; then
opt_msg "Spotlight indexing resumed"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to resume Spotlight indexing"
return 1
fi
fi

if sudo mdutil -E / > /dev/null 2>&1; then
opt_msg "Spotlight deep rebuild started"
echo -e " ${GRAY}Indexing will continue in background${NC}"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to start Spotlight rebuild"
return 1
fi

[[ "$failed" -eq 0 ]]
}

# Dock cache refresh.
opt_dock_refresh() {
local dock_support="$HOME/Library/Application Support/Dock"
Expand Down Expand Up @@ -1393,6 +1542,7 @@ execute_optimization() {
network_stack_optimize) opt_network_stack_optimize ;;
disk_permissions_repair) opt_disk_permissions_repair ;;
spotlight_index_optimize) opt_spotlight_index_optimize ;;
spotlight_deep_reset) opt_spotlight_deep_reset ;;
launch_agents_cleanup) opt_launch_agents_cleanup ;;
periodic_maintenance) opt_periodic_maintenance ;;
shared_file_list_repair) opt_shared_file_list_repair ;;
Expand Down
9 changes: 6 additions & 3 deletions tests/completion.bats
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ setup() {
[[ "$output" == *"complete -F _mole_completions mole mo"* ]]
}

@test "completion bash includes current clean, analyze, and purge options only" {
@test "completion bash includes current clean, optimize, analyze, and purge options only" {
run "$PROJECT_ROOT/bin/completion.sh" bash
[ "$status" -eq 0 ]
[[ "$output" == *"--dry-run -n --external --whitelist --debug --help -h"* ]]
[[ "$output" == *"--dry-run --spotlight-deep-reset --whitelist --debug --help -h"* ]]
[[ "$output" == *"--json --help -h"* ]]
[[ "$output" == *"--paths --dry-run -n --include-empty --debug --help -h"* ]]
[[ "$output" != *"--select"* ]]
Expand Down Expand Up @@ -116,12 +117,13 @@ setup() {
[[ "$output" == *"clean:Free up disk space"* ]]
}

@test "completion zsh includes current clean, analyze, and purge options only" {
@test "completion zsh includes current clean, optimize, analyze, and purge options only" {
run "$PROJECT_ROOT/bin/completion.sh" zsh
[ "$status" -eq 0 ]
[[ "$output" == *"--dry-run"* ]]
[[ "$output" == *"--external"* ]]
[[ "$output" == *"--whitelist"* ]]
[[ "$output" == *"--spotlight-deep-reset"* ]]
[[ "$output" == *"--json"* ]]
[[ "$output" == *"--include-empty"* ]]
[[ "$output" != *"--select"* ]]
Expand All @@ -145,12 +147,13 @@ setup() {
[ "$mo_count" -gt 0 ]
}

@test "completion fish includes current clean, analyze, and purge options only" {
@test "completion fish includes current clean, optimize, analyze, and purge options only" {
run "$PROJECT_ROOT/bin/completion.sh" fish
[ "$status" -eq 0 ]
[[ "$output" == *"-l dry-run"* ]]
[[ "$output" == *"-l external"* ]]
[[ "$output" == *"-l whitelist"* ]]
[[ "$output" == *"-l spotlight-deep-reset"* ]]
[[ "$output" == *"-l json"* ]]
[[ "$output" == *"-l include-empty"* ]]
[[ "$output" != *"-l select"* ]]
Expand Down
Loading