luv-cli 0.0.18__tar.gz → 0.0.20__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.18 → luv_cli-0.0.20}/PKG-INFO +1 -1
- {luv_cli-0.0.18 → luv_cli-0.0.20}/luv/__init__.py +47 -3
- {luv_cli-0.0.18 → luv_cli-0.0.20}/pyproject.toml +1 -1
- {luv_cli-0.0.18 → luv_cli-0.0.20}/.claude/settings.json +0 -0
- {luv_cli-0.0.18 → luv_cli-0.0.20}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.0.18 → luv_cli-0.0.20}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.18 → luv_cli-0.0.20}/.gitignore +0 -0
- {luv_cli-0.0.18 → luv_cli-0.0.20}/LICENSE +0 -0
- {luv_cli-0.0.18 → luv_cli-0.0.20}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.20
|
|
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
|
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import random
|
|
4
4
|
import re
|
|
5
5
|
import shutil
|
|
6
|
+
import stat
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
@@ -399,6 +400,49 @@ def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
|
399
400
|
SAFE_AGE_SECONDS = 24 * 3600
|
|
400
401
|
|
|
401
402
|
|
|
403
|
+
def _on_rm_error(func, path, _exc):
|
|
404
|
+
"""rmtree handler: make `path` (and its parent dir) writable, then retry."""
|
|
405
|
+
parent = os.path.dirname(path)
|
|
406
|
+
try:
|
|
407
|
+
os.chmod(parent, os.stat(parent).st_mode | stat.S_IWUSR | stat.S_IXUSR)
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
os.chmod(path, stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR)
|
|
411
|
+
func(path)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _docker_wipe(path: Path) -> bool:
|
|
415
|
+
"""rm -rf `path` from inside a busybox container so the container's root can
|
|
416
|
+
delete files that a previous container bind-mounted in as root (e.g. Rust
|
|
417
|
+
target dirs built inside the workspace's dev-environment). Returns True on
|
|
418
|
+
success. No-op if docker isn't available."""
|
|
419
|
+
if shutil.which("docker") is None:
|
|
420
|
+
return False
|
|
421
|
+
parent = path.resolve().parent
|
|
422
|
+
name = path.name
|
|
423
|
+
r = subprocess.run(
|
|
424
|
+
["docker", "run", "--rm",
|
|
425
|
+
"-v", f"{parent}:/p",
|
|
426
|
+
"busybox", "rm", "-rf", f"/p/{name}"],
|
|
427
|
+
capture_output=True, text=True,
|
|
428
|
+
)
|
|
429
|
+
if r.returncode != 0:
|
|
430
|
+
sys.stderr.write(f"luv: docker rm fallback failed for {path}: {r.stderr.strip()}\n")
|
|
431
|
+
return False
|
|
432
|
+
return not path.exists()
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _force_rmtree(path: Path) -> None:
|
|
436
|
+
"""rmtree that survives read-only files (chmod-and-retry) and root-owned
|
|
437
|
+
files left behind by Docker bind-mounts (containerized rm -rf fallback)."""
|
|
438
|
+
kwargs = {"onexc": _on_rm_error} if sys.version_info >= (3, 12) else {"onerror": _on_rm_error}
|
|
439
|
+
try:
|
|
440
|
+
shutil.rmtree(path, **kwargs)
|
|
441
|
+
except PermissionError:
|
|
442
|
+
if not _docker_wipe(path):
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
|
|
402
446
|
def cmd_clean(force: bool = False, safe: bool = False) -> None:
|
|
403
447
|
"""Scan ~/prs/ and delete fully-pushed, clean work folders."""
|
|
404
448
|
if not PRS_DIR.exists():
|
|
@@ -421,7 +465,7 @@ def cmd_clean(force: bool = False, safe: bool = False) -> None:
|
|
|
421
465
|
if safe and (now - entry.stat().st_mtime) < SAFE_AGE_SECONDS:
|
|
422
466
|
skipped.append((entry.name, "younger than 24h (--safe)"))
|
|
423
467
|
continue
|
|
424
|
-
|
|
468
|
+
_force_rmtree(entry)
|
|
425
469
|
cleaned.append(entry.name)
|
|
426
470
|
continue
|
|
427
471
|
|
|
@@ -464,7 +508,7 @@ def cmd_clean(force: bool = False, safe: bool = False) -> None:
|
|
|
464
508
|
if local_sha != pr_head_sha:
|
|
465
509
|
skipped.append((entry.name, "local HEAD differs from merged PR head"))
|
|
466
510
|
continue
|
|
467
|
-
|
|
511
|
+
_force_rmtree(entry)
|
|
468
512
|
cleaned.append(entry.name)
|
|
469
513
|
continue
|
|
470
514
|
|
|
@@ -474,7 +518,7 @@ def cmd_clean(force: bool = False, safe: bool = False) -> None:
|
|
|
474
518
|
skipped.append((entry.name, "unpushed commits"))
|
|
475
519
|
continue
|
|
476
520
|
|
|
477
|
-
|
|
521
|
+
_force_rmtree(entry)
|
|
478
522
|
cleaned.append(entry.name)
|
|
479
523
|
|
|
480
524
|
if skipped:
|
|
@@ -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.20"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|