luv-cli 0.0.1__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.1 → luv_cli-0.0.3}/PKG-INFO +2 -1
- {luv_cli-0.0.1 → luv_cli-0.0.3}/README.md +1 -0
- {luv_cli-0.0.1 → luv_cli-0.0.3}/luv/__init__.py +81 -27
- {luv_cli-0.0.1 → luv_cli-0.0.3}/pyproject.toml +1 -1
- {luv_cli-0.0.1 → luv_cli-0.0.3}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.1 → 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:
|
|
@@ -398,7 +418,24 @@ def cmd_clean(force: bool = False) -> None:
|
|
|
398
418
|
print("luv: nothing to clean")
|
|
399
419
|
|
|
400
420
|
|
|
401
|
-
def
|
|
421
|
+
def find_latest_clone(repo: str) -> Path | None:
|
|
422
|
+
"""Return the highest-numbered local {repo}-{N} folder, or None."""
|
|
423
|
+
if not PRS_DIR.exists():
|
|
424
|
+
return None
|
|
425
|
+
best: Path | None = None
|
|
426
|
+
best_num = -1
|
|
427
|
+
for entry in PRS_DIR.iterdir():
|
|
428
|
+
if not entry.is_dir():
|
|
429
|
+
continue
|
|
430
|
+
parts = entry.name.rsplit("-", 1)
|
|
431
|
+
if len(parts) == 2 and parts[0] == repo and parts[1].isdigit():
|
|
432
|
+
n = int(parts[1])
|
|
433
|
+
if n > best_num:
|
|
434
|
+
best, best_num = entry, n
|
|
435
|
+
return best
|
|
436
|
+
|
|
437
|
+
|
|
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:
|
|
402
439
|
"""Open an existing work folder or remote branch by number."""
|
|
403
440
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
404
441
|
|
|
@@ -407,11 +444,11 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
407
444
|
print(f"luv: opening existing folder {clone_dir.name}")
|
|
408
445
|
ensure_pr_rules()
|
|
409
446
|
if nav_mode:
|
|
410
|
-
navigate(clone_dir)
|
|
447
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
411
448
|
elif resume_mode:
|
|
412
|
-
resume(clone_dir)
|
|
449
|
+
resume(clone_dir, extra_env=extra_env)
|
|
413
450
|
else:
|
|
414
|
-
launch(clone_dir, prompt)
|
|
451
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
415
452
|
return # unreachable
|
|
416
453
|
|
|
417
454
|
# 2. Check remote branch luv-{number}
|
|
@@ -434,14 +471,14 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
434
471
|
print(f"luv: ready — {clone_dir.name}, branch {branch}")
|
|
435
472
|
ensure_pr_rules()
|
|
436
473
|
if nav_mode:
|
|
437
|
-
navigate(clone_dir)
|
|
474
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
438
475
|
elif resume_mode:
|
|
439
|
-
resume(clone_dir)
|
|
476
|
+
resume(clone_dir, extra_env=extra_env)
|
|
440
477
|
else:
|
|
441
|
-
launch(clone_dir, prompt)
|
|
478
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
442
479
|
|
|
443
480
|
|
|
444
|
-
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:
|
|
445
482
|
"""Open any GitHub PR by org/repo/number, cloning if needed."""
|
|
446
483
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
447
484
|
|
|
@@ -449,11 +486,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
449
486
|
print(f"luv: opening existing folder {clone_dir.name}")
|
|
450
487
|
ensure_pr_rules()
|
|
451
488
|
if nav_mode:
|
|
452
|
-
navigate(clone_dir)
|
|
489
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
453
490
|
elif resume_mode:
|
|
454
|
-
resume(clone_dir)
|
|
491
|
+
resume(clone_dir, extra_env=extra_env)
|
|
455
492
|
else:
|
|
456
|
-
launch(clone_dir, prompt)
|
|
493
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
457
494
|
return # unreachable
|
|
458
495
|
|
|
459
496
|
# Resolve the actual branch name via GitHub API
|
|
@@ -476,11 +513,11 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
476
513
|
print(f"luv: ready — {clone_dir.name}, branch {branch}")
|
|
477
514
|
ensure_pr_rules()
|
|
478
515
|
if nav_mode:
|
|
479
|
-
navigate(clone_dir)
|
|
516
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
480
517
|
elif resume_mode:
|
|
481
|
-
resume(clone_dir)
|
|
518
|
+
resume(clone_dir, extra_env=extra_env)
|
|
482
519
|
else:
|
|
483
|
-
launch(clone_dir, prompt)
|
|
520
|
+
launch(clone_dir, prompt, extra_env=extra_env)
|
|
484
521
|
|
|
485
522
|
|
|
486
523
|
def main() -> None:
|
|
@@ -489,7 +526,9 @@ def main() -> None:
|
|
|
489
526
|
nav_mode = "-n" in args
|
|
490
527
|
resume_mode = "-r" in args
|
|
491
528
|
force = "-f" in args or "--force" in args
|
|
492
|
-
|
|
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 {}
|
|
493
532
|
|
|
494
533
|
if not args or args[0] in ("-h", "--help"):
|
|
495
534
|
print("""\
|
|
@@ -498,6 +537,7 @@ Usage: luv [flags] <command>
|
|
|
498
537
|
Flags:
|
|
499
538
|
-n navigate: open a shell in the work folder instead of launching Claude
|
|
500
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
|
|
501
541
|
-f, --force (with --clean) skip safety checks and delete all work folders
|
|
502
542
|
|
|
503
543
|
Commands:
|
|
@@ -506,6 +546,8 @@ Commands:
|
|
|
506
546
|
luv [org/]<repo> <number> [prompt] reopen an existing work folder by number
|
|
507
547
|
luv -l <PR URL> [prompt] open any GitHub PR by URL
|
|
508
548
|
luv [org/]<repo> -pr <number> [prompt] open a GitHub PR by repo + number
|
|
549
|
+
luv [org/]<repo> -n open shell in latest local clone
|
|
550
|
+
luv [org/]<repo> -r resume Claude in latest local clone
|
|
509
551
|
luv --clean [-f] delete fully-pushed work folders
|
|
510
552
|
|
|
511
553
|
Org resolution:
|
|
@@ -536,7 +578,7 @@ Docker:
|
|
|
536
578
|
die(f"cannot parse PR URL: {url}")
|
|
537
579
|
org, repo, number = m.group(1), m.group(2), int(m.group(3))
|
|
538
580
|
prompt = " ".join(args[2:]) or None
|
|
539
|
-
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)
|
|
540
582
|
return
|
|
541
583
|
|
|
542
584
|
raw = args[0].rstrip("/")
|
|
@@ -556,19 +598,31 @@ Docker:
|
|
|
556
598
|
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
|
|
557
599
|
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
|
|
558
600
|
prompt = " ".join(prompt_parts) or None
|
|
559
|
-
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)
|
|
560
602
|
return
|
|
561
603
|
|
|
562
604
|
# Detect optional numeric second argument
|
|
563
605
|
if len(args) > 1 and args[1].isdigit():
|
|
564
606
|
number = int(args[1])
|
|
565
607
|
prompt = " ".join(args[2:]) or None
|
|
566
|
-
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)
|
|
567
609
|
return
|
|
568
610
|
|
|
569
611
|
org = resolve_org(explicit_org)
|
|
570
612
|
prompt = " ".join(args[1:]) if len(args) > 1 else None
|
|
571
613
|
|
|
614
|
+
# luv <repo> -n/-r → open latest local clone (no new workspace)
|
|
615
|
+
if (nav_mode or resume_mode) and not prompt:
|
|
616
|
+
clone_dir = find_latest_clone(repo)
|
|
617
|
+
if clone_dir is None:
|
|
618
|
+
die(f"no local clones of '{repo}' found in {PRS_DIR}")
|
|
619
|
+
print(f"luv: opening latest clone {clone_dir.name}")
|
|
620
|
+
if nav_mode:
|
|
621
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
622
|
+
else:
|
|
623
|
+
resume(clone_dir, extra_env=extra_env)
|
|
624
|
+
return
|
|
625
|
+
|
|
572
626
|
# 1. Verify repo exists
|
|
573
627
|
r = run(["gh", "api", f"repos/{org}/{repo}"])
|
|
574
628
|
if r.returncode != 0:
|
|
@@ -610,8 +664,8 @@ Docker:
|
|
|
610
664
|
|
|
611
665
|
# 7. Launch claude, resume session, or open shell (replace this process)
|
|
612
666
|
if nav_mode:
|
|
613
|
-
navigate(clone_dir)
|
|
667
|
+
navigate(clone_dir, extra_env=extra_env)
|
|
614
668
|
elif resume_mode:
|
|
615
|
-
resume(clone_dir)
|
|
669
|
+
resume(clone_dir, extra_env=extra_env)
|
|
616
670
|
else:
|
|
617
|
-
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
|