#!/bin/zsh
# keepfree — vigia o espaço do seu Mac e limpa só o que é lixo seguro.
# Licença MIT.

emulate -R zsh
setopt NULL_GLOB
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:$PATH"

VERSAO="2.0.1"
SCRIPT="${0:A}"
DIR_CONF="${KEEPFREE_DIR:-$HOME/.config/keepfree}"
CONF="$DIR_CONF/config"
HIST="$DIR_CONF/historico.csv"
ESTADO="$DIR_CONF/estado"
LOG="$HOME/Library/Logs/keepfree.log"
LABEL="br.com.psdsoftware.keepfree.vigia"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"

# Quem vem do Disco Livre (o nome até a 1.5): a configuração, a quarentena, o histórico e o
# log mudam de lugar na primeira vez que o keepfree roda, antes de ler a configuração.
# O resto (vigilância, app, comando e skill antigos) sai no `keepfree migrar`, que o install.sh chama.
ANTIGO_CONF="$HOME/.config/disco-livre"
ANTIGO_LOG="$HOME/Library/Logs/disco-livre.log"
ANTIGO_LABEL="com.disco-livre.vigia"
if [[ -z ${KEEPFREE_DIR-} && -d $ANTIGO_CONF && ! -L $ANTIGO_CONF && ! -e $DIR_CONF ]]; then
  mv "$ANTIGO_CONF" "$DIR_CONF" 2>/dev/null
fi
[[ -f $ANTIGO_LOG && ! -e $LOG ]] && mv "$ANTIGO_LOG" "$LOG" 2>/dev/null

# Padrões — mude com `keepfree config`
META_GB=30            # espaço livre mínimo que você quer manter
AUTO_LIMPEZA=nao      # limpar o lixo seguro sozinho quando ficar abaixo da meta
LIMPAR_CRASH=sim      # apagar sozinho surtos de relatórios de crash
INTERVALO_HORAS=3     # de quanto em quanto tempo a vigilância roda
LOG_DIAS=7            # logs mais velhos que isso são lixo
ALERTA_QUEDA_GB=10    # avisa se o espaço livre cair isso em 24h
LIMITE_CRASH=500      # quantos relatórios de crash contam como surto
CRITICO_GB=5          # abaixo disso a vigilância avisa toda vez, sem esperar
QUARENTENA_DIAS=7     # quanto tempo o que foi pra quarentena fica até sair de vez
PROJETOS_DIAS=30      # projeto sem mudança há isso conta como parado
MEMORIA_MIN=15        # memória livre (%) abaixo disso avisa
SWAP_AVISO_GB=8       # swap (GB) a partir disso avisa e sugere reiniciar
DEV_HORAS=3           # programa de programação parado há isso conta como esquecido
DOCKER_ALERTA_GB=5    # avisa se o disco do Docker crescer isso em 7 dias
CPU_MINUTOS=10        # app com o processador no máximo por isso avisa
RESUMO_SEMANA=sim     # um aviso por semana com o que foi liberado e o que cresceu
APPS_DIAS=90          # app sem uso há isso entra na análise geral
PESADO_MEM_MIN=35     # memória livre (%) pra começar tarefa pesada (a trava do Claude manda, se existir)
PESADO_SWAP_GB=10     # swap máxima (GB) pra começar tarefa pesada
ENCAMINHAR_CRITICO=nao # avisos urgentes também vão pro programa de encaminhar

# A configuração é LIDA, nunca executada: só CHAVE=valor conhecidas e com valor
# válido entram. Uma linha estranha no arquivo não roda como código.
CHAVES_NUM=(META_GB INTERVALO_HORAS LOG_DIAS ALERTA_QUEDA_GB LIMITE_CRASH CRITICO_GB QUARENTENA_DIAS PROJETOS_DIAS
            MEMORIA_MIN SWAP_AVISO_GB DEV_HORAS DOCKER_ALERTA_GB CPU_MINUTOS APPS_DIAS PESADO_MEM_MIN PESADO_SWAP_GB)
CHAVES_SN=(AUTO_LIMPEZA LIMPAR_CRASH RESUMO_SEMANA ENCAMINHAR_CRITICO)
ler_config() {
  local linha k v
  [[ -f $CONF ]] || return 0
  while IFS= read -r linha || [[ -n $linha ]]; do
    [[ $linha =~ '^([A-Z_]+)=([A-Za-z0-9]+)[[:space:]]*$' ]] || continue
    k=$match[1] v=$match[2]
    if (( ${CHAVES_NUM[(Ie)$k]} )); then
      [[ $v == <-> ]] || continue
    elif (( ${CHAVES_SN[(Ie)$k]} )); then
      [[ $v == (sim|nao) ]] || continue
    else
      continue
    fi
    typeset -g "$k=$v"
  done < "$CONF"
}
ler_config

# Pastas que a pessoa mandou ignorar (keepfree ignorar): uma por linha
IGNORADOS_ARQ="$DIR_CONF/ignorados"
IGNORADOS=()
[[ -f $IGNORADOS_ARQ ]] && IGNORADOS=(${(f)"$(cat -- "$IGNORADOS_ARQ")"})

# cor só no terminal (ou com KEEPFREE_COR=sempre, ex.: pra usar com less -R); NO_COLOR desliga
if [[ -z ${NO_COLOR-} && ( -t 1 || ${KEEPFREE_COR-} == sempre ) ]]; then
  B=$'\e[1m' D=$'\e[2m' R=$'\e[31m' G=$'\e[32m' Y=$'\e[33m' C=$'\e[36m' N=$'\e[0m'
else
  B= D= R= G= Y= C= N=
fi

# ─── utilitários ────────────────────────────────────────────────────────────

