luv-cli 0.0.25__tar.gz → 0.2.0__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.25
3
+ Version: 0.2.0
4
4
  Summary: Launch Claude Code or Codex 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
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  A CLI that launches Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments.
27
27
 
28
- `luv` clones a repo, creates a branch, and drops you into a Claude session ready to work. When the repo ships a `.luv/settings.json`, it spins up Docker Compose automatically so every command runs in the right environment.
28
+ `luv` clones a repo, creates a branch, and drops you into a Claude session ready to work. When the repo ships a `.luv/settings.json`, it spins up Docker Compose automatically so every command runs in the right environment. Point it at a remote machine and the whole thing runs there in a tmux session you can detach from and reattach to later.
29
29
 
30
30
  ## Install
31
31
 
@@ -42,8 +42,8 @@ pip install luv-cli
42
42
  ## Quick start
43
43
 
44
44
  ```bash
45
- # Configure your default GitHub org (one-time setup)
46
- luv --init
45
+ # One-time setup: default GitHub org, and optionally a remote machine
46
+ luv config
47
47
 
48
48
  # Create a new workspace and launch Claude
49
49
  luv my-repo "add user authentication"
@@ -74,6 +74,14 @@ luv -r my-repo 42
74
74
 
75
75
  # Clean up fully-merged workspaces
76
76
  luv --clean
77
+
78
+ # Tear down a finished session: kill its tmux, delete its workspace
79
+ luv rm myrepo-42
80
+ luv rm --merged # everything whose PR has landed
81
+
82
+ # See what's running on your remote machine, and reattach
83
+ luv ls
84
+ luv continue
77
85
  ```
78
86
 
79
87
  ## How it works
@@ -85,11 +93,21 @@ luv --clean
85
93
 
86
94
  All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
87
95
 
