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.
- {luv_cli-0.0.21 → luv_cli-0.0.23}/PKG-INFO +7 -2
- {luv_cli-0.0.21 → luv_cli-0.0.23}/README.md +6 -1
- {luv_cli-0.0.21 → luv_cli-0.0.23}/luv/__init__.py +61 -7
- {luv_cli-0.0.21 → luv_cli-0.0.23}/pyproject.toml +1 -1
- {luv_cli-0.0.21 → luv_cli-0.0.23}/.claude/settings.json +0 -0
- {luv_cli-0.0.21 → luv_cli-0.0.23}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.0.21 → luv_cli-0.0.23}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.0.21 → luv_cli-0.0.23}/.gitignore +0 -0
- {luv_cli-0.0.21 → luv_cli-0.0.23}/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.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
|
|
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
|
|
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 ["/
|
|
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 ["/
|
|
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
|
|
788
|
-
|
|
789
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|