luv-cli 0.0.21__tar.gz → 0.0.23__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.21
3
+ Version: 0.0.23
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
@@ -48,6 +48,9 @@ luv --init
48
48
  # Create a new workspace and launch Claude
49
49
  luv my-repo "add user authentication"
50
50
 
51
+ # Base a new workspace off a non-default branch
52
+ luv my-repo -b develop "add user authentication"
53
+
51
54
  # Use a different org inline
52
55
  luv other-org/my-repo "fix the bug"
53
56
 
@@ -72,7 +75,7 @@ luv --clean
72
75
  1. Clones the repo into `~/prs/{repo}-{number}/`
73
76
  2. Creates a new branch `luv-{number}`
74
77
  3. Trusts the project in Claude Code config
75
- 4. Launches Claude with Opus 4.8 at max effort; prompt-less sessions auto-run `/effort ultracode` as their first input to enter ultracode mode
78
+ 4. Launches Claude with Opus 4.8 at max effort
76
79
 
77
80
  All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
78
81
 
@@ -82,6 +85,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
82
85
  |---------|-------------|
83
86
  | `luv --init` | Configure default GitHub org |
84
87
  | `luv [org/]<repo> [prompt...]` | Create a new workspace and launch Claude |
88
+ | `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
85
89
  | `luv [org/]<repo> <number> [prompt]` | Reopen an existing workspace |
86
90
  | `luv -l <PR URL> [prompt]` | Open any GitHub PR by URL |
87
91
  | `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number |
@@ -98,6 +102,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
98
102
  | `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
99
103
  | `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL); streams `stream-json` events to stdout |