96
+ With a remote host configured, steps 1–4 happen on that machine inside a tmux session instead — see [Remote sessions](#remote-sessions).
97
+
88
98
  ## Commands
89
99
 
90
100
  | Command | Description |
91
101
  |---------|-------------|
92
- | `luv --init` | Configure default GitHub org |
102
+ | `luv config` | Interactive setup: remote host, SSH key, default org |
103
+ | `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
104
+ | `luv config list` | Show all settings |
105
+ | `luv --init` | Configure default GitHub org only |
106
+ | `luv ls [--host H] [--prune] [--no-pr]` | List live sessions across hosts, with each one's PR link |
107
+ | `luv continue [<repo> [number]]` | Attach to a live session |
108
+ | `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
109
+ | `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
110
+ | `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
93
111
  | `luv [org/]<repo> [prompt...]` | Create a new workspace and launch Claude (default) |
94
112
  | `luv --codex [org/]<repo> [prompt...]` | Create a workspace and launch Codex in YOLO mode |
95
113
  | `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
@@ -113,6 +131,9 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
113
131
  | `-m MODEL` | Model to use; Claude defaults to `claude-opus-5`, while Codex uses its configured CLI default |
114
132
  | `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
115
133
  | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
134
+ | `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
135
+ | `-i PATH` | SSH identity file to use for this invocation |
136
+ | `--local` | Force local execution even when a remote host is configured |
116
137
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
117
138
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
118
139
 
@@ -166,6 +187,24 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
166
187
 
167
188
  Docker mode works with all flags: `-n` opens a bash shell in the container, `-r` resumes a Claude session in the container.
168
189
 
190
+ ## Remote sessions
191
+
192
+ Point `luv` at another machine and every workspace command runs there, inside a tmux session that survives disconnects.
193
+
194
+ ```bash
195
+ luv config # set the remote host, SSH key, and org
196
+ luv myrepo "fix the flaky test" # clones and launches Claude on the remote
197
+ # ...close the laptop, go home...
198
+ luv ls # see what's still running
199
+ luv continue # reattach exactly where you left off
200
+ ```
201
+
202
+ `luv` re-invokes itself on the remote over SSH, so every flag works there unchanged. The remote needs `luv`, `tmux`, `gh`, and `git` on its `PATH`.
203
+
204
+ Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
205
+
206
+ Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
207
+
169
208
  ## Workspace cleanup
170
209
 
171
210
  `luv --clean` scans `~/prs/` and safely removes workspaces that are fully pushed. It checks:
@@ -176,12 +215,55 @@ Docker mode works with all flags: `-n` opens a bash shell in the container, `-r`
176
215
 
177
216
  Use `luv --clean -f` to skip all safety checks and delete everything. Add `--safe` (i.e. `luv --clean --safe -f`) to restrict force-delete to workspaces whose folder mtime is older than 24 hours, leaving recently-touched workspaces alone.
178
217
 
218
+ Workspaces with a live tmux session are skipped unless you pass `-f`.
219
+
220
+ ### Removing a specific session
221
+
222
+ `luv --clean` is a garbage collector: it decides for itself what is safe to
223
+ delete, and never touches a workspace with a live session. `luv rm` is the
224
+ opposite — you name what goes, and it goes, on whichever host it lives on.
225
+
226
+ ```bash
227
+ luv rm myrepo-42 # kill its tmux session, delete ~/prs/myrepo-42 on its host
228
+ luv rm luv-myrepo-42 # the session name works too
229
+ luv rm --merged # every session whose PR has been merged
230
+ luv rm --dead # workspaces left behind by sessions that already exited
231
+ luv rm --dead --host box # scope either selector to one machine
232
+ ```
233
+
234
+ A named target is taken as intent and removed without further checks —
235
+ including any uncommitted work in it. The `--merged` and `--dead` selectors
236
+ list what they matched and ask first; `-f` skips that prompt.
237
+
238
+ `--dead` is the counterpart to `luv ls`: reconciliation drops a session from the
239
+ registry as soon as its tmux session exits, but the clone it was working in
240
+ stays on the remote disk. Those orphans are invisible to `luv ls` and are
241
+ usually what is actually filling up the box.
242
+
243
+ Only folders named `{repo}-{N}` are ever eligible — anything else in the
244
+ workspace root is left alone, and a target that doesn't resolve to one is an
245
+ error rather than an `rm -rf`.
246
+
179
247
  ## Configuration
180
248
 
181
- Run `luv --init` to set your default GitHub org. This saves to `~/.luv/config.json`.
249
+ Run `luv config` for interactive setup, or `luv --init` to set just your default GitHub org. Both save to `~/.luv/config.json`:
250
+
251
+ ```json
252
+ {
253
+ "org": "exosphere",
254
+ "prs_dir": "~/prs",
255
+ "remote": {
256
+ "host": "box",
257
+ "identity_file": "~/.ssh/id_ed25519",
258
+ "dir": "~/prs"
259
+ }
260
+ }
261
+ ```
182
262
 
183
263
  You can also pass `org/repo` inline to override the default for any command (e.g., `luv other-org/my-repo`).
184
264
 
265
+ Every key, with defaults and resolution order: **[docs/configuration.md](docs/configuration.md)**. The session registry that backs `luv ls`: **[docs/sessions.md](docs/sessions.md)**.
266
+
185
267
  ## License
186
268
 
187
269
  MIT
@@ -2,7 +2,7 @@
2
2
 
3
3
  A CLI that launches Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments.
4
4
 
5
- `luv` clones a repo, creates a branch, and drops you into a Claude session ready to work. When the repo ships a `.luv/settings.json`, it spins up Docker Compose automatically so every command runs in the right environment.
5
+ `luv` clones a repo, creates a branch, and drops you into a Claude session ready to work. When the repo ships a `.luv/settings.json`, it spins up Docker Compose automatically so every command runs in the right environment. Point it at a remote machine and the whole thing runs there in a tmux session you can detach from and reattach to later.
6
6
 
7
7
  ## Install
8
8
 
@@ -19,8 +19,8 @@ pip install luv-cli
19
19
  ## Quick start
20
20
 
21
21
  ```bash
22
- # Configure your default GitHub org (one-time setup)
23
- luv --init
22
+ # One-time setup: default GitHub org, and optionally a remote machine
23
+ luv config
24
24
 
25
25
  # Create a new workspace and launch Claude
26
26
  luv my-repo "add user authentication"
@@ -51,6 +51,14 @@ luv -r my-repo 42
51
51
 
52
52
  # Clean up fully-merged workspaces
53
53
  luv --clean
54
+
55
+ # Tear down a finished session: kill its tmux, delete its workspace
56
+ luv rm myrepo-42
57
+ luv rm --merged # everything whose PR has landed
58
+
59
+ # See what's running on your remote machine, and reattach
60
+ luv ls
61
+ luv continue
54
62
  ```
55
63
 
56
64
  ## How it works
@@ -62,11 +70,21 @@ luv --clean
62
70
 
63
71
  All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
64
72
 
73
+ With a remote host configured, steps 1–4 happen on that machine inside a tmux session instead — see [Remote sessions](#remote-sessions).
74
+
65
75
  ## Commands
66
76
 
67
77
  | Command | Description |
68
78
  |---------|-------------|
69
- | `luv --init` | Configure default GitHub org |
79
+ | `luv config` | Interactive setup: remote host, SSH key, default org |
80
+ | `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
81
+ | `luv config list` | Show all settings |
82
+ | `luv --init` | Configure default GitHub org only |
83
+ | `luv ls [--host H] [--prune] [--no-pr]` | List live sessions across hosts, with each one's PR link |
84
+ | `luv continue [<repo> [number]]` | Attach to a live session |
85
+ | `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
86
+ | `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
87
+ | `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
70
88
  | `luv [org/]<repo> [prompt...]` | Create a new workspace and launch Claude (default) |
71
89
  | `luv --codex [org/]<repo> [prompt...]` | Create a workspace and launch Codex in YOLO mode |
72
90
  | `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
@@ -90,6 +108,9 @@ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issu
90
108
  | `-m MODEL` | Model to use; Claude defaults to `claude-opus-5`, while Codex uses its configured CLI default |
91
109
  | `-b BRANCH` | Base a new workspace off `BRANCH` (clone + branch from it); recorded in `git config luv.base` so the PR can target it |
92
110
  | `-e` | Env: pass `LUV_*` environment variables (with prefix stripped) into the session |
111
+ | `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
112
+ | `-i PATH` | SSH identity file to use for this invocation |
113
+ | `--local` | Force local execution even when a remote host is configured |
93
114
  | `-f`, `--force` | Skip safety checks (with `--clean`) |
94
115
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
95
116
 
@@ -143,6 +164,24 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
143
164
 
144
165
  Docker mode works with all flags: `-n` opens a bash shell in the container, `-r` resumes a Claude session in the container.
145
166
 
167
+ ## Remote sessions
168
+
169
+ Point `luv` at another machine and every workspace command runs there, inside a tmux session that survives disconnects.
170
+
171
+ ```bash
172
+ luv config # set the remote host, SSH key, and org
173
+ luv myrepo "fix the flaky test" # clones and launches Claude on the remote
174
+ # ...close the laptop, go home...
175
+ luv ls # see what's still running
176
+ luv continue # reattach exactly where you left off
177
+ ```
178
+
179
+ `luv` re-invokes itself on the remote over SSH, so every flag works there unchanged. The remote needs `luv`, `tmux`, `gh`, and `git` on its `PATH`.
180
+
181
+ Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
182
+
183
+ Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
184
+
146
185
  ## Workspace cleanup
147
186
 
148
187
  `luv --clean` scans `~/prs/` and safely removes workspaces that are fully pushed. It checks:
@@ -153,12 +192,55 @@ Docker mode works with all flags: `-n` opens a bash shell in the container, `-r`
153
192
 
154
193
  Use `luv --clean -f` to skip all safety checks and delete everything. Add `--safe` (i.e. `luv --clean --safe -f`) to restrict force-delete to workspaces whose folder mtime is older than 24 hours, leaving recently-touched workspaces alone.
155
194
 
195
+ Workspaces with a live tmux session are skipped unless you pass `-f`.
196
+
197
+ ### Removing a specific session
198
+
199
+ `luv --clean` is a garbage collector: it decides for itself what is safe to
200
+ delete, and never touches a workspace with a live session. `luv rm` is the
201
+ opposite — you name what goes, and it goes, on whichever host it lives on.
202
+
203
+ ```bash
204
+ luv rm myrepo-42 # kill its tmux session, delete ~/prs/myrepo-42 on its host
205
+ luv rm luv-myrepo-42 # the session name works too
206
+ luv rm --merged # every session whose PR has been merged
207
+ luv rm --dead # workspaces left behind by sessions that already exited
208
+ luv rm --dead --host box # scope either selector to one machine
209
+ ```
210
+
211
+ A named target is taken as intent and removed without further checks —
212
+ including any uncommitted work in it. The `--merged` and `--dead` selectors
213
+ list what they matched and ask first; `-f` skips that prompt.
214
+
215
+ `--dead` is the counterpart to `luv ls`: reconciliation drops a session from the
216
+ registry as soon as its tmux session exits, but the clone it was working in
217
+ stays on the remote disk. Those orphans are invisible to `luv ls` and are
218
+ usually what is actually filling up the box.
219
+
220
+ Only folders named `{repo}-{N}` are ever eligible — anything else in the
221
+ workspace root is left alone, and a target that doesn't resolve to one is an
222
+ error rather than an `rm -rf`.
223
+
156
224
  ## Configuration
157
225
 
158
- Run `luv --init` to set your default GitHub org. This saves to `~/.luv/config.json`.
226
+ Run `luv config` for interactive setup, or `luv --init` to set just your default GitHub org. Both save to `~/.luv/config.json`:
227
+
228
+ ```json
229
+ {
230
+ "org": "exosphere",
231
+ "prs_dir": "~/prs",
232
+ "remote": {
233
+ "host": "box",
234
+ "identity_file": "~/.ssh/id_ed25519",
235
+ "dir": "~/prs"
236
+ }
237
+ }
238
+ ```
159
239
 
160
240
  You can also pass `org/repo` inline to override the default for any command (e.g., `luv other-org/my-repo`).
161
241
 
242
+ Every key, with defaults and resolution order: **[docs/configuration.md](docs/configuration.md)**. The session registry that backs `luv ls`: **[docs/sessions.md](docs/sessions.md)**.
243
+
162
244
  ## License
163
245
 
164
246
  MIT
@@ -0,0 +1,140 @@
1
+ # Configuration
2
+
3
+ luv reads a single JSON file: `~/.luv/config.json`. It is created by
4
+ `luv config` and can be edited by hand.
5
+
6
+ - [The `luv config` command](#the-luv-config-command)
7
+ - [Keys](#keys)
8
+ - [Resolution order](#resolution-order)
9
+ - [Multi-host example](#multi-host-example)
10
+ - [Not this file](#not-this-file)
11
+
12
+ ## The `luv config` command
13
+
14
+ ```bash
15
+ luv config # interactive wizard
16
+ luv config list # show everything, as dotted keys
17
+ luv config get remote.host
18
+ luv config set remote.host box
19
+ luv config unset remote.host # back to local-only
20
+ ```
21
+
22
+ `set` parses the value as JSON when it can, so numbers, booleans, and lists work
23
+ as you'd expect:
24
+
25
+ ```bash
26
+ luv config set remote.port 2222 # → 2222 (number)
27
+ luv config set remote.ssh_opts '["-o","Compression=yes"]'
28
+ luv config set org exosphere # → "exosphere" (string)
29
+ ```
30
+
31
+ Anything that isn't valid JSON is stored as a plain string, which is what you
32
+ want for hosts, paths, and org names.
33
+
34
+ `luv --init` still exists and sets only the org.
35
+
36
+ ## Keys
37
+
38
+ ### `org`
39
+
40
+ *string.* Default GitHub owner, used when you write `luv myrepo` instead of
41
+ `luv someorg/myrepo`. Set by `luv --init` or the wizard.
42
+
43
+ The **laptop** resolves this before dispatching, so a remote machine never needs
44
+ its own `org` configured.
45
+
46
+ ### `prs_dir`
47
+
48
+ *string, default `~/prs`.* Where workspace folders are created on **this**
49
+ machine. `~` is expanded.
50
+
51
+ ### `remote.host`
52
+
53
+ *string.* SSH destination — an `~/.ssh/config` alias (`box`), a `user@host`, or
54
+ a bare hostname. **Setting this turns on remote mode**: every workspace command
55
+ then runs there. Unset it, or pass `--local`, to run locally.
56
+
57
+ ### `remote.identity_file`
58
+
59
+ *string.* Path to the SSH private key, passed as `ssh -i`. `~` is expanded by
60
+ luv, since `ssh` won't expand it when luv execs directly rather than through a
61
+ shell.
62
+
63
+ ### `remote.port`
64
+
65
+ *number.* Passed as `ssh -p`. Omit for the default 22.
66
+
67
+ ### `remote.dir`
68
+
69
+ *string.* Workspace root **on the remote**. Forwarded to the remote luv, which
70
+ uses it in place of its own `prs_dir`.
71
+
72
+ ### `remote.luv_bin`
73
+
74
+ *string.* Full path to `luv` on the remote. Only needed when a login shell
75
+ there can't find it on `PATH` — see the
76
+ [troubleshooting section](remote-sessions.md#troubleshooting).
77
+
78
+ ### `remote.ssh_opts`
79
+
80
+ *list of strings.* Extra arguments inserted into every `ssh` invocation, e.g.
81
+ `["-o", "ServerAliveInterval=30"]`. Prefer `~/.ssh/config` for anything durable.
82
+
83
+ ### `remote.hosts.<name>`
84
+
85
+ *object.* Per-host overrides. Any of `identity_file`, `port`, `dir`, `luv_bin`,
86
+ or `ssh_opts` may be set here and takes precedence over the `remote.*` value of
87
+ the same name. This is what makes `-s otherbox` usable: without it, one global
88
+ key would be wrong for every host but the default.
89
+
90
+ ## Resolution order
91
+
92
+ **Host:** `-s HOST` → `remote.host`. If neither, luv runs locally.
93
+
94
+ **Per-host settings:** `-i PATH` (identity only) → `remote.hosts.<host>.<key>` →
95
+ `remote.<key>`.
96
+
97
+ **Workspace root:** `_LUV_PRS_DIR` (set by the dispatcher from `remote.dir`) →
98
+ `prs_dir` → `~/prs`.
99
+
100
+ **Org:** `org/repo` written explicitly → `org` from config → error.
101
+
102
+ `--local` overrides everything and is rejected alongside `-s` or `-i`, since
103
+ those combinations contradict each other.
104
+
105
+ ## Multi-host example
106
+
107
+ ```json
108
+ {
109
+ "org": "exosphere",
110
+ "prs_dir": "~/prs",
111
+ "remote": {
112
+ "host": "box",
113
+ "identity_file": "~/.ssh/id_ed25519",
114
+ "dir": "~/prs",
115
+ "ssh_opts": ["-o", "ServerAliveInterval=30"],
116
+ "hosts": {
117
+ "gpu-box": {
118
+ "identity_file": "~/.ssh/gpu_key",
119
+ "port": 2222,
120
+ "dir": "/scratch/prs",
121
+ "luv_bin": "/opt/venv/bin/luv"
122
+ }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ With this, `luv myrepo "…"` goes to `box` on port 22 with `id_ed25519`, and
129
+ `luv -s gpu-box ml "…"` goes to `gpu-box` on port 2222 with `gpu_key`, cloning
130
+ into `/scratch/prs` and using an explicit luv binary.
131
+
132
+ ## Not this file
133
+
134
+ Two other files are sometimes confused with this one:
135
+
136
+ - **`~/.luv/sessions.json`** — the session registry, written by luv itself. Not
137
+ configuration; see [sessions.md](sessions.md).
138
+ - **`<repo>/.luv/settings.json`** — a *repo-local* file, committed to the repo,
139
+ whose only key is `compose_file`. It turns on Docker mode for that repo and
140
+ has nothing to do with `~/.luv/config.json`.