❯ Controlled WordPress
cwpgit for WordPress environments. Not a metaphor decorating a deployment tool: the model itself. The tree is the working copy, an environment is a remote, and git commit is git commit. cwp moves state between the two and refuses to push anything upward that has no committed source. It orchestrates DDEV, WP‑CLI and your host, and reimplements none of them.
Sharpest on Bricks 2.4, where it wires the whole AI ability surface to your editor. Also correct on a site with no page builder at all.
❯ npm i -g @xumana/cwp
cwp pull dev --yes✓ page/home — unchanged content/page/home.yml✓ page/about — unchanged content/page/about.yml✓ page/services — unchanged content/page/services.yml✓ page/pricing — unchanged content/page/pricing.yml✓ page/contact — unchanged content/page/contact.yml✓ page/privacy — unchanged content/page/privacy.yml✓ page/careers — unchanged content/page/careers.yml✓ content — 7 item(s)✓ color-palettes — 26 item(s)✓ theme-styles — 2 item(s)✓ classes — 39 item(s)✓ variables — 18 item(s)✓ breakpoints — 1 item(s)✓ settings — 6 item(s)– sensitive settings — api-keys, custom-code left out — pass --sensitive to include them✓ bricks pull — 0 written, 11 unchanged in bricks (9.8 KB)✓ bricks — 11 item(s)– settings/core.yml — 23 option(s) — unchanged– settings/frontpage.yml — 4 option(s) — unchanged– settings/snn.yml — 5 option(s) — unchanged– settings/smtp_mailer.yml — 0 option(s) — unchanged not carried: smtp_mailer_options — it holds a credential under `smtp_password` the tree already holds what the site does review the diff before committing: git -C ~/Code/example/example.com diff✓ settings — 4 item(s)– widgets/sidebar.yml — 5 widget(s) — unchanged✓ widgets — 1 item(s)– roles.yml — 5 role(s), 116 capability(s) — unchanged✓ roles — 5 item(s)✓ core — WordPress 7.1 in de_DE✓ plugins — 2 recorded✓ themes — 3 recorded✓ languages — de_DE your own code under `tracked` is not recorded — it is deployed, not installed✓ write — inventory.yml (unchanged)✓ inventory — 5 item(s)✓ media — 0 captured, 5 already in the tree✓ commit — 20d43c3 — 14 path(s) your local WordPress is untouched — the database and the uploads are `cwp db pull` and `cwp media pull`cwp status✓ cwp — 1.0.0 (32598ac-dirty, built 2026-08-29 09:01)✓ project — example.com — ~/Code/example/example.com✓ docroot — public✓ php — 8.3✓ environment local — https://example.com.ddev.site — this working copy — not adopted✓ environment dev — https://example.com — app example.com — managed — not adopted– ddev — unhealthy — https://example.com.ddev.site– last pull with a snapshot (local) — unknown — no pre-pull snapshot on disk. A `cwp db pull --no-snapshot`, or a `cwp media pull`, leaves none, and `cwp db rm` removes the record with the restore point– last pull with a snapshot (dev) — unknown — no pre-pull snapshot on disk. A `cwp db pull --no-snapshot`, or a `cwp media pull`, leaves none, and `cwp db rm` removes the record with the restore point– snapshots — none in ~/Code/example/example.com/.ddev/db_snapshots artifact repo detail! bricks ? not a git repository, or git did not answer! content/ ? not a git repository, or git did not answer– inventory.yml – no inventory.yml. Run `cwp pull --only inventory`– settings/ – no `settings:` block. This project does not carry options– roles.yml – no roles.yml. Run `cwp pull --only roles`– widgets/ – no `widgets:` declaration. This project does not carry widget areas only the repo column was read. Pass --drift to compare the local site, --remote to compare an environment as wellcwp push dev --yes WordPress itself is not applied upward — a core update is the platform, which the host owns✓ remote backup — demo.xumana.com — restore with: cloudron backup list --app demo.xumana.com✓ page/about — updated #2✓ page/careers — updated #22✓ page/contact — updated #5✓ page/home — updated #1✓ page/pricing — updated #4✓ page/privacy — updated #6✓ page/services — updated #3✓ record — dev now says what this run pushed — the dashboard widget is in place✓ content — 7 item(s) converged✓ record — dev now says what this run pushed✓ bricks — 1 item(s) converged– settings — dev already matches the tree– widgets — dev already matches the tree– roles — dev already matches the tree– inventory — dev already matches the tree✓ push public/wp-content/plugins/demo-site — already up to date — 5 files on /app/data/wp-content/plugins/demo-site, 5 verified by digest✓ commit — 6557dad — 1 path(s) content: Write 7 post(s) to "dev" page/about (#2) page/careers (#22) page/contact (#5) page/home (#1) page/pricing (#4) page/privacy (#6) page/services (#3) bricks: Push 92 design-system item(s) to "dev" 85 item(s) already exist and will be SKIPPEDcwp push prod WordPress itself is not applied upward — a core update is the platform, which the host owns✗ "prod" is a protected environment and refuses upward writes → pass --force if you really mean to write to demo.xumana.comOne loop, recorded against a real site: the environment comes down into the tree, you look at what differs, and it goes back up. The last step is the same command against the same site under its protected name, `prod` instead of `dev`, and cwp refuses that one.
The shape those four steps run in: your repository is the centre, every site is a spoke, and direction is measured from the centre. The last step is not a warning and not a confirmation dialog. It is exit 5 and nothing sent, because that spoke is marked protected in cwp.yml.
Everything through the hub is in and out. The environment argument names which site, never which way.
localthe DDEV container — a database and a filesystem, like the others
receives database and uploads from dev, on an edge that never passes through the tree
deva remote install, not marked protected
- databasedev → localno guard — bulk site state moving toward this machine, and it never enters the tree
- uploadsdev → localno guard — bulk site state moving toward this machine, and it never enters the tree
prodprotectedmarked protected in cwp.yml
nothing moves here in this command
The edge that skips the hub: site to site, never through the tree. It has one arrowhead and it points away from every site that matters, which is invariant 1 drawn instead of stated.
Everything through the hub is in and out. The environment argument names which site, never which way.
localthe DDEV container — a database and a filesystem, like the others
nothing moves here in this command
deva remote install, not marked protected
nothing moves here in this command
prodprotectedmarked protected in cwp.yml
- tracked pathstree → prodrefused — prod is protected, and --dry-run does not soften it
The same arc, one spoke further out. prod is protected, so the gate is shut — and it stays shut under --dry-run, because a refusal that a dry run softens is not a refusal.
Why it existsThe guards, in fullInstall and quickstart
This site is being built. The manual, the architecture story and the invariant catalogue ship with v1.0.