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.
@@ -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"
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pocketshell = pocketshell.cli:main