❯ Controlled WordPress

cwp

git 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.com

Recorded from cwp 1.0.0. Not written by hand.cwp push prod: exit 5

One 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.

treecontent/, bricks/, settings/, widgets/, roles.yml, inventory.yml, tracked paths

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.

This site is being built. The manual, the architecture story and the invariant catalogue ship with v1.0.