kb()       { du -sk "$1" 2>/dev/null | awk '{s = $1} END {print s + 0}'; }   # 0 se não existe
# Só arquivos de log e de diagnóstico. Em ~/Library/Logs também moram arquivos de
# estado e de dados de outros programas (e o próprio histórico do keepfree):
# esses nunca entram, e nenhuma pasta é apagada.
LOG_PADROES=('*.log' '*.log.*' '*.ips' '*.crash' '*.diag' '*.hang' '*.spin' '*.panic')
logs_antigos() {   # $1 = pasta → lista separada por \0
  local -a expr
  local x
  for x in $LOG_PADROES; do expr+=(-o -iname "$x"); done
  find "$1" -type f \( "${(@)expr[2,-1]}" \) ! -name "${LOG:t}" -mtime +$LOG_DIAS -print0 2>/dev/null
}
kb_antigos() { logs_antigos "$1" | xargs -0 stat -f%z 2>/dev/null | awk '{s+=$1} END{printf "%d\n", s/1024}'; }
# GB/MB decimais, igual ao Finder e aos Ajustes do Sistema
humano()   { awk -v k="${1:-0}" 'BEGIN{ if(k<0)k=-k; b=k*1024; if(b>=1e9) s=sprintf("%.1f GB", b/1e9); else if(b>=1e6) s=sprintf("%.0f MB", b/1e6); else s=sprintf("%d KB", k); gsub(/\./, ",", s); print s }'; }
gb_kb()    { print $(( $1 * 976562 )); }
livre_kb() { df -k "$HOME" | awk 'NR==2{print $4}'; }
total_kb() { df -k "$HOME" | awk 'NR==2{print $2}'; }
curto()    { print -r -- "${1/#$HOME/~}"; }
# a pasta (ou algo dentro dela) foi ignorada?
ignorado() { local i; for i in $IGNORADOS; do [[ $1 == "$i" || $1 == "$i"/* ]] && return 0; done; return 1; }
titulo()   { print -r -- ""; print -r -- "${B}$1${N}"; }
erro()     { print -r -- "${R}✗${N} $1" >&2; exit 1; }
log()      { mkdir -p "${LOG:h}"; print -r -- "$(date '+%F %T') $*" >> "$LOG"; }
# avisar <título> <texto> [tom]   tom: critico, atencao, seguranca, sucesso ou info
# Com o app aberto, o aviso sai por ele: um cartão animado no canto da tela (ou a
# notificação do sistema, se a pessoa preferir). Sem o app, pelo osascript
# (aparece como "Editor de Scripts").
avisar() {
  [[ -n ${KEEPFREE_SEM_AVISO-} ]] && return 0   # testes: nenhum aviso sai
  [[ ${3-} == critico && $ENCAMINHAR_CRITICO == sim ]] && encaminhar "$1" "$2"
  # número e unidade juntos (espaço que não quebra): o aviso nunca termina com "GB" sozinho numa linha
  local nb=$'\u00a0' texto
  texto=$(print -r -- "$2" | sed -E "s/([0-9]) (GB|MB|KB)/\\1${nb}\\2/g")
  if [[ -z ${KEEPFREE_SEM_APP-} ]] && pgrep -qx KeepFree 2>/dev/null; then
    osascript -l JavaScript -e 'function run(a) { ObjC.import("Foundation");
      $.NSDistributedNotificationCenter.defaultCenter.postNotificationNameObjectUserInfoDeliverImmediately(
        "br.com.psdsoftware.keepfree.aviso", $(), $({titulo: a[0], texto: a[1], tom: a[2]}), true); return "" }' "$1" "$texto" "${3:-info}" >/dev/null 2>&1 && return 0
  fi
  osascript -e "display notification \"${texto//\"/\'}\" with title \"${1//\"/\'}\"" >/dev/null 2>&1
}
confirmar() { local r; read -r "r?$1 [s/N] " || return 1; [[ $r == [sSyY]* ]]; }
ativo()    { launchctl print "gui/$UID/$LABEL" >/dev/null 2>&1; }


estado_ler()    { grep "^$1=" "$ESTADO" 2>/dev/null | cut -d= -f2; }
estado_gravar() {
  mkdir -p "$DIR_CONF"
  { grep -v "^$1=" "$ESTADO" 2>/dev/null; print -r -- "$1=$2"; } > "$ESTADO.tmp" && mv "$ESTADO.tmp" "$ESTADO"
}

# Só avisa de novo depois de N segundos (evita spam de notificação)
pode_avisar() {
  local agora=$(date +%s) ult
  ult=$(estado_ler "$1")
  (( agora - ${ult:-0} < $2 )) && return 1
  estado_gravar "$1" $agora
}

barra() {
  local usado=$1 total=$2 w=30 n i s=""
  n=$(( usado * w / total ))
  for (( i = 0; i < w; i++ )); do
    (( i < n )) && s+="█" || s+="░"
  done
  print -rn -- "$s"
}

# ─── catálogo do que dá pra limpar ─────────────────────────────────────────
# nivel|tipo|descrição|caminho|quem precisa estar fechado|nota
#   nivel:  seguro  = se recria sozinho (limpeza automática pode apagar)
#           revisar = é da pessoa; só sai se ela marcar, e vai pra quarentena
#   tipo:   conteudo        apaga tudo dentro da pasta (a pasta fica)
#           antigos         apaga arquivos com mais de $LOG_DIAS dias
#           electron        apaga os caches internos de um app Electron
#           quarentena-pasta     manda a pasta inteira pra quarentena
#           quarentena-conteudo  manda o que está dentro da pasta pra quarentena
#           quarentena-antigos:DIAS:PROFUNDIDADE:PADRÕES  manda arquivos velhos pra quarentena
#   fechado: "-" ninguém · "?" descobre sozinho (arquivos abertos)
#            "Nome" app ou programa que não pode estar aberto (vírgula separa vários;
#            "Nome*" = começa com; "/trecho/" = trecho da linha de comando)

# Caminho com "|" ou caractere de controle (ex.: quebra de linha) quebraria as
# linhas campo|campo e poderia apontar pra outro lugar: nunca entra no catálogo.
caminho_ok() { [[ $1 != *[\|[:cntrl:]]* ]]; }

crashpads() {
  local c
  for c in "$HOME/Library/Application Support"/*/Crashpad(N/) \
           "$HOME/Library/Application Support"/*/*/Crashpad(N/) \
           "$HOME/Library/Caches"/*/Crashpad(N/); do
    caminho_ok "$c" && print -r -- "$c"
  done
}

ELECTRON_CACHES=(Cache "Code Cache" GPUCache DawnGraphiteCache DawnWebGPUCache GrShaderCache ShaderCache)

# Cache dos sites dentro de cada perfil do Chrome, Edge e Brave. Só cache: cookies,
# logins, favoritos, histórico e dados dos sites (IndexedDB, Local Storage) ficam.
PERFIL_CACHES=("Service Worker/CacheStorage" "Service Worker/ScriptCache" "Code Cache" GPUCache DawnGraphiteCache DawnWebGPUCache)
perfil_dirs() {
  local s
  for s in $PERFIL_CACHES; do [[ -d $1/$s ]] && print -r -- "$1/$s"; done
}

electron_dirs() {
  local s
  for s in $ELECTRON_CACHES; do [[ -d $1/$s ]] && print -r -- "$1/$s"; done
}

# o catálogo, sem o que a pessoa mandou ignorar
alvos() {
  local l nivel tipo desc cam
  alvos_todos | while IFS= read -r l; do
    IFS='|' read -r nivel tipo desc cam _ <<< "$l"
    ignorado "$cam" || print -r -- "$l"
  done
}

alvos_todos() {
  local c s a L="$HOME/Library" AS="$HOME/Library/Application Support" CA="$HOME/Library/Caches"
  # relatórios de crash de qualquer app
  for c in ${(f)"$(crashpads)"}; do
    for s in pending completed new; do
      [[ -d $c/$s ]] && print -r -- "seguro|conteudo|Relatórios de crash — ${c:h:t}|$c/$s|-|"
    done
  done
  print -r -- "seguro|antigos|Logs com mais de $LOG_DIAS dias|$L/Logs|-|Só arquivos de log (.log, .ips, .crash…). O histórico do keepfree fica"

  # navegadores
  print -r -- "seguro|conteudo|Cache do Chrome|$CA/Google/Chrome|Google Chrome|"
  print -r -- "seguro|conteudo|Cache do Edge|$CA/Microsoft Edge|Microsoft Edge|"
  print -r -- "seguro|conteudo|Cache do Brave|$CA/BraveSoftware|Brave Browser|"
  print -r -- "seguro|conteudo|Cache do Opera GX|$CA/com.operasoftware.OperaGX|Opera GX|"
  print -r -- "seguro|conteudo|Cache do Arc|$CA/Arc|Arc|"
  print -r -- "seguro|conteudo|Cache do Firefox|$CA/Firefox|Firefox|"

  # criação
  print -r -- "seguro|conteudo|Cache do Camera Raw (Lightroom/Photoshop)|$CA/Adobe Camera Raw 2|Adobe Lightroom*,Adobe Photoshop*,Adobe Bridge*|"
  print -r -- "seguro|conteudo|Cache do Spotify|$CA/com.spotify.client|Spotify|"

  # IA
  print -r -- "seguro|conteudo|Cache do Codex|$CA/Codex|?|"
  print -r -- "seguro|conteudo|Runtimes baixados do Codex|$HOME/.cache/codex-runtimes|?|Baixados de novo quando o Codex precisar"
  print -r -- "seguro|conteudo|Cache do Claude Code|$CA/claude-cli-nodejs|?|"
  print -r -- "seguro|conteudo|Cache do Hugging Face (Xet)|$HOME/.cache/huggingface/xet|?|"

  # programação
  print -r -- "seguro|conteudo|Cache do npm|$HOME/.npm/_cacache|-|"
  print -r -- "seguro|conteudo|Cache do pip|$CA/pip|-|"
  print -r -- "seguro|conteudo|Cache do uv (Python)|$HOME/.cache/uv|?|"
  print -r -- "seguro|conteudo|Cache do pnpm|$CA/pnpm|-|"
  print -r -- "seguro|conteudo|Cache do Yarn|$CA/Yarn|-|"
  print -r -- "seguro|conteudo|Cache do Homebrew|$CA/Homebrew|/Library/Homebrew/|"
  print -r -- "seguro|conteudo|Cache do Go|$CA/go-build|-|"
  print -r -- "seguro|conteudo|Cache do CocoaPods|$CA/CocoaPods|-|"
  print -r -- "seguro|conteudo|Xcode DerivedData|$L/Developer/Xcode/DerivedData|Xcode|"

  # atualizações já baixadas
  for c in "$CA"/*ShipIt(N/); do
    caminho_ok "$c" || continue
    print -r -- "seguro|conteudo|Atualização já baixada — ${${c:t:r}##*.}|$c|-|"
  done

  # cache dos sites de cada perfil do Chrome, Edge e Brave (com o navegador fechado)
  local nav dir app rot pf nome
  for nav in "Google/Chrome|Google Chrome|Chrome" "Microsoft Edge|Microsoft Edge|Edge" "BraveSoftware/Brave-Browser|Brave Browser|Brave"; do
    dir=${nav%%|*} app=${${nav#*|}%%|*} rot=${nav##*|}
    for pf in "$AS/$dir"/(Default|Profile\ *|Guest\ Profile)(N/); do
      caminho_ok "$pf" || continue
      [[ -n $(perfil_dirs "$pf") ]] || continue
      nome=${pf:t}; [[ $nome == Default ]] && nome="perfil principal"; nome=${nome/Profile /perfil }
      print -r -- "seguro|perfil-navegador|Cache dos sites — $rot ($nome)|$pf|$app|Os sites baixam de novo o que precisam; logins, favoritos e dados ficam"
    done
  done

  # caches internos de apps Electron (Claude, Codex, Cursor, VS Code, Slack, Discord, Figma…)
  for a in "$AS"/*(N/); do
    caminho_ok "$a" || continue
    [[ -n $(electron_dirs "$a") ]] && print -r -- "seguro|electron|Cache do app ${${a:t}##*.}|$a|?|"
  done

  # Daqui pra baixo é tudo "revise": o resumo (que o app pede a cada 10 min) não precisa
  # disso. E aqui se mexe em ~/Documents e ~/Downloads, que o macOS protege: rodando pelo
  # app, o primeiro acesso abre um pedido de permissão e o comando espera a resposta.
  [[ -n ${SO_SEGUROS-} ]] && return 0

  # dependências e builds de projetos parados (voltam com npm install, pip, build…)
  projetos_parados

  # ferramentas de desenvolvimento e IA: só o que não está em uso
  ferramentas_dev

  # apps sem uso há APPS_DIAS (vão pra quarentena; os dados do app ficam)
  apps_sem_uso_catalogo

  # Downloads e Mesa: o que ninguém abre há 90 dias, separado por tipo
  print -r -- "revisar|quarentena-antigos-sem-uso:90:2:*.mp4 *.mov *.mkv *.avi *.m4v *.webm|Vídeos em Downloads sem abrir há 90 dias|$HOME/Downloads|-|Só os que ninguém abriu nesse tempo"
  print -r -- "revisar|quarentena-antigos-sem-uso:90:2:*.zip *.rar *.7z *.tar *.tgz *.gz *.bz2 *.xz|Compactados em Downloads sem abrir há 90 dias|$HOME/Downloads|-|Arquivos .zip, .rar, .7z… que ninguém abriu nesse tempo"
  print -r -- "revisar|quarentena-antigos-sem-uso:90:1:Captura?de?Tela* Screenshot* Grava*Tela* Screen?Recording* CleanShot*|Capturas e gravações de tela antigas na Mesa|$HOME/Desktop|-|Mais de 90 dias, sem abrir nesse tempo"

  # revisar: é da pessoa, vai pra quarentena (dá pra desfazer)
  print -r -- "revisar|quarentena-antigos:14:1:*.dmg *.pkg *.iso *.xip|Instaladores antigos em Downloads|$HOME/Downloads|-|Arquivos .dmg/.pkg com mais de 14 dias"
  print -r -- "revisar|quarentena-antigos:90:6:*.jsonl|Conversas do Codex com mais de 90 dias|$HOME/.codex/sessions|?|O histórico antigo some do Codex"
  print -r -- "revisar|quarentena-conteudo|Navegadores do Playwright|$CA/ms-playwright|?|Automações com Playwright vão precisar baixar de novo"
  print -r -- "revisar|quarentena-conteudo|Suporte a iPhone do Xcode|$L/Developer/Xcode/iOS DeviceSupport|Xcode|O Xcode recria ao conectar o iPhone"
}

# Projetos de programação parados há PROJETOS_DIAS: node_modules, ambientes Python,
# target do Rust/Maven, .next e .turbo. "Parado" = nenhum arquivo do projeto (fora
# dessas pastas e do .git) mudou nesse tempo. Sempre "revise", e vai pra quarentena.
RAIZES_PROJETOS=(Code Projects Projetos Developer dev src repos GitHub Documents/GitHub Documents/Projetos)
GERADAS=(node_modules .venv venv target .next .turbo)
projetos_parados() {
  local r d proj tipo volta
  local -a nomes poda
  for d in $GERADAS; do nomes+=(-o -name "$d"); done
  poda=("${(@)nomes[2,-1]}" -o -name .git)
  for r in $RAIZES_PROJETOS; do
    [[ -d $HOME/$r && ! -L $HOME/$r ]] || continue
    find "$HOME/$r" -maxdepth 4 -type d \( "${(@)nomes[2,-1]}" \) -prune -print 2>/dev/null
  done | while IFS= read -r d; do
    caminho_ok "$d" || continue
    proj=${d:h}
    case ${d:t} in
      node_modules) tipo="Dependências (node_modules)"; volta="Voltam com npm, pnpm ou yarn install" ;;
      .venv|venv)   [[ -f $d/pyvenv.cfg ]] || continue
                    tipo="Ambiente Python (${d:t})"; volta="Recrie com python -m venv e pip install" ;;
      target)       [[ -f $proj/Cargo.toml || -f $proj/pom.xml ]] || continue
                    tipo="Build (target)"; volta="Volta no próximo build" ;;
      *)            tipo="Cache de build (${d:t})"; volta="Volta no próximo build" ;;
    esac
    [[ -n $(find "$proj" -maxdepth 3 \( "${(@)poda}" \) -prune -o -type f -mtime -$PROJETOS_DIAS -print -quit 2>/dev/null) ]] && continue
    print -r -- "revisar|quarentena-pasta|$tipo — ${proj:t}, parado há $PROJETOS_DIAS+ dias|$d|?|$volta"
  done
}


# ─── ferramentas de desenvolvimento e IA (só na análise geral) ────────────
# Cada item aqui sai de olhar o que está EM USO: versão padrão e versões rodando
# ficam; projeto que ainda existe fica; só sobra, cópia antiga e cache saem.

# Editores da família do VS Code: pasta em Application Support|pasta das extensões|app
EDITORES=("Code|.vscode|Code" "Code - Insiders|.vscode-insiders|Code - Insiders" "Cursor|.cursor|Cursor"
          "Antigravity|.antigravity|Antigravity" "Antigravity IDE|.antigravity-ide|Antigravity IDE"
          "Windsurf|.windsurf|Windsurf" "VSCodium|.vscode-oss|VSCodium" "Kiro|.kiro|Kiro" "Trae|.trae|Trae")

# Versões de extensão que sobraram de atualizações: o editor só carrega a mais nova
# de cada uma (e as que ele mesmo marcou como obsoletas em .obsolete).
extensoes_antigas() {   # $1 = pasta de extensões → um caminho por linha
  local d
  {
    for d in "$1"/*(N/); do
      [[ ${d:t} =~ '^([^-]+(-[^0-9][^-]*)*)-([0-9]+(\.[0-9]+)+)(-.*)?$' ]] && print -r -- "$match[1]"$'\t'"$match[3]"$'\t'"$d"
    done | sort -t$'\t' -k1,1 -k2,2Vr | awk -F'\t' '$1 == ant { print $3 } { ant = $1 }'
    [[ -f $1/.obsolete ]] && grep -oE '"[^"]+"[[:space:]]*:[[:space:]]*true' "$1/.obsolete" | cut -d'"' -f2 |
      while IFS= read -r d; do [[ -d $1/$d ]] && print -r -- "$1/$d"; done
  } | sort -u
}

# Estado guardado pra projetos que não existem mais (pasta ou arquivo .code-workspace apagados)
workspace_orfaos() {   # $1 = .../User/workspaceStorage
  local w alvo
  for w in "$1"/*(N/); do
    [[ -f $w/workspace.json ]] || continue
    alvo=$(grep -oE '"(folder|workspace)"[[:space:]]*:[[:space:]]*"file://[^"]+"' "$w/workspace.json" | head -n 1 | sed -E 's/.*"file:\/\/([^"]+)"/\1/')
    [[ -n $alvo ]] || continue                 # remoto (ssh, container): não dá pra saber, fica
    alvo=$(printf '%b' "${alvo//\%/\\x}")
    [[ -e $alvo ]] || print -r -- "$w"
  done
}

# Conversas do Claude Code de pastas que não existem mais (worktrees, pastas temporárias)
claude_orfaos() {   # $1 = ~/.claude/projects
  local p f cwd
  for p in "$1"/*(N/); do
    f=$(print -rl -- "$p"/*.jsonl(N.om[1]))
    [[ -n $f ]] || continue
    cwd=$(head -c 60000 -- "$f" | grep -oE '"cwd":"[^"]+"' | head -n 1 | cut -d'"' -f4)
    [[ -n $cwd && ! -e $cwd ]] && print -r -- "$p"
  done
}

# Versões de Node e Python instaladas por gerenciador, fora a padrão e as que estão rodando
versoes_sem_uso() {
  local d v padrao rodando
  rodando=$(ps -axww -o comm= 2>/dev/null)
  # nvm
  if [[ -d $HOME/.nvm/versions/node ]]; then
    padrao=$(cat "$HOME/.nvm/alias/default" 2>/dev/null); padrao=${padrao#v}
    local -a vs; vs=("$HOME"/.nvm/versions/node/v*(N/))
    local manter=
    if [[ $padrao == <->* ]]; then
      manter=$(print -rl -- $vs | grep -E "/v${padrao//./\\.}(\.|$)" | sort -V | tail -n 1)
    elif [[ $padrao == (node|stable) ]]; then
      manter=$(print -rl -- $vs | sort -V | tail -n 1)
    else
      vs=()      # "lts/…" ou outro apelido: não dá pra saber qual é a padrão, então nada sai
    fi
    for d in $vs; do
      [[ $d == $manter ]] && continue
      print -r -- "$rodando" | grep -qF "$d/" && continue
      print -r -- "nvm|${d:t}|$d"
    done
  fi
  # fnm
  local F="$HOME/Library/Application Support/fnm"
  if [[ -d $F/node-versions ]]; then
    padrao=$(readlink "$F/aliases/default" 2>/dev/null)
    for d in "$F"/node-versions/v*(N/); do
      [[ -n $padrao && $padrao == $d* ]] && continue
      print -r -- "$rodando" | grep -qF "$d/" && continue
      print -r -- "fnm|${d:t}|$d"
    done
  fi
  # pyenv
  if [[ -d $HOME/.pyenv/versions ]]; then
    local -a globais; globais=(${(f)"$(cat "$HOME/.pyenv/version" 2>/dev/null)"})
    for d in "$HOME"/.pyenv/versions/*(N/); do
      (( ${globais[(Ie)${d:t}]} )) && continue
      print -r -- "$rodando" | grep -qF "$d/" && continue
      print -r -- "pyenv|${d:t}|$d"
    done
  fi
}

# Caches de versões antigas do JetBrains (fica a pasta da versão mais nova de cada produto)
jetbrains_antigos() {
  local d
  for d in "$HOME"/Library/Caches/JetBrains/*(N/) "$HOME"/Library/Logs/JetBrains/*(N/); do
    [[ ${d:t} =~ '^([A-Za-z]+)([0-9]{4}\.[0-9]+)$' ]] && print -r -- "${d:h}|$match[1]"$'\t'"$match[2]"$'\t'"$d"
  done | sort -t$'\t' -k1,1 -k2,2Vr | awk -F'\t' '$1 == ant { print $3 } { ant = $1 }'
}

ferramentas_dev() {
  local e as dot app d v nome base CA="$HOME/Library/Caches" L="$HOME/Library" AS="$HOME/Library/Application Support"
  for e in $EDITORES; do
    as=${e%%|*} dot=${${e#*|}%%|*} app=${e##*|}
    [[ -d $HOME/$dot/extensions ]] && print -r -- "seguro|extensoes-antigas|Versões antigas de extensões — $as|$HOME/$dot/extensions|-|O editor só usa a versão mais nova de cada extensão; estas sobraram de atualizações"
    [[ -d $AS/$as/User/workspaceStorage ]] && print -r -- "revisar|workspace-orfaos|Dados de projetos apagados — $as|$AS/$as/User/workspaceStorage|$app|Estado salvo de pastas que não existem mais; os projetos que existem ficam"
    [[ -d $AS/$as/CachedExtensionVSIXs ]] && print -r -- "seguro|conteudo|Instaladores de extensões guardados — $as|$AS/$as/CachedExtensionVSIXs|-|Cópias dos pacotes já instalados"
    [[ -d $AS/$as/logs ]] && print -r -- "seguro|antigos|Logs antigos — $as|$AS/$as/logs|-|Só arquivos de log com mais de $LOG_DIAS dias"
  done

  # Claude Code
  if [[ -d $HOME/.claude ]]; then
    [[ -d $HOME/.claude/projects ]] && print -r -- "revisar|claude-orfaos|Conversas do Claude Code de pastas que não existem mais|$HOME/.claude/projects|-|Eram de pastas temporárias ou apagadas; as dos projetos que existem ficam"
    [[ -d $HOME/.claude/file-history ]] && print -r -- "revisar|quarentena-antigos:30:4:*|Cópias de edição do Claude Code com mais de 30 dias|$HOME/.claude/file-history|-|Servem pra desfazer edições antigas; as recentes ficam"
    for d in debug shell-snapshots paste-cache; do
      [[ -d $HOME/.claude/$d ]] && print -r -- "seguro|apagar-antigos:7:3:*|Rastros antigos do Claude Code ($d)|$HOME/.claude/$d|-|Arquivos temporários com mais de 7 dias"
    done
  fi
  [[ -d $HOME/.codex/log ]] && print -r -- "seguro|apagar-antigos:7:2:*.log|Logs antigos do Codex|$HOME/.codex/log|-|Só logs com mais de 7 dias"

  # versões de Node e Python fora de uso
  local g
  versoes_sem_uso | while IFS='|' read -r g v d; do
    case $g in
      pyenv) print -r -- "revisar|quarentena-pasta|Python $v ($g), não é a padrão nem está rodando|$d|?|Projetos que pedem essa versão vão precisar de pyenv install $v" ;;
      *)     print -r -- "revisar|quarentena-pasta|Node $v ($g), não é a padrão nem está rodando|$d|?|Projetos que pedem essa versão vão precisar de $g install ${v#v}" ;;
    esac
  done

  # gerenciadores de pacotes: tudo volta no próximo build
  # comandos só quando a pasta deles está na pasta pessoal (num HOME de teste, nada roda no sistema)
  command -v brew >/dev/null 2>&1 && [[ -d $CA/Homebrew ]] && print -r -- "seguro|comando-brew|Homebrew: versões antigas e downloads|$CA/Homebrew|/Library/Homebrew/|brew cleanup: as versões em uso ficam"
  local gm=
  command -v go >/dev/null 2>&1 && gm=$(go env GOMODCACHE 2>/dev/null)
  [[ $gm == $HOME/* && -d $gm ]] && print -r -- "seguro|comando-go|Cache de módulos do Go|$gm|?|go clean -modcache: baixa de novo no próximo build"
  [[ -d $HOME/.gradle/caches ]] && print -r -- "seguro|conteudo|Cache do Gradle|$HOME/.gradle/caches|?|Baixa de novo no próximo build"
  [[ -d $HOME/.cargo/registry/cache ]] && print -r -- "seguro|conteudo|Cache de pacotes do Rust (cargo)|$HOME/.cargo/registry/cache|?|Baixa de novo no próximo build"
  [[ -d $HOME/.bun/install/cache ]] && print -r -- "seguro|conteudo|Cache do Bun|$HOME/.bun/install/cache|?|Baixa de novo no próximo install"
  [[ -d $HOME/.yarn/berry/cache ]] && print -r -- "seguro|conteudo|Cache do Yarn (berry)|$HOME/.yarn/berry/cache|?|Baixa de novo no próximo install"
  for d in deno electron node-gyp typescript; do
    [[ -d $CA/$d ]] && print -r -- "seguro|conteudo|Cache do $d|$CA/$d|?|Baixa de novo quando precisar"
  done
  [[ -d $HOME/.m2/repository ]] && print -r -- "revisar|quarentena-conteudo|Pacotes do Maven|$HOME/.m2/repository|?|O próximo build baixa tudo de novo (pode demorar)"
  [[ -d $HOME/.cache/puppeteer ]] && print -r -- "revisar|quarentena-conteudo|Navegadores do Puppeteer|$HOME/.cache/puppeteer|?|Automações com Puppeteer vão precisar baixar de novo"
  jetbrains_antigos | while IFS= read -r d; do
    print -r -- "seguro|apagar-pasta|Cache de versão antiga do JetBrains (${d:t})|$d|-|O IDE atual usa a pasta da versão mais nova"
  done
  [[ -d $L/Developer/CoreSimulator/Devices ]] && command -v xcrun >/dev/null 2>&1 && xcrun --find simctl >/dev/null 2>&1 &&
    print -r -- "seguro|comando-simctl|Simuladores do Xcode que não existem mais|$L/Developer/CoreSimulator/Devices|Simulator|xcrun simctl delete unavailable"

  # IA: apps e modelos baixados
  [[ -d $AS/Claude/vm_bundles ]] && print -r -- "revisar|quarentena-pasta|Máquina virtual do Cowork (Claude)|$AS/Claude/vm_bundles|Claude|O Claude baixa de novo quando você usar o Cowork"
  for d in "$HOME"/.cache/huggingface/hub/models--*(N/) "$HOME"/.cache/huggingface/hub/datasets--*(N/); do
    nome=${${d:t}#*--}; nome=${nome//--//}
    print -r -- "revisar|quarentena-pasta|Modelo $nome (Hugging Face)|$d|?|Baixado de novo se algum programa precisar"
  done
  for base in "$HOME/.lmstudio/models" "$HOME/.cache/lm-studio/models"; do
    for d in "$base"/*/*(N/); do
      print -r -- "revisar|quarentena-pasta|Modelo ${d:h:t}/${d:t} (LM Studio)|$d|LM Studio|Baixe de novo pelo LM Studio se precisar"
    done
  done
}

# Apps de /Applications e ~/Applications sem uso há APPS_DIAS (ou nunca abertos), fora os
# da Apple e os que estão abertos. kb|dias(-1 = nunca)|nome|caminho|movível(0/1)|dados_kb
listar_apps_sem_uso() {
  local a id u dias nome mov dados agora=$(date +%s) lim=$APPS_DIAS
  local rodando=$(ps -axww -o comm= 2>/dev/null)
  # KEEPFREE_APLICATIVOS troca a pasta do sistema (os testes usam uma pasta falsa)
  for a in "${KEEPFREE_APLICATIVOS:-/Applications}"/*.app(N/) "$HOME"/Applications/*.app(N/); do
    caminho_ok "$a" || continue
    ignorado "$a" && continue
    id=$(mdls -raw -name kMDItemCFBundleIdentifier "$a" 2>/dev/null)
    [[ $id == com.apple.* || $id == br.com.psdsoftware.keepfree || $id == "(null)" ]] && continue
    print -r -- "$rodando" | grep -qF "$a/" && continue
    u=$(mdls -raw -name kMDItemLastUsedDate "$a" 2>/dev/null)
    if [[ -n $u && $u != "(null)" ]]; then
      u=$(date -j -f '%Y-%m-%d %H:%M:%S %z' "$u" +%s 2>/dev/null) || continue
      dias=$(( (agora - u) / 86400 ))
      (( dias >= lim )) || continue
    else
      u=$(mdls -raw -name kMDItemDateAdded "$a" 2>/dev/null)
      u=$(date -j -f '%Y-%m-%d %H:%M:%S %z' "$u" +%s 2>/dev/null) || u=0
      (( u == 0 || (agora - u) / 86400 >= lim )) || continue
      dias=-1
    fi
    nome=${a:t:r}
    [[ -w $a && -w ${a:h} ]] && mov=1 || mov=0
    dados=0
    for d in "$HOME/Library/Application Support/$nome" "$HOME/Library/Caches/$id" "$HOME/Library/Containers/$id"; do
      [[ -d $d ]] && (( dados += $(kb "$d") ))
    done
    print -r -- "$(kb "$a")|$dias|$nome|$a|$mov|$dados"
  done | sort -t'|' -k1,1nr
}

apps_sem_uso_catalogo() {
  local k dias nome a mov dados quando
  listar_apps_sem_uso | while IFS='|' read -r k dias nome a mov dados; do
    (( mov )) || continue
    (( dias < 0 )) && quando="nunca aberto" || quando="sem uso há $dias dias"
    print -r -- "revisar|quarentena-pasta|App $nome ($quando)|$a|$nome|Sai de Aplicativos; dá pra desfazer. Os dados do app ($(humano $dados)) ficam"
  done
}

# ─── relatório: o que está em uso em cada ferramenta ──────────────────────
# nome|kb|uso|dias(-1 = agora)|limpavel_kb|veredito
uso_ferramenta() {   # $1 = nome do processo ou app (vazio = não checa), $2... = pastas → "agora" ou dias desde a última mudança
  local p=$1 d t mais=0
  shift
  # "Nome.app" = só o app (o caminho tem /Nome.app/); senão, o nome do programa
  if [[ -n $p ]] && ps -axww -o comm= | awk -v p="$p" 'BEGIN { p = tolower(p); app = (p ~ /\.app$/) }
      { l = tolower($0); n = split(l, x, "/"); if (app ? index(l, "/" p "/") : (x[n] == p || index(l, "/" p ".app/"))) { achou = 1; exit } }
      END { exit !achou }'; then
    print -r -- "-1"; return
  fi
  for d; do
    [[ -e $d ]] || continue
    t=$(find "$d" -maxdepth 2 -print0 2>/dev/null | xargs -0 stat -f %m 2>/dev/null | sort -rn | head -n 1)
    (( ${t:-0} > mais )) && mais=$t
  done
  (( mais > 0 )) && print $(( ($(date +%s) - mais) / 86400 )) || print ""
}

listar_ferramentas() {
  local -a linhas
  linhas=(${(f)"$(linhas_geral)"})
  limpavel() { local l s=0 pre; for l in $linhas; do for pre; do [[ ${l##*|} == $pre || ${l##*|} == $pre/* ]] && { (( s += ${${l#*|*|}%%|*} )); break; }; done; done; print $s; }
  ferramenta() {   # nome processo raízes...
    local nome=$1 proc=$2 r tot=0 dias lim ver
    shift 2
    local -a raizes=()
    for r; do [[ -e $r ]] && raizes+=("$r"); done
    (( ${#raizes} )) || return 0
    for r in $raizes; do (( tot += $(kb "$r") )); done
    (( tot >= 51200 )) || return 0
    dias=$(uso_ferramenta "$proc" $raizes)
    lim=$(limpavel $raizes)
    if [[ $dias == -1 ]]; then ver="Em uso agora: só sobras (versões antigas, cache, pastas apagadas) podem sair."
    elif [[ -n $dias ]] && (( dias > 90 )); then ver="Parado há $dias dias: o cache pode sair e, se não usa mais, dá pra desinstalar."
    elif [[ -n $dias ]]; then ver="Usado $(quando_dias $dias): limpar o cache faz baixar de novo na próxima vez."
    else ver="Sem sinal de uso recente."; fi
    print -r -- "$nome|$tot|${dias:--2}|$lim|$ver"
  }
  local e as dot app AS="$HOME/Library/Application Support" CA="$HOME/Library/Caches"
  ferramenta "Claude Code" claude "$HOME/.claude" "$CA/claude-cli-nodejs"
  ferramenta "Claude (app)" Claude.app "$AS/Claude"
  ferramenta "Codex" codex "$HOME/.codex" "$AS/Codex" "$CA/Codex" "$HOME/.cache/codex-runtimes"
  for e in $EDITORES; do
    as=${e%%|*} dot=${${e#*|}%%|*} app=${e##*|}
    ferramenta "$as" "$app.app" "$AS/$as" "$HOME/$dot"
  done
  ferramenta "Docker" Docker.app "$HOME/Library/Containers/com.docker.docker"
  ferramenta "Node (npm, pnpm, yarn, bun, nvm)" node "$HOME/.npm" "$CA/pnpm" "$CA/Yarn" "$HOME/.bun" "$HOME/.nvm" "$AS/fnm"
  ferramenta "Python (pip, uv, pyenv)" python3 "$CA/pip" "$HOME/.cache/uv" "$HOME/.pyenv"
  ferramenta "Homebrew" brew "$CA/Homebrew"
  ferramenta "Java e Android (Gradle, Maven)" java "$HOME/.gradle" "$HOME/.m2"
  ferramenta "Rust e Go" cargo "$HOME/.cargo" "$HOME/go"
  ferramenta "JetBrains" "" "$CA/JetBrains"
  ferramenta "Xcode" Xcode.app "$HOME/Library/Developer"
  ferramenta "Ollama" ollama "$HOME/.ollama"
  ferramenta "LM Studio" "LM Studio.app" "$HOME/.lmstudio" "$HOME/.cache/lm-studio"
  ferramenta "Hugging Face" "" "$HOME/.cache/huggingface"
  ferramenta "Playwright" "" "$CA/ms-playwright"
}

quando_dias() { case $1 in 0) print -rn hoje ;; 1) print -rn ontem ;; *) print -rn "há $1 dias" ;; esac; }

ferramentas() {
  local nome k dias lim ver
  listar_ferramentas | sort -t'|' -k2,2nr | while IFS='|' read -r nome k dias lim ver; do
    printf "  %s%-9s%s %s ${D}(%s)${N}\n" "$B" "$(humano $k)" "$N" "$nome" \
      "$( [[ $dias == -1 ]] && print 'aberto agora' || { [[ $dias == -2 ]] && print 'sem sinal de uso' || print "usado $(quando_dias $dias)"; })"
    print -r -- "            ${D}↳ $ver${${lim:#0}:+ Dá pra limpar: $(humano $lim).}${N}"
  done
}

# formato antigo (tipo|descrição|caminho|fechado), só o que é seguro
alvos_seguros() {
  local nivel tipo desc cam cond nota
  SO_SEGUROS=1 alvos | while IFS='|' read -r nivel tipo desc cam cond nota; do
    [[ $nivel == seguro ]] && print -r -- "$tipo|$desc|$cam|$cond"
  done
}

arquivos_antigos() {   # $1 = quarentena-antigos:DIAS:PROF:PADRÕES (separados por espaço; ? no lugar de espaço)  $2 = pasta  → lista separada por \0
  local -a p pads expr
  p=("${(@s.:.)1}")
  pads=(${=p[4]})
  local x
  for x in $pads; do expr+=(-o -iname "$x"); done
  expr=("${(@)expr[2,-1]}")
  find "$2" -maxdepth $p[3] -type f \( "${(@)expr}" \) -mtime +$p[2] -print0 2>/dev/null
}

soma_kb() { xargs -0 stat -f%z 2>/dev/null | awk '{s+=$1} END{printf "%d\n", s/1024}'; }

medir() {
  local t=0 d
  case $1 in
    conteudo|quarentena-pasta|quarentena-conteudo) kb "$2" ;;
    antigos)  kb_antigos "$2" ;;
    electron) for d in ${(f)"$(electron_dirs "$2")"}; do (( t += $(kb "$d") )); done; print $t ;;
    perfil-navegador) for d in ${(f)"$(perfil_dirs "$2")"}; do (( t += $(kb "$d") )); done; print $t ;;
    quarentena-antigos:*) arquivos_antigos "$1" "$2" | soma_kb ;;
    quarentena-antigos-sem-uso:*) arquivos_antigos "$1" "$2" | sem_uso_recente "$1" | soma_kb ;;
    apagar-antigos:*) arquivos_antigos "$1" "$2" | soma_kb ;;
    apagar-pasta) kb "$2" ;;
    extensoes-antigas) lista_kb ${(f)"$(extensoes_antigas "$2")"} ;;
    workspace-orfaos)  lista_kb ${(f)"$(workspace_orfaos "$2")"} ;;
    claude-orfaos)     lista_kb ${(f)"$(claude_orfaos "$2")"} ;;
    comando-brew) sem_travar 120 env HOMEBREW_NO_AUTO_UPDATE=1 brew cleanup --dry-run 2>/dev/null |
                    awk '/free approximately/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9.]+[KMGT]?B$/) { v = $i; u = v; sub(/[KMGT]?B$/, "", v); sub(/^[0-9.]+/, "", u)
                      print int(v * (u == "GB" ? 976562.5 : u == "MB" ? 976.5625 : u == "KB" ? 0.9765625 : u == "TB" ? 976562500 : 1 / 1024)); exit } }' ;;
    comando-go) kb "$2" ;;
    comando-simctl) lista_kb ${(f)"$(simuladores_sem_uso)"} ;;
  esac
}

lista_kb() { local d t=0; for d; do [[ -e $d ]] && (( t += $(kb "$d") )); done; print $t; }

# Filtra (lista separada por \0) só os arquivos que ninguém abriu nos últimos DIAS
# (pelo Spotlight: kMDItemLastUsedDate). $1 = o tipo, que traz os DIAS.
sem_uso_recente() {
  local -a p; p=("${(@s.:.)1}")
  local lim=$(( $(date +%s) - p[2] * 86400 )) f u
  while IFS= read -r -d '' f; do
    u=$(mdls -raw -name kMDItemLastUsedDate "$f" 2>/dev/null)
    if [[ -n $u && $u != "(null)" ]]; then
      u=$(date -j -f '%Y-%m-%d %H:%M:%S %z' "$u" +%s 2>/dev/null)
      (( ${u:-0} > lim )) && continue
    fi
    print -rn -- "$f"$'\0'
  done
}

simuladores_sem_uso() {
  xcrun simctl list devices unavailable 2>/dev/null | grep -oE '\(([0-9A-F-]{36})\)' | tr -d '()' |
    while IFS= read -r u; do print -r -- "$HOME/Library/Developer/CoreSimulator/Devices/$u"; done
}

# Uma foto só dos processos e dos arquivos abertos, tirada antes de medir ou limpar
# (antes era um `lsof +D` por pasta: ~5 s dos ~8 s do resumo num Mac com 20 caches).
#   PROCS:   pid|app|sistema(0/1)|executável  (app = o .app mais de fora: "Google Chrome")
#   ARGS:    a linha de comando de cada processo (sem o ambiente)
#   ABERTOS: app<TAB>arquivo, só de processos que não são do sistema (um arquivo do
#            Chrome aberto pela Central de Notificações não é motivo pra fechar nada)
fotografar() {
  PROCS=$(ps -axww -o pid=,comm= | awk '{
    pid = $1; sub(/^ *[0-9]+ +/, ""); exe = $0
    app = exe; i = index(app, ".app/"); if (i) app = substr(app, 1, i - 1)
    n = split(app, p, "/"); app = p[n]
    sis = (exe ~ /^\/(System|usr|bin|sbin|Library\/Apple)\//) ? 1 : 0
    print pid "|" app "|" sis "|" exe }')
  ARGS=$(ps -axww -o args=)
  ABERTOS=$(awk -F'|' '
    NR == FNR { app[$1] = $2; sis[$1] = $3; next }
    /^p/ { pid = substr($0, 2); next }
    /^n/ { if ((pid in app) && !sis[pid]) print app[pid] "\t" substr($0, 2) }
  ' <(print -r -- "$PROCS") <(lsof -n -P -w -u "$UID" -Fpn 2>/dev/null))
  FOTO=1
}

# Quem está usando? Vazio = pode limpar. Senão, o nome do app que precisa fechar.
quem_usa() {
  local tipo=$1 cam=$2 cond=$3 nome d alvo
  (( FOTO )) || fotografar
  case $cond in
    -|"") return 0 ;;
    \?)
      local -a dirs
      if [[ $tipo == electron ]]; then dirs=(${(f)"$(electron_dirs "$cam")"}); else dirs=("$cam"); fi
      for d in $dirs; do
        nome=$(print -r -- "$ABERTOS" | DIR="$d/" awk '{
          i = index($0, "\t"); if (index(substr($0, i + 1), ENVIRON["DIR"]) == 1) { print substr($0, 1, i - 1); exit } }')
        [[ -n $nome ]] && { print -r -- "$nome"; return 0; }
      done ;;
    *)
      for alvo in ${(s:,:)cond}; do
        if [[ $alvo == /* ]]; then
          # trecho da linha de comando (só os argumentos: o ambiente não conta)
          print -r -- "$ARGS" | grep -qF -- "$alvo" && { print -r -- "${${alvo%/}:t}"; return 0; }
        else
          nome=$(print -r -- "$PROCS" | ALVO="$alvo" awk -F'|' '
            BEGIN { a = tolower(ENVIRON["ALVO"]); pre = substr(a, length(a)) == "*"; if (pre) a = substr(a, 1, length(a) - 1) }
            { n = split($4, p, "/"); exe = tolower(p[n]); app = tolower($2)
              if (pre ? (index(app, a) == 1 || index(exe, a) == 1) : (app == a || exe == a)) { print $2; exit } }')
          [[ -n $nome ]] && { print -r -- "$nome"; return 0; }
        fi
      done ;;
  esac
}

# Manda pra Lixeira (recuperável). macOS 15+: /usr/bin/trash; antes disso, pelo Finder.
mandar_lixeira() {
  (( $# )) || return 0
  if [[ -x /usr/bin/trash ]]; then
    /usr/bin/trash "$@" 2>/dev/null
  else
    local f
    for f; do
      osascript -e 'on run a' -e 'tell application "Finder" to delete (POSIX file (item 1 of a) as alias)' -e 'end run' "$f" >/dev/null 2>&1
    done
  fi
  return 0
}

# ─── quarentena: o que é da pessoa sai do lugar, mas dá pra desfazer ─────────
# Cada limpeza vira um lote em $QUAR/AAAAMMDD-HHMMSS-PID: itens/N (o que foi movido)
# e manifesto (N e o caminho original, separados por \0). No mesmo disco, mover é só
# renomear: instantâneo. A vigilância apaga de vez os lotes com mais de QUARENTENA_DIAS.
QUAR="$DIR_CONF/quarentena"
LOTE= QN=0 FOI_LIXEIRA=0

novo_lote() {
  [[ -n $LOTE ]] && return 0
  LOTE="$QUAR/$(date +%Y%m%d-%H%M%S)-$$"
  mkdir -p "$LOTE/itens"
}

# Move pra quarentena. Arquivo de outro disco vai pra Lixeira (não copia GBs).
quarentenar() {
  (( $# )) || return 0
  local f
  novo_lote
  for f; do
    [[ -e $f || -L $f ]] || continue
    if [[ $(stat -f %d "$f") != $(stat -f %d "$LOTE") ]]; then
      mandar_lixeira "$f"; FOI_LIXEIRA=1; continue
    fi
    (( ++QN ))
    mv -- "$f" "$LOTE/itens/$QN" && print -rn -- "$QN"$'\0'"$f"$'\0' >> "$LOTE/manifesto"
  done
}

lotes()   { print -rl -- "$QUAR"/*(N/On); }   # o mais novo primeiro
lote_ok() { [[ $1 =~ '^[0-9]{8}-[0-9]{6}-[0-9]+$' ]]; }   # nome de lote, nunca um caminho
n_lotes() { local -a a; a=("$QUAR"/*(N/)); print ${#a}; }
lote_ts() { local n=${1:t}; date -j -f %Y%m%d-%H%M%S "${n%-*}" +%s 2>/dev/null; }
lote_n()  { [[ -f $1/manifesto ]] && print $(( $(tr -cd '\0' < "$1/manifesto" | wc -c) / 2 )) || print 0; }

# Apaga de vez os lotes vencidos (chamado pela vigilância)
quarentena_vencida() {
  local l ts k agora=$(date +%s)
  for l in ${(f)"$(lotes)"}; do
    ts=$(lote_ts "$l"); [[ -n $ts ]] || continue
    (( agora - ts >= QUARENTENA_DIAS * 86400 )) || continue
    k=$(kb "$l")
    rm -rf -- "$l"
    log "quarentena venceu: ${l:t} ($(humano $k) apagados de vez)"
    registrar_limpeza $k "Quarentena de ${l:t} (venceu)" apagado
  done
}

# Registro que o app lê: data|kb|descrição|destino (apagado, quarentena ou lixeira)
LIMPEZAS="$DIR_CONF/limpezas.csv"
registrar_limpeza() {
  mkdir -p "$DIR_CONF"
  print -r -- "$(date +%s)|$1|${2//|/ }|$3" >> "$LIMPEZAS"
  if (( $(wc -l < "$LIMPEZAS") > 2000 )); then
    tail -n 1000 "$LIMPEZAS" > "$LIMPEZAS.tmp" && mv "$LIMPEZAS.tmp" "$LIMPEZAS"
  fi
}

apagar_alvo() {
  local d
  # modo de teste: faz tudo, menos apagar
  [[ -n $KEEPFREE_SIMULAR ]] && { print -r -- "[simulado] $1 $2" >> "$DIR_CONF/simulado.log"; return 0; }
  case $1 in
    conteudo) find "$2" -mindepth 1 -delete 2>/dev/null ;;
    antigos)  logs_antigos "$2" | xargs -0 rm -f -- 2>/dev/null ;;
    electron) for d in ${(f)"$(electron_dirs "$2")"}; do find "$d" -mindepth 1 -delete 2>/dev/null; done ;;
    perfil-navegador) for d in ${(f)"$(perfil_dirs "$2")"}; do find "$d" -mindepth 1 -delete 2>/dev/null; done ;;
    quarentena-pasta)     quarentenar "$2" ;;
    quarentena-conteudo)  quarentenar "$2"/*(DN) ;;
    quarentena-antigos:*) quarentenar ${(0)"$(arquivos_antigos "$1" "$2")"} ;;
    quarentena-antigos-sem-uso:*) quarentenar ${(0)"$(arquivos_antigos "$1" "$2" | sem_uso_recente "$1")"} ;;
    apagar-antigos:*) arquivos_antigos "$1" "$2" | xargs -0 rm -f -- 2>/dev/null ;;
    apagar-pasta) [[ $2 == $HOME/Library/* ]] && rm -rf -- "$2" ;;
    extensoes-antigas) for d in ${(f)"$(extensoes_antigas "$2")"}; do [[ $d == $2/* ]] && rm -rf -- "$d"; done ;;
    workspace-orfaos)  quarentenar ${(f)"$(workspace_orfaos "$2")"} ;;
    claude-orfaos)     quarentenar ${(f)"$(claude_orfaos "$2")"} ;;
    comando-brew) sem_travar 900 env HOMEBREW_NO_AUTO_UPDATE=1 brew cleanup >/dev/null 2>&1 ;;
    comando-go) sem_travar 600 go clean -modcache >/dev/null 2>&1 ;;
    comando-simctl) sem_travar 300 xcrun simctl delete unavailable >/dev/null 2>&1 ;;
  esac
  return 0
}

vai_pra_quarentena() { [[ $1 == quarentena-* || $1 == workspace-orfaos || $1 == claude-orfaos ]]; }

# O que acontece se limpar (texto pro app e pras IAs)
efeito() {
  vai_pra_quarentena "$1" && { print -r -- "Vai pra quarentena por $QUARENTENA_DIAS dias: sai daqui, mas dá pra desfazer. Depois disso é apagado de vez."; return; }
  case $1 in
    antigos) print -r -- "Apaga na hora só os arquivos de log antigos. A pasta e o resto ficam." ;;
    extensoes-antigas) print -r -- "Apaga as versões antigas das extensões. A versão em uso de cada uma fica." ;;
    comando-*) print -r -- "Roda o comando de limpeza da própria ferramenta, que sabe o que está em uso." ;;
    apagar-antigos:*) print -r -- "Apaga na hora só os arquivos antigos. Os recentes ficam." ;;
    *)       print -r -- "Apaga na hora. É cache: o app recria quando precisar (pode ficar um pouco mais lento na primeira vez)." ;;
  esac
}

# ─── dicas pras pastas grandes ─────────────────────────────────────────────

dica() {
  case $1 in
    */Crashpad*)                 print "Relatórios de crash — lixo. keepfree limpar" ;;
    *"Adobe Camera Raw"*)        print "Cache do Lightroom. Lightroom → Ajustes → Desempenho: mude pro HD externo e limite o tamanho" ;;
    */Application\ Support/Google/Chrome|*/Application\ Support/Google) print "Perfis do Chrome (favoritos, logins). Apague perfis que não usa pelo próprio Chrome" ;;
    */Caches/Google*)            print "Cache do Chrome — keepfree limpar (com o Chrome fechado)" ;;
    */.lmstudio*|*/.ollama*|*huggingface*) print "Modelos de IA locais. Apague os que não usa ou mova pro HD externo pelo app" ;;
    */.codex)                    print "Codex: histórico de conversas (sessions) e plugins. Arquive sessões antigas" ;;
    */Application\ Support/Codex) print "App do Codex. Se for grande, veja a pasta Crashpad dentro dela" ;;
    */Documents/Codex)           print "Pastas de trabalho que o Codex cria por data pra tarefas sem projeto" ;;
    */Claude/vm_bundles|*/Application\ Support/Claude) print "Claude Desktop (inclui a máquina virtual do Cowork)" ;;
    */.claude)                   print "Claude Code: histórico de sessões e projetos" ;;
    */Movies/.gallery)           print "Stills da galeria do DaVinci Resolve. keepfree mover pro HD externo" ;;
    */CacheClip*)                print "Cache de render do DaVinci. No Resolve: Playback → Delete Render Cache" ;;
    */Blackmagic\ Design)        print "DaVinci Resolve (bancos de projetos, LUTs)" ;;
    */MobileSync*)               print "Backups de iPhone/iPad. Finder → seu iPhone → Gerenciar backups" ;;
    */Developer/CoreSimulator)   print "Simuladores iOS. xcrun simctl delete unavailable" ;;
    */Developer/Xcode)           print "Xcode (DerivedData, arquivos de iOS). keepfree limpar limpa o DerivedData" ;;
    */Containers/com.docker.docker) print "Disco do Docker (Docker.raw). Docker Desktop → Settings → Resources limita o tamanho; docker system prune limpa imagens velhas" ;;
    */Application\ Support/com.docker.install) print "Sobra do instalador do Docker. Com o Docker atualizado, pode apagar" ;;
    */.docker)                   print "Configuração e extensões do Docker" ;;
    */Caches/ms-playwright)      print "Navegadores do Playwright. Automações precisam deles: só tire se não usa Playwright (pela análise geral, vai pra quarentena)" ;;
    */.config)                   print "Configurações de programas de terminal (inclui a quarentena do keepfree: keepfree quarentena)" ;;
    */Steam)                     print "Jogos da Steam. Desinstale ou mova a biblioteca pelo app" ;;
    */Downloads)                 print "Downloads. Apague instaladores (.dmg, .zip, .pkg) antigos" ;;
    */Desktop)                   print "Mesa (Desktop). Capturas de tela costumam acumular aqui" ;;
    */.npm|*/.cache|*/pnpm|*/Caches/pip) print "Cache de programação — keepfree limpar" ;;
    */Library/Logs)              print "Logs — keepfree limpar apaga os antigos" ;;
    */Library/Containers)        print "Dados de apps da App Store. Veja qual app pesa em Ajustes → Armazenamento" ;;
    *WhatsApp*|*Telegram*)       print "Mídia de conversas. Limpe pelo próprio app (Armazenamento)" ;;
    */Library/Containers/*)      print "Dados do app ${1:t} (App Store ou sandbox). Limpe pelo próprio app" ;;
    */Group\ Containers/*)       print "Dados que um grupo de apps compartilha (${1:t}). Limpe pelo próprio app" ;;
    */Caches/*)                  print "Cache de app — costuma ser seguro apagar com o app fechado" ;;
    */Pictures/*.photoslibrary)  print "Fotos. Ative 'Otimizar armazenamento' no app Fotos" ;;
    */Music/*)                   print "Música/podcasts baixados" ;;
  esac
}

# ─── comandos ──────────────────────────────────────────────────────────────

cmd_status() {
  local livre=$(livre_kb) total=$(total_kb) meta=$(( $(gb_kb $META_GB) ))
  print -r -- "${B}keepfree${N} ${D}$VERSAO${N}"
  print -r -- ""
  printf "  Espaço livre: %s%s%s de %s\n" "$B" "$(humano $livre)" "$N" "$(humano $total)"
  printf "  %s\n" "$(barra $(( total - livre )) $total)"
  if (( livre >= meta )); then
    printf "  Meta: %s GB livres  %s✓ dentro da meta%s (%s de folga)\n" "$META_GB" "$G" "$N" "$(humano $(( livre - meta )))"
  else
    printf "  Meta: %s GB livres  %s✗ faltam %s%s → keepfree analisar\n" "$META_GB" "$R" "$(humano $(( meta - livre )))" "$N"
  fi
  print -r -- ""
  if ativo; then
    print -r -- "  Vigilância automática: ${G}ligada${N} (a cada ${INTERVALO_HORAS}h)"
  else
    print -r -- "  Vigilância automática: ${Y}desligada${N} → keepfree ativar"
  fi
  print -r -- "  Limpeza automática abaixo da meta: $AUTO_LIMPEZA  ${D}(keepfree config auto-limpeza sim|nao)${N}"
  print -r -- ""
  print -r -- "  ${D}keepfree ajuda — lista todos os comandos${N}"
}

cmd_limpar() {
  local executar=0 sim=0 silencioso=0 a
  for a; do
    case $a in
      --executar)   executar=1 ;;
      --sim|-y)     sim=1 ;;
      --silencioso) silencioso=1 ;;
      *) erro "opção desconhecida: $a" ;;
    esac
  done

  local -a fila
  local total=0 tipo desc cam bloq t quem
  while IFS='|' read -r t desc bloq tipo cam quem; do
    if (( bloq )); then
      (( silencioso )) || printf "  %s%-9s%s %s %s(feche o %s pra limpar)%s\n" "$D" "$(humano $t)" "$N" "$desc" "$Y" "${quem:-app}" "$N"
      continue
    fi
    fila+=("$tipo|$cam|$desc|$t")
    (( total += t ))
    (( silencioso )) || printf "  %s%-9s%s %s\n" "$B" "$(humano $t)" "$N" "$desc"
  done < <(listar_lixo | sort -t'|' -k1,1nr)

  LIBERADO_KB=0
  if (( ${#fila} == 0 )); then
    (( silencioso )) || print -r -- "  ${G}Nada pra limpar agora.${N}"
    return 0
  fi
  (( silencioso )) || print -r -- "  ${B}Total: $(humano $total)${N}"

  if (( ! executar )); then
    print -r -- "  ${D}Simulação — nada foi apagado. Pra apagar: ${N}${C}keepfree limpar --executar${N}"
    return 0
  fi
  if (( ! sim )) && ! confirmar "Apagar $(humano $total) de lixo seguro?"; then
    print -r -- "Cancelado."
    return 0
  fi

  local antes=$(livre_kb) item
  for item in $fila; do
    IFS='|' read -r tipo cam desc t <<< "$item"
    apagar_alvo $tipo "$cam"
    log "limpou: $desc ($(humano $t))"
    registrar_limpeza $t "$desc" apagado
  done
  LIBERADO_KB=$total
  (( silencioso )) || print -r -- "  ${G}✓ Limpo.${N} Livre agora: $(humano $(livre_kb)) (antes $(humano $antes))"
}

# ─── coleta (saída em linhas campo|campo, usada pelo terminal e pelo app) ──

# kb|descrição|bloqueado(0/1)|tipo|caminho|quem está usando
listar_lixo() {
  local tipo desc cam cond t quem
  fotografar
  while IFS='|' read -r tipo desc cam cond; do
    [[ -d $cam ]] || continue
    t=$(medir $tipo "$cam")
    (( t < 1024 )) && continue
    quem=$(quem_usa "$tipo" "$cam" "$cond")
    print -r -- "$t|$desc|${${quem:+1}:-0}|$tipo|$cam|$quem"
  done < <(alvos_seguros)
}

id_de() { md5 -qs "$1|$2" | cut -c1-8; }

# id|nivel|kb|quem está usando|descrição|nota|tipo|caminho — tudo que a análise geral encontra
listar_geral() {
  local nivel tipo desc cam cond nota t
  fotografar
  while IFS='|' read -r nivel tipo desc cam cond nota; do
    [[ -e $cam ]] || continue
    t=$(medir "$tipo" "$cam")
    (( t < 5120 )) && continue
    print -r -- "$(id_de "$tipo" "$cam")|$nivel|$t|$(quem_usa "$tipo" "$cam" "$cond")|$desc|$nota|$tipo|$cam"
  done < <(alvos) | sort -t'|' -k2,2r -k3,3nr
}

# n|kb|app|pasta — só apps acima do limite de crash
listar_crashes() {
  local c n
  for c in ${(f)"$(crashpads)"}; do
    ignorado "$c" && continue
    n=$(find "$c" -type f 2>/dev/null | wc -l | tr -d ' ')
    (( n > LIMITE_CRASH )) && print -r -- "$n|$(kb "$c")|${c:h:t}|$c"
  done
}

# tipo(reabre|erro|interrompida)|label|app, código ou sinal|programa
#   interrompida = encerrada de fora (SIGTERM, SIGKILL, SIGINT, SIGHUP): o sistema, um
#                  tempo-limite ou a própria tarefa. Não é o mesmo que dar erro.
listar_tarefas() {
  local lista desativadas f label linha pid st prog ka sinal
  lista=$(launchctl list 2>/dev/null)
  desativadas=$(launchctl print-disabled "gui/$UID" 2>/dev/null)
  for f in "$HOME/Library/LaunchAgents"/*.plist(N); do
    label=$(/usr/libexec/PlistBuddy -c 'Print :Label' "$f" 2>/dev/null) || continue
    print -r -- "$desativadas" | grep -qF "\"$label\" => disabled" && continue
    print -r -- "$desativadas" | grep -qF "\"$label\" => true" && continue
    linha=$(print -r -- "$lista" | awk -v l="$label" '$3==l')
    [[ -z $linha ]] && continue
    pid=${${=linha}[1]} st=${${=linha}[2]}
    prog=$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$f" 2>/dev/null ||
           /usr/libexec/PlistBuddy -c 'Print :Program' "$f" 2>/dev/null)
    ka=$(/usr/libexec/PlistBuddy -c 'Print :KeepAlive' "$f" 2>/dev/null)
    if [[ $prog == *.app/Contents/MacOS/* && -n $ka && $ka != false ]]; then
      print -r -- "reabre|$label|${${prog%%.app/*}:t}|$prog"
    elif [[ $pid == - && $st == -<-> ]]; then
      sinal="SIG$(kill -l ${st#-} 2>/dev/null)"
      case ${st#-} in
        1|2|9|15) print -r -- "interrompida|$label|$sinal|$prog" ;;
        *)        print -r -- "erro|$label|$sinal|$prog" ;;   # travou (SIGSEGV, SIGABRT…)
      esac
    elif [[ $pid == - && $st != 0 ]]; then
      print -r -- "erro|$label|$st|$prog"
    fi
  done
}

# ─── tarefas novas em segundo plano ───────────────────────────────────────
# A cada rodada, a vigilância guarda a lista de LaunchAgents/LaunchDaemons. O que
# aparece de uma rodada pra outra é avisado: é assim que uma automação instala algo
# que reabre o navegador sozinho, ou um programa se instala pra rodar sempre.
TAREFAS="$DIR_CONF/tarefas.lista"
NOVAS="$DIR_CONF/tarefas-novas.csv"     # data|label|arquivo|programa
VISTAS="$DIR_CONF/tarefas-vistas"       # arquivos que a pessoa já conferiu
PASTAS_TAREFAS=("$HOME/Library/LaunchAgents" /Library/LaunchAgents /Library/LaunchDaemons)

foto_tarefas() {
  local d f
  for d in $PASTAS_TAREFAS; do
    for f in "$d"/*.plist(N); do caminho_ok "$f" && print -r -- "$f"; done
  done | LC_ALL=C sort
}
label_de() { /usr/libexec/PlistBuddy -c 'Print :Label' "$1" 2>/dev/null || print -r -- "${1:t:r}"; }
prog_de()  {
  /usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$1" 2>/dev/null ||
    /usr/libexec/PlistBuddy -c 'Print :Program' "$1" 2>/dev/null
}

vigiar_tarefas() {
  local agora=$(date +%s) atual f label prog
  atual=$(foto_tarefas)
  if [[ -f $TAREFAS ]]; then
    for f in ${(f)"$(LC_ALL=C comm -13 "$TAREFAS" <(print -r -- "$atual"))"}; do
      [[ -n $f && $f != $PLIST ]] || continue
      label=$(label_de "$f"); prog=$(prog_de "$f")
      mkdir -p "$DIR_CONF"
      print -r -- "$agora|${label//|/ }|$f|${prog//|/ }" >> "$NOVAS"
      log "tarefa nova em segundo plano: $label ($f → $prog)"
      avisar "Nova tarefa em segundo plano" "$label vai rodar sozinha ($(curto "${prog:-$f}")). Se não reconhece, veja no KeepFree." seguranca
    done
  fi
  mkdir -p "$DIR_CONF"
  print -r -- "$atual" > "$TAREFAS"
}

# data|label|arquivo|programa — as que apareceram nos últimos 7 dias e a pessoa não conferiu
listar_novas() {
  [[ -f $NOVAS ]] || return 0
  local t label f prog lim=$(( $(date +%s) - 7 * 86400 ))
  while IFS='|' read -r t label f prog; do
    (( t >= lim )) || continue
    [[ -e $f ]] || continue
    [[ -f $VISTAS ]] && grep -qxF -- "$f" "$VISTAS" && continue
    print -r -- "$t|$label|$f|$prog"
  done < "$NOVAS"
}

# keepfree tarefa-ok <label ou arquivo> — "conferi, é minha": some dos alertas
cmd_tarefa_ok() {
  [[ -n $1 ]] || erro "use: keepfree tarefa-ok <nome-da-tarefa>"
  local t label f prog achou=0
  while IFS='|' read -r t label f prog; do
    [[ $1 == "$label" || $1 == "$f" ]] || continue
    mkdir -p "$DIR_CONF"; print -r -- "$f" >> "$VISTAS"; achou=1
  done < <(listar_novas)
  (( achou )) || erro "não achei $1 entre as tarefas novas"
  log "tarefa conferida: $1"
  print -r -- "${G}✓${N} Ok, $1 não aparece mais nos alertas."
}

# Navegador com controle remoto: avisa uma vez por dia por navegador e porta
vigiar_remoto() {
  local app porta perfil
  while IFS='|' read -r app porta perfil; do
    pode_avisar "remoto-${app//[^A-Za-z0-9]/}-$porta" 86400 || continue
    log "controle remoto ligado: $app porta $porta${perfil:+ (perfil $perfil)}"
    avisar "$app com controle remoto ligado" "Qualquer programa do Mac pode comandá-lo (porta $porta). Se não é uma automação sua, feche." seguranca
  done < <(listar_remoto)
}

# ─── regras: limpar um item quando ele passar de um tamanho ─────────────────
REGRAS="$DIR_CONF/regras"               # caminho|kb

tamanho_kb() {   # 500M, 3G ou 3 (GB) → KB
  case $1 in
    <->[Mm]) print $(( ${1%?} * 977 )) ;;
    <->[Gg]) print $(( ${1%?} * 976562 )) ;;
    <->)     print $(( $1 * 976562 )) ;;
    *)       return 1 ;;
  esac
}

# keepfree regra                             lista
# keepfree regra <caminho ou id> <3G|500M>   a vigilância limpa quando passar disso
# keepfree regra --remover <caminho>
cmd_regra() {
  local remover=0 alvo lim nivel tipo desc cam cond nota achou=0 l
  [[ $1 == --remover ]] && { remover=1; shift; }
  if (( $# == 0 )); then
    [[ -s $REGRAS ]] || { print -r -- "Nenhuma regra. Ex.: keepfree regra ~/Library/Caches/Google/Chrome 3G"; return 0; }
    print -r -- "${B}Regras${N} ${D}(a vigilância limpa o item quando ele passa do tamanho)${N}"
    while IFS='|' read -r cam lim; do print -r -- "  $(curto "$cam")  acima de $(humano $lim)"; done < "$REGRAS"
    return 0
  fi
  alvo=$1
  if [[ $alvo =~ '^[0-9a-f]{8}$' && ! -e $alvo ]]; then
    l=$(linhas_geral | awk -F'|' -v id="$alvo" '$1 == id {print $8; exit}')
    [[ -n $l ]] || erro "não achei o item $alvo (rode: keepfree dados geral)"
    alvo=$l
  fi
  alvo=${${alvo/#\~/$HOME}:a}
  if (( remover )); then
    [[ -f $REGRAS ]] && grep -q "^${(b)alvo}|" "$REGRAS" || erro "não há regra pra $(curto "$alvo")"
    grep -v "^${(b)alvo}|" "$REGRAS" > "$REGRAS.tmp"; mv "$REGRAS.tmp" "$REGRAS"
    log "regra removida: $alvo"
    print -r -- "${G}✓${N} Regra de $(curto "$alvo") removida."
    return 0
  fi
  lim=$(tamanho_kb "$2") || erro "use: keepfree regra <pasta> <tamanho>  (ex.: 3G ou 500M)"
  while IFS='|' read -r nivel tipo desc cam cond nota; do
    [[ $cam == "$alvo" && $nivel == seguro ]] && { achou=1; break; }
  done < <(alvos)
  (( achou )) || erro "regra só vale pra lixo seguro do catálogo (veja: keepfree limpar)"
  mkdir -p "$DIR_CONF"
  { [[ -f $REGRAS ]] && grep -v "^${(b)alvo}|" "$REGRAS"; print -r -- "$alvo|$lim"; } > "$REGRAS.tmp" && mv "$REGRAS.tmp" "$REGRAS"
  log "regra: limpar $alvo acima de $(humano $lim)"
  print -r -- "${G}✓${N} A vigilância limpa ${B}$desc${N} quando passar de $(humano $lim)."
}

# Chamado pela vigilância: limpa o que passou do tamanho (se ninguém estiver usando)
aplicar_regras() {
  [[ -s $REGRAS ]] || return 0
  local cam lim nivel tipo desc c cond nota t quem l
  local -a catalogo
  catalogo=(${(f)"$(alvos_seguros)"})
  fotografar
  while IFS='|' read -r cam lim; do
    for l in $catalogo; do
      IFS='|' read -r tipo desc c cond <<< "$l"
      [[ $c == "$cam" && -e $cam ]] || continue
      t=$(medir "$tipo" "$cam")
      (( t > lim )) || continue
      quem=$(quem_usa "$tipo" "$cam" "$cond")
      if [[ -n $quem ]]; then log "regra: $desc passou de $(humano $lim), mas o $quem está aberto"; continue; fi
      apagar_alvo "$tipo" "$cam"
      registrar_limpeza $t "$desc (regra)" apagado
      log "regra: limpou $desc ($(humano $t), limite $(humano $lim))"
      avisar "Regra: limpei $desc" "Tinha $(humano $t) (seu limite é $(humano $lim))." sucesso
    done
  done < "$REGRAS"
}

# app|porta|perfil
listar_remoto() {
  local l exe porta perfil
  ps -axo command= | grep -E -- '--remote-debugging-port=[0-9]+' | grep -v -- '--type=' | grep -v grep |
  while read -r l; do
    exe=${${l%%.app/*}:t}
    porta=${l##*--remote-debugging-port=}; porta=${porta%% *}
    perfil=""
    [[ $l == *--user-data-dir=* ]] && perfil=${${l##*--user-data-dir=}%% --*}
    print -r -- "$exe|$porta|$perfil"
  done
}

# kb|caminho — as N maiores pastas ($2 = tamanho mínimo em KB, padrão 512 MB)
listar_maiores() {
  local -a cands
  cands=(
    "$HOME/Library/Application Support"/*(N/)
    "$HOME/Library/Caches"/*(N/)
    "$HOME/Library/Group Containers"/*(N/)
    "$HOME/Library/Developer"/*(N/)
    "$HOME/Library/Containers"/*(N/) "$HOME/Library/Logs"
    "$HOME"/.*(N/)
    "$HOME/Movies"/*(N/) "$HOME/Movies"/.*(N/)
    "$HOME/Documents"/*(N/) "$HOME/Documents"/.*(N/)
    "$HOME/Pictures"/*(N/) "$HOME/Music"/*(N/)
    "$HOME/Downloads" "$HOME/Desktop"
  )
  local p t
  for p in $cands; do
    [[ ${p:t} == .Trash ]] && continue
    caminho_ok "$p" || continue
    t=$(kb "$p")
    (( t >= ${2:-524288} )) && ! ignorado "$p" && print -r -- "$t|$p"
  done | sort -t'|' -k1,1nr | head -n ${1:-15}
}

oculta() { [[ ${1#$HOME} == */.* ]]; }

