pocketshell 0.1.0__tar.gz → 0.3.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.
@@ -0,0 +1,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: pocketshell
3
+ Version: 0.3.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
+ 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
- version = "0.1.0"
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.0"
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-cli` is not on PATH.
4
+ `pip install -e .` / `uv tool install pocketshell` is not on PATH.
5
5
  """
6
6
 
7
7
  from __future__ import annotations