caffeinated-whale-cli 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/PKG-INFO +27 -13
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/README.md +26 -12
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/pyproject.toml +1 -1
- caffeinated_whale_cli-2.3.0/src/caffeinated_whale_cli/__init__.py +1 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/apps.py +31 -11
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/axi.py +34 -9
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/apps.py +97 -8
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/docker.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/errors.py +7 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/list.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/restart.py +5 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/rm.py +2 -1
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/start.py +5 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/status.py +5 -2
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/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.0}/LICENSE +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/setup.cfg +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/backup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/config.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/console.html +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/init.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/label.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/list.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/logs.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/open.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/restart.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/restore.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/rm.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/run.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/scale.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/serve.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/start.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/status.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/stop.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/where.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/backup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/config.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/doctor.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/envelope.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/fleet.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/init.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/label.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/logs.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/open.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/restore.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/run.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/scale.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/supervision.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/unlock.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/update.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/url.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/version.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/where.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/main.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/update_notice.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/cache.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/config_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/console.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/startup.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/tips.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/toon.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
- {caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/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.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
|
|
@@ -1114,14 +1114,24 @@ cwcli apps checkout [OPTIONS] PROJECT_NAME APP REF
|
|
|
1114
1114
|
|
|
1115
1115
|
**`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
1116
|
|
|
1117
|
-
**`apps install`** -
|
|
1117
|
+
**`apps install`** - ensures each app is present on the bench, then installs it on the target site(s).
|
|
1118
|
+
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.
|
|
1119
|
+
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).
|
|
1120
|
+
`--fetch-only` ensures the app is present without installing it on any site.
|
|
1121
|
+
`--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.
|
|
1122
|
+
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.
|
|
1123
|
+
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.
|
|
1124
|
+
A running manager that cwcli does not own is verified without being restarted; an unhealthy site fails with a manual-restart remedy.
|
|
1125
|
+
`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.
|
|
1126
|
+
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.
|
|
1127
|
+
A bench that was not running is left alone.
|
|
1118
1128
|
|
|
1119
1129
|
**`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
1130
|
|
|
1121
1131
|
**`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
1132
|
|
|
1123
1133
|
**`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` (
|
|
1134
|
+
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
1135
|
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
1136
|
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
1137
|
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 +2234,19 @@ cwcli axi apps update frappe-one erpnext
|
|
|
2224
2234
|
cwcli axi apps update frappe-one frappe # runs 'bench update --reset'
|
|
2225
2235
|
|
|
2226
2236
|
# Put ONE branch, tag, or commit under test in an app already in the bench -
|
|
2227
|
-
# the gap 'apps install' (
|
|
2237
|
+
# the gap 'apps install' (bench acquisition and site installation) and 'apps update' (the tracked
|
|
2228
2238
|
# upstream) leave. Add --reset to force a clean tree at the fetched ref.
|
|
2229
2239
|
cwcli axi apps checkout frappe-one myapp feature/new-thing
|
|
2230
2240
|
cwcli axi apps checkout frappe-one myapp feature/new-thing --reset
|
|
2231
2241
|
|
|
2232
|
-
#
|
|
2242
|
+
# Ensure and install ONE app on ONE named site. --site is required (there is no
|
|
2233
2243
|
# fan-out here), and an app already installed on that site is refused rather
|
|
2234
|
-
# than
|
|
2244
|
+
# than reinstalled over its existing data unless --if-not-present requests a skip.
|
|
2235
2245
|
cwcli axi apps install frappe-one hrms --site erp.localhost
|
|
2236
2246
|
cwcli axi apps install frappe-one https://github.com/me/myapp --site erp.localhost --branch develop
|
|
2247
|
+
# Idempotent: skip (do not reinstall) an app already on the site and exit 0, so a
|
|
2248
|
+
# CI step can trust the exit code. A real install failure still exits non-zero.
|
|
2249
|
+
cwcli axi apps install frappe-one hrms --site erp.localhost --if-not-present
|
|
2237
2250
|
|
|
2238
2251
|
# Provision a new instance, bench, and site; the report prints as ONE TOON
|
|
2239
2252
|
# document. BLOCKS for the full 10-20 minute run (like axi apps update) and
|
|
@@ -2278,9 +2291,9 @@ Use `cwcli open`'s banner for the host address and `cwcli status` for the HTTP o
|
|
|
2278
2291
|
|
|
2279
2292
|
`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
2293
|
|
|
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` (
|
|
2294
|
+
`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
2295
|
|
|
2283
|
-
`cwcli axi apps install <project> <app-or-git-url> --site <site>`
|
|
2296
|
+
`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
2297
|
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
2298
|
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
2299
|
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 +2303,15 @@ So the verb ships scoped to the first case and refuses the second, rather than b
|
|
|
2290
2303
|
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
2304
|
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
2305
|
|
|
2293
|
-
**
|
|
2306
|
+
**By default, an app already installed on that site is refused**, before anything is fetched, as `app.already_installed` with exit `1`.
|
|
2294
2307
|
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
2308
|
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
|
|
2309
|
+
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.
|
|
2310
|
+
The escape hatch for a genuine reinstall is the human verb, which is where a human confirms one.
|
|
2298
2311
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2312
|
+
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.
|
|
2313
|
+
`--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.
|
|
2314
|
+
It is not a `--force`: there is still no way to reinstall over an app the site already has.
|
|
2301
2315
|
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
2316
|
Private-repo fetches use the same credential bridge as `apps update`/`apps checkout`, so no token is stored in the container.
|
|
2303
2317
|
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.
|
|
@@ -1074,14 +1074,24 @@ cwcli apps checkout [OPTIONS] PROJECT_NAME APP REF
|
|
|
1074
1074
|
|
|
1075
1075
|
**`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
1076
|
|
|
1077
|
-
**`apps install`** -
|
|
1077
|
+
**`apps install`** - ensures each app is present on the bench, then installs it on the target site(s).
|
|
1078
|
+
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.
|
|
1079
|
+
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).
|
|
1080
|
+
`--fetch-only` ensures the app is present without installing it on any site.
|
|
1081
|
+
`--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.
|
|
1082
|
+
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.
|
|
1083
|
+
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.
|
|
1084
|
+
A running manager that cwcli does not own is verified without being restarted; an unhealthy site fails with a manual-restart remedy.
|
|
1085
|
+
`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.
|
|
1086
|
+
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.
|
|
1087
|
+
A bench that was not running is left alone.
|
|
1078
1088
|
|
|
1079
1089
|
**`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
1090
|
|
|
1081
1091
|
**`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
1092
|
|
|
1083
1093
|
**`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` (
|
|
1094
|
+
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
1095
|
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
1096
|
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
1097
|
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 +2194,19 @@ cwcli axi apps update frappe-one erpnext
|
|
|
2184
2194
|
cwcli axi apps update frappe-one frappe # runs 'bench update --reset'
|
|
2185
2195
|
|
|
2186
2196
|
# Put ONE branch, tag, or commit under test in an app already in the bench -
|
|
2187
|
-
# the gap 'apps install' (
|
|
2197
|
+
# the gap 'apps install' (bench acquisition and site installation) and 'apps update' (the tracked
|
|
2188
2198
|
# upstream) leave. Add --reset to force a clean tree at the fetched ref.
|
|
2189
2199
|
cwcli axi apps checkout frappe-one myapp feature/new-thing
|
|
2190
2200
|
cwcli axi apps checkout frappe-one myapp feature/new-thing --reset
|
|
2191
2201
|
|
|
2192
|
-
#
|
|
2202
|
+
# Ensure and install ONE app on ONE named site. --site is required (there is no
|
|
2193
2203
|
# fan-out here), and an app already installed on that site is refused rather
|
|
2194
|
-
# than
|
|
2204
|
+
# than reinstalled over its existing data unless --if-not-present requests a skip.
|
|
2195
2205
|
cwcli axi apps install frappe-one hrms --site erp.localhost
|
|
2196
2206
|
cwcli axi apps install frappe-one https://github.com/me/myapp --site erp.localhost --branch develop
|
|
2207
|
+
# Idempotent: skip (do not reinstall) an app already on the site and exit 0, so a
|
|
2208
|
+
# CI step can trust the exit code. A real install failure still exits non-zero.
|
|
2209
|
+
cwcli axi apps install frappe-one hrms --site erp.localhost --if-not-present
|
|
2197
2210
|
|
|
2198
2211
|
# Provision a new instance, bench, and site; the report prints as ONE TOON
|
|
2199
2212
|
# document. BLOCKS for the full 10-20 minute run (like axi apps update) and
|
|
@@ -2238,9 +2251,9 @@ Use `cwcli open`'s banner for the host address and `cwcli status` for the HTTP o
|
|
|
2238
2251
|
|
|
2239
2252
|
`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
2253
|
|
|
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` (
|
|
2254
|
+
`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
2255
|
|
|
2243
|
-
`cwcli axi apps install <project> <app-or-git-url> --site <site>`
|
|
2256
|
+
`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
2257
|
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
2258
|
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
2259
|
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 +2263,15 @@ So the verb ships scoped to the first case and refuses the second, rather than b
|
|
|
2250
2263
|
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
2264
|
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
2265
|
|
|
2253
|
-
**
|
|
2266
|
+
**By default, an app already installed on that site is refused**, before anything is fetched, as `app.already_installed` with exit `1`.
|
|
2254
2267
|
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
2268
|
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
|
|
2269
|
+
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.
|
|
2270
|
+
The escape hatch for a genuine reinstall is the human verb, which is where a human confirms one.
|
|
2258
2271
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2272
|
+
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.
|
|
2273
|
+
`--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.
|
|
2274
|
+
It is not a `--force`: there is still no way to reinstall over an app the site already has.
|
|
2261
2275
|
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
2276
|
Private-repo fetches use the same credential bridge as `apps update`/`apps checkout`, so no token is stored in the container.
|
|
2263
2277
|
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.0"
|
|
@@ -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:
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/apps.py
RENAMED
|
@@ -74,7 +74,7 @@ class AppResult:
|
|
|
74
74
|
|
|
75
75
|
app: str
|
|
76
76
|
site: str | None # None for the bench-wide get-app step
|
|
77
|
-
action: str # "get-app" | "install-app" | "uninstall-app" | "restart-processes" | a git step
|
|
77
|
+
action: str # "get-app" | "install-app" | "skip-install" | "uninstall-app" | "restart-processes" | a git step
|
|
78
78
|
ok: bool
|
|
79
79
|
|
|
80
80
|
|
|
@@ -535,6 +535,41 @@ def _refuse_if_installed(
|
|
|
535
535
|
)
|
|
536
536
|
|
|
537
537
|
|
|
538
|
+
def _read_installed_by_site(
|
|
539
|
+
frappe_container,
|
|
540
|
+
path: str,
|
|
541
|
+
sites: list[str],
|
|
542
|
+
*,
|
|
543
|
+
emit: OnEvent,
|
|
544
|
+
) -> dict[str, set[str]]:
|
|
545
|
+
"""Each target site's installed-apps set. FAILS CLOSED on an unreadable site.
|
|
546
|
+
|
|
547
|
+
Backs the idempotent ``if_not_present`` install path. A site whose ``list-apps``
|
|
548
|
+
read fails cannot be confirmed clean, so it refuses (PRECONDITION) rather than
|
|
549
|
+
proceeding on an unknown - the same fail-honest rule ``_refuse_if_installed``
|
|
550
|
+
applies, so "could not read the site" never degrades to "the app is not there"
|
|
551
|
+
and gets silently (re)installed.
|
|
552
|
+
"""
|
|
553
|
+
installed: dict[str, set[str]] = {}
|
|
554
|
+
for site in sites:
|
|
555
|
+
command, ok, site_apps = _installed_apps(frappe_container, path, site)
|
|
556
|
+
emit(AppsCommand(command=command))
|
|
557
|
+
if not ok:
|
|
558
|
+
raise CwcliError(
|
|
559
|
+
ErrorKind.PRECONDITION,
|
|
560
|
+
"app.install_state_unknown",
|
|
561
|
+
f"Could not read the installed apps on site '{site}' (it may not exist "
|
|
562
|
+
"on this bench), so it cannot be confirmed whether the app is already "
|
|
563
|
+
"installed there.",
|
|
564
|
+
hint=(
|
|
565
|
+
f"Check the site exists and is readable: 'cwcli axi apps list "
|
|
566
|
+
f"<project> --site {site}'."
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
installed[site] = set(site_apps)
|
|
570
|
+
return installed
|
|
571
|
+
|
|
572
|
+
|
|
538
573
|
def install_apps(
|
|
539
574
|
project_name: str,
|
|
540
575
|
apps: list[str],
|
|
@@ -546,10 +581,17 @@ def install_apps(
|
|
|
546
581
|
fetch_only: bool = False,
|
|
547
582
|
auto_start: bool = False,
|
|
548
583
|
require_absent: bool = False,
|
|
584
|
+
if_not_present: bool = False,
|
|
549
585
|
on_event: OnEvent | None = None,
|
|
550
586
|
) -> Result[AppsReport]:
|
|
551
587
|
"""Fetch (``bench get-app``) and install app(s) on the target site(s).
|
|
552
588
|
|
|
589
|
+
The fetch is idempotent on bench-presence: an app already present under the
|
|
590
|
+
bench's ``apps/`` (e.g. a bench built from a pre-warmed base that carries it)
|
|
591
|
+
would make ``bench get-app`` fail on the existing directory, so it is skipped
|
|
592
|
+
and the install-app phase proceeds. This is distinct from ``require_absent``
|
|
593
|
+
below, which is a per-SITE guard, not a per-bench one.
|
|
594
|
+
|
|
553
595
|
``require_absent`` refuses, BEFORE fetching anything, if an app is already
|
|
554
596
|
installed on a target site. It defaults off so the human verb is unchanged, and
|
|
555
597
|
the rule lives here rather than in a frontend so ``axi`` and any future GUI share
|
|
@@ -560,6 +602,18 @@ def install_apps(
|
|
|
560
602
|
The check FAILS CLOSED: a site whose ``list-apps`` read fails cannot be confirmed
|
|
561
603
|
clean, so it refuses rather than proceeding on an unknown (``core.where``'s
|
|
562
604
|
fail-honest rule - an unreadable state must never degrade to "nothing is there").
|
|
605
|
+
|
|
606
|
+
``if_not_present`` is the idempotent reading of "install": an app already
|
|
607
|
+
installed on a target site is SKIPPED (reported as an ``ok`` ``skip-install``
|
|
608
|
+
result, never re-installed) instead of refused, so a caller that just wants the
|
|
609
|
+
app present succeeds (exit 0) and can still trust the exit code for a genuine
|
|
610
|
+
failure. It is the opt-in alternative to ``require_absent``'s refusal: the two
|
|
611
|
+
answer the same "already installed on the site" case oppositely, so a frontend
|
|
612
|
+
passes at most one. It does NOT re-run install hooks for a skipped app (that is
|
|
613
|
+
exactly what the refusal guarded against), and it FAILS CLOSED on an unreadable
|
|
614
|
+
site the same way - "could not read" is never treated as "not installed". An app
|
|
615
|
+
absent from a given site still installs there normally, so a multi-site run
|
|
616
|
+
skips only the sites that already have it.
|
|
563
617
|
"""
|
|
564
618
|
emit: OnEvent = on_event or _noop
|
|
565
619
|
|
|
@@ -580,14 +634,29 @@ def install_apps(
|
|
|
580
634
|
# so every git-URL fetch is wrapped and torn down; see core.credbridge.
|
|
581
635
|
with credbridge.credential_bridge(frappe_container, path):
|
|
582
636
|
for target in apps:
|
|
637
|
+
app_name = derive_app_name(target)
|
|
638
|
+
command, before_apps = _available_apps(frappe_container, path)
|
|
639
|
+
before = set(before_apps)
|
|
640
|
+
|
|
641
|
+
# Idempotent fetch: if the app is already on the BENCH (its apps/ dir
|
|
642
|
+
# exists), `bench get-app` fails on the existing directory, which used to
|
|
643
|
+
# fail the whole install of an app a pre-warmed base already carries. Skip
|
|
644
|
+
# the fetch and install what is present. This is bench-presence, DISTINCT
|
|
645
|
+
# from the per-site installed-apps guard (`_installed_apps` /
|
|
646
|
+
# `_refuse_if_installed`): that gates a double SITE-install, this gates the
|
|
647
|
+
# bench FETCH. The install-app phase below is unchanged either way.
|
|
648
|
+
if app_name in before:
|
|
649
|
+
emit(AppsCommand(command=command))
|
|
650
|
+
results.append(AppResult(app=app_name, site=None, action="get-app", ok=True))
|
|
651
|
+
fetched.append((target, app_name))
|
|
652
|
+
continue
|
|
653
|
+
|
|
583
654
|
get_cmd = f"bench get-app {branch_arg}{shlex.quote(target)}"
|
|
584
655
|
|
|
585
|
-
# Announce BEFORE the apps/ read, so --verbose stderr keeps its
|
|
586
|
-
# order: "Fetching x..." then the read's echo then get-app's own
|
|
656
|
+
# Announce BEFORE the apps/ read echo, so --verbose stderr keeps its
|
|
657
|
+
# historical order: "Fetching x..." then the read's echo then get-app's own.
|
|
587
658
|
emit(AppsAnnounce(phase="get-app", app=target))
|
|
588
|
-
command, before_apps = _available_apps(frappe_container, path)
|
|
589
659
|
emit(AppsCommand(command=command))
|
|
590
|
-
before = set(before_apps)
|
|
591
660
|
|
|
592
661
|
code = _run_step(
|
|
593
662
|
frappe_container, get_cmd, path, emit=emit, phase="get-app", app=target
|
|
@@ -601,7 +670,7 @@ def install_apps(
|
|
|
601
670
|
emit(AppsCommand(command=command))
|
|
602
671
|
|
|
603
672
|
new_dirs = set(after_apps) - before
|
|
604
|
-
app_name = new_dirs.pop() if len(new_dirs) == 1 else
|
|
673
|
+
app_name = new_dirs.pop() if len(new_dirs) == 1 else app_name
|
|
605
674
|
fetched.append((target, app_name))
|
|
606
675
|
|
|
607
676
|
if not fetch_only:
|
|
@@ -613,8 +682,25 @@ def install_apps(
|
|
|
613
682
|
"no sites on the bench to install on; app(s) fetched only.",
|
|
614
683
|
)
|
|
615
684
|
)
|
|
685
|
+
# Idempotent path: read each target site's installed set ONCE (fail-closed),
|
|
686
|
+
# so an app already installed on a site is skipped below instead of having
|
|
687
|
+
# its install hooks re-run against that site's existing data.
|
|
688
|
+
installed_by_site: dict[str, set[str]] = (
|
|
689
|
+
_read_installed_by_site(frappe_container, path, target_sites, emit=emit)
|
|
690
|
+
if if_not_present and target_sites
|
|
691
|
+
else {}
|
|
692
|
+
)
|
|
616
693
|
for _target, app_name in fetched:
|
|
617
694
|
for site in target_sites:
|
|
695
|
+
if if_not_present and app_name in installed_by_site.get(site, set()):
|
|
696
|
+
# Already present on this site: report it and move on. NOT an
|
|
697
|
+
# install-app step, so it drives no resync (nothing changed) and
|
|
698
|
+
# never re-runs the app's install hooks.
|
|
699
|
+
emit(AppsAnnounce(phase="skip-install", app=app_name, site=site))
|
|
700
|
+
results.append(
|
|
701
|
+
AppResult(app=app_name, site=site, action="skip-install", ok=True)
|
|
702
|
+
)
|
|
703
|
+
continue
|
|
618
704
|
install_cmd = (
|
|
619
705
|
f"bench --site {shlex.quote(site)} install-app {shlex.quote(app_name)}"
|
|
620
706
|
)
|
|
@@ -631,6 +717,8 @@ def install_apps(
|
|
|
631
717
|
results.append(
|
|
632
718
|
AppResult(app=app_name, site=site, action="install-app", ok=code == 0)
|
|
633
719
|
)
|
|
720
|
+
if if_not_present and code == 0:
|
|
721
|
+
installed_by_site.setdefault(site, set()).add(app_name)
|
|
634
722
|
|
|
635
723
|
# Only the sites an install actually landed on: a site whose install-app failed
|
|
636
724
|
# was not changed, so it has nothing to re-verify and must not fail the restart.
|
|
@@ -753,8 +841,9 @@ def checkout_app(
|
|
|
753
841
|
) -> Result[AppsReport]:
|
|
754
842
|
"""Fetch and check out an arbitrary ``ref`` into an app that ALREADY EXISTS.
|
|
755
843
|
|
|
756
|
-
The gap ``install``/``update`` leave: ``install``
|
|
757
|
-
|
|
844
|
+
The gap ``install``/``update`` leave: ``install`` acquires an app with ``bench
|
|
845
|
+
get-app`` when it is absent and installs it on sites, while ``update`` uses
|
|
846
|
+
``bench update --pull`` against the TRACKED
|
|
758
847
|
upstream on every app). Neither fetches one named branch/tag/commit into an
|
|
759
848
|
existing ``apps/<app>`` checkout, which is exactly what putting a feature branch
|
|
760
849
|
under test in the instance the app lives in needs.
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/docker.py
RENAMED
|
@@ -22,7 +22,7 @@ import subprocess
|
|
|
22
22
|
import docker
|
|
23
23
|
from docker.errors import DockerException
|
|
24
24
|
|
|
25
|
-
from .errors import CwcliError, ErrorKind
|
|
25
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
26
26
|
|
|
27
27
|
_COMPOSE_INSTALL_HINT = (
|
|
28
28
|
"Install the Docker Compose v2 plugin: it ships with Docker Desktop, or "
|
|
@@ -332,6 +332,7 @@ def get_frappe_container(project_name: str):
|
|
|
332
332
|
ErrorKind.DOCKER,
|
|
333
333
|
"docker.unreachable",
|
|
334
334
|
"Could not connect to Docker daemon.",
|
|
335
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
335
336
|
)
|
|
336
337
|
|
|
337
338
|
if not containers:
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/errors.py
RENAMED
|
@@ -28,6 +28,13 @@ class ErrorKind(Enum):
|
|
|
28
28
|
INTERNAL = "internal" # unexpected
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# The one next-step string for every "could not connect to the Docker daemon"
|
|
32
|
+
# CwcliError, so every frontend (axi included) renders the same actionable
|
|
33
|
+
# `help:` line instead of a bare error with no remedy. Mirrors the fix string
|
|
34
|
+
# `core/doctor.py`'s own daemon check already carries.
|
|
35
|
+
DOCKER_UNREACHABLE_HINT = "start Docker (Docker Desktop, or `systemctl start docker`)"
|
|
36
|
+
|
|
37
|
+
|
|
31
38
|
class CwcliError(Exception):
|
|
32
39
|
"""A hard failure the core cannot recover from.
|
|
33
40
|
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/list.py
RENAMED
|
@@ -15,7 +15,7 @@ import docker
|
|
|
15
15
|
from docker.errors import DockerException
|
|
16
16
|
|
|
17
17
|
from .envelope import Result, Status
|
|
18
|
-
from .errors import CwcliError, ErrorKind
|
|
18
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
@@ -64,6 +64,7 @@ def list_instances(*, service_name: str = "frappe") -> Result[list[InstanceDTO]]
|
|
|
64
64
|
ErrorKind.DOCKER,
|
|
65
65
|
"docker.unreachable",
|
|
66
66
|
"Could not connect to Docker daemon.",
|
|
67
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
67
68
|
detail={"output": str(e)},
|
|
68
69
|
) from e
|
|
69
70
|
|
|
@@ -23,7 +23,7 @@ from docker.errors import APIError, NotFound
|
|
|
23
23
|
from . import resolvers, supervision
|
|
24
24
|
from .docker import get_project_containers
|
|
25
25
|
from .envelope import Choice, Message, Result, Status
|
|
26
|
-
from .errors import CwcliError, ErrorKind
|
|
26
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
@@ -52,7 +52,10 @@ def restart_process(
|
|
|
52
52
|
containers = get_project_containers(project_name)
|
|
53
53
|
if containers is None:
|
|
54
54
|
raise CwcliError(
|
|
55
|
-
ErrorKind.DOCKER,
|
|
55
|
+
ErrorKind.DOCKER,
|
|
56
|
+
"docker.unreachable",
|
|
57
|
+
"Could not connect to Docker daemon.",
|
|
58
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
56
59
|
)
|
|
57
60
|
if not containers:
|
|
58
61
|
raise CwcliError(
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/rm.py
RENAMED
|
@@ -50,7 +50,7 @@ from ..utils import bench_sites, db_utils
|
|
|
50
50
|
from ..utils.config_utils import PROJECTS_DIR, cwcli_home
|
|
51
51
|
from .docker import get_project_containers, get_project_networks, get_project_volumes
|
|
52
52
|
from .envelope import Result, Status
|
|
53
|
-
from .errors import CwcliError, ErrorKind
|
|
53
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
54
54
|
from .resolvers import DEFAULT_BENCH_PATH
|
|
55
55
|
|
|
56
56
|
# Marker in a Frappe backup filename that identifies the database dump - the one
|
|
@@ -839,6 +839,7 @@ def remove(
|
|
|
839
839
|
ErrorKind.DOCKER,
|
|
840
840
|
"docker.unreachable",
|
|
841
841
|
f"Could not connect to Docker to inspect '{project_name}'.",
|
|
842
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
842
843
|
)
|
|
843
844
|
|
|
844
845
|
project_dir = PROJECTS_DIR / project_name
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/start.py
RENAMED
|
@@ -28,7 +28,7 @@ from dataclasses import dataclass
|
|
|
28
28
|
from . import resolvers, supervision
|
|
29
29
|
from .docker import align_container_user_to_host, get_project_containers
|
|
30
30
|
from .envelope import Message, Result, Status
|
|
31
|
-
from .errors import CwcliError, ErrorKind
|
|
31
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
32
32
|
|
|
33
33
|
# Shell metacharacters rejected in the bench path (it is interpolated into the
|
|
34
34
|
# launch shell command; command-injection guard, mirrors core.backup).
|
|
@@ -93,7 +93,10 @@ def start(
|
|
|
93
93
|
containers = get_project_containers(project_name)
|
|
94
94
|
if containers is None:
|
|
95
95
|
raise CwcliError(
|
|
96
|
-
ErrorKind.DOCKER,
|
|
96
|
+
ErrorKind.DOCKER,
|
|
97
|
+
"docker.unreachable",
|
|
98
|
+
"Could not connect to Docker daemon.",
|
|
99
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
97
100
|
)
|
|
98
101
|
if not containers:
|
|
99
102
|
raise CwcliError(
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/status.py
RENAMED
|
@@ -102,7 +102,7 @@ from docker.errors import APIError, NotFound
|
|
|
102
102
|
from . import resolvers, supervision
|
|
103
103
|
from .docker import get_project_containers
|
|
104
104
|
from .envelope import Message, Result, Status
|
|
105
|
-
from .errors import CwcliError, ErrorKind
|
|
105
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
106
106
|
from .supervision import ProcessHealth
|
|
107
107
|
|
|
108
108
|
OFFLINE = "offline"
|
|
@@ -250,7 +250,10 @@ def status(
|
|
|
250
250
|
containers = get_project_containers(project_name)
|
|
251
251
|
if containers is None:
|
|
252
252
|
raise CwcliError(
|
|
253
|
-
ErrorKind.DOCKER,
|
|
253
|
+
ErrorKind.DOCKER,
|
|
254
|
+
"docker.unreachable",
|
|
255
|
+
"Could not connect to Docker daemon.",
|
|
256
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
254
257
|
)
|
|
255
258
|
# A truly-nonexistent project (typo / never created) has NO containers with the
|
|
256
259
|
# label at all - distinct from a real-but-stopped project, which has a non-empty
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/stop.py
RENAMED
|
@@ -32,7 +32,7 @@ from docker.errors import APIError, NotFound
|
|
|
32
32
|
from . import resolvers, supervision
|
|
33
33
|
from .docker import get_project_containers
|
|
34
34
|
from .envelope import Message, Result, Status
|
|
35
|
-
from .errors import CwcliError, ErrorKind
|
|
35
|
+
from .errors import DOCKER_UNREACHABLE_HINT, CwcliError, ErrorKind
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
@@ -65,6 +65,7 @@ def stop(project_name: str) -> Result[StopOutcome]:
|
|
|
65
65
|
ErrorKind.DOCKER,
|
|
66
66
|
"docker.unreachable",
|
|
67
67
|
"Could not connect to Docker daemon.",
|
|
68
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
68
69
|
)
|
|
69
70
|
|
|
70
71
|
if not containers:
|
|
@@ -131,7 +132,10 @@ def stop_bench(
|
|
|
131
132
|
containers = get_project_containers(project_name)
|
|
132
133
|
if containers is None:
|
|
133
134
|
raise CwcliError(
|
|
134
|
-
ErrorKind.DOCKER,
|
|
135
|
+
ErrorKind.DOCKER,
|
|
136
|
+
"docker.unreachable",
|
|
137
|
+
"Could not connect to Docker daemon.",
|
|
138
|
+
hint=DOCKER_UNREACHABLE_HINT,
|
|
135
139
|
)
|
|
136
140
|
if not containers:
|
|
137
141
|
raise CwcliError(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/commands/rm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/backup.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/doctor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/fleet.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/init.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/label.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/logs.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/run.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/scale.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/unlock.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/update.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/url.py
RENAMED
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/core/where.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/main.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/tips.py
RENAMED
|
File without changes
|
{caffeinated_whale_cli-2.2.0 → caffeinated_whale_cli-2.3.0}/src/caffeinated_whale_cli/utils/toon.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|