Add Herdr update and live handoff menu actions

The Ctrl+A h command menu gains two host-scoped actions: a plain
`herdr update` that streams updater output into the popup, and an
experimental update-and-hot-restart flow that verifies a live server
handoff and reconnects the originating Hub tab once the popup closes.

Remote updates use the remote host's own proxy settings. A standalone
helper reads literal proxy assignments from shell profiles and running
Clash/Mihomo listeners without executing startup files; Windows reads
process, user and machine proxy variables through PowerShell.
This commit is contained in:
2026-10-03 02:51:44 +00:00
parent a5e8e32f1e
commit 6f933e1c80
10 changed files with 1130 additions and 38 deletions
+22
View File
@@ -8,6 +8,7 @@ import tempfile
import time
import unittest
import uuid
from unittest.mock import patch
from herdr_hub.cli import (
BOOTSTRAP_WINDOW_NAME,
@@ -15,11 +16,32 @@ from herdr_hub.cli import (
HubError,
TmuxHub,
WINDOW_SHORT_LABEL_OPTION,
build_parser,
command_control,
)
@unittest.skipUnless(shutil.which("tmux"), "tmux is required")
class TmuxIntegrationTests(unittest.TestCase):
def test_verified_handoff_reconnects_only_originating_client(self) -> None:
self.hub.ensure_server()
original = self.hub.create_window(Endpoint.local("first"), str(self.fake_herdr))
other = self.hub.create_window(Endpoint.local("second"), str(self.fake_herdr))
self.hub.select_window(original.window_id)
def pid(window):
return self.hub.run(["display-message", "-p", "-t", window.window_id, "#{pane_pid}"]).stdout.strip()
original_pid, other_pid = pid(original), pid(other)
def close_popup(run):
self.hub.select_window(other.window_id)
run.__self__.reconnect_requested = True
return 0
with patch("herdr_hub.cli.make_hub", return_value=self.hub), patch(
"herdr_hub.cli.resolve_herdr", return_value=str(self.fake_herdr)
), patch("herdr_hub.cli.curses.wrapper", side_effect=close_popup):
self.assertEqual(command_control(build_parser().parse_args(["control"])), 0)
self.assertNotEqual(pid(original), original_pid)
self.assertEqual(pid(other), other_pid)
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary_directory.cleanup)