Adding plugins and themes
Four sources, and the last two are why this exists as a feature rather than a link to
wp-admin.
From wp.org
Section titled “From wp.org”Search and install from the directory, in the app or:
rex wp myshop.rex plugin install woocommerce --activaterex wp myshop.rex theme install twentytwentyfive --activaterex wp myshop.rex plugin update woocommerceFrom a zip
Section titled “From a zip”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.
From a git repository
Section titled “From a git repository”Clone a plugin or theme straight into wp-content, and keep working on it there:
rex repo myshop.rex add https://github.com/you/your-plugin --installrex repo myshop.rex status your-pluginrex repo myshop.rex branches your-pluginrex repo myshop.rex checkout your-plugin feature/thing --installrex repo myshop.rex pull your-plugin --installWhat you get beyond a plain clone:
- Branch and tag switching from the panel, with
--installre-running composer or npm only when the operation actually changed a lockfile. - PR refs —
repo prslists 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 checkreports whether composer or npm dependencies are missing or stale and offers the steps;--installis 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.
From a folder on your disk
Section titled “From a folder on your disk”If the plugin already lives somewhere you like — a monorepo, a shared folder — symlink it in:
rex repo myshop.rex link ~/Projects/my-pluginUpdates, including the paid ones
Section titled “Updates, including the paid ones”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.
A terminal in the plugin’s own folder
Section titled “A terminal in the plugin’s own folder”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.
Why operations report themselves
Section titled “Why operations report themselves”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.