caffeinated-whale-cli 3.0.0__tar.gz → 3.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.
Files changed (97) hide show
  1. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/PKG-INFO +54 -3
  2. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/README.md +53 -2
  3. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/pyproject.toml +7 -1
  4. caffeinated_whale_cli-3.1.0/src/caffeinated_whale_cli/__init__.py +1 -0
  5. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/axi.py +38 -27
  6. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/init.py +7 -0
  7. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/inspect.py +17 -5
  8. caffeinated_whale_cli-3.1.0/src/caffeinated_whale_cli/commands/setup.py +134 -0
  9. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/cred_bridge.py +8 -1
  10. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/credbridge.py +10 -3
  11. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/docker.py +15 -1
  12. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/doctor.py +43 -3
  13. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/inspect.py +41 -7
  14. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/rm.py +6 -1
  15. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/rm_site.py +9 -3
  16. caffeinated_whale_cli-3.1.0/src/caffeinated_whale_cli/core/setup.py +478 -0
  17. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/main.py +8 -0
  18. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/config_utils.py +21 -4
  19. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/cred_daemon.py +39 -3
  20. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/db_utils.py +12 -5
  21. caffeinated_whale_cli-3.1.0/src/caffeinated_whale_cli/utils/shared_home.py +206 -0
  22. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/startup.py +64 -0
  23. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +3 -0
  24. caffeinated_whale_cli-3.0.0/src/caffeinated_whale_cli/__init__.py +0 -1
  25. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/LICENSE +0 -0
  26. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/setup.cfg +0 -0
  27. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/__main__.py +0 -0
  28. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
  29. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/apps.py +0 -0
  30. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/backup.py +0 -0
  31. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/config.py +0 -0
  32. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/console.html +0 -0
  33. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
  34. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/label.py +0 -0
  35. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/list.py +0 -0
  36. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/logs.py +0 -0
  37. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/open.py +0 -0
  38. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/restart.py +0 -0
  39. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/restore.py +0 -0
  40. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/rm.py +0 -0
  41. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
  42. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/run.py +0 -0
  43. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/scale.py +0 -0
  44. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
  45. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/serve.py +0 -0
  46. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/start.py +0 -0
  47. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/status.py +0 -0
  48. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/stop.py +0 -0
  49. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
  50. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/update.py +0 -0
  51. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/utils.py +0 -0
  52. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/commands/where.py +0 -0
  53. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/__init__.py +0 -0
  54. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/apps.py +0 -0
  55. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
  56. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/backup.py +0 -0
  57. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
  58. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/config.py +0 -0
  59. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/envelope.py +0 -0
  60. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/errors.py +0 -0
  61. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
  62. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/fleet.py +0 -0
  63. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/init.py +0 -0
  64. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/label.py +0 -0
  65. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/list.py +0 -0
  66. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/logs.py +0 -0
  67. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/open.py +0 -0
  68. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
  69. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/restart.py +0 -0
  70. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/restore.py +0 -0
  71. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/run.py +0 -0
  72. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/scale.py +0 -0
  73. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/start.py +0 -0
  74. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/status.py +0 -0
  75. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/stop.py +0 -0
  76. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/supervision.py +0 -0
  77. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/unlock.py +0 -0
  78. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/update.py +0 -0
  79. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/url.py +0 -0
  80. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/version.py +0 -0
  81. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/core/where.py +0 -0
  82. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/update_notice.py +0 -0
  83. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
  84. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
  85. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
  86. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
  87. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
  88. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/cache.py +0 -0
  89. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
  90. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/console.py +0 -0
  91. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/daemon_identity.py +0 -0
  92. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
  93. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
  94. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
  95. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/tips.py +0 -0
  96. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/toon.py +0 -0
  97. {caffeinated_whale_cli-3.0.0 → caffeinated_whale_cli-3.1.0}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: caffeinated-whale-cli
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: A CLI tool to help manage Frappe Docker instances.
5
5
  Author-email: Christopher McKay <mckay.christopher73@outlook.com>
6
6
  License: MIT
@@ -158,6 +158,47 @@ python -m caffeinated_whale_cli ls
158
158
 
159
159
  This bypasses the shim entirely and behaves identically to `cwcli`/`caffeinated-whale-cli`.
160
160
 
161
+ ## Shared (multi-user) install
162
+
163
+ By default cwcli's on-disk state is **per-user**: everything lives under `~/.cwcli`, the cache is `0700`, the config `0600`, and the credential-bridge daemon runs as you.
164
+ That is correct for one person per machine, and it is unchanged - if you do not opt in, nothing here applies to you.
165
+
166
+ When several people on **one Linux/UNIX box** need to drive the **same** Frappe instances (they already share the containers through the `docker` group), the per-user modes get in the way: the second person hits `Permission denied` on the cache, cannot edit the shared config, and the credential bridge either locks them out or hands them the first person's token.
167
+ Shared mode fixes this with an opt-in, admin-provisioned layout modelled on Docker's `root:docker` socket and Homebrew-on-Linux's shared tree: a dedicated `cwcli` group, a system state tree with setgid group-writable directories, and one machine-wide credential bridge behind a group-owned socket.
168
+
169
+ **Enable it (once, by an admin):**
170
+
171
+ ```bash
172
+ sudo cwcli setup shared --users alice,bob # create the group + tree, add members
173
+ sudo cwcli setup shared --users alice,bob --cred-bridge # also stand up the machine-wide credential bridge
174
+ ```
175
+
176
+ This creates the `cwcli` group and a `cwcli` service account, provisions `/var/lib/cwcli` (config, cache, projects, archive) as `root:cwcli` with setgid `2770` directories, and writes the marker `/etc/cwcli/shared.toml` that turns shared mode on.
177
+ It is idempotent - safe to re-run to add users or repair modes.
178
+ Members must log out and back in for the new group to take effect.
179
+ Windows is unaffected (shared mode is POSIX-only; cwcli stays per-user there).
180
+
181
+ **Consolidate existing per-user homes into the shared tree:**
182
+
183
+ ```bash
184
+ sudo cwcli setup migrate --from /home/alice/.cwcli --from /home/bob/.cwcli
185
+ sudo cwcli setup migrate --discover # find every /home/*/.cwcli and /root/.cwcli
186
+ ```
187
+
188
+ `migrate` copies each user's project state into the shared tree with the correct group ownership and modes.
189
+ It **never deletes a source** and **refuses to overwrite an existing shared project** (a name clash is reported, not clobbered), so it is safe to re-run.
190
+ The per-user SQLite cache is regenerable (`cwcli inspect`) and deliberately not merged.
191
+
192
+ **Opting an invocation back to a private home:** `CWCLI_HOME` always wins over shared mode, so `CWCLI_HOME=~/.cwcli cwcli ...` uses your own home even on a shared box.
193
+
194
+ **Security consequences - state them plainly before enabling:**
195
+
196
+ - **`cwcli` group membership is the trust boundary.** Anyone in the group can read every instance's cache, read the rm/rm-site backups (which contain full database dumps), edit the shared config, and - with `--cred-bridge` - ask the machine-wide bridge for a credential. This is the same "docker group ≈ root-ish capability" trade every shared dev tool makes. Keep membership deliberate.
197
+ - **The credential bridge in shared mode serves one machine-owned, read-only service-account token** to any group member's container git, so its audit log records the host and project but **not which human** triggered a fetch (they all present as the service account). Provision that account with a read-only deploy token; rotate it in one place if it leaks.
198
+ - The group-owned socket (`0660 root:cwcli`) closes the world-connectable `0666` socket that could otherwise leak the first user's token to a second user on the box. A uid outside the `cwcli` group cannot reach it.
199
+
200
+ Run `cwcli doctor` to verify the shared install (group present, tree modes, service account).
201
+
161
202
  ## Quick Start
162
203
 
