luv-cli 0.0.12__tar.gz → 0.0.15__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.12 → luv_cli-0.0.15}/PKG-INFO +3 -1
- {luv_cli-0.0.12 → luv_cli-0.0.15}/README.md +2 -0
- {luv_cli-0.0.12 → luv_cli-0.0.15}/luv/__init__.py +37 -22
- {luv_cli-0.0.12 → luv_cli-0.0.15}/pyproject.toml +1 -1
- {luv_cli-0.0.12 → luv_cli-0.0.15}/.claude/settings.json +0 -0
- {luv_cli-0.0.12 → luv_cli-0.0.15}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.0.12 → luv_cli-0.0.15}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.12 → luv_cli-0.0.15}/.gitignore +0 -0
- {luv_cli-0.0.12 → luv_cli-0.0.15}/LICENSE +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.15
|
|
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,8 @@ 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
|
+
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
98
|
+
| `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL) |
|
|
97
99
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
98
100
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
99
101
|
|
|
@@ -71,6 +71,8 @@ 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
|
+
| `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
|
|
75
|
+
| `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL) |
|
|
74
76
|
| `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
|
|
75
77
|
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
76
78
|
|
|
@@ -347,7 +347,8 @@ def resume(clone_dir: Path, extra_env: dict[str, str] = {}) -> None:
|
|
|
347
347
|
"--remote-control-session-name-prefix", clone_dir.name])
|
|
348
348
|
|
|
349
349
|
|
|
350
|
-
def launch(clone_dir: Path, prompt: str | None,
|
|
350
|
+
def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
|
|
351
|
+
non_interactive: bool = False, extra_env: dict[str, str] = {}) -> None:
|
|
351
352
|
"""Trust, resolve claude, chdir, and exec — replacing this process."""
|
|
352
353
|
trust_project(clone_dir)
|
|
353
354
|
os.chdir(str(clone_dir))
|
|
@@ -359,12 +360,17 @@ def launch(clone_dir: Path, prompt: str | None, extra_env: dict[str, str] = {})
|
|
|
359
360
|
"--effort", "max",
|
|
360
361
|
"--remote-control",
|
|
361
362
|
"--remote-control-session-name-prefix", clone_dir.name]
|
|
362
|
-
if
|
|
363
|
+
if non_interactive:
|
|
364
|
+
if not prompt:
|
|
365
|
+
die("-nit requires a prompt")
|
|
366
|
+
mode_flags = []
|
|
367
|
+
initial_args = ["-p", prompt]
|
|
368
|
+
elif plan_mode:
|
|
363
369
|
mode_flags = ["--permission-mode", "plan"]
|
|
364
|
-
initial_args = [prompt]
|
|
370
|
+
initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
|
|
365
371
|
else:
|
|
366
372
|
mode_flags = ["--permission-mode", "bypassPermissions"]
|
|
367
|
-
initial_args = [f"/color {pick_color()}"]
|
|
373
|
+
initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
|
|
368
374
|
|
|
369
375
|
if compose_file:
|
|
370
376
|
project = docker_project_name(clone_dir)
|
|
@@ -489,7 +495,7 @@ def find_latest_clone(repo: str) -> Path | None:
|
|
|
489
495
|
return best
|
|
490
496
|
|
|
491
497
|
|
|
492
|
-
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:
|
|
498
|
+
def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] = {}) -> None:
|
|
493
499
|
"""Open an existing work folder or remote branch by number."""
|
|
494
500
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
495
501
|
|
|
@@ -502,7 +508,7 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
502
508
|
elif resume_mode:
|
|
503
509
|
resume(clone_dir, extra_env=extra_env)
|
|
504
510
|
else:
|
|
505
|
-
launch(clone_dir, prompt, extra_env=extra_env)
|
|
511
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
506
512
|
return # unreachable
|
|
507
513
|
|
|
508
514
|
# 2. Check remote branch luv-{number}
|
|
@@ -529,10 +535,10 @@ def open_existing(org: str, repo: str, number: int, prompt: str | None, nav_mode
|
|
|
529
535
|
elif resume_mode:
|
|
530
536
|
resume(clone_dir, extra_env=extra_env)
|
|
531
537
|
else:
|
|
532
|
-
launch(clone_dir, prompt, extra_env=extra_env)
|
|
538
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
533
539
|
|
|
534
540
|
|
|
535
|
-
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:
|
|
541
|
+
def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool = False, resume_mode: bool = False, plan_mode: bool = False, non_interactive: bool = False, extra_env: dict[str, str] = {}) -> None:
|
|
536
542
|
"""Open any GitHub PR by org/repo/number, cloning if needed."""
|
|
537
543
|
clone_dir = PRS_DIR / f"{repo}-{number}"
|
|
538
544
|
|
|
@@ -544,7 +550,7 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
544
550
|
elif resume_mode:
|
|
545
551
|
resume(clone_dir, extra_env=extra_env)
|
|
546
552
|
else:
|
|
547
|
-
launch(clone_dir, prompt, extra_env=extra_env)
|
|
553
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
548
554
|
return # unreachable
|
|
549
555
|
|
|
550
556
|
# Resolve the actual branch name via GitHub API
|
|
@@ -571,7 +577,7 @@ def open_pr(org: str, repo: str, number: int, prompt: str | None, nav_mode: bool
|
|
|
571
577
|
elif resume_mode:
|
|
572
578
|
resume(clone_dir, extra_env=extra_env)
|
|
573
579
|
else:
|
|
574
|
-
launch(clone_dir, prompt, extra_env=extra_env)
|
|
580
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, extra_env=extra_env)
|
|
575
581
|
|
|
576
582
|
|
|
577
583
|
def main() -> None:
|
|
@@ -579,9 +585,11 @@ def main() -> None:
|
|
|
579
585
|
|
|
580
586
|
nav_mode = "-n" in args
|
|
581
587
|
resume_mode = "-r" in args
|
|
588
|
+
plan_mode = "-p" in args
|
|
589
|
+
non_interactive = "-nit" in args
|
|
582
590
|
force = "-f" in args or "--force" in args
|
|
583
591
|
env_mode = "-e" in args
|
|
584
|
-
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force")]
|
|
592
|
+
args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit")]
|
|
585
593
|
extra_env = collect_luv_env() if env_mode else {}
|
|
586
594
|
|
|
587
595
|
if not args or args[0] in ("-h", "--help"):
|
|
@@ -591,6 +599,8 @@ Usage: luv [flags] <command>
|
|
|
591
599
|
Flags:
|
|
592
600
|
-n navigate: open a shell in the work folder instead of launching Claude
|
|
593
601
|
-r resume: resume the last Claude session in the work folder
|
|
602
|
+
-p launch Claude in plan permission mode (default: bypassPermissions)
|
|
603
|
+
-nit non-interactive: run claude -p <prompt> and exit (no REPL)
|
|
594
604
|
-e env: pass LUV_* environment variables (with prefix stripped) into the session
|
|
595
605
|
-f, --force (with --clean) skip safety checks and delete all work folders
|
|
596
606
|
|
|
@@ -632,7 +642,7 @@ Docker:
|
|
|
632
642
|
die(f"cannot parse PR URL: {url}")
|
|
633
643
|
org, repo, number = m.group(1), m.group(2), int(m.group(3))
|
|
634
644
|
prompt = " ".join(args[2:]) or None
|
|
635
|
-
open_pr(org, repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
645
|
+
open_pr(org, repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
636
646
|
return
|
|
637
647
|
|
|
638
648
|
raw = args[0].rstrip("/")
|
|
@@ -652,14 +662,14 @@ Docker:
|
|
|
652
662
|
die(f"expected a PR number after -pr, got '{args[idx + 1]}'")
|
|
653
663
|
prompt_parts = [a for i, a in enumerate(args) if i not in (0, idx, idx + 1)]
|
|
654
664
|
prompt = " ".join(prompt_parts) or None
|
|
655
|
-
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
665
|
+
open_pr(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
656
666
|
return
|
|
657
667
|
|
|
658
668
|
# Detect optional numeric second argument
|
|
659
669
|
if len(args) > 1 and args[1].isdigit():
|
|
660
670
|
number = int(args[1])
|
|
661
671
|
prompt = " ".join(args[2:]) or None
|
|
662
|
-
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, extra_env=extra_env)
|
|
672
|
+
open_existing(resolve_org(explicit_org), repo, number, prompt, nav_mode, resume_mode, plan_mode, non_interactive, extra_env=extra_env)
|
|
663
673
|
return
|
|
664
674
|
|
|
665
675
|
org = resolve_org(explicit_org)
|
|
@@ -682,13 +692,18 @@ Docker:
|
|
|
682
692
|
if r.returncode != 0:
|
|
683
693
|
die(f"repo '{org}/{repo}' not found or gh auth failed.\n{r.stderr.strip()}")
|
|
684
694
|
|
|
685
|
-
# 2. Get latest issue/PR number (shared counter on GitHub)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
695
|
+
# 2. Get latest issue/PR number (shared counter on GitHub).
|
|
696
|
+
# /issues is documented to include PRs but in practice returns [] for repos
|
|
697
|
+
# with no plain issues, so query both endpoints and take the max.
|
|
698
|
+
def _latest(endpoint: str) -> int:
|
|
699
|
+
r = run(["gh", "api",
|
|
700
|
+
f"repos/{org}/{repo}/{endpoint}?state=all&per_page=1&sort=created&direction=desc"])
|
|
701
|
+
if r.returncode != 0:
|
|
702
|
+
die(f"failed to fetch {endpoint}.\n{r.stderr.strip()}")
|
|
703
|
+
items = json.loads(r.stdout)
|
|
704
|
+
return items[0]["number"] if items else 0
|
|
705
|
+
|
|
706
|
+
latest = max(_latest("issues"), _latest("pulls"))
|
|
692
707
|
candidate = latest + 1
|
|
693
708
|
|
|
694
709
|
# 3. Find free local folder
|
|
@@ -723,4 +738,4 @@ Docker:
|
|
|
723
738
|
elif resume_mode:
|
|
724
739
|
resume(clone_dir, extra_env=extra_env)
|
|
725
740
|
else:
|
|
726
|
-
launch(clone_dir, prompt, extra_env=extra_env)
|
|
741
|
+
launch(clone_dir, prompt, plan_mode=plan_mode, non_interactive=non_interactive, 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.15"
|
|
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
|