diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index e1f862e..cb673c5 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -46,6 +46,7 @@ from ..agent_provider import AgentProvisionPlan, get_provider, build_agent_provi from ..egress import EgressPlan from ..git_gate import GitGatePlan from ..log import die, info, warn +from ..util import read_tty_line from ..manifest import Manifest, ManifestIndex from ..supervise import SupervisePlan from ..util import expand_tilde @@ -668,15 +669,6 @@ def get_bottle_backend( return backends[resolved] -def _read_tty_line() -> str: - """Read a line from /dev/tty, falling back to stdin.""" - try: - with open("/dev/tty", "r", encoding="utf-8") as tty: - return tty.readline().rstrip("\n") - except OSError: - return sys.stdin.readline().rstrip("\n") - - def _platform_vm_suggestion() -> str: """Platform-appropriate VM backend name for install suggestions.""" return "macos-container" if sys.platform == "darwin" else "firecracker" @@ -733,7 +725,7 @@ def _auto_select_backend() -> str: "bot-bottle: choice [i/d/q]: " ) sys.stderr.flush() - reply = _read_tty_line().strip().lower() + reply = read_tty_line().strip().lower() if reply == "d": return "docker" if reply == "i": diff --git a/bot_bottle/cli/_common.py b/bot_bottle/cli/_common.py index 0008a1e..c8d52bd 100644 --- a/bot_bottle/cli/_common.py +++ b/bot_bottle/cli/_common.py @@ -3,18 +3,10 @@ from __future__ import annotations import os -import sys from pathlib import Path +from ..util import read_tty_line + PROG = "cli.py" USER_CWD = os.getcwd() REPO_DIR = str(Path(__file__).resolve().parent.parent.parent) - - -def read_tty_line() -> str: - """Mirror `IFS= read -r REPLY bool: @@ -17,6 +18,15 @@ def is_ip_literal(value: str) -> bool: return True +def read_tty_line() -> str: + """Mirror `IFS= read -r REPLY str: """Expand a leading '~' to $HOME. Leaves paths without a leading tilde unchanged. Falls back to the empty string if $HOME is unset diff --git a/tests/unit/test_backend_selection.py b/tests/unit/test_backend_selection.py index 61cc5eb..7fb8cd9 100644 --- a/tests/unit/test_backend_selection.py +++ b/tests/unit/test_backend_selection.py @@ -66,7 +66,7 @@ class TestGetBottleBackend(unittest.TestCase): "macos-container": _FakeBackend("macos-container", False), "docker": _FakeBackend("docker", True), }), \ - patch.object(backend_mod, "_read_tty_line", return_value="d"): + patch.object(backend_mod, "read_tty_line", return_value="d"): b = get_bottle_backend() self.assertEqual("docker", b.name) @@ -136,7 +136,7 @@ class TestGetBottleBackend(unittest.TestCase): "macos-container": _FakeBackend("macos-container", False), "docker": _FakeBackend("docker", True), }), \ - patch.object(backend_mod, "_read_tty_line", return_value="q"), \ + patch.object(backend_mod, "read_tty_line", return_value="q"), \ patch.object(backend_mod, "die", side_effect=SystemExit("die")): with self.assertRaises(SystemExit): get_bottle_backend() @@ -159,7 +159,7 @@ class TestGetBottleBackend(unittest.TestCase): "macos-container": _FakeBackend("macos-container", False), "docker": _FakeBackend("docker", True), }), \ - patch.object(backend_mod, "_read_tty_line", return_value="i"), \ + patch.object(backend_mod, "read_tty_line", return_value="i"), \ patch.object(backend_mod, "_print_vm_install_instructions") as mock_inst, \ patch.object(backend_mod, "die", side_effect=SystemExit("die")): with self.assertRaises(SystemExit): @@ -168,26 +168,26 @@ class TestGetBottleBackend(unittest.TestCase): class TestReadTtyLine(unittest.TestCase): - """Unit tests for the _read_tty_line helper.""" + """Unit tests for the shared read_tty_line helper in bot_bottle.util.""" def test_reads_from_dev_tty(self): from unittest.mock import mock_open - from bot_bottle.backend import _read_tty_line + from bot_bottle.util import read_tty_line m = mock_open(read_data="hello\n") with patch("builtins.open", m): - result = _read_tty_line() + result = read_tty_line() self.assertEqual("hello", result) m.assert_called_once_with("/dev/tty", "r", encoding="utf-8") def test_falls_back_to_stdin_on_oserror(self): import io - from bot_bottle.backend import _read_tty_line + from bot_bottle.util import read_tty_line import sys as _sys with patch("builtins.open", side_effect=OSError("no tty")), \ patch.object(_sys, "stdin", io.StringIO("world\n")): - result = _read_tty_line() + result = read_tty_line() self.assertEqual("world", result)