163
204
  ```bash
@@ -193,6 +234,8 @@ A cluster containing an unknown short option is rejected as a whole.
193
234
 
194
235
  Creates a complete Frappe development environment in a single step. Downloads compose files, starts containers, initializes bench, creates a site, and starts the bench's dev services.
195
236
 
237
+ Blocks for roughly 10-20 minutes (image pulls, bench init, site creation). Running several `cwcli init` invocations in parallel can OOM-kill the host on a memory-constrained machine - serialize them instead. Adding a bench to an already-running instance (`cwcli init existing-project --bench another-bench`) never disturbs it or its other benches: the frappe container is never pulled, recreated, or restarted, so every already-serving bench keeps running throughout.
238
+
196
239
  ```bash
197
240
  cwcli init [OPTIONS] [PROJECT_NAME]
198
241
  ```
@@ -934,6 +977,7 @@ cwcli inspect [OPTIONS] PROJECT_NAME
934
977
 
935
978
  | Option | Description |
936
979
  |--------|-------------|
980
+ | `--bench <index\|label>` | Narrow the report to one bench on a multi-bench project (its numeric index or label). Every bench is still discovered/refreshed/cached regardless; only the output is narrowed |
937
981
  | `-v`, `--verbose` | Enable verbose diagnostic output |
938
982
  | `-j`, `--json` | Output the result as a JSON object |
939
983
  | `-u`, `--update` | Update the cache by re-inspecting the project |
@@ -988,6 +1032,9 @@ cwcli inspect frappe-one --no-refresh
988
1032
  # Get JSON output
989
1033
  cwcli inspect frappe-one --json
990
1034
 
1035
+ # Narrow the report to one bench in a multi-bench project
1036
+ cwcli inspect frappe-one --bench 1
1037
+
991
1038
  # Interactively label each bench (durable handle for --bench)
992
1039
  cwcli inspect frappe-one --interactive
993
1040
  ```
@@ -2021,6 +2068,7 @@ This is the same class of exposure as SSH agent forwarding or VS Code's own cred
2021
2068
  Every request is written to an audit log (`~/.cwcli/run/credbridge-audit.log`, one line per request: timestamp, instance, host, answered - never a credential byte).
2022
2069
  For a tighter blast radius, authenticate `gh`/`glab` with a fine-grained, expiring token scoped to only the repos you need; the bridge faithfully forwards whatever the host tools hold, so the token's reach is your auth choice.
2023
2070
  On a shared multi-user host, note the per-instance socket lives in the instance's workspace directory under `~/.cwcli/projects/<name>/data/`; `enable` tightens `~/.cwcli/projects` to owner-only (0700) for exactly that reason.
