pocketshell 0.1.0__py3-none-any.whl
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.
- pocketshell/__init__.py +14 -0
- pocketshell/__main__.py +14 -0
- pocketshell/cli.py +67 -0
- pocketshell/usage.py +115 -0
- pocketshell-0.1.0.dist-info/METADATA +108 -0
- pocketshell-0.1.0.dist-info/RECORD +8 -0
- pocketshell-0.1.0.dist-info/WHEEL +4 -0
- pocketshell-0.1.0.dist-info/entry_points.txt +2 -0
pocketshell/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Unified server-side Python utility for the PocketShell Android client.
|
|
2
|
+
|
|
3
|
+
This package is intended to replace the separately-installed `quse` and
|
|
4
|
+
`tmuxctl` utilities the PocketShell app currently probes for. The first PR
|
|
5
|
+
ships the skeleton plus the `pocketshell usage` subcommand only; later PRs
|
|
6
|
+
will add `jobs`, `agent-log`, `sessions`, `repos`, and daemon mode.
|
|
7
|
+
|
|
8
|
+
See https://github.com/alexeygrigorev/pocketshell/issues/170.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
|
14
|
+
__version__ = "0.1.0"
|
pocketshell/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Allow `python -m pocketshell` as an alternative to the installed entry point.
|
|
2
|
+
|
|
3
|
+
Used in CI and developer environments where the console-script shim from
|
|
4
|
+
`pip install -e .` / `uv tool install pocketshell-cli` is not on PATH.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from pocketshell.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
sys.exit(main())
|
pocketshell/cli.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Top-level Click dispatcher for the unified `pocketshell` CLI.
|
|
2
|
+
|
|
3
|
+
This is the skeleton landed in the first PR of issue
|
|
4
|
+
[#170](https://github.com/alexeygrigorev/pocketshell/issues/170). Only the
|
|
5
|
+
`usage` subcommand is wired up today; later PRs will add `jobs`,
|
|
6
|
+
`agent-log`, `sessions`, `repos`, and `daemon`.
|
|
7
|
+
|
|
8
|
+
Per the D22 locked principle (no backwards compatibility, hard cuts only)
|
|
9
|
+
the eventual goal is for the PocketShell Android app to probe for this
|
|
10
|
+
single binary instead of `quse` / `tmuxctl`. The first PR keeps the
|
|
11
|
+
existing probes in place — parallel detection, not legacy detection — so
|
|
12
|
+
the app keeps working while we ramp up `pocketshell`'s feature parity.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional, Sequence
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from pocketshell import __version__
|
|
23
|
+
from pocketshell.usage import usage_command
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group(
|
|
27
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
28
|
+
help=(
|
|
29
|
+
"Unified server-side helper for the PocketShell Android client.\n\n"
|
|
30
|
+
"Subcommands replace the separately-installed `quse` and `tmuxctl` "
|
|
31
|
+
"CLIs. The first PR ships `usage` only; more subcommands will land "
|
|
32
|
+
"in follow-up rounds."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
@click.version_option(__version__, "-V", "--version", prog_name="pocketshell")
|
|
36
|
+
def cli() -> None:
|
|
37
|
+
"""Top-level group. Each subcommand is registered below."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
cli.add_command(usage_command, name="usage")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
44
|
+
"""Entrypoint for both the console-script and `python -m pocketshell`.
|
|
45
|
+
|
|
46
|
+
Returns an integer exit code rather than letting Click call
|
|
47
|
+
`sys.exit` so the function is testable from the unit suite.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
result = cli.main(args=list(argv) if argv is not None else None,
|
|
51
|
+
prog_name="pocketshell",
|
|
52
|
+
standalone_mode=False)
|
|
53
|
+
except click.exceptions.Exit as exc:
|
|
54
|
+
return int(exc.exit_code)
|
|
55
|
+
except click.ClickException as exc:
|
|
56
|
+
exc.show()
|
|
57
|
+
return int(exc.exit_code)
|
|
58
|
+
if result is None:
|
|
59
|
+
return 0
|
|
60
|
+
try:
|
|
61
|
+
return int(result)
|
|
62
|
+
except (TypeError, ValueError):
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
pocketshell/usage.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""`pocketshell usage` subcommand.
|
|
2
|
+
|
|
3
|
+
First-PR implementation: delegate to the existing `quse` CLI via
|
|
4
|
+
`subprocess.run`. The arguments and stdout are proxied through verbatim
|
|
5
|
+
so the JSON payload (and human-readable lines) are byte-identical to
|
|
6
|
+
`quse [provider] [--json]`. The existing Kotlin `QuseUsageJsonParser`
|
|
7
|
+
keeps working when the Android app eventually switches its probe over.
|
|
8
|
+
|
|
9
|
+
Why subprocess instead of `import quse`:
|
|
10
|
+
|
|
11
|
+
- `quse` is currently not published to PyPI; declaring it as a normal
|
|
12
|
+
`pyproject.toml` dependency would break `uv tool install
|
|
13
|
+
pocketshell-cli` for any user (including the maintainer's dev box).
|
|
14
|
+
- Subprocess delegation keeps `pocketshell-cli` decoupled from `quse`'s
|
|
15
|
+
internal module layout, so updates to `quse` don't break the wrapper.
|
|
16
|
+
- The PATH-discovery story for `quse` is already solved on the app side
|
|
17
|
+
(see issue #41 + the `pathOverride` column on `HostEntity`). Wrapping
|
|
18
|
+
`quse` here means the app's existing PATH override mechanism keeps
|
|
19
|
+
working without re-implementation.
|
|
20
|
+
|
|
21
|
+
Later PRs will fold the provider-detection logic in directly so
|
|
22
|
+
`pocketshell-cli` is the canonical implementation and the subprocess
|
|
23
|
+
hop disappears, but that is explicit non-scope here per the brief on
|
|
24
|
+
issue [#170](https://github.com/alexeygrigorev/pocketshell/issues/170).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from typing import Optional, Sequence
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_quse_binary() -> Optional[str]:
|
|
38
|
+
"""Locate the `quse` CLI on PATH, or return ``None`` if absent.
|
|
39
|
+
|
|
40
|
+
Pulled out as a function so the unit suite can monkeypatch it.
|
|
41
|
+
`shutil.which` returns the same path the user would see from
|
|
42
|
+
`command -v quse`, which is the probe the Android app already runs.
|
|
43
|
+
"""
|
|
44
|
+
return shutil.which("quse")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _run_quse(args: Sequence[str]) -> int:
|
|
48
|
+
"""Invoke `quse` with [args]; proxy stdout/stderr and exit code.
|
|
49
|
+
|
|
50
|
+
Using `subprocess.run(..., check=False)` and forwarding the captured
|
|
51
|
+
output rather than `os.execvp` keeps the call testable (the test
|
|
52
|
+
suite can monkeypatch `subprocess.run`) and lets us decorate the
|
|
53
|
+
failure mode with a friendly hint when `quse` is missing.
|
|
54
|
+
"""
|
|
55
|
+
quse_path = _resolve_quse_binary()
|
|
56
|
+
if quse_path is None:
|
|
57
|
+
# Same wording the bootstrap sheet uses so the user sees a
|
|
58
|
+
# consistent message whether they hit the bin via `pocketshell
|
|
59
|
+
# usage` or the app's poll loop.
|
|
60
|
+
click.echo(
|
|
61
|
+
"pocketshell: `quse` is not installed on this host. "
|
|
62
|
+
"Install it via `uv tool install quse` or `pipx install quse` "
|
|
63
|
+
"and re-run.",
|
|
64
|
+
err=True,
|
|
65
|
+
)
|
|
66
|
+
return 127
|
|
67
|
+
|
|
68
|
+
completed = subprocess.run(
|
|
69
|
+
[quse_path, *args],
|
|
70
|
+
check=False,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
)
|
|
74
|
+
# Echo verbatim so the JSON output is byte-identical to `quse --json`.
|
|
75
|
+
if completed.stdout:
|
|
76
|
+
sys.stdout.write(completed.stdout)
|
|
77
|
+
if completed.stderr:
|
|
78
|
+
sys.stderr.write(completed.stderr)
|
|
79
|
+
return completed.returncode
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.command(
|
|
83
|
+
context_settings={"help_option_names": ["-h", "--help"], "ignore_unknown_options": True},
|
|
84
|
+
)
|
|
85
|
+
@click.argument("provider", required=False)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--json",
|
|
88
|
+
"json_output",
|
|
89
|
+
is_flag=True,
|
|
90
|
+
help="Emit machine-readable JSON output identical to `quse --json`.",
|
|
91
|
+
)
|
|
92
|
+
@click.pass_context
|
|
93
|
+
def usage_command(
|
|
94
|
+
ctx: click.Context,
|
|
95
|
+
provider: Optional[str] = None,
|
|
96
|
+
json_output: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Report quota / usage for coding-agent providers on this host.
|
|
99
|
+
|
|
100
|
+
Delegates to the `quse` CLI via subprocess. Output shape (both human
|
|
101
|
+
and JSON) is byte-identical to `quse [provider] [--json]` so any
|
|
102
|
+
consumer that already parses `quse` output keeps working when the
|
|
103
|
+
PocketShell app routes through `pocketshell usage` instead.
|
|
104
|
+
"""
|
|
105
|
+
args: list[str] = []
|
|
106
|
+
if provider:
|
|
107
|
+
args.append(provider)
|
|
108
|
+
if json_output:
|
|
109
|
+
args.append("--json")
|
|
110
|
+
exit_code = _run_quse(args)
|
|
111
|
+
# Click ignores the return value of a callback by default; we need to
|
|
112
|
+
# explicitly propagate non-zero exit codes through `ctx.exit` so the
|
|
113
|
+
# outer `main()` (and the OS) sees the same exit code `quse` reported.
|
|
114
|
+
if exit_code != 0:
|
|
115
|
+
ctx.exit(exit_code)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pocketshell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
|
+
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
|
+
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
7
|
+
Author: Alexey Grigorev
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agents,pocketshell,ssh,tmux,usage
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: click>=8.2.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.15.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pocketshell-cli
|
|
28
|
+
|
|
29
|
+
Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
|
|
30
|
+
Android client. Replaces the separately-installed `quse` and `tmuxctl`
|
|
31
|
+
utilities the app currently probes for on every remote host.
|
|
32
|
+
|
|
33
|
+
This first release ships the **skeleton plus the `pocketshell usage`
|
|
34
|
+
subcommand only**. Follow-up rounds will add `jobs`, `agent-log`,
|
|
35
|
+
`sessions`, `repos`, and an optional daemon mode. See
|
|
36
|
+
[issue #170](https://github.com/alexeygrigorev/pocketshell/issues/170) for
|
|
37
|
+
the design spike and phased roll-out plan.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
The recommended path is `uv tool install`, which lands the binary on PATH
|
|
42
|
+
under `~/.local/bin/`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install pocketshell-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For local development from a clone:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd tools/pocketshell-cli
|
|
52
|
+
uv venv
|
|
53
|
+
uv pip install -e .
|
|
54
|
+
pocketshell --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`pipx install pocketshell-cli` works the same way for users who prefer
|
|
58
|
+
pipx. Both install paths produce a `pocketshell` binary that the
|
|
59
|
+
PocketShell app's bootstrap probe detects.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
pocketshell usage # human-readable lines, one per provider
|
|
65
|
+
pocketshell usage --json # machine-readable JSON (consumed by the app)
|
|
66
|
+
pocketshell usage codex # filter to a single provider
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The output shape is byte-identical to `quse [provider] [--json]` so any
|
|
70
|
+
consumer that already parses `quse` output keeps working when the app
|
|
71
|
+
routes through `pocketshell usage` instead. Under the hood the first
|
|
72
|
+
release delegates to the `quse` CLI via subprocess; later rounds will
|
|
73
|
+
fold the provider-detection logic in directly and drop the subprocess
|
|
74
|
+
hop.
|
|
75
|
+
|
|
76
|
+
If `quse` is not installed, `pocketshell usage` exits with code 127 and
|
|
77
|
+
prints an install hint to stderr.
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cd tools/pocketshell-cli
|
|
83
|
+
uv venv
|
|
84
|
+
uv pip install -e ".[dev]"
|
|
85
|
+
uv run pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or via the dependency-group:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync --group dev
|
|
92
|
+
uv run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The tests stub `quse.usage.collect_usage` so they run in seconds without
|
|
96
|
+
hitting any provider API.
|
|
97
|
+
|
|
98
|
+
## Why a unified CLI?
|
|
99
|
+
|
|
100
|
+
The PocketShell app previously probed for two binaries (`quse`,
|
|
101
|
+
`tmuxctl`) on every host. That meant two installs to keep up to date,
|
|
102
|
+
two probes to surface failures from, and two PATH-discovery edge cases
|
|
103
|
+
(see [issue #41](https://github.com/alexeygrigorev/pocketshell/issues/41)).
|
|
104
|
+
A single `pocketshell` binary collapses those into one install, one
|
|
105
|
+
probe, one bootstrap row. The app keeps detecting `quse` and `tmuxctl`
|
|
106
|
+
as a parallel path while `pocketshell` ramps up to feature parity; once
|
|
107
|
+
parity is reached, the legacy probes are removed in a hard-cut follow-up
|
|
108
|
+
(no compat shim — see decision D22 in `docs/decisions.md`).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pocketshell/__init__.py,sha256=eFpihxo-FUOOyZmY5Xv9rbtU4n57-okAzai3FRHSJN0,517
|
|
2
|
+
pocketshell/__main__.py,sha256=DQ0VPFw3Flj0Qm92SIEOKqKg6ESov4fcmxiWTTVKJ20,361
|
|
3
|
+
pocketshell/cli.py,sha256=4PzpqH4E7tK39aKuH-BEA3WQ8gL5socFg1uujGP_1Yw,2221
|
|
4
|
+
pocketshell/usage.py,sha256=JoefJo6TnuF5dinwb-tSH7Yp5xQIA1OTrDDa6mrFBvU,4271
|
|
5
|
+
pocketshell-0.1.0.dist-info/METADATA,sha256=fyyPDngTt1eDZmJjENpdguzqymtdRCS-q4KcBbFiERU,3659
|
|
6
|
+
pocketshell-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
pocketshell-0.1.0.dist-info/entry_points.txt,sha256=XR1LQMp6m3b5B1jQlNocYv5OZf9PJqxKIDWOdiqduHo,53
|
|
8
|
+
pocketshell-0.1.0.dist-info/RECORD,,
|