live-cmd 0.1.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.
- live_cmd-0.1.0/.gitignore +7 -0
- live_cmd-0.1.0/DESIGN.md +91 -0
- live_cmd-0.1.0/DEVELOPMENT.md +64 -0
- live_cmd-0.1.0/PKG-INFO +53 -0
- live_cmd-0.1.0/README.md +40 -0
- live_cmd-0.1.0/pyproject.toml +29 -0
- live_cmd-0.1.0/src/live/__init__.py +1 -0
- live_cmd-0.1.0/src/live/cli.py +250 -0
- live_cmd-0.1.0/src/live/completion.py +312 -0
- live_cmd-0.1.0/src/live/config.py +80 -0
- live_cmd-0.1.0/src/live/follow.py +199 -0
- live_cmd-0.1.0/src/live/format.py +206 -0
- live_cmd-0.1.0/src/live/lock.py +82 -0
- live_cmd-0.1.0/src/live/paths.py +38 -0
- live_cmd-0.1.0/src/live/reader.py +529 -0
- live_cmd-0.1.0/src/live/recorder.py +441 -0
- live_cmd-0.1.0/src/live/select_session.py +48 -0
- live_cmd-0.1.0/src/live/sweep.py +221 -0
- live_cmd-0.1.0/src/live/verbose.py +48 -0
- live_cmd-0.1.0/src/live/verbs.py +495 -0
- live_cmd-0.1.0/src/live/watcher.py +270 -0
- live_cmd-0.1.0/tests/conftest.py +50 -0
- live_cmd-0.1.0/tests/test_ansi.py +50 -0
- live_cmd-0.1.0/tests/test_byte_cursor.py +58 -0
- live_cmd-0.1.0/tests/test_completion.py +74 -0
- live_cmd-0.1.0/tests/test_completion_live.py +374 -0
- live_cmd-0.1.0/tests/test_follow.py +177 -0
- live_cmd-0.1.0/tests/test_head.py +103 -0
- live_cmd-0.1.0/tests/test_hung.py +68 -0
- live_cmd-0.1.0/tests/test_ls_selector.py +46 -0
- live_cmd-0.1.0/tests/test_partial.py +75 -0
- live_cmd-0.1.0/tests/test_rotation.py +81 -0
- live_cmd-0.1.0/tests/test_smoke.py +74 -0
- live_cmd-0.1.0/tests/test_sweep.py +162 -0
- live_cmd-0.1.0/tests/test_time.py +80 -0
- live_cmd-0.1.0/tests/test_unit.py +147 -0
live_cmd-0.1.0/DESIGN.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# `live` — design
|
|
2
|
+
|
|
3
|
+
Stream long-lived command output to coding agents. `live run <cmd>` runs `<cmd>` under a PTY, mirrors output to the terminal, and records the bytes to disk under `~/.live/`. Agents read with `live cat`, `live tail`, or resumable `live tail -n +N`, piping to `grep`/`awk` as needed.
|
|
4
|
+
|
|
5
|
+
The recorder is the sole writer per session and holds an exclusive `flock` on `process.lock` for its lifetime — that lock IS the liveness signal. Read verbs hold no per-process state and piggyback lifecycle sweeps. No daemon, no broker, no persistent server.
|
|
6
|
+
|
|
7
|
+
Python 3.14+, POSIX-only (Linux, macOS, WSL). Zero runtime deps — PTY, flock, ioctl, signals, atomic rename, struct packing, JSON, UUIDv7, and the kqueue/inotify primitives that power `tail -f` are all stdlib. PyPI: `live-cmd`.
|
|
8
|
+
|
|
9
|
+
## CLI
|
|
10
|
+
|
|
11
|
+
| Verb | Purpose |
|
|
12
|
+
| --------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
13
|
+
| `live run [-n NAME] [--] <cmd…>` | Run `<cmd>` under a PTY; record. |
|
|
14
|
+
| `live ls [-a] [-g] [--json] [SELECTOR]` | List sessions in scope; `SELECTOR` filters by NAME or UUID-prefix. |
|
|
15
|
+
| `live cat [-v] [-g] [--strip-ansi\|--raw] <SEL>` | Concatenate session. |
|
|
16
|
+
| `live head [-v] [-g] [-n N\|-c K\|-t T] <SEL>` | `-n N` first N lines (default 10; `-N` drops last N), `-c K` first K bytes (`-K` drops last K), `-t T` lines with idx `t <= T`. |
|
|
17
|
+
| `live tail [-f] [-v] [-g] [-n N\|-c K\|-t T] <SEL>` | `-n N` last N lines (default 10; `+N` for `n >= N`), `-c K` last K bytes (`+K` for bytes past offset K), `-t T` lines with idx `t > T`; `-f` follows. |
|
|
18
|
+
| `live rm [-f] [-g] [--all-exited] <SEL…>` | Delete sessions; `-f` SIGTERMs live runs. |
|
|
19
|
+
| `live llms.txt` | Print agent guide. |
|
|
20
|
+
| `live completion <bash\|zsh\|fish>` | Print shell completion. |
|
|
21
|
+
| `live update-shell [SHELL]` | Install completion for `$SHELL` (or override). |
|
|
22
|
+
|
|
23
|
+
`live <verb> -h` for full flag details. `-g` widens scope from cwd-and-below to all sessions. ANSI: default strips when stdout isn't a TTY; `--strip-ansi` / `--raw` override.
|
|
24
|
+
|
|
25
|
+
Exit codes: `0` success; `1` runtime error; `2` usage error (bad flag, missing session, ambiguous selector).
|
|
26
|
+
|
|
27
|
+
## Selectors
|
|
28
|
+
|
|
29
|
+
A single positional token, resolved like a git ref:
|
|
30
|
+
|
|
31
|
+
1. **NAME** match wins. `cat`/`head`/`tail` pick the most recent; `rm` operates on all matches.
|
|
32
|
+
2. **UUID prefix** fallthrough. Unique match required; ambiguous → error.
|
|
33
|
+
|
|
34
|
+
"Most recent" = UUIDv7 lex-descending sort. Use `--` to pass a token starting with `-`.
|
|
35
|
+
|
|
36
|
+
## Verbose output
|
|
37
|
+
|
|
38
|
+
With `-v`, stderr carries metadata; without it, stderr is silent on success. Errors are always printed. The trailing line of any verbose read is:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
live: id=<uuid> at-line=<L> at-time=<T> at-byte=<B>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Agents using `tail -vn +N` pass `<L>+1` as the next cursor and reset to `0` when `<uuid>` changes. `<T>` (active stream mtime, partial-bytes-aware since heartbeats only touch idx) and `<B>` (cumulative byte cursor) are alternates for `tail -t T` / `tail -c +K`.
|
|
45
|
+
|
|
46
|
+
Possible preceding lines, in order: `dropped <k> lines …` (gap), `since=<N> > at-line=<L>; check id` / `time=<T> > at-time=<T>; check id` / `bytes=<B> > at-byte=<B>; check id` (cursor ahead), `partial-line bytes=<k> age=<s>`, `status=hung last-activity=<s>`, `exit=inconsistent`, `exit-code=<N>`. The last two can co-appear if the recorder wrote meta before a sweeper observed a torn recording.
|
|
47
|
+
|
|
48
|
+
## On-disk layout
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
~/.live/
|
|
52
|
+
config.json
|
|
53
|
+
sessions/
|
|
54
|
+
<uuid>/
|
|
55
|
+
meta.json # session metadata; writer-only, replaced atomically
|
|
56
|
+
process.lock # held by the recorder for its lifetime — liveness signal
|
|
57
|
+
deadAt # post-mortem marker; mtime = TTL clock, content = verdict
|
|
58
|
+
stream.NNNN.log # raw PTY bytes
|
|
59
|
+
lines.NNNN.idx # binary line index: struct.pack(">Qd", n, t), one per line
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The recorder appends to the highest-numbered pair; frozen segments are immutable until retention unlinks them. Session IDs are UUIDv7 (lex-monotonic = chronological).
|
|
63
|
+
|
|
64
|
+
Scope is a filter on `meta.cwd`: read verbs default to cwd-or-descendant (symlinks resolved); `-g` widens.
|
|
65
|
+
|
|
66
|
+
## Invariants
|
|
67
|
+
|
|
68
|
+
- **Single writer, lock = liveness.** Recorder holds the `flock` for its lifetime. Probe with non-blocking `LOCK_EX`: success = recorder is gone.
|
|
69
|
+
- **Prefix invariant.** Stream is always one complete line ahead of, or equal to, the index — never the inverse. Crash leaves an unindexed complete line; sweepers stamp it `inconsistent`.
|
|
70
|
+
- **Absolute line numbers.** `n` is monotonic across the session's lifetime. Retention deletes but never renumbers; cursors past the oldest retained line get a `dropped` notice.
|
|
71
|
+
- **Heartbeat.** Recorder advances the active idx mtime every `heartbeatSec`. Staleness past `3 × heartbeatSec` while the lock is held = `hung`.
|
|
72
|
+
- **Sweep on every read.** Each verb that touches sessions stamps dead-but-unmarked ones (exclusive create of `deadAt`) and deletes those past `ttlDays`. Races are safe.
|
|
73
|
+
- **Graceful exit ordering.** `meta.json` → `deadAt` → unlock, in that order, so no sweeper races in with a wrong verdict.
|
|
74
|
+
- **Signals.** `SIGWINCH` propagates window size. `SIGTERM`/`SIGHUP` forward to the child. `SIGINT` forwards only when stdin isn't a TTY (otherwise line discipline routes ^C directly). `live run` exits with the child's code, or `128 + signum` if signal-killed.
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
`~/.live/config.json`, auto-created. Partial files valid; unknown keys ignored; malformed fields fall back to defaults with a stderr warning.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{ "ttlDays": 7, "maxKb": 512, "segmentKb": 64, "heartbeatSec": 30 }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Default | |
|
|
85
|
+
| ------------ | ------------------------------------------------------------ |
|
|
86
|
+
| Store | `~/.live/sessions/` (single global store) |
|
|
87
|
+
| Scope | cwd descendants by default; `-g` widens to all |
|
|
88
|
+
| TTL | 7 days from `deadAt` mtime, dead sessions only |
|
|
89
|
+
| Segment size | 64 KB rotation threshold; lines never split |
|
|
90
|
+
| Retention | 512 KB total per session; oldest segments unlinked when over |
|
|
91
|
+
| Heartbeat | active idx mtime advanced every 30 s |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Install from source:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
cd path/to/here
|
|
7
|
+
uv venv --python ">=3.14"
|
|
8
|
+
source .venv/bin/activate
|
|
9
|
+
uv pip install -e .
|
|
10
|
+
live --version
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`live` is now on `$PATH` for the activated shell, and edits to `src/live/` take
|
|
14
|
+
effect on the next invocation.
|
|
15
|
+
|
|
16
|
+
## Tests
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uv pip install pytest
|
|
20
|
+
pytest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Some completion tests are skipped unless `bash`, `zsh`, and/or `fish` are on `$PATH`.
|
|
24
|
+
|
|
25
|
+
## Release
|
|
26
|
+
|
|
27
|
+
1. Bump `version` in [`pyproject.toml`](pyproject.toml).
|
|
28
|
+
2. Commit and tag:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
git commit -am "release vX.Y.Z"
|
|
32
|
+
git tag vX.Y.Z
|
|
33
|
+
git push && git push --tags
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Build sdist + wheel into `dist/`:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
rm -rf dist/
|
|
40
|
+
uv build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
4. Sanity-check the wheel installs cleanly in a throwaway env:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv tool install --from dist/live_cmd-X.Y.Z-py3-none-any.whl live-cmd
|
|
47
|
+
live --version
|
|
48
|
+
uv tool uninstall live-cmd
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. Publish to TestPyPI first, install from it, smoke-test:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
uv publish --publish-url https://test.pypi.org/legacy/ --token "$TESTPYPI_TOKEN"
|
|
55
|
+
uv tool install --index https://test.pypi.org/simple/ live-cmd
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
6. Publish to PyPI:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
uv publish --token "$PYPI_TOKEN"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
API tokens come from <https://pypi.org/manage/account/token/> (and the TestPyPI equivalent). Scope them to the `live-cmd` project once it's been registered.
|
live_cmd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: live-cmd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Live stream command line output.
|
|
5
|
+
Project-URL: Homepage, https://github.com/astralarium/live
|
|
6
|
+
Author: Mara Kim
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: POSIX
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# live
|
|
15
|
+
|
|
16
|
+
Live stream command line output.
|
|
17
|
+
|
|
18
|
+
See [DESIGN.md](DESIGN.md) for the design spec and [DEVELOPMENT.md](DEVELOPMENT.md) for working on `live` itself.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pipx install live-cmd
|
|
24
|
+
# or
|
|
25
|
+
uv tool install live-cmd
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
live run -n dev npm run dev # record under PTY, mirror to terminal
|
|
32
|
+
live ls # list sessions started under cwd
|
|
33
|
+
live ls -g # list all sessions
|
|
34
|
+
live tail -vn +0 dev # resumable polling for agents
|
|
35
|
+
live cat dev # full output
|
|
36
|
+
live rm dev # delete
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sessions are stored under `~/.live/sessions/`; `live ls`/`cat`/`head`/`tail`/`rm` filter to sessions started in the current directory. Pass `-g` / `--global` to search globally.
|
|
40
|
+
|
|
41
|
+
## Shell completion
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
live update-shell # detect $SHELL, install completion, reload your shell
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or print the script and place it yourself:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
live completion bash > ~/.local/share/bash-completion/completions/live
|
|
51
|
+
live completion zsh > "${fpath[1]}/_live"
|
|
52
|
+
live completion fish > ~/.config/fish/completions/live.fish
|
|
53
|
+
```
|
live_cmd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# live
|
|
2
|
+
|
|
3
|
+
Live stream command line output.
|
|
4
|
+
|
|
5
|
+
See [DESIGN.md](DESIGN.md) for the design spec and [DEVELOPMENT.md](DEVELOPMENT.md) for working on `live` itself.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pipx install live-cmd
|
|
11
|
+
# or
|
|
12
|
+
uv tool install live-cmd
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
live run -n dev npm run dev # record under PTY, mirror to terminal
|
|
19
|
+
live ls # list sessions started under cwd
|
|
20
|
+
live ls -g # list all sessions
|
|
21
|
+
live tail -vn +0 dev # resumable polling for agents
|
|
22
|
+
live cat dev # full output
|
|
23
|
+
live rm dev # delete
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sessions are stored under `~/.live/sessions/`; `live ls`/`cat`/`head`/`tail`/`rm` filter to sessions started in the current directory. Pass `-g` / `--global` to search globally.
|
|
27
|
+
|
|
28
|
+
## Shell completion
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
live update-shell # detect $SHELL, install completion, reload your shell
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or print the script and place it yourself:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
live completion bash > ~/.local/share/bash-completion/completions/live
|
|
38
|
+
live completion zsh > "${fpath[1]}/_live"
|
|
39
|
+
live completion fish > ~/.config/fish/completions/live.fish
|
|
40
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "live-cmd"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Live stream command line output."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Mara Kim" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: POSIX",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
live = "live.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/astralarium/live"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/live"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Top-level CLI dispatch: `live <verb> ...`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from . import verbs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _count_or_cursor(prefix: str):
|
|
13
|
+
"""Build a parser for `N` (count) or `<prefix>N` (cursor) on `-n` / `-c`.
|
|
14
|
+
|
|
15
|
+
`tail` uses `+N` (lines `n >= N`, Unix); `head` uses `-N` (drop last N,
|
|
16
|
+
GNU). The opposite sign is accepted but treated as a plain count.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def parse(value: str) -> tuple[str, int]:
|
|
20
|
+
if value.startswith(prefix):
|
|
21
|
+
rest = value[1:]
|
|
22
|
+
if rest.isdigit():
|
|
23
|
+
return ("cursor", int(rest))
|
|
24
|
+
elif value.startswith(("+", "-")) and value[1:].isdigit():
|
|
25
|
+
return ("count", int(value[1:]))
|
|
26
|
+
elif value.isdigit():
|
|
27
|
+
return ("count", int(value))
|
|
28
|
+
raise argparse.ArgumentTypeError(f"expected N or {prefix}N (got {value!r})")
|
|
29
|
+
|
|
30
|
+
return parse
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _make_parser() -> argparse.ArgumentParser:
|
|
34
|
+
p = argparse.ArgumentParser(
|
|
35
|
+
prog="live",
|
|
36
|
+
description="Stream long-lived command output to coding agents.",
|
|
37
|
+
add_help=True,
|
|
38
|
+
)
|
|
39
|
+
p.add_argument("--version", action="version", version=f"live {__version__}")
|
|
40
|
+
sub = p.add_subparsers(dest="verb", metavar="<verb>")
|
|
41
|
+
|
|
42
|
+
# run
|
|
43
|
+
run_p = sub.add_parser("run", help="Run <cmd> under a PTY; record.")
|
|
44
|
+
run_p.add_argument(
|
|
45
|
+
"-n", "--name", default=None, help="Session name."
|
|
46
|
+
)
|
|
47
|
+
run_p.add_argument(
|
|
48
|
+
"cmd",
|
|
49
|
+
nargs=argparse.REMAINDER,
|
|
50
|
+
help="Command to run; `--` for flag-starting commands.",
|
|
51
|
+
)
|
|
52
|
+
run_p.set_defaults(func=verbs.cmd_run)
|
|
53
|
+
|
|
54
|
+
# ls
|
|
55
|
+
ls_p = sub.add_parser("ls", help="List sessions in scope.")
|
|
56
|
+
ls_p.add_argument("-a", "--all", action="store_true", help="Include exited.")
|
|
57
|
+
ls_p.add_argument(
|
|
58
|
+
"-g",
|
|
59
|
+
"--global",
|
|
60
|
+
action="store_true",
|
|
61
|
+
dest="global_",
|
|
62
|
+
help="Global scope.",
|
|
63
|
+
)
|
|
64
|
+
ls_p.add_argument("--json", action="store_true", help="Emit NDJSON.")
|
|
65
|
+
ls_p.add_argument(
|
|
66
|
+
"selector", nargs="?", default=None, help="NAME or UUID-prefix filter."
|
|
67
|
+
)
|
|
68
|
+
ls_p.set_defaults(func=verbs.cmd_ls)
|
|
69
|
+
|
|
70
|
+
# cat
|
|
71
|
+
cat_p = sub.add_parser("cat", help="Concatenate session.")
|
|
72
|
+
cat_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
73
|
+
cat_p.add_argument(
|
|
74
|
+
"-g",
|
|
75
|
+
"--global",
|
|
76
|
+
action="store_true",
|
|
77
|
+
dest="global_",
|
|
78
|
+
help="Global scope.",
|
|
79
|
+
)
|
|
80
|
+
ag = cat_p.add_mutually_exclusive_group()
|
|
81
|
+
ag.add_argument(
|
|
82
|
+
"--strip-ansi",
|
|
83
|
+
action="store_true",
|
|
84
|
+
dest="strip_ansi",
|
|
85
|
+
help="Strip ANSI.",
|
|
86
|
+
)
|
|
87
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
88
|
+
cat_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
89
|
+
cat_p.set_defaults(func=verbs.cmd_cat)
|
|
90
|
+
|
|
91
|
+
# head
|
|
92
|
+
head_p = sub.add_parser("head", help="Head session.")
|
|
93
|
+
head_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
94
|
+
head_p.add_argument(
|
|
95
|
+
"-g",
|
|
96
|
+
"--global",
|
|
97
|
+
action="store_true",
|
|
98
|
+
dest="global_",
|
|
99
|
+
help="Global scope.",
|
|
100
|
+
)
|
|
101
|
+
ag = head_p.add_mutually_exclusive_group()
|
|
102
|
+
ag.add_argument(
|
|
103
|
+
"--strip-ansi",
|
|
104
|
+
action="store_true",
|
|
105
|
+
dest="strip_ansi",
|
|
106
|
+
help="Strip ANSI.",
|
|
107
|
+
)
|
|
108
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
109
|
+
mode = head_p.add_mutually_exclusive_group()
|
|
110
|
+
mode.add_argument(
|
|
111
|
+
"-n",
|
|
112
|
+
"--lines",
|
|
113
|
+
type=_count_or_cursor("-"),
|
|
114
|
+
default=None,
|
|
115
|
+
help="First N lines (default 10); -N drops last N.",
|
|
116
|
+
)
|
|
117
|
+
mode.add_argument(
|
|
118
|
+
"-c",
|
|
119
|
+
"--bytes",
|
|
120
|
+
dest="bytes_",
|
|
121
|
+
metavar="BYTES",
|
|
122
|
+
type=_count_or_cursor("-"),
|
|
123
|
+
default=None,
|
|
124
|
+
help="First K bytes; -K drops last K.",
|
|
125
|
+
)
|
|
126
|
+
mode.add_argument(
|
|
127
|
+
"-t",
|
|
128
|
+
"--time",
|
|
129
|
+
type=float,
|
|
130
|
+
default=None,
|
|
131
|
+
help="Lines with idx t <= T (epoch).",
|
|
132
|
+
)
|
|
133
|
+
head_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
134
|
+
head_p.set_defaults(func=verbs.cmd_head)
|
|
135
|
+
|
|
136
|
+
# tail
|
|
137
|
+
tail_p = sub.add_parser("tail", help="Tail session.")
|
|
138
|
+
tail_p.add_argument(
|
|
139
|
+
"-f", "--follow", action="store_true", help="Follow until exit."
|
|
140
|
+
)
|
|
141
|
+
tail_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
142
|
+
tail_p.add_argument(
|
|
143
|
+
"-g",
|
|
144
|
+
"--global",
|
|
145
|
+
action="store_true",
|
|
146
|
+
dest="global_",
|
|
147
|
+
help="Global scope.",
|
|
148
|
+
)
|
|
149
|
+
ag = tail_p.add_mutually_exclusive_group()
|
|
150
|
+
ag.add_argument(
|
|
151
|
+
"--strip-ansi",
|
|
152
|
+
action="store_true",
|
|
153
|
+
dest="strip_ansi",
|
|
154
|
+
help="Strip ANSI.",
|
|
155
|
+
)
|
|
156
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
157
|
+
mode = tail_p.add_mutually_exclusive_group()
|
|
158
|
+
mode.add_argument(
|
|
159
|
+
"-n",
|
|
160
|
+
"--lines",
|
|
161
|
+
type=_count_or_cursor("+"),
|
|
162
|
+
default=None,
|
|
163
|
+
help="Last N lines (default 10); +N for lines n >= N.",
|
|
164
|
+
)
|
|
165
|
+
mode.add_argument(
|
|
166
|
+
"-c",
|
|
167
|
+
"--bytes",
|
|
168
|
+
dest="bytes_",
|
|
169
|
+
metavar="BYTES",
|
|
170
|
+
type=_count_or_cursor("+"),
|
|
171
|
+
default=None,
|
|
172
|
+
help="Last K bytes; +K for bytes after offset K.",
|
|
173
|
+
)
|
|
174
|
+
mode.add_argument(
|
|
175
|
+
"-t",
|
|
176
|
+
"--time",
|
|
177
|
+
type=float,
|
|
178
|
+
default=None,
|
|
179
|
+
help="Lines with idx t > T (epoch).",
|
|
180
|
+
)
|
|
181
|
+
tail_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
182
|
+
tail_p.set_defaults(func=verbs.cmd_tail)
|
|
183
|
+
|
|
184
|
+
# rm
|
|
185
|
+
rm_p = sub.add_parser("rm", help="Delete sessions.")
|
|
186
|
+
rm_p.add_argument(
|
|
187
|
+
"-f",
|
|
188
|
+
"--force",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="SIGTERM live runs; ignore missing.",
|
|
191
|
+
)
|
|
192
|
+
rm_p.add_argument(
|
|
193
|
+
"-g",
|
|
194
|
+
"--global",
|
|
195
|
+
action="store_true",
|
|
196
|
+
dest="global_",
|
|
197
|
+
help="Global scope.",
|
|
198
|
+
)
|
|
199
|
+
rm_p.add_argument(
|
|
200
|
+
"--all-exited",
|
|
201
|
+
action="store_true",
|
|
202
|
+
dest="all_exited",
|
|
203
|
+
help="Remove all dead sessions.",
|
|
204
|
+
)
|
|
205
|
+
rm_p.add_argument(
|
|
206
|
+
"selectors", nargs="*", help="NAME(s) or UUID-prefix(es)."
|
|
207
|
+
)
|
|
208
|
+
rm_p.set_defaults(func=verbs.cmd_rm)
|
|
209
|
+
|
|
210
|
+
# llms.txt
|
|
211
|
+
llms_p = sub.add_parser("llms.txt", help="Print agent guide.")
|
|
212
|
+
llms_p.set_defaults(func=verbs.cmd_llms_txt)
|
|
213
|
+
|
|
214
|
+
# completion
|
|
215
|
+
comp_p = sub.add_parser("completion", help="Print shell completion script.")
|
|
216
|
+
comp_p.add_argument("shell", choices=["bash", "zsh", "fish"])
|
|
217
|
+
comp_p.set_defaults(func=verbs.cmd_completion)
|
|
218
|
+
|
|
219
|
+
# update-shell
|
|
220
|
+
up_p = sub.add_parser(
|
|
221
|
+
"update-shell", help="Install completion for the current shell."
|
|
222
|
+
)
|
|
223
|
+
up_p.add_argument(
|
|
224
|
+
"shell",
|
|
225
|
+
nargs="?",
|
|
226
|
+
choices=["bash", "zsh", "fish"],
|
|
227
|
+
default=None,
|
|
228
|
+
help="Target shell (default: $SHELL).",
|
|
229
|
+
)
|
|
230
|
+
up_p.set_defaults(func=verbs.cmd_update_shell)
|
|
231
|
+
|
|
232
|
+
return p
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(argv: list[str] | None = None) -> int:
|
|
236
|
+
args = argv if argv is not None else sys.argv[1:]
|
|
237
|
+
parser = _make_parser()
|
|
238
|
+
|
|
239
|
+
parsed = parser.parse_args(args)
|
|
240
|
+
if not getattr(parsed, "verb", None):
|
|
241
|
+
parser.print_help()
|
|
242
|
+
return 0
|
|
243
|
+
try:
|
|
244
|
+
return parsed.func(parsed)
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
return 130
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|