2071
+ This per-user daemon is distinct from the **machine-wide** bridge of an opt-in [shared install](#shared-multi-user-install): there the daemon runs once as the `cwcli` service account (started by a system unit at boot), serves a single machine-owned read-only service-account token, and its socket is `0660 root:cwcli` so a uid outside the `cwcli` group cannot reach it. See that section for the security consequences.
2024
2072
 
2025
2073
  **Host allowlist:** the daemon only answers credential requests for `github.com` and `gitlab.com` by default.
2026
2074
  Using a self-hosted GitLab (or GitHub Enterprise)?
@@ -2297,8 +2345,11 @@ cwcli axi scale frappe-one --to 8 --yes
2297
2345
  # Every site carries installed_apps_verified: true only on a full read - the
2298
2346
  # cache/partial tiers carry a site's apps (and their git refs) forward without
2299
2347
  # re-observing them, so a non-full read with a remembered app list also warns.
2348
+ # --bench <index|label> narrows the report to one bench (every tier still
2349
+ # refreshes/caches every bench regardless; only the emitted report is narrowed).
2300
2350
  cwcli axi inspect frappe-one
2301
2351
  cwcli axi inspect frappe-one --update
2352
+ cwcli axi inspect frappe-one --bench 1
2302
2353
 
2303
2354
  # List a project's benches with their indices and labels. This is what answers
2304
2355
  # "pass --bench <index|label>" from any other verb - no other verb can.
@@ -2364,7 +2415,7 @@ cwcli axi config
2364
2415
  cwcli axi doctor
2365
2416
  ```
2366
2417
 
2367
- `cwcli axi ls`, `cwcli axi where`, `cwcli axi backup`, `cwcli axi unlock`, `cwcli axi stop`, `cwcli axi start`, `cwcli axi status`, `cwcli axi restart`, `cwcli axi inspect`, `cwcli axi benches`, and `cwcli axi label` run on the same logic core as their human counterparts; only the output (always TOON, never JSON) and choice-handling differ. `cwcli axi start` never prompts: an ambiguous multi-bench project is a `--bench` usage error (exit 2), and an unresolved port conflict is a `CONFLICT` error naming `--yes` (exit 1). `cwcli axi unlock` follows `cwcli axi backup`'s conventions exactly: an ambiguous multi-bench project is a `--bench` usage error (exit 2) and a stopped instance is a usage error pointing at `cwcli start` (exit 2). `cwcli axi stop` is idempotent for an agent - stopping an already-stopped project is a success, not an error. `cwcli axi where` answers from the cache, which outlives the instances it describes, so every match carries `project_state` (`present`, `absent`, or `unverified`) and the document carries `verified`: an agent must read those before acting on a hit, because an `absent` row describes an instance that no longer exists and an `unverified` row means the liveness check could not run. An unreachable Docker daemon degrades the whole answer to `unverified` rather than vouching for it, and `--no-verify` skips the check (reporting `unverified`) when a caller has already established liveness. `cwcli axi status` always exits 0, leading with the instance `overall` aggregate (`offline`/`online`/`running`/`degraded`). It reports **every** bench, in one document, under `benches[N]` - with no `--bench` it used to refuse a multi-bench project with exit 2 and send you off to `cwcli axi benches` to poll once per bench, reassembling the instance view from documents that never said which bench they described. The shape is uniform, so `supervisor_up`, `web_http_code`, `processes` and `not_cwcli_supervised` live inside `benches[i]` even for a single-bench project (`benches[0].processes`), and there is one parse path rather than a branch on bench count. Each bench also carries `bench_present` (`present`/`absent`/`unverified`) - the bench LIST comes from the cache, and a bench whose directory was deleted has no marker and no supervisord, which is indistinguishable from one that was simply never started, so it used to read as `online`; the token answers that, and `overall` keeps its four tokens. Each bench also carries `index`, `bench_path`, `label`, its own `overall`, `web_port`/`web_port_verified` (the port that bench's `web_http_code` was actually measured on), and `web_site` (the site it was measured FOR - Frappe routes by `Host`, so the code belongs to that site, and a probe naming no site is answered `404` by a healthy bench). When that port cannot be read it is `null`/`false` and **no probe was made** - cwcli never falls back to `:8000`, because on a multi-bench instance that reports a different bench's web server as this one's. A stopped instance carries `benches: []`. `cwcli axi stop` takes `--bench <index|label>` to stop ONE bench's dev processes, leaving sibling benches and every container running - the inverse of `cwcli axi start --bench`; without it, it stops the whole instance's containers, and either form is idempotent. `cwcli axi restart` requires `--process`; an unknown/ambiguous process is a usage error listing the valid labels (exit 2). `cwcli axi inspect` serves whichever freshness tier answers the request (`served_from` names it) and, unlike every other bench-scoped verb, has deliberately NO `--yes` - a stopped project on the refresh path is a usage error (exit 2) naming `cwcli start`, and a drift escalation that can no longer discover the bench serves the cached data with `degraded: true` and a warning (exit 0). Every site also carries `installed_apps_verified`: only a full read actually re-observes an app's installed version and git ref, so a cache or partial read carries the remembered list forward unverified and, when that list is non-empty, adds a warning naming `--update` - an agent must not treat an unverified `installed_apps` entry as the live git state. `cwcli axi benches` is the discovery verb behind every other verb's `--bench`: when a bench-scoped verb reports "multiple benches; pass `--bench <index|label>`", this is what tells you the valid values, and a project that has never been inspected is a structured error naming `cwcli axi inspect` rather than an empty list. Its rows come from the cache, which outlives the benches it describes, so every row carries `state` (`present`, `absent`, or `unverified`) and the document carries `verified` - the same contract `cwcli axi where` uses, cross-checked here against the running container in one exec. An agent must read `state` before acting on a row: `absent` means that bench directory no longer exists, and `unverified` means the check could not run (a stopped project, an unreachable daemon, or `--no-verify`) - never a confirmation. A stale row is reported, never pruned; `cwcli axi inspect <project> --update` is what refreshes it. `cwcli axi label` never starts a stopped project (the label marker lives inside the bench), and listing is deliberately `cwcli axi benches` rather than a mode of the mutation verb. `cwcli axi self-update --check` is read-only and **exits 0 whenever the check succeeds, including when an update is available** - the answer is the `is_outdated` field, not the exit code, because on the agent surface a non-zero exit means an error. This deliberately differs from the human `cwcli self-update --check`, which exits 1 when an update is available so shell scripts can gate on it; the mutating `cwcli axi self-update` is deliberately not offered. `cwcli axi config` is the read-only counterpart of `cwcli config show`: one TOON document carrying the search paths, all three auto-inspect state stores, the credential-bridge state, the tips setting, and the file locations. **No config-mutating axi verbs exist** (no `paths add`/`remove`, no `cache clear`, no `auto-inspect enable`/`disable`): an agent rewriting the user's search paths or wiping the cache is a product decision that deserves its own evidence, so mutations stay on the human `cwcli config` surface (whose reads all have `--json`). `cwcli axi doctor` is the read-only, system-wide counterpart of `cwcli doctor`: one TOON document listing every check with a machine-readable `status` (`pass`/`warn`/`fail`) an agent gates on directly, never by parsing the `detail` text. It exits 0 whenever every check is pass/warn and non-zero only when a check fails, exactly matching the human command, so a fleet can run it before driving any instance and `&&`-chain on the result.
2418
+ `cwcli axi ls`, `cwcli axi where`, `cwcli axi backup`, `cwcli axi unlock`, `cwcli axi stop`, `cwcli axi start`, `cwcli axi status`, `cwcli axi restart`, `cwcli axi inspect`, `cwcli axi benches`, and `cwcli axi label` run on the same logic core as their human counterparts; only the output (always TOON, never JSON) and choice-handling differ. `cwcli axi start` never prompts: an ambiguous multi-bench project is a `--bench` usage error (exit 2), and an unresolved port conflict is a `CONFLICT` error naming `--yes` (exit 1). `cwcli axi unlock` follows `cwcli axi backup`'s conventions exactly: an ambiguous multi-bench project is a `--bench` usage error (exit 2) and a stopped instance is a usage error pointing at `cwcli start` (exit 2). `cwcli axi stop` is idempotent for an agent - stopping an already-stopped project is a success, not an error. `cwcli axi where` answers from the cache, which outlives the instances it describes, so every match carries `project_state` (`present`, `absent`, or `unverified`) and the document carries `verified`: an agent must read those before acting on a hit, because an `absent` row describes an instance that no longer exists and an `unverified` row means the liveness check could not run. An unreachable Docker daemon degrades the whole answer to `unverified` rather than vouching for it, and `--no-verify` skips the check (reporting `unverified`) when a caller has already established liveness. `cwcli axi status` always exits 0, leading with the instance `overall` aggregate (`offline`/`online`/`running`/`degraded`). It reports **every** bench, in one document, under `benches[N]` - with no `--bench` it used to refuse a multi-bench project with exit 2 and send you off to `cwcli axi benches` to poll once per bench, reassembling the instance view from documents that never said which bench they described. The shape is uniform, so `supervisor_up`, `web_http_code`, `processes` and `not_cwcli_supervised` live inside `benches[i]` even for a single-bench project (`benches[0].processes`), and there is one parse path rather than a branch on bench count. Each bench also carries `bench_present` (`present`/`absent`/`unverified`) - the bench LIST comes from the cache, and a bench whose directory was deleted has no marker and no supervisord, which is indistinguishable from one that was simply never started, so it used to read as `online`; the token answers that, and `overall` keeps its four tokens. Each bench also carries `index`, `bench_path`, `label`, its own `overall`, `web_port`/`web_port_verified` (the port that bench's `web_http_code` was actually measured on), and `web_site` (the site it was measured FOR - Frappe routes by `Host`, so the code belongs to that site, and a probe naming no site is answered `404` by a healthy bench). When that port cannot be read it is `null`/`false` and **no probe was made** - cwcli never falls back to `:8000`, because on a multi-bench instance that reports a different bench's web server as this one's. A stopped instance carries `benches: []`. `cwcli axi stop` takes `--bench <index|label>` to stop ONE bench's dev processes, leaving sibling benches and every container running - the inverse of `cwcli axi start --bench`; without it, it stops the whole instance's containers, and either form is idempotent. `cwcli axi restart` requires `--process`; an unknown/ambiguous process is a usage error listing the valid labels (exit 2). `cwcli axi inspect` serves whichever freshness tier answers the request (`served_from` names it) and, unlike every other bench-scoped verb, has deliberately NO `--yes` - a stopped project on the refresh path is a usage error (exit 2) naming `cwcli start`, and a drift escalation that can no longer discover the bench serves the cached data with `degraded: true` and a warning (exit 0). It takes `--bench <index|label>` like every other bench-scoped verb, narrowing the emitted report to one bench; every tier still discovers/refreshes/caches every bench regardless of the selector, and an unknown selector is a `bench.not_found` error (exit 1). Every site also carries `installed_apps_verified`: only a full read actually re-observes an app's installed version and git ref, so a cache or partial read carries the remembered list forward unverified and, when that list is non-empty, adds a warning naming `--update` - an agent must not treat an unverified `installed_apps` entry as the live git state. `cwcli axi benches` is the discovery verb behind every other verb's `--bench`: when a bench-scoped verb reports "multiple benches; pass `--bench <index|label>`", this is what tells you the valid values, and a project that has never been inspected is a structured error naming `cwcli axi inspect` rather than an empty list. Its rows come from the cache, which outlives the benches it describes, so every row carries `state` (`present`, `absent`, or `unverified`) and the document carries `verified` - the same contract `cwcli axi where` uses, cross-checked here against the running container in one exec. An agent must read `state` before acting on a row: `absent` means that bench directory no longer exists, and `unverified` means the check could not run (a stopped project, an unreachable daemon, or `--no-verify`) - never a confirmation. A stale row is reported, never pruned; `cwcli axi inspect <project> --update` is what refreshes it. `cwcli axi label` never starts a stopped project (the label marker lives inside the bench), and listing is deliberately `cwcli axi benches` rather than a mode of the mutation verb. `cwcli axi self-update --check` is read-only and **exits 0 whenever the check succeeds, including when an update is available** - the answer is the `is_outdated` field, not the exit code, because on the agent surface a non-zero exit means an error. This deliberately differs from the human `cwcli self-update --check`, which exits 1 when an update is available so shell scripts can gate on it; the mutating `cwcli axi self-update` is deliberately not offered. `cwcli axi config` is the read-only counterpart of `cwcli config show`: one TOON document carrying the search paths, all three auto-inspect state stores, the credential-bridge state, the tips setting, and the file locations. **No config-mutating axi verbs exist** (no `paths add`/`remove`, no `cache clear`, no `auto-inspect enable`/`disable`): an agent rewriting the user's search paths or wiping the cache is a product decision that deserves its own evidence, so mutations stay on the human `cwcli config` surface (whose reads all have `--json`). `cwcli axi doctor` is the read-only, system-wide counterpart of `cwcli doctor`: one TOON document listing every check with a machine-readable `status` (`pass`/`warn`/`fail`) an agent gates on directly, never by parsing the `detail` text. It exits 0 whenever every check is pass/warn and non-zero only when a check fails, exactly matching the human command, so a fleet can run it before driving any instance and `&&`-chain on the result.
2368
2419
 
2369
2420
  `cwcli axi url <project>` answers two questions no other `axi` verb does: what HOST URL a bench actually serves on, and whether it is answering HTTP right now.
2370
2421
  `cwcli axi status`'s `web_http_code` is measured against the bench's CONTAINER-internal port and never states the host address; the host URL is computed only by `cwcli open`'s success banner and the human `cwcli init` banner, neither reachable from `cwcli axi`, so getting either answer meant dropping to a raw `docker inspect`/`curl` against the container.
@@ -2418,7 +2469,7 @@ It exits non-zero if a restart fails or the named site does not answer Frappe's
2418
2469
 
2419
2470
  These are agent-only for now: there is no human `cwcli migrate`, `cwcli run-tests`, or `cwcli build` yet. Use `cwcli run <project> --site <site> migrate` for the human equivalent.
2420
2471
 
2421
- `cwcli axi init` provisions a new instance, bench, and site the way `cwcli init` does, but non-interactively and as ONE terminal TOON document. It **blocks** for the full 10-20 minute run - the same block-and-emit-one-document shape as `cwcli axi apps update`, because a progress stream would break the one-TOON-document contract - narrating coarse phase labels to stderr; run `cwcli logs <project>` / `cwcli status <project>` from a second shell for live progress. After the bench+site is created, it starts the bench's dev services by default (the same `core.start` behind `cwcli start`/`cwcli axi start`), leaving a running bench rather than a created-but-idle one; `--no-start` creates without starting, for automation/CI, distinct from Docker container startup (stage 1 always brings the containers up regardless of this flag). A dev-services start failure degrades to a stderr warning rather than a non-zero exit, since the bench was already created. The site administrator password comes from the **`CWCLI_ADMIN_PASSWORD` environment variable (recommended)** or `--admin-password`; the flag wins if both are set, and with neither set the verb refuses with a usage error (exit 2) naming both - it never generates a password and never prompts. The env var keeps the secret off the process argv (visible in `ps`/shell history) that the flag exposes; the container-side `bench new-site` still receives it, so treat the value as one-time. The MariaDB root password takes the same shape via `CWCLI_DB_ROOT_PASSWORD` / `--db-root-password` (default `123`). The interactive decisions the human `cwcli init` prompts for become non-prompting errors: an existing bench with neither `--reuse-bench` nor `--no-reuse-bench` is a usage error (exit 2) naming both; a port conflict names `--port` (exit 1); containers that do not come up point at `cwcli status`/`cwcli logs` (exit 1). There is no `--auto-start` (compose `cwcli axi start` then re-run) and no `--verbose` (stdout is always TOON). `--bench` here is the NAME of the bench to create, distinct from the `--bench <index|label>` selector the other verbs use.
2472
+ `cwcli axi init` provisions a new instance, bench, and site the way `cwcli init` does, but non-interactively and as ONE terminal TOON document. It **blocks** for the full 10-20 minute run - the same block-and-emit-one-document shape as `cwcli axi apps update`, because a progress stream would break the one-TOON-document contract - narrating coarse phase labels to stderr; run `cwcli logs <project>` / `cwcli status <project>` from a second shell for live progress. Running several `axi init` calls in parallel can OOM-kill the host on a memory-constrained machine - serialize them instead. Adding a bench to an already-running instance never disturbs it or its other benches: the frappe container is never pulled, recreated, or restarted, so every already-serving bench keeps running throughout. After the bench+site is created, it starts the bench's dev services by default (the same `core.start` behind `cwcli start`/`cwcli axi start`), leaving a running bench rather than a created-but-idle one; `--no-start` creates without starting, for automation/CI, distinct from Docker container startup (stage 1 always brings the containers up regardless of this flag). A dev-services start failure degrades to a stderr warning rather than a non-zero exit, since the bench was already created. The site administrator password comes from the **`CWCLI_ADMIN_PASSWORD` environment variable (recommended)** or `--admin-password`; the flag wins if both are set, and with neither set the verb refuses with a usage error (exit 2) naming both - it never generates a password and never prompts. The env var keeps the secret off the process argv (visible in `ps`/shell history) that the flag exposes; the container-side `bench new-site` still receives it, so treat the value as one-time. The MariaDB root password takes the same shape via `CWCLI_DB_ROOT_PASSWORD` / `--db-root-password` (default `123`). The interactive decisions the human `cwcli init` prompts for become non-prompting errors: an existing bench with neither `--reuse-bench` nor `--no-reuse-bench` is a usage error (exit 2) naming both; a port conflict names `--port` (exit 1); containers that do not come up point at `cwcli status`/`cwcli logs` (exit 1). There is no `--auto-start` (compose `cwcli axi start` then re-run) and no `--verbose` (stdout is always TOON). `--bench` here is the NAME of the bench to create, distinct from the `--bench <index|label>` selector the other verbs use.
2422
2473
 
2423
2474
  `cwcli axi rm <project> --yes` permanently removes an instance: its containers, named volumes (databases, sites, and files), own Docker Compose network, and local project directory.
2424
2475
  It is the agent form of `cwcli rm`, and it runs the same code.
@@ -118,6 +118,47 @@ python -m caffeinated_whale_cli ls
118
118
 
119
119
  This bypasses the shim entirely and behaves identically to `cwcli`/`caffeinated-whale-cli`.
120
120
 
121
+ ## Shared (multi-user) install
122
+
123
+ By default cwcli's on-disk state is **per-user**: everything lives under `~/.cwcli`, the cache is `0700`, the config `0600`, and the credential-bridge daemon runs as you.
124
+ That is correct for one person per machine, and it is unchanged - if you do not opt in, nothing here applies to you.
125
+
126
+ When several people on **one Linux/UNIX box** need to drive the **same** Frappe instances (they already share the containers through the `docker` group), the per-user modes get in the way: the second person hits `Permission denied` on the cache, cannot edit the shared config, and the credential bridge either locks them out or hands them the first person's token.
127
+ Shared mode fixes this with an opt-in, admin-provisioned layout modelled on Docker's `root:docker` socket and Homebrew-on-Linux's shared tree: a dedicated `cwcli` group, a system state tree with setgid group-writable directories, and one machine-wide credential bridge behind a group-owned socket.
128
+
129
+ **Enable it (once, by an admin):**
130
+
131
+ ```bash
132
+ sudo cwcli setup shared --users alice,bob # create the group + tree, add members
133
+ sudo cwcli setup shared --users alice,bob --cred-bridge # also stand up the machine-wide credential bridge
134
+ ```
135
+
136
+ This creates the `cwcli` group and a `cwcli` service account, provisions `/var/lib/cwcli` (config, cache, projects, archive) as `root:cwcli` with setgid `2770` directories, and writes the marker `/etc/cwcli/shared.toml` that turns shared mode on.
137
+ It is idempotent - safe to re-run to add users or repair modes.
138
+ Members must log out and back in for the new group to take effect.
139
+ Windows is unaffected (shared mode is POSIX-only; cwcli stays per-user there).
140
+
141
+ **Consolidate existing per-user homes into the shared tree:**
142
+
143
+ ```bash
144
+ sudo cwcli setup migrate --from /home/alice/.cwcli --from /home/bob/.cwcli
145
+ sudo cwcli setup migrate --discover # find every /home/*/.cwcli and /root/.cwcli
146
+ ```
147
+
148
+ `migrate` copies each user's project state into the shared tree with the correct group ownership and modes.
149
+ It **never deletes a source** and **refuses to overwrite an existing shared project** (a name clash is reported, not clobbered), so it is safe to re-run.
150
+ The per-user SQLite cache is regenerable (`cwcli inspect`) and deliberately not merged.
151
+
152
+ **Opting an invocation back to a private home:** `CWCLI_HOME` always wins over shared mode, so `CWCLI_HOME=~/.cwcli cwcli ...` uses your own home even on a shared box.
153
+
154
+ **Security consequences - state them plainly before enabling:**
155
+
156
+ - **`cwcli` group membership is the trust boundary.** Anyone in the group can read every instance's cache, read the rm/rm-site backups (which contain full database dumps), edit the shared config, and - with `--cred-bridge` - ask the machine-wide bridge for a credential. This is the same "docker group ≈ root-ish capability" trade every shared dev tool makes. Keep membership deliberate.
157
+ - **The credential bridge in shared mode serves one machine-owned, read-only service-account token** to any group member's container git, so its audit log records the host and project but **not which human** triggered a fetch (they all present as the service account). Provision that account with a read-only deploy token; rotate it in one place if it leaks.
158
+ - The group-owned socket (`0660 root:cwcli`) closes the world-connectable `0666` socket that could otherwise leak the first user's token to a second user on the box. A uid outside the `cwcli` group cannot reach it.
159
+
160
+ Run `cwcli doctor` to verify the shared install (group present, tree modes, service account).
161
+
121
162
  ## Quick Start
122
163
 
123
164
  ```bash
@@ -153,6 +194,8 @@ A cluster containing an unknown short option is rejected as a whole.
153
194
 
154
195
  Creates a complete Frappe development environment in a single step. Downloads compose files, starts containers, initializes bench, creates a site, and starts the bench's dev services.
155
196
 
197
+ Blocks for roughly 10-20 minutes (image pulls, bench init, site creation). Running several `cwcli init` invocations in parallel can OOM-kill the host on a memory-constrained machine - serialize them instead. Adding a bench to an already-running instance (`cwcli init existing-project --bench another-bench`) never disturbs it or its other benches: the frappe container is never pulled, recreated, or restarted, so every already-serving bench keeps running throughout.
198
+
156
199
  ```bash
157
200
  cwcli init [OPTIONS] [PROJECT_NAME]
158
201
  ```
@@ -894,6 +937,7 @@ cwcli inspect [OPTIONS] PROJECT_NAME
894
937
 
895
938
  | Option | Description |
896
939
  |--------|-------------|
940
+ | `--bench <index\|label>` | Narrow the report to one bench on a multi-bench project (its numeric index or label). Every bench is still discovered/refreshed/cached regardless; only the output is narrowed |
897
941
  | `-v`, `--verbose` | Enable verbose diagnostic output |
898
942
  | `-j`, `--json` | Output the result as a JSON object |
899
943
  | `-u`, `--update` | Update the cache by re-inspecting the project |
@@ -948,6 +992,9 @@ cwcli inspect frappe-one --no-refresh
948
992
  # Get JSON output
949
993
  cwcli inspect frappe-one --json
950
994
 
995
+ # Narrow the report to one bench in a multi-bench project
996
+ cwcli inspect frappe-one --bench 1
997
+
951
998
  # Interactively label each bench (durable handle for --bench)
952
999
  cwcli inspect frappe-one --interactive
953
1000
  ```
@@ -1981,6 +2028,7 @@ This is the same class of exposure as SSH agent forwarding or VS Code's own cred
1981
2028
  Every request is written to an audit log (`~/.cwcli/run/credbridge-audit.log`, one line per request: timestamp, instance, host, answered - never a credential byte).
1982
2029
  For a tighter blast radius, authenticate `gh`/`glab` with a fine-grained, expiring token scoped to only the repos you need; the bridge faithfully forwards whatever the host tools hold, so the token's reach is your auth choice.
1983
2030
  On a shared multi-user host, note the per-instance socket lives in the instance's workspace directory under `~/.cwcli/projects/<name>/data/`; `enable` tightens `~/.cwcli/projects` to owner-only (0700) for exactly that reason.
2031
+ This per-user daemon is distinct from the **machine-wide** bridge of an opt-in [shared install](#shared-multi-user-install): there the daemon runs once as the `cwcli` service account (started by a system unit at boot), serves a single machine-owned read-only service-account token, and its socket is `0660 root:cwcli` so a uid outside the `cwcli` group cannot reach it. See that section for the security consequences.
1984
2032
 
1985
2033
  **Host allowlist:** the daemon only answers credential requests for `github.com` and `gitlab.com` by default.
1986
2034
  Using a self-hosted GitLab (or GitHub Enterprise)?
@@ -2257,8 +2305,11 @@ cwcli axi scale frappe-one --to 8 --yes
2257
2305
  # Every site carries installed_apps_verified: true only on a full read - the
2258
2306
  # cache/partial tiers carry a site's apps (and their git refs) forward without
2259
2307
  # re-observing them, so a non-full read with a remembered app list also warns.
2308
+ # --bench <index|label> narrows the report to one bench (every tier still
2309
+ # refreshes/caches every bench regardless; only the emitted report is narrowed).
2260
2310
  cwcli axi inspect frappe-one
2261
2311
  cwcli axi inspect frappe-one --update
2312
+ cwcli axi inspect frappe-one --bench 1
2262
2313
 
2263
2314
  # List a project's benches with their indices and labels. This is what answers
2264
2315
  # "pass --bench <index|label>" from any other verb - no other verb can.
@@ -2324,7 +2375,7 @@ cwcli axi config
2324
2375
  cwcli axi doctor
2325
2376
  ```
2326
2377
 
2327
- `cwcli axi ls`, `cwcli axi where`, `cwcli axi backup`, `cwcli axi unlock`, `cwcli axi stop`, `cwcli axi start`, `cwcli axi status`, `cwcli axi restart`, `cwcli axi inspect`, `cwcli axi benches`, and `cwcli axi label` run on the same logic core as their human counterparts; only the output (always TOON, never JSON) and choice-handling differ. `cwcli axi start` never prompts: an ambiguous multi-bench project is a `--bench` usage error (exit 2), and an unresolved port conflict is a `CONFLICT` error naming `--yes` (exit 1). `cwcli axi unlock` follows `cwcli axi backup`'s conventions exactly: an ambiguous multi-bench project is a `--bench` usage error (exit 2) and a stopped instance is a usage error pointing at `cwcli start` (exit 2). `cwcli axi stop` is idempotent for an agent - stopping an already-stopped project is a success, not an error. `cwcli axi where` answers from the cache, which outlives the instances it describes, so every match carries `project_state` (`present`, `absent`, or `unverified`) and the document carries `verified`: an agent must read those before acting on a hit, because an `absent` row describes an instance that no longer exists and an `unverified` row means the liveness check could not run. An unreachable Docker daemon degrades the whole answer to `unverified` rather than vouching for it, and `--no-verify` skips the check (reporting `unverified`) when a caller has already established liveness. `cwcli axi status` always exits 0, leading with the instance `overall` aggregate (`offline`/`online`/`running`/`degraded`). It reports **every** bench, in one document, under `benches[N]` - with no `--bench` it used to refuse a multi-bench project with exit 2 and send you off to `cwcli axi benches` to poll once per bench, reassembling the instance view from documents that never said which bench they described. The shape is uniform, so `supervisor_up`, `web_http_code`, `processes` and `not_cwcli_supervised` live inside `benches[i]` even for a single-bench project (`benches[0].processes`), and there is one parse path rather than a branch on bench count. Each bench also carries `bench_present` (`present`/`absent`/`unverified`) - the bench LIST comes from the cache, and a bench whose directory was deleted has no marker and no supervisord, which is indistinguishable from one that was simply never started, so it used to read as `online`; the token answers that, and `overall` keeps its four tokens. Each bench also carries `index`, `bench_path`, `label`, its own `overall`, `web_port`/`web_port_verified` (the port that bench's `web_http_code` was actually measured on), and `web_site` (the site it was measured FOR - Frappe routes by `Host`, so the code belongs to that site, and a probe naming no site is answered `404` by a healthy bench). When that port cannot be read it is `null`/`false` and **no probe was made** - cwcli never falls back to `:8000`, because on a multi-bench instance that reports a different bench's web server as this one's. A stopped instance carries `benches: []`. `cwcli axi stop` takes `--bench <index|label>` to stop ONE bench's dev processes, leaving sibling benches and every container running - the inverse of `cwcli axi start --bench`; without it, it stops the whole instance's containers, and either form is idempotent. `cwcli axi restart` requires `--process`; an unknown/ambiguous process is a usage error listing the valid labels (exit 2). `cwcli axi inspect` serves whichever freshness tier answers the request (`served_from` names it) and, unlike every other bench-scoped verb, has deliberately NO `--yes` - a stopped project on the refresh path is a usage error (exit 2) naming `cwcli start`, and a drift escalation that can no longer discover the bench serves the cached data with `degraded: true` and a warning (exit 0). Every site also carries `installed_apps_verified`: only a full read actually re-observes an app's installed version and git ref, so a cache or partial read carries the remembered list forward unverified and, when that list is non-empty, adds a warning naming `--update` - an agent must not treat an unverified `installed_apps` entry as the live git state. `cwcli axi benches` is the discovery verb behind every other verb's `--bench`: when a bench-scoped verb reports "multiple benches; pass `--bench <index|label>`", this is what tells you the valid values, and a project that has never been inspected is a structured error naming `cwcli axi inspect` rather than an empty list. Its rows come from the cache, which outlives the benches it describes, so every row carries `state` (`present`, `absent`, or `unverified`) and the document carries `verified` - the same contract `cwcli axi where` uses, cross-checked here against the running container in one exec. An agent must read `state` before acting on a row: `absent` means that bench directory no longer exists, and `unverified` means the check could not run (a stopped project, an unreachable daemon, or `--no-verify`) - never a confirmation. A stale row is reported, never pruned; `cwcli axi inspect <project> --update` is what refreshes it. `cwcli axi label` never starts a stopped project (the label marker lives inside the bench), and listing is deliberately `cwcli axi benches` rather than a mode of the mutation verb. `cwcli axi self-update --check` is read-only and **exits 0 whenever the check succeeds, including when an update is available** - the answer is the `is_outdated` field, not the exit code, because on the agent surface a non-zero exit means an error. This deliberately differs from the human `cwcli self-update --check`, which exits 1 when an update is available so shell scripts can gate on it; the mutating `cwcli axi self-update` is deliberately not offered. `cwcli axi config` is the read-only counterpart of `cwcli config show`: one TOON document carrying the search paths, all three auto-inspect state stores, the credential-bridge state, the tips setting, and the file locations. **No config-mutating axi verbs exist** (no `paths add`/`remove`, no `cache clear`, no `auto-inspect enable`/`disable`): an agent rewriting the user's search paths or wiping the cache is a product decision that deserves its own evidence, so mutations stay on the human `cwcli config` surface (whose reads all have `--json`). `cwcli axi doctor` is the read-only, system-wide counterpart of `cwcli doctor`: one TOON document listing every check with a machine-readable `status` (`pass`/`warn`/`fail`) an agent gates on directly, never by parsing the `detail` text. It exits 0 whenever every check is pass/warn and non-zero only when a check fails, exactly matching the human command, so a fleet can run it before driving any instance and `&&`-chain on the result.
2378
+ `cwcli axi ls`, `cwcli axi where`, `cwcli axi backup`, `cwcli axi unlock`, `cwcli axi stop`, `cwcli axi start`, `cwcli axi status`, `cwcli axi restart`, `cwcli axi inspect`, `cwcli axi benches`, and `cwcli axi label` run on the same logic core as their human counterparts; only the output (always TOON, never JSON) and choice-handling differ. `cwcli axi start` never prompts: an ambiguous multi-bench project is a `--bench` usage error (exit 2), and an unresolved port conflict is a `CONFLICT` error naming `--yes` (exit 1). `cwcli axi unlock` follows `cwcli axi backup`'s conventions exactly: an ambiguous multi-bench project is a `--bench` usage error (exit 2) and a stopped instance is a usage error pointing at `cwcli start` (exit 2). `cwcli axi stop` is idempotent for an agent - stopping an already-stopped project is a success, not an error. `cwcli axi where` answers from the cache, which outlives the instances it describes, so every match carries `project_state` (`present`, `absent`, or `unverified`) and the document carries `verified`: an agent must read those before acting on a hit, because an `absent` row describes an instance that no longer exists and an `unverified` row means the liveness check could not run. An unreachable Docker daemon degrades the whole answer to `unverified` rather than vouching for it, and `--no-verify` skips the check (reporting `unverified`) when a caller has already established liveness. `cwcli axi status` always exits 0, leading with the instance `overall` aggregate (`offline`/`online`/`running`/`degraded`). It reports **every** bench, in one document, under `benches[N]` - with no `--bench` it used to refuse a multi-bench project with exit 2 and send you off to `cwcli axi benches` to poll once per bench, reassembling the instance view from documents that never said which bench they described. The shape is uniform, so `supervisor_up`, `web_http_code`, `processes` and `not_cwcli_supervised` live inside `benches[i]` even for a single-bench project (`benches[0].processes`), and there is one parse path rather than a branch on bench count. Each bench also carries `bench_present` (`present`/`absent`/`unverified`) - the bench LIST comes from the cache, and a bench whose directory was deleted has no marker and no supervisord, which is indistinguishable from one that was simply never started, so it used to read as `online`; the token answers that, and `overall` keeps its four tokens. Each bench also carries `index`, `bench_path`, `label`, its own `overall`, `web_port`/`web_port_verified` (the port that bench's `web_http_code` was actually measured on), and `web_site` (the site it was measured FOR - Frappe routes by `Host`, so the code belongs to that site, and a probe naming no site is answered `404` by a healthy bench). When that port cannot be read it is `null`/`false` and **no probe was made** - cwcli never falls back to `:8000`, because on a multi-bench instance that reports a different bench's web server as this one's. A stopped instance carries `benches: []`. `cwcli axi stop` takes `--bench <index|label>` to stop ONE bench's dev processes, leaving sibling benches and every container running - the inverse of `cwcli axi start --bench`; without it, it stops the whole instance's containers, and either form is idempotent. `cwcli axi restart` requires `--process`; an unknown/ambiguous process is a usage error listing the valid labels (exit 2). `cwcli axi inspect` serves whichever freshness tier answers the request (`served_from` names it) and, unlike every other bench-scoped verb, has deliberately NO `--yes` - a stopped project on the refresh path is a usage error (exit 2) naming `cwcli start`, and a drift escalation that can no longer discover the bench serves the cached data with `degraded: true` and a warning (exit 0). It takes `--bench <index|label>` like every other bench-scoped verb, narrowing the emitted report to one bench; every tier still discovers/refreshes/caches every bench regardless of the selector, and an unknown selector is a `bench.not_found` error (exit 1). Every site also carries `installed_apps_verified`: only a full read actually re-observes an app's installed version and git ref, so a cache or partial read carries the remembered list forward unverified and, when that list is non-empty, adds a warning naming `--update` - an agent must not treat an unverified `installed_apps` entry as the live git state. `cwcli axi benches` is the discovery verb behind every other verb's `--bench`: when a bench-scoped verb reports "multiple benches; pass `--bench <index|label>`", this is what tells you the valid values, and a project that has never been inspected is a structured error naming `cwcli axi inspect` rather than an empty list. Its rows come from the cache, which outlives the benches it describes, so every row carries `state` (`present`, `absent`, or `unverified`) and the document carries `verified` - the same contract `cwcli axi where` uses, cross-checked here against the running container in one exec. An agent must read `state` before acting on a row: `absent` means that bench directory no longer exists, and `unverified` means the check could not run (a stopped project, an unreachable daemon, or `--no-verify`) - never a confirmation. A stale row is reported, never pruned; `cwcli axi inspect <project> --update` is what refreshes it. `cwcli axi label` never starts a stopped project (the label marker lives inside the bench), and listing is deliberately `cwcli axi benches` rather than a mode of the mutation verb. `cwcli axi self-update --check` is read-only and **exits 0 whenever the check succeeds, including when an update is available** - the answer is the `is_outdated` field, not the exit code, because on the agent surface a non-zero exit means an error. This deliberately differs from the human `cwcli self-update --check`, which exits 1 when an update is available so shell scripts can gate on it; the mutating `cwcli axi self-update` is deliberately not offered. `cwcli axi config` is the read-only counterpart of `cwcli config show`: one TOON document carrying the search paths, all three auto-inspect state stores, the credential-bridge state, the tips setting, and the file locations. **No config-mutating axi verbs exist** (no `paths add`/`remove`, no `cache clear`, no `auto-inspect enable`/`disable`): an agent rewriting the user's search paths or wiping the cache is a product decision that deserves its own evidence, so mutations stay on the human `cwcli config` surface (whose reads all have `--json`). `cwcli axi doctor` is the read-only, system-wide counterpart of `cwcli doctor`: one TOON document listing every check with a machine-readable `status` (`pass`/`warn`/`fail`) an agent gates on directly, never by parsing the `detail` text. It exits 0 whenever every check is pass/warn and non-zero only when a check fails, exactly matching the human command, so a fleet can run it before driving any instance and `&&`-chain on the result.
2328
2379
 
