pocketshell 0.1.0__tar.gz → 0.3.1__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.
- pocketshell-0.3.1/PKG-INFO +306 -0
- pocketshell-0.3.1/README.md +278 -0
- {pocketshell-0.1.0 → pocketshell-0.3.1}/pyproject.toml +14 -1
- {pocketshell-0.1.0 → pocketshell-0.3.1}/src/pocketshell/__main__.py +1 -1
- pocketshell-0.3.1/src/pocketshell/agent_log.py +390 -0
- pocketshell-0.3.1/src/pocketshell/cli.py +223 -0
- pocketshell-0.3.1/src/pocketshell/daemon.py +1095 -0
- pocketshell-0.3.1/src/pocketshell/jobs.py +539 -0
- pocketshell-0.3.1/src/pocketshell/qr_share.py +429 -0
- pocketshell-0.3.1/src/pocketshell/repos.py +1542 -0
- pocketshell-0.3.1/src/pocketshell/sessions.py +154 -0
- pocketshell-0.3.1/src/pocketshell/usage.py +222 -0
- pocketshell-0.3.1/tests/test_agent_log.py +459 -0
- pocketshell-0.3.1/tests/test_daemon.py +760 -0
- pocketshell-0.3.1/tests/test_jobs.py +734 -0
- pocketshell-0.3.1/tests/test_qr_share.py +241 -0
- pocketshell-0.3.1/tests/test_repos.py +1833 -0
- pocketshell-0.3.1/tests/test_sessions.py +227 -0
- pocketshell-0.3.1/uv.lock +247 -0
- pocketshell-0.1.0/PKG-INFO +0 -108
- pocketshell-0.1.0/README.md +0 -82
- pocketshell-0.1.0/src/pocketshell/cli.py +0 -67
- pocketshell-0.1.0/src/pocketshell/usage.py +0 -115
- pocketshell-0.1.0/uv.lock +0 -139
- {pocketshell-0.1.0 → pocketshell-0.3.1}/.gitignore +0 -0
- {pocketshell-0.1.0 → pocketshell-0.3.1}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.1.0 → pocketshell-0.3.1}/tests/__init__.py +0 -0
- {pocketshell-0.1.0 → pocketshell-0.3.1}/tests/test_usage.py +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pocketshell
|
|
3
|
+
Version: 0.3.1
|
|
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
|
+
Provides-Extra: qr
|
|
26
|
+
Requires-Dist: qrcode[pil]>=7.4; extra == 'qr'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pocketshell
|
|
30
|
+
|
|
31
|
+
Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
|
|
32
|
+
Android client. Replaces the separately-installed `quse` and `tmuxctl`
|
|
33
|
+
utilities the app currently probes for on every remote host.
|
|
34
|
+
|
|
35
|
+
This first release ships the **skeleton plus the `pocketshell usage`
|
|
36
|
+
subcommand only**. Follow-up rounds will add `jobs`, `agent-log`,
|
|
37
|
+
`sessions`, `repos`, and an optional daemon mode. See
|
|
38
|
+
[issue #170](https://github.com/alexeygrigorev/pocketshell/issues/170) for
|
|
39
|
+
the design spike and phased roll-out plan.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
The recommended path is `uv tool install`, which lands the binary on PATH
|
|
44
|
+
under `~/.local/bin/`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install pocketshell
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For local development from a clone:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd tools/pocketshell
|
|
54
|
+
uv venv
|
|
55
|
+
uv pip install -e .
|
|
56
|
+
pocketshell --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`pipx install pocketshell` works the same way for users who prefer
|
|
60
|
+
pipx. Both install paths produce a `pocketshell` binary that the
|
|
61
|
+
PocketShell app's bootstrap probe detects.
|
|
62
|
+
|
|
63
|
+
### Optional extras
|
|
64
|
+
|
|
65
|
+
`pocketshell qr-share` requires the `qrcode[pil]` package (Pillow) to
|
|
66
|
+
render QR images. Because Pillow is heavy and not needed by any other
|
|
67
|
+
subcommand, it ships behind an optional `qr` extra:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv tool install pocketshell --with qrcode[pil]
|
|
71
|
+
# or
|
|
72
|
+
pip install pocketshell[qr]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without the extra, every other subcommand keeps working; only
|
|
76
|
+
`pocketshell qr-share` exits 127 with a friendly install hint.
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
pocketshell usage # human-readable lines, one per provider
|
|
82
|
+
pocketshell usage --json # machine-readable JSON (consumed by the app)
|
|
83
|
+
pocketshell usage codex # filter to a single provider
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The output shape is byte-identical to `quse [provider] [--json]` so any
|
|
87
|
+
consumer that already parses `quse` output keeps working when the app
|
|
88
|
+
routes through `pocketshell usage` instead. Under the hood the first
|
|
89
|
+
release delegates to the `quse` CLI via subprocess; later rounds will
|
|
90
|
+
fold the provider-detection logic in directly and drop the subprocess
|
|
91
|
+
hop.
|
|
92
|
+
|
|
93
|
+
If `quse` is not installed, `pocketshell usage` exits with code 127 and
|
|
94
|
+
prints an install hint to stderr.
|
|
95
|
+
|
|
96
|
+
### `pocketshell repos list`
|
|
97
|
+
|
|
98
|
+
Enumerate git repositories — either cloned on this host (`--local`) or
|
|
99
|
+
owned by the authenticated GitHub user (`--remote`). The two modes
|
|
100
|
+
share one unified JSON schema so a future merged view can interleave
|
|
101
|
+
them transparently.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pocketshell repos list --local # scan ~/git for clones (human)
|
|
105
|
+
pocketshell repos list --local --json # same, JSON output
|
|
106
|
+
pocketshell repos list --remote --json # via owner-only `gh api user/repos`
|
|
107
|
+
pocketshell repos list --remote --limit 20
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Schema (every entry):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"owner": "alexeygrigorev", // null when remote URL is non-GitHub
|
|
115
|
+
"name": "pocketshell", // local dir basename, or GH repo name
|
|
116
|
+
"full_name": "alexeygrigorev/pocketshell", // null when owner unknown
|
|
117
|
+
"local": { // populated by --local scans
|
|
118
|
+
"path": "/home/alexey/git/pocketshell",
|
|
119
|
+
"head": "main"
|
|
120
|
+
},
|
|
121
|
+
"remote": { // populated by --remote scans
|
|
122
|
+
"default_branch": "main",
|
|
123
|
+
"html_url": "https://github.com/alexeygrigorev/pocketshell",
|
|
124
|
+
"ssh_url": "git@github.com:alexeygrigorev/pocketshell.git",
|
|
125
|
+
"updated_at": "2026-05-27T12:00:00Z"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`--local` scans `~/git` by default (override with one or more `--root`
|
|
131
|
+
flags or the colon-separated `POCKETSHELL_REPOS_ROOTS` env var) and
|
|
132
|
+
populates `local` for every entry. `owner` and `full_name` are
|
|
133
|
+
best-effort from the parsed `remote.origin.url`; non-GitHub remotes
|
|
134
|
+
leave them `null`.
|
|
135
|
+
|
|
136
|
+
`--remote` delegates to `gh api 'user/repos?affiliation=owner&sort=updated' --paginate --slurp`.
|
|
137
|
+
Requires `gh` on PATH (`apt install gh` on Debian/Ubuntu,
|
|
138
|
+
`brew install gh` on macOS) authenticated via
|
|
139
|
+
`gh auth login -s repo:read`. Sorted by `updated_at` descending so the
|
|
140
|
+
picker shows the most-recently-touched repos first. Missing `gh` exits
|
|
141
|
+
127 with an install hint; a non-zero `gh` exit (auth missing,
|
|
142
|
+
rate-limit, etc.) propagates the exit code and stderr verbatim.
|
|
143
|
+
|
|
144
|
+
With neither flag, defaults to `--local` and prints a one-line
|
|
145
|
+
discoverability hint mentioning `--remote`.
|
|
146
|
+
|
|
147
|
+
Daemon mode caches `repos.list_local` for 10 s and `repos.list_remote`
|
|
148
|
+
for 5 min. `--no-daemon` forces the in-process path; `--no-cache`
|
|
149
|
+
forces the daemon to re-run upstream on the next call.
|
|
150
|
+
|
|
151
|
+
### `pocketshell qr-share`
|
|
152
|
+
|
|
153
|
+
Builds a `pocketshell.ssh-import.v1` payload from an `~/.ssh/config`
|
|
154
|
+
alias (resolved via `ssh -G`) or from explicit flags, wraps it in one or
|
|
155
|
+
more `pocketshell.qr.v1` chunked envelopes (matching the Kotlin
|
|
156
|
+
`QrChunkCodec` byte-for-byte), and emits QR codes for the phone-side
|
|
157
|
+
scanner to consume (issue #129).
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pocketshell qr-share prod # ssh-config alias
|
|
161
|
+
pocketshell qr-share --host h --user u --key ~/.ssh/id_ed25519 --name h
|
|
162
|
+
pocketshell qr-share prod --png --out-dir /tmp/qr # write PNGs
|
|
163
|
+
pocketshell qr-share prod --print-only --id deadbeef # debug envelopes
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When stdout is a TTY the QRs are drawn inline as Unicode blocks; between
|
|
167
|
+
multi-part transmissions the command pauses on "Press Enter for next
|
|
168
|
+
QR" so the user can scan each in turn. When stdout is not a TTY (or
|
|
169
|
+
`--png` is passed) a numbered PNG sequence (`qr-share-01.png`,
|
|
170
|
+
`qr-share-02.png`, ...) is written to `--out-dir`.
|
|
171
|
+
|
|
172
|
+
Requires the optional `qr` extra (see [Optional extras](#optional-extras)).
|
|
173
|
+
Without it, the command exits 127 with the install hint and every other
|
|
174
|
+
subcommand keeps working.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
cd tools/pocketshell
|
|
180
|
+
uv venv
|
|
181
|
+
uv pip install -e ".[dev]"
|
|
182
|
+
uv run pytest
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Or via the dependency-group:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv sync --group dev
|
|
189
|
+
uv run pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The tests stub `quse.usage.collect_usage` so they run in seconds without
|
|
193
|
+
hitting any provider API.
|
|
194
|
+
|
|
195
|
+
## Release flow
|
|
196
|
+
|
|
197
|
+
`pocketshell` ships in lockstep with the Android app. Every time the
|
|
198
|
+
maintainer cuts an Android release tag (`vX.Y.Z`), the
|
|
199
|
+
[`Build`](../../.github/workflows/build.yml) workflow assembles the APK
|
|
200
|
+
and **also** builds the Python sdist + wheel and publishes them to PyPI.
|
|
201
|
+
|
|
202
|
+
### Version coupling
|
|
203
|
+
|
|
204
|
+
Two files must agree on the release version:
|
|
205
|
+
|
|
206
|
+
- `app/build.gradle.kts` -> `versionName = "X.Y.Z"`
|
|
207
|
+
- `tools/pocketshell/pyproject.toml` -> `version = "X.Y.Z"`
|
|
208
|
+
|
|
209
|
+
[`scripts/check-pypi-version.sh`](../../scripts/check-pypi-version.sh)
|
|
210
|
+
enforces this. The release workflow runs it with `--check-tag vX.Y.Z`
|
|
211
|
+
before publishing, so a tag pushed with mismatched versions fails the
|
|
212
|
+
job loudly before anything reaches PyPI.
|
|
213
|
+
|
|
214
|
+
Run it locally before tagging:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
scripts/check-pypi-version.sh # local match check
|
|
218
|
+
scripts/check-pypi-version.sh --check-tag v0.3.0
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Bumping a release
|
|
222
|
+
|
|
223
|
+
1. Pick the next semantic version after the latest GitHub Release/tag.
|
|
224
|
+
2. Update **both** version sources in the same commit:
|
|
225
|
+
- `app/build.gradle.kts` -> bump `versionName` (and `versionCode`).
|
|
226
|
+
- `tools/pocketshell/pyproject.toml` -> bump `version` to the
|
|
227
|
+
same value as `versionName`.
|
|
228
|
+
3. Run `scripts/check-pypi-version.sh` to confirm they match.
|
|
229
|
+
4. Commit the bump on `main`, push, and run the emulator release
|
|
230
|
+
validation gate (`scripts/release-emulator-validation.sh`) as
|
|
231
|
+
described in [`process.md`](../../process.md) -> "Release Builds".
|
|
232
|
+
5. Push the tag with `scripts/push-release-tag.sh`. The tag-triggered
|
|
233
|
+
`Build` workflow then:
|
|
234
|
+
- builds and uploads the APK + creates the GitHub Release
|
|
235
|
+
- runs `scripts/check-pypi-version.sh --check-tag vX.Y.Z`
|
|
236
|
+
- builds the Python sdist + wheel
|
|
237
|
+
- publishes them to PyPI via OIDC trusted publishing
|
|
238
|
+
|
|
239
|
+
The PyPI publish job depends on the APK build job, so a broken APK
|
|
240
|
+
build also aborts the PyPI publish. If only the PyPI publish fails the
|
|
241
|
+
maintainer can re-trigger the workflow at the same tag from the
|
|
242
|
+
Actions tab; the APK build is idempotent against an existing release
|
|
243
|
+
(`softprops/action-gh-release` updates the existing release rather
|
|
244
|
+
than failing).
|
|
245
|
+
|
|
246
|
+
## PyPI trusted publishing setup (one-time)
|
|
247
|
+
|
|
248
|
+
The `publish-pypi` job uses GitHub's OIDC token instead of a long-lived
|
|
249
|
+
API token. This avoids storing a `PYPI_API_TOKEN` secret in the repo
|
|
250
|
+
and means there is nothing to rotate. The trade-off is that the
|
|
251
|
+
project owner must complete one configuration step on pypi.org before
|
|
252
|
+
the first automated tag publish:
|
|
253
|
+
|
|
254
|
+
1. Sign in to https://pypi.org/ with the project owner account.
|
|
255
|
+
2. Open the `pocketshell` project page ->
|
|
256
|
+
**Manage** -> **Publishing**.
|
|
257
|
+
3. Under **Trusted publishers**, click **Add a new pending publisher**
|
|
258
|
+
(if the project is empty) or **Add a new publisher**, then fill in:
|
|
259
|
+
- **PyPI Project Name**: `pocketshell`
|
|
260
|
+
- **Owner**: `alexeygrigorev`
|
|
261
|
+
- **Repository name**: `pocketshell`
|
|
262
|
+
- **Workflow name**: `build.yml`
|
|
263
|
+
- **Environment name**: `pypi`
|
|
264
|
+
4. Save the publisher.
|
|
265
|
+
5. In this repository on GitHub, open
|
|
266
|
+
**Settings** -> **Environments** -> **New environment** -> name it
|
|
267
|
+
`pypi`. No secrets or reviewers are required; the environment exists
|
|
268
|
+
purely to scope the OIDC token. (If the environment already exists,
|
|
269
|
+
confirm it has no protection rules that would block the workflow
|
|
270
|
+
from running.)
|
|
271
|
+
6. Push the next release tag. The `Publish to PyPI via trusted
|
|
272
|
+
publishing` step should succeed without any token configuration.
|
|
273
|
+
|
|
274
|
+
### Why trusted publishing (and not `PYPI_API_TOKEN`)?
|
|
275
|
+
|
|
276
|
+
- No long-lived secret to rotate, leak, or accidentally print in logs.
|
|
277
|
+
- The OIDC subject is scoped to `repo=alexeygrigorev/pocketshell`,
|
|
278
|
+
`workflow=build.yml`, `environment=pypi`, so a compromised fork or
|
|
279
|
+
a different workflow file in this repo cannot reuse it.
|
|
280
|
+
- D22 (no backwards-compat): we do not also maintain a token-fallback
|
|
281
|
+
path. If trusted publishing breaks, fix it; do not add a token
|
|
282
|
+
branch alongside.
|
|
283
|
+
|
|
284
|
+
If trusted publishing is ever unavailable for a tag (e.g. PyPI outage
|
|
285
|
+
on the OIDC verifier), the recommended manual escape hatch is:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
cd tools/pocketshell
|
|
289
|
+
python -m build
|
|
290
|
+
python -m twine upload dist/*
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
with the maintainer's account. Do not re-add a `PYPI_API_TOKEN` secret
|
|
294
|
+
as a permanent fallback.
|
|
295
|
+
|
|
296
|
+
## Why a unified CLI?
|
|
297
|
+
|
|
298
|
+
The PocketShell app previously probed for two binaries (`quse`,
|
|
299
|
+
`tmuxctl`) on every host. That meant two installs to keep up to date,
|
|
300
|
+
two probes to surface failures from, and two PATH-discovery edge cases
|
|
301
|
+
(see [issue #41](https://github.com/alexeygrigorev/pocketshell/issues/41)).
|
|
302
|
+
A single `pocketshell` binary collapses those into one install, one
|
|
303
|
+
probe, one bootstrap row. The app keeps detecting `quse` and `tmuxctl`
|
|
304
|
+
as a parallel path while `pocketshell` ramps up to feature parity; once
|
|
305
|
+
parity is reached, the legacy probes are removed in a hard-cut follow-up
|
|
306
|
+
(no compat shim — see decision D22 in `docs/decisions.md`).
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# pocketshell
|
|
2
|
+
|
|
3
|
+
Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
|
|
4
|
+
Android client. Replaces the separately-installed `quse` and `tmuxctl`
|
|
5
|
+
utilities the app currently probes for on every remote host.
|
|
6
|
+
|
|
7
|
+
This first release ships the **skeleton plus the `pocketshell usage`
|
|
8
|
+
subcommand only**. Follow-up rounds will add `jobs`, `agent-log`,
|
|
9
|
+
`sessions`, `repos`, and an optional daemon mode. See
|
|
10
|
+
[issue #170](https://github.com/alexeygrigorev/pocketshell/issues/170) for
|
|
11
|
+
the design spike and phased roll-out plan.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
The recommended path is `uv tool install`, which lands the binary on PATH
|
|
16
|
+
under `~/.local/bin/`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install pocketshell
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For local development from a clone:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd tools/pocketshell
|
|
26
|
+
uv venv
|
|
27
|
+
uv pip install -e .
|
|
28
|
+
pocketshell --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`pipx install pocketshell` works the same way for users who prefer
|
|
32
|
+
pipx. Both install paths produce a `pocketshell` binary that the
|
|
33
|
+
PocketShell app's bootstrap probe detects.
|
|
34
|
+
|
|
35
|
+
### Optional extras
|
|
36
|
+
|
|
37
|
+
`pocketshell qr-share` requires the `qrcode[pil]` package (Pillow) to
|
|
38
|
+
render QR images. Because Pillow is heavy and not needed by any other
|
|
39
|
+
subcommand, it ships behind an optional `qr` extra:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install pocketshell --with qrcode[pil]
|
|
43
|
+
# or
|
|
44
|
+
pip install pocketshell[qr]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Without the extra, every other subcommand keeps working; only
|
|
48
|
+
`pocketshell qr-share` exits 127 with a friendly install hint.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
pocketshell usage # human-readable lines, one per provider
|
|
54
|
+
pocketshell usage --json # machine-readable JSON (consumed by the app)
|
|
55
|
+
pocketshell usage codex # filter to a single provider
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The output shape is byte-identical to `quse [provider] [--json]` so any
|
|
59
|
+
consumer that already parses `quse` output keeps working when the app
|
|
60
|
+
routes through `pocketshell usage` instead. Under the hood the first
|
|
61
|
+
release delegates to the `quse` CLI via subprocess; later rounds will
|
|
62
|
+
fold the provider-detection logic in directly and drop the subprocess
|
|
63
|
+
hop.
|
|
64
|
+
|
|
65
|
+
If `quse` is not installed, `pocketshell usage` exits with code 127 and
|
|
66
|
+
prints an install hint to stderr.
|
|
67
|
+
|
|
68
|
+
### `pocketshell repos list`
|
|
69
|
+
|
|
70
|
+
Enumerate git repositories — either cloned on this host (`--local`) or
|
|
71
|
+
owned by the authenticated GitHub user (`--remote`). The two modes
|
|
72
|
+
share one unified JSON schema so a future merged view can interleave
|
|
73
|
+
them transparently.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pocketshell repos list --local # scan ~/git for clones (human)
|
|
77
|
+
pocketshell repos list --local --json # same, JSON output
|
|
78
|
+
pocketshell repos list --remote --json # via owner-only `gh api user/repos`
|
|
79
|
+
pocketshell repos list --remote --limit 20
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Schema (every entry):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"owner": "alexeygrigorev", // null when remote URL is non-GitHub
|
|
87
|
+
"name": "pocketshell", // local dir basename, or GH repo name
|
|
88
|
+
"full_name": "alexeygrigorev/pocketshell", // null when owner unknown
|
|
89
|
+
"local": { // populated by --local scans
|
|
90
|
+
"path": "/home/alexey/git/pocketshell",
|
|
91
|
+
"head": "main"
|
|
92
|
+
},
|
|
93
|
+
"remote": { // populated by --remote scans
|
|
94
|
+
"default_branch": "main",
|
|
95
|
+
"html_url": "https://github.com/alexeygrigorev/pocketshell",
|
|
96
|
+
"ssh_url": "git@github.com:alexeygrigorev/pocketshell.git",
|
|
97
|
+
"updated_at": "2026-05-27T12:00:00Z"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`--local` scans `~/git` by default (override with one or more `--root`
|
|
103
|
+
flags or the colon-separated `POCKETSHELL_REPOS_ROOTS` env var) and
|
|
104
|
+
populates `local` for every entry. `owner` and `full_name` are
|
|
105
|
+
best-effort from the parsed `remote.origin.url`; non-GitHub remotes
|
|
106
|
+
leave them `null`.
|
|
107
|
+
|
|
108
|
+
`--remote` delegates to `gh api 'user/repos?affiliation=owner&sort=updated' --paginate --slurp`.
|
|
109
|
+
Requires `gh` on PATH (`apt install gh` on Debian/Ubuntu,
|
|
110
|
+
`brew install gh` on macOS) authenticated via
|
|
111
|
+
`gh auth login -s repo:read`. Sorted by `updated_at` descending so the
|
|
112
|
+
picker shows the most-recently-touched repos first. Missing `gh` exits
|
|
113
|
+
127 with an install hint; a non-zero `gh` exit (auth missing,
|
|
114
|
+
rate-limit, etc.) propagates the exit code and stderr verbatim.
|
|
115
|
+
|
|
116
|
+
With neither flag, defaults to `--local` and prints a one-line
|
|
117
|
+
discoverability hint mentioning `--remote`.
|
|
118
|
+
|
|
119
|
+
Daemon mode caches `repos.list_local` for 10 s and `repos.list_remote`
|
|
120
|
+
for 5 min. `--no-daemon` forces the in-process path; `--no-cache`
|
|
121
|
+
forces the daemon to re-run upstream on the next call.
|
|
122
|
+
|
|
123
|
+
### `pocketshell qr-share`
|
|
124
|
+
|
|
125
|
+
Builds a `pocketshell.ssh-import.v1` payload from an `~/.ssh/config`
|
|
126
|
+
alias (resolved via `ssh -G`) or from explicit flags, wraps it in one or
|
|
127
|
+
more `pocketshell.qr.v1` chunked envelopes (matching the Kotlin
|
|
128
|
+
`QrChunkCodec` byte-for-byte), and emits QR codes for the phone-side
|
|
129
|
+
scanner to consume (issue #129).
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pocketshell qr-share prod # ssh-config alias
|
|
133
|
+
pocketshell qr-share --host h --user u --key ~/.ssh/id_ed25519 --name h
|
|
134
|
+
pocketshell qr-share prod --png --out-dir /tmp/qr # write PNGs
|
|
135
|
+
pocketshell qr-share prod --print-only --id deadbeef # debug envelopes
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When stdout is a TTY the QRs are drawn inline as Unicode blocks; between
|
|
139
|
+
multi-part transmissions the command pauses on "Press Enter for next
|
|
140
|
+
QR" so the user can scan each in turn. When stdout is not a TTY (or
|
|
141
|
+
`--png` is passed) a numbered PNG sequence (`qr-share-01.png`,
|
|
142
|
+
`qr-share-02.png`, ...) is written to `--out-dir`.
|
|
143
|
+
|
|
144
|
+
Requires the optional `qr` extra (see [Optional extras](#optional-extras)).
|
|
145
|
+
Without it, the command exits 127 with the install hint and every other
|
|
146
|
+
subcommand keeps working.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
cd tools/pocketshell
|
|
152
|
+
uv venv
|
|
153
|
+
uv pip install -e ".[dev]"
|
|
154
|
+
uv run pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or via the dependency-group:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
uv sync --group dev
|
|
161
|
+
uv run pytest
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The tests stub `quse.usage.collect_usage` so they run in seconds without
|
|
165
|
+
hitting any provider API.
|
|
166
|
+
|
|
167
|
+
## Release flow
|
|
168
|
+
|
|
169
|
+
`pocketshell` ships in lockstep with the Android app. Every time the
|
|
170
|
+
maintainer cuts an Android release tag (`vX.Y.Z`), the
|
|
171
|
+
[`Build`](../../.github/workflows/build.yml) workflow assembles the APK
|
|
172
|
+
and **also** builds the Python sdist + wheel and publishes them to PyPI.
|
|
173
|
+
|
|
174
|
+
### Version coupling
|
|
175
|
+
|
|
176
|
+
Two files must agree on the release version:
|
|
177
|
+
|
|
178
|
+
- `app/build.gradle.kts` -> `versionName = "X.Y.Z"`
|
|
179
|
+
- `tools/pocketshell/pyproject.toml` -> `version = "X.Y.Z"`
|
|
180
|
+
|
|
181
|
+
[`scripts/check-pypi-version.sh`](../../scripts/check-pypi-version.sh)
|
|
182
|
+
enforces this. The release workflow runs it with `--check-tag vX.Y.Z`
|
|
183
|
+
before publishing, so a tag pushed with mismatched versions fails the
|
|
184
|
+
job loudly before anything reaches PyPI.
|
|
185
|
+
|
|
186
|
+
Run it locally before tagging:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
scripts/check-pypi-version.sh # local match check
|
|
190
|
+
scripts/check-pypi-version.sh --check-tag v0.3.0
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Bumping a release
|
|
194
|
+
|
|
195
|
+
1. Pick the next semantic version after the latest GitHub Release/tag.
|
|
196
|
+
2. Update **both** version sources in the same commit:
|
|
197
|
+
- `app/build.gradle.kts` -> bump `versionName` (and `versionCode`).
|
|
198
|
+
- `tools/pocketshell/pyproject.toml` -> bump `version` to the
|
|
199
|
+
same value as `versionName`.
|
|
200
|
+
3. Run `scripts/check-pypi-version.sh` to confirm they match.
|
|
201
|
+
4. Commit the bump on `main`, push, and run the emulator release
|
|
202
|
+
validation gate (`scripts/release-emulator-validation.sh`) as
|
|
203
|
+
described in [`process.md`](../../process.md) -> "Release Builds".
|
|
204
|
+
5. Push the tag with `scripts/push-release-tag.sh`. The tag-triggered
|
|
205
|
+
`Build` workflow then:
|
|
206
|
+
- builds and uploads the APK + creates the GitHub Release
|
|
207
|
+
- runs `scripts/check-pypi-version.sh --check-tag vX.Y.Z`
|
|
208
|
+
- builds the Python sdist + wheel
|
|
209
|
+
- publishes them to PyPI via OIDC trusted publishing
|
|
210
|
+
|
|
211
|
+
The PyPI publish job depends on the APK build job, so a broken APK
|
|
212
|
+
build also aborts the PyPI publish. If only the PyPI publish fails the
|
|
213
|
+
maintainer can re-trigger the workflow at the same tag from the
|
|
214
|
+
Actions tab; the APK build is idempotent against an existing release
|
|
215
|
+
(`softprops/action-gh-release` updates the existing release rather
|
|
216
|
+
than failing).
|
|
217
|
+
|
|
218
|
+
## PyPI trusted publishing setup (one-time)
|
|
219
|
+
|
|
220
|
+
The `publish-pypi` job uses GitHub's OIDC token instead of a long-lived
|
|
221
|
+
API token. This avoids storing a `PYPI_API_TOKEN` secret in the repo
|
|
222
|
+
and means there is nothing to rotate. The trade-off is that the
|
|
223
|
+
project owner must complete one configuration step on pypi.org before
|
|
224
|
+
the first automated tag publish:
|
|
225
|
+
|
|
226
|
+
1. Sign in to https://pypi.org/ with the project owner account.
|
|
227
|
+
2. Open the `pocketshell` project page ->
|
|
228
|
+
**Manage** -> **Publishing**.
|
|
229
|
+
3. Under **Trusted publishers**, click **Add a new pending publisher**
|
|
230
|
+
(if the project is empty) or **Add a new publisher**, then fill in:
|
|
231
|
+
- **PyPI Project Name**: `pocketshell`
|
|
232
|
+
- **Owner**: `alexeygrigorev`
|
|
233
|
+
- **Repository name**: `pocketshell`
|
|
234
|
+
- **Workflow name**: `build.yml`
|
|
235
|
+
- **Environment name**: `pypi`
|
|
236
|
+
4. Save the publisher.
|
|
237
|
+
5. In this repository on GitHub, open
|
|
238
|
+
**Settings** -> **Environments** -> **New environment** -> name it
|
|
239
|
+
`pypi`. No secrets or reviewers are required; the environment exists
|
|
240
|
+
purely to scope the OIDC token. (If the environment already exists,
|
|
241
|
+
confirm it has no protection rules that would block the workflow
|
|
242
|
+
from running.)
|
|
243
|
+
6. Push the next release tag. The `Publish to PyPI via trusted
|
|
244
|
+
publishing` step should succeed without any token configuration.
|
|
245
|
+
|
|
246
|
+
### Why trusted publishing (and not `PYPI_API_TOKEN`)?
|
|
247
|
+
|
|
248
|
+
- No long-lived secret to rotate, leak, or accidentally print in logs.
|
|
249
|
+
- The OIDC subject is scoped to `repo=alexeygrigorev/pocketshell`,
|
|
250
|
+
`workflow=build.yml`, `environment=pypi`, so a compromised fork or
|
|
251
|
+
a different workflow file in this repo cannot reuse it.
|
|
252
|
+
- D22 (no backwards-compat): we do not also maintain a token-fallback
|
|
253
|
+
path. If trusted publishing breaks, fix it; do not add a token
|
|
254
|
+
branch alongside.
|
|
255
|
+
|
|
256
|
+
If trusted publishing is ever unavailable for a tag (e.g. PyPI outage
|
|
257
|
+
on the OIDC verifier), the recommended manual escape hatch is:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
cd tools/pocketshell
|
|
261
|
+
python -m build
|
|
262
|
+
python -m twine upload dist/*
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
with the maintainer's account. Do not re-add a `PYPI_API_TOKEN` secret
|
|
266
|
+
as a permanent fallback.
|
|
267
|
+
|
|
268
|
+
## Why a unified CLI?
|
|
269
|
+
|
|
270
|
+
The PocketShell app previously probed for two binaries (`quse`,
|
|
271
|
+
`tmuxctl`) on every host. That meant two installs to keep up to date,
|
|
272
|
+
two probes to surface failures from, and two PATH-discovery edge cases
|
|
273
|
+
(see [issue #41](https://github.com/alexeygrigorev/pocketshell/issues/41)).
|
|
274
|
+
A single `pocketshell` binary collapses those into one install, one
|
|
275
|
+
probe, one bootstrap row. The app keeps detecting `quse` and `tmuxctl`
|
|
276
|
+
as a parallel path while `pocketshell` ramps up to feature parity; once
|
|
277
|
+
parity is reached, the legacy probes are removed in a hard-cut follow-up
|
|
278
|
+
(no compat shim — see decision D22 in `docs/decisions.md`).
|
|
@@ -4,7 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pocketshell"
|
|
7
|
-
|
|
7
|
+
# Must equal `versionName` in app/build.gradle.kts on every release tag.
|
|
8
|
+
# scripts/check-pypi-version.sh enforces this; .github/workflows/build.yml
|
|
9
|
+
# runs that check before publishing to PyPI. See
|
|
10
|
+
# tools/pocketshell/README.md ("Release flow") for the bump procedure.
|
|
11
|
+
version = "0.3.1"
|
|
8
12
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
9
13
|
readme = "README.md"
|
|
10
14
|
requires-python = ">=3.11"
|
|
@@ -53,6 +57,15 @@ dev = [
|
|
|
53
57
|
"pytest>=8.4.0",
|
|
54
58
|
"ruff>=0.15.0",
|
|
55
59
|
]
|
|
60
|
+
# Optional extra for `pocketshell qr-share`. The `qrcode[pil]` chain is
|
|
61
|
+
# heavy (Pillow) and is only needed for QR rendering, so we keep it out
|
|
62
|
+
# of the default install. Without this extra, `pocketshell qr-share`
|
|
63
|
+
# exits 127 with a friendly install hint; every other subcommand keeps
|
|
64
|
+
# working unchanged. Install with `pip install pocketshell[qr]` or
|
|
65
|
+
# `uv tool install pocketshell --with qrcode[pil]`.
|
|
66
|
+
qr = [
|
|
67
|
+
"qrcode[pil]>=7.4",
|
|
68
|
+
]
|
|
56
69
|
|
|
57
70
|
[tool.hatch.build.targets.wheel]
|
|
58
71
|
packages = ["src/pocketshell"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Allow `python -m pocketshell` as an alternative to the installed entry point.
|
|
2
2
|
|
|
3
3
|
Used in CI and developer environments where the console-script shim from
|
|
4
|
-
`pip install -e .` / `uv tool install pocketshell
|
|
4
|
+
`pip install -e .` / `uv tool install pocketshell` is not on PATH.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|