feat(browser): auto-commit con 60 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Executable
+356
@@ -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
|
||||
Reference in New Issue
Block a user