# ─── o que o macOS esconde ("Dados do Sistema") ───────────────────────────

# Espaço que o macOS libera sozinho quando precisa (caches do sistema, snapshots):
# a diferença entre o livre "pra uso importante" e o livre de verdade.
purgavel_kb() {
  local v
  v=$(osascript -l JavaScript -e 'function run(a) { ObjC.import("Foundation");
    var r = $.NSURL.fileURLWithPath(a[0]).resourceValuesForKeysError(["NSURLVolumeAvailableCapacityForImportantUsageKey", "NSURLVolumeAvailableCapacityKey"], null);
    var o = ObjC.deepUnwrap(r);
    return Math.max(0, Math.floor((o.NSURLVolumeAvailableCapacityForImportantUsageKey - o.NSURLVolumeAvailableCapacityKey) / 1024)) }' "$HOME" 2>/dev/null)
  print -r -- "${v:-0}"
}
snapshots_n() { tmutil listlocalsnapshots / 2>/dev/null | grep -c 'com.apple'; }
DOCKER_RAW="$HOME/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
docker_kb()  { [[ -f $DOCKER_RAW ]] && kb "$DOCKER_RAW" || print 0; }
simuladores_n() {
  xcrun --find simctl >/dev/null 2>&1 || { print 0; return; }
  xcrun simctl list devices unavailable 2>/dev/null | grep -c '(unavailable'
}

