caffeinated-whale-cli 3.1.0__tar.gz → 3.1.2__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 (99) hide show
  1. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/PKG-INFO +58 -6
  2. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/README.md +57 -5
  3. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/pyproject.toml +1 -1
  4. caffeinated_whale_cli-3.1.2/src/caffeinated_whale_cli/__init__.py +1 -0
  5. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/axi.py +124 -0
  6. caffeinated_whale_cli-3.1.2/src/caffeinated_whale_cli/commands/rm_bench.py +184 -0
  7. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/docker.py +175 -3
  8. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/init.py +8 -4
  9. caffeinated_whale_cli-3.1.2/src/caffeinated_whale_cli/core/rm_bench.py +353 -0
  10. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/scale.py +4 -1
  11. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/start.py +12 -9
  12. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/update.py +23 -0
  13. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/main.py +2 -0
  14. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +2 -0
  15. caffeinated_whale_cli-3.1.0/src/caffeinated_whale_cli/__init__.py +0 -1
  16. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/LICENSE +0 -0
  17. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/setup.cfg +0 -0
  18. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/__main__.py +0 -0
  19. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
  20. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/apps.py +0 -0
  21. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/backup.py +0 -0
  22. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/config.py +0 -0
  23. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/console.html +0 -0
  24. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
  25. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/init.py +0 -0
  26. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
  27. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/label.py +0 -0
  28. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/list.py +0 -0
  29. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/logs.py +0 -0
  30. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/open.py +0 -0
  31. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/restart.py +0 -0
  32. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/restore.py +0 -0
  33. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/rm.py +0 -0
  34. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
  35. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/run.py +0 -0
  36. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/scale.py +0 -0
  37. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
  38. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/serve.py +0 -0
  39. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/setup.py +0 -0
  40. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/start.py +0 -0
  41. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/status.py +0 -0
  42. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/stop.py +0 -0
  43. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
  44. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/update.py +0 -0
  45. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/utils.py +0 -0
  46. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/commands/where.py +0 -0
  47. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/__init__.py +0 -0
  48. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/apps.py +0 -0
  49. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
  50. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/backup.py +0 -0
  51. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
  52. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/config.py +0 -0
  53. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/cred_bridge.py +0 -0
  54. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
  55. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/doctor.py +0 -0
  56. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/envelope.py +0 -0
  57. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/errors.py +0 -0
  58. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
  59. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/fleet.py +0 -0
  60. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/inspect.py +0 -0
  61. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/label.py +0 -0
  62. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/list.py +0 -0
  63. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/logs.py +0 -0
  64. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/open.py +0 -0
  65. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
  66. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/restart.py +0 -0
  67. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/restore.py +0 -0
  68. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/rm.py +0 -0
  69. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
  70. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/run.py +0 -0
  71. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/setup.py +0 -0
  72. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/status.py +0 -0
  73. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/stop.py +0 -0
  74. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/supervision.py +0 -0
  75. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/unlock.py +0 -0
  76. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/url.py +0 -0
  77. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/version.py +0 -0
  78. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/core/where.py +0 -0
  79. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/update_notice.py +0 -0
  80. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
  81. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
  82. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
  83. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
  84. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
  85. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/cache.py +0 -0
  86. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
  87. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/config_utils.py +0 -0
  88. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/console.py +0 -0
  89. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/cred_daemon.py +0 -0
  90. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/daemon_identity.py +0 -0
  91. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
  92. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
  93. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
  94. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
  95. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/shared_home.py +0 -0
  96. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/startup.py +0 -0
  97. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/tips.py +0 -0
  98. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/src/caffeinated_whale_cli/utils/toon.py +0 -0
  99. {caffeinated_whale_cli-3.1.0 → caffeinated_whale_cli-3.1.2}/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.1.0