100
104
  | `-m MODEL` | Claude model to use (default: `claude-opus-4-8`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
105
+ | `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
101
106
  | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
102
107
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
103
108
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
@@ -25,6 +25,9 @@ luv --init
25
25
  # Create a new workspace and launch Claude
26
26
  luv my-repo "add user authentication"
27
27
 
28
+ # Base a new workspace off a non-default branch
29
+ luv my-repo -b develop "add user authentication"
30
+
28
31
  # Use a different org inline
29
32
  luv other-org/my-repo "fix the bug"
30
33
 
@@ -49,7 +52,7 @@ luv --clean
49
52
  1. Clones the repo into `~/prs/{repo}-{number}/`
50
53
  2. Creates a new branch `luv-{number}`
51
54
  3. Trusts the project in Claude Code config
52
- 4. Launches Claude with Opus 4.8 at max effort; prompt-less sessions auto-run `/effort ultracode` as their first input to enter ultracode mode
55
+ 4. Launches Claude with Opus 4.8 at max effort
53
56
 
54
57
  All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
55
58
 
@@ -59,6 +62,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
59
62
  |---------|-------------|
60
63
  | `luv --init` | Configure default GitHub org |
61
64
  | `luv [org/]<repo> [prompt...]` | Create a new workspace and launch Claude |
65
+ | `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
62
66
  | `luv [org/]<repo> <number> [prompt]` | Reopen an existing workspace |
63
67
  | `luv -l <PR URL> [prompt]` | Open any GitHub PR by URL |
64
68
  | `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number |
@@ -75,6 +79,7 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
75
79
  | `-p` | Launch Claude in plan permission mode (default: `bypassPermissions`) |
76
80
  | `-nit` | Non-interactive: run `claude -p <prompt>` and exit (no REPL); streams `stream-json` events to stdout |
77
81
  | `-m MODEL` | Claude model to use (default: `claude-opus-4-8`); passed through to `claude --model`, so aliases like `opus`/`sonnet`/`haiku` work |
82
+ | `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
78
83
  | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
79
84
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
80
85
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import os
3
+ import random
3
4
  import re
4
5
  import shutil
5
6
  import stat
@@ -15,6 +16,13 @@ PRS_DIR = Path.home() / "prs"
15
16
  CLAUDE_JSON = Path.home() / ".claude.json"
16
17
  CLAUDE_SETTINGS_JSON = Path.home() / ".claude" / "settings.json"
17
18
 
19
+ COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan", "default"]
20
+
21
+
22
+ def pick_color() -> str:
23
+ """Pick a random /color value so each luv session is visually distinct."""
24
+ return random.choice(COLORS)
25
+
18
26
  PR_RULES = """
19
27
  # Pull Request Management
20
28
 
@@ -366,10 +374,10 @@ def launch(clone_dir: Path, prompt: str | None, plan_mode: bool = False,
366
374
  initial_args = ["-p", prompt]
367
375
  elif plan_mode:
368
376
  mode_flags = ["--permission-mode", "plan"]
369
- initial_args = [prompt] if prompt else ["/effort ultracode"]
377
+ initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
370
378
  else:
371
379
  mode_flags = ["--permission-mode", "bypassPermissions"]
372
- initial_args = [prompt] if prompt else ["/effort ultracode"]
380
+ initial_args = [prompt] if prompt else [f"/color {pick_color()}"]
373
381
 
374
382
  if compose_file:
375
383
  project = docker_project_name(clone_dir)
@@ -655,6 +663,19 @@ def main() -> None:
655
663
  die("-m requires a model name")
656
664
  args = args[:idx] + args[idx + 2:]
657
665
 
666
+ # -b takes a value (base branch); extract before the boolean-flag strip below.
667
+ base = None
668
+ if args.count("-b") > 1:
669
+ die("-b may only be provided once")
670
+ if "-b" in args:
671
+ idx = args.index("-b")
672
+ if idx + 1 >= len(args):
673
+ die("-b requires a branch name")
674
+ base = args[idx + 1].strip()
675
+ if not base or base.startswith("-"):
676
+ die("-b requires a branch name")
677
+ args = args[:idx] + args[idx + 2:]
678
+
658
679
  args = [a for a in args if a not in ("-n", "-r", "-e", "-f", "--force", "-p", "-nit", "--safe")]
659
680
  extra_env = collect_luv_env() if env_mode else {}
660
681
 
@@ -668,6 +689,7 @@ Flags:
668
689
  -p launch Claude in plan permission mode (default: bypassPermissions)
669
690
  -nit non-interactive: run claude -p <prompt> and exit (no REPL)
670
691
  -m MODEL claude model to use (default: claude-opus-4-8)
692
+ -b BRANCH base a new workspace off BRANCH (clone + branch from it; recorded in git config luv.base)
671
693
  -e env: pass LUV_* environment variables (with prefix stripped) into the session
672
694
  -f, --force (with --clean) skip safety checks and delete all work folders
673
695
  --safe (with --clean -f) only delete folders older than 24h
@@ -675,6 +697,7 @@ Flags:
675
697
  Commands:
676
698
  luv --init configure default GitHub org
677
699
  luv [org/]<repo> [prompt...] create a new PR workspace
700
+ luv [org/]<repo> -b <branch> [prompt] create a workspace based off <branch>
678
701
  luv [org/]<repo> <number> [prompt] reopen an existing work folder by number
679
702
  luv -l <PR URL> [prompt] open any GitHub PR by URL
680
703
  luv [org/]<repo> -pr <number> [prompt] open a GitHub PR by repo + number
@@ -695,6 +718,19 @@ Docker:
695
718
  if safe and (args[0] != "--clean" or not force):
696
719
  die("--safe only works with --clean -f")
697
720
 
721
+ # -b only makes sense when creating a NEW workspace; reject it on every path
722
+ # that early-returns without cloning+branching from scratch.
723
+ if base is not None:
724
+ is_reopen_by_number = len(args) > 1 and args[1].isdigit()
725
+ opens_latest_local = (nav_mode or resume_mode) and len(args) == 1
726
+ if (args[0] in ("--clean", "--init", "-l")
727
+ or "-pr" in args
728
+ or is_reopen_by_number
729
+ or opens_latest_local):
730
+ die("-b only applies when creating a new workspace "
731
+ "(luv [org/]<repo> [prompt...]); it cannot be combined with "
732
+ "--clean, --init, -l, -pr, reopen-by-number, or bare -n/-r")
733
+
698
734
  if args[0] == "--clean":
699
735
  cmd_clean(force=force, safe=safe)
700
736
  return
@@ -763,6 +799,14 @@ Docker:
763
799
  if r.returncode != 0:
764
800
  die(f"repo '{org}/{repo}' not found or gh auth failed.\n{r.stderr.strip()}")
765
801
 
802
+ clone_url = f"https://github.com/{org}/{repo}"
803
+
804
+ # 1b. If a base branch was requested, verify it exists before cloning.
805
+ if base is not None:
806
+ r = run(["git", "ls-remote", "--heads", clone_url, base])
807
+ if r.returncode != 0 or f"refs/heads/{base}" not in r.stdout:
808
+ die(f"base branch '{base}' not found on {org}/{repo}")
809
+
766
810
  # 2. Get latest issue/PR number (shared counter on GitHub).
767
811
  # /issues is documented to include PRs but in practice returns [] for repos
768
812
  # with no plain issues, so query both endpoints and take the max.
@@ -783,20 +827,30 @@ Docker:
783
827
  candidate += 1
784
828
  clone_dir = PRS_DIR / f"{repo}-{candidate}"
785
829
 
786
- # 4. Clone
787
- clone_url = f"https://github.com/{org}/{repo}"
788
- print(f"luv: cloning {clone_url} -> {clone_dir}")
789
- r = subprocess.run(["git", "clone", clone_url, str(clone_dir)])
830
+ # 4. Clone (off the base branch when -b was given)
831
+ print(f"luv: cloning {clone_url} -> {clone_dir}" + (f" (base {base})" if base else ""))
832
+ clone_cmd = ["git", "clone"]
833
+ if base is not None:
834
+ clone_cmd += ["--branch", base]
835
+ clone_cmd += [clone_url, str(clone_dir)]
836
+ r = subprocess.run(clone_cmd)
790
837
  if r.returncode != 0:
791
838
  die(f"git clone failed (exit {r.returncode})")
792
839
 
793
- # 5. Create branch
840
+ # 5. Create branch off the cloned HEAD (= base when -b was given, else default)
794
841
  branch = f"luv-{candidate}"
795
842
  print(f"luv: creating branch {branch}")
796
843
  r = subprocess.run(["git", "checkout", "-b", branch], cwd=str(clone_dir))
797
844
  if r.returncode != 0:
798
845
  die(f"git checkout -b failed (exit {r.returncode})")
799
846
 
847
+ # 5b. Record the base so the eventual PR can target it (local .git/config only).
848
+ if base is not None:
849
+ r = run(["git", "config", "luv.base", base], cwd=str(clone_dir))
850
+ if r.returncode != 0:
851
+ print(f"luv: warning: could not record base branch ({r.stderr.strip()})",
852
+ file=sys.stderr)
853
+
800
854
  # 6. Ensure PR rules in ~/.claude/CLAUDE.md and bypass-permissions default
801
855
  ensure_pr_rules()
802
856
  ensure_default_permission_mode()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "luv-cli"
7
- version = "0.0.21"
7
+ version = "0.0.23"
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