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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luv-cli
3
- Version: 0.0.12
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, extra_env: dict[str, 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 prompt:
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
- r = run(["gh", "api",
687
- f"repos/{org}/{repo}/issues?state=all&filter=all&per_page=1&sort=created&direction=desc"])
688
- if r.returncode != 0:
689
- die(f"failed to fetch issues.\n{r.stderr.strip()}")
690
- items = json.loads(r.stdout)
691
- latest = items[0]["number"] if items else 0
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.12"
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