# kb|caminho|último uso (0 = nunca) — arquivos de 1 GB ou mais, fora de ~/Library e de
# pastas ocultas, que não mudam nem são abertos há 90 dias (pelo Spotlight: rápido).
listar_esquecidos() {
  local f u lim=$(( $(date +%s) - 90 * 86400 ))
  mdfind -onlyin "$HOME" 'kMDItemFSSize >= 1000000000 && kMDItemContentModificationDate < $time.today(-90)' 2>/dev/null |
  while IFS= read -r f; do
    [[ $f == $HOME/Library/* || ${f#$HOME} == */.* || $f == *.app/* ]] && continue
    [[ -f $f ]] && caminho_ok "$f" || continue
    ignorado "$f" && continue
    u=$(mdls -raw -name kMDItemLastUsedDate "$f" 2>/dev/null)
    if [[ -n $u && $u != "(null)" ]]; then
      u=$(date -j -f '%Y-%m-%d %H:%M:%S %z' "$u" +%s 2>/dev/null)
      (( ${u:-0} > lim )) && continue
    else
      u=0
    fi
    print -r -- "$(kb "$f")|$f|${u:-0}"
  done | sort -t'|' -k1,1nr | head -n 20
}

esquecidos() {
  local k f u achou=0
  while IFS='|' read -r k f u; do
    achou=1
    printf "  %s%-9s%s %s ${D}(%s)${N}\n" "$B" "$(humano $k)" "$N" "$(curto "$f")" \
      "$( (( u )) && print "aberto em $(date -r $u '+%d/%m/%Y')" || print "sem registro de uso")"
  done < <(listar_esquecidos)
  (( achou )) || print -r -- "  ${G}Nenhum arquivo de 1 GB+ parado há 90 dias fora das pastas do sistema.${N}"
  (( achou )) && print -r -- "  ${D}↳ São seus: apague o que não precisa ou use keepfree mover pra um HD externo.${N}"
}

# keepfree snapshots [--liberar] — snapshots locais do Time Machine
cmd_snapshots() {
  local n=$(snapshots_n) antes
  if [[ $1 != --liberar ]]; then
    print -r -- "Snapshots locais do Time Machine: $n"
    print -r -- "Espaço que o macOS libera sozinho quando precisa: $(humano $(purgavel_kb))"
    (( n )) && print -r -- "${D}Pra liberar já: keepfree snapshots --liberar${N}"
    return 0
  fi
  (( n )) || { print -r -- "Não há snapshots locais."; return 0; }
  if [[ $2 != --sim ]] && ! confirmar "Pedir ao macOS pra liberar o espaço dos $n snapshot(s) locais? Os backups do Time Machine no HD continuam."; then
    print -r -- "Cancelado."; return 0
  fi
  antes=$(livre_kb)
  tmutil thinlocalsnapshots / 999999999999 4 >/dev/null 2>&1 || erro "o macOS não liberou (tmutil thinlocalsnapshots falhou)"
  log "liberou snapshots locais ($n antes, $(snapshots_n) depois)"
  print -r -- "${G}✓${N} Pronto. Snapshots: $n → $(snapshots_n). Livre: $(humano $antes) → $(humano $(livre_kb))"
}

# ─── memória, processador, processos esquecidos ───────────────────────────

memoria_total_kb() { print $(( $(sysctl -n hw.memsize) / 1024 )); }
memoria_livre_pct() { local v; v=$(sysctl -n kern.memorystatus_level 2>/dev/null); print -r -- "${v:-0}"; }
# swap: total reservado no disco e quanto está em uso (KB), do "vm.swapusage"
swap_kb() {   # $1 = total | used
  sysctl -n vm.swapusage 2>/dev/null | awk -v q="$1" '{
    for (i = 1; i <= NF; i++) if ($i == q) { v = $(i + 2); u = substr(v, length(v)); sub(/[MG]$/, "", v)
      printf "%d\n", (u == "G" ? v * 1048576 : v * 1024); exit } }'
}
pressao() {
  case $(sysctl -n kern.memorystatus_vm_pressure_level 2>/dev/null) in
    4) print critico ;; 2) print aviso ;; *) print normal ;;
  esac
}
ligado_ha_s() { print $(( $(date +%s) - $(sysctl -n kern.boottime | sed -E 's/^\{ sec = ([0-9]+),.*/\1/') )); }

# app ou programa dono do executável (o .app mais de fora: "Google Chrome")
APP_DE_EXE='{ app = exe; i = index(app, ".app/"); if (i) app = substr(app, 1, i - 1); n = split(app, p, "/"); app = p[n] }'

# kb|app|processos — memória por app: o Chrome com todos os ajudantes dele conta como um
listar_memoria() {
  ps -axww -o rss=,comm= | awk '{ kb = $1; sub(/^ *[0-9]+ +/, ""); exe = $0; '"$APP_DE_EXE"'
    soma[app] += kb; qtd[app]++ }
    END { for (a in soma) if (soma[a] > 0) print soma[a] "|" a "|" qtd[a] }' | sort -t'|' -k1,1nr | head -n ${1:-8}
}

# " Quem mais usa: Chrome (6,2 GB), Code (3,1 GB)." — pro texto dos avisos
quem_mais_usa() {
  local k app n partes=()
  while IFS='|' read -r k app n; do partes+=("$app ($(humano $k))"); done < <(listar_memoria 3)
  (( ${#partes} )) && print -rn -- " Quem mais usa: ${(j:, :)partes}."
}

# Programas de programação (node, python, servidores de MCP…) do usuário, parados há
# DEV_HORAS horas, quase sem processador e com 50 MB ou mais. orfao = o processo que
# abriu ele já fechou (o pai virou o launchd): é o que sobra de sessão encerrada.
# pid|nome|kb|horas|cpu|orfao|pai|comando
DEV_NOMES='node|python|python3|Python|bun|deno|ruby|java|php|uv|uvx|npm|npx|pnpm|tsx|esbuild|mcp-server.*'
listar_dev() {   # $1 = horas parado (padrão DEV_HORAS), $2 = memória mínima em KB (padrão 50 MB)
  local minimo=$(( ${1:-$DEV_HORAS} * 3600 ))
  awk -v uid=$UID -v minimo=$minimo -v minkb=${2:-51200} -v nomes="^($DEV_NOMES)\$" -F'\t' '
    function segundos(e,   d, p, n) { d = 0; if (index(e, "-")) { split(e, p, "-"); d = p[1]; e = p[2] }
      n = split(e, p, ":"); return d * 86400 + (n == 3 ? p[1] * 3600 + p[2] * 60 + p[3] : p[1] * 60 + p[2]) }
    NR == FNR { cmd[$1] = $2; next }
    { pid = $1; ppid = $2; rss = $3; cpu = $4; et = $5; u = $6; exe = $7
      n = split(exe, q, "/"); base = q[n]; pais[pid] = base
      if (u != uid) next
      sis = (exe ~ /^\/(System|usr\/libexec|usr\/sbin|sbin)\//)
      ehdev = (base ~ nomes) || (tolower(cmd[pid]) ~ /(^|[\/ @-])mcp([- _\/@]|$)|model-?context-?protocol/)
      if (sis || !ehdev) next
      s = segundos(et)
      if (s < minimo || cpu + 0 >= 1 || rss < minkb) next
      linha[pid] = pid "|" base "|" rss "|" int(s / 3600) "|" cpu "|" (ppid == 1 ? 1 : 0); pp[pid] = ppid
      cm[pid] = substr(cmd[pid], 1, 160) }
    END { for (p in linha) { pai = (pp[p] in pais) ? pais[pp[p]] : "launchd"; print linha[p] "|" pai "|" cm[p] } }
  ' <(ps -axww -o pid=,args= | sed -E 's/^ *([0-9]+) /\1\t/') \
    <(ps -axww -o pid=,ppid=,rss=,pcpu=,etime=,uid=,comm= | awk '{ c = $0; for (i = 1; i <= 6; i++) sub(/^ *[^ ]+ +/, "", c)
        printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, c }') | sort -t'|' -k3,3nr
}

# keepfree encerrar <pid> [--sim] — fecha um programa de programação esquecido.
# Só aceita o que a lista de esquecidos mostraria (do usuário, node/python/MCP…).
cmd_encerrar() {
  local pid=$1 sim=0 linha nome
  [[ $2 == --sim ]] && sim=1
  [[ $pid == <-> ]] || erro "use: keepfree encerrar <pid>  (veja: keepfree processos)"
  linha=$(listar_dev 0 0 | awk -F'|' -v p=$pid '$1 == p')
  [[ -n $linha ]] || erro "o processo $pid não é um programa de programação parado deste usuário: não encerro"
  nome=${${linha#*|}%%|*}
  if (( ! sim )) && ! confirmar "Encerrar $nome (pid $pid, $(humano ${${linha#*|*|}%%|*}))?"; then
    print -r -- "Cancelado."; return 0
  fi
  kill -TERM $pid 2>/dev/null || erro "não consegui encerrar $pid"
  local i; for i in 1 2 3 4 5 6; do kill -0 $pid 2>/dev/null || break; sleep 0.5; done
  if kill -0 $pid 2>/dev/null; then
    log "pediu pra encerrar $nome ($pid), ainda rodando"
    print -r -- "${Y}!${N} $nome ($pid) ainda está rodando. Feche pelo programa que abriu ele."
    return 1
  fi
  log "encerrou $nome ($pid)"
  print -r -- "${G}✓${N} $nome ($pid) encerrado."
}

processos() {
  local pid nome k h cpu orfao pai cmd achou=0
  while IFS='|' read -r pid nome k h cpu orfao pai cmd; do
    achou=1
    printf "  %s%-9s%s %s ${D}(pid %s, há %sh%s)${N}\n" "$B" "$(humano $k)" "$N" "$nome" $pid $h \
      "$( (( orfao )) && print ', sem o programa que abriu' || print ", aberto por $pai")"
    print -r -- "            ${D}↳ ${cmd[1,90]}${N}"
  done < <(listar_dev)
  (( achou )) || print -r -- "  ${G}Nenhum programa de programação parado há mais de ${DEV_HORAS}h.${N}"
  (( achou )) && print -r -- "  ${D}Pra fechar um: keepfree encerrar <pid>${N}"
}

# Processador no máximo: guarda há quanto tempo cada app está acima de 90% e avisa
# depois de CPU_MINUTOS. Só apps (.app): ferramenta de linha de comando trabalhando
# pesado (build, render) não é travamento.
CPU_EST="$DIR_CONF/processador.estado"   # pid|desde|app
checar_processador() {
  local agora=$(date +%s) pid desde app
  local -a quentes
  quentes=(${(f)"$(ps -axww -o pid=,pcpu=,comm= | awk -v uid=$UID '{ pid = $1; cpu = $2; sub(/^ *[0-9]+ +[0-9.]+ +/, ""); exe = $0
      if (cpu + 0 < 90 || index(exe, ".app/") == 0 || exe ~ /^\/System\//) next; '"$APP_DE_EXE"'
      print pid "|" app }')"})
  local -A antes
  [[ -f $CPU_EST ]] && while IFS='|' read -r pid desde app; do antes[$pid]=$desde; done < "$CPU_EST"
  mkdir -p "$DIR_CONF"
  : > "$CPU_EST.tmp"
  for l in $quentes; do
    pid=${l%%|*} app=${l#*|}
    desde=${antes[$pid]:-$agora}
    print -r -- "$pid|$desde|$app" >> "$CPU_EST.tmp"
    if (( agora - desde >= CPU_MINUTOS * 60 )) && pode_avisar "cpu-$pid" 3600; then
      log "processador no máximo: $app ($pid) há $(( (agora - desde) / 60 )) min"
      avisar "$app está com o processador no máximo" "Há $(( (agora - desde) / 60 )) minutos. Se ele não está fazendo algo pesado (render, exportação), pode estar travado: feche ou reinicie o app." atencao
    fi
  done
  mv "$CPU_EST.tmp" "$CPU_EST"
}

# Memória apertada: avisa (de hora em hora, ou já se ficar crítica) antes de o Mac travar
checar_memoria() {
  local pct=$(memoria_livre_pct) usada=$(swap_kb used) p=$(pressao)
  if (( pct < MEMORIA_MIN )) || [[ $p == critico ]] || (( usada >= SWAP_AVISO_GB * 1048576 )); then
    local chave=memoria janela=3600
    [[ $p == critico ]] && janela=900
    pode_avisar $chave $janela || return 0
    log "memória apertada: ${pct}% livre, swap $(humano $usada), pressão $p"
    avisar "Memória quase no fim: ${pct}% livre" "A reserva no disco (swap) está em $(humano $usada).$(quem_mais_usa) Fechar o que não está usando evita travar." $([[ $p == critico || $pct -lt 8 ]] && print critico || print atencao)
  fi
}

# Chamado pelo app a cada minuto: memória e processador (os dois são baratos)
cmd_checar_recursos() { checar_memoria; checar_processador; }

# Swap ocupa disco e só some com o reinício: sugere reiniciar, no máximo uma vez por dia
checar_reinicio() {
  local swap=$(swap_kb total) dias=$(( $(ligado_ha_s) / 86400 ))
  (( swap >= SWAP_AVISO_GB * 1048576 && dias >= 1 )) || return 0
  pode_avisar reinicio 86400 || return 0
  log "sugeriu reiniciar: swap $(humano $swap), ligado há $dias dia(s)"
  avisar "Reinicie o Mac quando puder" "A reserva de memória no disco (swap) ocupa $(humano $swap) e só some no reinício. Ligado há $dias dia(s)." atencao
}


# Há quantos dias a pasta (ou o arquivo) não muda: o item mais novo dela e do 1º nível
dias_sem_mexer() {
  local t
  t=$(find "$1" -maxdepth 1 -print0 2>/dev/null | xargs -0 stat -f %m 2>/dev/null | sort -rn | head -n 1)
  [[ -n $t ]] && print $(( ($(date +%s) - t) / 86400 ))
}
# ─── liberar memória antes de uma tarefa pesada ───────────────────────────
# Pronto pra um build, render ou modelo local? Compara com os limites de "tarefa pesada"
# (PESADO_MEM_MIN e PESADO_SWAP_GB) e, se existir, com a trava de memória do Claude Code
# (~/.claude/hooks/memoria-livre.sh), que recusa comando pesado sem folga.
TRAVA_CLAUDE="$HOME/.claude/hooks/memoria-livre.sh"
limites_pesado() {   # → mem_min|swap_max_gb|origem
  local m=$PESADO_MEM_MIN s=$PESADO_SWAP_GB o=keepfree
  if [[ -f $TRAVA_CLAUDE ]]; then
    local mm ss
    mm=$(grep -oE 'MEM_MIN="\$\{MEM_MIN:-[0-9]+\}"' "$TRAVA_CLAUDE" | grep -oE '[0-9]+' | head -n 1)
    ss=$(grep -oE 'SWAP_MAX_GB="\$\{SWAP_MAX_GB:-[0-9]+\}"' "$TRAVA_CLAUDE" | grep -oE '[0-9]+' | head -n 1)
    [[ -n $mm ]] && m=$mm; [[ -n $ss ]] && s=$ss; o=claude
  fi
  print -r -- "$m|$s|$o"
}

liberar_memoria() {
  local l m s o pct usada pronto=sim
  l=$(limites_pesado); m=${l%%|*}; s=${${l#*|}%%|*}; o=${l##*|}
  pct=$(memoria_livre_pct); usada=$(swap_kb used)
  (( pct >= m && usada < s * 1048576 )) || pronto=nao
  titulo "Pronto pra uma tarefa pesada? $( [[ $pronto == sim ]] && print "${G}sim${N}" || print "${Y}ainda não${N}")"
  print -r -- "  Memória livre: ${B}${pct}%${N} (precisa de $m%) · swap: ${B}$(humano $usada)${N} (até $s GB)$( [[ $o == claude ]] && print " ${D}· limites da trava do Claude Code${N}")"
  print -r -- ""
  print -r -- "  ${B}Feche o que não está usando:${N}"
  local k app n
  listar_memoria 6 | while IFS='|' read -r k app n; do printf "    %s%-9s%s %s\n" "$B" "$(humano $k)" "$N" "$app"; done
  local pid nome h orfao pai cm
  if [[ -n $(listar_dev) ]]; then
    print -r -- ""
    print -r -- "  ${B}Programas de programação esquecidos:${N} ${D}(keepfree encerrar <pid>)${N}"
    listar_dev | while IFS='|' read -r pid nome k h cpu orfao pai cm; do printf "    %s%-9s%s %s (pid %s, há %sh)\n" "$B" "$(humano $k)" "$N" "$nome" $pid $h; done
  fi
}

# ─── avisos também no WhatsApp (ou onde quiser) ───────────────────────────
# Um programa seu recebe a mensagem pronta num argumento ("💽 *Título*", linha em branco,
# texto: a formatação do WhatsApp) e as partes em KEEPFREE_TITULO e KEEPFREE_TEXTO.
# Serve um script que manda WhatsApp, um webhook do n8n… Fica em ~/.config/keepfree/encaminhar.
# Vai: o resumo da semana sempre, e os avisos urgentes se ENCAMINHAR_CRITICO=sim.
ENCAMINHAR="$DIR_CONF/encaminhar"
encaminhador() {   # o programa configurado, se ainda for seguro rodar
  local f
  [[ -f $ENCAMINHAR ]] || return 1
  f=$(head -n 1 "$ENCAMINHAR")
  [[ $f == /* && -f $f && -x $f && -O $f ]] || return 1
  (( (8#$(stat -f %Lp "$f") & 8#022) == 0 )) || return 1   # gravável pelo grupo ou por outros: não roda
  print -r -- "$f"
}
mandar_por() {   # $1 programa, $2 título, $3 texto
  KEEPFREE_TITULO=$2 KEEPFREE_TEXTO=$3 sem_travar 60 "$1" "💽 *$2*"$'\n\n'"$3"
}
encaminhar() {   # $1 título, $2 texto
  local f
  f=$(encaminhador) || return 0
  mandar_por "$f" "$1" "$2" >/dev/null 2>&1 &!
  log "encaminhou: $1"
}

cmd_encaminhar() {
  local f
  case $1 in
    "")
      if f=$(encaminhador); then print -r -- "Avisos encaminhados para: $(curto "$f") (urgentes: $ENCAMINHAR_CRITICO)"
      else print -r -- "Nada configurado. Ex.: keepfree encaminhar ~/bin/avisar-whatsapp.sh"; fi ;;
    --remover) rm -f "$ENCAMINHAR"; print -r -- "${G}✓${N} Não encaminha mais." ;;
    --testar)
      f=$(encaminhador) || erro "nada configurado (keepfree encaminhar <programa>)"
      mandar_por "$f" "Teste do KeepFree" "Se chegou, o resumo da semana vai chegar aqui também." >/dev/null 2>&1 &&
        print -r -- "${G}✓${N} Enviado por $(curto "$f")." || erro "o programa falhou" ;;
    *)
      f=${${1/#\~/$HOME}:a}
      [[ -f $f && -x $f ]] || erro "precisa ser um programa executável: $f"
      [[ -O $f ]] || erro "o programa precisa ser seu (dono: você)"
      (( (8#$(stat -f %Lp "$f") & 8#022) == 0 )) || erro "o programa não pode ser gravável por outros (chmod go-w)"
      mkdir -p "$DIR_CONF"; print -r -- "$f" > "$ENCAMINHAR"
      log "encaminhar avisos para $f"
      print -r -- "${G}✓${N} O resumo da semana vai também para $(curto "$f"). Urgentes também: keepfree config encaminhar-urgente sim" ;;
  esac
}

# ─── o nome antigo ─────────────────────────────────────────────────────────
# O que sobrou do Disco Livre sai: a vigilância antiga (a nova liga no lugar, se a antiga
# estava ligada), o app, o comando e a skill. Rodar de novo não faz nada.
cmd_migrar() {
  local a s n=0 vigia=0 velho="$HOME/Library/LaunchAgents/$ANTIGO_LABEL.plist"
  if [[ -f $velho ]]; then
    launchctl bootout "gui/$UID/$ANTIGO_LABEL" 2>/dev/null
    rm -f "$velho" && vigia=1 && (( ++n ))
  fi
  for a in "${KEEPFREE_APLICATIVOS:-/Applications}/Disco Livre.app" "$HOME/Applications/Disco Livre.app"; do
    [[ -d $a ]] || continue
    pkill -x DiscoLivre 2>/dev/null
    rm -rf "$a" && (( ++n ))
  done
  [[ -f $HOME/.local/bin/disco-livre ]] && rm -f "$HOME/.local/bin/disco-livre" && (( ++n ))
  for s in "$HOME/.claude/skills/disco-livre" "$HOME/.codex/skills/disco-livre"; do
    [[ -d $s ]] && rm -rf "$s" && (( ++n ))
  done
  (( vigia )) && ! ativo && cmd_ativar >/dev/null
  if (( n )); then
    log "migrou do Disco Livre: $n item(ns) antigo(s) removido(s)"
    print -r -- "${G}✓${N} O Disco Livre virou KeepFree: a configuração, a quarentena e o histórico vieram junto."
  fi
  return 0
}

# ─── atualizar ─────────────────────────────────────────────────────────────
# De onde vem a versão nova (só quando a pessoa pede; a rotina nunca fala com a rede):
#   site:https://…     o site do KeepFree: o versao.json e o pacote da versão (padrão)
#   github:dono/repo   as releases do GitHub, pelo `gh` (entra em repositório privado)
# Pra trocar, uma linha em ~/.config/keepfree/atualizar-de.
ATUALIZAR_DE_ARQ="$DIR_CONF/atualizar-de"
SITE_PADRAO="https://keepfree.pages.dev"   # rede: atualizar
origem_atualizacao() { [[ -f $ATUALIZAR_DE_ARQ ]] && head -n 1 "$ATUALIZAR_DE_ARQ" || print -r -- "site:$SITE_PADRAO"; }

# versao|origem|sha256 do pacote (só no site): a versão mais nova
release_nova() {
  local o=$(origem_atualizacao) j v s tag
  case $o in
    site:https://*)   # rede: atualizar
      j=$(curl -fsSL --max-time 20 "${o#site:}/versao.json" 2>/dev/null) || return 1   # rede: atualizar
      v=$(print -r -- "$j" | sed -nE 's/.*"versao": *"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' | head -n 1)
      s=$(print -r -- "$j" | sed -nE 's/.*"sha256": *"([0-9a-f]{64})".*/\1/p' | head -n 1)
      [[ -n $v && -n $s ]] || return 1
      print -r -- "$v|$o|$s" ;;
    github:?*/?*)
      command -v gh >/dev/null 2>&1 || return 1
      tag=$(sem_travar 30 gh release view -R "${o#github:}" --json tagName --jq .tagName 2>/dev/null) || return 1   # rede: atualizar
      [[ $tag == v<->.<->.<-> ]] || return 1
      print -r -- "${tag#v}|$o|" ;;
    *) return 1 ;;
  esac
}

mais_nova() { [[ $1 != $2 && $(print -rl -- "$1" "$2" | sort -V | tail -n 1) == $1 ]]; }   # $1 é mais nova que $2?

# baixa a versão $1 de $2 (com o SHA-256 $3, no site) em $4 → a pasta com o install.sh e o KeepFree.zip
baixar_versao() {
  local nova=$1 o=$2 sha=$3 tmp=$4 repo
  case $o in
    site:*)
      curl -fsSL --max-time 300 -o "$tmp/pacote.tgz" "${o#site:}/baixar/keepfree-$nova.tar.gz" 2>/dev/null || erro "não baixei o pacote da $nova"   # rede: atualizar
      [[ $(shasum -a 256 "$tmp/pacote.tgz" | cut -d' ' -f1) == $sha ]] || erro "o pacote baixado não bate com o SHA-256 do site: não instalei nada"
      tar -xzf "$tmp/pacote.tgz" -C "$tmp" 2>/dev/null || erro "o pacote da $nova veio estragado"
      print -r -- "$tmp/keepfree-$nova" ;;
    github:*)
      repo=${o#github:}
      sem_travar 120 gh api "repos/$repo/tarball/v$nova" > "$tmp/fonte.tgz" 2>/dev/null || erro "não baixei o código da v$nova"   # rede: atualizar
      mkdir -p "$tmp/fonte" && tar -xzf "$tmp/fonte.tgz" -C "$tmp/fonte" --strip-components 1 2>/dev/null || erro "o download da v$nova veio estragado"
      sem_travar 300 gh release download "v$nova" -R "$repo" -p KeepFree.zip -D "$tmp/fonte" 2>/dev/null || erro "não baixei o app da v$nova"   # rede: atualizar
      print -r -- "$tmp/fonte" ;;
  esac
}

cmd_atualizar() {
  local json=0 verificar=0 sim=0 a r nova o sha
  for a; do case $a in --json) json=1 ;; --verificar) verificar=1 ;; --sim) sim=1 ;; esac; done
  if ! r=$(release_nova); then
    (( json )) && { print -r -- "{\"atual\":$(js "$VERSAO"),\"nova\":null,\"erro\":$(js "Não consegui consultar a versão nova agora (sem internet?).")}"; return 0; }
    erro "não consegui consultar a versão nova (sem internet? de onde: $(origem_atualizacao))"
  fi
  nova=${r%%|*} o=${${r#*|}%|*} sha=${r##*|}
  if (( json )); then
    print -r -- "{\"atual\":$(js "$VERSAO"),\"nova\":$(js "$nova"),\"tem\":$(mais_nova "$nova" "$VERSAO" && print -n true || print -n false)}"
    (( verificar )) && return 0
  fi
  if ! mais_nova "$nova" "$VERSAO"; then
    (( json )) || print -r -- "${G}✓${N} Você já está na versão mais nova ($VERSAO)."
    return 0
  fi
  (( verificar )) && { print -r -- "Versão nova: $nova (você tem $VERSAO). Pra instalar: keepfree atualizar"; return 0; }
  (( sim )) || confirmar "Instalar a versão $nova? O app fecha e abre de novo." || { print -r -- "Cancelado."; return 0; }
  local tmp=$(mktemp -d) dir
  dir=$(baixar_versao "$nova" "$o" "$sha" "$tmp") || exit 1
  [[ -f $dir/install.sh && -f $dir/KeepFree.zip && -f $dir/app/KeepFree.zip.sha256 ]] || erro "o download da $nova veio incompleto: não instalei nada"
  # o app pronto, conferido pelo SHA-256 da própria versão (o instalador confere de novo, e a assinatura, se houver)
  local esperado=$(cut -d' ' -f1 < "$dir/app/KeepFree.zip.sha256") obtido=$(shasum -a 256 "$dir/KeepFree.zip" | cut -d' ' -f1)
  [[ -n $esperado && $esperado == $obtido ]] || erro "o app baixado não bate com o SHA-256 da $nova: não instalei nada"
  log "atualizando $VERSAO → $nova (de $o)"
  # o instalador da própria versão faz o resto (ferramenta, skill, vigilância, app)
  ( cd "$dir" && KEEPFREE_ZIP="$dir/KeepFree.zip" zsh ./install.sh --sim --meta $META_GB --vigia $(ativo && print sim || print nao) --auto-limpeza $AUTO_LIMPEZA --nao-abrir ) || erro "o instalador da $nova falhou"
  rm -rf "$tmp"
  open -a "KeepFree" 2>/dev/null
  print -r -- "${G}✓${N} Atualizado para $nova."
}

# ─── Docker ────────────────────────────────────────────────────────────────

sem_travar() { perl -e 'alarm shift; exec @ARGV' "$@"; }   # $1 = segundos; o Docker parado pode travar o comando
docker_ok() { command -v docker >/dev/null 2>&1 && sem_travar 8 docker info >/dev/null 2>&1; }

# tipo|kb|recuperavel_kb (Images, Containers, Local Volumes, Build Cache)
docker_df() {
  docker_ok || return 1
  sem_travar 20 docker system df --format '{{.Type}}|{{.Size}}|{{.Reclaimable}}' 2>/dev/null | awk -F'|' '
    function kb(s,   n, u) { n = s; sub(/[A-Za-z].*$/, "", n); u = s; sub(/^[0-9.]+/, "", u); sub(/ .*/, "", u)
      return int(n * (u == "TB" ? 976562500 : u == "GB" ? 976562.5 : u == "MB" ? 976.5625 : (u == "kB" || u == "KB") ? 0.9765625 : 1 / 1024)) }
    { print $1 "|" kb($2) "|" kb($3) }'
}

DOCKER_HIST="$DIR_CONF/docker.csv"   # data|kb do Docker.raw
vigiar_docker() {
  local k=$(docker_kb) ref agora=$(date +%s)
  (( k > 0 )) || return 0
  mkdir -p "$DIR_CONF"
  print -r -- "$agora|$k" >> "$DOCKER_HIST"
  ref=$(awk -F'|' -v t=$(( agora - 7 * 86400 )) '$1 >= t {print $2; exit}' "$DOCKER_HIST")
  if [[ -n $ref ]] && (( k - ref > DOCKER_ALERTA_GB * 976562 )) && pode_avisar docker 86400; then
    log "disco do Docker cresceu $(humano $(( k - ref ))) em 7 dias"
    avisar "O disco do Docker cresceu $(humano $(( k - ref )))" "Em 7 dias; agora tem $(humano $k). Dá pra limpar imagens sem uso e o cache de build pelo KeepFree." atencao
  fi
}

# keepfree docker [--limpar [--tudo] [--sim]]
#   --limpar: cache de build e imagens sem nome (dangling). --tudo: também imagens que
#   nenhum container usa (baixam de novo se precisar). Volumes e containers NUNCA.
cmd_docker() {
  local limpar=0 tudo=0 sim=0 a tipo k r
  for a; do case $a in --limpar) limpar=1 ;; --tudo) tudo=1 ;; --sim) sim=1 ;; esac; done
  if ! docker_ok; then
    print -r -- "O Docker não está rodando (ou não está instalado). Disco do Docker: $(humano $(docker_kb))."
    return 0
  fi
  if (( ! limpar )); then
    print -r -- "${B}Docker${N} ${D}(o arquivo do disco: $(humano $(docker_kb)))${N}"
    while IFS='|' read -r tipo k r; do
      printf "  %-14s %9s  %s dá pra recuperar\n" "$tipo" "$(humano $k)" "$(humano $r)"
    done < <(docker_df)
    print -r -- "  ${D}Limpar cache de build e imagens sem nome: keepfree docker --limpar (volumes e containers ficam)${N}"
    return 0
  fi
  local o_que="o cache de build e as imagens sem nome"
  (( tudo )) && o_que="o cache de build e TODAS as imagens que nenhum container usa (baixam de novo se precisar)"
  if (( ! sim )) && ! confirmar "Apagar $o_que? Volumes e containers não são tocados."; then
    print -r -- "Cancelado."; return 0
  fi
  local antes=$(docker_df | awk -F'|' '{s += $2} END {print s + 0}')
  sem_travar 600 docker builder prune -f >/dev/null 2>&1
  if (( tudo )); then sem_travar 600 docker image prune -a -f >/dev/null 2>&1
  else sem_travar 600 docker image prune -f >/dev/null 2>&1; fi
  local depois=$(docker_df | awk -F'|' '{s += $2} END {print s + 0}')
  local foi=$(( antes > depois ? antes - depois : 0 ))
  log "docker: limpou $o_que ($(humano $foi))"
  registrar_limpeza $foi "Docker: $o_que" apagado
  print -r -- "${G}✓${N} Docker: $(humano $foi) a menos. ${D}O arquivo Docker.raw devolve o espaço ao macOS aos poucos.${N}"
}

# ─── duplicados e backups de iPhone ───────────────────────────────────────

# kb|sha|caminho — arquivos de 100 MB ou mais com o mesmo conteúdo (tamanho e SHA-256),
# fora de ~/Library e de pastas ocultas. Lê os arquivos: só roda quando a pessoa pede.
listar_duplicados() {
  local f t s
  mdfind -onlyin "$HOME" 'kMDItemFSSize >= 100000000' 2>/dev/null | while IFS= read -r f; do
    [[ $f == $HOME/Library/* || ${f#$HOME} == */.* || $f == *.app/* ]] && continue
    [[ -f $f && ! -L $f ]] && caminho_ok "$f" || continue
    ignorado "$f" && continue
    print -r -- "$(stat -f %z "$f")|$f"
  done | sort -t'|' -k1,1n | awk -F'|' '{ n[$1]++; l[$1] = l[$1] "\n" $0 } END { for (t in n) if (n[t] > 1) printf "%s", l[t] }' |
  while IFS='|' read -r t f; do
    [[ -n $f ]] || continue
    s=$(shasum -a 256 "$f" 2>/dev/null | cut -c1-64) || continue
    print -r -- "$(( t / 1024 ))|$s|$f"
  done | sort -t'|' -k2,2 | awk -F'|' '{ n[$2]++; l[$2] = l[$2] $0 "\n" } END { for (h in n) if (n[h] > 1) printf "%s", l[h] }' |
  sort -t'|' -k1,1nr -k2,2
}

duplicados() {
  local k s f ant= achou=0 sobra=0
  while IFS='|' read -r k s f; do
    achou=1
    if [[ $s != $ant ]]; then print -r -- ""; print -r -- "  ${B}$(humano $k)${N} cada ${D}(${s[1,12]}…)${N}"; ant=$s
    else (( sobra += k )); fi
    print -r -- "    $(curto "$f")"
  done < <(listar_duplicados)
  (( achou )) || { print -r -- "  ${G}Nenhum arquivo de 100 MB+ repetido fora das pastas do sistema.${N}"; return 0; }
  print -r -- ""
  print -r -- "  Apagando as cópias repetidas (fica uma de cada), você libera ${B}$(humano $sobra)${N}. São seus: escolha pelo Finder."
}

BACKUPS="$HOME/Library/Application Support/MobileSync/Backup"
# kb|data|aparelho|pasta — ou "SEM-ACESSO": o macOS só deixa ler com Acesso Total ao Disco
listar_backups() {
  [[ -d $BACKUPS ]] || return 0
  ls "$BACKUPS" >/dev/null 2>&1 || { print -r -- "SEM-ACESSO"; return 0; }
  local d nome data
  for d in "$BACKUPS"/*(N/); do
    nome=$(plutil -extract "Device Name" raw "$d/Info.plist" 2>/dev/null)
    data=$(stat -f %m "$d/Info.plist" 2>/dev/null || stat -f %m "$d")
    print -r -- "$(kb "$d")|$data|${nome:-iPhone ou iPad}|$d"
  done | sort -t'|' -k2,2nr
}

backups() {
  local k t nome d achou=0
  while IFS='|' read -r k t nome d; do
    if [[ $k == SEM-ACESSO ]]; then
      print -r -- "  ${Y}Sem acesso:${N} pra ver os backups, dê ao Terminal (ou ao KeepFree) o Acesso Total ao Disco em Ajustes → Privacidade e Segurança."
      return 0
    fi
    achou=1
    printf "  %s%-9s%s %s ${D}(último backup: %s)${N}\n" "$B" "$(humano $k)" "$N" "$nome" "$(date -r $t '+%d/%m/%Y')"
  done < <(listar_backups)
  (( achou )) || print -r -- "  ${G}Nenhum backup de iPhone ou iPad neste Mac.${N}"
  (( achou )) && print -r -- "  ${D}↳ Apague os antigos pelo Finder: selecione o aparelho → Gerenciar Backups.${N}"
}

# ─── resumo da semana ─────────────────────────────────────────────────────

texto_semana() {
  local lim=$(( $(date +%s) - 7 * 86400 )) lib=0 quar=0 ref agora=$(livre_kb) l c
  if [[ -f $LIMPEZAS ]]; then
    lib=$(awk -F'|' -v t=$lim '$1 >= t && $4 == "apagado" {s += $2} END {print s + 0}' "$LIMPEZAS")
    quar=$(awk -F'|' -v t=$lim '$1 >= t && $4 == "quarentena" {s += $2} END {print s + 0}' "$LIMPEZAS")
  fi
  [[ -f $HIST ]] && ref=$(awk -F, -v t=$lim '$1 >= t {print $2; exit}' "$HIST")
  local partes=()
  (( lib > 0 )) && partes+=("liberou $(humano $lib)")
  (( quar > 0 )) && partes+=("$(humano $quar) foram pra quarentena")
  l=$(crescimento 604800 1)
  [[ -n $l ]] && { c=${${l#*|*|}%%|*}; partes+=("quem mais cresceu foi $(curto "$c") (+$(humano ${l%%|*}))"); }
  [[ -n $ref ]] && partes+=("espaço livre: $(humano $ref) → $(humano $agora)")
  (( ${#partes} )) || partes=("nada mudou muito no disco")
  local t="${(j:; :)partes}."
  print -r -- "${(U)t[1]}${t[2,-1]}"
}

checar_semana() {
  [[ $RESUMO_SEMANA == sim && -f $HIST ]] || return 0
  # só com uma semana de histórico
  (( $(date +%s) - $(head -n 1 "$HIST" | cut -d, -f1) >= 6 * 86400 )) || return 0
  pode_avisar semana 604800 || return 0
  local t=$(texto_semana)
  avisar "Sua semana no KeepFree" "$t" sucesso
  encaminhar "Sua semana no KeepFree" "$t"
}

# ─── o que cresceu ─────────────────────────────────────────────────────────
# A vigilância guarda, a cada rodada, as maiores pastas (data|kb|caminho). Comparando
# a foto de agora com a de 24 h (ou 7 dias) atrás dá pra dizer QUEM encheu o disco.
PASTAS="$DIR_CONF/pastas.csv"

registrar_pastas() {
  local agora=$(date +%s)
  mkdir -p "$DIR_CONF"
  listar_maiores 40 102400 | awk -v t=$agora '{print t "|" $0}' >> "$PASTAS"
  if (( $(wc -l < "$PASTAS") > 24000 )); then
    tail -n 16000 "$PASTAS" > "$PASTAS.tmp" && mv "$PASTAS.tmp" "$PASTAS"
  fi
}

# cresceu_kb|kb_agora|caminho|desde — o que mais cresceu nos últimos $1 segundos.
# Sem foto tão antiga, compara com a mais antiga que existe (o "desde" diz qual).
crescimento() {
  [[ -f $PASTAS ]] || return 0
  awk -F'|' -v alvo=$(( $(date +%s) - $1 )) '
    { if (!($1 in visto)) { visto[$1] = 1; ts[++k] = $1 }; kb[$1, $3] = $2 }
    END {
      if (k < 2) exit
      ult = ts[k]; ref = ts[1]
      for (i = k - 1; i >= 1; i--) if (ts[i] <= alvo) { ref = ts[i]; break }
      for (c in kb) {
        split(c, p, SUBSEP); if (p[1] != ult) continue
        a = ((ref, p[2]) in kb) ? kb[ref, p[2]] : 0
        d = kb[c] - a
        if (d > 102400) print d "|" kb[c] "|" p[2] "|" ref
      }
    }' "$PASTAS" | sort -t'|' -k1,1nr | head -n ${2:-5}
}

# " Quem mais cresceu: ~/pasta (+X)." — pro texto dos avisos
quem_cresceu() {
  local l c
  l=$(crescimento $1 1)
  [[ -n $l ]] || return 0
  c=${${l#*|*|}%%|*}
  print -rn -- " Quem mais cresceu: $(curto "$c") (+$(humano ${l%%|*}))."
}

cresceu() {
  local janela seg rotulo achou=0 d k c desde
  for janela in 86400:24h 604800:7\ dias; do
    seg=${janela%%:*} rotulo=${janela#*:}
    while IFS='|' read -r d k c desde; do
      (( achou++ )) || true
      [[ -n $rotulo ]] && { print -r -- "  ${D}Últimas $rotulo (desde $(date -r $desde '+%d/%m %H:%M')):${N}"; rotulo=; }
      printf "  %s+%-8s%s %s ${D}(agora %s)${N}\n" "$Y" "$(humano $d)" "$N" "$(curto "$c")" "$(humano $k)"
    done < <(crescimento $seg 5)
  done
  (( achou )) || print -r -- "  ${D}Ainda sem histórico: a vigilância guarda uma foto das pastas a cada rodada.${N}"
}

# ─── relatórios no terminal ───────────────────────────────────────────────

maiores() {
  local t p d marca
  listar_maiores ${1:-15} | while IFS='|' read -r t p; do
    marca=""
    oculta "$p" && marca=" ${Y}(oculta no Finder)${N}"
    printf "  %s%-9s%s %s%s\n" "$B" "$(humano $t)" "$N" "$(curto "$p")" "$marca"
    d=$(dica "$p")
    [[ -n $d ]] && print -r -- "            ${D}↳ $d${N}"
  done
}

tarefas() {
  local achou=0 tipo label info prog
  while IFS='|' read -r tipo label info prog; do
    achou=1
    if [[ $tipo == reabre ]]; then
      print -r -- "  ${Y}⚠${N} ${B}$label${N} abre ${B}$info${N} sozinho e ${Y}reabre se você fechar${N}"
      print -r -- "            ${D}↳ Se não reconhece: keepfree pausar-tarefa $label${N}"
    elif [[ $tipo == interrompida ]]; then
      print -r -- "  ${D}•${N} ${B}$label${N} foi encerrada de fora ($info)"
      print -r -- "            ${D}↳ Pode ter sido o sistema, um tempo-limite ou a própria tarefa. Só pause se não reconhecer.${N}"
    else
      [[ $info == SIG* ]] && print -r -- "  ${R}✗${N} ${B}$label${N} travou ($info)" \
                          || print -r -- "  ${R}✗${N} ${B}$label${N} está terminando com erro (código $info)"
      print -r -- "            ${D}↳ $(curto "$prog")${N}"
      print -r -- "            ${D}↳ Se não usa mais: keepfree pausar-tarefa $label${N}"
    fi
  done < <(listar_tarefas)
  (( achou )) || print -r -- "  ${G}Nenhuma tarefa com problema.${N}"
}

controle_remoto() {
  local achou=0 exe porta perfil
  while IFS='|' read -r exe porta perfil; do
    achou=1
    print -r -- "  ${Y}⚠${N} ${B}$exe${N} está com controle remoto ligado (porta $porta)"
    [[ -n $perfil ]] && print -r -- "            ${D}↳ perfil: $(curto "$perfil")${N}"
    print -r -- "            ${D}↳ Qualquer programa do Mac pode comandar esse navegador. Se não é uma automação sua, feche.${N}"
  done < <(listar_remoto)
  (( achou )) || print -r -- "  ${G}Nenhum navegador sendo controlado.${N}"
}

surtos_crash() {
  local achou=0 n t app c
  while IFS='|' read -r n t app c; do
    achou=1
    print -r -- "  ${R}✗${N} ${B}$app${N} gerou ${B}$n${N} relatórios de crash ($(humano $t))"
    print -r -- "            ${D}↳ O app está travando em loop. Limpe e reinicie/atualize o app.${N}"
  done < <(listar_crashes)
  (( achou )) || print -r -- "  ${G}Nenhum app crashando em massa.${N}"
}

# variação do espaço livre em 7 dias (KB); vazio se não há histórico
tendencia_kb() {
  [[ -f $HIST ]] || return 0
  local ref
  ref=$(awk -F, -v t=$(( $(date +%s) - 7*86400 )) '$1>=t {print $2; exit}' "$HIST")
  [[ -n $ref ]] && print $(( $(livre_kb) - ref ))
}

tendencia() {
  local dif=$(tendencia_kb)
  [[ -z $dif ]] && return 0
  if (( dif < -1048576 )); then
    print -r -- "  ${Y}Últimos 7 dias: você perdeu $(humano $dif) de espaço livre${N}"
  elif (( dif > 1048576 )); then
    print -r -- "  ${G}Últimos 7 dias: você ganhou $(humano $dif) de espaço livre${N}"
  fi
}

cmd_analisar() {
  registrar
  cmd_status
  tendencia

  titulo "1. Lixo seguro (é recriado sozinho)"
  cmd_limpar

  titulo "2. Apps crashando em massa"
  surtos_crash

  titulo "3. Maiores pastas pra revisar ${D}(medindo… pode levar um minuto)${N}"
  maiores 15

  titulo "3b. O que mais cresceu"
  cresceu

  titulo "3c. Arquivos grandes esquecidos ${D}(1 GB+, sem uso há 90 dias)${N}"
  esquecidos

  titulo "4. Tarefas em segundo plano"
  tarefas

  titulo "5. Navegadores com controle remoto"
  controle_remoto

  local lixeira snaps purg dk sims quar
  lixeira=$(kb "$HOME/.Trash") snaps=$(snapshots_n) purg=$(purgavel_kb) dk=$(docker_kb) sims=$(simuladores_n) quar=$(kb "$QUAR")
  titulo "6. Outros ${D}(o que costuma virar \"Dados do Sistema\")${N}"
  (( purg > 102400 )) && print -r -- "  ${B}$(humano $purg)${N} que o macOS libera sozinho quando precisa ${D}(espaço purgável)${N}"
  (( snaps > 0 )) && print -r -- "  $snaps snapshot(s) local(is) do Time Machine ${D}→ keepfree snapshots --liberar${N}"
  (( dk > 1048576 )) && print -r -- "  Disco do Docker: ${B}$(humano $dk)${N} ${D}→ Docker Desktop: Settings → Resources (limite) ou docker system prune${N}"
  (( sims > 0 )) && print -r -- "  $sims simulador(es) do Xcode sem uso ${D}→ xcrun simctl delete unavailable${N}"
  (( quar > 0 )) && print -r -- "  Quarentena do keepfree: ${B}$(humano $quar)${N} ${D}→ keepfree quarentena${N}"
  (( lixeira > 102400 )) && print -r -- "  Lixeira com ${B}$(humano $lixeira)${N} ${D}→ esvazie pelo Finder${N}"
  (( purg <= 102400 && snaps == 0 && dk <= 1048576 && sims == 0 && quar == 0 && lixeira <= 102400 )) && print -r -- "  ${G}Nada escondido aqui.${N}"
  print -r -- ""
}

registrar() {
  mkdir -p "$DIR_CONF"
  print -r -- "$(date +%s),$(livre_kb)" >> "$HIST"
  # guarda só os últimos ~60 dias (a cada 3h ≈ 500 linhas)
  if (( $(wc -l < "$HIST") > 1000 )); then
    tail -n 500 "$HIST" > "$HIST.tmp" && mv "$HIST.tmp" "$HIST"
  fi
}

cmd_vigiar() {
  registrar
  quarentena_vencida
  registrar_pastas
  vigiar_tarefas
  vigiar_remoto
  aplicar_regras
  vigiar_docker
  checar_memoria
  checar_reinicio
  checar_semana
  local livre=$(livre_kb) meta=$(( $(gb_kb $META_GB) )) c n tam s

  # 1) surto de crash — o problema que enche o disco em poucos dias
  for c in ${(f)"$(crashpads)"}; do
    ignorado "$c" && continue
    n=$(find "$c" -type f 2>/dev/null | wc -l | tr -d ' ')
    (( n > LIMITE_CRASH )) || continue
    tam=$(kb "$c")
    if [[ $LIMPAR_CRASH == sim ]]; then
      for s in pending completed new; do [[ -d $c/$s ]] && find "$c/$s" -mindepth 1 -delete 2>/dev/null; done
      log "surto de crash: ${c:h:t} — apagou $n relatórios ($(humano $tam))"
      avisar "${c:h:t} está crashando sem parar" "Apaguei $n relatórios de crash ($(humano $tam)). Vale reiniciar ou atualizar o app." atencao
    else
      log "surto de crash: ${c:h:t} — $n relatórios ($(humano $tam))"
      avisar "${c:h:t} está crashando sem parar" "$n relatórios de crash ($(humano $tam)). Rode: keepfree limpar" atencao
    fi
  done
  livre=$(livre_kb)

  # 2) queda rápida — algo novo está enchendo o disco
  local maximo queda
  maximo=$(awk -F, -v t=$(( $(date +%s) - 86400 )) '$1>=t && $2>m {m=$2} END{print m+0}' "$HIST")
  queda=$(( maximo - livre ))
  if (( queda > $(gb_kb $ALERTA_QUEDA_GB) )) && pode_avisar queda 43200; then
    log "queda rápida: -$(humano $queda) em 24h"
    avisar "Seu disco está enchendo rápido" "Você perdeu $(humano $queda) nas últimas 24h.$(quem_cresceu 86400)" atencao
  fi

  # 3) meta de espaço livre
  if (( livre < meta )); then
    if [[ $AUTO_LIMPEZA == sim ]]; then
      cmd_limpar --executar --sim --silencioso
      livre=$(livre_kb)
      (( LIBERADO_KB > 0 )) && avisar "Limpeza automática" "Liberei $(humano $LIBERADO_KB). Agora: $(humano $livre) livres." sucesso
    fi
    if (( livre < meta )); then
      # toda medição abaixo da meta vai pro histórico, avisando ou não
      log "abaixo da meta: $(humano $livre) livres (meta $META_GB GB)"
      local ult=$(estado_ler meta_kb)
      if (( livre < $(gb_kb $CRITICO_GB) )); then
        # crítico: avisa toda vez, sem esperar as 6 h
        avisar "Disco quase cheio: $(humano $livre) livres" "Abaixo de $CRITICO_GB GB o Mac fica lento e apps podem falhar.$(quem_cresceu 86400)" critico
        estado_gravar meta $(date +%s); estado_gravar meta_kb $livre
      elif pode_avisar meta 21600 || (( ${ult:-0} > 0 && ult - livre > $(gb_kb 2) )); then
        # de 6 em 6 h, ou antes disso se piorou mais 2 GB desde o último aviso
        avisar "Pouco espaço: $(humano $livre) livres" "Sua meta é $META_GB GB.$(quem_cresceu 86400)" atencao
        estado_gravar meta $(date +%s); estado_gravar meta_kb $livre
      fi
    fi
  else
    estado_gravar meta_kb 0
  fi
}

salvar_config() {
  mkdir -p "$DIR_CONF"
  cat > "$CONF" <<EOF
# keepfree — configuração (edite à vontade ou use: keepfree config)
META_GB=$META_GB
AUTO_LIMPEZA=$AUTO_LIMPEZA
LIMPAR_CRASH=$LIMPAR_CRASH
INTERVALO_HORAS=$INTERVALO_HORAS
LOG_DIAS=$LOG_DIAS
ALERTA_QUEDA_GB=$ALERTA_QUEDA_GB
LIMITE_CRASH=$LIMITE_CRASH
CRITICO_GB=$CRITICO_GB
QUARENTENA_DIAS=$QUARENTENA_DIAS
PROJETOS_DIAS=$PROJETOS_DIAS
MEMORIA_MIN=$MEMORIA_MIN
SWAP_AVISO_GB=$SWAP_AVISO_GB
DEV_HORAS=$DEV_HORAS
DOCKER_ALERTA_GB=$DOCKER_ALERTA_GB
CPU_MINUTOS=$CPU_MINUTOS
RESUMO_SEMANA=$RESUMO_SEMANA
APPS_DIAS=$APPS_DIAS
PESADO_MEM_MIN=$PESADO_MEM_MIN
PESADO_SWAP_GB=$PESADO_SWAP_GB
ENCAMINHAR_CRITICO=$ENCAMINHAR_CRITICO
EOF
}

cmd_config() {
  if (( $# == 0 )); then
    print -r -- "${B}Configuração${N} ${D}($(curto "$CONF"))${N}"
    print -r -- "  meta           $META_GB GB      espaço livre mínimo"
    print -r -- "  auto-limpeza   $AUTO_LIMPEZA        limpar lixo seguro sozinho abaixo da meta"
    print -r -- "  limpar-crash   $LIMPAR_CRASH        apagar sozinho surtos de crash"
    print -r -- "  intervalo      ${INTERVALO_HORAS}h        de quanto em quanto tempo vigiar"
    print -r -- "  logs-dias      $LOG_DIAS          idade pra log virar lixo"
    print -r -- "  alerta-queda   $ALERTA_QUEDA_GB GB      avisar se perder isso em 24h"
    print -r -- "  critico        $CRITICO_GB GB       abaixo disso avisa toda vez, sem esperar"
    print -r -- "  quarentena-dias $QUARENTENA_DIAS        quanto tempo a quarentena guarda antes de apagar"
    print -r -- "  projetos-dias  $PROJETOS_DIAS         projeto sem mudança há isso conta como parado"
    print -r -- "  memoria-min    $MEMORIA_MIN%         memória livre abaixo disso avisa"
    print -r -- "  swap-aviso     $SWAP_AVISO_GB GB       swap a partir disso avisa e sugere reiniciar"
    print -r -- "  dev-horas      ${DEV_HORAS}h         programa de programação parado há isso é esquecido"
    print -r -- "  docker-alerta  $DOCKER_ALERTA_GB GB       avisa se o Docker crescer isso em 7 dias"
    print -r -- "  cpu-minutos    $CPU_MINUTOS min       app com o processador no máximo por isso avisa"
    print -r -- "  resumo-semana  $RESUMO_SEMANA        um aviso por semana"
    print -r -- "  apps-dias      $APPS_DIAS         app sem uso há isso entra na análise"
    print -r -- "  pesado-memoria $PESADO_MEM_MIN%         memória livre pra tarefa pesada"
    print -r -- "  pesado-swap    $PESADO_SWAP_GB GB       swap máxima pra tarefa pesada"
    print -r -- "  encaminhar-urgente $ENCAMINHAR_CRITICO    urgentes também vão pro programa de encaminhar"
    print -r -- "  ignorados      ${#IGNORADOS}          pastas fora da análise (keepfree ignorar)"
    print -r -- ""
    print -r -- "  ${D}Ex.: keepfree config meta 30${N}"
    return 0
  fi
  local chave=$1 valor=$2
  case $chave in
    meta)         [[ $valor == <1-> ]] || erro "use: keepfree config meta 30"; META_GB=$valor ;;
    auto-limpeza) [[ $valor == (sim|nao) ]] || erro "use: sim ou nao"; AUTO_LIMPEZA=$valor ;;
    limpar-crash) [[ $valor == (sim|nao) ]] || erro "use: sim ou nao"; LIMPAR_CRASH=$valor ;;
    intervalo)    [[ $valor == <1-24> ]] || erro "use um número de horas (1 a 24)"; INTERVALO_HORAS=$valor ;;
    logs-dias)    [[ $valor == <1-> ]] || erro "use um número de dias"; LOG_DIAS=$valor ;;
    alerta-queda) [[ $valor == <1-> ]] || erro "use um número em GB"; ALERTA_QUEDA_GB=$valor ;;
    critico)      [[ $valor == <1-> ]] || erro "use um número em GB"; CRITICO_GB=$valor ;;
    quarentena-dias) [[ $valor == <1-90> ]] || erro "use um número de dias (1 a 90)"; QUARENTENA_DIAS=$valor ;;
    projetos-dias) [[ $valor == <1-> ]] || erro "use um número de dias"; PROJETOS_DIAS=$valor ;;
    memoria-min)  [[ $valor == <1-50> ]] || erro "use uma porcentagem (1 a 50)"; MEMORIA_MIN=$valor ;;
    swap-aviso)   [[ $valor == <1-> ]] || erro "use um número em GB"; SWAP_AVISO_GB=$valor ;;
    dev-horas)    [[ $valor == <1-> ]] || erro "use um número de horas"; DEV_HORAS=$valor ;;
    docker-alerta) [[ $valor == <1-> ]] || erro "use um número em GB"; DOCKER_ALERTA_GB=$valor ;;
    cpu-minutos)  [[ $valor == <1-> ]] || erro "use um número de minutos"; CPU_MINUTOS=$valor ;;
    resumo-semana) [[ $valor == (sim|nao) ]] || erro "use: sim ou nao"; RESUMO_SEMANA=$valor ;;
    apps-dias)    [[ $valor == <1-> ]] || erro "use um número de dias"; APPS_DIAS=$valor ;;
    pesado-memoria) [[ $valor == <1-90> ]] || erro "use uma porcentagem (1 a 90)"; PESADO_MEM_MIN=$valor ;;
    pesado-swap)  [[ $valor == <1-> ]] || erro "use um número em GB"; PESADO_SWAP_GB=$valor ;;
    encaminhar-urgente) [[ $valor == (sim|nao) ]] || erro "use: sim ou nao"; ENCAMINHAR_CRITICO=$valor ;;
    *) erro "opção desconhecida: $chave (veja: keepfree config)" ;;
  esac
  salvar_config
  print -r -- "${G}✓${N} $chave = $valor"
  [[ $chave == intervalo ]] && ativo && cmd_ativar
  return 0
}

cmd_ativar() {
  [[ -f $CONF ]] || salvar_config
  mkdir -p "${PLIST:h}"
  cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$LABEL</string>
  <key>ProgramArguments</key><array><string>$SCRIPT</string><string>vigiar</string></array>
  <key>StartInterval</key><integer>$(( INTERVALO_HORAS * 3600 ))</integer>
  <key>RunAtLoad</key><true/>
  <key>ProcessType</key><string>Background</string>
  <key>LowPriorityIO</key><true/>
  <key>Nice</key><integer>10</integer>
</dict></plist>
EOF
  launchctl bootout "gui/$UID/$LABEL" 2>/dev/null
  launchctl enable "gui/$UID/$LABEL" 2>/dev/null
  launchctl bootstrap "gui/$UID" "$PLIST" || erro "não consegui ativar a vigilância"
  print -r -- "${G}✓${N} Vigilância ligada: confere o disco a cada ${INTERVALO_HORAS}h e te avisa abaixo de $META_GB GB livres."
}

cmd_desativar() {
  launchctl bootout "gui/$UID/$LABEL" 2>/dev/null
  rm -f "$PLIST"
  print -r -- "${G}✓${N} Vigilância desligada."
}

plist_da_tarefa() {
  local f
  for f in "$HOME/Library/LaunchAgents"/*.plist(N); do
    [[ $(/usr/libexec/PlistBuddy -c 'Print :Label' "$f" 2>/dev/null) == $1 ]] && { print -r -- "$f"; return 0; }
  done
  return 1
}

cmd_pausar_tarefa() {
  [[ -n $1 ]] || erro "use: keepfree pausar-tarefa <nome-da-tarefa>"
  plist_da_tarefa "$1" >/dev/null || erro "não achei a tarefa $1 em ~/Library/LaunchAgents"
  launchctl bootout "gui/$UID/$1" 2>/dev/null
  launchctl disable "gui/$UID/$1"
  log "pausou tarefa: $1"
  print -r -- "${G}✓${N} $1 parada e não volta ao reiniciar. Pra religar: keepfree religar-tarefa $1"
}

cmd_religar_tarefa() {
  [[ -n $1 ]] || erro "use: keepfree religar-tarefa <nome-da-tarefa>"
  local f
  f=$(plist_da_tarefa "$1") || erro "não achei a tarefa $1 em ~/Library/LaunchAgents"
  launchctl enable "gui/$UID/$1"
  launchctl bootstrap "gui/$UID" "$f" 2>/dev/null
  log "religou tarefa: $1"
  print -r -- "${G}✓${N} $1 religada."
}

# Tudo o que existe na pasta, uma linha por item: tipo|tamanho|destino do atalho|caminho.
# Pastas entram sem tamanho (ele muda de um disco pro outro). Ignora o que o Finder
# cria sozinho (.DS_Store e ._*). Duas pastas iguais têm o mesmo manifesto.
manifesto() {
  (
    cd "$1" || exit 1
    find . -mindepth 1 -type d ! -name '._*' 2>/dev/null | sed 's/^/Directory|-|-|/'
    find . ! -type d ! -name .DS_Store ! -name '._*' -print0 2>/dev/null |
      xargs -0 stat -f '%HT|%z|%Y|%N' 2>/dev/null
  ) | LC_ALL=C sort
}

cmd_mover() {
  (( $# == 2 )) || erro "use: keepfree mover <pasta> <pasta-de-destino-no-HD-externo>"
  local entrada=${1%/}
  [[ -L $entrada ]] && erro "$entrada já é um atalho — já foi movida"
  [[ -d $entrada ]] || erro "pasta não encontrada: $entrada"
  [[ -d $2 ]] || erro "destino não existe: $2 (o HD está conectado?)"
  local orig=${entrada:A} dest=${2:A}
  [[ $orig == $HOME/* ]] || erro "por segurança, só movo pastas dentro de $(curto $HOME)"
  [[ $dest == $orig || $dest == $orig/* ]] && erro "o destino não pode ficar dentro da própria pasta"

  [[ -n $(find "$orig" -mindepth 1 -print -quit 2>/dev/null) ]] || erro "a pasta está vazia ou não consegui ler: $(curto "$orig")"

  local nome=${orig:t}
  local final="$dest/${nome#.}"
  [[ -e $final ]] && erro "já existe $final"

  local tam=$(kb "$orig") livre_dest
  livre_dest=$(df -k "$dest" | awk 'NR==2{print $4}')
  (( livre_dest > tam + 1048576 )) || erro "sem espaço no destino ($(humano $livre_dest) livres, precisa de $(humano $tam))"

  print -r -- "${B}Mover $(humano $tam)${N}"
  print -r -- "  de:   $(curto "$orig")"
  print -r -- "  para: $final"
  print -r -- "  No lugar fica um atalho — os apps continuam achando os arquivos."
  print -r -- "  ${Y}Feche os apps que usam essa pasta antes de continuar.${N}"
  print -r -- "  ${Y}Depois, deixe o HD conectado quando for usar esses arquivos.${N}"
  confirmar "Continuar?" || { print -r -- "Cancelado."; return 0; }

  print -r -- "Copiando…"
  # ditto copia atalhos (links simbólicos), etiquetas do Finder, permissões e datas.
  # Se o HD não aceita algum desses (ex.: exFAT não tem atalho), ele falha e nada é apagado.
  ditto "$orig" "$final" || erro "a cópia falhou — nada foi apagado do Mac. Cópia parcial em: $final"

  local m1 m2 n b
  m1=$(manifesto "$orig") m2=$(manifesto "$final")
  if [[ $m1 != $m2 ]]; then
    print -r -- "  ${R}Diferenças (original ↔ cópia):${N}" >&2
    diff <(print -r -- "$m1") <(print -r -- "$m2") | head -n 10 >&2
    erro "a cópia não bateu — nada foi apagado do Mac. Cópia parcial em: $final"
  fi
  n=$(print -r -- "$m2" | grep -vc '^Directory|')
  b=$(print -r -- "$m2" | awk -F'|' '$1 == "Regular File" {s += $2} END {printf "%d\n", s}')
  print -r -- "  ${G}✓${N} Cópia conferida item por item: $n arquivos e atalhos, $(humano $(( b / 1024 )))"

  rm -rf "$orig" && ln -s "$final" "$orig" || erro "copiei, mas não consegui trocar a pasta pelo atalho"
  log "moveu: $orig → $final ($(humano $tam))"
  print -r -- "  ${G}✓${N} Pronto. Livre agora: $(humano $(livre_kb))"
}

# keepfree testar-aviso [tom] — mostra um aviso de exemplo agora.
cmd_testar_aviso() {
  case ${1:-info} in
    critico)   avisar "Exemplo: disco quase cheio" "É só um teste. Assim aparece quando o espaço livre cai abaixo de $CRITICO_GB GB." critico ;;
    atencao)   avisar "Exemplo: pouco espaço" "É só um teste. Assim aparece quando o espaço livre fica abaixo da sua meta ($META_GB GB)." atencao ;;
    seguranca) avisar "Exemplo: tarefa nova em segundo plano" "É só um teste. Assim aparece quando algo se instala pra rodar sozinho no Mac." seguranca ;;
    sucesso)   avisar "Exemplo: limpeza feita" "É só um teste. Assim aparece quando uma regra ou a limpeza automática libera espaço." sucesso ;;
    *)         avisar "Tudo certo por aqui" "Assim aparecem os avisos do KeepFree. Espaço livre agora: $(humano $(livre_kb))." info ;;
  esac
  if pgrep -qx KeepFree; then print -r -- "${G}✓${N} Aviso enviado pelo app (canto da tela)."
  else print -r -- "${G}✓${N} Aviso enviado pelo Editor de Scripts (o app está fechado)."; fi
}

# ─── pastas ignoradas ─────────────────────────────────────────────────────

# keepfree ignorar                      lista
# keepfree ignorar <pasta ou id>        tira da análise, da limpeza e da vigilância
# keepfree ignorar --desfazer <pasta>   volta a considerar
cmd_ignorar() {
  local desfazer=0 alvo i
  [[ $1 == --desfazer ]] && { desfazer=1; shift; }
  if (( $# == 0 )); then
    (( desfazer )) && erro "use: keepfree ignorar --desfazer <pasta>"
    if (( ${#IGNORADOS} == 0 )); then
      print -r -- "Nenhuma pasta ignorada. Pra ignorar: keepfree ignorar <pasta>"
    else
      print -r -- "${B}Pastas ignoradas${N} ${D}(não entram na análise, na limpeza nem na vigilância)${N}"
      for i in $IGNORADOS; do print -r -- "  $(curto "$i")"; done
    fi
    return 0
  fi
  alvo=$1
  # id da análise geral (keepfree dados geral) → caminho
  if [[ $alvo =~ '^[0-9a-f]{8}$' && ! -e $alvo ]]; then
    i=$(linhas_geral | awk -F'|' -v id="$alvo" '$1 == id {print $8; exit}')
    [[ -n $i ]] || erro "não achei o item $alvo (rode: keepfree dados geral)"
    alvo=$i
  fi
  alvo=${${alvo/#\~/$HOME}:a}
  [[ $alvo == / || $alvo == $HOME ]] && erro "não dá pra ignorar $(curto "$alvo") inteira"
  [[ $alvo == *[$'\n'\|]* ]] && erro "caminho com quebra de linha ou | não é aceito"
  if (( desfazer )); then
    (( ${IGNORADOS[(Ie)$alvo]} )) || erro "$(curto "$alvo") não estava ignorada"
    IGNORADOS=(${IGNORADOS:#$alvo})
  else
    (( ${IGNORADOS[(Ie)$alvo]} )) && { print -r -- "$(curto "$alvo") já estava ignorada."; return 0; }
    IGNORADOS+=("$alvo")
  fi
  mkdir -p "$DIR_CONF"
  print -rl -- $IGNORADOS > "$IGNORADOS_ARQ.tmp" && mv "$IGNORADOS_ARQ.tmp" "$IGNORADOS_ARQ"
  rm -f "$DIR_CONF/geral.cache"   # a próxima análise já sai sem ela
  if (( desfazer )); then
    log "voltou a considerar: $alvo"
    print -r -- "${G}✓${N} $(curto "$alvo") volta a entrar na análise."
  else
    log "ignorou: $alvo"
    print -r -- "${G}✓${N} $(curto "$alvo") não entra mais na análise, na limpeza nem na vigilância."
    print -r -- "  ${D}Pra desfazer: keepfree ignorar --desfazer $(curto "$alvo")${N}"
  fi
}

# ─── quarentena: listar, liberar, desfazer ───────────────────────────────

# keepfree quarentena                      lista os lotes
# keepfree quarentena --liberar [lote]     apaga de vez (todos ou um lote)
cmd_quarentena() {
  local a l k n ts total=0 liberar=0 sim=0 alvo=
  for a; do
    case $a in
      --liberar) liberar=1 ;;
      --sim|-y)  sim=1 ;;
      *)         alvo=$a ;;
    esac
  done
  local -a ls
  ls=(${(f)"$(lotes)"})
  if [[ -n $alvo ]]; then
    lote_ok "$alvo" && [[ -d $QUAR/$alvo ]] || erro "não achei o lote $alvo (veja: keepfree quarentena)"
    ls=("$QUAR/$alvo")
  fi
  if (( ! liberar )); then
    (( ${#ls} )) || { print -r -- "A quarentena está vazia."; return 0; }
    print -r -- "${B}Quarentena${N} ${D}(cada lote sai sozinho depois de $QUARENTENA_DIAS dias)${N}"
    for l in $ls; do
      k=$(kb "$l"); n=$(lote_n "$l"); ts=$(lote_ts "$l")
      printf "  %-24s %9s  %4d itens  sai em %s\n" "${l:t}" "$(humano $k)" $n "$(date -r $(( ${ts:-0} + QUARENTENA_DIAS * 86400 )) '+%d/%m %H:%M')"
      (( total += k ))
    done
    print -r -- "  ${B}Total: $(humano $total)${N}"
    print -r -- "  ${D}Desfazer o último: keepfree desfazer · liberar já: keepfree quarentena --liberar${N}"
    return 0
  fi
  (( ${#ls} )) || { print -r -- "A quarentena já está vazia."; return 0; }
  for l in $ls; do (( total += $(kb "$l") )); done
  if (( ! sim )) && ! confirmar "Apagar de vez $(humano $total) que estão na quarentena?"; then
    print -r -- "Cancelado."
    return 0
  fi
  local antes=$(livre_kb)
  for l in $ls; do
    rm -rf -- "$l"
    log "liberou a quarentena: ${l:t}"
  done
  registrar_limpeza $total "Quarentena liberada" apagado
  print -r -- "${G}✓${N} Quarentena liberada: $(humano $total). Livre agora: $(humano $(livre_kb)) (antes $(humano $antes))"
}

# keepfree desfazer [lote] [--json]   devolve tudo do último lote (ou do lote dado)
cmd_desfazer() {
  local a l= json=0 n orig voltou=0 conflito=0
  for a; do
    case $a in
      --json) json=1 ;;
      *)      lote_ok "$a" || erro "lote inválido: $a (veja: keepfree quarentena)"; l="$QUAR/$a" ;;
    esac
  done
  [[ -z $l ]] && l=$(lotes | head -n 1)
  [[ -n $l && -d $l ]] || erro "nada pra desfazer: a quarentena está vazia"
  if [[ -f $l/manifesto ]]; then
    while IFS= read -r -d '' n && IFS= read -r -d '' orig; do
      [[ -e $l/itens/$n || -L $l/itens/$n ]] || continue
      if [[ -e $orig || -L $orig ]]; then (( ++conflito )); continue; fi
      mkdir -p "${orig:h}" && mv -- "$l/itens/$n" "$orig" && (( ++voltou ))
    done < "$l/manifesto"
  fi
  (( conflito )) || rm -rf -- "$l"
  log "desfez: ${l:t} ($voltou de volta, $conflito já existiam no lugar)"
  if (( json )); then
    print -r -- "{\"lote\":$(js "${l:t}"),\"voltou\":$voltou,\"conflitos\":$conflito}"
  else
    print -r -- "${G}✓${N} $voltou item(ns) de volta ao lugar."
    (( conflito )) && print -r -- "  ${Y}⚠${N} $conflito já existiam no lugar e ficaram na quarentena (lote ${l:t})."
  fi
  return 0
}

# ─── análise geral (primeira vez e sob demanda) ───────────────────────────

CACHE_GERAL="$DIR_CONF/geral.cache"

primeira_feita()  { grep -q '^primeira=' "$ESTADO" 2>/dev/null; }
marcar_primeira() { mkdir -p "$DIR_CONF"; primeira_feita || print -r -- "primeira=$(date +%s)" >> "$ESTADO"; }

# linhas da análise: usa a medição dos últimos 15 min se existir (medir tudo leva ~30 s)
linhas_geral() {
  if [[ -f $CACHE_GERAL && $1 != --nova ]] && (( $(date +%s) - $(stat -f %m "$CACHE_GERAL") < 900 )); then
    cat "$CACHE_GERAL"
  else
    mkdir -p "$DIR_CONF"
    listar_geral | tee "$CACHE_GERAL"
  fi
}

# Limpa as linhas recebidas (formato de listar_geral). Só apaga o que está no
# catálogo de agora (o cache em disco não decide o que é apagado) e confere de novo
# se o app não foi aberto nesse meio-tempo. Define LIBERADO_KB, QUARENTENA_KB,
# LIXEIRA_KB (o que estava em outro disco), LIMPOS e PULADOS.
limpar_linhas() {
  local l c id nivel t quem desc nota tipo cam cond achou cn ct cdesc cc cnota destino
  local -a catalogo
  catalogo=(${(f)"$(alvos)"})
  LIBERADO_KB=0 QUARENTENA_KB=0 LIXEIRA_KB=0 LIMPOS=0 PULADOS=()
  fotografar
  for l in "$@"; do
    IFS='|' read -r id nivel t quem desc nota tipo cam <<< "$l"
    achou=0
    for c in $catalogo; do
      IFS='|' read -r cn ct cdesc cc cond cnota <<< "$c"
      [[ $ct == "$tipo" && $cc == "$cam" ]] && { achou=1; break; }
    done
    if (( ! achou )); then
      log "recusou (fora do catálogo): $tipo $cam"
      continue
    fi
    [[ -e $cam ]] || continue
    quem=$(quem_usa "$tipo" "$cam" "$cond")
    if [[ -n $quem ]]; then PULADOS+=("$desc|$quem"); continue; fi
    FOI_LIXEIRA=0
    apagar_alvo "$tipo" "$cam"
    if ! vai_pra_quarentena "$tipo"; then
      (( LIBERADO_KB += t )); destino=apagado
    elif (( FOI_LIXEIRA )); then
      (( LIXEIRA_KB += t )); destino=lixeira
    else
      (( QUARENTENA_KB += t )); destino=quarentena
    fi
    (( LIMPOS++ ))
    registrar_limpeza $t "$desc" $destino
    log "análise geral: $desc ($(humano $t))${${destino:#apagado}:+ → $destino}"
  done
  rm -f "$CACHE_GERAL"
  marcar_primeira
}

comemorar() {   # $1 = livre antes (KB)
  local agora=$(livre_kb)
  print -r -- ""
  print -r -- "  ${G}════════════════════════════════════════════════════${N}"
  if (( LIBERADO_KB > 0 )); then
    print -r -- "   🎉  ${B}Você liberou $(humano $LIBERADO_KB) que estavam escondidos!${N}"
  else
    print -r -- "   ✅  ${B}Pronto!${N}"
  fi
  print -r -- "       Antes: $(humano $1) livres  →  Agora: ${B}$(humano $agora) livres${N}"
  if (( QUARENTENA_KB > 0 )); then
    print -r -- ""
    print -r -- "   📦  Mais ${B}$(humano $QUARENTENA_KB)${N} foram pra quarentena por $QUARENTENA_DIAS dias."
    print -r -- "       Mudou de ideia: ${C}keepfree desfazer${N} · Liberar já: ${C}keepfree quarentena --liberar${N}"
  fi
  if (( LIXEIRA_KB > 0 )); then
    print -r -- ""
    print -r -- "   🗑️   Mais ${B}$(humano $LIXEIRA_KB)${N} (de outro disco) foram pra Lixeira (dá pra recuperar)."
  fi
  local p q
  for p in $PULADOS; do
    print -r -- "   ${Y}⚠${N}  Pulei ${p%%|*}: o ${p##*|} estava aberto."
  done
  print -r -- "  ${G}════════════════════════════════════════════════════${N}"
  (( LIBERADO_KB > 0 )) && avisar "KeepFree" "Você liberou $(humano $LIBERADO_KB). Agora: $(humano $agora) livres." sucesso
}

cmd_geral() {
  if [[ $1 == --pular ]]; then marcar_primeira; print -r -- "${G}✓${N} Ok, não pergunto mais."; return 0; fi

  print -r -- ""
  print -r -- "${B}🔎 Análise geral do seu Mac${N}"
  print -r -- "   Procurando espaço escondido… ${D}(uns 30 segundos)${N}"
  local -a linhas marc
  linhas=(${(f)"$(linhas_geral --nova)"})
  if (( ${#linhas} == 0 )); then
    print -r -- "   ${G}Seu Mac já está limpo 👌${N}"
    marcar_primeira
    return 0
  fi

  local i id nivel t quem desc nota tipo cam
  for (( i = 1; i <= ${#linhas}; i++ )); do
    IFS='|' read -r id nivel t quem desc nota tipo cam <<< "$linhas[i]"
    [[ $nivel == seguro && -z $quem ]] && marc[i]=1 || marc[i]=0
  done

  _mostrar() {
    local grupo="" sel=0 tot=0 caixa
    print -r -- ""
    for (( i = 1; i <= ${#linhas}; i++ )); do
      IFS='|' read -r id nivel t quem desc nota tipo cam <<< "$linhas[i]"
      if [[ $nivel != $grupo ]]; then
        grupo=$nivel
        [[ $nivel == seguro ]] && print -r -- "  ${B}SEGURO${N} ${D}— se recria sozinho, não perde nada${N}" \
                               || { print -r -- ""; print -r -- "  ${B}REVISE${N} ${D}— é seu: só sai se você marcar${N}"; }
      fi
      (( tot += t ))
      if (( marc[i] )); then caixa="${G}[✓]${N}"; (( sel += t )); else caixa="[ ]"; fi
      printf "   %s %2d  %s%9s%s  %s\n" "$caixa" $i "$B" "$(humano $t)" "$N" "$desc"
      [[ -n $nota ]] && print -r -- "                    ${D}↳ $nota${N}"
      [[ -n $quem ]] && print -r -- "                    ${Y}⚠ feche o $quem pra limpar${N}"
    done
    print -r -- ""
    print -r -- "  Encontrei ${B}$(humano $tot)${N} · Marcado: ${G}${B}$(humano $sel)${N}"
  }

  if [[ ! -t 0 ]]; then
    _mostrar
    print -r -- ""
    print -r -- "  Pra limpar, rode no Terminal: keepfree geral"
    print -r -- "  (ou, por uma IA: keepfree dados geral  →  keepfree limpar-itens <ids>)"
    return 0
  fi

  local r n
  while true; do
    _mostrar
    print -r -- ""
    print -r -- "  ${D}Números pra marcar/desmarcar (ex: 3 7) · t = marcar todo o seguro · r = conferir de novo depois de fechar apps${N}"
    # sem resposta (entrada fechada, Ctrl+D) = sair sem limpar. Nunca limpa por padrão.
    if ! read -r "r?  ${B}Enter${N} pra limpar o que está marcado · ${B}q${N} sai sem limpar: "; then
      print -r -- ""; print -r -- "  Nada foi apagado."
      return 0
    fi
    case $r in
      "") break ;;
      q|Q) print -r -- "  Nada foi apagado."; marcar_primeira; return 0 ;;
      t|T) for (( i = 1; i <= ${#linhas}; i++ )); do
             IFS='|' read -r id nivel t quem desc nota tipo cam <<< "$linhas[i]"
             [[ $nivel == seguro && -z $quem ]] && marc[i]=1
           done ;;
      r|R) print -r -- "  Conferindo…"
           linhas=(${(f)"$(linhas_geral --nova)"}) ;;
      *) for n in ${=r}; do
           [[ $n == <-> ]] && (( n >= 1 && n <= ${#linhas} )) || continue
           IFS='|' read -r id nivel t quem desc nota tipo cam <<< "$linhas[n]"
           if [[ -n $quem ]]; then
             print -r -- "  ${Y}⚠ Feche o $quem primeiro e digite r pra eu conferir de novo.${N}"
             continue
           fi
           (( marc[n] = !marc[n] ))
         done ;;
    esac
  done

  local -a escolhidas
  for (( i = 1; i <= ${#linhas}; i++ )); do (( marc[i] )) && escolhidas+=("$linhas[i]"); done
  if (( ${#escolhidas} == 0 )); then
    print -r -- "  Nada marcado. Nada foi apagado."
    marcar_primeira
    return 0
  fi

  local soma=0 l2
  for l2 in $escolhidas; do (( soma += ${${l2#*|*|}%%|*} )); done
  print -r -- ""
  if ! confirmar "  Limpar $(humano $soma) (${#escolhidas} itens)?"; then
    print -r -- "  Nada foi apagado."
    return 0
  fi

  local antes=$(livre_kb)
  print -r -- "  Limpando…"
  limpar_linhas "${escolhidas[@]}"
  comemorar $antes
}

# Usado pelo app e pelas IAs: limpa só os ids escolhidos
cmd_limpar_itens() {
  local json=0 a
  local -A quer
  for a; do
    case $a in
      --json) json=1 ;;
      *) quer[$a]=1 ;;
    esac
  done
  (( ${#quer} )) || erro "use: keepfree limpar-itens <id> [<id>…] [--json]  (ids em: keepfree dados geral)"

  local -a escolhidas
  local l
  for l in ${(f)"$(linhas_geral)"}; do
    (( ${+quer[${l%%|*}]} )) && escolhidas+=("$l")
  done
  local antes=$(livre_kb)
  limpar_linhas "${escolhidas[@]}"

  if (( json )); then
    local p sep=
    print -rn -- "{\"liberado_kb\":$LIBERADO_KB,\"quarentena_kb\":$QUARENTENA_KB,\"lixeira_kb\":$LIXEIRA_KB,\"lote\":$(js "${LOTE:t}"),\"quarentena_dias\":$QUARENTENA_DIAS,\"itens\":$LIMPOS,\"livre_antes\":$antes,\"livre_depois\":$(livre_kb),\"pulados\":["
    for p in $PULADOS; do
      print -rn -- "$sep{\"desc\":$(js "${p%%|*}"),\"quem\":$(js "${p##*|}")}"; sep=,
    done
    print -r -- "]}"
  else
    comemorar $antes
  fi
}

cmd_esvaziar_lixeira() {
  if [[ $1 != --sim ]]; then
    confirmar "Esvaziar a Lixeira? Isso apaga de vez o que está lá." || { print -r -- "Cancelado."; return 0; }
  fi
  local antes=$(livre_kb)
  osascript -e 'tell application "Finder" to empty trash' >/dev/null 2>&1 || erro "não consegui esvaziar (o macOS pode pedir permissão pra controlar o Finder)"
  log "esvaziou a Lixeira"
  print -r -- "${G}✓${N} Lixeira esvaziada. Livre agora: $(humano $(livre_kb)) (antes $(humano $antes))"
}

# ─── dados pro app (JSON) ─────────────────────────────────────────────────

js() {
  local s=${1//\\/\\\\}
  s=${s//\"/\\\"}
  s=${s//$'\n'/\\n}; s=${s//$'\r'/\\r}; s=${s//$'\t'/\\t}
  s=${s//[[:cntrl:]]/ }
  print -rn -- "\"$s\""
}
bool() { (( $1 )) && print -rn true || print -rn false; }

cmd_dados() {
  local sep t desc b tipo cam quem n app c label info prog porta perfil p
  case ${1:-resumo} in
    resumo)
      local tend=$(tendencia_kb) at=0 al=0
      ativo && at=1
      [[ $AUTO_LIMPEZA == sim ]] && al=1
      print -rn -- "{\"livre\":$(livre_kb),\"total\":$(total_kb),\"meta_kb\":$(gb_kb $META_GB),\"meta_gb\":$META_GB"
      print -rn -- ",\"vigilancia\":$(bool $at),\"auto_limpeza\":$(bool $al),\"intervalo\":$INTERVALO_HORAS"
      print -rn -- ",\"tendencia_kb\":${tend:-null},\"primeira_feita\":$(primeira_feita && print -n true || print -n false)"
      print -rn -- ",\"quarentena_kb\":$(kb "$QUAR" 2>/dev/null || print 0),\"quarentena_lotes\":${#${(f)"$(lotes)"}},\"versao\":$(js "$VERSAO")"

      print -rn -- ",\"lixo\":["; sep=
      while IFS='|' read -r t desc b tipo cam quem; do
        print -rn -- "$sep{\"kb\":$t,\"desc\":$(js "$desc"),\"bloqueado\":$(bool $b),\"cam\":$(js "$cam"),\"quem\":$(js "$quem")}"; sep=,
      done < <(listar_lixo)

      print -rn -- "],\"crashes\":["; sep=
      while IFS='|' read -r n t app c; do
        print -rn -- "$sep{\"n\":$n,\"kb\":$t,\"app\":$(js "$app"),\"cam\":$(js "$c")}"; sep=,
      done < <(listar_crashes)

      print -rn -- "],\"tarefas\":["; sep=
      while IFS='|' read -r tipo label info prog; do
        print -rn -- "$sep{\"tipo\":$(js "$tipo"),\"label\":$(js "$label"),\"info\":$(js "$info"),\"prog\":$(js "$prog")}"; sep=,
      done < <(listar_tarefas)

      print -rn -- "],\"novas_tarefas\":["; sep=
      local quando arq
      while IFS='|' read -r quando label arq prog; do
        [[ $arq == $HOME/Library/LaunchAgents/* ]] && b=1 || b=0
        print -rn -- "$sep{\"quando\":$quando,\"label\":$(js "$label"),\"arquivo\":$(js "$arq"),\"prog\":$(js "$prog"),\"pausavel\":$(bool $b)}"; sep=,
      done < <(listar_novas)

      print -rn -- "],\"remoto\":["; sep=
      while IFS='|' read -r app porta perfil; do
        print -rn -- "$sep{\"app\":$(js "$app"),\"porta\":$(js "$porta"),\"perfil\":$(js "$perfil")}"; sep=,
      done < <(listar_remoto)
      print -r -- "]}"
      ;;
    pastas)
      print -rn -- "["; sep=
      while IFS='|' read -r t p; do
        oculta "$p" && b=1 || b=0
        print -rn -- "$sep{\"kb\":$t,\"cam\":$(js "$p"),\"oculta\":$(bool $b),\"dica\":$(js "$(dica "$p")")}"; sep=,
      done < <(listar_maiores 20)
      print -r -- "]"
      ;;
    geral)
      local id nivel nota
      print -rn -- "{\"livre\":$(livre_kb),\"itens\":["; sep=
      while IFS='|' read -r id nivel t quem desc nota tipo cam; do
        vai_pra_quarentena "$tipo" && b=1 || b=0
        local ud=
        # "há quanto tempo não muda" só faz sentido pra pasta inteira; os itens "antigos" já são filtrados pela idade
        [[ $tipo == (conteudo|quarentena-pasta|quarentena-conteudo|apagar-pasta) ]] && ud=$(dias_sem_mexer "$cam")
        print -rn -- "$sep{\"uso_dias\":${ud:-null},\"id\":$(js "$id"),\"nivel\":$(js "$nivel"),\"kb\":$t,\"quem\":$(js "$quem"),\"desc\":$(js "$desc"),\"nota\":$(js "$nota"),\"quarentena\":$(bool $b),\"lixeira\":$(bool $b),\"cam\":$(js "$cam"),\"efeito\":$(js "$(efeito "$tipo")")}"; sep=,
      done < <(linhas_geral --nova)
      print -r -- "]}"
      ;;
    historico)
      # espaço livre nos últimos 30 dias (historico.csv) e as últimas limpezas (limpezas.csv)
      local lim=$(( $(date +%s) - 30 * 86400 )) quando kbl dsc dst soma=0
      print -rn -- "{\"livre\":["; sep=
      if [[ -f $HIST ]]; then
        while IFS=, read -r quando kbl; do
          (( quando >= lim )) || continue
          print -rn -- "$sep{\"t\":$quando,\"kb\":$kbl}"; sep=,
        done < "$HIST"
      fi
      print -rn -- "],\"limpezas\":["; sep=
      if [[ -f $LIMPEZAS ]]; then
        while IFS='|' read -r quando kbl dsc dst; do
          (( quando >= lim )) && [[ $dst == apagado ]] && (( soma += kbl ))
        done < "$LIMPEZAS"
        while IFS='|' read -r quando kbl dsc dst; do
          print -rn -- "$sep{\"t\":$quando,\"kb\":$kbl,\"desc\":$(js "$dsc"),\"destino\":$(js "$dst")}"; sep=,
        done < <(tail -n 30 "$LIMPEZAS" | tail -r)
      fi
      print -r -- "],\"liberado_mes_kb\":$soma}"
      ;;
    memoria)
      local qtd
      print -rn -- "{\"total_kb\":$(memoria_total_kb),\"livre_pct\":$(memoria_livre_pct),\"pressao\":$(js "$(pressao)")"
      print -rn -- ",\"swap_total_kb\":$(swap_kb total),\"swap_usada_kb\":$(swap_kb used),\"ligado_ha_s\":$(ligado_ha_s),\"apps\":["; sep=
      while IFS='|' read -r t app qtd; do
        print -rn -- "$sep{\"kb\":$t,\"app\":$(js "$app"),\"processos\":$qtd}"; sep=,
      done < <(listar_memoria 8)
      print -r -- "]}"
      ;;
    processos)
      local pid nm h cpu orfao pai cm
      print -rn -- "["; sep=
      while IFS='|' read -r pid nm t h cpu orfao pai cm; do
        print -rn -- "$sep{\"pid\":$pid,\"nome\":$(js "$nm"),\"kb\":$t,\"horas\":$h,\"orfao\":$(bool $orfao),\"pai\":$(js "$pai"),\"comando\":$(js "$cm")}"; sep=,
      done < <(listar_dev)
      print -r -- "]"
      ;;
    docker)
      local tp r at=0
      docker_ok && at=1
      print -rn -- "{\"ativo\":$(bool $at),\"disco_kb\":$(docker_kb),\"itens\":["; sep=
      if (( at )); then
        while IFS='|' read -r tp t r; do
          print -rn -- "$sep{\"tipo\":$(js "$tp"),\"kb\":$t,\"recuperavel_kb\":$r}"; sep=,
        done < <(docker_df)
      fi
      print -r -- "]}"
      ;;
    duplicados)
      local sha f
      print -rn -- "["; sep=
      while IFS='|' read -r t sha f; do
        print -rn -- "$sep{\"kb\":$t,\"sha\":$(js "$sha"),\"cam\":$(js "$f")}"; sep=,
      done < <(listar_duplicados)
      print -r -- "]"
      ;;
    backups)
      local data nome d acesso=1
      print -rn -- "{\"itens\":["; sep=
      while IFS='|' read -r t data nome d; do
        [[ $t == SEM-ACESSO ]] && { acesso=0; continue; }
        print -rn -- "$sep{\"kb\":$t,\"data\":$data,\"aparelho\":$(js "$nome"),\"cam\":$(js "$d")}"; sep=,
      done < <(listar_backups)
      print -r -- "],\"acesso\":$(bool $acesso)}"
      ;;
    semana)
      print -r -- "{\"texto\":$(js "$(texto_semana)")}"
      ;;
    ferramentas)
      local fn fk fd fl fv
      print -rn -- "["; sep=
      while IFS='|' read -r fn fk fd fl fv; do
        print -rn -- "$sep{\"nome\":$(js "$fn"),\"kb\":$fk,\"dias\":$fd,\"limpavel_kb\":$fl,\"veredito\":$(js "$fv")}"; sep=,
      done < <(listar_ferramentas | sort -t'|' -k2,2nr)
      print -r -- "]"
      ;;
    apps)
      local ak ad an aa am ados
      print -rn -- "["; sep=
      while IFS='|' read -r ak ad an aa am ados; do
        print -rn -- "$sep{\"kb\":$ak,\"dias\":$ad,\"nome\":$(js "$an"),\"cam\":$(js "$aa"),\"movivel\":$(bool $am),\"dados_kb\":$ados}"; sep=,
      done < <(listar_apps_sem_uso)
      print -r -- "]"
      ;;
    pesado)
      local lp lm ls lo
      lp=$(limites_pesado); lm=${lp%%|*}; ls=${${lp#*|}%%|*}; lo=${lp##*|}
      print -r -- "{\"mem_min\":$lm,\"swap_max_gb\":$ls,\"origem\":$(js "$lo"),\"livre_pct\":$(memoria_livre_pct),\"swap_usada_kb\":$(swap_kb used)}"
      ;;
    encaminhar)
      local ef
      ef=$(encaminhador) || ef=
      print -r -- "{\"programa\":$(js "$ef"),\"urgentes\":$([[ $ENCAMINHAR_CRITICO == sim ]] && print -n true || print -n false)}"
      ;;
    config)
      local k v i
      print -rn -- "{"; sep=
      for k in $CHAVES_NUM; do print -rn -- "$sep\"${(L)k}\":${(P)k}"; sep=,; done
      for k in $CHAVES_SN; do print -rn -- "$sep\"${(L)k}\":$([[ ${(P)k} == sim ]] && print -n true || print -n false)"; done
      print -rn -- ",\"ignorados\":["; sep=
      for i in $IGNORADOS; do print -rn -- "$sep$(js "$i")"; sep=,; done
      print -rn -- "],\"regras\":["; sep=
      if [[ -f $REGRAS ]]; then
        while IFS='|' read -r i v; do print -rn -- "$sep{\"cam\":$(js "$i"),\"kb\":$v}"; sep=,; done < "$REGRAS"
      fi
      print -r -- "],\"versao\":$(js "$VERSAO")}"
      ;;
    sistema)
      print -r -- "{\"purgavel_kb\":$(purgavel_kb),\"swap_total_kb\":$(swap_kb total),\"ligado_ha_s\":$(ligado_ha_s),\"snapshots\":$(snapshots_n),\"docker_kb\":$(docker_kb),\"simuladores\":$(simuladores_n),\"lixeira_kb\":$(kb "$HOME/.Trash"),\"quarentena_kb\":$(kb "$QUAR")}"
      ;;
    esquecidos)
      local f u
      print -rn -- "["; sep=
      while IFS='|' read -r t f u; do
        print -rn -- "$sep{\"kb\":$t,\"cam\":$(js "$f"),\"usado\":$u}"; sep=,
      done < <(listar_esquecidos)
      print -r -- "]"
      ;;
    crescimento)
      local janela d k desde ref primeiro
      print -rn -- "{"; primeiro=1
      for janela in 86400:24h 604800:7d; do
        (( primeiro )) || print -rn -- ","; primeiro=0
        print -rn -- "\"${janela#*:}\":{\"itens\":["; sep= ref=null
        while IFS='|' read -r d k c desde; do
          print -rn -- "$sep{\"mais_kb\":$d,\"kb\":$k,\"cam\":$(js "$c")}"; sep=, ref=$desde
        done < <(crescimento ${janela%%:*} 5)
        print -rn -- "],\"desde\":$ref}"
      done
      print -r -- "}"
      ;;
    quarentena)
      local l
      print -rn -- "{\"dias\":$QUARENTENA_DIAS,\"lotes\":["; sep=
      for l in ${(f)"$(lotes)"}; do
        t=$(lote_ts "$l")
        print -rn -- "$sep{\"id\":$(js "${l:t}"),\"kb\":$(kb "$l"),\"itens\":$(lote_n "$l"),\"criado\":${t:-0},\"sai\":$(( ${t:-0} + QUARENTENA_DIAS * 86400 ))}"; sep=,
      done
      print -r -- "]}"
      ;;
    *) erro "use: keepfree dados resumo|pastas|geral|quarentena|crescimento|sistema|esquecidos|historico|memoria|processos|docker|duplicados|backups|semana|config|ferramentas|apps|pesado|encaminhar" ;;
  esac
}

cmd_ajuda() {
  cat <<EOF
${B}keepfree${N} — mantém seu Mac com espaço livre

${B}Uso:${N}
  keepfree                      resumo: espaço livre, meta e vigilância
  keepfree geral                análise geral: acha o espaço escondido e você escolhe o que limpar
  keepfree analisar             raio-x completo: o que ocupa espaço e por quê
  keepfree cresceu              o que mais cresceu nas últimas 24 h e 7 dias
  keepfree testar-aviso [tom]   mostra um aviso agora (tom: critico, atencao, seguranca, sucesso, info)
  keepfree esquecidos           arquivos de 1 GB+ sem uso há 90 dias
  keepfree memoria              memória livre, swap e quem mais usa (somando cada app)
  keepfree processos            node, python e MCP parados há horas, ocupando memória
  keepfree encerrar <pid>       fecha um desses programas (pede confirmação)
  keepfree docker [--limpar]    o que dá pra recuperar no Docker (volumes nunca)
  keepfree duplicados           arquivos de 100 MB+ repetidos
  keepfree backups              backups de iPhone e iPad, com a data
  keepfree semana               o resumo da semana
  keepfree ferramentas          IDEs, CLIs e apps de IA: o que está em uso e o que dá pra limpar
  keepfree apps                 apps sem uso há ${APPS_DIAS} dias ou mais
  keepfree liberar-memoria      pronto pra tarefa pesada? o que fechar antes
  keepfree encaminhar <prog>    manda o resumo da semana (e urgentes) por um programa seu (WhatsApp…)
  keepfree atualizar            instala a versão nova (pelo gh)
  keepfree snapshots [--liberar] snapshots locais do Time Machine e espaço purgável
  keepfree limpar               mostra o lixo seguro (simulação)
  keepfree limpar --executar    apaga o lixo seguro (pede confirmação)
  keepfree esvaziar-lixeira     esvazia a Lixeira (pede confirmação)
  keepfree quarentena           o que está na quarentena (sai sozinho depois de ${QUARENTENA_DIAS} dias)
  keepfree quarentena --liberar apaga de vez o que está na quarentena
  keepfree desfazer             devolve ao lugar o que foi pra quarentena na última limpeza
  keepfree mover <pasta> <dest> move uma pasta pro HD externo e deixa um atalho

  keepfree ativar               liga a vigilância automática
  keepfree desativar            desliga a vigilância automática
  keepfree config               mostra/muda a configuração
  keepfree config meta 30       espaço livre mínimo (GB)
  keepfree config auto-limpeza sim   limpa sozinho quando passar da meta

  keepfree pausar-tarefa <nome>  para uma tarefa em segundo plano (reversível)
  keepfree religar-tarefa <nome> religa uma tarefa pausada

  keepfree tarefa-ok <nome>      "é minha": tira uma tarefa nova dos alertas
  keepfree regra <pasta> <3G>    a vigilância limpa um item seguro quando passar do tamanho

  keepfree ignorar               lista as pastas ignoradas
  keepfree ignorar <pasta>       tira uma pasta da análise, da limpeza e da vigilância
  keepfree ignorar --desfazer <pasta>

${B}O que a vigilância faz${N} (a cada ${INTERVALO_HORAS}h):
  • avisa quando o espaço livre fica abaixo da meta (de 6 em 6 h, antes se piorar 2 GB)
  • abaixo de ${CRITICO_GB} GB avisa toda vez, dizendo quem mais cresceu
  • avisa quando o disco perde mais de ${ALERTA_QUEDA_GB} GB em 24h
  • detecta apps crashando em loop e apaga os relatórios
  • avisa quando aparece uma tarefa nova em segundo plano ou um navegador com controle remoto
  • aplica as regras (keepfree regra)
  • avisa com a memória apertada, sugere reiniciar quando a swap passa de ${SWAP_AVISO_GB} GB
  • avisa quando o disco do Docker cresce ${DOCKER_ALERTA_GB} GB em 7 dias
  • manda um resumo por semana
  • se auto-limpeza = sim, limpa o lixo seguro sozinho

Histórico do que foi apagado: $(curto "$LOG")
EOF
}

# carregado com `source` (testes): só define as funções
[[ $ZSH_EVAL_CONTEXT == *:file ]] && return 0

# --json nos comandos de leitura devolve o mesmo JSON que o app lê
if [[ ${@[(Ie)--json]} -gt 0 ]]; then
  case $1 in
    status|""|analisar|a) cmd_dados resumo; exit ;;
    cresceu)    cmd_dados crescimento; exit ;;
    esquecidos) cmd_dados esquecidos; exit ;;
    quarentena) cmd_dados quarentena; exit ;;
    snapshots)  cmd_dados sistema; exit ;;
  esac
fi

case ${1:-status} in
  status)          cmd_status ;;
  analisar|a)      cmd_analisar ;;
  limpar)          shift; cmd_limpar "$@" ;;
  vigiar)          cmd_vigiar ;;
  dados)           cmd_dados "$2" ;;
  geral)           cmd_geral "$2" ;;
  limpar-itens)    shift; cmd_limpar_itens "$@" ;;
  esvaziar-lixeira) cmd_esvaziar_lixeira "$2" ;;
  config)          shift; cmd_config "$@" ;;
  ativar)          cmd_ativar ;;
  migrar)          cmd_migrar ;;
  desativar)       cmd_desativar ;;
  mover)           shift; cmd_mover "$@" ;;
  pausar-tarefa)   cmd_pausar_tarefa "$2" ;;
  religar-tarefa)  cmd_religar_tarefa "$2" ;;
  ignorar)         shift; cmd_ignorar "$@" ;;
  quarentena)      shift; cmd_quarentena "$@" ;;
  cresceu)         cresceu ;;
  testar-aviso)    shift; cmd_testar_aviso "$@" ;;
  tarefa-ok)       cmd_tarefa_ok "$2" ;;
  memoria)         titulo "Memória: $(memoria_livre_pct)% livre · swap $(humano $(swap_kb used)) de $(humano $(swap_kb total)) · pressão $(pressao)"
                   listar_memoria 10 | while IFS='|' read -r k a q; do printf "  %s%-9s%s %s ${D}(%s processos)${N}\n" "$B" "$(humano $k)" "$N" "$a" $q; done ;;
  processos)       titulo "Programas de programação parados há ${DEV_HORAS}h ou mais"; processos ;;
  encerrar)        cmd_encerrar "$2" "$3" ;;
  checar-recursos) cmd_checar_recursos ;;
  docker)          shift; cmd_docker "$@" ;;
  duplicados)      titulo "Arquivos de 100 MB+ repetidos ${D}(lendo os arquivos… pode demorar)${N}"; duplicados ;;
  backups)         titulo "Backups de iPhone e iPad"; backups ;;
  semana)          texto_semana ;;
  ferramentas)     titulo "Ferramentas de desenvolvimento e IA ${D}(o que está em uso e o que dá pra limpar)${N}"; ferramentas ;;
  apps)            titulo "Apps sem uso há $APPS_DIAS dias ou mais"
                   [[ -n $(listar_apps_sem_uso) ]] || print -r -- "  ${G}✓${N} Nenhum: todos os apps foram abertos nos últimos $APPS_DIAS dias."
                   listar_apps_sem_uso | while IFS='|' read -r k d n a m x; do printf "  %s%-9s%s %s ${D}(%s%s)${N}\n" "$B" "$(humano $k)" "$N" "$n" "$( (( d < 0 )) && print 'nunca aberto' || print "sem uso há $d dias")" "$( (( m )) || print '; remova pelo Finder')"; done ;;
  liberar-memoria) liberar_memoria ;;
  encaminhar)      shift; cmd_encaminhar "$@" ;;
  atualizar)       shift; cmd_atualizar "$@" ;;
  regra)           shift; cmd_regra "$@" ;;
  esquecidos)      esquecidos ;;
  snapshots)       shift; cmd_snapshots "$@" ;;
  desfazer)        shift; cmd_desfazer "$@" ;;
  versao|-v|--version) print -r -- "keepfree $VERSAO" ;;
  ajuda|-h|--help) cmd_ajuda ;;
  *) print -r -- "Comando desconhecido: $1"; cmd_ajuda; exit 1 ;;
esac