2329
2380
  `cwcli axi url <project>` answers two questions no other `axi` verb does: what HOST URL a bench actually serves on, and whether it is answering HTTP right now.
2330
2381
  `cwcli axi status`'s `web_http_code` is measured against the bench's CONTAINER-internal port and never states the host address; the host URL is computed only by `cwcli open`'s success banner and the human `cwcli init` banner, neither reachable from `cwcli axi`, so getting either answer meant dropping to a raw `docker inspect`/`curl` against the container.
@@ -2378,7 +2429,7 @@ It exits non-zero if a restart fails or the named site does not answer Frappe's
2378
2429
 
2379
2430
  These are agent-only for now: there is no human `cwcli migrate`, `cwcli run-tests`, or `cwcli build` yet. Use `cwcli run <project> --site <site> migrate` for the human equivalent.
2380
2431
 
2381
- `cwcli axi init` provisions a new instance, bench, and site the way `cwcli init` does, but non-interactively and as ONE terminal TOON document. It **blocks** for the full 10-20 minute run - the same block-and-emit-one-document shape as `cwcli axi apps update`, because a progress stream would break the one-TOON-document contract - narrating coarse phase labels to stderr; run `cwcli logs <project>` / `cwcli status <project>` from a second shell for live progress. After the bench+site is created, it starts the bench's dev services by default (the same `core.start` behind `cwcli start`/`cwcli axi start`), leaving a running bench rather than a created-but-idle one; `--no-start` creates without starting, for automation/CI, distinct from Docker container startup (stage 1 always brings the containers up regardless of this flag). A dev-services start failure degrades to a stderr warning rather than a non-zero exit, since the bench was already created. The site administrator password comes from the **`CWCLI_ADMIN_PASSWORD` environment variable (recommended)** or `--admin-password`; the flag wins if both are set, and with neither set the verb refuses with a usage error (exit 2) naming both - it never generates a password and never prompts. The env var keeps the secret off the process argv (visible in `ps`/shell history) that the flag exposes; the container-side `bench new-site` still receives it, so treat the value as one-time. The MariaDB root password takes the same shape via `CWCLI_DB_ROOT_PASSWORD` / `--db-root-password` (default `123`). The interactive decisions the human `cwcli init` prompts for become non-prompting errors: an existing bench with neither `--reuse-bench` nor `--no-reuse-bench` is a usage error (exit 2) naming both; a port conflict names `--port` (exit 1); containers that do not come up point at `cwcli status`/`cwcli logs` (exit 1). There is no `--auto-start` (compose `cwcli axi start` then re-run) and no `--verbose` (stdout is always TOON). `--bench` here is the NAME of the bench to create, distinct from the `--bench <index|label>` selector the other verbs use.
2432
+ `cwcli axi init` provisions a new instance, bench, and site the way `cwcli init` does, but non-interactively and as ONE terminal TOON document. It **blocks** for the full 10-20 minute run - the same block-and-emit-one-document shape as `cwcli axi apps update`, because a progress stream would break the one-TOON-document contract - narrating coarse phase labels to stderr; run `cwcli logs <project>` / `cwcli status <project>` from a second shell for live progress. Running several `axi init` calls in parallel can OOM-kill the host on a memory-constrained machine - serialize them instead. Adding a bench to an already-running instance never disturbs it or its other benches: the frappe container is never pulled, recreated, or restarted, so every already-serving bench keeps running throughout. After the bench+site is created, it starts the bench's dev services by default (the same `core.start` behind `cwcli start`/`cwcli axi start`), leaving a running bench rather than a created-but-idle one; `--no-start` creates without starting, for automation/CI, distinct from Docker container startup (stage 1 always brings the containers up regardless of this flag). A dev-services start failure degrades to a stderr warning rather than a non-zero exit, since the bench was already created. The site administrator password comes from the **`CWCLI_ADMIN_PASSWORD` environment variable (recommended)** or `--admin-password`; the flag wins if both are set, and with neither set the verb refuses with a usage error (exit 2) naming both - it never generates a password and never prompts. The env var keeps the secret off the process argv (visible in `ps`/shell history) that the flag exposes; the container-side `bench new-site` still receives it, so treat the value as one-time. The MariaDB root password takes the same shape via `CWCLI_DB_ROOT_PASSWORD` / `--db-root-password` (default `123`). The interactive decisions the human `cwcli init` prompts for become non-prompting errors: an existing bench with neither `--reuse-bench` nor `--no-reuse-bench` is a usage error (exit 2) naming both; a port conflict names `--port` (exit 1); containers that do not come up point at `cwcli status`/`cwcli logs` (exit 1). There is no `--auto-start` (compose `cwcli axi start` then re-run) and no `--verbose` (stdout is always TOON). `--bench` here is the NAME of the bench to create, distinct from the `--bench <index|label>` selector the other verbs use.
2382
2433
 
