diff --git a/Makefile b/Makefile index ace4b3ec5..38dd5de50 100644 --- a/Makefile +++ b/Makefile @@ -701,7 +701,7 @@ lua-check-hammerspoon-dev: ## Run the Hammerspoon Lua check inside the Nix dev s ##@ Launchd Services .PHONY: launchctl -launchctl: launchctl-brew-upgrader launchctl-openclaw launchctl-cliproxyapi launchctl-cliproxyapi-backup launchctl-code-syncer launchctl-docker-postgres launchctl-dotfiles-updater launchctl-neverssl-keepalive launchctl-ollama ## Restart all launchd agents. +launchctl: launchctl-brew-upgrader launchctl-openclaw launchctl-cliproxyapi launchctl-cliproxyapi-backup launchctl-code-syncer launchctl-docker-postgres launchctl-dotfiles-updater launchctl-neverssl-keepalive launchctl-ollama launchctl-tmux-session-logger ## Restart all launchd agents. .PHONY: launchctl-brew-upgrader launchctl-brew-upgrader: ## Restart brew-upgrader launchd agent. @@ -776,10 +776,18 @@ launchctl-ollama: ## Restart ollama launchd agent. @launchctl load ~/Library/LaunchAgents/org.nix-community.home.ollama.plist @echo "✅ ollama restarted" +.PHONY: launchctl-tmux-session-logger +launchctl-tmux-session-logger: ## Restart tmux-session-logger launchd agent. + @echo "🔄 Restarting tmux-session-logger..." + @launchctl unload ~/Library/LaunchAgents/org.nix-community.home.tmux-session-logger.plist 2>/dev/null || true + @sleep 3 + @launchctl load ~/Library/LaunchAgents/org.nix-community.home.tmux-session-logger.plist + @echo "✅ tmux-session-logger restarted" + ##@ Systemd Services (Linux) .PHONY: systemctl -systemctl: systemctl-cliproxyapi systemctl-code-syncer systemctl-docker-postgres systemctl-dotfiles-updater systemctl-ollama systemctl-openclaw ## Restart all systemd user services. +systemctl: systemctl-cliproxyapi systemctl-cliproxyapi-backup systemctl-code-syncer systemctl-docker-postgres systemctl-dotfiles-updater systemctl-make-updater systemctl-neverssl-keepalive systemctl-ollama systemctl-openclaw systemctl-tmux-session-logger ## Restart all systemd user services. .PHONY: systemctl-cliproxyapi systemctl-cliproxyapi: ## Reload systemd units for cliproxyapi (home-manager handles restart). @@ -787,6 +795,12 @@ systemctl-cliproxyapi: ## Reload systemd units for cliproxyapi (home-manager han @systemctl --user daemon-reload @echo "✅ cliproxyapi reloaded" +.PHONY: systemctl-cliproxyapi-backup +systemctl-cliproxyapi-backup: ## Restart cliproxyapi-backup systemd user service. + @echo "🔄 Restarting cliproxyapi-backup..." + @systemctl --user restart cliproxyapi-backup.service || true + @echo "✅ cliproxyapi-backup restarted" + .PHONY: systemctl-code-syncer systemctl-code-syncer: ## Restart code-syncer systemd user service. @echo "🔄 Restarting code-syncer..." @@ -805,6 +819,18 @@ systemctl-dotfiles-updater: ## Restart dotfiles-updater systemd user service. @systemctl --user restart dotfiles-updater.service || true @echo "✅ dotfiles-updater restarted" +.PHONY: systemctl-make-updater +systemctl-make-updater: ## Restart make-updater systemd timer and service. + @echo "🔄 Restarting make-updater..." + @systemctl --user restart make-updater.timer || true + @echo "✅ make-updater restarted" + +.PHONY: systemctl-neverssl-keepalive +systemctl-neverssl-keepalive: ## Restart neverssl-keepalive systemd timer and service. + @echo "🔄 Restarting neverssl-keepalive..." + @systemctl --user restart neverssl-keepalive.timer || true + @echo "✅ neverssl-keepalive restarted" + .PHONY: systemctl-ollama systemctl-ollama: ## Restart ollama systemd user service. @echo "🔄 Restarting ollama..." @@ -822,6 +848,12 @@ systemctl-openclaw: ## Restart OpenClaw gateway systemd user service. @echo "✅ openclaw restarted" +.PHONY: systemctl-tmux-session-logger +systemctl-tmux-session-logger: ## Restart tmux-session-logger systemd timer and service. + @echo "🔄 Restarting tmux-session-logger..." + @systemctl --user restart tmux-session-logger.timer || true + @echo "✅ tmux-session-logger restarted" + .PHONY: git-submodule-sync git-submodule-sync: ## Sync and update git submodules. @echo "🔁 Syncing and updating git submodules..." @@ -845,18 +877,34 @@ shell-test-dev: ## Run shell tests inside the Nix dev shell (mirrors CI). .PHONY: fish-test fish-test: ## Run fish function tests using fishtape. @echo "🐟 Running fish function tests..." - @if ! command -v fishtape >/dev/null 2>&1; then \ - echo " fishtape not found, running inside Nix dev shell..."; \ - $(MAKE) fish-test-dev; \ - else \ - fish_errors=$$(mktemp); \ - fishtape spec/fish/*_test.fish 2>"$$fish_errors"; \ - rc=$$?; \ - if [ -s "$$fish_errors" ]; then \ - echo "fish test stderr (failing):"; cat "$$fish_errors"; rm -f "$$fish_errors"; exit 1; \ + @fish_runner=$$(command -v fishtape 2>/dev/null || true); \ + if [ -z "$$fish_runner" ]; then \ + set -- /nix/store/*-fishtape/bin/fishtape; \ + if [ -x "$$1" ]; then \ + fish_runner=$$1; \ + echo " fishtape not on PATH, using $$fish_runner"; \ + else \ + echo " fishtape not found, running inside Nix dev shell..."; \ + $(MAKE) fish-test-dev; \ + exit $$?; \ fi; \ - rm -f "$$fish_errors"; exit $$rc; \ - fi + fi; \ + fish_home=$$(mktemp -d "$${TMPDIR:-/tmp}/fish-test.XXXXXX"); \ + fish_errors=$$(mktemp); \ + fish_errors_filtered=$$(mktemp); \ + trap 'rm -rf "$$fish_home" "$$fish_errors" "$$fish_errors_filtered"' EXIT; \ + mkdir -p "$$fish_home/.config/fish" "$$fish_home/.local/state" "$$fish_home/.local/share"; \ + HOME="$$fish_home" \ + XDG_CONFIG_HOME="$$fish_home/.config" \ + XDG_STATE_HOME="$$fish_home/.local/state" \ + XDG_DATA_HOME="$$fish_home/.local/share" \ + "$$fish_runner" spec/fish/*_test.fish 2>"$$fish_errors"; \ + rc=$$?; \ + grep -v -E '^(warning: notify_register_file_descriptor\(\) failed with status 9\.|warning: Universal variable notifications may not be received\.)$$' "$$fish_errors" >"$$fish_errors_filtered" || true; \ + if [ -s "$$fish_errors_filtered" ]; then \ + echo "fish test stderr (failing):"; cat "$$fish_errors_filtered"; exit 1; \ + fi; \ + exit $$rc .PHONY: fish-test-dev fish-test-dev: ## Run fish tests inside the Nix dev shell (mirrors CI). diff --git a/home-manager/programs/tmux/session-logger.sh b/home-manager/programs/tmux/session-logger.sh index fc126c4b6..7dcb8e16c 100644 --- a/home-manager/programs/tmux/session-logger.sh +++ b/home-manager/programs/tmux/session-logger.sh @@ -5,40 +5,36 @@ LOG=~/.local/share/tmux/session-history.log PANE_DIR=~/.local/share/tmux/panes ARCHIVE_DIR=~/.local/share/tmux/archive -while true; do - sleep 30 +timestamp=$(date +%Y-%m-%dT%H:%M:%S) - # Append window metadata - tmux list-windows -a \ - -F "$(date +%Y-%m-%dT%H:%M:%S) #{session_name}:#{window_index} #{window_name} #{pane_current_path}" \ - >>"$LOG" 2>/dev/null +# Append window metadata +tmux list-windows -a \ + -F "${timestamp} #{session_name}:#{window_index} #{window_name} #{pane_current_path}" \ + >>"$LOG" 2>/dev/null - # Rotate existing live snapshots to .old - for f in "$PANE_DIR"/*.txt; do - [ -f "$f" ] && mv "$f" "${f%.txt}.old" - done +# Rotate existing live snapshots to .old +for f in "$PANE_DIR"/*.txt; do + [ -f "$f" ] && mv "$f" "${f%.txt}.old" +done - # Recapture all currently live panes - tmux list-panes -a -F "#{session_name} #{window_index} #{pane_index} #{pane_id}" \ - 2>/dev/null | while IFS= read -r line; do - sess=$(printf '%s' "$line" | cut -d' ' -f1) - widx=$(printf '%s' "$line" | cut -d' ' -f2) - pidx=$(printf '%s' "$line" | cut -d' ' -f3) - pane_id=$(printf '%s' "$line" | cut -d' ' -f4) +# Recapture all currently live panes +tmux list-panes -a -F "#{session_name} #{window_index} #{pane_index} #{pane_id}" \ + 2>/dev/null | while IFS=' ' read -r sess widx pidx pane_id; do + if [ -n "$sess" ]; then tmux capture-pane -pt "$pane_id" -S - 2>/dev/null \ >"$PANE_DIR/$sess--$widx--$pidx.txt" - done + fi +done - # For each .old: if a live .txt exists → pane survived → delete .old - # if no live .txt → pane closed → archive with timestamp - ts=$(date +%Y%m%d-%H%M%S) - for old in "$PANE_DIR"/*.old; do - [ -f "$old" ] || continue - base=$(basename "${old%.old}") - if [ -f "$PANE_DIR/$base.txt" ]; then - rm -f "$old" - else - mv "$old" "$ARCHIVE_DIR/${base}--${ts}.txt" - fi - done +# For each .old: if a live .txt exists → pane survived → delete .old +# if no live .txt → pane closed → archive with timestamp +ts=$(date +%Y%m%d-%H%M%S) +for old in "$PANE_DIR"/*.old; do + [ -f "$old" ] || continue + base=$(basename "${old%.old}") + if [ -f "$PANE_DIR/$base.txt" ]; then + rm -f "$old" + else + mv "$old" "$ARCHIVE_DIR/${base}--${ts}.txt" + fi done diff --git a/home-manager/programs/tmux/tmux.conf b/home-manager/programs/tmux/tmux.conf index 52f82aaf9..fe13c3f29 100644 --- a/home-manager/programs/tmux/tmux.conf +++ b/home-manager/programs/tmux/tmux.conf @@ -157,5 +157,4 @@ set -g @extrakto_key 'tab' set -g history-limit 2147483647 -# Persistent session history logger (starts once per server boot) -run-shell "pgrep -f 'session-logger.sh' > /dev/null 2>&1 || (nohup ~/.config/tmux/session-logger.sh > /dev/null 2>&1 &)" +# Persistent session history logger is managed by launchd/systemd. diff --git a/home-manager/services/default.nix b/home-manager/services/default.nix index fbc4a2e9d..d86314376 100644 --- a/home-manager/services/default.nix +++ b/home-manager/services/default.nix @@ -17,6 +17,7 @@ let sshAgent = import ./ssh-agent { inherit config lib pkgs; }; + tmuxSessionLogger = import ./tmux-session-logger { inherit pkgs; }; in [ brewUpgrader @@ -29,4 +30,5 @@ in neversslKeepalive ollama sshAgent + tmuxSessionLogger ] diff --git a/home-manager/services/tmux-session-logger/default.nix b/home-manager/services/tmux-session-logger/default.nix new file mode 100644 index 000000000..e6562f705 --- /dev/null +++ b/home-manager/services/tmux-session-logger/default.nix @@ -0,0 +1,54 @@ +{ pkgs, ... }: +let + inherit (pkgs) lib; + sessionLoggerScript = ../../programs/tmux/session-logger.sh; + servicePath = lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.tmux + ]; +in +{ + launchd.agents.tmux-session-logger = lib.mkIf pkgs.stdenv.isDarwin { + enable = true; + config = { + ProgramArguments = [ + "${pkgs.bash}/bin/bash" + "${sessionLoggerScript}" + ]; + Environment = { + PATH = "${servicePath}:/usr/bin:/bin:/usr/sbin:/sbin"; + }; + RunAtLoad = true; + StartInterval = 30; + StandardOutPath = "/tmp/tmux-session-logger.log"; + StandardErrorPath = "/tmp/tmux-session-logger.error.log"; + }; + }; + + systemd.user.services.tmux-session-logger = lib.mkIf pkgs.stdenv.isLinux { + Unit = { + Description = "Persist tmux pane history snapshots"; + }; + Service = { + Type = "oneshot"; + Environment = "PATH=${servicePath}"; + ExecStart = "${pkgs.bash}/bin/bash ${sessionLoggerScript}"; + }; + }; + + systemd.user.timers.tmux-session-logger = lib.mkIf pkgs.stdenv.isLinux { + Unit = { + Description = "Timer for tmux session history logging"; + }; + Timer = { + OnBootSec = "1s"; + OnUnitActiveSec = "30s"; + AccuracySec = "1s"; + Unit = "tmux-session-logger.service"; + }; + Install = { + WantedBy = [ "timers.target" ]; + }; + }; +}