caffeinated-whale-cli 2.2.0__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/PKG-INFO +34 -15
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/README.md +33 -14
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/pyproject.toml +1 -1
- caffeinated_whale_cli-2.3.1/src/caffeinated_whale_cli/__init__.py +1 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/apps.py +31 -11
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/axi.py +35 -10
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/init.py +13 -4
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/start.py +12 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/apps.py +97 -8
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/docker.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/errors.py +7 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/init.py +67 -7
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/list.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/restart.py +5 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/rm.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/scale.py +12 -9
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/start.py +48 -4
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/status.py +5 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/stop.py +6 -2
- caffeinated_whale_cli-2.2.0/src/caffeinated_whale_cli/__init__.py +0 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/LICENSE +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/setup.cfg +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/backup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/config.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/console.html +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/label.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/list.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/logs.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/open.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/restart.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/restore.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/rm.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/run.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/scale.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/serve.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/status.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/stop.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/commands/where.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/backup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/config.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/doctor.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/envelope.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/fleet.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/label.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/logs.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/open.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/restore.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/run.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/supervision.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/unlock.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/url.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/version.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/core/where.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/main.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/update_notice.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/cache.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/config_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/console.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/startup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/tips.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/toon.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.1}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: caffeinated-whale-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
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
|
|
@@ -320,15 +320,19 @@ Then open http://development.localhost:8000 (or `cwcli open my-project`).
|
|
|
320
320
|
If the dev services fail to start, init still exits successfully (the bench was already created) and prints a warning telling you to run `cwcli start` yourself.
|
|
321
321
|
The start itself waits for the web server to answer on the bench's own configured port before declaring success; if that times out, init reports the same "not running" output even though the containers and supervisor did launch, since the web isn't actually serving yet.
|
|
322
322
|
If the port cannot be read, start skips the wait and warns instead of probing another bench's port.
|
|
323
|
+
If the web server is running inside the container but its port is not published to the host, init prints a warning naming the missing host binding and advises recreating the instance.
|
|
323
324
|
|
|
324
325
|
**Port Conflict Handling:**
|
|
325
326
|
|
|
326
|
-
|
|
327
|
+
Init retries occupied ports five times, one second apart, to allow bindings from a just-removed instance to finish releasing.
|
|
328
|
+
Each retry is announced in both interactive and non-interactive runs.
|
|
329
|
+
If the ports remain occupied, init exits nonzero before creating the instance and names every conflicting port:
|
|
327
330
|
|
|
328
331
|
```
|
|
329
332
|
Error: The following ports are already in use: 8000-8005
|
|
330
333
|
|
|
331
|
-
Tip:
|
|
334
|
+
Tip: If an instance using these ports was just removed, they may still be releasing - wait a few seconds and retry.
|
|
335
|
+
Tip: Otherwise, use the --port flag to select a different starting port.
|
|
332
336
|
Example: cwcli init my-project --port 10000
|
|
333
337
|
```
|
|
334
338
|
|
|
@@ -498,6 +502,7 @@ cwcli start [OPTIONS] [PROJECT_NAME]...
|
|
|
498
502
|
- **Idempotent:** a re-run on an already-running bench is a clean no-op ("already running: N/N processes up"), never a second supervisor stack
|
|
499
503
|
- **Per-process supervision:** each Procfile process runs under supervisord, so one can be restarted or auto-healed without disturbing the others
|
|
500
504
|
- **Waits for the web server:** on a genuine launch, `start` blocks until the bench's **own** web port actually answers before reporting the bench as running, so a scripted `cwcli start && cwcli status` never catches a transient `degraded`. The port is read from that bench's `sites/common_site_config.json`, never assumed - waiting on a hardcoded `:8000` meant `--bench 1` watched bench 0's port and then warned that a healthy bench had not started. If the port cannot be read, the wait is **skipped** (`web_ready` stays unset) rather than spent on a guess. A timeout (60s) does not fail the start (the stack IS launched) - it prints a warning naming the real port and telling you to check `cwcli status` / `cwcli logs`
|
|
505
|
+
- **Host port verification:** on both a genuine launch and an idempotent no-op, `start` warns when the bench's web server has no live host port binding. The warning names the container port and advises recreating the instance with `cwcli init`
|
|
501
506
|
- **Port Conflict Detection:** Automatically checks if required ports are available
|
|
502
507
|
- **Interactive Resolution:** Offers to stop conflicting Frappe projects (use `--yes` to auto-confirm)
|
|
503
508
|
- **Process Identification:** Shows which processes are using ports (cross-platform)
|
|
@@ -1114,14 +1119,24 @@ cwcli apps checkout [OPTIONS] PROJECT_NAME APP REF
|
|
|
1114
1119
|
|
|
1115
1120
|
**`apps list`** - lists apps available in the bench (live `ls apps/`); with `--installed`/`--site` it also lists the apps installed per site (all sites by default, grouped by site).
|
|
1116
1121
|
|
|
1117
|
-
**`apps install`** -
|
|
1122
|
+
**`apps install`** - ensures each app is present on the bench, then installs it on the target site(s).
|
|
1123
|
+
An app absent from `apps/` is fetched with `bench get-app` (honoring `--branch`); an app already present is not fetched again, so pre-warmed benches install it without failing on the existing directory.
|
|
1124
|
+
Each `APP` is a known app name **or** a git URL (passed straight to `bench get-app` when a fetch is needed, so custom apps not in bench's registry work).
|
|
1125
|
+
`--fetch-only` ensures the app is present without installing it on any site.
|
|
1126
|
+
`--if-not-present` makes install idempotent: an app already installed on a target site is skipped (reported, not reinstalled, with its install hooks never rerun) and the command still exits 0, so "ensure this app is installed" is a single call whose exit code a script can trust; an app the site does not have is still installed normally, and a genuine install failure still exits non-zero.
|
|
1127
|
+
A bench that is already running may still serve code it loaded before the install and fail to see the new app: install therefore **resynchronises** that bench - it restarts every cwcli-supervised program that runs the bench's Python (`web`, `schedule`, and every worker) and requires every changed site to answer Frappe before it reports success.
|
|
1128
|
+
Each restart is announced as it happens and the step is reported as a `restart-processes` row, and the command exits non-zero rather than claiming success for a site it could not confirm.
|
|
1129
|
+
A running manager that cwcli does not own is verified without being restarted; an unhealthy site fails with a manual-restart remedy.
|
|
1130
|
+
`socketio`, `watch`, and the redis programs are deliberately left alone: they import no Frappe app, and cycling redis would drop the cache and the job queue for nothing.
|
|
1131
|
+
Restarting a worker does interrupt a job in flight, which is the honest cost of not leaving background jobs running code that no longer exists.
|
|
1132
|
+
A bench that was not running is left alone.
|
|
1118
1133
|
|
|
1119
1134
|
**`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.
|
|
1120
1135
|
|
|
1121
1136
|
**`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.
|
|
1122
1137
|
|
|
1123
1138
|
**`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.
|
|
1124
|
-
Unlike `apps install` (
|
|
1139
|
+
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).
|
|
1125
1140
|
A **dirty working tree is refused** before anything is fetched, so uncommitted work in the in-instance checkout is never carried across a branch switch.
|
|
1126
1141
|
Once the checkout step moves the tree, the command runs the same **resynchronise** step `apps install` uses, even if a later `--reset` step fails.
|
|
1127
1142
|
Swapping the code under a running bench and leaving it serving the branch you just moved off is the quietest form of that defect, because nothing errors at all.
|
|
@@ -2224,16 +2239,19 @@ cwcli axi apps update frappe-one erpnext
|
|
|
2224
2239
|
cwcli axi apps update frappe-one frappe # runs 'bench update --reset'
|
|
2225
2240
|
|
|
2226
2241
|
# Put ONE branch, tag, or commit under test in an app already in the bench -
|
|
2227
|
-
# the gap 'apps install' (
|
|
2242
|
+
# the gap 'apps install' (bench acquisition and site installation) and 'apps update' (the tracked
|
|
2228
2243
|
# upstream) leave. Add --reset to force a clean tree at the fetched ref.
|
|
2229
2244
|
cwcli axi apps checkout frappe-one myapp feature/new-thing
|
|
2230
2245
|
cwcli axi apps checkout frappe-one myapp feature/new-thing --reset
|
|
2231
2246
|
|
|
2232
|
-
#
|
|
2247
|
+
# Ensure and install ONE app on ONE named site. --site is required (there is no
|
|
2233
2248
|
# fan-out here), and an app already installed on that site is refused rather
|
|
2234
|
-
# than
|
|
2249
|
+
# than reinstalled over its existing data unless --if-not-present requests a skip.
|
|
2235
2250
|
cwcli axi apps install frappe-one hrms --site erp.localhost
|
|
2236
2251
|
cwcli axi apps install frappe-one https://github.com/me/myapp --site erp.localhost --branch develop
|
|
2252
|
+
# Idempotent: skip (do not reinstall) an app already on the site and exit 0, so a
|
|
2253
|
+
# CI step can trust the exit code. A real install failure still exits non-zero.
|
|
2254
|
+
cwcli axi apps install frappe-one hrms --site erp.localhost --if-not-present
|
|
2237
2255
|
|
|
2238
2256
|
# Provision a new instance, bench, and site; the report prints as ONE TOON
|
|
2239
2257
|
# document. BLOCKS for the full 10-20 minute run (like axi apps update) and
|
|
@@ -2278,9 +2296,9 @@ Use `cwcli open`'s banner for the host address and `cwcli status` for the HTTP o
|
|
|
2278
2296
|
|
|
2279
2297
|
`cwcli axi apps list` is the read that answers what is *on* the bench `cwcli axi benches` names: the bench's available apps, and with `--installed`/`--site` which apps are installed on which site (only the app name, never the version column `bench list-apps` prints). A site whose read FAILED is reported as `null` and exits `1`, never as an empty list - "has no apps" and "could not tell" are different facts, and only one of them is safe to act on. Like every other bench-scoped verb it takes `--bench`, has no `--yes`, and reports a stopped project as a usage error naming `cwcli start` (exit 2). **`cwcli axi apps uninstall` deliberately does not exist:** letting an agent drop the tables of a real site is a product decision that deserves its own evidence, not something settled as a side effect of moving code onto the logic core. Use the human `cwcli apps uninstall` (it has `--json` and honest exit codes) until that decision is taken. `cwcli axi apps install` was held under that same shared rationale and now exists, scoped to the half of it that rationale never covered - see below.
|
|
2280
2298
|
|
|
2281
|
-
`cwcli axi apps checkout <project> <app> <ref>` puts ONE named branch, tag, or commit under test in an app that is already in the bench - the gap `apps install` (
|
|
2299
|
+
`cwcli axi apps checkout <project> <app> <ref>` puts ONE named branch, tag, or commit under test in an app that is already in the bench - the gap `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app) leave. It emits the same per-step TOON report as the other `apps` verbs, one row per git step, and its exit code reads that report's `ok`, so a refused checkout exits `1` rather than looking like a success. **It existed before `apps install` did and while `apps uninstall` still does not, and that was deliberate rather than inconsistent:** their shared deferral names one threat, an agent destroying site data (uninstalling an app drops its tables), while a checkout runs `git fetch` then `git checkout -B` inside the app's source directory - no bench command, no site, no SQL. Its guards each protect against something named: there is no `--yes` (an agent must not start containers you deliberately stopped, so a stopped project is a usage error pointing at `cwcli start`); the app must already be a git checkout, so a typo'd name errors instead of silently doing nothing or implying an install; and a **dirty working tree is refused outright**, before anything is fetched, so your uncommitted edits are never carried across a branch switch. That refusal is deliberately **stronger than git's own**: git blocks only a checkout that would overwrite a modified file, which used to let a non-conflicting edit ride silently onto another ref. It matters here because these checkouts live in a shared dev instance, where the work carried across may not even be yours. Be precise about what counts as dirty: **anything `git status --porcelain` reports - staged changes, unstaged modifications to tracked files, and untracked files** - refuses. Untracked files count because a new module written but not yet added is uncommitted work, and it is exactly the case where the tool must not decide for you that the file is worthless; `.gitignore`d build residue is not reported by git at all, so it never blocks a checkout. `--reset` is the explicit opt-in through the refusal: it is the one destructive flag, is never implied, and is reported as its own row in the output; it discards tracked local edits in the container's copy of the app, which is what the post-merge step of an app delivery workflow wants. It does **not** delete untracked files - cwcli never runs `git clean` - so it lets the checkout proceed and leaves them in place. Private-repo fetches use the same credential bridge as `apps install`/`apps update`, so no token is stored in the container. One current limit: the report does not tell you which commit you landed on - that is deliberately left to a future read verb rather than bolted onto the shared mutation report.
|
|
2282
2300
|
|
|
2283
|
-
`cwcli axi apps install <project> <app-or-git-url> --site <site>`
|
|
2301
|
+
`cwcli axi apps install <project> <app-or-git-url> --site <site>` ensures the app is present on the bench, fetching it only when absent, and installs it on one named site.
|
|
2284
2302
|
Installing an app is the first step of essentially any Frappe app work, and until this verb existed it was the one routine operation with no agent-surface form, so an agent had to drop to the raw human command for it.
|
|
2285
2303
|
It is deliberately narrower than the human `cwcli apps install`, in exactly two ways, because the original deferral of `apps install`/`apps uninstall` named a real threat - an agent destroying site data - that covers `uninstall` unconditionally but covers `install` only in one case.
|
|
2286
2304
|
Installing an app a site does **not** have creates that app's own tables and touches no other app's data; installing over an app the site **already** has re-runs that app's install hooks against rows that already exist.
|
|
@@ -2290,14 +2308,15 @@ So the verb ships scoped to the first case and refuses the second, rather than b
|
|
|
2290
2308
|
The human verb installs on every site on the bench when you omit it; an unqualified fan-out is how an agent reaches a site nobody named, so on this surface the target is always explicit.
|
|
2291
2309
|
That follows `cwcli axi run-tests`, which requires its site for the same reason: `install-app` runs the app's `after_install`, which is arbitrary Python from the repository being installed, against a live database, and when the effect is unbounded, defaulting the target is the wrong default.
|
|
2292
2310
|
|
|
2293
|
-
**
|
|
2311
|
+
**By default, an app already installed on that site is refused**, before anything is fetched, as `app.already_installed` with exit `1`.
|
|
2294
2312
|
The refusal names what to do instead: `cwcli axi apps checkout` to move the app to another ref, `cwcli axi apps update` to pull and migrate it, or the human `cwcli apps install` for a genuine reinstall.
|
|
2295
2313
|
A site whose installed-app list cannot be *read* is refused too (`app.install_state_unknown`), because an unreadable state must never be treated as "nothing is installed there".
|
|
2296
|
-
There is deliberately **no flag to bypass this**: a `--force`
|
|
2297
|
-
The escape hatch is the human verb, which is where a human confirms
|
|
2314
|
+
There is deliberately **no flag to bypass this by reinstalling**: a `--force` that re-ran the install hooks over existing data has no beneficiary in the workflow the verb serves (install, check out a ref, migrate, test), and its mere existence invites its use.
|
|
2315
|
+
The escape hatch for a genuine reinstall is the human verb, which is where a human confirms one.
|
|
2298
2316
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2317
|
+
By default the already-installed case is **not** reported as an idempotent exit-`0` no-op, even though the agent surface generally treats an already-satisfied desired state as a success: the desired state here is "installed from this branch", and cwcli cannot confirm the copy already on the site matches the `--branch` you asked for, so a silent exit `0` would be asserting something it has not verified.
|
|
2318
|
+
`--if-not-present` opts into the idempotent reading for a caller that just wants the app present: an app already installed on the site is then **skipped** (reported as a `skip-install` row, with its install hooks never rerun) and the verb exits `0`, so a CI step can trust the exit code instead of swallowing every failure with `|| true`; an app the site lacks is still installed, and a genuine install failure still exits non-zero.
|
|
2319
|
+
It is not a `--force`: there is still no way to reinstall over an app the site already has.
|
|
2301
2320
|
Like every other bench-scoped verb it takes `--bench`, has no `--yes`, and reports a stopped project as a usage error naming `cwcli start`.
|
|
2302
2321
|
Private-repo fetches use the same credential bridge as `apps update`/`apps checkout`, so no token is stored in the container.
|
|
2303
2322
|
On an already-serving bench, the operation resynchronises that bench - it restarts every supervised program running the bench's Python (`web`, `schedule`, workers) and reports a `restart-processes` row.
|
|
@@ -280,15 +280,19 @@ Then open http://development.localhost:8000 (or `cwcli open my-project`).
|
|
|
280
280
|
If the dev services fail to start, init still exits successfully (the bench was already created) and prints a warning telling you to run `cwcli start` yourself.
|
|
281
281
|
The start itself waits for the web server to answer on the bench's own configured port before declaring success; if that times out, init reports the same "not running" output even though the containers and supervisor did launch, since the web isn't actually serving yet.
|
|
282
282
|
If the port cannot be read, start skips the wait and warns instead of probing another bench's port.
|
|
283
|
+
If the web server is running inside the container but its port is not published to the host, init prints a warning naming the missing host binding and advises recreating the instance.
|
|
283
284
|
|
|
284
285
|
**Port Conflict Handling:**
|
|
285
286
|
|
|
286
|
-
|
|
287
|
+
Init retries occupied ports five times, one second apart, to allow bindings from a just-removed instance to finish releasing.
|
|
288
|
+
Each retry is announced in both interactive and non-interactive runs.
|
|
289
|
+
If the ports remain occupied, init exits nonzero before creating the instance and names every conflicting port:
|
|
287
290
|
|
|
288
291
|
```
|
|
289
292
|
Error: The following ports are already in use: 8000-8005
|
|
290
293
|
|
|
291
|
-
Tip:
|
|
294
|
+
Tip: If an instance using these ports was just removed, they may still be releasing - wait a few seconds and retry.
|
|
295
|
+
Tip: Otherwise, use the --port flag to select a different starting port.
|
|
292
296
|
Example: cwcli init my-project --port 10000
|
|
293
297
|
```
|
|
294
298
|
|
|
@@ -458,6 +462,7 @@ cwcli start [OPTIONS] [PROJECT_NAME]...
|
|
|
458
462
|
- **Idempotent:** a re-run on an already-running bench is a clean no-op ("already running: N/N processes up"), never a second supervisor stack
|
|
459
463
|
- **Per-process supervision:** each Procfile process runs under supervisord, so one can be restarted or auto-healed without disturbing the others
|
|
460
464
|
- **Waits for the web server:** on a genuine launch, `start` blocks until the bench's **own** web port actually answers before reporting the bench as running, so a scripted `cwcli start && cwcli status` never catches a transient `degraded`. The port is read from that bench's `sites/common_site_config.json`, never assumed - waiting on a hardcoded `:8000` meant `--bench 1` watched bench 0's port and then warned that a healthy bench had not started. If the port cannot be read, the wait is **skipped** (`web_ready` stays unset) rather than spent on a guess. A timeout (60s) does not fail the start (the stack IS launched) - it prints a warning naming the real port and telling you to check `cwcli status` / `cwcli logs`
|
|
465
|
+
- **Host port verification:** on both a genuine launch and an idempotent no-op, `start` warns when the bench's web server has no live host port binding. The warning names the container port and advises recreating the instance with `cwcli init`
|
|
461
466
|
- **Port Conflict Detection:** Automatically checks if required ports are available
|
|
462
467
|
- **Interactive Resolution:** Offers to stop conflicting Frappe projects (use `--yes` to auto-confirm)
|
|
463
468
|
- **Process Identification:** Shows which processes are using ports (cross-platform)
|
|
@@ -1074,14 +1079,24 @@ cwcli apps checkout [OPTIONS] PROJECT_NAME APP REF
|
|
|
1074
1079
|
|
|
1075
1080
|
**`apps list`** - lists apps available in the bench (live `ls apps/`); with `--installed`/`--site` it also lists the apps installed per site (all sites by default, grouped by site).
|
|
1076
1081
|
|
|
1077
|
-
**`apps install`** -
|
|
1082
|
+
**`apps install`** - ensures each app is present on the bench, then installs it on the target site(s).
|
|
1083
|
+
An app absent from `apps/` is fetched with `bench get-app` (honoring `--branch`); an app already present is not fetched again, so pre-warmed benches install it without failing on the existing directory.
|
|
1084
|
+
Each `APP` is a known app name **or** a git URL (passed straight to `bench get-app` when a fetch is needed, so custom apps not in bench's registry work).
|
|
1085
|
+
`--fetch-only` ensures the app is present without installing it on any site.
|
|
1086
|
+
`--if-not-present` makes install idempotent: an app already installed on a target site is skipped (reported, not reinstalled, with its install hooks never rerun) and the command still exits 0, so "ensure this app is installed" is a single call whose exit code a script can trust; an app the site does not have is still installed normally, and a genuine install failure still exits non-zero.
|
|
1087
|
+
A bench that is already running may still serve code it loaded before the install and fail to see the new app: install therefore **resynchronises** that bench - it restarts every cwcli-supervised program that runs the bench's Python (`web`, `schedule`, and every worker) and requires every changed site to answer Frappe before it reports success.
|
|
1088
|
+
Each restart is announced as it happens and the step is reported as a `restart-processes` row, and the command exits non-zero rather than claiming success for a site it could not confirm.
|
|
1089
|
+
A running manager that cwcli does not own is verified without being restarted; an unhealthy site fails with a manual-restart remedy.
|
|
1090
|
+
`socketio`, `watch`, and the redis programs are deliberately left alone: they import no Frappe app, and cycling redis would drop the cache and the job queue for nothing.
|
|
1091
|
+
Restarting a worker does interrupt a job in flight, which is the honest cost of not leaving background jobs running code that no longer exists.
|
|
1092
|
+
A bench that was not running is left alone.
|
|
1078
1093
|
|
|
1079
1094
|
**`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.
|
|
1080
1095
|
|
|
1081
1096
|
**`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.
|
|
1082
1097
|
|
|
1083
1098
|
**`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.
|
|
1084
|
-
Unlike `apps install` (
|
|
1099
|
+
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).
|
|
1085
1100
|
A **dirty working tree is refused** before anything is fetched, so uncommitted work in the in-instance checkout is never carried across a branch switch.
|
|
1086
1101
|
Once the checkout step moves the tree, the command runs the same **resynchronise** step `apps install` uses, even if a later `--reset` step fails.
|
|
1087
1102
|
Swapping the code under a running bench and leaving it serving the branch you just moved off is the quietest form of that defect, because nothing errors at all.
|
|
@@ -2184,16 +2199,19 @@ cwcli axi apps update frappe-one erpnext
|
|
|
2184
2199
|
cwcli axi apps update frappe-one frappe # runs 'bench update --reset'
|
|
2185
2200
|
|
|
2186
2201
|
# Put ONE branch, tag, or commit under test in an app already in the bench -
|
|
2187
|
-
# the gap 'apps install' (
|
|
2202
|
+
# the gap 'apps install' (bench acquisition and site installation) and 'apps update' (the tracked
|
|
2188
2203
|
# upstream) leave. Add --reset to force a clean tree at the fetched ref.
|
|
2189
2204
|
cwcli axi apps checkout frappe-one myapp feature/new-thing
|
|
2190
2205
|
cwcli axi apps checkout frappe-one myapp feature/new-thing --reset
|
|
2191
2206
|
|
|
2192
|
-
#
|
|
2207
|
+
# Ensure and install ONE app on ONE named site. --site is required (there is no
|
|
2193
2208
|
# fan-out here), and an app already installed on that site is refused rather
|
|
2194
|
-
# than
|
|
2209
|
+
# than reinstalled over its existing data unless --if-not-present requests a skip.
|
|
2195
2210
|
cwcli axi apps install frappe-one hrms --site erp.localhost
|
|
2196
2211
|
cwcli axi apps install frappe-one https://github.com/me/myapp --site erp.localhost --branch develop
|
|
2212
|
+
# Idempotent: skip (do not reinstall) an app already on the site and exit 0, so a
|
|
2213
|
+
# CI step can trust the exit code. A real install failure still exits non-zero.
|
|
2214
|
+
cwcli axi apps install frappe-one hrms --site erp.localhost --if-not-present
|
|
2197
2215
|
|
|
2198
2216
|
# Provision a new instance, bench, and site; the report prints as ONE TOON
|
|
2199
2217
|
# document. BLOCKS for the full 10-20 minute run (like axi apps update) and
|
|
@@ -2238,9 +2256,9 @@ Use `cwcli open`'s banner for the host address and `cwcli status` for the HTTP o
|
|
|
2238
2256
|
|
|
2239
2257
|
`cwcli axi apps list` is the read that answers what is *on* the bench `cwcli axi benches` names: the bench's available apps, and with `--installed`/`--site` which apps are installed on which site (only the app name, never the version column `bench list-apps` prints). A site whose read FAILED is reported as `null` and exits `1`, never as an empty list - "has no apps" and "could not tell" are different facts, and only one of them is safe to act on. Like every other bench-scoped verb it takes `--bench`, has no `--yes`, and reports a stopped project as a usage error naming `cwcli start` (exit 2). **`cwcli axi apps uninstall` deliberately does not exist:** letting an agent drop the tables of a real site is a product decision that deserves its own evidence, not something settled as a side effect of moving code onto the logic core. Use the human `cwcli apps uninstall` (it has `--json` and honest exit codes) until that decision is taken. `cwcli axi apps install` was held under that same shared rationale and now exists, scoped to the half of it that rationale never covered - see below.
|
|
2240
2258
|
|
|
2241
|
-
`cwcli axi apps checkout <project> <app> <ref>` puts ONE named branch, tag, or commit under test in an app that is already in the bench - the gap `apps install` (
|
|
2259
|
+
`cwcli axi apps checkout <project> <app> <ref>` puts ONE named branch, tag, or commit under test in an app that is already in the bench - the gap `apps install` (bench acquisition when needed, then site installation) and `apps update` (the tracked upstream on every app) leave. It emits the same per-step TOON report as the other `apps` verbs, one row per git step, and its exit code reads that report's `ok`, so a refused checkout exits `1` rather than looking like a success. **It existed before `apps install` did and while `apps uninstall` still does not, and that was deliberate rather than inconsistent:** their shared deferral names one threat, an agent destroying site data (uninstalling an app drops its tables), while a checkout runs `git fetch` then `git checkout -B` inside the app's source directory - no bench command, no site, no SQL. Its guards each protect against something named: there is no `--yes` (an agent must not start containers you deliberately stopped, so a stopped project is a usage error pointing at `cwcli start`); the app must already be a git checkout, so a typo'd name errors instead of silently doing nothing or implying an install; and a **dirty working tree is refused outright**, before anything is fetched, so your uncommitted edits are never carried across a branch switch. That refusal is deliberately **stronger than git's own**: git blocks only a checkout that would overwrite a modified file, which used to let a non-conflicting edit ride silently onto another ref. It matters here because these checkouts live in a shared dev instance, where the work carried across may not even be yours. Be precise about what counts as dirty: **anything `git status --porcelain` reports - staged changes, unstaged modifications to tracked files, and untracked files** - refuses. Untracked files count because a new module written but not yet added is uncommitted work, and it is exactly the case where the tool must not decide for you that the file is worthless; `.gitignore`d build residue is not reported by git at all, so it never blocks a checkout. `--reset` is the explicit opt-in through the refusal: it is the one destructive flag, is never implied, and is reported as its own row in the output; it discards tracked local edits in the container's copy of the app, which is what the post-merge step of an app delivery workflow wants. It does **not** delete untracked files - cwcli never runs `git clean` - so it lets the checkout proceed and leaves them in place. Private-repo fetches use the same credential bridge as `apps install`/`apps update`, so no token is stored in the container. One current limit: the report does not tell you which commit you landed on - that is deliberately left to a future read verb rather than bolted onto the shared mutation report.
|
|
2242
2260
|
|
|
2243
|
-
`cwcli axi apps install <project> <app-or-git-url> --site <site>`
|
|
2261
|
+
`cwcli axi apps install <project> <app-or-git-url> --site <site>` ensures the app is present on the bench, fetching it only when absent, and installs it on one named site.
|
|
2244
2262
|
Installing an app is the first step of essentially any Frappe app work, and until this verb existed it was the one routine operation with no agent-surface form, so an agent had to drop to the raw human command for it.
|
|
2245
2263
|
It is deliberately narrower than the human `cwcli apps install`, in exactly two ways, because the original deferral of `apps install`/`apps uninstall` named a real threat - an agent destroying site data - that covers `uninstall` unconditionally but covers `install` only in one case.
|
|
2246
2264
|
Installing an app a site does **not** have creates that app's own tables and touches no other app's data; installing over an app the site **already** has re-runs that app's install hooks against rows that already exist.
|
|
@@ -2250,14 +2268,15 @@ So the verb ships scoped to the first case and refuses the second, rather than b
|
|
|
2250
2268
|
The human verb installs on every site on the bench when you omit it; an unqualified fan-out is how an agent reaches a site nobody named, so on this surface the target is always explicit.
|
|
2251
2269
|
That follows `cwcli axi run-tests`, which requires its site for the same reason: `install-app` runs the app's `after_install`, which is arbitrary Python from the repository being installed, against a live database, and when the effect is unbounded, defaulting the target is the wrong default.
|
|
2252
2270
|
|
|
2253
|
-
**
|
|
2271
|
+
**By default, an app already installed on that site is refused**, before anything is fetched, as `app.already_installed` with exit `1`.
|
|
2254
2272
|
The refusal names what to do instead: `cwcli axi apps checkout` to move the app to another ref, `cwcli axi apps update` to pull and migrate it, or the human `cwcli apps install` for a genuine reinstall.
|
|
2255
2273
|
A site whose installed-app list cannot be *read* is refused too (`app.install_state_unknown`), because an unreadable state must never be treated as "nothing is installed there".
|
|
2256
|
-
There is deliberately **no flag to bypass this**: a `--force`
|
|
2257
|
-
The escape hatch is the human verb, which is where a human confirms
|
|
2274
|
+
There is deliberately **no flag to bypass this by reinstalling**: a `--force` that re-ran the install hooks over existing data has no beneficiary in the workflow the verb serves (install, check out a ref, migrate, test), and its mere existence invites its use.
|
|
2275
|
+
The escape hatch for a genuine reinstall is the human verb, which is where a human confirms one.
|
|
2258
2276
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2277
|
+
By default the already-installed case is **not** reported as an idempotent exit-`0` no-op, even though the agent surface generally treats an already-satisfied desired state as a success: the desired state here is "installed from this branch", and cwcli cannot confirm the copy already on the site matches the `--branch` you asked for, so a silent exit `0` would be asserting something it has not verified.
|
|
2278
|
+
`--if-not-present` opts into the idempotent reading for a caller that just wants the app present: an app already installed on the site is then **skipped** (reported as a `skip-install` row, with its install hooks never rerun) and the verb exits `0`, so a CI step can trust the exit code instead of swallowing every failure with `|| true`; an app the site lacks is still installed, and a genuine install failure still exits non-zero.
|
|
2279
|
+
It is not a `--force`: there is still no way to reinstall over an app the site already has.
|
|
2261
2280
|
Like every other bench-scoped verb it takes `--bench`, has no `--yes`, and reports a stopped project as a usage error naming `cwcli start`.
|
|
2262
2281
|
Private-repo fetches use the same credential bridge as `apps update`/`apps checkout`, so no token is stored in the container.
|
|
2263
2282
|
On an already-serving bench, the operation resynchronises that bench - it restarts every supervised program running the bench's Python (`web`, `schedule`, workers) and reports a `restart-processes` row.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.1"
|
|
@@ -53,6 +53,9 @@ _ANNOUNCE = {
|
|
|
53
53
|
"install-app": lambda app_name, site: (
|
|
54
54
|
f"[bold cyan]Installing[/bold cyan] {app_name} on [magenta]{site}[/magenta]..."
|
|
55
55
|
),
|
|
56
|
+
"skip-install": lambda app_name, site: (
|
|
57
|
+
f"[dim]Skipping[/dim] {app_name} on [magenta]{site}[/magenta] (already installed)..."
|
|
58
|
+
),
|
|
56
59
|
"uninstall-app": lambda app_name, site: (
|
|
57
60
|
f"[bold cyan]Uninstalling[/bold cyan] {app_name} from [magenta]{site}[/magenta]..."
|
|
58
61
|
),
|
|
@@ -267,7 +270,9 @@ def install_apps(
|
|
|
267
270
|
project_name: str = typer.Argument(
|
|
268
271
|
..., help="The Docker Compose project name.", autocompletion=complete_project_names
|
|
269
272
|
),
|
|
270
|
-
apps: list[str] = typer.Argument(
|
|
273
|
+
apps: list[str] = typer.Argument(
|
|
274
|
+
..., help="App name(s) or git URL(s) to ensure on the bench and install."
|
|
275
|
+
),
|
|
271
276
|
bench: str = typer.Option(
|
|
272
277
|
None, "--bench", help="Which bench to target: its numeric index or label."
|
|
273
278
|
),
|
|
@@ -283,16 +288,28 @@ def install_apps(
|
|
|
283
288
|
"--fetch-only",
|
|
284
289
|
help="Fetch the app(s) into the bench without installing on any site.",
|
|
285
290
|
),
|
|
291
|
+
if_not_present: bool = typer.Option(
|
|
292
|
+
False,
|
|
293
|
+
"--if-not-present",
|
|
294
|
+
help=(
|
|
295
|
+
"Idempotent: skip (do not re-install) an app already installed on a "
|
|
296
|
+
"target site instead of re-running its install hooks. A skipped app is "
|
|
297
|
+
"reported and the command still exits 0."
|
|
298
|
+
),
|
|
299
|
+
),
|
|
286
300
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
287
301
|
yes: bool = typer.Option(
|
|
288
302
|
False, "--yes", "-y", help="Auto-start stopped containers without prompting."
|
|
289
303
|
),
|
|
290
304
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output."),
|
|
291
305
|
):
|
|
292
|
-
"""
|
|
306
|
+
"""Ensure app(s) are present on the bench, then install them on the target site(s).
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
Apps absent from apps/ are fetched with bench get-app; apps already present skip
|
|
309
|
+
that fetch. Each app is a known app name OR a git URL.
|
|
295
310
|
Multi-site by default: with no --site the app is installed on every site.
|
|
311
|
+
With --if-not-present an app already installed on a target site is skipped rather
|
|
312
|
+
than re-installed, so "ensure this app is installed" is a single idempotent call.
|
|
296
313
|
"""
|
|
297
314
|
ensure_containers_running(project_name, require_running=True, verbose=verbose, auto_start=yes)
|
|
298
315
|
resolved = _resolve_bench(project_name, bench, bench_path, verbose)
|
|
@@ -305,6 +322,7 @@ def install_apps(
|
|
|
305
322
|
sites=sites,
|
|
306
323
|
branch=branch,
|
|
307
324
|
fetch_only=fetch_only,
|
|
325
|
+
if_not_present=if_not_present,
|
|
308
326
|
on_event=_make_renderer(json_output=json_output, verbose=verbose),
|
|
309
327
|
)
|
|
310
328
|
except CwcliError as e:
|
|
@@ -322,13 +340,15 @@ def install_apps(
|
|
|
322
340
|
_refresh_cache(project_name, verbose)
|
|
323
341
|
|
|
324
342
|
# The banner must match what actually happened: only claim "installed" when an
|
|
325
|
-
# install-app step ran (not for --fetch-only
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
success_msg="App(s)
|
|
331
|
-
|
|
343
|
+
# install-app step ran (not for --fetch-only, a bench with no sites, or an
|
|
344
|
+
# --if-not-present run that only skipped already-installed apps).
|
|
345
|
+
if any(r.action == "install-app" for r in report.results):
|
|
346
|
+
success_msg = "App(s) installed."
|
|
347
|
+
elif any(r.action == "skip-install" for r in report.results):
|
|
348
|
+
success_msg = "App(s) already installed; nothing to do."
|
|
349
|
+
else:
|
|
350
|
+
success_msg = "App(s) fetched."
|
|
351
|
+
_report_and_exit(report, json_output, success_msg=success_msg)
|
|
332
352
|
|
|
333
353
|
|
|
334
354
|
# ------------------------------------------------------------------------ uninstall
|
|
@@ -450,7 +470,7 @@ def checkout_app(
|
|
|
450
470
|
):
|
|
451
471
|
"""Fetch and check out a branch/ref into an app already installed in the instance.
|
|
452
472
|
|
|
453
|
-
Unlike 'apps install' (
|
|
473
|
+
Unlike 'apps install' (bench acquisition and site installation) and 'apps update' (the tracked
|
|
454
474
|
upstream on every app), this puts a specific feature branch, tag, or commit
|
|
455
475
|
under test in the EXISTING apps/<app> checkout, authenticated for private repos
|
|
456
476
|
through the same credential bridge as install/update. Use --reset to force a
|
|
@@ -1563,7 +1563,7 @@ def axi_apps_checkout(
|
|
|
1563
1563
|
) -> None:
|
|
1564
1564
|
"""Fetch and check out a ref into an app already in the bench; emit the report as TOON.
|
|
1565
1565
|
|
|
1566
|
-
The gap `apps install` (
|
|
1566
|
+
The gap `apps install` (bench acquisition and site installation) and `apps update` (the tracked
|
|
1567
1567
|
upstream on every app) leave: putting ONE named branch, tag, or commit under
|
|
1568
1568
|
test in the EXISTING apps/<app> checkout. There is no `axi run`, so this is
|
|
1569
1569
|
the only agent-surface route to that step.
|
|
@@ -1665,15 +1665,27 @@ def axi_apps_install(
|
|
|
1665
1665
|
# Named `app_name` because `app` is this module's Typer instance; the metavar
|
|
1666
1666
|
# keeps the agent-visible usage line matching the human `cwcli apps install`.
|
|
1667
1667
|
app_name: str = typer.Argument(
|
|
1668
|
-
..., metavar="APP", help="App name or git URL to
|
|
1668
|
+
..., metavar="APP", help="App name or git URL to ensure on the bench and install."
|
|
1669
1669
|
),
|
|
1670
1670
|
site: str = typer.Option(
|
|
1671
1671
|
..., "--site", help="The single site to install on. Required: there is no fan-out here."
|
|
1672
1672
|
),
|
|
1673
1673
|
bench: str = typer.Option(None, "--bench", help="Which bench: numeric index or label."),
|
|
1674
1674
|
branch: str = typer.Option(None, "--branch", help="Git branch to fetch (passed to get-app)."),
|
|
1675
|
+
if_not_present: bool = typer.Option(
|
|
1676
|
+
False,
|
|
1677
|
+
"--if-not-present",
|
|
1678
|
+
help=(
|
|
1679
|
+
"Idempotent: if the app is already installed on the site, skip it and "
|
|
1680
|
+
"exit 0 instead of refusing. Does NOT re-install or re-run install "
|
|
1681
|
+
"hooks - it only reports the app as already present."
|
|
1682
|
+
),
|
|
1683
|
+
),
|
|
1675
1684
|
) -> None:
|
|
1676
|
-
"""
|
|
1685
|
+
"""Ensure and install ONE app on ONE named site; emit the report as TOON.
|
|
1686
|
+
|
|
1687
|
+
An app absent from apps/ is fetched with bench get-app; an app already present
|
|
1688
|
+
skips that fetch and proceeds to the site installation.
|
|
1677
1689
|
|
|
1678
1690
|
Installing an app is the first step of essentially any Frappe app work, and
|
|
1679
1691
|
without this verb it was the one routine operation with no agent-surface form,
|
|
@@ -1711,11 +1723,21 @@ def axi_apps_install(
|
|
|
1711
1723
|
`axi migrate`'s absent `--skip-maintenance`. The escape hatch is the human verb,
|
|
1712
1724
|
which is where a human confirms a reinstall.
|
|
1713
1725
|
|
|
1714
|
-
The already-installed case is deliberately NOT reported as an
|
|
1715
|
-
no-op, against the general AXI rule that an already-satisfied
|
|
1716
|
-
success. The desired state here is "installed FROM this
|
|
1717
|
-
cannot confirm the copy already on the site matches the
|
|
1718
|
-
exit 0 would assert something it has not
|
|
1726
|
+
The already-installed case is, BY DEFAULT, deliberately NOT reported as an
|
|
1727
|
+
idempotent exit-0 no-op, against the general AXI rule that an already-satisfied
|
|
1728
|
+
desired state is a success. The desired state here is "installed FROM this
|
|
1729
|
+
branch", and cwcli cannot confirm the copy already on the site matches the
|
|
1730
|
+
requested `--branch`, so a silent exit 0 would assert something it has not
|
|
1731
|
+
verified.
|
|
1732
|
+
|
|
1733
|
+
`--if-not-present` opts INTO the idempotent reading for a caller that just wants
|
|
1734
|
+
the app present: an app already installed on the site is then SKIPPED (reported
|
|
1735
|
+
as an `ok` `skip-install` row, its install hooks NOT re-run) and the verb exits
|
|
1736
|
+
0. It is not a bypass of the safety the refusal guards - it never re-installs
|
|
1737
|
+
over existing data - it is the honest "ensure installed" answer, so a CI step can
|
|
1738
|
+
trust the exit code (a genuine install failure still exits 1) instead of
|
|
1739
|
+
swallowing every failure with `|| true`. It is NOT a `--force`: there is still
|
|
1740
|
+
no way to make the verb re-run install hooks over an app the site already has.
|
|
1719
1741
|
|
|
1720
1742
|
NO --yes and no auto-start: a stopped project is a usage error naming
|
|
1721
1743
|
`cwcli start`, as every bench-scoped axi verb already does. The private-repo
|
|
@@ -1731,7 +1753,10 @@ def axi_apps_install(
|
|
|
1731
1753
|
sites=[site],
|
|
1732
1754
|
branch=branch,
|
|
1733
1755
|
auto_start=False,
|
|
1734
|
-
|
|
1756
|
+
# --if-not-present is the opt-in idempotent path; without it the
|
|
1757
|
+
# already-installed refusal (require_absent) stays the default.
|
|
1758
|
+
require_absent=not if_not_present,
|
|
1759
|
+
if_not_present=if_not_present,
|
|
1735
1760
|
on_event=_checkout_narrate,
|
|
1736
1761
|
)
|
|
1737
1762
|
except CwcliError as error:
|
|
@@ -2311,7 +2336,7 @@ def axi_init(
|
|
|
2311
2336
|
)
|
|
2312
2337
|
else:
|
|
2313
2338
|
for warning in start_result.warnings:
|
|
2314
|
-
if warning.code
|
|
2339
|
+
if warning.code in ("start.web_not_ready", "start.no_host_port"):
|
|
2315
2340
|
print(f"Warning: {warning.text}", file=sys.stderr, flush=True)
|
|
2316
2341
|
|
|
2317
2342
|
emit_result(bench_result.data, warnings=bench_result.warnings)
|
|
@@ -173,6 +173,10 @@ class _InitRenderer:
|
|
|
173
173
|
stderr_console.print(f"[yellow]Warning: {event.text}[/yellow]")
|
|
174
174
|
elif code == "instance.already_running" and self.verbose:
|
|
175
175
|
stderr_console.print(f"[dim]{event.text}[/dim]")
|
|
176
|
+
elif code == "ports.retry":
|
|
177
|
+
# Unmissable in both rendering modes: a caller waiting on this run
|
|
178
|
+
# needs to know it's retrying, not stuck.
|
|
179
|
+
stderr_console.print(f"[yellow]{event.text}[/yellow]")
|
|
176
180
|
|
|
177
181
|
def _on_trace(self, event) -> None:
|
|
178
182
|
if not self.verbose:
|
|
@@ -191,7 +195,11 @@ def _render_error_exit(e: CwcliError, project_name: str, *, verbose: bool = Fals
|
|
|
191
195
|
stderr_console.print(f"[bold red]Error:[/bold red] {e.message}")
|
|
192
196
|
if e.code == "ports.in_use":
|
|
193
197
|
stderr_console.print(
|
|
194
|
-
"\n[yellow]Tip:[/yellow]
|
|
198
|
+
"\n[yellow]Tip:[/yellow] If an instance using these ports was just removed, "
|
|
199
|
+
"they may still be releasing - wait a few seconds and retry."
|
|
200
|
+
)
|
|
201
|
+
stderr_console.print(
|
|
202
|
+
"[yellow]Tip:[/yellow] Otherwise, use the [cyan]--port[/cyan] flag to select "
|
|
195
203
|
"a different starting port."
|
|
196
204
|
)
|
|
197
205
|
stderr_console.print(f"[dim]Example: cwcli init {project_name} --port 10000[/dim]")
|
|
@@ -376,10 +384,11 @@ def _start_services(project: str, bench_path: str) -> tuple[bool, bool | None]:
|
|
|
376
384
|
)
|
|
377
385
|
return False, None
|
|
378
386
|
# core.start now blocks until the web server binds this bench's assigned port, so "running" is
|
|
379
|
-
# honest by the time we return. If it timed out,
|
|
380
|
-
# user isn't told the web is up
|
|
387
|
+
# honest by the time we return. If it timed out, or the port never got published
|
|
388
|
+
# to the host at all, surface the warning so the user isn't told the web is up
|
|
389
|
+
# when it isn't reachable.
|
|
381
390
|
for warning in result.warnings:
|
|
382
|
-
if warning.code
|
|
391
|
+
if warning.code in ("start.web_not_ready", "start.no_host_port"):
|
|
383
392
|
stderr_console.print(f"[yellow]Warning:[/yellow] {warning.text}")
|
|
384
393
|
running = result.status is not Status.NEEDS_CHOICE
|
|
385
394
|
web_ready = result.data.web_ready if running and result.data is not None else None
|
|
@@ -322,7 +322,12 @@ def _start_project(
|
|
|
322
322
|
# be visible here too, since restart's whole-stack path and the auto-start
|
|
323
323
|
# path (ensure_containers_running) both funnel through this helper.
|
|
324
324
|
for warning in result.warnings:
|
|
325
|
-
if warning.code in (
|
|
325
|
+
if warning.code in (
|
|
326
|
+
"start.uid_align_failed",
|
|
327
|
+
"bench.default_used",
|
|
328
|
+
"start.web_not_ready",
|
|
329
|
+
"start.no_host_port",
|
|
330
|
+
):
|
|
326
331
|
stderr_console.print(f"[yellow]Warning: {warning.text}[/yellow]")
|
|
327
332
|
elif verbose:
|
|
328
333
|
stderr_console.print(f"[dim]{warning.text}[/dim]")
|
|
@@ -522,7 +527,12 @@ def _run_start(
|
|
|
522
527
|
# init.uid_align_failed precedent for the uid remap failure): both signal the
|
|
523
528
|
# bench workspace may not behave as expected, not just verbose diagnostics.
|
|
524
529
|
for warning in result.warnings:
|
|
525
|
-
if warning.code in (
|
|
530
|
+
if warning.code in (
|
|
531
|
+
"start.uid_align_failed",
|
|
532
|
+
"bench.default_used",
|
|
533
|
+
"start.web_not_ready",
|
|
534
|
+
"start.no_host_port",
|
|
535
|
+
):
|
|
526
536
|
stderr_console.print(f"[yellow]Warning: {warning.text}[/yellow]")
|
|
527
537
|
elif verbose:
|
|
528
538
|
stderr_console.print(f"[dim]{warning.text}[/dim]")
|