luv-cli 0.0.2__tar.gz → 0.0.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.
- luv_cli-0.0.3/.claude/settings.json +316 -0
- luv_cli-0.0.3/.failproofai/policies-config.json +39 -0
- luv_cli-0.0.3/.gitignore +4 -0
- {luv_cli-0.0.2 → luv_cli-0.0.3}/PKG-INFO +2 -1
- {luv_cli-0.0.2 → luv_cli-0.0.3}/README.md +1 -0
- {luv_cli-0.0.2 → luv_cli-0.0.3}/luv/__init__.py +52 -29
- {luv_cli-0.0.2 → luv_cli-0.0.3}/pyproject.toml +1 -1
- {luv_cli-0.0.2 → luv_cli-0.0.3}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.2 → luv_cli-0.0.3}/LICENSE +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SessionStart",
|
|
9
|
+
"timeout": 60000,
|
|
10
|
+
"__failproofai_hook__": true
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SessionEnd": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SessionEnd",
|
|
21
|
+
"timeout": 60000,
|
|
22
|
+
"__failproofai_hook__": true
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"UserPromptSubmit": [
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook UserPromptSubmit",
|
|
33
|
+
"timeout": 60000,
|
|
34
|
+
"__failproofai_hook__": true
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"PreToolUse": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PreToolUse",
|
|
45
|
+
"timeout": 60000,
|
|
46
|
+
"__failproofai_hook__": true
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"PermissionRequest": [
|
|
52
|
+
{
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PermissionRequest",
|
|
57
|
+
"timeout": 60000,
|
|
58
|
+
"__failproofai_hook__": true
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"PermissionDenied": [
|
|
64
|
+
{
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PermissionDenied",
|
|
69
|
+
"timeout": 60000,
|
|
70
|
+
"__failproofai_hook__": true
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"PostToolUse": [
|
|
76
|
+
{
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostToolUse",
|
|
81
|
+
"timeout": 60000,
|
|
82
|
+
"__failproofai_hook__": true
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"PostToolUseFailure": [
|
|
88
|
+
{
|
|
89
|
+
"hooks": [
|
|
90
|
+
{
|
|
91
|
+
"type": "command",
|
|
92
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostToolUseFailure",
|
|
93
|
+
"timeout": 60000,
|
|
94
|
+
"__failproofai_hook__": true
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"Notification": [
|
|
100
|
+
{
|
|
101
|
+
"hooks": [
|
|
102
|
+
{
|
|
103
|
+
"type": "command",
|
|
104
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Notification",
|
|
105
|
+
"timeout": 60000,
|
|
106
|
+
"__failproofai_hook__": true
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"SubagentStart": [
|
|
112
|
+
{
|
|
113
|
+
"hooks": [
|
|
114
|
+
{
|
|
115
|
+
"type": "command",
|
|
116
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SubagentStart",
|
|
117
|
+
"timeout": 60000,
|
|
118
|
+
"__failproofai_hook__": true
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"SubagentStop": [
|
|
124
|
+
{
|
|
125
|
+
"hooks": [
|
|
126
|
+
{
|
|
127
|
+
"type": "command",
|
|
128
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook SubagentStop",
|
|
129
|
+
"timeout": 60000,
|
|
130
|
+
"__failproofai_hook__": true
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"TaskCreated": [
|
|
136
|
+
{
|
|
137
|
+
"hooks": [
|
|
138
|
+
{
|
|
139
|
+
"type": "command",
|
|
140
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TaskCreated",
|
|
141
|
+
"timeout": 60000,
|
|
142
|
+
"__failproofai_hook__": true
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"TaskCompleted": [
|
|
148
|
+
{
|
|
149
|
+
"hooks": [
|
|
150
|
+
{
|
|
151
|
+
"type": "command",
|
|
152
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TaskCompleted",
|
|
153
|
+
"timeout": 60000,
|
|
154
|
+
"__failproofai_hook__": true
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"Stop": [
|
|
160
|
+
{
|
|
161
|
+
"hooks": [
|
|
162
|
+
{
|
|
163
|
+
"type": "command",
|
|
164
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Stop",
|
|
165
|
+
"timeout": 60000,
|
|
166
|
+
"__failproofai_hook__": true
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"StopFailure": [
|
|
172
|
+
{
|
|
173
|
+
"hooks": [
|
|
174
|
+
{
|
|
175
|
+
"type": "command",
|
|
176
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook StopFailure",
|
|
177
|
+
"timeout": 60000,
|
|
178
|
+
"__failproofai_hook__": true
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"TeammateIdle": [
|
|
184
|
+
{
|
|
185
|
+
"hooks": [
|
|
186
|
+
{
|
|
187
|
+
"type": "command",
|
|
188
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook TeammateIdle",
|
|
189
|
+
"timeout": 60000,
|
|
190
|
+
"__failproofai_hook__": true
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"InstructionsLoaded": [
|
|
196
|
+
{
|
|
197
|
+
"hooks": [
|
|
198
|
+
{
|
|
199
|
+
"type": "command",
|
|
200
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook InstructionsLoaded",
|
|
201
|
+
"timeout": 60000,
|
|
202
|
+
"__failproofai_hook__": true
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"ConfigChange": [
|
|
208
|
+
{
|
|
209
|
+
"hooks": [
|
|
210
|
+
{
|
|
211
|
+
"type": "command",
|
|
212
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook ConfigChange",
|
|
213
|
+
"timeout": 60000,
|
|
214
|
+
"__failproofai_hook__": true
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"CwdChanged": [
|
|
220
|
+
{
|
|
221
|
+
"hooks": [
|
|
222
|
+
{
|
|
223
|
+
"type": "command",
|
|
224
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook CwdChanged",
|
|
225
|
+
"timeout": 60000,
|
|
226
|
+
"__failproofai_hook__": true
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
"FileChanged": [
|
|
232
|
+
{
|
|
233
|
+
"hooks": [
|
|
234
|
+
{
|
|
235
|
+
"type": "command",
|
|
236
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook FileChanged",
|
|
237
|
+
"timeout": 60000,
|
|
238
|
+
"__failproofai_hook__": true
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"WorktreeCreate": [
|
|
244
|
+
{
|
|
245
|
+
"hooks": [
|
|
246
|
+
{
|
|
247
|
+
"type": "command",
|
|
248
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook WorktreeCreate",
|
|
249
|
+
"timeout": 60000,
|
|
250
|
+
"__failproofai_hook__": true
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"WorktreeRemove": [
|
|
256
|
+
{
|
|
257
|
+
"hooks": [
|
|
258
|
+
{
|
|
259
|
+
"type": "command",
|
|
260
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook WorktreeRemove",
|
|
261
|
+
"timeout": 60000,
|
|
262
|
+
"__failproofai_hook__": true
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
"PreCompact": [
|
|
268
|
+
{
|
|
269
|
+
"hooks": [
|
|
270
|
+
{
|
|
271
|
+
"type": "command",
|
|
272
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PreCompact",
|
|
273
|
+
"timeout": 60000,
|
|
274
|
+
"__failproofai_hook__": true
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
"PostCompact": [
|
|
280
|
+
{
|
|
281
|
+
"hooks": [
|
|
282
|
+
{
|
|
283
|
+
"type": "command",
|
|
284
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook PostCompact",
|
|
285
|
+
"timeout": 60000,
|
|
286
|
+
"__failproofai_hook__": true
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"Elicitation": [
|
|
292
|
+
{
|
|
293
|
+
"hooks": [
|
|
294
|
+
{
|
|
295
|
+
"type": "command",
|
|
296
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook Elicitation",
|
|
297
|
+
"timeout": 60000,
|
|
298
|
+
"__failproofai_hook__": true
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"ElicitationResult": [
|
|
304
|
+
{
|
|
305
|
+
"hooks": [
|
|
306
|
+
{
|
|
307
|
+
"type": "command",
|
|
308
|
+
"command": "\"/home/ubuntu/.nvm/versions/node/v24.14.0/bin/failproofai\" --hook ElicitationResult",
|
|
309
|
+
"timeout": 60000,
|
|
310
|
+
"__failproofai_hook__": true
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabledPolicies": [
|
|
3
|
+
"sanitize-jwt",
|
|
4
|
+
"sanitize-api-keys",
|
|
5
|
+
"sanitize-connection-strings",
|
|
6
|
+
"sanitize-private-key-content",
|
|
7
|
+
"sanitize-bearer-tokens",
|
|
8
|
+
"protect-env-vars",
|
|
9
|
+
"block-env-files",
|
|
10
|
+
"block-read-outside-cwd",
|
|
11
|
+
"block-sudo",
|
|
12
|
+
"block-curl-pipe-sh",
|
|
13
|
+
"block-rm-rf",
|
|
14
|
+
"block-failproofai-commands",
|
|
15
|
+
"block-secrets-write",
|
|
16
|
+
"block-push-master",
|
|
17
|
+
"block-force-push",
|
|
18
|
+
"block-work-on-main",
|
|
19
|
+
"warn-git-amend",
|
|
20
|
+
"warn-git-stash-drop",
|
|
21
|
+
"warn-all-files-staged",
|
|
22
|
+
"warn-destructive-sql",
|
|
23
|
+
"warn-schema-alteration",
|
|
24
|
+
"warn-package-publish",
|
|
25
|
+
"warn-global-package-install",
|
|
26
|
+
"warn-large-file-write",
|
|
27
|
+
"warn-background-process",
|
|
28
|
+
"warn-repeated-tool-calls",
|
|
29
|
+
"require-commit-before-stop",
|
|
30
|
+
"require-push-before-stop",
|
|
31
|
+
"require-pr-before-stop",
|
|
32
|
+
"require-ci-green-before-stop"
|
|
33
|
+
],
|
|
34
|
+
"policyParams": {
|
|
35
|
+
"block-force-push": {
|
|
36
|
+
"hint": "Create a new branch from your current HEAD (e.g. `git checkout -b <new-branch>`) and push that instead."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
luv_cli-0.0.3/.gitignore
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Launch Claude Code 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
|
|
@@ -94,6 +94,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
|
|
|
94
94
|
|------|-------------|
|
|
95
95
|
| `-n` | Navigate: open a shell instead of Claude |
|
|
96
96
|
| `-r` | Resume: resume the last Claude session |
|
|
97
|
+
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
97
98
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
98
99
|
|
|
99
100
|
## Docker dev environments
|
|
@@ -71,6 +71,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
|
|
|
71
71
|
|------|-------------|
|
|
72
72
|
| `-n` | Navigate: open a shell instead of Claude |
|
|
73
73
|
| `-r` | Resume: resume the last Claude session |
|
|
74
|
+
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
74
75
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
75
76
|
|
|
76
77
|
## Docker dev environments
|
|
@@ -119,6 +119,23 @@ def trust_project(path: Path) -> None:
|
|
|
119
119
|
os.replace(tmp_path, CLAUDE_JSON)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def collect_luv_env() -> dict[str, str]:
|
|
123
|
+
"""Collect LUV_* env vars, strip prefix, return as dict."""
|
|
124
|
+
result = {}
|
|
125
|
+
for key, value in os.environ.items():
|
|
126
|
+
if key.startswith("LUV_") and len(key) > 4:
|
|
127
|
+
result[key[4:]] = value
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def docker_env_flags(env_vars: dict[str, str]) -> list[str]:
|
|
132
|
+
"""Convert env dict to docker compose exec -e flags."""
|
|
133
|
+
flags: list[str] = []
|
|
134
|
+
for key, value in env_vars.items():
|
|
135
|
+
flags.extend(["-e", f"{key}={value}"])
|
|
136
|
+
return flags
|
|
137
|
+
|
|
138
|
+
|
|
122
139
|
def ensure_pr_rules() -> None:
|
|
123
140
|
claude_dir = Path.home() / ".claude"
|
|
124
141
|
claude_md = claude_dir / "CLAUDE.md"
|
|
@@ -229,7 +246,7 @@ def stop_docker(clone_dir: Path, compose_file: str, project: str) -> None:
|
|
|
229
246
|
subprocess.run(base + ["down", "-v", "--remove-orphans"])
|
|
230
247
|
|
|
231
248
|
|
|
232
|
-
def navigate(clone_dir: Path) -> None:
|
|
249
|
+
def navigate(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
233
250
|
"""Chdir into the work folder and exec a shell — replacing this process."""
|
|
234
251
|
os.chdir(str(clone_dir))
|
|
235
252
|
settings = load_luv_settings(clone_dir)
|
|
@@ -240,16 +257,17 @@ def navigate(clone_dir: Path) -> None:
|
|
|
240
257
|
start_docker(clone_dir, compose_file, project)
|
|
241
258
|
try:
|
|
242
259
|
base = docker_compose_base(clone_dir, compose_file, project)
|
|
243
|
-
r = subprocess.run(base + ["exec", "-it"
|
|
260
|
+
r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment", "bash"])
|
|
244
261
|
sys.exit(r.returncode)
|
|
245
262
|
finally:
|
|
246
263
|
stop_docker(clone_dir, compose_file, project)
|
|
247
264
|
else:
|
|
248
265
|
shell = os.environ.get("SHELL", "/bin/bash")
|
|
266
|
+
os.environ.update(extra_env)
|
|
249
267
|
os.execv(shell, [shell])
|
|
250
268
|
|
|
251
269
|
|
|
252
|
-
def resume(clone_dir: Path) -> None:
|
|
270
|
+
def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
253
271
|
"""Trust, chdir, and exec claude --resume — replacing this process."""
|
|
254
272
|
trust_project(clone_dir)
|
|
255
273
|
os.chdir(str(clone_dir))
|
|
@@ -261,7 +279,7 @@ def resume(clone_dir: Path) -> None:
|
|
|
261
279
|
start_docker(clone_dir, compose_file, project)
|
|
262
280
|
try:
|
|
263
281
|
base = docker_compose_base(clone_dir, compose_file, project)
|
|
264
|
-
r = subprocess.run(base + ["exec", "-it"
|
|
282
|
+
r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment",
|
|
265
283
|
"claude", "--dangerously-skip-permissions",
|
|
266
284
|
"--model", "claude-opus-4-6",
|
|
267
285
|
"--effort", "max", "--resume"])
|
|
@@ -272,11 +290,12 @@ def resume(clone_dir: Path) -> None:
|
|
|
272
290
|
claude_bin = shutil.which("claude")
|
|
273
291
|
if not claude_bin:
|
|
274
292
|
die("'claude' not found in PATH")
|
|
293
|
+
os.environ.update(extra_env)
|
|
275
294
|
os.execv(claude_bin, [claude_bin, "--dangerously-skip-permissions",
|
|
276
295
|
"--model", "claude-opus-4-6", "--effort", "max", "--resume"])
|
|
277
296
|
|
|
278
297
|
|
|
279
|
-
def launch(clone_dir: Path, prompt: str | None) -> None:
|
|
298
|
+
def launch(clone_dir: Path, prompt: str | None, extra_env: dict[str, str] = {}) -> None:
|
|
280
299
|
"""Trust, resolve claude, chdir, and exec — replacing this process."""
|
|
281
300
|
trust_project(clone_dir)
|
|
282
301
|
os.chdir(str(clone_dir))
|
|
@@ -293,7 +312,7 @@ def launch(clone_dir: Path, prompt: str | None) -> None:
|
|
|
293
312
|
"--model", "claude-opus-4-6", "--effort", "max"]
|
|
294
313
|
if prompt:
|
|
295
314
|
claude_cmd.append(f"/plan {prompt}")
|
|
296
|
-
r = subprocess.run(base + ["exec", "-it"
|
|
315
|
+
r = subprocess.run(base + ["exec", "-it"] + docker_env_flags(extra_env) + ["dev-environment"] + claude_cmd)
|
|
297
316
|
sys.exit(r.returncode)
|
|
298
317
|
finally:
|
|
299
318
|
stop_docker(clone_dir, compose_file, project)
|
|
@@ -304,6 +323,7 @@ def launch(clone_dir: Path, prompt: str | None) -> None:
|
|
|
304
323
|
base_args = [claude_bin, "--dangerously-skip-permissions",
|
|
305
324
|
"--permission-mode", "bypassPermissions",
|
|
306
325
|
"--model", "claude-opus-4-6", "--effort", "max"]
|
|
326
|
+
os.environ.update(extra_env)
|
|
307
327
|
if prompt:
|
|
308
328
|
os.execv(claude_bin, base_args + [f"/plan {prompt}"])
|
|
309
329
|
else:
|
|
@@ -415,7 +435,7 @@ def find_latest_clone(repo: str) -> Path | None:
|
|
|
415
435
|
return best
|
|
416
436
|
|
|
417
437
|
|
|
418
|
-
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False) -> None:
|
|
438
|
+
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, extra_env: dict[str, str] = {}) -> None:
|
|
419
439
|
"""Open an existing work folder or remote branch by number."""
|
|
420
440
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
421
441
|
|
|
@@ -424,11 +444,11 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
424
444
|
print(f"luv: opening existing folder {clone_dir.name}")
|
|
425
445
|
ensure_pr_rules()
|
|
426
446
|
if nav_mode:
|
|
427
|
-
navigate(clone_dir)
|
|
447
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
428
448
|
elif resume_mode:
|
|
429
|
-
resume(clone_dir)
|
|
449
|
+
resume(clone_dir, extra_env=extra_env)
|
|
430
450
|
else:
|
|
431
|
-
launch(clone_dir, prompt)
|
|
451
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
432
452
|
return # unreachable
|
|
433
453
|
|
|
434
454
|
# 2. Check remote branch luv-{number}
|
|
@@ -451,14 +471,14 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
451
471
|
print(f"luv: ready — {clone_dir.name}, branch {branch}")
|
|
452
472
|
ensure_pr_rules()
|
|
453
473
|
if nav_mode:
|
|
454
|
-
navigate(clone_dir)
|
|
474
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
455
475
|
elif resume_mode:
|
|
456
|
-
resume(clone_dir)
|
|
476
|
+
resume(clone_dir, extra_env=extra_env)
|
|
457
477
|
else:
|
|
458
|
-
launch(clone_dir, prompt)
|
|
478
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
459
479
|
|
|
460
480
|
|
|
461
|
-
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False) -> None:
|
|
481
|
+
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, extra_env: dict[str, str] = {}) -> None:
|
|
462
482
|
"""Open any GitHub PR by org/repo/number, cloning if needed."""
|
|
463
483
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
464
484
|
|
|
@@ -466,11 +486,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
466
486
|
print(f"luv: opening existing folder {clone_dir.name}")
|
|
467
487
|
ensure_pr_rules()
|
|
468
488
|
if nav_mode:
|
|
469
|
-
navigate(clone_dir)
|
|
489
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
470
490
|
elif resume_mode:
|
|
471
|
-
resume(clone_dir)
|
|
491
|
+
resume(clone_dir, extra_env=extra_env)
|
|
472
492
|
else:
|
|
473
|
-
launch(clone_dir, prompt)
|
|
493
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
474
494
|
return # unreachable
|
|
475
495
|
|
|
476
496
|
# Resolve the actual branch name via GitHub API
|
|
@@ -493,11 +513,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
493
513
|
print(f"luv: ready — {clone_dir.name}, branch {branch}")
|
|
494
514
|
ensure_pr_rules()
|
|
495
515
|
if nav_mode:
|
|
496
|
-
navigate(clone_dir)
|
|
516
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
497
517
|
elif resume_mode:
|
|
498
|
-
resume(clone_dir)
|
|
518
|
+
resume(clone_dir, extra_env=extra_env)
|
|
499
519
|
else:
|
|
500
|
-
launch(clone_dir, prompt)
|
|
520
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
501
521
|
|
|
502
522
|
|
|
503
523
|
def main() -> None:
|
|
@@ -506,7 +526,9 @@ def main() -> None:
|
|
|
506
526
|
nav_mode = "-n" in args
|
|
507
527
|
resume_mode = "-r" in args
|
|
508
528
|
force = "-f" in args or "--force" in args
|
|
509
|
-
|
|
529
|
+
env_mode = "-e" in args
|
|
530
|
+
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force")]
|
|
531
|
+
extra_env = collect_luv_env() if env_mode else {}
|
|
510
532
|
|
|
511
533
|
if not args or args[0] in ("-h", "--help"):
|
|
512
534
|
print("""\
|
|
@@ -515,6 +537,7 @@ Usage: luv [flags] <command>
|
|
|
515
537
|
Flags:
|
|
516
538
|
-n navigate: open a shell in the work folder instead of launching Claude
|
|
517
539
|
-r resume: resume the last Claude session in the work folder
|
|
540
|
+
-e env: pass LUV_* environment variables (with prefix stripped) into the session
|
|
518
541
|
-f, --force (with --clean) skip safety checks and delete all work folders
|
|
519
542
|
|
|
520
543
|
Commands:
|
|
@@ -555,7 +578,7 @@ Docker:
|
|
|
555
578
|
die(f"cannot parse PR URL: {url}")
|
|
556
579
|
org, repo, number = m.group(1), m.group(2), int(m.group(3))
|
|
557
580
|
prompt = " ".join(args[2:]) or None
|
|
558
|
-
open_pr(org, repo, number, prompt, nav_mode, resume_mode)
|
|
581
|
+
open_pr(org, repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
559
582
|
return
|
|
560
583
|
|
|
561
584
|
raw = args[0].rstrip("/")
|
|
@@ -575,14 +598,14 @@ Docker:
|
|
|
575
598
|
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
|
|
576
599
|
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
|
|
577
600
|
prompt = " ".join(prompt_parts) or None
|
|
578
|
-
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode)
|
|
601
|
+
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
579
602
|
return
|
|
580
603
|
|
|
581
604
|
# Detect optional numeric second argument
|
|
582
605
|
if len(args) > 1 and args[1].isdigit():
|
|
583
606
|
number = int(args[1])
|
|
584
607
|
prompt = " ".join(args[2:]) or None
|
|
585
|
-
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode)
|
|
608
|
+
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
586
609
|
return
|
|
587
610
|
|
|
588
611
|
org = resolve_org(explicit_org)
|
|
@@ -595,9 +618,9 @@ Docker:
|
|
|
595
618
|
die(f"no local clones of '{repo}' found in {PRS_DIR}")
|
|
596
619
|
print(f"luv: opening latest clone {clone_dir.name}")
|
|
597
620
|
if nav_mode:
|
|
598
|
-
navigate(clone_dir)
|
|
621
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
599
622
|
else:
|
|
600
|
-
resume(clone_dir)
|
|
623
|
+
resume(clone_dir, extra_env=extra_env)
|
|
601
624
|
return
|
|
602
625
|
|
|
603
626
|
# 1. Verify repo exists
|
|
@@ -641,8 +664,8 @@ Docker:
|
|
|
641
664
|
|
|
642
665
|
# 7. Launch claude, resume session, or open shell (replace this process)
|
|
643
666
|
if nav_mode:
|
|
644
|
-
navigate(clone_dir)
|
|
667
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
645
668
|
elif resume_mode:
|
|
646
|
-
resume(clone_dir)
|
|
669
|
+
resume(clone_dir, extra_env=extra_env)
|
|
647
670
|
else:
|
|
648
|
-
launch(clone_dir, prompt)
|
|
671
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "luv-cli"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
description = "Launch Claude Code agents on GitHub repos with isolated workspaces and optional Docker dev environments"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|