feat(browser): auto-commit con 60 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:42:31 +02:00
parent 37aacfcfa9
commit 8742cb25be
71 changed files with 5660 additions and 192 deletions
@@ -0,0 +1,64 @@
---
name: reboot_all_claudes
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "reboot_all_claudes([--go|--yes] [--resume-mode resume|continue|none] [--exclude-current] [--only-idle] [-h|--help])"
description: "Cierra todas las terminales kitty con una sesion de Claude Code corriendo y las relanza retomando la misma sesion (claude --resume <sessionId>). Mapea cada PID vivo a su ~/.claude/sessions/<PID>.json para sacar sessionId, cwd y la ventana kitty. DRY-RUN por defecto; --go ejecuta de verdad de forma desacoplada."
tags: [claude, session, terminal, kitty, reboot, infra, terminal-capture]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "--go"
desc: "Ejecuta de verdad: mata las ventanas kitty y relanza las sesiones (detached). Alias --yes. Sin esto es dry-run."
- name: "--yes"
desc: "Alias de --go."
- name: "--resume-mode <resume|continue|none>"
desc: "Estrategia de reanudacion. resume (default): claude --resume <sessionId>. continue: claude --continue. none: sesion nueva en el mismo cwd."
- name: "--exclude-current"
desc: "No cierra ni relanza la terminal desde la que se invoca. Detecta el claude propio subiendo por la cadena de PPIDs hasta hallar un ancestro con comm=claude."
- name: "--only-idle"
desc: "Omite las sesiones con status busy (no pierde el turno en vuelo). Por defecto se incluyen todas y el dry-run avisa cuales estan busy."
- name: "-h|--help"
desc: "Muestra el uso y termina."
output: "Imprime una tabla del plan (PID, KITTY_PID, status, accion, sessionId, cwd) y el comando claude exacto por sesion. En dry-run no toca nada. Con --go lanza un script desacoplado en /tmp que cierra ventanas y relanza. Exit 0 normal; exit 2 si flags invalidos."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/reboot_all_claudes.sh"
notes: "Mecanismo (Claude Code 2.1.x sobre Linux + kitty): pgrep -x claude -> PIDs vivos; ~/.claude/sessions/<PID>.json -> sessionId/cwd/status/procStart; anti-PID-reciclado comparando procStart del JSON con el campo 22 de /proc/<PID>/stat; KITTY_PID del environ -> ventana a cerrar con SIGTERM; cmdline -> flags conservados (sin argv0 ni resume previos). El relanzamiento usa setsid kitty --directory <cwd> zsh -ic 'claude ...; exec zsh'. Como la propia terminal es una victima, el plan --go se escribe a /tmp y se lanza con setsid para sobrevivir al cierre del padre."
---
## Ejemplo
```bash
# Dry-run (default seguro): ver el plan sin tocar nada.
reboot_all_claudes
# Reiniciar de verdad todas las sesiones MENOS la terminal actual.
reboot_all_claudes --go --exclude-current
# Reiniciar solo las sesiones idle (no perder turnos en vuelo), de verdad.
reboot_all_claudes --go --only-idle
# Arrancar sesiones nuevas (sin reanudar la conversacion) en cada cwd.
reboot_all_claudes --go --resume-mode none
```
## Cuando usarla
Tras actualizar Claude Code (para que todas las sesiones corran la version nueva), o cuando varias sesiones se cuelgan y quieres reiniciarlas todas de golpe retomando exactamente la conversacion donde estaba cada una. Lanza siempre primero sin flags para revisar el plan; luego repite con `--go`.
## Gotchas
- **Es impura y se auto-mata.** La terminal desde la que la invocas suele ser una de las victimas; por eso el modo `--go` escribe un script a `/tmp/reboot_all_claudes.<pid>.<ts>.sh` y lo lanza con `setsid` para que el reparenting a init garantice los relanzamientos aunque el padre muera. Usa `--exclude-current` si quieres conservar la terminal actual.
- **Sesiones `busy` pierden el turno en vuelo.** Por defecto se reinician igual y el dry-run lo avisa explicitamente. Al reanudar con `--resume` se recupera hasta el ultimo mensaje completo guardado en el `.jsonl`. Usa `--only-idle` para no tocarlas.
- **Depende de `~/.claude/sessions/<PID>.json`** (formato de Claude Code 2.1.x). Si una version futura cambia el formato, la funcion deja de mapear PID -> sessionId y omitira las sesiones.
- **Asume kitty como terminal.** Si un claude corre fuera de kitty (sin `KITTY_PID` en el environ, p.ej. terminal integrado de un editor), el fallback mata directamente el PID de claude y abre una kitty nueva en su `cwd`.
- **Anti-PID-reciclado:** valida `procStart` del JSON contra el campo 22 de `/proc/<PID>/stat`; si no coincide (o el JSON no existe, o `kill -0` falla) la sesion se omite como huerfana.
+356
View File
@@ -0,0 +1,356 @@
#!/usr/bin/env bash
# reboot_all_claudes — Cierra todas las terminales con una sesion de Claude Code
# corriendo y las relanza retomando exactamente la sesion que tenian
# (claude --resume <sessionId>). Por defecto es DRY-RUN: imprime el plan sin
# tocar nada. Usar --go para ejecutarlo de verdad.
#
# Mecanismo (Claude Code 2.1.x sobre Linux + kitty):
# - pgrep -x claude -> PIDs de las sesiones interactivas vivas.
# - ~/.claude/sessions/<PID>.json -> mapea PID a {sessionId, cwd, status, procStart}.
# - Anti-PID-reciclado: procStart del JSON debe coincidir con el campo 22 de
# /proc/<PID>/stat; ademas kill -0 <PID> debe tener exito.
# - KITTY_PID del environ del proceso -> ventana kitty a cerrar.
# - cmdline del proceso -> flags originales a conservar (sin argv0 ni resume previos).
# - Relanzamiento detached (setsid) para sobrevivir al cierre de la propia terminal.
set -euo pipefail
IFS=$' \t\n'
reboot_all_claudes() {
local mode="dry" # dry | go
local resume_mode="resume" # resume | continue | none
local exclude_current=0
local only_idle=0
# -----------------------------------------------------------------------
# Parseo de argumentos
# -----------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--go|--yes)
mode="go"
;;
--resume-mode)
shift
resume_mode="${1:-}"
case "$resume_mode" in
resume|continue|none) ;;
*)
echo "reboot_all_claudes: --resume-mode invalido: '$resume_mode' (usa resume|continue|none)" >&2
return 2
;;
esac
;;
--exclude-current)
exclude_current=1
;;
--only-idle)
only_idle=1
;;
-h|--help)
cat <<'USAGE'
Uso: reboot_all_claudes [opciones]
Cierra todas las terminales con una sesion de Claude Code corriendo y las
relanza retomando la misma sesion (claude --resume <sessionId>).
Por defecto es DRY-RUN (accion destructiva => default seguro): imprime el plan
y NO mata ni relanza nada.
Opciones:
--go, --yes Ejecuta de verdad (kills + relanzamientos detached).
--resume-mode <modo> resume (default) | continue | none.
resume -> claude --resume <sessionId>
continue -> claude --continue
none -> claude (sesion nueva en el mismo cwd)
--exclude-current No cierra ni relanza la terminal desde la que se invoca.
--only-idle Omite sesiones con status busy (no pierde turnos en vuelo).
-h, --help Muestra esta ayuda.
Ejemplos:
reboot_all_claudes # dry-run, ve el plan
reboot_all_claudes --go --exclude-current # reinicia todas menos esta terminal
USAGE
return 0
;;
*)
echo "reboot_all_claudes: opcion desconocida: '$1' (usa -h)" >&2
return 2
;;
esac
shift
done
# -----------------------------------------------------------------------
# Detectar el PID de la sesion actual subiendo por la cadena de ancestros
# hasta encontrar un proceso cuyo comm sea exactamente "claude".
# -----------------------------------------------------------------------
local current_claude_pid=""
if [[ "$exclude_current" -eq 1 ]]; then
local walk="$$"
local guard=0
while [[ -n "$walk" && "$walk" != "0" && "$walk" != "1" ]]; do
local comm=""
comm="$(cat "/proc/$walk/comm" 2>/dev/null || true)"
if [[ "$comm" == "claude" ]]; then
current_claude_pid="$walk"
break
fi
# campo 4 de /proc/<pid>/stat es el PPID
walk="$(awk '{print $4}' "/proc/$walk/stat" 2>/dev/null || true)"
guard=$((guard + 1))
[[ "$guard" -gt 64 ]] && break
done
fi
# -----------------------------------------------------------------------
# Recolectar las sesiones vivas y validarlas.
# -----------------------------------------------------------------------
local sessions_dir="$HOME/.claude/sessions"
local pids=""
pids="$(pgrep -x claude 2>/dev/null || true)"
if [[ -z "$pids" ]]; then
echo "reboot_all_claudes: no hay sesiones de Claude Code vivas (pgrep -x claude vacio)."
return 0
fi
# Arrays paralelos con el plan validado.
local -a plan_pid plan_kitty plan_status plan_cwd plan_sid plan_cmd plan_skip plan_skipreason
local pid
for pid in $pids; do
# Validacion 1: el proceso debe seguir vivo.
if ! kill -0 "$pid" 2>/dev/null; then
continue
fi
# Validacion 2: debe existir su JSON de sesion.
local json="$sessions_dir/$pid.json"
if [[ ! -f "$json" ]]; then
continue
fi
# Parsear el JSON con python3 (campos sessionId, cwd, status, procStart).
# Salida: lineas "clave=valor" en orden fijo.
local parsed=""
parsed="$(python3 - "$json" <<'PY' 2>/dev/null || true
import json, sys
try:
with open(sys.argv[1]) as fh:
d = json.load(fh)
except Exception:
sys.exit(0)
print("sessionId=" + str(d.get("sessionId", "")))
print("cwd=" + str(d.get("cwd", "")))
print("status=" + str(d.get("status", "")))
print("procStart=" + str(d.get("procStart", "")))
PY
)"
[[ -z "$parsed" ]] && continue
local sid cwd status proc_start_json
sid="$(printf '%s\n' "$parsed" | sed -n 's/^sessionId=//p')"
cwd="$(printf '%s\n' "$parsed" | sed -n 's/^cwd=//p')"
status="$(printf '%s\n' "$parsed" | sed -n 's/^status=//p')"
proc_start_json="$(printf '%s\n' "$parsed" | sed -n 's/^procStart=//p')"
[[ -z "$sid" ]] && continue
# Validacion 3 (anti-PID-reciclado): procStart del JSON debe coincidir
# con el campo 22 de /proc/<PID>/stat.
local proc_start_real=""
proc_start_real="$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true)"
if [[ -n "$proc_start_json" && "$proc_start_json" != "$proc_start_real" ]]; then
# JSON huerfano de un PID reciclado: omitir.
continue
fi
# KITTY_PID de la ventana kitty (vacio si claude no corre en kitty).
local kitty_pid=""
kitty_pid="$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | sed -n 's/^KITTY_PID=//p' | head -n1)"
# Flags originales: leer cmdline, descartar argv0 (claude) y cualquier
# flag de resume/continue previo para no duplicarlos.
local raw_cmd=""
raw_cmd="$(tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true)"
local -a kept_flags=()
local first=1 tok skipnext=0
while IFS= read -r tok; do
[[ -z "$tok" ]] && continue
if [[ "$first" -eq 1 ]]; then
# argv0 (la ruta o nombre de claude) — descartar.
first=0
continue
fi
if [[ "$skipnext" -eq 1 ]]; then
skipnext=0
continue
fi
case "$tok" in
--resume|--continue|-r|-c)
# Resume/continue previos: omitir (y su posible valor para --resume).
if [[ "$tok" == "--resume" || "$tok" == "-r" ]]; then
skipnext=1
fi
continue
;;
esac
kept_flags+=("$tok")
done <<< "$raw_cmd"
# Construir la estrategia de resume.
local -a launch_args=()
case "$resume_mode" in
resume) launch_args=("--resume" "$sid") ;;
continue) launch_args=("--continue") ;;
none) launch_args=() ;;
esac
launch_args+=("${kept_flags[@]}")
# Comando claude final (para mostrar y ejecutar).
local claude_cmd="claude"
local a
for a in "${launch_args[@]}"; do
claude_cmd+=" $(printf '%q' "$a")"
done
# Decidir si se omite esta sesion del plan.
local skip=0 skipreason=""
if [[ "$exclude_current" -eq 1 && -n "$current_claude_pid" && "$pid" == "$current_claude_pid" ]]; then
skip=1
skipreason="terminal actual (--exclude-current)"
elif [[ "$only_idle" -eq 1 && "$status" == "busy" ]]; then
skip=1
skipreason="busy (--only-idle)"
fi
plan_pid+=("$pid")
plan_kitty+=("${kitty_pid:-}")
plan_status+=("${status:-?}")
plan_cwd+=("${cwd:-?}")
plan_sid+=("$sid")
plan_cmd+=("$claude_cmd")
plan_skip+=("$skip")
plan_skipreason+=("$skipreason")
done
local total="${#plan_pid[@]}"
if [[ "$total" -eq 0 ]]; then
echo "reboot_all_claudes: ninguna sesion valida encontrada (todos los PIDs eran huerfanos o reciclados)."
return 0
fi
# -----------------------------------------------------------------------
# Imprimir el plan (siempre, tanto en dry-run como en --go).
# -----------------------------------------------------------------------
echo "reboot_all_claudes — modo: ${mode} resume: ${resume_mode} sesiones: ${total}"
echo
printf '%-8s %-9s %-7s %-6s %-38s %s\n' "PID" "KITTY" "STATUS" "ACCION" "SESSION_ID" "CWD"
printf '%-8s %-9s %-7s %-6s %-38s %s\n' "--------" "---------" "-------" "------" "--------------------------------------" "---"
local i busy_count=0 act_count=0
for ((i = 0; i < total; i++)); do
local accion="reinic"
if [[ "${plan_skip[$i]}" -eq 1 ]]; then
accion="OMITE"
else
act_count=$((act_count + 1))
fi
[[ "${plan_status[$i]}" == "busy" ]] && busy_count=$((busy_count + 1))
printf '%-8s %-9s %-7s %-6s %-38s %s\n' \
"${plan_pid[$i]}" \
"${plan_kitty[$i]:-(none)}" \
"${plan_status[$i]}" \
"$accion" \
"${plan_sid[$i]}" \
"${plan_cwd[$i]}"
if [[ "${plan_skip[$i]}" -eq 1 ]]; then
echo " -> omitida: ${plan_skipreason[$i]}"
else
echo " -> ${plan_cmd[$i]}"
fi
done
echo
# Aviso explicito de sesiones busy que SI se van a reiniciar.
if [[ "$only_idle" -eq 0 ]]; then
local warned=0
for ((i = 0; i < total; i++)); do
if [[ "${plan_skip[$i]}" -eq 0 && "${plan_status[$i]}" == "busy" ]]; then
if [[ "$warned" -eq 0 ]]; then
echo "AVISO: las siguientes sesiones estan BUSY y se reiniciaran; perderan el turno en vuelo"
echo " (al reanudar con --resume se recupera hasta el ultimo mensaje completo guardado):"
warned=1
fi
echo " - PID ${plan_pid[$i]} cwd=${plan_cwd[$i]}"
fi
done
[[ "$warned" -eq 1 ]] && echo
fi
# -----------------------------------------------------------------------
# DRY-RUN: parar aqui.
# -----------------------------------------------------------------------
if [[ "$mode" == "dry" ]]; then
echo "DRY-RUN: no se ha matado ni relanzado nada."
echo "Para ejecutar de verdad: reboot_all_claudes --go"
return 0
fi
if [[ "$act_count" -eq 0 ]]; then
echo "reboot_all_claudes: nada que hacer (todas las sesiones quedaron omitidas)."
return 0
fi
# -----------------------------------------------------------------------
# MODO --go: construir un script desacoplado que mata las ventanas y
# relanza las sesiones. Se ejecuta con setsid para que sobreviva al cierre
# de la propia terminal (que es una de las victimas).
# -----------------------------------------------------------------------
local ts script log
ts="$(date +%s)"
script="/tmp/reboot_all_claudes.$$.$ts.sh"
log="/tmp/reboot_all_claudes.$ts.log"
{
echo '#!/usr/bin/env bash'
echo 'set -uo pipefail'
echo '# Dar tiempo a que la terminal padre devuelva el control antes de matar.'
echo 'sleep 1'
echo
for ((i = 0; i < total; i++)); do
[[ "${plan_skip[$i]}" -eq 1 ]] && continue
local kp="${plan_kitty[$i]}"
local cp="${plan_pid[$i]}"
local cwd="${plan_cwd[$i]}"
local cmd="${plan_cmd[$i]}"
echo "# --- sesion PID ${cp} (kitty ${kp:-none}) ---"
if [[ -n "$kp" ]]; then
# Cerrar la ventana kitty limpia con SIGTERM.
echo "kill $(printf '%q' "$kp") 2>/dev/null || true"
else
# Sin kitty: matar el propio claude.
echo "kill $(printf '%q' "$cp") 2>/dev/null || true"
fi
# Relanzar en una kitty nueva, detached, en el cwd correcto.
# zsh -ic '...; exec zsh' replica el patron del usuario: al salir de
# claude queda una shell interactiva viva.
printf 'setsid kitty --directory %q zsh -ic %q </dev/null >/dev/null 2>&1 &\n' \
"$cwd" "${cmd}; exec zsh"
echo
done
echo 'exit 0'
} > "$script"
chmod +x "$script"
echo "reboot_all_claudes: lanzando plan desacoplado -> $script (log: $log)"
setsid bash "$script" </dev/null >>"$log" 2>&1 &
disown 2>/dev/null || true
echo "reboot_all_claudes: hecho. Las terminales se cerraran y reabriran en ~1s."
return 0
}
# Permitir ejecutar el archivo directamente (no solo como funcion sourced).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
reboot_all_claudes "$@"
fi