3
+ Version: 3.1.2
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
@@ -886,6 +886,58 @@ See [For agents: the `cwcli axi` surface](#for-agents-the-cwcli-axi-surface).
886
886
 
887
887
  ---
888
888
 
889
+ ### `rm-bench` - Remove One Bench
890
+
891
+ Permanently removes ONE bench from an instance, leaving every other bench and every container running.
892
+ Like `rm-site`, this is a separate, noun-scoped command rather than a `--bench` flag on `cwcli rm`: `cwcli rm` destroys the whole instance, while `rm-bench` only ever touches the one bench you name, so forgetting a flag can never turn a single-bench cleanup into an instance-wide deletion.
893
+
894
+ A bench is a directory on the instance's shared workspace bind mount, and its sites' data lives in the shared database - a bench has **no** dedicated container or named volume of its own.
895
+ So `cwcli rm-bench` backs up and drops every site on the target bench (deleting each site's database and files), then deletes the bench directory.
896
+
897
+ **WARNING:** This action is destructive and cannot be undone.
898
+ Each site is dropped through the same verified copy-out that `cwcli rm-site` uses: `bench drop-site` backs the site up, and cwcli copies that archive out to `~/.cwcli/archive/{project}_dropped_sites/` (or `$CWCLI_HOME/archive/...` when that override is set) and verifies it on disk before the bench directory is deleted.
899
+ If any site's backup cannot be copied out, the bench directory is kept (so the trapped backup is never destroyed) and the command exits non-zero.
900
+
901
+ A bench that is still running is refused: stop it first with `cwcli stop <project> --bench <selector>`.
902
+ `cwcli rm-bench` never deletes a bench out from under its own running code.
903
+
904
+ ```bash
905
+ cwcli rm-bench [OPTIONS] PROJECT_NAME
906
+ ```
907
+
908
+ **Arguments:**
909
+
910
+ | Argument | Description |
911
+ |----------|-------------|
912
+ | `PROJECT_NAME` | The Docker Compose project name (required) |
913
+
914
+ **Options:**
915
+
916
+ | Option | Description |
917
+ |--------|-------------|
918
+ | `--bench TEXT` | Which bench to remove: its numeric index or label (see [Working with Multiple Benches](#working-with-multiple-benches)) |
919
+ | `-p`, `--path TEXT` | Explicit bench directory inside the container (lower-level alternative to `--bench`; cannot be combined with it) |
920
+ | `--db-root-password TEXT` | MariaDB root password used by `bench drop-site` (default: `123`, matching the compose file's own default) |
921
+ | `-y`, `--yes` | Skip the destructive confirmation and auto-start the container |
922
+ | `-v`, `--verbose` | Enable verbose output |
923
+
924
+ **Examples:**
925
+
926
+ ```bash
927
+ # Remove a bench (with confirmation)
928
+ cwcli rm-bench my-project --bench 2
929
+
930
+ # Skip the confirmation prompt
931
+ cwcli rm-bench my-project --bench staging --yes
932
+ ```
933
+
934
+ **Agent surface:** `cwcli axi rm-bench <project> --bench <index|label> --yes` emits the outcome as TOON, including `sites_dropped`, `archived_host_paths`, `dir_removed`, and `ok`.
935
+ `--yes` is required (never a prompt) and grants ONLY consent - it never auto-starts a stopped project, exactly like every other bench-scoped `axi` verb.
936
+ A running bench is a non-zero refusal naming `cwcli stop`.
937
+ See [For agents: the `cwcli axi` surface](#for-agents-the-cwcli-axi-surface).
938
+
939
+ ---
940
+
889
941
  ### `logs` - View Bench Logs
890
942
 
891
943
  View the bench's process logs. supervisord writes one log file per Procfile
@@ -1201,7 +1253,7 @@ A bench that was not running is left alone.
1201
1253
  **`apps uninstall`** - removes each app from the target site(s) (`bench --site <site> uninstall-app`). This destroys site data, so it is gated by `-y`/`--yes` or an interactive confirmation (a non-TTY without `--yes` refuses). It uses the same resynchronise-and-verify step as install, so no supervised process - web, scheduler, or worker - can keep running against the code and tables of an app that is gone.
1202
1254
 
1203
1255
  **`apps update`** - the canonical app-update path (what the deprecated `cwcli update` now delegates to). Updating the `frappe` framework app runs `bench update --reset`; other apps use the normal git-pull + migrate flow. After the migrations finish and maintenance mode is lifted, it runs the same **resynchronise** step as `apps install`, so a `git pull` cannot leave the bench's web, scheduler, and workers on the code that was there before it; every migrated site must answer Frappe before the run reports success. `--site` narrows which affected sites are migrated; if none of the named site(s) actually have the app installed, the command refuses and exits non-zero rather than silently migrating nothing (a genuine typo/mismatch guard - a bench with no affected sites at all still exits zero). It accepts the same migration flags as the [deprecated `update` command](#update---update-apps-and-migrate) (`--clear-cache`, `--clear-website-cache`, `--build`, `--skip-maintenance`, `--no-recache`). When updating the `frappe` framework app the flow runs the bench-wide `bench update --reset`, so `--site` and those per-app migration flags do not apply and are reported as ignored.
1204
- `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`.
1256
+ `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`. `--force` also clears a git **"dubious ownership"** refusal before pulling: when an app's repository is owned by a different user than the container runs as - common on a shared-mode or migrated instance whose app sources are owned by a pre-migration uid - it re-owns the repository to the container user so the pull proceeds, rather than surfacing a raw git ownership error. A `cwcli start`/`cwcli restart` self-heals the same ownership so a plain `apps update` works too; `--force` fixes it inline without a restart.
1205
1257
 
1206
1258
  **`apps checkout`** - fetches and checks out an arbitrary branch, tag, or commit (`REF`) into an app that is **already present** in the bench (`apps/<app>`), so a specific feature branch can be put under test in the instance the app lives in.
1207
1259
  Unlike `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app), this targets one existing checkout: it runs `git fetch <remote> <ref>` then `git checkout -B <ref> FETCH_HEAD` in the app directory (the remote is auto-detected - `upstream` for a bench-installed app, `origin` for a hand-cloned one).
@@ -2204,11 +2256,11 @@ cwcli run my-project migrate --bench 1 # by index
2204
2256
  cwcli update my-project --app erpnext --bench staging # by label
2205
2257
  ```
2206
2258
 
2207
- `--bench` is available on `run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `start`, `stop`, `restart`, `status`, and `logs`. Setting labels lives in the [`label`](#label---manage-bench-labels) command.
2259
+ `--bench` is available on `run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `rm-bench`, `start`, `stop`, `restart`, `status`, and `logs`. Setting labels lives in the [`label`](#label---manage-bench-labels) command.
2208
2260
 
2209
2261
  **No silent guessing on data commands:**
2210
2262
 
2211
- If a project has more than one bench and you do not pass `--bench` (or `--path`), the data commands (`run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`) stop and list the benches instead of guessing:
2263
+ If a project has more than one bench and you do not pass `--bench` (or `--path`), the data commands (`run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `rm-bench`) stop and list the benches instead of guessing:
2212
2264
 
2213
2265
  ```
2214
2266
  Error: project 'my-project' has multiple benches; specify one with --bench <index|label>:
@@ -2592,14 +2644,14 @@ The CLI uses:
2592
2644
  - **Peewee ORM** - SQLite-based caching
2593
2645
 
2594
2646
  **Logic core:** business logic and I/O live in a UI-pure `core/` package that carries no `rich`/`questionary`/`typer`; it returns a serializable typed envelope (or raises a typed error) so the human CLI, the `cwcli axi` agent surface, and the Console GUI are all thin frontends over one implementation.
2595
- `backup`, `unlock`, `stop`, `label`, `run`, `ls`/`list`, `where`, `start`/`status`/`restart`, `logs`, `inspect`, `apps`, `update`, `open`, `init`, `config`, `restore`, `rm`, and `rm-site` are migrated onto it so far.
2647
+ `backup`, `unlock`, `stop`, `label`, `run`, `ls`/`list`, `where`, `start`/`status`/`restart`, `logs`, `inspect`, `apps`, `update`, `open`, `init`, `config`, `restore`, `rm`, `rm-site`, and `rm-bench` are migrated onto it so far.
2596
2648
 
2597
2649
  **Data Directories:**
2598
2650
  - **Projects**: `~/.cwcli/projects/` - Project directories created by `cwcli init`
2599
2651
  - **Config**: `~/.cwcli/config/` - Configuration files
2600
2652
  - **Cache**: `~/.cwcli/cache/cwc-cache.db` - Project inspection cache
2601
2653
  - **Runtime**: `~/.cwcli/run/` - PID and log files for background services
2602
- - **Archive**: `~/.cwcli/archive/` - Pre-deletion backups and config snapshots from `cwcli rm`, plus dropped-site archives from `cwcli rm-site`
2654
+ - **Archive**: `~/.cwcli/archive/` - Pre-deletion backups and config snapshots from `cwcli rm`, plus dropped-site archives from `cwcli rm-site` and `cwcli rm-bench`
2603
2655
  - **Transfers**: `~/.cwcli/tmp/` by default, or `$TMPDIR/cwcli/`, `$TEMP/cwcli/`, or `$TMP/cwcli/` when the first non-empty variable in that order is set. Temporary files used by `restore --send` and `restore --receive`; each attempt is removed when it finishes or fails
2604
2656
 
2605
2657
  **Relocating cwcli's data (`CWCLI_HOME`):**
@@ -846,6 +846,58 @@ See [For agents: the `cwcli axi` surface](#for-agents-the-cwcli-axi-surface).
846
846
 
847
847
  ---
848
848
 
849
+ ### `rm-bench` - Remove One Bench
850
+
851
+ Permanently removes ONE bench from an instance, leaving every other bench and every container running.
852
+ Like `rm-site`, this is a separate, noun-scoped command rather than a `--bench` flag on `cwcli rm`: `cwcli rm` destroys the whole instance, while `rm-bench` only ever touches the one bench you name, so forgetting a flag can never turn a single-bench cleanup into an instance-wide deletion.
853
+
854
+ A bench is a directory on the instance's shared workspace bind mount, and its sites' data lives in the shared database - a bench has **no** dedicated container or named volume of its own.
855
+ So `cwcli rm-bench` backs up and drops every site on the target bench (deleting each site's database and files), then deletes the bench directory.
856
+
857
+ **WARNING:** This action is destructive and cannot be undone.
858
+ Each site is dropped through the same verified copy-out that `cwcli rm-site` uses: `bench drop-site` backs the site up, and cwcli copies that archive out to `~/.cwcli/archive/{project}_dropped_sites/` (or `$CWCLI_HOME/archive/...` when that override is set) and verifies it on disk before the bench directory is deleted.
859
+ If any site's backup cannot be copied out, the bench directory is kept (so the trapped backup is never destroyed) and the command exits non-zero.
860
+
861
+ A bench that is still running is refused: stop it first with `cwcli stop <project> --bench <selector>`.
862
+ `cwcli rm-bench` never deletes a bench out from under its own running code.
863
+
864
+ ```bash
865
+ cwcli rm-bench [OPTIONS] PROJECT_NAME
866
+ ```
867
+
868
+ **Arguments:**
869
+
870
+ | Argument | Description |
871
+ |----------|-------------|
872
+ | `PROJECT_NAME` | The Docker Compose project name (required) |
873
+
874
+ **Options:**
875
+
876
+ | Option | Description |
877
+ |--------|-------------|
878
+ | `--bench TEXT` | Which bench to remove: its numeric index or label (see [Working with Multiple Benches](#working-with-multiple-benches)) |
879
+ | `-p`, `--path TEXT` | Explicit bench directory inside the container (lower-level alternative to `--bench`; cannot be combined with it) |
880
+ | `--db-root-password TEXT` | MariaDB root password used by `bench drop-site` (default: `123`, matching the compose file's own default) |
881
+ | `-y`, `--yes` | Skip the destructive confirmation and auto-start the container |
882
+ | `-v`, `--verbose` | Enable verbose output |
883
+
884
+ **Examples:**
885
+
886
+ ```bash
887
+ # Remove a bench (with confirmation)
888
+ cwcli rm-bench my-project --bench 2
889
+
890
+ # Skip the confirmation prompt
891
+ cwcli rm-bench my-project --bench staging --yes
892
+ ```
893
+
894
+ **Agent surface:** `cwcli axi rm-bench <project> --bench <index|label> --yes` emits the outcome as TOON, including `sites_dropped`, `archived_host_paths`, `dir_removed`, and `ok`.
895
+ `--yes` is required (never a prompt) and grants ONLY consent - it never auto-starts a stopped project, exactly like every other bench-scoped `axi` verb.
896
+ A running bench is a non-zero refusal naming `cwcli stop`.
897
+ See [For agents: the `cwcli axi` surface](#for-agents-the-cwcli-axi-surface).
898
+
899
+ ---
900
+
849
901
  ### `logs` - View Bench Logs
850
902
 
851
903
  View the bench's process logs. supervisord writes one log file per Procfile
@@ -1161,7 +1213,7 @@ A bench that was not running is left alone.
1161
1213
  **`apps uninstall`** - removes each app from the target site(s) (`bench --site <site> uninstall-app`). This destroys site data, so it is gated by `-y`/`--yes` or an interactive confirmation (a non-TTY without `--yes` refuses). It uses the same resynchronise-and-verify step as install, so no supervised process - web, scheduler, or worker - can keep running against the code and tables of an app that is gone.
1162
1214
 
1163
1215
  **`apps update`** - the canonical app-update path (what the deprecated `cwcli update` now delegates to). Updating the `frappe` framework app runs `bench update --reset`; other apps use the normal git-pull + migrate flow. After the migrations finish and maintenance mode is lifted, it runs the same **resynchronise** step as `apps install`, so a `git pull` cannot leave the bench's web, scheduler, and workers on the code that was there before it; every migrated site must answer Frappe before the run reports success. `--site` narrows which affected sites are migrated; if none of the named site(s) actually have the app installed, the command refuses and exits non-zero rather than silently migrating nothing (a genuine typo/mismatch guard - a bench with no affected sites at all still exits zero). It accepts the same migration flags as the [deprecated `update` command](#update---update-apps-and-migrate) (`--clear-cache`, `--clear-website-cache`, `--build`, `--skip-maintenance`, `--no-recache`). When updating the `frappe` framework app the flow runs the bench-wide `bench update --reset`, so `--site` and those per-app migration flags do not apply and are reported as ignored.
1164
- `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`.
1216
+ `--force` is an opt-in, **destructive** recovery for a conflicting update: if an app's `git pull` hits a **conflict** (local changes that would be overwritten, or a conflicted merge), it hard-resets that app to its remote tracking branch (`git fetch` then `git reset --hard @{u}`), discarding the local changes, so the update completes instead of failing. It fires **only on a conflict** - a clean update is untouched, and a pull that failed for another reason (network, auth) is left as the failure it is rather than having work discarded. Without `--force`, a conflict fails safely and discards nothing. Every force-reset app is reported (`force_reset_apps`) and a warning is printed at the reset. `--force` does not delete untracked files (it never runs `git clean`), matching `apps checkout --reset`. `--force` also clears a git **"dubious ownership"** refusal before pulling: when an app's repository is owned by a different user than the container runs as - common on a shared-mode or migrated instance whose app sources are owned by a pre-migration uid - it re-owns the repository to the container user so the pull proceeds, rather than surfacing a raw git ownership error. A `cwcli start`/`cwcli restart` self-heals the same ownership so a plain `apps update` works too; `--force` fixes it inline without a restart.
1165
1217
 
1166
1218
  **`apps checkout`** - fetches and checks out an arbitrary branch, tag, or commit (`REF`) into an app that is **already present** in the bench (`apps/<app>`), so a specific feature branch can be put under test in the instance the app lives in.
1167
1219
  Unlike `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app), this targets one existing checkout: it runs `git fetch <remote> <ref>` then `git checkout -B <ref> FETCH_HEAD` in the app directory (the remote is auto-detected - `upstream` for a bench-installed app, `origin` for a hand-cloned one).
@@ -2164,11 +2216,11 @@ cwcli run my-project migrate --bench 1 # by index
2164
2216
  cwcli update my-project --app erpnext --bench staging # by label
2165
2217
  ```
2166
2218
 
2167
- `--bench` is available on `run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `start`, `stop`, `restart`, `status`, and `logs`. Setting labels lives in the [`label`](#label---manage-bench-labels) command.
2219
+ `--bench` is available on `run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `rm-bench`, `start`, `stop`, `restart`, `status`, and `logs`. Setting labels lives in the [`label`](#label---manage-bench-labels) command.
2168
2220
 
2169
2221
  **No silent guessing on data commands:**
2170
2222
 
2171
- If a project has more than one bench and you do not pass `--bench` (or `--path`), the data commands (`run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`) stop and list the benches instead of guessing:
2223
+ If a project has more than one bench and you do not pass `--bench` (or `--path`), the data commands (`run`, `backup`, `update`, `open`, `unlock`, `restore`, `rm-site`, `rm-bench`) stop and list the benches instead of guessing:
2172
2224
 
2173
2225
  ```
2174
2226
  Error: project 'my-project' has multiple benches; specify one with --bench <index|label>:
@@ -2552,14 +2604,14 @@ The CLI uses:
2552
2604
  - **Peewee ORM** - SQLite-based caching
2553
2605
 
2554
2606
  **Logic core:** business logic and I/O live in a UI-pure `core/` package that carries no `rich`/`questionary`/`typer`; it returns a serializable typed envelope (or raises a typed error) so the human CLI, the `cwcli axi` agent surface, and the Console GUI are all thin frontends over one implementation.
2555
- `backup`, `unlock`, `stop`, `label`, `run`, `ls`/`list`, `where`, `start`/`status`/`restart`, `logs`, `inspect`, `apps`, `update`, `open`, `init`, `config`, `restore`, `rm`, and `rm-site` are migrated onto it so far.
2607
+ `backup`, `unlock`, `stop`, `label`, `run`, `ls`/`list`, `where`, `start`/`status`/`restart`, `logs`, `inspect`, `apps`, `update`, `open`, `init`, `config`, `restore`, `rm`, `rm-site`, and `rm-bench` are migrated onto it so far.
2556
2608
 
2557
2609
  **Data Directories:**
2558
2610
  - **Projects**: `~/.cwcli/projects/` - Project directories created by `cwcli init`
2559
2611
  - **Config**: `~/.cwcli/config/` - Configuration files
2560
2612
  - **Cache**: `~/.cwcli/cache/cwc-cache.db` - Project inspection cache
2561
2613
  - **Runtime**: `~/.cwcli/run/` - PID and log files for background services
2562
- - **Archive**: `~/.cwcli/archive/` - Pre-deletion backups and config snapshots from `cwcli rm`, plus dropped-site archives from `cwcli rm-site`
2614
+ - **Archive**: `~/.cwcli/archive/` - Pre-deletion backups and config snapshots from `cwcli rm`, plus dropped-site archives from `cwcli rm-site` and `cwcli rm-bench`
2563
2615
  - **Transfers**: `~/.cwcli/tmp/` by default, or `$TMPDIR/cwcli/`, `$TEMP/cwcli/`, or `$TMP/cwcli/` when the first non-empty variable in that order is set. Temporary files used by `restore --send` and `restore --receive`; each attempt is removed when it finishes or fails
2564
2616
 
2565
2617
  **Relocating cwcli's data (`CWCLI_HOME`):**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "caffeinated-whale-cli"
7
- version = "3.1.0"
7
+ version = "3.1.2"
8
8
  authors = [
9
9
  { name = "Christopher McKay", email = "mckay.christopher73@outlook.com" },
10
10
  ]
@@ -0,0 +1 @@
1
+ __version__ = "3.1.2"
@@ -47,6 +47,7 @@ from ..core import list as core_list
47
47
  from ..core import logs as core_logs
48
48
  from ..core import restart as core_restart
49
49
  from ..core import rm as core_rm
50
+ from ..core import rm_bench as core_rm_bench
50
51
  from ..core import rm_site as core_rm_site
51
52
  from ..core import scale as core_scale
52
53
  from ..core import start as core_start
@@ -578,6 +579,11 @@ def _choice_error_message(choice: Choice) -> str:
578
579
  # `axi rm-site` only. Dropping a site deletes its database and files;
579
580
  # the agent must consent. The --yes flag rides the `help:` line below.
580
581
  return "dropping this site permanently deletes its database and files"
582
+ if choice.kind == "confirm_remove_bench":
583
+ # `axi rm-bench` only. Removing a bench drops every site on it (their
584
+ # databases and files) and deletes the bench directory; the agent must
585
+ # consent. The --yes flag rides the `help:` line below.
586
+ return "removing this bench drops every site on it and deletes its directory"
581
587
  return f"a decision is required: {choice.prompt}"
582
588
 
583
589
 
@@ -602,6 +608,8 @@ def emit_axi_choice_as_usage_error(choice: Choice) -> None:
602
608
  typer.echo(toon.kv("help", "re-run with --yes to accept the whole-instance restart"))
603
609
  elif choice.kind == "confirm_drop_site":
604
610
  typer.echo(toon.kv("help", "re-run with --yes to consent to permanently dropping it"))
611
+ elif choice.kind == "confirm_remove_bench":
612
+ typer.echo(toon.kv("help", "re-run with --yes to consent to permanently removing it"))
605
613
  elif choice.kind == "confirm_reuse_bench":
606
614
  typer.echo(
607
615
  toon.kv(
@@ -2673,6 +2681,122 @@ def axi_rm_site(
2673
2681
  raise typer.Exit(0 if outcome.ok else 1)
2674
2682
 
2675
2683
 
2684
+ # --------------------------------------------------------------------------- rm-bench
2685
+
2686
+
2687
+ def _rm_bench_narrate(event) -> None:
2688
+ """Every rm-bench event to STDERR (the `_drop_site_narrate` reasoning): the
2689
+ per-site `bench drop-site` output and progress notes are not supervised-process
2690
+ logs, and they must not touch stdout, which carries the one TOON document.
2691
+ """
2692
+ if isinstance(event, core_rm_bench.BenchRmCommand):
2693
+ print(f"$ {event.command}", file=sys.stderr, flush=True)
2694
+ elif isinstance(event, core_rm_bench.BenchRmOutput):
2695
+ print(event.text, end="", file=sys.stderr, flush=True)
2696
+ elif isinstance(event, (core_rm_bench.BenchRmNotice, core_rm_bench.BenchRmStep)):
2697
+ text = getattr(event, "text", None) or getattr(event, "label", "")
2698
+ print(text, file=sys.stderr, flush=True)
2699
+ elif isinstance(event, core_rm_bench.BenchRmWarning):
2700
+ print(f"Warning: {event.text}", file=sys.stderr, flush=True)
2701
+ if event.hint:
2702
+ print(event.hint, file=sys.stderr, flush=True)
2703
+
2704
+
2705
+ @app.command("rm-bench")
2706
+ def axi_rm_bench(
2707
+ project: str = typer.Argument(..., help="The Docker Compose project name."),
2708
+ bench: str = typer.Option(
2709
+ None, "--bench", help="Which bench to remove: numeric index or label (see 'axi benches')."
2710
+ ),
2711
+ db_root_password: str = typer.Option(
2712
+ "123", "--db-root-password", help="MariaDB root password used by 'bench drop-site'."
2713
+ ),
2714
+ yes: bool = typer.Option(
2715
+ False,
2716
+ "--yes",
2717
+ help="REQUIRED consent to permanently remove this bench and drop its sites.",
2718
+ ),
2719
+ ) -> None:
2720
+ """Permanently remove ONE bench; emit the outcome as TOON (never prompts).
2721
+
2722
+ The bench-scoped sibling of `axi rm` (whole instance) and `axi rm-site` (one
2723
+ site). A bench has no dedicated container or named volume - it is a directory
2724
+ on the instance's shared workspace bind mount whose sites live in the shared
2725
+ database - so this backs up and drops every site on the bench (deleting each
2726
+ site's database and files) and then deletes the bench directory. Every OTHER
2727
+ bench and every container keep running; it never touches the instance's
2728
+ containers, named volumes, or project directory.
2729
+
2730
+ `--yes` is REQUIRED: without it this is a usage error naming the flag, never
2731
+ a prompt. NO auto-start: a stopped project is a usage error naming
2732
+ `cwcli start`, exactly as every other bench-scoped axi verb refuses it.
2733
+
2734
+ A bench that is still running is refused (exit 1) naming
2735
+ `cwcli stop <project> --bench <selector>`: a live bench is never deleted out
2736
+ from under its own running code.
2737
+
2738
+ Each site is dropped through the same verified copy-out `axi rm-site` uses:
2739
+ `bench drop-site` backs the site up, and cwcli copies that archive out to the
2740
+ managed host location (`archived_host_paths` in the report) and verifies it on
2741
+ disk before the bench directory is deleted. If any site's archive could not be
2742
+ copied out, the directory is kept (to preserve that trapped archive), the site
2743
+ is reported in `sites_failed`, and `ok: false` with a non-zero exit.
2744
+
2745
+ Each `bench drop-site`'s own output goes to stderr in full and unparsed.
2746
+ """
2747
+ if not yes:
2748
+ emit_axi_error(
2749
+ CwcliError(
2750
+ ErrorKind.USAGE,
2751
+ "rm_bench.consent_required",
2752
+ f"Refusing to remove a bench from '{project}' without explicit consent.",
2753
+ hint=(
2754
+ "re-run with --yes to permanently remove this bench and drop its "
2755
+ "sites' databases and files"
2756
+ ),
2757
+ )
2758
+ )
2759
+ raise typer.Exit(exit_for(ErrorKind.USAGE))
2760
+
2761
+ try:
2762
+ result = core_rm_bench.remove_bench(
2763
+ project,
2764
+ bench=bench,
2765
+ consent=yes,
2766
+ db_root_password=db_root_password,
2767
+ on_event=_rm_bench_narrate,
2768
+ )
2769
+ except CwcliError as error:
2770
+ emit_axi_error(error)
2771
+ raise typer.Exit(exit_for(error.kind)) from None
2772
+
2773
+ if result.status is CoreStatus.NEEDS_CHOICE:
2774
+ assert result.choice is not None # NEEDS_CHOICE always carries a Choice
2775
+ emit_axi_choice_as_usage_error(result.choice)
2776
+ raise typer.Exit(2)
2777
+
2778
+ assert result.data is not None # OK/WARNING always carries a BenchRemovalOutcome
2779
+ outcome = result.data
2780
+
2781
+ # Removing a bench changes the instance's bench set, so refresh the cache.
2782
+ # A failed recache is a stderr warning, NOT a non-zero exit: the removal
2783
+ # already happened, and failing here would make an agent retry a bench that
2784
+ # is already gone.
2785
+ if not cache.recache_project(project):
2786
+ print(
2787
+ f"Warning: bench removed, but re-caching '{project}' failed; "
2788
+ "run 'cwcli inspect --update' to refresh.",
2789
+ file=sys.stderr,
2790
+ flush=True,
2791
+ )
2792
+
2793
+ emit_result(outcome, warnings=result.warnings)
2794
+ # ok, NOT result.status: a partial removal (a site's backup trapped in the
2795
+ # container) is a WARNING-shaped envelope, which maps to exit 0 everywhere
2796
+ # else and would report a green exit for an incomplete removal.
2797
+ raise typer.Exit(0 if outcome.ok else 1)
2798
+
2799
+
2676
2800
  # ------------------------------------------------------------------------ self-update
2677
2801
 
2678
2802
 
@@ -0,0 +1,184 @@
1
+ """``cwcli rm-bench`` - the human frontend over ``core.remove_bench``.
2
+
3
+ A dedicated, noun-scoped verb (not a ``--bench`` flag on ``rm``): removing ONE
4
+ bench drops its sites and deletes its directory while every sibling bench and
5
+ container keeps running, whereas ``cwcli rm`` destroys the whole instance, and
6
+ hiding that difference behind a flag on the bigger command is a footgun. See
7
+ ``core/rm_bench.py``'s module docstring for the full rationale, the "refuse while
8
+ running" gate, and the per-site backup-then-drop the deletion is built from.
9
+
10
+ This module only prompts, resolves the bench, streams each site drop's own
11
+ output, and renders the outcome; the resolution, the running gate, the
12
+ ``bench drop-site`` fan-out, and the directory deletion live in the core.
13
+ """
14
+
15
+ import sys
16
+
17
+ import typer
18
+
19
+ from ..core import rm_bench as core_rm_bench
20
+ from ..core.envelope import Status
21
+ from ..core.errors import CwcliError
22
+ from ..core.rm_bench import (
23
+ BenchRmCommand,
24
+ BenchRmNotice,
25
+ BenchRmOutput,
26
+ BenchRmStep,
27
+ BenchRmWarning,
28
+ )
29
+ from ..utils import cache
30
+ from ..utils.completion_utils import complete_project_names
31
+ from ..utils.console import console, stderr_console
32
+ from ..utils.docker_utils import handle_docker_errors
33
+ from .utils import confirm_or_exit, ensure_containers_running, resolve_bench_path
34
+
35
+
36
+ def _make_renderer(*, verbose: bool):
37
+ def on_event(event) -> None:
38
+ if isinstance(event, BenchRmStep):
39
+ if verbose:
40
+ stderr_console.print(f"[dim]{event.label}[/dim]")
41
+ elif isinstance(event, BenchRmCommand):
42
+ if verbose:
43
+ stderr_console.print(f"[dim]$ {event.command}[/dim]")
44
+ elif isinstance(event, BenchRmOutput):
45
+ sys.stdout.write(event.text)
46
+ sys.stdout.flush()
47
+ elif isinstance(event, BenchRmNotice):
48
+ console.print(f"[dim]{event.text}[/dim]")
49
+ elif isinstance(event, BenchRmWarning):
50
+ stderr_console.print(f"[yellow]Warning:[/yellow] {event.text}")
51
+ if event.hint:
52
+ stderr_console.print(f"[dim]{event.hint}[/dim]")
53
+
54
+ return on_event
55
+
56
+
57
+ def _exit_on_error(e: CwcliError):
58
+ stderr_console.print(f"[bold red]Error:[/bold red] {e.message}")
59
+ if e.hint:
60
+ stderr_console.print(f"[dim]{e.hint}[/dim]")
61
+ raise typer.Exit(code=1) from e
62
+
63
+
64
+ @handle_docker_errors
65
+ def rm_bench(
66
+ project_name: str = typer.Argument(
67
+ ..., help="The Docker Compose project name.", autocompletion=complete_project_names
68
+ ),
69
+ bench: str = typer.Option(
70
+ None, "--bench", help="Which bench to remove: its numeric index or label."
71
+ ),
72
+ bench_path: str = typer.Option(
73
+ None, "--path", "-p", help="Explicit bench directory (lower-level alternative to --bench)."
74
+ ),
75
+ db_root_password: str = typer.Option(
76
+ "123", "--db-root-password", help="MariaDB root password used by 'bench drop-site'."
77
+ ),
78
+ yes: bool = typer.Option(
79
+ False,
80
+ "--yes",
81
+ "-y",
82
+ help="Skip the destructive confirmation and auto-start the container.",
83
+ ),
84
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output."),
85
+ ):
86
+ """Permanently remove ONE bench from an instance (destructive).
87
+
88
+ Backs up and drops every site on the target bench (deleting each site's
89
+ database and files), then deletes the bench directory. Every OTHER bench and
90
+ every container keep running - this never touches the instance's containers,
91
+ named volumes, or the project directory (use 'cwcli rm' to remove the whole
92
+ instance).
93
+
94
+ A bench that is still running is refused: stop it first with
95
+ 'cwcli stop <project> --bench <selector>'. Each site's backup is copied out
96
+ to ~/.cwcli/archive/<project>_dropped_sites/ (or $CWCLI_HOME/archive/... if
97
+ that override is set) and verified on disk before the bench directory is
98
+ deleted; if a backup cannot be copied out, the directory is kept so the
99
+ backup is never destroyed, and this command exits non-zero.
100
+
101
+ Examples:
102
+ cwcli rm-bench my-project --bench 2
103
+ cwcli rm-bench my-project --bench staging --yes
104
+ """
105
+ ensure_containers_running(project_name, require_running=True, verbose=verbose, auto_start=yes)
106
+
107
+ resolved = resolve_bench_path(project_name, bench, bench_path, verbose=verbose)
108
+ if resolved:
109
+ bench_path = resolved
110
+ if verbose:
111
+ stderr_console.print(f"[dim]Using bench path: {bench_path}[/dim]")
112
+
113
+ on_event = _make_renderer(verbose=verbose)
114
+
115
+ def _remove(consent: bool):
116
+ try:
117
+ return core_rm_bench.remove_bench(
118
+ project_name,
119
+ bench_path=bench_path,
120
+ consent=consent,
121
+ db_root_password=db_root_password,
122
+ on_event=on_event,
123
+ )
124
+ except CwcliError as e:
125
+ _exit_on_error(e)
126
+
127
+ result = _remove(False)
128
+
129
+ if (
130
+ result.status is Status.NEEDS_CHOICE
131
+ and result.choice is not None
132
+ and result.choice.kind == "confirm_remove_bench"
133
+ ):
134
+ confirm_or_exit(
135
+ result.choice.prompt,
136
+ assume_yes=yes,
137
+ refuse_message=(
138
+ "Removing a bench is destructive and no confirmation was given. "
139
+ "Pass --yes to proceed."
140
+ ),
141
+ )
142
+ console.print(f"[bold red]Removing bench '{bench_path}'...[/bold red]")
143
+ result = _remove(True)
144
+
145
+ if result.status is Status.NEEDS_CHOICE and result.choice is not None:
146
+ # A resolver fork not handled above (e.g. select_bench, if the cache
147
+ # changed under us): report rather than guess.
148
+ stderr_console.print(f"[bold red]Error:[/bold red] {result.choice.prompt}")
149
+ raise typer.Exit(code=1)
150
+
151
+ outcome = result.data
152
+ assert outcome is not None # OK/WARNING always carries a BenchRemovalOutcome
153
+
154
+ # Removing a bench changes the instance's bench set, so refresh the cache.
155
+ # A failed recache is a warning, not a non-zero exit: the removal already
156
+ # happened, and failing here would make a retry act on a bench that is gone.
157
+ if not cache.recache_project(project_name, verbose=verbose):
158
+ stderr_console.print(
159
+ f"[yellow]Warning:[/yellow] bench removed, but re-caching '{project_name}' failed; "
160
+ "run 'cwcli inspect --update' to refresh."
161
+ )
162
+
163
+ for warning in result.warnings:
164
+ stderr_console.print(f"[yellow]Warning:[/yellow] {warning.text}")
165
+
166
+ if outcome.dir_removed:
167
+ dropped = len(outcome.sites_dropped)
168
+ console.print(
169
+ f"[bold green]✓[/bold green] Bench '{outcome.bench_path}' removed "
170
+ f"({dropped} site(s) backed up and dropped)."
171
+ )
172
+ if outcome.archived_host_paths:
173
+ console.print(
174
+ "[dim]Each dropped site's backup archive was copied to "
175
+ f"{outcome.archived_host_paths[0].rsplit('/', 1)[0]}/.[/dim]"
176
+ )
177
+ else:
178
+ stderr_console.print(
179
+ f"[yellow]Bench '{outcome.bench_path}' was not fully removed; "
180
+ "see the warnings above.[/yellow]"
181
+ )
182
+
183
+ if not outcome.ok:
184
+ raise typer.Exit(code=1)