Repository navigation
feat: install-theme installs from WordPress.org, not activated (#350) - #356
Merged
Merged
Conversation
Rewrite the hard line before any code: Saddle installs a plugin or theme only from the WordPress.org directory, by slug, through WordPress's own installer, and the new install arrives inactive. Core updates are minor and security releases, plus a major one only behind WordPress's own switch for all new versions. Translations go through core's language pack upgrader. Nothing deletes a plugin or theme. Adds the two "What NOT to do" bullets and the ROADMAP entry. Refs #347 Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, read-only tool. It asks the WordPress.org directory through core's own plugins_api(), the same request Plugins > Add New makes, by search words or one exact slug, at most 10 results. Each row has the slug, name, author, version, active installs, rating, last update, tested up to, the WordPress and PHP it needs, and whether it is installed and active here, by core's install_plugin_install_status(). The directory answer is cached for an hour; the installed state never is. Tests answer the directory from responses recorded from the live API, which pins the field names. Refs #348 Co-Authored-By: Claude Opus 5.5 <[email protected]>
New admin-tier, gated tool. One slug per call. The preview asks the directory fresh and shows the version, installs, rating, last update, tested-up-to against this WordPress and the PHP it needs, with warnings for untested, two years without an update, or plugins it needs. The token binds the version. Refused before any token: a site that forbids file changes, an account without install_plugins, an unknown or closed slug, an installed plugin or a leftover folder, PHP or WordPress too old, and a package outside downloads.wordpress.org. A confirm queues the install on the existing runner; the request never calls the installer (pinned, and the update-plugin test now counts upgrader calls too). Saddle_Install_Runner asks the directory again, installs only the confirmed version through Plugin_Upgrader::install(), and the plugin arrives inactive. Saddle_Directory holds the lookups and refusals both sides need. The installed check reads core's rule without install_plugin_install_status(), which forces an update check. Refs #349 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ated Same shape as install-plugin: one slug, a fresh directory answer through themes_api(), a preview with the version, installs, rating, last update and the WordPress and PHP it needs, a token bound to the version, a background run through Theme_Upgrader::install(). The theme is never activated. The permission capability is switch_themes and the handler checks install_themes after naming any blocked_by() reason. Core's theme installer fetches a missing parent theme on its own. A child theme whose parent is not installed is refused before a token, the run checks again (the folder too, since WP_Theme can answer from the object cache after a delete), and during the run the directory answers only the confirmed slug. Saddle_Directory now only talks to the API. The installed check and the refusals moved to Saddle_Install_Runner, where the run and the abilities both reach them, as blocked_by() sits on the update runner. Refs #350 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found end to end on Playground: install-theme over MCP failed with "Call to undefined function themes_api()". The update runner's loader keys the admin theme.php on wp_get_themes(), which wp-includes always defines, so a REST request never loaded it. The test suite loads the admin files up front, which hid it. Refs #350 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #350
Stacked on #355 (base
feat/349-install-plugin). Merge #353, #354, #355, then this, with merge commits.What
A new tool,
saddle/install-theme. It installs one theme from the WordPress.org directory, by slug, through WordPress's own installer in the background, after a confirmed preview. The theme is never activated;activate-themestays its own step.Why
The second half of #347's installs, with the same hard line as
install-plugin.How
install-plugin:themes_api()answerTheme_Upgrader::install()switch_themesto call the tool,install_themeschecked in the handler after anyblocked_by()reasondownloads.wordpress.org/theme/Theme_Upgrader::check_parent_theme_filter()), which nobody previewed. Three layers stop that:WP_Themecan answer from the object cache for up to 30 minutes after a delete; a test caught this.themes_apifilter answers only the confirmed slug. A test proves it: with the guard removed, the test fails because core asks WordPress.org for the parent.Saddle_Directorynow only talks to the API (242 lines). The installed check and the refusals moved toSaddle_Install_Runner(253 lines), where both the run and the abilities reach them, asblocked_by()sits on the update runner.install_pluginandinstall_themeshare oneinstall( $type, $input )(the class is 289 lines).themes_api()was undefined in a real REST request. The update runner loads the admintheme.phpkeyed onwp_get_themes(), which wp-includes always defines. The suite loads admin files up front, which hid it.Saddle_Directory::load_api()now keys onthemes_api. The update path is unchanged.update-theme's description, External services item 7, and a 1.5.2 changelog line. The .pot is regenerated.Testing
phpuniton PHP 8.2: 1490 tests, 8342 assertions, 1 skipped (the existing 11-plugin test).phpcson every changed PHP file: clean.tests/installs-test.php: 10 theme tests, with fixtures recorded from the live themes API (Twenty Twenty-Five, and the real child themelegacy-kindergarten-child, which pins that WordPress.org names the parent intemplateandparent):Theme_Upgraderthat never switches the themeinstall-theme legacy-kindergarten-childwas refused, naming its missing parent.install-theme twentytwentytwo: preview, then confirm, then a page load ran cron. The result wasinstalled, and list-themes shows Twenty Twenty-Two 2.2 with active: false while Twenty Twenty-Five stays active.classic-editorinstalled, inactive.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.