2383
2434
  `cwcli axi rm <project> --yes` permanently removes an instance: its containers, named volumes (databases, sites, and files), own Docker Compose network, and local project directory.
2384
2435
  It is the agent form of `cwcli rm`, and it runs the same code.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "caffeinated-whale-cli"
7
- version = "3.0.0"
7
+ version = "3.1.0"
8
8
  authors = [
9
9
  { name = "Christopher McKay", email = "mckay.christopher73@outlook.com" },
10
10
  ]
@@ -80,12 +80,18 @@ select = [
80
80
  "N", # pep8-naming
81
81
  "UP", # pyupgrade
82
82
  "B", # flake8-bugbear
83
+ "TID251", # banned API imports and access
83
84
  ]
84
85
  ignore = [
85
86
  "E501", # line too long (handled by black)
86
87
  "B008", # function call in default argument (required for Typer)
87
88
  ]
88
89
 
90
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
91
+ "typer._click" = { msg = "Use Typer's public API instead of its private Click shim." }
92
+ "importlib.import_module" = { msg = "Dynamic imports are forbidden. For a justified exception, add `# noqa: TID251` on that line." }
93
+ "builtins.__import__" = { msg = "The __import__ dynamic import is forbidden. For a justified exception, add `# noqa: TID251` on that line." }
94
+
89
95
  [tool.mypy]
