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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luv-cli
3
- Version: 0.0.18
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
- shutil.rmtree(entry)
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
- shutil.rmtree(entry)
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
- shutil.rmtree(entry)
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.18"
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