luv-cli 0.5.1__tar.gz → 0.6.0__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.
- {luv_cli-0.5.1 → luv_cli-0.6.0}/PKG-INFO +27 -1
- {luv_cli-0.5.1 → luv_cli-0.6.0}/README.md +26 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/docs/configuration.md +29 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/docs/remote-sessions.md +13 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/luv/__init__.py +86 -3
- {luv_cli-0.5.1 → luv_cli-0.6.0}/pyproject.toml +1 -1
- {luv_cli-0.5.1 → luv_cli-0.6.0}/tests/test_agents.py +96 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/.claude/settings.json +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/.failproofai/policies/version-policies.mjs +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/.gitignore +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/LICENSE +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/claude-output.txt +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/docs/sessions.md +0 -0
- {luv_cli-0.5.1 → luv_cli-0.6.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -169,6 +169,32 @@ Handover flags:
|
|
|
169
169
|
| `--purge` | Delete the source folder once the copy is verified |
|
|
170
170
|
| `-y` | Skip the "an agent may still be running" confirmation |
|
|
171
171
|
|
|
172
|
+
## The session's environment
|
|
173
|
+
|
|
174
|
+
A session luv starts for you — a remote dispatch, a handover, a detached start
|
|
175
|
+
— is exec'd by tmux or ssh with no shell in between, so it never sources
|
|
176
|
+
`~/.bashrc` or `~/.zshrc` and would otherwise run with whatever environment the
|
|
177
|
+
tmux server happened to start with. luv asks your login shell (`$SHELL -lic`)
|
|
178
|
+
for its environment and fills in the gaps before cloning or launching anything,
|
|
179
|
+
so the agent gets the API keys and the `PATH` you'd have in a terminal there.
|
|
180
|
+
|
|
181
|
+
Precedence, highest first:
|
|
182
|
+
|
|
183
|
+
1. Anything already set — `FOO=bar luv …`, or `-e` — is a decision about *this*
|
|
184
|
+
session and is never overwritten.
|
|
185
|
+
2. Your rc's exports fill in the rest. `PATH` is merged instead of replaced:
|
|
186
|
+
entries the rc adds (nvm, pyenv, `~/.local/bin`) are appended after the ones
|
|
187
|
+
already there.
|
|
188
|
+
|
|
189
|
+
The probe runs on the machine the session runs on, so a remote session picks up
|
|
190
|
+
the *remote* user's rc. It is skipped when you run `luv` yourself — that
|
|
191
|
+
process already inherited your shell — and can be turned off entirely with
|
|
192
|
+
`luv config set shell_env false`. An rc that hangs is given 15 seconds and then
|
|
193
|
+
ignored.
|
|
194
|
+
|
|
195
|
+
Docker sessions are the exception: the container has its own environment, and
|
|
196
|
+
only `-e` crosses into it (see below).
|
|
197
|
+
|
|
172
198
|
## Docker dev environments
|
|
173
199
|
|
|
174
200
|
If a repo contains `.luv/settings.json` with a `compose_file` key, `luv` automatically starts a Docker Compose environment and runs Claude inside the `dev-environment` container.
|
|
@@ -146,6 +146,32 @@ Handover flags:
|
|
|
146
146
|
| `--purge` | Delete the source folder once the copy is verified |
|
|
147
147
|
| `-y` | Skip the "an agent may still be running" confirmation |
|
|
148
148
|
|
|
149
|
+
## The session's environment
|
|
150
|
+
|
|
151
|
+
A session luv starts for you — a remote dispatch, a handover, a detached start
|
|
152
|
+
— is exec'd by tmux or ssh with no shell in between, so it never sources
|
|
153
|
+
`~/.bashrc` or `~/.zshrc` and would otherwise run with whatever environment the
|
|
154
|
+
tmux server happened to start with. luv asks your login shell (`$SHELL -lic`)
|
|
155
|
+
for its environment and fills in the gaps before cloning or launching anything,
|
|
156
|
+
so the agent gets the API keys and the `PATH` you'd have in a terminal there.
|
|
157
|
+
|
|
158
|
+
Precedence, highest first:
|
|
159
|
+
|
|
160
|
+
1. Anything already set — `FOO=bar luv …`, or `-e` — is a decision about *this*
|
|
161
|
+
session and is never overwritten.
|
|
162
|
+
2. Your rc's exports fill in the rest. `PATH` is merged instead of replaced:
|
|
163
|
+
entries the rc adds (nvm, pyenv, `~/.local/bin`) are appended after the ones
|
|
164
|
+
already there.
|
|
165
|
+
|
|
166
|
+
The probe runs on the machine the session runs on, so a remote session picks up
|
|
167
|
+
the *remote* user's rc. It is skipped when you run `luv` yourself — that
|
|
168
|
+
process already inherited your shell — and can be turned off entirely with
|
|
169
|
+
`luv config set shell_env false`. An rc that hangs is given 15 seconds and then
|
|
170
|
+
ignored.
|
|
171
|
+
|
|
172
|
+
Docker sessions are the exception: the container has its own environment, and
|
|
173
|
+
only `-e` crosses into it (see below).
|
|
174
|
+
|
|
149
175
|
## Docker dev environments
|
|
150
176
|
|
|
151
177
|
If a repo contains `.luv/settings.json` with a `compose_file` key, `luv` automatically starts a Docker Compose environment and runs Claude inside the `dev-environment` container.
|
|
@@ -70,6 +70,35 @@ pushed. Workspaces created before slugs existed keep working.
|
|
|
70
70
|
*string, default `~/prs`.* Where workspace folders are created on **this**
|
|
71
71
|
machine. `~` is expanded.
|
|
72
72
|
|
|
73
|
+
### `shell_env`
|
|
74
|
+
|
|
75
|
+
*boolean, default `true`.* Whether a session luv starts should inherit your
|
|
76
|
+
shell's environment.
|
|
77
|
+
|
|
78
|
+
tmux and ssh exec their command directly, so a remote dispatch, a handover, or
|
|
79
|
+
a detached start never sources `~/.bashrc` or `~/.zshrc` — it runs with the
|
|
80
|
+
environment the tmux server was started with, frozen at whenever that server
|
|
81
|
+
first came up. Anything you export from your rc (the API key an agent
|
|
82
|
+
authenticates with, the `PATH` entry that makes `codex` resolvable) is simply
|
|
83
|
+
absent, and only for the sessions luv started for you.
|
|
84
|
+
|
|
85
|
+
With this on, luv runs `$SHELL -lic` on the machine the session runs on and
|
|
86
|
+
fills in what's missing before it clones or launches anything. Values already
|
|
87
|
+
set win, so `FOO=bar luv …` and `-e` still decide; `PATH` is merged rather than
|
|
88
|
+
replaced, with the rc's entries appended. An rc that never returns is abandoned
|
|
89
|
+
after 15 seconds.
|
|
90
|
+
|
|
91
|
+
Set it to `false` if your rc is slow, or if you want sessions to see only what
|
|
92
|
+
you pass them explicitly:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
luv config set shell_env false
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is read on the machine the session runs on, so a remote host can opt out
|
|
99
|
+
independently of your laptop. It does not apply to Docker sessions — the
|
|
100
|
+
container has its own environment, and only `-e` crosses into it.
|
|
101
|
+
|
|
73
102
|
### `remote.host`
|
|
74
103
|
|
|
75
104
|
*string.* SSH destination — an `~/.ssh/config` alias (`box`), a `user@host`, or
|
|
@@ -521,6 +521,19 @@ The key isn't installed or isn't being offered. Test with `ssh -v box`. Check
|
|
|
521
521
|
`gh` is authenticated on your laptop but not on the remote. luv numbers
|
|
522
522
|
workspaces with `gh api` *on the remote*, so it needs its own login there.
|
|
523
523
|
|
|
524
|
+
**The agent can't find an API key / a tool it needs is missing**
|
|
525
|
+
Sessions inherit your rc: before cloning or launching, luv asks the remote's
|
|
526
|
+
login shell (`$SHELL -lic`) for its environment and fills in whatever tmux
|
|
527
|
+
didn't pass on, `PATH` included. So this means the var isn't exported by the
|
|
528
|
+
*remote* user's `~/.bashrc`/`~/.zshrc` — check with
|
|
529
|
+
`ssh box 'zsh -lic "printenv | cut -d= -f1"'` — or that the rc takes longer than
|
|
530
|
+
15 seconds, or that `shell_env` is `false` there. `-e` with a `LUV_`-prefixed
|
|
531
|
+
variable forwards one from your laptop regardless. See
|
|
532
|
+
[configuration.md](configuration.md#shell_env).
|
|
533
|
+
|
|
534
|
+
Note that this happens *inside* the remote luv, so it can't help ssh find `luv`
|
|
535
|
+
itself — that's the `PATH` problem above.
|
|
536
|
+
|
|
524
537
|
**My prompt didn't get sent**
|
|
525
538
|
Reopening a workspace that already has a live session attaches to it, and tmux
|
|
526
539
|
discards the command — so the prompt is dropped. luv prints a note when this can
|
|
@@ -57,13 +57,13 @@ def die(msg: str) -> None:
|
|
|
57
57
|
sys.exit(1)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def run(cmd: list[str], *, cwd: str | None = None,
|
|
61
|
-
|
|
60
|
+
def run(cmd: list[str], *, cwd: str | None = None, timeout: float | None = None,
|
|
61
|
+
stdin: int | None = None) -> subprocess.CompletedProcess:
|
|
62
62
|
"""Capture a subprocess. A timeout surfaces as a failure, not an exception,
|
|
63
63
|
so callers keep their plain returncode checks."""
|
|
64
64
|
try:
|
|
65
65
|
return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd,
|
|
66
|
-
timeout=timeout)
|
|
66
|
+
timeout=timeout, stdin=stdin)
|
|
67
67
|
except subprocess.TimeoutExpired:
|
|
68
68
|
return subprocess.CompletedProcess(cmd, 124, "", f"timed out after {timeout}s")
|
|
69
69
|
|
|
@@ -743,6 +743,84 @@ def collect_luv_env() -> dict[str, str]:
|
|
|
743
743
|
return result
|
|
744
744
|
|
|
745
745
|
|
|
746
|
+
# An rc file is a program, and some of them never finish: one that starts tmux,
|
|
747
|
+
# or waits on a prompt, would otherwise hang every session start.
|
|
748
|
+
SHELL_ENV_TIMEOUT = 15
|
|
749
|
+
|
|
750
|
+
# Vars that describe the shell we just ran rather than the setup we asked it
|
|
751
|
+
# about. Importing them would tell the agent it is somewhere it is not.
|
|
752
|
+
SHELL_ENV_SKIP = {"_", "OLDPWD", "PWD", "SHLVL"}
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def shell_env() -> dict[str, str]:
|
|
756
|
+
"""Everything the user's login+interactive shell exports.
|
|
757
|
+
|
|
758
|
+
tmux and ssh exec their command directly, with no shell in between, so a
|
|
759
|
+
session luv started — a remote dispatch, a handover, a detached start —
|
|
760
|
+
never sources ~/.bashrc or ~/.zshrc. It runs with whatever environment the
|
|
761
|
+
tmux server was started with, frozen at whenever that server first came up.
|
|
762
|
+
The API key an agent authenticates with and the PATH entry that makes
|
|
763
|
+
'codex' resolvable both live in the rc, and both go missing.
|
|
764
|
+
|
|
765
|
+
Asking $SHELL for its own environment is the only way to get them back: rc
|
|
766
|
+
files are code, not data, and there is no reading them from outside. -l
|
|
767
|
+
covers the profile files, -i the rc files; between them they reach where
|
|
768
|
+
bash and zsh users actually put their exports.
|
|
769
|
+
"""
|
|
770
|
+
shell = os.environ.get("SHELL") or "/bin/bash"
|
|
771
|
+
marker = "__luv_env__"
|
|
772
|
+
code = ("import json,os,sys;"
|
|
773
|
+
f"sys.stdout.write({marker!r} + json.dumps(dict(os.environ)))")
|
|
774
|
+
probe = f"{shlex.quote(sys.executable)} -c {shlex.quote(code)}"
|
|
775
|
+
# DEVNULL because an interactive shell sharing our tty would fight the
|
|
776
|
+
# agent for it, and lose to SIGTTOU rather than to the timeout.
|
|
777
|
+
r = run([shell, "-lic", probe], timeout=SHELL_ENV_TIMEOUT,
|
|
778
|
+
stdin=subprocess.DEVNULL)
|
|
779
|
+
# Match on the marker, not the exit status: rc files greet, warn, and fail
|
|
780
|
+
# in ways that say nothing about whether the environment came back. What
|
|
781
|
+
# precedes the marker is the greeting; raw_decode drops anything an exit
|
|
782
|
+
# hook prints after it.
|
|
783
|
+
if marker not in r.stdout:
|
|
784
|
+
return {}
|
|
785
|
+
try:
|
|
786
|
+
env, _ = json.JSONDecoder().raw_decode(r.stdout.split(marker, 1)[1].lstrip())
|
|
787
|
+
except ValueError:
|
|
788
|
+
return {}
|
|
789
|
+
return {k: v for k, v in env.items() if isinstance(v, str)}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def merged_path(current: str, from_rc: str) -> str:
|
|
793
|
+
"""`current` first, then whatever the rc adds. PATH order is a precedence
|
|
794
|
+
list, so the binaries that got us this far keep winning."""
|
|
795
|
+
entries = [p for p in current.split(os.pathsep) if p]
|
|
796
|
+
entries += [p for p in from_rc.split(os.pathsep) if p and p not in entries]
|
|
797
|
+
return os.pathsep.join(entries)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def apply_shell_env() -> None:
|
|
801
|
+
"""Fill this process's environment in from the user's shell.
|
|
802
|
+
|
|
803
|
+
Only for the luv that tmux or ssh started (_LUV_INNER). A luv you ran
|
|
804
|
+
yourself already inherited the shell it ran in; re-deriving it there would
|
|
805
|
+
charge every session start for an rc that has already run.
|
|
806
|
+
|
|
807
|
+
What is already set wins. 'FOO=bar luv …' and -e are statements about this
|
|
808
|
+
session, while the rc is the background default, and a session that quietly
|
|
809
|
+
overrode the value you handed it would be worse than one missing it. PATH
|
|
810
|
+
is the exception, being a list rather than a value: the rc's entries are
|
|
811
|
+
appended, which is how nvm's node and ~/.local/bin become findable.
|
|
812
|
+
"""
|
|
813
|
+
if not os.environ.get("_LUV_INNER") or load_config().get("shell_env") is False:
|
|
814
|
+
return
|
|
815
|
+
for key, value in shell_env().items():
|
|
816
|
+
if key in SHELL_ENV_SKIP:
|
|
817
|
+
continue
|
|
818
|
+
if key == "PATH":
|
|
819
|
+
os.environ["PATH"] = merged_path(os.environ.get("PATH", ""), value)
|
|
820
|
+
elif key not in os.environ:
|
|
821
|
+
os.environ[key] = value
|
|
822
|
+
|
|
823
|
+
|
|
746
824
|
def docker_env_flags(env_vars: dict[str, str]) -> list[str]:
|
|
747
825
|
"""Convert env dict to docker compose exec -e flags."""
|
|
748
826
|
flags: list[str] = []
|
|
@@ -3457,6 +3535,11 @@ def main() -> None:
|
|
|
3457
3535
|
die("--local cannot be combined with -s or -i")
|
|
3458
3536
|
|
|
3459
3537
|
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit", "--safe", "--claude", "--codex", "--local", "--no-agent-state", "--no-attach", "--purge", "-y")]
|
|
3538
|
+
|
|
3539
|
+
# Before anything shells out: the clone needs gh and git on PATH as much as
|
|
3540
|
+
# the agent needs its credentials, and both come from the same rc. Ahead of
|
|
3541
|
+
# collect_luv_env() too, so a LUV_* exported there is one -e can forward.
|
|
3542
|
+
apply_shell_env()
|
|
3460
3543
|
extra_env = collect_luv_env() if env_mode else {}
|
|
3461
3544
|
|
|
3462
3545
|
# _LUV_INNER marks the remote-side luv: it must never dispatch onward.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "luv-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
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"
|
|
@@ -812,6 +812,102 @@ class ConfigTests(unittest.TestCase):
|
|
|
812
812
|
self.assertIn("ConnectTimeout=5", base)
|
|
813
813
|
|
|
814
814
|
|
|
815
|
+
def _shell_env_output(env, before="", after=""):
|
|
816
|
+
"""What the probe sees: an rc's greeting, the marker, the environment."""
|
|
817
|
+
return f"{before}__luv_env__{json.dumps(env)}{after}"
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class ShellEnvTests(unittest.TestCase):
|
|
821
|
+
"""tmux and ssh exec without a shell, so the rc has to be asked for."""
|
|
822
|
+
|
|
823
|
+
def _probe(self, stdout, returncode=0):
|
|
824
|
+
with (patch.dict(luv.os.environ, {"SHELL": "/bin/zsh"}, clear=True),
|
|
825
|
+
patch.object(luv, "run",
|
|
826
|
+
return_value=_completed(stdout, returncode)) as run):
|
|
827
|
+
return luv.shell_env(), run.call_args
|
|
828
|
+
|
|
829
|
+
def test_asks_the_users_own_shell_as_login_and_interactive(self):
|
|
830
|
+
env, call = self._probe(_shell_env_output({"API_BASE": "https://x"}))
|
|
831
|
+
|
|
832
|
+
self.assertEqual(env, {"API_BASE": "https://x"})
|
|
833
|
+
self.assertEqual(call.args[0][:2], ["/bin/zsh", "-lic"])
|
|
834
|
+
self.assertEqual(call.kwargs["stdin"], subprocess.DEVNULL)
|
|
835
|
+
self.assertEqual(call.kwargs["timeout"], luv.SHELL_ENV_TIMEOUT)
|
|
836
|
+
|
|
837
|
+
def test_greeting_before_the_marker_is_ignored(self):
|
|
838
|
+
env, _ = self._probe(_shell_env_output({"A": "1"}, before="welcome back!\n"))
|
|
839
|
+
|
|
840
|
+
self.assertEqual(env, {"A": "1"})
|
|
841
|
+
|
|
842
|
+
def test_exit_hook_after_the_environment_is_ignored(self):
|
|
843
|
+
env, _ = self._probe(_shell_env_output({"A": "1"}, after="\ngoodbye\n"))
|
|
844
|
+
|
|
845
|
+
self.assertEqual(env, {"A": "1"})
|
|
846
|
+
|
|
847
|
+
def test_failing_rc_still_yields_what_it_exported(self):
|
|
848
|
+
env, _ = self._probe(_shell_env_output({"A": "1"}), returncode=1)
|
|
849
|
+
|
|
850
|
+
self.assertEqual(env, {"A": "1"})
|
|
851
|
+
|
|
852
|
+
def test_a_shell_that_says_nothing_useful_is_not_an_error(self):
|
|
853
|
+
self.assertEqual(self._probe("")[0], {})
|
|
854
|
+
self.assertEqual(self._probe("__luv_env__not json")[0], {})
|
|
855
|
+
|
|
856
|
+
def test_timeout_is_not_an_error(self):
|
|
857
|
+
# run() turns a hung rc into returncode 124 rather than an exception.
|
|
858
|
+
self.assertEqual(self._probe("", returncode=124)[0], {})
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class ApplyShellEnvTests(unittest.TestCase):
|
|
862
|
+
def _apply(self, environ, rc, config=None):
|
|
863
|
+
with (patch.dict(luv.os.environ, environ, clear=True),
|
|
864
|
+
patch.object(luv, "load_config", return_value=config or {}),
|
|
865
|
+
patch.object(luv, "shell_env", return_value=rc) as probe):
|
|
866
|
+
luv.apply_shell_env()
|
|
867
|
+
return dict(luv.os.environ), probe.called
|
|
868
|
+
|
|
869
|
+
def test_only_runs_for_the_luv_tmux_or_ssh_started(self):
|
|
870
|
+
env, probed = self._apply({}, {"API_KEY": "from-rc"})
|
|
871
|
+
|
|
872
|
+
self.assertFalse(probed)
|
|
873
|
+
self.assertNotIn("API_KEY", env)
|
|
874
|
+
|
|
875
|
+
def test_fills_in_what_the_session_never_sourced(self):
|
|
876
|
+
env, _ = self._apply({"_LUV_INNER": "1"}, {"API_KEY": "from-rc"})
|
|
877
|
+
|
|
878
|
+
self.assertEqual(env["API_KEY"], "from-rc")
|
|
879
|
+
|
|
880
|
+
def test_what_the_caller_set_wins(self):
|
|
881
|
+
env, _ = self._apply({"_LUV_INNER": "1", "API_KEY": "explicit"},
|
|
882
|
+
{"API_KEY": "from-rc", "OTHER": "from-rc"})
|
|
883
|
+
|
|
884
|
+
self.assertEqual(env["API_KEY"], "explicit")
|
|
885
|
+
self.assertEqual(env["OTHER"], "from-rc")
|
|
886
|
+
|
|
887
|
+
def test_path_is_merged_rather_than_replaced(self):
|
|
888
|
+
env, _ = self._apply({"_LUV_INNER": "1", "PATH": "/usr/bin:/bin"},
|
|
889
|
+
{"PATH": "/home/u/.nvm/bin:/usr/bin:/home/u/.local/bin"})
|
|
890
|
+
|
|
891
|
+
self.assertEqual(env["PATH"],
|
|
892
|
+
"/usr/bin:/bin:/home/u/.nvm/bin:/home/u/.local/bin")
|
|
893
|
+
|
|
894
|
+
def test_the_probe_shells_own_bookkeeping_is_left_behind(self):
|
|
895
|
+
env, _ = self._apply({"_LUV_INNER": "1"},
|
|
896
|
+
{"PWD": "/home/u", "SHLVL": "3", "_": "/usr/bin/env",
|
|
897
|
+
"OLDPWD": "/tmp", "KEEP": "yes"})
|
|
898
|
+
|
|
899
|
+
self.assertEqual(env["KEEP"], "yes")
|
|
900
|
+
for key in ("PWD", "SHLVL", "_", "OLDPWD"):
|
|
901
|
+
self.assertNotIn(key, env)
|
|
902
|
+
|
|
903
|
+
def test_config_can_turn_it_off(self):
|
|
904
|
+
env, probed = self._apply({"_LUV_INNER": "1"}, {"API_KEY": "from-rc"},
|
|
905
|
+
config={"shell_env": False})
|
|
906
|
+
|
|
907
|
+
self.assertFalse(probed)
|
|
908
|
+
self.assertNotIn("API_KEY", env)
|
|
909
|
+
|
|
910
|
+
|
|
815
911
|
class CleanGuardTests(unittest.TestCase):
|
|
816
912
|
def test_live_session_folder_is_skipped(self):
|
|
817
913
|
tempdir = tempfile.TemporaryDirectory()
|
|
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
|