90
96
  python_version = "3.10"
91
97
  warn_return_any = true
@@ -0,0 +1 @@
1
+ __version__ = "3.1.0"
@@ -23,7 +23,6 @@ stderr; stdout carries only TOON.
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- import importlib
27
26
  import os
28
27
  import re
29
28
  import sys
@@ -74,32 +73,34 @@ from ..utils import agent_hooks, cache, toon
74
73
  # rendering axi-surface parse failures as the same `error:`+`help:` TOON the core
75
74
  # errors already use, while leaving the human CLI's rich rendering untouched.
76
75
  #
77
- # Typer >= ~0.17 VENDORS Click as `typer._click`, so a TyperGroup then raises that
78
- # vendored ClickException/UsageError/Abort - a DIFFERENT class hierarchy from the
76
+ # Typer >= ~0.17 VENDORS Click, so a TyperGroup then raises vendored
77
+ # ClickException/UsageError/Abort - a DIFFERENT class hierarchy from the
79
78
  # separately-installed `click` package this module imports. `except click.X` then
80
79
  # silently misses them, and a bare `cwcli` (Missing command) escapes uncaught to
81
- # the top-level rich TRACEBACK instead of the intended usage error (exit 2). Catch
82
- # and type-test against BOTH flavors; older Typer (<= 0.16) has no `typer._click`
83
- # and uses standalone click alone. Deliberately paired with the protocol-based
84
- # `_is_argument`/`_is_option` param checks below, which vendored params also fail.
85
- try:
86
- _vendored_click_exc = importlib.import_module("typer._click.exceptions")
87
- _CLICK_EXCEPTIONS: tuple[type[click.ClickException], ...] = (
88
- click.ClickException,
89
- _vendored_click_exc.ClickException,
90
- )
91
- _USAGE_ERRORS: tuple[type[click.UsageError], ...] = (
92
- click.UsageError,
93
- _vendored_click_exc.UsageError,
94
- )
95
- _ABORTS: tuple[type[click.exceptions.Abort], ...] = (
96
- click.exceptions.Abort,
97
- _vendored_click_exc.Abort,
98
- )
99
- except ModuleNotFoundError: # standalone click (Typer <= 0.16)
100
- _CLICK_EXCEPTIONS = (click.ClickException,)
101
- _USAGE_ERRORS = (click.UsageError,)
102
- _ABORTS = (click.exceptions.Abort,)
80
+ # the top-level rich TRACEBACK instead of the intended usage error (exit 2). So we
81
+ # catch and type-test against BOTH flavors.
82
+ #
83
+ # Get the vendored bases from PUBLIC typer exports, never the private `typer._click`
84
+ # module: its layout is not API and shifts across releases (typer 0.27 dropped
85
+ # `Abort` from `typer._click.exceptions`, which crashed this import at module load).
86
+ # `typer.Abort` is the vendored Abort; `typer.BadParameter` subclasses the vendored
87
+ # `UsageError` -> `ClickException`, so its MRO yields those two bases by name. On
88
+ # Typer <= 0.16 (standalone click) these resolve to the same classes as `click.*`,
89
+ # and the sets below dedup - one code path, no version branch. Deliberately paired
90
+ # with the protocol-based `_is_argument`/`_is_option` param checks below, which
91
+ # vendored params also fail.
92
+ _typer_exc_bases: dict[str, type[BaseException]] = {
93
+ cls.__name__: cls for cls in typer.BadParameter.__mro__ if issubclass(cls, BaseException)
94
+ }
95
+ _CLICK_EXCEPTIONS: tuple[type[click.ClickException], ...] = tuple(
96
+ {click.ClickException, cast("type[click.ClickException]", _typer_exc_bases["ClickException"])}
97
+ )
98
+ _USAGE_ERRORS: tuple[type[click.UsageError], ...] = tuple(
99
+ {click.UsageError, cast("type[click.UsageError]", _typer_exc_bases["UsageError"])}
100
+ )
101
+ _ABORTS: tuple[type[click.exceptions.Abort], ...] = tuple(
102
+ {click.exceptions.Abort, cast("type[click.exceptions.Abort]", typer.Abort)}
103
+ )
103
104
 
