luv-cli 0.6.2__tar.gz → 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: luv-cli
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Launch Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments
5
5
  Project-URL: Homepage, https://github.com/exospherehost/luv
6
6
  Project-URL: Repository, https://github.com/exospherehost/luv
@@ -59,12 +59,14 @@ def die(msg: str) -> None:
59
59
 
60
60
 
61
61
  def run(cmd: list[str], *, cwd: str | None = None, timeout: float | None = None,
62
- stdin: int | None = None) -> subprocess.CompletedProcess:
62
+ stdin: int | None = None,
63
+ new_session: bool = False) -> subprocess.CompletedProcess:
63
64
  """Capture a subprocess. A timeout surfaces as a failure, not an exception,
64
65
  so callers keep their plain returncode checks."""
65
66
  try:
66
67
  return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd,
67
- timeout=timeout, stdin=stdin)
68
+ timeout=timeout, stdin=stdin,
69
+ start_new_session=new_session)
68
70
  except subprocess.TimeoutExpired:
69
71
  return subprocess.CompletedProcess(cmd, 124, "", f"timed out after {timeout}s")
70
72
 
@@ -773,10 +775,13 @@ def shell_env() -> dict[str, str]:
773
775
  code = ("import json,os,sys;"
774
776
  f"sys.stdout.write({marker!r} + json.dumps(dict(os.environ)))")
775
777
  probe = f"{shlex.quote(sys.executable)} -c {shlex.quote(code)}"
776
- # DEVNULL because an interactive shell sharing our tty would fight the
777
- # agent for it, and lose to SIGTTOU rather than to the timeout.
778
+ # A session of its own, so the probe has no controlling tty to touch. An
779
+ # interactive zsh opens /dev/tty whatever stdin is and makes its own process
780
+ # group the foreground one, then execs the probe rather than forking it, so
781
+ # nothing is left to hand the terminal back. luv and the agent it execs would
782
+ # be in the background, and the agent's first tcsetattr gets SIGTTOU.
778
783
  r = run([shell, "-lic", probe], timeout=SHELL_ENV_TIMEOUT,
779
- stdin=subprocess.DEVNULL)
784
+ stdin=subprocess.DEVNULL, new_session=True)
780
785
  # Match on the marker, not the exit status: rc files greet, warn, and fail
781
786
  # in ways that say nothing about whether the environment came back. What
782
787
  # precedes the marker is the greeting; raw_decode drops anything an exit
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "luv-cli"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  description = "Launch Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments"
9
9
  requires-python = ">=3.10"
10
10
  license = "MIT"
@@ -1,7 +1,10 @@
1
1
  import contextlib
2
2
  import io
3
3
  import json
4
+ import os
5
+ import pty
4
6
  import re
7
+ import shutil
5
8
  import subprocess
6
9
  import tempfile
7
10
  import time
@@ -880,8 +883,33 @@ class ShellEnvTests(unittest.TestCase):
880
883
  self.assertEqual(env, {"API_BASE": "https://x"})
881
884
  self.assertEqual(call.args[0][:2], ["/bin/zsh", "-lic"])
882
885
  self.assertEqual(call.kwargs["stdin"], subprocess.DEVNULL)
886
+ self.assertTrue(call.kwargs["new_session"])
883
887
  self.assertEqual(call.kwargs["timeout"], luv.SHELL_ENV_TIMEOUT)
884
888
 
889
+ @unittest.skipUnless(shutil.which("zsh"), "needs zsh")
890
+ def test_probe_leaves_the_terminal_in_the_foreground_of_the_caller(self):
891
+ # A real zsh on a real pty, because only that shows the failure: zsh
892
+ # takes the foreground and execs the probe, so the caller, and the agent
893
+ # it later execs, would be stopped by SIGTTOU on touching the terminal.
894
+ with tempfile.TemporaryDirectory() as zdotdir:
895
+ pid, fd = pty.fork()
896
+ if pid == 0:
897
+ status = 1
898
+ try:
899
+ with patch.dict(luv.os.environ,
900
+ {"SHELL": "zsh", "ZDOTDIR": zdotdir}):
901
+ luv.shell_env()
902
+ status = 0 if os.tcgetpgrp(0) == os.getpgrp() else 1
903
+ finally:
904
+ os._exit(status)
905
+ with contextlib.suppress(OSError):
906
+ while os.read(fd, 4096):
907
+ pass
908
+ os.close(fd)
909
+ _, status = os.waitpid(pid, 0)
910
+
911
+ self.assertEqual(os.waitstatus_to_exitcode(status), 0)
912
+
885
913
  def test_greeting_before_the_marker_is_ignored(self):
886
914
  env, _ = self._probe(_shell_env_output({"A": "1"}, before="welcome back!\n"))
887
915
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes