#!/bin/bash # Stale Process Monitor # Manages and cleans up stale background processes from Claude Code sessions set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Process patterns to monitor STALE_PATTERNS=( "kubectl port-forward" "npm run dev" "npx tsx" "node dist/index.js" "docker push" "kubectl exec" ) # Maximum age in minutes before a process is considered stale MAX_AGE_MINUTES=${MAX_AGE_MINUTES:-60} print_header() { echo -e "${BLUE}========================================${NC}" echo -e "${BLUE} Stale Process Monitor${NC}" echo -e "${BLUE}========================================${NC}" echo "" } list_processes() { local pattern="$1" local count=0 while IFS= read -r line; do if [[ -n "$line" ]]; then ((count++)) local pid=$(echo "$line" | awk '{print $1}') local start_time=$(ps -o lstart= -p "$pid" 2>/dev/null || echo "unknown") local elapsed=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ' || echo "unknown") local cmd=$(echo "$line" | awk '{$1=""; $2=""; print $0}' | sed 's/^ *//') echo -e " ${YELLOW}PID:${NC} $pid ${YELLOW}Elapsed:${NC} $elapsed" echo -e " ${YELLOW}Cmd:${NC} ${cmd:0:80}..." echo "" fi done < <(ps aux | grep -E "$pattern" | grep -v grep | grep -v "stale-process-monitor") return $count } show_status() { print_header echo -e "${GREEN}Scanning for potentially stale processes...${NC}" echo "" local total=0 for pattern in "${STALE_PATTERNS[@]}"; do local matches=$(ps aux | grep -E "$pattern" | grep -v grep | grep -v "stale-process-monitor" | wc -l) if [[ $matches -gt 0 ]]; then echo -e "${YELLOW}[$pattern]${NC} - $matches process(es) found:" echo "" list_processes "$pattern" ((total += matches)) fi done echo -e "${BLUE}----------------------------------------${NC}" echo -e "${GREEN}Total potentially stale processes: $total${NC}" echo "" if [[ $total -eq 0 ]]; then echo -e "${GREEN}No stale processes detected.${NC}" fi } kill_pattern() { local pattern="$1" local dry_run="$2" # Get PIDs from the second column (proper PID column in ps aux) local pids=$(ps aux | grep -E "$pattern" | grep -v grep | grep -v "stale-process-monitor" | awk '{print $2}') if [[ -z "$pids" ]]; then echo -e "${YELLOW}No processes matching '$pattern'${NC}" return 0 fi for pid in $pids; do # Validate that it's actually a number if [[ "$pid" =~ ^[0-9]+$ ]]; then if [[ "$dry_run" == "true" ]]; then echo -e "${YELLOW}[DRY-RUN] Would kill PID $pid${NC}" else echo -e "${RED}Killing PID $pid...${NC}" kill -9 "$pid" 2>/dev/null || echo -e "${YELLOW} (already dead)${NC}" fi fi done } clean_all() { local dry_run="$1" print_header if [[ "$dry_run" == "true" ]]; then echo -e "${YELLOW}DRY RUN MODE - No processes will be killed${NC}" else echo -e "${RED}CLEANING ALL STALE PROCESSES${NC}" fi echo "" for pattern in "${STALE_PATTERNS[@]}"; do echo -e "${BLUE}Processing: $pattern${NC}" kill_pattern "$pattern" "$dry_run" echo "" done echo -e "${GREEN}Cleanup complete.${NC}" } clean_port_forwards() { local dry_run="$1" print_header echo -e "${BLUE}Cleaning kubectl port-forward processes...${NC}" echo "" kill_pattern "kubectl port-forward" "$dry_run" echo "" echo -e "${GREEN}Port-forward cleanup complete.${NC}" } clean_dev_servers() { local dry_run="$1" print_header echo -e "${BLUE}Cleaning npm run dev processes...${NC}" echo "" kill_pattern "npm run dev" "$dry_run" kill_pattern "vite" "$dry_run" kill_pattern "node.*dist/index.js" "$dry_run" echo "" echo -e "${GREEN}Dev server cleanup complete.${NC}" } interactive_menu() { while true; do print_header echo "Options:" echo " 1) Show status (list all stale processes)" echo " 2) Clean all stale processes" echo " 3) Clean all (dry-run)" echo " 4) Clean port-forwards only" echo " 5) Clean dev servers only" echo " 6) Clean specific pattern" echo " q) Quit" echo "" read -p "Select option: " choice case $choice in 1) show_status ;; 2) clean_all "false" ;; 3) clean_all "true" ;; 4) clean_port_forwards "false" ;; 5) clean_dev_servers "false" ;; 6) read -p "Enter pattern to clean: " pattern kill_pattern "$pattern" "false" ;; q|Q) exit 0 ;; *) echo -e "${RED}Invalid option${NC}" ;; esac echo "" read -p "Press Enter to continue..." done } usage() { echo "Usage: $0 [command]" echo "" echo "Commands:" echo " status Show all potentially stale processes" echo " clean Clean all stale processes" echo " clean-dry Dry-run cleanup (show what would be killed)" echo " ports Clean kubectl port-forward processes only" echo " devs Clean dev server processes only" echo " kill Kill processes matching pattern" echo " interactive Interactive menu mode" echo "" echo "Environment variables:" echo " MAX_AGE_MINUTES Maximum process age before considered stale (default: 60)" echo "" echo "Examples:" echo " $0 status" echo " $0 clean" echo " $0 kill 'kubectl port-forward'" } # Main case "${1:-}" in status) show_status ;; clean) clean_all "false" ;; clean-dry) clean_all "true" ;; ports) clean_port_forwards "false" ;; devs) clean_dev_servers "false" ;; kill) if [[ -z "${2:-}" ]]; then echo "Error: Pattern required" usage exit 1 fi kill_pattern "$2" "false" ;; interactive|i) interactive_menu ;; help|-h|--help) usage ;; "") show_status ;; *) echo "Unknown command: $1" usage exit 1 ;; esac