104
105
 
105
106
  def _usage_error_message(error: click.UsageError) -> str:
@@ -1222,6 +1223,7 @@ def axi_inspect(
1222
1223
  "--no-refresh",
1223
1224
  help="Serve the cached data verbatim; zero container calls (may be stale).",
1224
1225
  ),
1226
+ bench: str = typer.Option(None, "--bench", help="Which bench: numeric index or label."),
1225
1227
  ) -> None:
1226
1228
  """Inspect a project's benches, sites, and apps; emit the report as TOON.
1227
1229
 
@@ -1247,10 +1249,15 @@ def axi_inspect(
1247
1249
  Deliberately NO ``--yes``: an axi verb must never open a start-from-axi path.
1248
1250
  A stopped project on the refresh path is a usage error (exit 2) naming
1249
1251
  ``cwcli start``, matching every other stopped-project fork on this surface.
1252
+
1253
+ ``--bench <index|label>`` narrows the report to one bench (the same selector
1254
+ ``axi status``/``axi logs``/``axi apps`` take); an unknown selector is a
1255
+ ``bench.not_found`` error (exit 1). Every tier still refreshes/caches every
1256
+ bench regardless of the selector - only the emitted report is narrowed.
1250
1257
  """
1251
1258
  refresh = "full" if update else ("cache_only" if no_refresh else "auto")
