#!/usr/bin/env bash # dt — gestionnaire de sessions claude-dans-dtach (1 session = 1 claude = 1 socket) # Sockets dans ~/.dtach/. La creation se fait via l'alias `cc` (voir plus bas), # pas ici : dt ne gere que listing / rattachement / kill. # # dt ls lister (nom, age, dossier, commande) # dt at rattacher une session # dt kill tuer une session # dt --raw listing TSV (pour fzf) # dt sock imprime le chemin du socket (utilitaire interne) # # Detacher une session attachee : Ctrl-\ # # CREATION (a mettre dans ~/.bashrc) : # dtach_claude() { dtach -c "$HOME/.dtach/${1:-claude-$(date +%H%M%S)}" -e '^\' claude; } # alias cc='dtach_claude' # # usage : cd ~/projets/seo && cc seo set -uo pipefail DTDIR="${DTACH_DIR:-$HOME/.dtach}" mkdir -p "$DTDIR" now=$(date +%s) # PID(s) du process dtach pour ce socket, via la cmdline (ss ne liste pas les # sockets dtach de maniere fiable selon la version). Matche -c/-n/-A/-N. _pids_for_socket() { local sock="$1" pgrep -f -- "dtach -[cnAN] $sock" 2>/dev/null | sort -u } _cwd_of() { readlink -f "/proc/$1/cwd" 2>/dev/null; } # Heure de demarrage REELLE du process (fiable, immuable), en epoch. _starttime_of() { local ls; ls=$(ps -o lstart= -p "$1" 2>/dev/null) || return [ -n "$ls" ] && date -d "$ls" +%s 2>/dev/null } _age() { local epoch="${1:-$now}" # garde-fou : si epoch n'est pas numerique, age=0 [[ "$epoch" =~ ^[0-9]+$ ]] || epoch="$now" local age="$(( now - epoch ))" (( age < 0 )) && age=0 if (( age<60 )); then echo "${age}s" elif (( age<3600 )); then echo "$(( age/60 ))m" elif (( age<86400));then echo "$(( age/3600 ))h$(( (age%3600)/60 ))m" else echo "$(( age/86400 ))j$(( (age%86400)/3600 ))h"; fi } # Une ligne TSV par session vivante. Nettoie les sockets morts au passage. emit_raw() { shopt -s nullglob local sock name pids master cwd start for sock in "$DTDIR"/*; do [ -S "$sock" ] || continue name=$(basename "$sock") pids=$(_pids_for_socket "$sock") if [ -z "$pids" ]; then rm -f "$sock"; continue; fi # orphelin -> menage master=$(echo "$pids" | head -1) cwd=$(_cwd_of "$master"); cwd="${cwd:-?}" start=$(_starttime_of "$master"); start="${start:-$now}" # colonnes : NOM(cache pour fzf) TAB DOSSIER TAB AGE printf '%s\t%s\t%s\n' "$name" "${cwd/#$HOME/\~}" "$(_age "$start")" done } cmd_at() { local name="${1:?nom requis}"; local sock="$DTDIR/$name" [ -S "$sock" ] || { echo "Session '$name' introuvable."; return 1; } exec dtach -a "$sock" -e '^\' } cmd_kill() { local name="${1:?nom requis}"; local sock="$DTDIR/$name" [ -S "$sock" ] || { echo "Session '$name' introuvable."; return 1; } local master; master=$(_pids_for_socket "$sock" | head -1) [ -n "$master" ] && kill "$master" 2>/dev/null rm -f "$sock" echo "Session '$name' tuee." } cmd_sock() { echo "$DTDIR/${1:?nom requis}"; } cmd_ls() { local rows; rows=$(emit_raw) if [ -z "$rows" ]; then echo "Aucune session dtach."; return 0; fi printf '\n \033[1;36mSessions claude\033[0m\n\n' printf ' \033[2m%-40s %s\033[0m\n' DOSSIER AGE printf '%s\n' "$rows" | while IFS=$'\t' read -r name cwd age; do printf ' \033[34m%-40s\033[0m \033[33m%s\033[0m\n' "$cwd" "$age" done printf '\n' } case "${1:-}" in ls) shift; cmd_ls "$@" ;; at) shift; cmd_at "$@" ;; kill) shift; cmd_kill "$@" ;; sock) shift; cmd_sock "$@" ;; --raw) emit_raw ;; ""|-h|--help) sed -n '2,18p' "$0" | sed 's/^# \?//' ;; *) echo "Commande inconnue: $1"; sed -n '2,18p' "$0" | sed 's/^# \?//'; exit 1 ;; esac