Skip to content

Adding plugins and themes

Four sources, and the last two are why this exists as a feature rather than a link to wp-admin.

Search and install from the directory, in the app or:

Terminal window
rex wp myshop.rex plugin install woocommerce --activate
rex wp myshop.rex theme install twentytwentyfive --activate
rex wp myshop.rex plugin update woocommerce

Upload a zip in the app. Useful for premium plugins and for a build you produced yourself.

If the folder is already there, WP-CLI refuses and nothing is unpacked — its own summary line is Error: No plugins installed., which is true, unactionable, and the last thing you see after picking the zip you meant. So the card names the directory that is in the way, read out of WP-CLI’s own warning rather than guessed from the filename, and offers Replace with the uploaded zip. A red ✕ says something went wrong; this says something is in the way.

--force is never the default at any layer, including rex wp … plugin install.

Clone a plugin or theme straight into wp-content, and keep working on it there:

Terminal window
rex repo myshop.rex add https://github.com/you/your-plugin --install
rex repo myshop.rex status your-plugin
rex repo myshop.rex branches your-plugin
rex repo myshop.rex checkout your-plugin feature/thing --install
rex repo myshop.rex pull your-plugin --install

What you get beyond a plain clone:

  • Branch and tag switching from the panel, with --install re-running composer or npm only when the operation actually changed a lockfile.
  • PR refs — repo prs lists the remote’s pull-request head refs so you can check one out to test it. Checking one out lands you detached, because those refs carry a number and a sha and nothing else.
  • A dependency check that runs nothing. repo check reports whether composer or npm dependencies are missing or stale and offers the steps; --install is what actually runs them, stopping at the first failure.
  • Dev watchers. repo watch start <dir> <script> runs a package.json script — a Vite watcher, a Sass build — inside the app. They stop when the app quits.
  • Private repositories over your own ssh keys. No token to paste, nothing stored.

If the plugin already lives somewhere you like — a monorepo, a shared folder — symlink it in:

Terminal window
rex repo myshop.rex link ~/Projects/my-plugin

The plugin list badges what has an update, and premium plugins are in that list. They used to be missing, and not because of parsing: every premium updater measured on a real 47-plugin site registers its update filter behind a capability check —

if ( ! current_user_can( 'manage_options' ) && ! $doing_cron ) { return; }

— and a WP-CLI run has no user at all, so WordPress built its update data with every paid plugin missing. wp-admin listed BetterDocs Pro 3.9.0 → 4.1.0 while rexenv’s list showed nothing.

The update check now loads one extra file beside the WP-CLI phar. It defines WP_ADMIN and grants three named capabilities — manage_options, update_plugins, update_themes — to that one process. Nobody is impersonated: no user is logged in, no session, no cookie, and a later WP-CLI run has none of it. Both halves were measured rather than assumed: WP_ADMIN alone moved nothing, the capabilities moved four of five, WP_ADMIN on top moved the fifth. Result on that site — 2 of 10 paid plugins reported an update before, 5 after. The rest are ones wp-admin says nothing about either.

The update itself carries the same context, because the premium package URL comes out of the same filter: without it, wp plugin update <paid-slug> answers “No plugin updates available” and the new badge would be a button that cannot work.

Where it deliberately does not ride: the fast plugin list, install, activate, deactivate, delete, the site’s Terminal tab, and the raw WP-CLI runner an agent drives over MCP. And if the context run fails for any reason other than the clock, the plain list runs instead — so vendor code dying on a site rexenv has never seen costs the premium rows, never every badge.

Every plugin row and theme card carries a terminal button. It opens the site’s Terminal tab with a separate session whose working directory is that asset’s folder — not a cd typed into the site shell, which may be mid-composer install. rexenv resolves the folder through the same code that recorded it, so a Bedrock app/ or a Radicle content/ layout lands in the right place; an asset with no folder of its own (a must-use or drop-in row) shows no button rather than falling back to the docroot.

The chevron beside that button opens the same folder in your own terminal app instead — Terminal, iTerm, Warp, Ghostty, WezTerm, kitty or Alacritty, whichever are installed. The site’s quick tile has the same chevron, and the Terminal tab’s toolbar has an Open in terminal list. That shell is your plain login shell: no bundled PHP and no wp on PATH, so php -v there answers with whatever your Mac has. The built-in tab is still the one that answers with the site’s PHP. It is only ever handed a folder, because “a terminal handed a file would run it”, and an app whose way of taking a starting folder has not been established is left off the list rather than opening a window in your home directory.

The terminal spawns a login shell. rexenv is launched by launchd and inherits a bare /usr/bin:/bin:/usr/sbin:/sbin, so a non-login shell read only ~/.zshrc — and code, rex, Homebrew’s git and every version-manager shim answered command not found inside rexenv while working fine in Terminal.app.

Switching tabs no longer kills the session: a composer install used to die on an alt-tab and lose its output on the way back. Four live shells are kept — each is a full login shell — and an evicted one reopens on the next visit.

Rows are named, and the search box searches what you can read

Section titled “Rows are named, and the search box searches what you can read”

A plugin or theme row is labelled with the thing’s own name — “Twenty Twenty-Five”, not twentytwentyfive — with the slug kept beside it rather than instead of it, because the slug is the folder name and the argument every command takes. A theme whose header carries no name falls back to the slug rather than rendering blank.

The installed-plugin filter matches both. It used to match only the slug, so typing “loopback” against a row plainly reading rexenv loopback DNS (slug rexenv-dns) answered “No plugins match” — a filter searching a string you cannot see is indistinguishable from a broken one.

Every one of these actions toasts its outcome — naming the action and the asset — rather than only updating a row.

That is not decoration. On a 26-plugin list the row you just flipped is routinely off screen, and a streamed job whose only completion signal is a glyph inside its own card looks identical to a job that never started once you have scrolled away or switched to the browser.

What it claims is bounded by what is known: an install announces its settled result, says nothing when it merely starts, and reports partial as its own outcome rather than rounding it to success or failure.

The install card clears itself only when the install succeeded — three seconds later, once the toast has said so and the list below shows it. Anything else stays, with an × to dismiss, because a failed card holds the only copy of the reason. Opening its log holds that timer, and a job that is still running offers Cancel rather than Dismiss.