1252
1259
  try:
1253
- result = core_inspect.inspect(project, refresh=refresh)
1260
+ result = core_inspect.inspect(project, refresh=refresh, bench=bench)
1254
1261
  except CwcliError as error:
1255
1262
  emit_axi_error(error)
1256
1263
  raise typer.Exit(exit_for(error.kind)) from None
@@ -2223,7 +2230,11 @@ def axi_init(
2223
2230
  update` blocks on a long update - streaming N documents would break the
2224
2231
  one-TOON-document contract. Coarse phase progress goes to STDERR; for live or
2225
2232
  deeper progress, run `cwcli logs <project>` / `cwcli status <project>` from a
2226
- second shell.
2233
+ second shell. Running several `axi init` calls in parallel can OOM-kill the
2234
+ host on a memory-constrained machine - serialize them. Adding a bench to an
2235
+ already-running instance never disturbs it or its other benches: the frappe
2236
+ container is never pulled, recreated, or restarted, so every already-serving
2237
+ bench keeps running throughout.
2227
2238
 
2228
2239
  After the bench+site is created, dev services start by default (reusing the
2229
2240
  same `core.start` behind `cwcli start`/`axi start`), so this leaves a running
@@ -562,6 +562,13 @@ def init(
562
562
  Creates project directory, downloads compose files, starts containers,
563
563
  initializes bench, and creates a site. Optionally installs ERPNext.
564
564
 
565
+ Blocks for roughly 10-20 minutes (image pulls, bench init, site creation).
566
+ Running several `cwcli init` invocations in parallel can OOM-kill the host
567
+ on a memory-constrained machine - serialize them instead. Adding a bench
568
+ to an already-running instance never disturbs it or its other benches:
569
+ the frappe container is never pulled, recreated, or restarted, so every
570
+ already-serving bench keeps running throughout.
571
+
565
572
  If project_name is not provided, prompts interactively.
566
573
 
567
574
  The site's administrator password is generated and printed once when
@@ -83,6 +83,11 @@ def inspect(
83
83
  project_name: str = typer.Argument(
84
84
  ..., help="The Docker Compose project to inspect.", autocompletion=complete_project_names
85
85
  ),
86
+ bench: str = typer.Option(
87
+ None,
88
+ "--bench",
89
+ help="Narrow the report to one bench: its numeric index or label (multi-bench projects).",
90
+ ),
86
91
  verbose: bool = typer.Option(
87
92
  False, "--verbose", "-v", help="Enable verbose diagnostic output."
88
93
  ),
@@ -122,6 +127,11 @@ def inspect(
122
127
  """
123
128
  Inspects a Project to find all Bench Instances, Sites, and Apps within it.
124
129
  Caches the results for faster subsequent inspects.
130
+
131
+ ``--bench <index|label>`` narrows the report to one bench, on a multi-bench
132
+ project. Every tier still discovers/refreshes/caches every bench regardless
133
+ of the selector - only the rendered output is narrowed. An unknown selector
134
+ exits non-zero.
125
135
  """
126
136
  if verbose:
127
137
  console_err.print(f"VERBOSE: --- Inspecting Project: {project_name} ---")
@@ -141,6 +151,7 @@ def inspect(
141
151
  refresh=refresh,
142
152
  offer_choice=prompt_to_start,
143
153
  on_event=on_event,
154
+ bench=bench,
144
155
  )
145
156
  except CwcliError as e:
146
157
  raise render_error_exit(project_name, e) from None
@@ -185,13 +196,14 @@ def inspect(
185
196
  )
186
197
 
187
198
  assignments: list[tuple[str, str]] = []
188
- for position, bench in enumerate(bench_instances_data):
189
- index = bench.get("index", position)
190
- existing = bench.get("label")
199
+ for position, bench_instance in enumerate(bench_instances_data):
200
+ index = bench_instance.get("index", position)
201
+ existing = bench_instance.get("label")
191
202
  existing_hint = f" [current: '{existing}']" if existing else ""
192
203
  try:
193
204
  answer = questionary.text(
194
- f"Bench [{index}] at {bench['path']} on '{project_name}'.{existing_hint}\n"
205
+ f"Bench [{index}] at {bench_instance['path']} on '{project_name}'."
206
+ f"{existing_hint}\n"
195
207
  "Label (blank to keep, letters/digits/.-_ only): "
196
208
  ).ask()
197
209
  if answer is None: # User pressed Ctrl+C
@@ -205,7 +217,7 @@ def inspect(
205
217
  continue
206
218
 
207
219
  if answer.strip(): # Blank keeps the existing label.
208
- assignments.append((bench["path"], answer))
220
+ assignments.append((bench_instance["path"], answer))
209
221
 
210
222
  label_result = core_label.set_labels(project_name, assignments)
211
223
  for warning in label_result.warnings: