caffeinated-whale-cli 2.1.0__tar.gz → 2.2.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 (90) hide show
  1. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/PKG-INFO +79 -6
  2. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/README.md +78 -5
  3. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/pyproject.toml +1 -1
  4. caffeinated_whale_cli-2.2.0/src/caffeinated_whale_cli/__init__.py +1 -0
  5. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/axi.py +34 -0
  6. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/console.html +929 -280
  7. caffeinated_whale_cli-2.2.0/src/caffeinated_whale_cli/commands/doctor.py +79 -0
  8. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/init.py +1 -1
  9. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/open.py +7 -5
  10. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/restore.py +22 -14
  11. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/serve.py +131 -3
  12. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/docker.py +105 -12
  13. caffeinated_whale_cli-2.2.0/src/caffeinated_whale_cli/core/doctor.py +604 -0
  14. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/init.py +5 -3
  15. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/inspect.py +12 -0
  16. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/open.py +15 -14
  17. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/scale.py +1 -0
  18. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/start.py +3 -3
  19. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/version.py +20 -5
  20. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/main.py +22 -6
  21. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/config_utils.py +11 -0
  22. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/docker_utils.py +48 -19
  23. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +2 -0
  24. caffeinated_whale_cli-2.1.0/src/caffeinated_whale_cli/__init__.py +0 -1
  25. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/LICENSE +0 -0
  26. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/setup.cfg +0 -0
  27. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
  28. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/apps.py +0 -0
  29. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/backup.py +0 -0
  30. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/config.py +0 -0
  31. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
  32. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/label.py +0 -0
  33. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/list.py +0 -0
  34. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/logs.py +0 -0
  35. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/restart.py +0 -0
  36. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/rm.py +0 -0
  37. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
  38. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/run.py +0 -0
  39. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/scale.py +0 -0
  40. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
  41. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/start.py +0 -0
  42. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/status.py +0 -0
  43. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/stop.py +0 -0
  44. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
  45. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/update.py +0 -0
  46. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/utils.py +0 -0
  47. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/commands/where.py +0 -0
  48. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/__init__.py +0 -0
  49. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/apps.py +0 -0
  50. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
  51. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/backup.py +0 -0
  52. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
  53. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/config.py +0 -0
  54. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
  55. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/envelope.py +0 -0
  56. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/errors.py +0 -0
  57. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
  58. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/fleet.py +0 -0
  59. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/label.py +0 -0
  60. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/list.py +0 -0
  61. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/logs.py +0 -0
  62. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
  63. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/restart.py +0 -0
  64. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/restore.py +0 -0
  65. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/rm.py +0 -0
  66. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
  67. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/run.py +0 -0
  68. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/status.py +0 -0
  69. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/stop.py +0 -0
  70. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/supervision.py +0 -0
  71. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/unlock.py +0 -0
  72. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/update.py +0 -0
  73. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/url.py +0 -0
  74. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/core/where.py +0 -0
  75. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/update_notice.py +0 -0
  76. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
  77. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
  78. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
  79. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
  80. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
  81. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/cache.py +0 -0
  82. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
  83. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/console.py +0 -0
  84. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
  85. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
  86. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
  87. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/startup.py +0 -0
  88. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/tips.py +0 -0
  89. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.0}/src/caffeinated_whale_cli/utils/toon.py +0 -0
  90. {caffeinated_whale_cli-2.1.0 → caffeinated_whale_cli-2.2.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: 2.1.0
3
+ Version: 2.2.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
@@ -40,7 +40,9 @@ Dynamic: license-file
40
40
 
41
41
  # Caffeinated Whale CLI
42
42
 
43
- A command-line interface (CLI) for managing Frappe/ERPNext Docker instances during local development. Simplify container management, bench operations, and development workflows with an intuitive set of commands.
43
+ A command-line interface (CLI) for managing Frappe/ERPNext Docker instances during local development.
44
+ The repository also contains the Phase 1 Tauri desktop shell, which runs the existing Console GUI over the same Python core.
45
+ See [`desktop/README.md`](./desktop/README.md) for its current source-build status, supported platforms, and build instructions.
44
46
 
45
47
  ## Features
46
48
 
@@ -59,15 +61,21 @@ A command-line interface (CLI) for managing Frappe/ERPNext Docker instances duri
59
61
  - **Private App Repos** - `apps install`/`apps update` authenticate private GitHub/GitLab app fetches through your host's already-signed-in `gh`/`glab`, with no token ever entering the container
60
62
  - **Update Management** - App updates with automatic migrations and lock cleanup
61
63
  - **Self-Update** - Upgrade cwcli itself to the latest release with `cwcli self-update` (install-method aware)
64
+ - **Environment Preflight** - Read-only checks for Docker, disk space, and tool dependencies with `cwcli doctor` (chainable exit code, `cwcli axi doctor` for agents; the shared cache layer may idempotently initialize cwcli's private cache directory while building the command tree)
62
65
  - **Update Notices** - A passive, once/day "a newer cwcli is available" hint on stderr, shown only to a human at a TTY
63
66
  - **Auto-Inspection** - Background process to keep project cache fresh automatically
64
67
  - **System Integration** - Auto-start on system boot with platform-specific configurations
65
68
  - **Contextual Tips** - Helpful tips displayed during long-running operations to help you discover features
69
+ - **Desktop Console** - A source-build Tauri 2 shell for the existing Console GUI on Linux and Windows; installer bundles are deferred to a later phase
66
70
 
67
71
  ## Installation
68
72
 
69
73
  `cwcli` requires Python 3.10+.
70
74
 
75
+ `cwcli init` and `cwcli scale` also require the Docker CLI with the Docker Compose v2 plugin (`docker compose version`).
76
+ Docker Desktop includes the plugin; on a bare Docker Engine host, install the `docker-compose-plugin` package.
77
+ Both commands verify this prerequisite before changing project state and return an actionable precondition error when it is unavailable.
78
+
71
79
  ### With uv (recommended)
72
80
 
73
81
  [uv](https://docs.astral.sh/uv/) installs `cwcli` into an isolated environment and puts the `cwcli` command on your `PATH`, without touching your system or project Python.
@@ -1541,7 +1549,9 @@ Backup: 20251112_105638-development_localhost-database.sql.gz
1541
1549
  ```
1542
1550
 
1543
1551
  For scripted (non-interactive) receives, pass `-y`/`--yes` to skip the confirmation and the missing-apps prompt. Without a TTY and without `--yes`, cwcli refuses and exits non-zero rather than silently overwriting the site's data.
1544
- Before downloading, cwcli validates the target site and requires at least 2 GiB free in its temporary [transfer directory](#architecture).
1552
+ Before transferring, cwcli requires at least 2 GiB free in its temporary [transfer directory](#architecture).
1553
+ It checks before the sender stages a backup and before the receiver downloads one.
1554
+ The receiver also validates the target site before downloading.
1545
1555
  The backup may require more space.
1546
1556
  Set `TMPDIR`, `TEMP`, or `TMP` to place transfers on a larger filesystem, in that priority order, or relocate all cwcli data with `CWCLI_HOME`.
1547
1557
  Copying the downloaded backup into the container streams through a bounded pipe instead of creating another full host-side copy.
@@ -1777,6 +1787,60 @@ cwcli status -w --interval 5 frappe-one # refresh every 5s
1777
1787
 
1778
1788
  ---
1779
1789
 
1790
+ ### `doctor` - Preflight Your Environment
1791
+
1792
+ Checks whether cwcli can operate on this machine at all - Docker, cwcli's own
1793
+ on-disk footprint, disk space, sendme, and the `gh`/`glab` credential-bridge
1794
+ dependencies - as a fast, read-only report. `status`/`inspect` answer "is this
1795
+ instance healthy"; `doctor` answers "will the next command even work here".
1796
+
1797
+ ```bash
1798
+ cwcli doctor [OPTIONS]
1799
+ ```
1800
+
1801
+ **Options:**
1802
+
1803
+ | Option | Description |
1804
+ |--------|-------------|
1805
+ | `-v`, `--verbose` | Show each check's stable id (e.g. `[c2]`) alongside its title |
1806
+
1807
+ **Read-only checks:** doctor never starts a container, installs anything, or
1808
+ writes a config file.
1809
+ There is one accepted shared-infrastructure exception before the checks run:
1810
+ building the command tree for `cwcli doctor` or `cwcli axi doctor` imports the
1811
+ pre-existing cache layer, which may idempotently create cwcli's private cache
1812
+ directory and apply mode `0700`.
1813
+ It does not touch project data.
1814
+ Making that initialization lazy is tracked separately as
1815
+ `cwcli-cache-dir-lazy-init`; this caveat should disappear when that lands.
1816
+ When a direct remedy is available, the finding includes the command or action
1817
+ to apply yourself.
1818
+
1819
+ **Checks always run in full** (no tiers, no selection flags), grouped into
1820
+ Docker, cwcli, Storage, Transfer, and Git hosting, plus a cross-instance
1821
+ port-range collision check under Instances. Each check is `pass`, `warn`, or
1822
+ `fail`; an absent *optional* tool (sendme, `gh`, `glab`) is `warn`, never
1823
+ `fail`.
1824
+
1825
+ **Exit codes:** `0` when every check is pass/warn (a warning never blocks -
1826
+ doctor is a chainable preflight gate), `1` when any check fails.
1827
+
1828
+ **Example:**
1829
+
1830
+ ```bash
1831
+ cwcli doctor # grouped glyph checklist
1832
+ cwcli doctor -v # same, with each check's stable id shown
1833
+ ```
1834
+
1835
+ For agents, `cwcli axi doctor` emits the same checks as one TOON document with
1836
+ a machine-readable `status` token per row.
1837
+ The version row also carries `version_verified`, so `status: pass` with
1838
+ `version_verified: false` is explicitly unverified rather than a claim that the
1839
+ installed version is current.
1840
+ See [the `axi` surface](#for-agents-the-cwcli-axi-surface).
1841
+
1842
+ ---
1843
+
1780
1844
  ### `config` - Manage Configuration
1781
1845
 
1782
1846
  Manages the CLI configuration and cache.
@@ -1963,7 +2027,11 @@ cwcli self-update --no-cache
1963
2027
 
1964
2028
  **Passive update notices:**
1965
2029
 
1966
- Every `cwcli` and `cwcli axi` run also does a passive, cache-only check: if a newer release is already known (from the same ≤1-day cache `self-update`/`--check` share) and you're at an interactive terminal, cwcli prints a one-line "a newer cwcli is available" hint - with the right upgrade command for how you installed it - to stderr. It never makes a blocking network call itself: PyPI is actually re-checked at most once/day, via a detached background refresh kicked off whenever the cache is missing or stale, so this never delays a command; until that refresh lands, the hint keeps showing on every run. It never touches stdout, so it's invisible to pipes, scripts, CI, and `cwcli axi`'s TOON output. Set `CWCLI_NO_UPDATE_CHECK=1` to suppress it entirely.
2030
+ Every `cwcli` and `cwcli axi` run except the strictly read-only doctor commands also does a passive, cache-only check: if a newer release is already known (from the same ≤1-day cache `self-update`/`--check` share) and you're at an interactive terminal, cwcli prints a one-line "a newer cwcli is available" hint with the right upgrade command for how you installed it to stderr.
2031
+ `cwcli doctor` and `cwcli axi doctor` skip this check because a stale or missing version cache would otherwise launch a background refresh that writes it.
2032
+ The notice never makes a blocking network call itself: PyPI is actually re-checked at most once/day, via a detached background refresh kicked off whenever the cache is missing or stale, so this never delays a command; until that refresh lands, the hint keeps showing on each eligible run.
2033
+ It never touches stdout, so it is invisible to pipes, scripts, CI, and `cwcli axi`'s TOON output.
2034
+ Set `CWCLI_NO_UPDATE_CHECK=1` to suppress it entirely.
1967
2035
 
1968
2036
  ---
1969
2037
 
@@ -2185,9 +2253,14 @@ cwcli axi self-update --check
2185
2253
  # auto-inspect state (config, live daemon, boot hook - reported separately),
2186
2254
  # tips, and the config-file/cache-DB locations.
2187
2255
  cwcli axi config
2256
+
2257
+ # System-wide environment preflight as one TOON document: Docker, cwcli's own
2258
+ # footprint, disk space, sendme, gh/glab. Machine-readable status per check;
2259
+ # exits 0 unless a check FAILS (a warning never blocks a chained preflight).
2260
+ cwcli axi doctor
2188
2261
  ```
2189
2262
 
2190
- `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 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`).
2263
+ `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 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.
2191
2264
 
2192
2265
  `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.
2193
2266
  `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.
@@ -2362,7 +2435,7 @@ The CLI uses:
2362
2435
  - **Questionary** - Interactive prompts
2363
2436
  - **Peewee ORM** - SQLite-based caching
2364
2437
 
2365
- **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 any future GUI are all thin frontends over one implementation.
2438
+ **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.
2366
2439
  `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.
2367
2440
 
2368
2441
  **Data Directories:**
@@ -1,6 +1,8 @@
1
1
  # Caffeinated Whale CLI
2
2
 
3
- A command-line interface (CLI) for managing Frappe/ERPNext Docker instances during local development. Simplify container management, bench operations, and development workflows with an intuitive set of commands.
3
+ A command-line interface (CLI) for managing Frappe/ERPNext Docker instances during local development.
4
+ The repository also contains the Phase 1 Tauri desktop shell, which runs the existing Console GUI over the same Python core.
5
+ See [`desktop/README.md`](./desktop/README.md) for its current source-build status, supported platforms, and build instructions.
4
6
 
5
7
  ## Features
6
8
 
@@ -19,15 +21,21 @@ A command-line interface (CLI) for managing Frappe/ERPNext Docker instances duri
19
21
  - **Private App Repos** - `apps install`/`apps update` authenticate private GitHub/GitLab app fetches through your host's already-signed-in `gh`/`glab`, with no token ever entering the container
20
22
  - **Update Management** - App updates with automatic migrations and lock cleanup
21
23
  - **Self-Update** - Upgrade cwcli itself to the latest release with `cwcli self-update` (install-method aware)
24
+ - **Environment Preflight** - Read-only checks for Docker, disk space, and tool dependencies with `cwcli doctor` (chainable exit code, `cwcli axi doctor` for agents; the shared cache layer may idempotently initialize cwcli's private cache directory while building the command tree)
22
25
  - **Update Notices** - A passive, once/day "a newer cwcli is available" hint on stderr, shown only to a human at a TTY
23
26
  - **Auto-Inspection** - Background process to keep project cache fresh automatically
24
27
  - **System Integration** - Auto-start on system boot with platform-specific configurations
25
28
  - **Contextual Tips** - Helpful tips displayed during long-running operations to help you discover features
29
+ - **Desktop Console** - A source-build Tauri 2 shell for the existing Console GUI on Linux and Windows; installer bundles are deferred to a later phase
26
30
 
27
31
  ## Installation
28
32
 
29
33
  `cwcli` requires Python 3.10+.
30
34
 
35
+ `cwcli init` and `cwcli scale` also require the Docker CLI with the Docker Compose v2 plugin (`docker compose version`).
36
+ Docker Desktop includes the plugin; on a bare Docker Engine host, install the `docker-compose-plugin` package.
37
+ Both commands verify this prerequisite before changing project state and return an actionable precondition error when it is unavailable.
38
+
31
39
  ### With uv (recommended)
32
40
 
33
41
  [uv](https://docs.astral.sh/uv/) installs `cwcli` into an isolated environment and puts the `cwcli` command on your `PATH`, without touching your system or project Python.
@@ -1501,7 +1509,9 @@ Backup: 20251112_105638-development_localhost-database.sql.gz
1501
1509
  ```
1502
1510
 
1503
1511
  For scripted (non-interactive) receives, pass `-y`/`--yes` to skip the confirmation and the missing-apps prompt. Without a TTY and without `--yes`, cwcli refuses and exits non-zero rather than silently overwriting the site's data.
1504
- Before downloading, cwcli validates the target site and requires at least 2 GiB free in its temporary [transfer directory](#architecture).
1512
+ Before transferring, cwcli requires at least 2 GiB free in its temporary [transfer directory](#architecture).
1513
+ It checks before the sender stages a backup and before the receiver downloads one.
1514
+ The receiver also validates the target site before downloading.
1505
1515
  The backup may require more space.
1506
1516
  Set `TMPDIR`, `TEMP`, or `TMP` to place transfers on a larger filesystem, in that priority order, or relocate all cwcli data with `CWCLI_HOME`.
1507
1517
  Copying the downloaded backup into the container streams through a bounded pipe instead of creating another full host-side copy.
@@ -1737,6 +1747,60 @@ cwcli status -w --interval 5 frappe-one # refresh every 5s
1737
1747
 
1738
1748
  ---
1739
1749
 
1750
+ ### `doctor` - Preflight Your Environment
1751
+
1752
+ Checks whether cwcli can operate on this machine at all - Docker, cwcli's own
1753
+ on-disk footprint, disk space, sendme, and the `gh`/`glab` credential-bridge
1754
+ dependencies - as a fast, read-only report. `status`/`inspect` answer "is this
1755
+ instance healthy"; `doctor` answers "will the next command even work here".
1756
+
1757
+ ```bash
1758
+ cwcli doctor [OPTIONS]
1759
+ ```
1760
+
1761
+ **Options:**
1762
+
1763
+ | Option | Description |
1764
+ |--------|-------------|
1765
+ | `-v`, `--verbose` | Show each check's stable id (e.g. `[c2]`) alongside its title |
1766
+
1767
+ **Read-only checks:** doctor never starts a container, installs anything, or
1768
+ writes a config file.
1769
+ There is one accepted shared-infrastructure exception before the checks run:
1770
+ building the command tree for `cwcli doctor` or `cwcli axi doctor` imports the
1771
+ pre-existing cache layer, which may idempotently create cwcli's private cache
1772
+ directory and apply mode `0700`.
1773
+ It does not touch project data.
1774
+ Making that initialization lazy is tracked separately as
1775
+ `cwcli-cache-dir-lazy-init`; this caveat should disappear when that lands.
1776
+ When a direct remedy is available, the finding includes the command or action
1777
+ to apply yourself.
1778
+
1779
+ **Checks always run in full** (no tiers, no selection flags), grouped into
1780
+ Docker, cwcli, Storage, Transfer, and Git hosting, plus a cross-instance
1781
+ port-range collision check under Instances. Each check is `pass`, `warn`, or
1782
+ `fail`; an absent *optional* tool (sendme, `gh`, `glab`) is `warn`, never
1783
+ `fail`.
1784
+
1785
+ **Exit codes:** `0` when every check is pass/warn (a warning never blocks -
1786
+ doctor is a chainable preflight gate), `1` when any check fails.
1787
+
1788
+ **Example:**
1789
+
1790
+ ```bash
1791
+ cwcli doctor # grouped glyph checklist
1792
+ cwcli doctor -v # same, with each check's stable id shown
1793
+ ```
1794
+
1795
+ For agents, `cwcli axi doctor` emits the same checks as one TOON document with
1796
+ a machine-readable `status` token per row.
1797
+ The version row also carries `version_verified`, so `status: pass` with
1798
+ `version_verified: false` is explicitly unverified rather than a claim that the
1799
+ installed version is current.
1800
+ See [the `axi` surface](#for-agents-the-cwcli-axi-surface).
1801
+
1802
+ ---
1803
+
1740
1804
  ### `config` - Manage Configuration
1741
1805
 
1742
1806
  Manages the CLI configuration and cache.
@@ -1923,7 +1987,11 @@ cwcli self-update --no-cache
1923
1987
 
1924
1988
  **Passive update notices:**
1925
1989
 
1926
- Every `cwcli` and `cwcli axi` run also does a passive, cache-only check: if a newer release is already known (from the same ≤1-day cache `self-update`/`--check` share) and you're at an interactive terminal, cwcli prints a one-line "a newer cwcli is available" hint - with the right upgrade command for how you installed it - to stderr. It never makes a blocking network call itself: PyPI is actually re-checked at most once/day, via a detached background refresh kicked off whenever the cache is missing or stale, so this never delays a command; until that refresh lands, the hint keeps showing on every run. It never touches stdout, so it's invisible to pipes, scripts, CI, and `cwcli axi`'s TOON output. Set `CWCLI_NO_UPDATE_CHECK=1` to suppress it entirely.
1990
+ Every `cwcli` and `cwcli axi` run except the strictly read-only doctor commands also does a passive, cache-only check: if a newer release is already known (from the same ≤1-day cache `self-update`/`--check` share) and you're at an interactive terminal, cwcli prints a one-line "a newer cwcli is available" hint with the right upgrade command for how you installed it to stderr.
1991
+ `cwcli doctor` and `cwcli axi doctor` skip this check because a stale or missing version cache would otherwise launch a background refresh that writes it.
1992
+ The notice never makes a blocking network call itself: PyPI is actually re-checked at most once/day, via a detached background refresh kicked off whenever the cache is missing or stale, so this never delays a command; until that refresh lands, the hint keeps showing on each eligible run.
1993
+ It never touches stdout, so it is invisible to pipes, scripts, CI, and `cwcli axi`'s TOON output.
1994
+ Set `CWCLI_NO_UPDATE_CHECK=1` to suppress it entirely.
1927
1995
 
1928
1996
  ---
1929
1997
 
@@ -2145,9 +2213,14 @@ cwcli axi self-update --check
2145
2213
  # auto-inspect state (config, live daemon, boot hook - reported separately),
2146
2214
  # tips, and the config-file/cache-DB locations.
2147
2215
  cwcli axi config
2216
+
2217
+ # System-wide environment preflight as one TOON document: Docker, cwcli's own
2218
+ # footprint, disk space, sendme, gh/glab. Machine-readable status per check;
2219
+ # exits 0 unless a check FAILS (a warning never blocks a chained preflight).
2220
+ cwcli axi doctor
2148
2221
  ```
2149
2222
 
2150
- `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 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`).
2223
+ `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 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.
2151
2224
 
2152
2225
  `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.
2153
2226
  `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.
@@ -2322,7 +2395,7 @@ The CLI uses:
2322
2395
  - **Questionary** - Interactive prompts
2323
2396
  - **Peewee ORM** - SQLite-based caching
2324
2397
 
2325
- **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 any future GUI are all thin frontends over one implementation.
2398
+ **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.
2326
2399
  `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.
2327
2400
 
2328
2401
  **Data Directories:**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "caffeinated-whale-cli"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  authors = [
9
9
  { name = "Christopher McKay", email = "mckay.christopher73@outlook.com" },
10
10
  ]
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0"
@@ -40,6 +40,7 @@ from ..core import apps as core_apps
40
40
  from ..core import backup as core_backup
41
41
  from ..core import bench_ops as core_bench_ops
42
42
  from ..core import config as core_config
43
+ from ..core import doctor as core_doctor
43
44
  from ..core import init as core_init
44
45
  from ..core import inspect as core_inspect
45
46
  from ..core import label as core_label
@@ -2033,6 +2034,39 @@ def axi_config() -> None:
2033
2034
  raise typer.Exit(0)
2034
2035
 
2035
2036
 
2037
+ # ------------------------------------------------------------------------------ doctor
2038
+
2039
+
2040
+ @app.command("doctor")
2041
+ def axi_doctor() -> None:
2042
+ """Run the system-wide, read-only environment preflight; emit it as one TOON document.
2043
+
2044
+ Answers "can cwcli operate on this machine at all" - Docker, cwcli's own
2045
+ on-disk footprint, storage, sendme, and the gh/glab credential-bridge
2046
+ dependencies - as a single fast, non-interactive read, so a fleet can gate a
2047
+ run on it before driving any instance. Every check's ``status`` token
2048
+ (``pass``/``warn``/``fail``) is machine-readable; an agent gates on the
2049
+ token, never on parsing ``detail`` prose (the ``where``/``inspect``
2050
+ precedent).
2051
+
2052
+ Strictly read-only: no check starts a container, installs anything, writes a
2053
+ config file, or triggers an interactive login flow. Always runs every check
2054
+ (no ``--fast``/selection flags - the surviving check set is already
2055
+ fast-band).
2056
+
2057
+ Exit codes mirror the human ``cwcli doctor`` (the maintainer's ruled
2058
+ contract, identical on both surfaces): 0 when every check is pass/warn, 1
2059
+ when any check fails. A WARN never blocks - doctor is a chainable preflight
2060
+ gate and cosmetic findings must not stop a script.
2061
+ """
2062
+ result = core_doctor.run_all()
2063
+ report = result.data
2064
+ assert report is not None # run_all always returns a DoctorReport
2065
+
2066
+ emit_result(report)
2067
+ raise typer.Exit(0 if report.ok else 1)
2068
+
2069
+
2036
2070
  # ------------------------------------------------------------------------------- init
2037
2071
 
2038
2072