changelog
What shipped.
Alpha, moving fast. Every release of the CLI, the WordPress plugin and the MCP server, newest first.
@loopress/cli
0.33.0
Minor Changes
- 0bf479c: Dynamic blocks (Loopress Full):
lps block push | pull | list | diff | rollback | rmand theblock_*MCP tools deployblocks/<slug>/folders (block.json+render.php, optionallystyle.cssandview.js) registered as the server-rendered blockloopress/<slug>. Each block’s CSS and JS load only where it renders, it works inside a Query Loop, and arender.phpthat throws renders nothing instead of breaking the page.lps push,lps pull,lps diffandlps validateinclude them. - 4613ded: Loopress Light no longer breaks the aggregate commands.
GET loopress/v1/capabilitiesnow lists the site’smodules, andlps pull,lps push(dry run included),lps diffand solps promoteskip a Full-only step (“needs Loopress Full”) when the project has no files for it. When it does,lps pushrefuses before pushing anything,lps pullandlps diffreport it.lps pushandlps theme pushskip templates and parts when the project has none, even withactivateChildTheme(uselps theme activatefor an empty child), andlps theme template pushalone refuses with “Nothing to push”. The removed SEO module warning is gone. - da7d681: The plugin now exposes
loopress/v1/capabilities(edition, version, environment type, ACF/form/snippet integrations, active theme and Loopress child theme), and the CLI requires it: an older plugin is asked to update.lps pushrefuses upfront, with the fix, when the project uses snippets, forms or ACF the site can’t hold, instead of failing at that step after pushing everything before it. Newlps theme activate(the Loopress child theme by default, which starts from its parent’s theme mods and Global Styles) and"activateChildTheme": truein loopress.json to keep the child active onlps theme template push. Global Styles keep the parent’s file name while the child is active.lps content copyactivates locally the theme and plugins active on the source.lps doctorreports the site’s capabilities.
Patch Changes
- 47d5a68: Consistency fixes from the 2026-10-27 audit.
lps apiandlps hookhelp examples pass the directory as a positional argument (there is no--pathflag), and the public-route warning links to docs.loopress.dev.lps --helpdescribes theapp,form,hook,menu,optionandpagetopics. The MCP server reports its real version, and its missing-project error nameslps init. Docs: API route and hook pushes are all or nothing, the declared resources appear in the workflow page, plus the snippet sidecar fields, theproject configprompts and the push exceptions that do delete. - cbe0bd9: Fix three CLI edge cases:
lps plugin auditnow aborts on a malformed vulnerability response instead of reporting it as clean, composer pull no longer treats a wpackagist theme as the provider of a plugin with the same slug (or the reverse), and"show_in_rest": nullin a meta file is now rejected. - b038a67:
lps plugin auditnow reads WordPress.org’slast_updateddate form, so abandoned plugins are flagged again.lps option listkeeps aCOLUMNSvalue you set. The top-level code check now catches code placed right after an attribute (#[Attr] foo();). - f24e50f: Narrow untyped data at the boundary instead of casting it. Malformed local files now fail with a clear error instead of crashing later: a
composer.jsonwith a non-string version, a non-objectloopress.jsonor Global Styles file, an apppackage.jsonwith non-string scripts. A malformedauth.jsonreads as logged out, an invalid snapshot file is skipped in listings, and an error thrown as a non-Error now shows its text instead ofundefined. - b698800: Validate local option files,
loopress.jsonplugin pins,composer.jsonsections and declared type arguments with zod.lps validatenow reports acomposer.jsonthat is not a JSON object instead of crashing.
0.32.2
Patch Changes
- ac984c0:
lps diff, push dry runs and the outside-edit check no longer report drift on ACF field groups or Global Styles that are in sync. ACF stores every setting with its default (wrapper,aria-label…), which a hand- or agent-written field group leaves out: the local files are now compared as the site would store them, through a new read-only plugin route (POST loopress/v1/acf/normalize/<type>) that runs them through ACF’s own validation without saving anything. Global Styles now compare a preset list and its origin-keyed form (palette: [...]andpalette: {custom: [...]}) as the same, and avar:preset|color|basereference as itsvar(--wp--preset--color--base)expansion, the two forms WordPress rewrites on save. With a plugin older than this release, ACF files are compared as written, as before. If the site can’t be read for the outside-edit check after an earlier push, the push now stops instead of skipping the check.
0.32.1
Patch Changes
- 26fb36c:
lps validateandlps hook pushno longer reject a hook whose method outputs a template (?> <p>...</p> <?phpinside the class) as “code outside the class”: only inline HTML at the top level of the file is flagged, as the plugin does.
0.32.0
Minor Changes
- 3f3beb9:
lps initno longer asks for a snippet provider. Most projects never use snippets, and the question came before any choice about them. To use snippets on a site that has neither Code Snippets nor WPCode, runlps plugin add code-snippets(orinsert-headers-and-footersfor WPCode). - 35310b3: The MCP server now tells a connecting agent what a Loopress project is and links the documentation of each file format, so an agent writes a Loopress hook instead of plain WordPress code.
lps validate(and the MCPvalidate_localtool) now runs the same checks on hook and API route files as push: a valid file name,declare(strict_types=1);exactly once, and no code outside the class (a hand-writtenABSPATHguard included, the plugin adds its own), anddeclare(strict_types=1);as the first statement. Those problems now surface before push; PHP syntax and class-name collisions are still checked by the server at push. - ad06a9a:
lps page pushcan now take over a page with the same slug that Loopress did not create, instead of refusing it. It lists those pages and asks once, or takes them over directly with--overwrite(required in CI and through the MCP server). The page keeps its URL and ID, so menus, links and SEO data keep working, and its original content is saved as a WordPress revision first. - ad938a6:
lps pushandlps pull(and solps promote,lps content copy,push_allandpull_all) now include Global Styles once the project tracks them, on a site whose active theme is a block theme, and skip them with a one-line note otherwise. Theme versions stay withlps theme push. ACF, forms and snippets are skipped when the project has no directory for them, solps push --dry-run(and thepush_allpreview) no longer fails with a 409 on a site without a form plugin, andlps diffno longer reports such a resource as inconclusive. - df54b2d: A push’s dry run now says what it would change: each local item is
created,changedorunchangedagainst the site, in the human output and underchangesin--json(existing fields are kept).lps acf pushalso names the fields added, changed or removed in a changed field group, andlps theme template pushlists templates, parts and CSS files separately, withremovedfor a file the child theme would drop.lps pushandlps theme pushnow return each step’s own result. The MCP push tools’ previews are these results, and their descriptions now tell the agent so. The plugin’s child theme endpoint reports a hash of each CSS file, so the dry run can tell whether the theme CSS changed. - 515153d:
lps scfis now an alias oflps acf, for sites running Secure Custom Fields instead of ACF.
0.31.0
Minor Changes
-
60ef881: New
lps content copy <from>: replaces the local site’s posts, pages, public custom post types and terms with those of<from>, then pushes the project to the local site so tracked configuration wins. Media stay served by the source site, users, comments and orders are never copied. -
5bb3a54:
lps hook listshows the last error each hook file threw while running, andlps doctorgets a “Hooks load and run without errors” check that fails on a hook that couldn’t load or keeps throwing. Needs the matching plugin release. -
ad4500c: A push no longer silently overwrites changes made on the site outside Loopress (wp-admin, another tool). Before writing, it compares the site with the state your last push from this machine left it in, using the existing rollback snapshots: an object changed there since, that the push would write a different version over, stops the push and is named (a terminal asks instead, a
--dry-runonly warns). Pass--overwriteto push anyway,--yesdoesn’t imply it. Pages now keep snapshots too, for this check.lps pushstops at the first resource that fails and lists the ones it didn’t attempt, instead of pushing the rest on top of a failed dependency (hooks without the Composer packages they use). Plugins and Composer are skipped when the project doesn’t manage them.A site that can’t be reached now says why (connection refused, host not found, timeout) with a hint to run
lps doctor, instead of an empty reason, andlps diffprints an error shared by every resource once. Its header reads “local files” instead of “local → local”.
Patch Changes
- 90779f2:
lps snippet pushkeeps an HTML snippet’s inline<?php ... ?>block instead of stripping it, keeps the description from the snippet’s.jsonfile instead of replacing it with “Imported from”, and skips, with a failed push, every file whose id another file also uses, instead of letting the last one silently overwrite the other on the site. lps diffnow shows a removed<?phpon a non-PHP snippet as drift.lps validateaccepts the{"version", "active"}plugin pinlps plugin pullwrites for an inactive plugin.lps promotetakes--overwrite, passed to its push, and says the local files may already hold part of<from>when its pull fails.lps hook listsays “an action”, andlps doctorno longer repeats “failed to load”.lps pushskips snippets on a project with nosnippets/directory instead of ending on an ENOENT error.lps form pushwrites the id WordPress assigned into a new form’s file, so the next push updates that form instead of creating a duplicate.lps theme version pullskips a theme with no version, the Loopress child theme included, instead of pinning it to an empty versionlps theme version pushthen fails on.lps plugin auditreads the affected version range wpvulnerability.net actually returns (operator), so it no longer reports every advisory ever published for a plugin, audits alatestpin as the newest WordPress.org release, decodes HTML entities in advisory names, and keeps its--jsonoutput when it exits with 1.lps composer pushnames the WordPress.org plugins and themes the site has and composer.json lacks, which the push uninstalls, on a dry run too, and asks before uninstalling them in a terminal.lps pull(and solps promote) skips ACF, forms and snippets, instead of failing, on a site without the plugin they need when the project has no directory for them.
0.30.0
Minor Changes
- a1a5147: New
lps cptcommands for custom post types declared ascpt/<slug>.jsonfiles:push,pull,list(every post type on the site and its source),rm,diffandrollback.lps pushandlps pullinclude them,lps validatechecks the file names and refuses arguments that would run code. Needs the matching plugin release. - aa60465: Commands no longer remember an environment. Without
--env, they target the project’slocalenvironment, or its only environment when it has just one. A project with several environments and nolocalone needs--envon every command.lps project switchnow picks a project only, andlps project listtags the default environment with[default]instead of an arrow on the switched one (isDefaultreplacesisCurrentper environment in--json). An existingconfig.jsonkeeps working: theenvfield ofcurrentProjectis ignored and dropped on the next write. - 049d127: New
lps metacommands for post meta keys declared asmeta/<post-type>.jsonfiles:push,pull,list(every registered meta key per post type, and whether Loopress declares it),rm,diffandrollback. Values are never synced.lps push,lps pullandlps validateinclude them. Needs the matching plugin release. - 22894f6: Breaking: the
lps seocommand group (pull,push,list,diff,rollback) is removed, along with its step inlps pull,lps pushandlps diffand theseoDirkey inloopress.json. Loopress versions configuration, never content: per-post SEO meta and redirects belong to the content editors manage in wp-admin, and most of that meta is data the SEO plugin generates itself. The site-wide SEO settings are one option row, tracked like any other option. To migrate, runlps option add rank-math-options-titles(Rank Math) orlps option add wpseo_titles(Yoast SEO), then delete theseo/folder and anyseoDirkey.lps pullandlps pushwarn while either is still there. - 03d3da0: A successful push now tells the site it happened (once per
lps <resource> push, never on a dry run), so the plugin’s Overview tab can show what was pushed, when, and whether it changed since. An older plugin that doesn’t know about it is ignored silently. - cd4b340: New
lps rolecommands for user roles declared asroles/<slug>.jsonfiles, andlps patterncommands for block patterns written aspatterns/<slug>.htmlfiles (block markup under atitle/categoriesheader, registered asloopress/<slug>, Loopress Full only):push,pull,list,rm,diffandrollbackfor both.lps push,lps pullandlps validateinclude them, patterns pushed before templates and pages. Needs the matching plugin release. - 66790cb: New
lps taxonomycommands for taxonomies declared astaxonomies/<slug>.jsonfiles:push,pull,list(every taxonomy on the site, its source and the post types it attaches to),rm,diffandrollback. Terms are never synced.lps push,lps pullandlps validateinclude them. Needs the matching plugin release. - 7139fd8:
lps theme template pushnow also pushes the theme CSS, bundled with esbuild:theme/css/style.css(the entry, it@imports the rest oftheme/css/in cascade order) becomes the Loopress child theme’sstyle.css, loaded on every page, and eachtheme/blocks/<namespace>/<block>.cssis loaded only on pages that render that block. Relativeurl()fonts and images are copied next to the CSS. The push warns about a block file named after no block registered on the site. NewcssDirandblockCssDirkeys inloopress.json, andlps devwatches both folders (css,blocks). Needs the matching plugin release. - 5fdd7ee: Block templates and template parts now live under
theme/, next to the Global Styles file:theme/templates/andtheme/parts/by default instead oftemplates/andparts/at the project root. Everythinglps thememanages sits in one directory. A project that keeps the old layout setstemplateDir: "templates"andpartDir: "parts"inloopress.json.
Patch Changes
- 95d9f8c:
lps devruns a push once per batch when several changed types share it: saving a template and a stylesheet together (templates,parts,css,blocks) rewrites the child theme once instead of once per type. - 3bbe20d: Error reports sent to Sentry no longer include HTTP headers, bodies, cookies, query params or stack frame variables, which Sentry 11 now collects by default.
- b87fa13:
lps project confignow removes any temporary administrator account left behind by an interrupted Loopress Full install (a killed process, a lost connection), andlps doctorreports one as a failed check. Only accounts matching both thelps-temp-username prefix and the@lps-temp.invalidemail are touched.
0.29.0
Minor Changes
-
cc5ede1: Options that store a page ID (EDD’s checkout pages in
edd_settings, WooCommerce’swoocommerce_shop_page_id…) can now be synced between environments. Declare them in the option file with"refs": {"purchase_page": "page"}, thenlps option pullwrites the page’s path instead of its ID, andlps option pushturns it back into the ID of that page on the target. A page missing on the target fails the push for that option with a clear message. -
cc5ede1:
lps plugin pullno longer adds every plugin installed on the site. Plugins the project already tracks are refreshed, and the others are offered as a list to tick, unticked by default, so a pull from production doesn’t bring its cache, security or backup plugins into the project. Without a terminal (--yes,--json, CI, the MCP server), only tracked plugins and those named with the new--plugin <slug>flag are written; the MCPplugin_pulltool takes them asplugins.Plugins under
require-devincomposer.jsonare now installed on thelocalenvironment bylps plugin pushandlps composer push, and still never on a remote site: use it for Query Monitor, or for a premium plugin’s free edition while production runs the paid one. -
bee1b73: Breaking: everything theme related now lives under
lps theme.lps template push/list/diffis nowlps theme template push/list/diff, and the versions-onlylps theme push/pullis nowlps theme version push/pull.lps theme pushnow pushes the whole topic in order (versions, then templates and parts, then Global Styles), andlps theme pullpulls versions and Global Styles.lps theme style pushaccepts--yes. In the MCP server,theme_push/theme_pullnow wrap the whole topic, the newtheme_version_push/theme_version_pullkeep the versions-only behavior, and thetemplate_*tool names are unchanged. Thetemplateandpartresource names forlps diff --only/--skipare unchanged.
Patch Changes
- cc5ede1: A push’s
--dry-runnow reads the target site, the same waylps diffdoes, instead of staying local. It used to announce ACF, SEO, forms or snippets as pushed on a site that would refuse them (plugin missing), and the real push then failed; the dry run now fails the same way. The MCP server’s preview and pre-apply check, built on the dry run, benefit too.lps push --dry-runalso says “would push” instead of “pushed”. - 426df4b: A refused WordPress or Loopress API request no longer ends its error with the raw
Caused by: HTTPError ... Code: ERR_NON_2XX_3XX_RESPONSElines: the server’s own reason, already in the message, is now the last thing printed.LPS_DEBUG=1still shows the whole chain. - 69ed247:
lps acf,lps seo,lps form,lps menuandlps theme stylepush/pullnow accept--json. The MCP server runs every command with--json, soacf_push,acf_pull,seo_push,seo_pull,form_push,form_pull,menu_push,menu_pull,theme_styles_pushandtheme_styles_pullalways failed with “Nonexistent flag: –json”. A test now checks that every command the MCP server can call supports--json. - 69ed247: The MCP server now refuses a change to
productioneven whenenvis omitted andproductionis the active environment. Before, only an explicitenv: "production"was refused, sohook_rm,api_rm, a*_rollbackor apushwithprunecould reach production without any human confirmation. A mutating tool called withoutenvnow resolves the active environment first and pins it, so the confirmed call also applies to the environment that was previewed, even if the active one is switched in between.lps status --jsonreports the resolvedenvironmentname for this. - 69ed247: The examples of
lps snippet push,lps snippet pull,lps snippet publishandlps api publishno longer show a--pathflag that does not exist: the directory is a positional argument (lps snippet push ./snippets). - b77aafe:
lps api removeandlps hook removeare now aliases oflps api rmandlps hook rm, matchinglps app removeandlps option remove.rmkeeps working. - e57f3a0:
lps initno longer asks for a snippets directory or a list of feature directories. It writes onlyprojectIdandrootDir, every resource uses its default directory, and a custom name is a<kind>Dirkey inloopress.json. The suggested next step is nowlps pull. - 426df4b: Template and part file names now accept
_, like the post type and taxonomy slugs the template hierarchy embeds:templates/taxonomy-download_tag.htmlortemplates/single-my_type.htmlno longer stoplps theme template push. Page file names keep their stricter rule. - a3c5097:
lps project config: “Authorize in my browser” works again. It was always rejected with “Rejected a cross-origin request to the login callback server”, then fell back to manual entry. WordPress now redirects straight to the CLI on127.0.0.1, without going throughapi.loopress.dev, so the Application Password never transits through a Loopress server. This needs WordPress 7.0 or later: on an older site, detected from its public version tag, the command goes straight to manual entry and says why.
0.28.0
Minor Changes
-
cd9479d:
lps app pushnow builds each app before pushing it: whenapps/<name>/package.jsonhas abuildscript, it runs with the app’s package manager (frompackageManager, else the nearest lockfile, else npm), on--dry-runtoo, so a forgotten rebuild no longer ships a staledist/reported as “up to date”. A failing build fails that app and uploads nothing. Pass--no-build(MCP:noBuild) when CI already built. Dependencies are never installed for you. -
4d1c8af: Apps can now stop other sites from embedding their files: set
"crossOrigin": "same-origin"(or"same-site") inloopress.app.json, and onlps app pushthe plugin writes an.htaccessin the app’s folder that sendsCross-Origin-Resource-Policywith every file. It overrides a site-widecross-originvalue set by a security plugin, and is removed when the setting is dropped.lps app pullkeeps the setting, andlps app pushwarns when it was not applied (plugin too old, or an app.htaccessthe site owner manages, which is left untouched). Apache and LiteSpeed only (nginx ignores.htaccess). -
20a406c: Block templates and parts:
templates/<slug>.htmlandparts/<slug>.htmlare written bylps template pushas the files of a child theme of the active block theme,<parent>-loopress, the parent staying untouched. Any template of the hierarchy can be overridden, and a template with atitle/postTypesheader becomes a page template a static page picks with itstemplateheader (no header or footer part gives a bare page). Edits made in the Site Editor are never overwritten and show up as drift inlps template diffandlps diff. Part oflps push(before pages),lps dev,lps initandlps validate. MCP gainstemplate_push,template_listandtemplate_diff. -
b1c46ac:
lps plugin pullandlps theme pullno longer stop when the project has acomposer.json: they pin the live versions into itsrequireaswpackagist-plugin/<slug>/wpackagist-theme/<slug>entries, leaving every other key untouched. A version constraint you wrote (^9.4) is kept, only exact pins move; packages inrequire-dev, provided by another package, or absent from WordPress.org (premium, custom) are skipped and listed in the output.loopress.jsonis not modified in that case. The file keeps its original indentation. If the WordPress.org check fails, the pull stops with the reason and leavescomposer.jsonuntouched.loopress.jsonnow records whether each plugin is active:lps plugin pullwrites an inactive plugin as{"version": "1.7.2", "active": false},lps plugin pushkeeps it inactive (and deactivates it if it was switched on), andlps plugin statusreports it as drift when it is active. A plain version string still means active, so existing files keep working. Before, a pull followed by a push activated every plugin that was inactive on the site.lps plugin pushno longer stops on a project with acomposer.json: it reads thewpackagist-plugin/*entries, pushes the whole file (themes and libraries included) and keeps its safety around the file swap, but activates nothing unless--activateis passed.lps pushthen skipscomposer pushso Composer runs once. Composer constraints like^9.4are no longer reported as version drift. -
61350a3: Breaking:
lps theme-styles pull/push/diff/rollbackis nowlps theme style pull/push/diff/rollback, grouped with the other theme commands. Thetheme-stylesresource name forlps diff --only/--skipand the MCP tool names (theme_styles_*) are unchanged, and existing rollback snapshots still work.
Patch Changes
- 4d1c8af:
lps app pullno longer overwrites an existingloopress.app.json: files go into itsassetsDirand itsentryis kept, so an app with a custom build folder or an explicit entry can still be pushed after a pull. The.htaccessfiles Loopress writes underwp-content/loopress/(apps/,vendor/) are now updated when a new plugin version changes them (for example thenosniffheader on apps), instead of only being written once. The rewrite is atomic (temp file + rename), so a request never sees half-written rules. A file whose first# Loopress:line was removed is left alone. - eb417c3:
lps promote <from> <to> --dry-runnow previews what<from>would really push: it pulls<from>into a throwaway copy of the project and dry-runs the push from there, instead of previewing your current local files. App password auto-rotation is skipped in CI (the runner can’t persist the new password, so rotating would revoke the one stored in the CI secret), with a warning to runlps project rotatewhere the config lives. A 403 with an empty body now shows the underlying error instead of ending on a bare colon.
0.27.0
Minor Changes
- d043549:
lps api pushrefuses a rootapi/index.phpbefore any network call, with a message pointing to a named or nested file (orders/index.phpis the/ordersroute).lps page pushsays when a pushedpages/home.htmlbecame, or stopped being, the site’s front page. - bb32133: The CLI now requires Node.js 22 or later (previously declared as 18+, but
lps validatealready relied on Node 20.12+ APIs). Node 22 is the oldest release line still receiving security fixes.
Patch Changes
-
1cd51fc:
lps api pushnow follows a symlink to a route file instead of silently skipping it (“Found 0 route files to push”). Broken symlinks are still ignored. -
bb32133:
lps devnow also watcheshooks/and pushes hook files on change. A request the Loopress plugin refuses on purpose with a 403 (for example reading a secret-looking option) now shows the plugin’s own reason instead of “Authentication failed, check your credentials”, and HTML entities in server messages (") are decoded. Errors from the Loopress cloud API now show the API’s own message (validation errors included) instead of a generic “Request failed with status code 400”.lps validatenow checks JSON files at any depth (ACF’sacf/<type>/, SEO’spost-meta/andredirects/were silently skipped before), tracked option files (a missingname,autoloadorvalueis reported), and the theme styles directory.lps plugin addandlps theme addsupport--json, and the MCP server gets matchingplugin_addandtheme_addtools (localloopress.jsonedit only).Rollback snapshots (
.loopress/snapshots/, inside your project) hold the environment’s full remote state; the.loopress/directory now writes its own.gitignoreso they are never committed by accident.lps push,lps pullandlps promotewith--jsonnow print a single valid JSON document: the progress output of the resource commands they run goes to stderr instead of polluting stdout, which broke the MCPpush_all,pull_allandproject_promotetools. A failed aggregate run now lists each failed resource and its reason. -
e042cde: Fix the MCP server on Windows: every tool failed with
ENOENTbecause it spawnedlpswithout a shell, and Windows installs it aslps.cmd, which Node refuses to run directly. The server now starts the installed CLI’sbin/run.jswith its own Node, falling back tolpsonPATHthroughtinyexec, which also works on Windows.LPS_BINalso accepts a.jsfile.@loopress/clinow exports itspackage.json, and the MCP server now declares Node 22 or later, like the CLI.lps initnow adds* text=auto eol=lfto.gitattributes, so a checkout on Windows (CRLF by default) keeps the same bytes as whatpullwrites.
0.26.0
Minor Changes
- b60df46: Adds
lps page push [SLUG],lps page listandlps page difffor static HTML pages (pages/<slug>.html,pageDirinloopress.json), with an optional<!-- title: ... / status: draft|publish -->header. Pages are part oflps push,lps diff --only page,lps devandlps init, and are checked bylps validate. The MCP server gets matchingpage_push,page_listandpage_difftools, and a CLI command that exits non-zero with a regular JSON payload (a diff reporting drift) is now returned as a result instead of an error. Requires Loopress Full.
0.25.0
Minor Changes
-
c15b4b9: Makes
api pushandhook pushatomic (#236). Previously,pushFile()sent one independent HTTP PUT per file: a push of N files that failed partway through (network error, a staleexpectedRevision, a class collision, a syntax error on file K) left files1..K-1already live on the site andK..Nuntouched, a mixed state discoverable only after the fact vialps diff.AbstractFilesDirectorygains a batch staging API (beginBatch/stageWrite/stageDelete/commitBatch/abortBatch): a batch stages into an inactive sibling directory seeded with a copy of every currently live file, andcommitBatch()swaps it into place with a directoryrename(), atomic on the filesystems WordPress actually runs on.AbstractFilesControllergains aPOST {routePath}/batchendpoint that validates and stages every file (and every--pruned filename) of one push, and only callscommitBatch()once every single one has succeeded; any failure aborts the batch and leaves the live directory completely untouched.api push/hook pushnow send one batch request instead of N sequential PUTs, folding--pruneinto the same atomic swap rather than a separate round of DELETEs after the fact. The single-filePUT/DELETEendpoints are unchanged, still used bylps <resource> rmand any direct API integration. -
3ec619c:
lps doctornow supports--json, returning a structured{project, environment, url, checks, ok, pluginVersion}result instead of throwing on failure (its human-readable output and exit code are unchanged).The MCP server gains three read-only tools that were missing from CLI parity:
project_diff(drift between local files and an environment, or between two environments),project_doctor(connectivity, plugin and credential diagnostics), andvalidate_local(checks local tracked files without contacting WordPress). -
1607fc5: Adds a
menuresource for syncing WordPress navigation menus and menu locations. The plugin exposesloopress/v1/menusandloopress/v1/menu-locationsREST endpoints, resolving menu items by identity (post type/taxonomy + slug) rather than raw object ids so they remain portable between environments. The CLI gainslps menu list/pull/push/diff, wired intolps push/lps pull/lps diff/lps initand local file validation, and the MCP server gains matchingmenu_push/menu_pull/menu_listtools. -
10d18ec:
lps push,lps pull, andlps promotenow support--json(each returns a structured per-resource result instead of erroring with “Nonexistent flag: –json”). This was a real bug in the MCP server’spush_allandpull_alltools, which always shell out with--jsonand so always failed.The MCP server also gains
project_promote, the tool forlps promotethat CLI parity was missing. -
ce19505: Adds
lps <resource> rollbackfor the 9 resource-state-backed resources (snippet, form, acf, api, hook, seo, menu, option, theme-styles).lps <resource> pushnow writes a snapshot of the environment’s state (before and about-to-be-pushed) to a local, gitignored cache (.loopress/snapshots/<resource>/, capped at the last 10 per resource) right before every real push.lps <resource> rollbackrestores the most recent snapshot (or--to <id>),--listshows what’s available. It refuses to overwrite a change made to the environment since the original push unless explicitly confirmed (interactive prompt, or--yes), the same shape as the production-push guard. No WordPress-side capability is required: restoring materializes the archived state into the resource’s own local file layout, then delegates to the existing push command.The MCP server gains a matching
<resource>_rollbacktool per resource, following the existing confirmToken handshake (list: truebypasses it as a plain read). -
4432466: New
lps theme-styles pull/push/diffcommand group syncs the active block theme’s Global Styles customizations (Site Editor > Styles: colors, typography, spacing) as a localtheme/<stylesheet>-global-styles.jsonfile, reusing WordPress core’s ownwp/v2/themesandwp/v2/global-styles/<id>REST endpoints (no new PHP controller needed). Classic (non-block) themes fail the command clearly instead of syncing nothing silently. This resource is included automatically inlps diff, but deliberately left out of the aggregatelps push/lps pull(likelps theme), so a classic-theme site’s routine sync isn’t broken by this opt-in feature.The MCP server gains matching
theme_styles_pullandtheme_styles_pushtools, andproject_diffnow coverstheme-stylestoo.
Patch Changes
-
6f5bc6b: Extends #234’s conditional write to the
acfresource:lps acf pushnow closes the WordPress-side race a write can still land in for field groups, post types, taxonomies, and options pages, the same way it was already closed foroptionin #235.GET /acf/{type}/{key}now returns arevision(a content hash of the exported object, excluding fields ACF itself computes such asmodified).acf pushreads it right before writing each object, and sends it back asexpectedRevision;POST /acf/{type}refuses the write (412) if the object’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The remaining 7 resource-state-backed resources (snippet, form, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
d2be612: Extends #234’s conditional-write pattern (introduced for
optionin #235) to the two file-based resources,apiandhook, which share the sameAbstractFilesController/AbstractFilesDirectorybase classes.GET /api-filesandGET /hook-filesnow return arevision(a content hash) per file, andPUTreturns one too on a successful push.api pushandhook pushcompute each file’s expected revision from the remote listing already read for the push’s own rollback snapshot, no extra network round trip, and send it back asexpectedRevision; the server refuses the write (412) if the file’s revision no longer matches, instead of silently overwriting whatever changed it in between. A file with no remote counterpart yet (a first push) has nothing to condition on, so it falls back to today’s unconditional create. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero.The remaining 6 resource-state-backed resources (snippet, form, acf, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout.
-
9bd5f61: Extends #234’s conditional write to
form:lps form pushnow closes the same WordPress-side race thatoption pushalready closed.GET /forms/{id}(and the create/update responses) now return arevision(a content hash of the form’s canonical state as reported by whichever form plugin is currently active, minus itsidand its own save-bookkeeping fields).form pushreads it right before writing each existing form, and sends it back asexpectedRevision;PUT /forms/{id}refuses the write (412) if the form’s revision no longer matches, instead of silently overwriting whatever changed it in between. A form with no local id yet (a first push) has nothing to condition on, so it falls back to today’s unconditional create, same asoption push. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The remaining resource-state-backed resources (snippet, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
1d05edd: Continues #234’s rollout:
lps menu pushnow closes the same WordPress-side race thatoption pushclosed in #235, this time for themenuresource.GET /menus/{slug}(and the upsert response) now return arevision(a content hash of the menu’snameanditems).menu pushreads it right before pushing each menu, and sends it back asexpectedRevision;POST /menusrefuses the write (412) if the menu’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. A menu that doesn’t exist remotely yet has nothing to condition on, so a first push is unchanged. The other 7 resource-state-backed resources (snippet, form, acf, api, hook, seo, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
bc2359c: Proof of concept for #234:
lps option pushnow closes the WordPress-side race a write can still land in, on the one resource this lands on first (option).GET /options/{name}now returns arevision(a content hash of the value).option pushreads it right before writing each option, and sends it back asexpectedRevision;PUT /options/{name}refuses the write (412) if the option’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The other 8 resource-state-backed resources (snippet, form, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
e4f4b0d: Fixes
lps plugin pushnever activating a plugin it just installed for the first time (plugin addfollowed bypush), even though the site ends up with the plugin present but inactive.diffPlugins()only ever routed a not-yet-installed plugin throughtoInstall, andpushonly activatedtoActivateplus whatever it defensively deactivated for a file swap, nevertoInstall.pushnow re-reads the site’s plugin list after a successful Composer sync to resolve the newly installed plugins’ WordPress core ids, and activates them alongside everything else. -
e4f4b0d: Fixes the non-interactive drift error from
lps <resource> rollbackmentioning “without –dry-run/–yes to leave it as is”, advice that can never apply in that branch since it is only reached once--dry-runis already known to be false. The message now only mentions the--yesoption that actually resolves it. -
2e05f74: Closes the same race for
seothat PR #235 closed foroption(#234):lps seo pushnow conditions its writes on the active SEO plugin’s (RankMath or Yoast) state at read time, instead of silently overwriting a change a different actor made on WordPress in between.GET /seo/settingsnow returns{revision, settings}(a content hash of the settings, wrapped so the hash never gets written back into wp_options as if it were a real Titles & Meta field), andGET /seo/post-meta/{type}/{slug}now includes arevisionalongsidemeta,slug, andtitle(a content hash of that post’s provider-prefixed SEO meta as a whole, since a write replaces the whole set, not just the keys a particular push happens to mention).seo pushreads the relevant revision right before writing each settings file or post-meta file, and sends it back asexpectedRevision;PUT /seo/settingsandPOST /seo/post-meta/{type}refuse the write (412) if the revision no longer matches, the same optimistic-concurrency checkoptionuses. The revision is bookkeeping, never persisted to local files:settings.jsonand post-meta files on disk are unchanged in shape.The remaining resources tracked in #234 (snippet, form, acf, api, hook, menu, theme-styles) aren’t covered yet.
-
f1e8e61: Closes the WordPress-side race
lps snippet pushcould still land in (#234), extending the conditional write introduced foroptionto thesnippetresource.GET /snippets/{id}(and thecreate/updateresponses) now return arevision, a content hash of every field an update can change (code, active state, location, priority, and the rest).snippet pushreads it right before writing each already-known snippet, and sends it back asexpectedRevision;PUT /snippets/{id}refuses the write (412) if the snippet’s revision no longer matches, instead of silently overwriting whatever changed it in between. A first push (no known remote id yet) is unaffected, it still falls through to a plain create. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap. The remaining resource-state-backed resources (form, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234. -
e4f4b0d: Fixes
lps option add,lps menu pull, andlps form pullwriting server-only bookkeeping fields (revision, andwarningsfor menus) into the local tracked files, despite the documented “never persisted locally” contract onRemoteOption.revision/Menu.revision/Menu.warnings/RemoteForm.revision. Each command now writes only the fields that belong to the tracked configuration.
0.24.1
Patch Changes
-
406eae1:
lps composer pushnow reflects that the server resolvescomposer.jsonitself and never installs from the uploadedcomposer.lock. When the server’s resolution moves a version your local lock had pinned, the command prints the drift and tells you to runlps composer pull. The--jsonoutput gains alockDriftarray. The MCPcomposer_pushtool description is updated to match. -
b28d36c:
lps api pull/lps hook pullnow skip a file the server declined to return (e.g. one over the new size limit): they print a warning and leave any local copy untouched, instead of overwriting it with nothing.lps api list/lps hook listshow the server’s reason next to such a file. -
d198555: Add
lps api rm <slug>/lps hook rm <slug>to delete one route or hook file from WordPress (confirmation prompt,--yesfor CI,--dry-run), andlps api push --prune/lps hook push --pruneto delete server-side files with no local counterpart after a push.--pruneandrmboth refuse to delete in a non-interactive shell without--yes: unlikepull’s local cleanup, a pruned file on the server is not recoverable from the repo. The MCP server gains matchingapi_rm/hook_rmtools and apruneoption onapi_push/hook_push, both behind the confirm-token handshake. -
69bdc0c:
lps api pushnow warns for each pushed route that runs with no authentication (#[Permission(public: true)]), andlps api listbadges those routes[PUBLIC](with a trailing warning).lps api list --jsonincludes"public"per route. The MCPapi_listtool exposes the same field. -
de7f283:
lps option addnow defaultsreadonly: truefor the behaviour-changing core options the server refuses to write (default_role,users_can_register,mailserver_*,uninstall_plugins,initial_db_version), alongside the ones that were already read-only by default (siteurl,home,cron, …).lps option pushskips read-only options, so tracking one of these fordiffvisibility no longer leads to a403on push. -
b43b2c7:
lps form pushandlps seo pushgain opt-in flags for the sync operations the server now gates:lps form push --allow-notificationsalso pushes each form’s notification and confirmation settings (off by default, so a stray push cannot redirect submissions).lps seo push --allow-external-redirectspermits pushing a redirect whose target is on another site (rejected by default).
The MCP
seo_pushtool gainsallowExternalRedirects.form_pushvia MCP always uses the safe default (notifications preserved); the opt-in is CLI-only by design. -
a52c231: Security: harden the
lps login/lps project configbrowser authorization flow (F24, F25).- The loopback callback server now generates a 32-byte
state, threads it through the authorization URL, and requires it back (constant-time compare) on any request that carries credentials. A local process or a web page open during the wait can no longer POST forged credentials intoconfig.json/auth.json. - The server also enforces an
Originallowlist (https://api.loopress.devfor site auth,https://console.loopress.devfor console login) and shuts down on the first callback-shaped request, valid or not, instead of staying open for the whole 5 minute window. authorizeWithBrowserno longer reads the Application Password or username from the URL query string, only from the relay’s form POST body, so they can no longer land in shell history, proxy logs or the browser address bar.
No API or console change is required: both relays copy the callback URL, and its
state, through verbatim. - The loopback callback server now generates a 32-byte
-
c2fb026: Security: strip WordPress site URLs and server response bodies from CLI crash reports (F26).
A failed WordPress request throws an
Errorwhose message is built byformatWpError: it carries the full request URL and, for a 4xx/5xx, the server’s raw response body appended after a newline (a Composer trace, a WordPress fatal, absolute server paths, package URLs).finally.tspassed that straight toSentry.captureException.A
beforeSendscrubber now runs on every event: it keeps the first line of each exception message (the “which endpoint, which status” summary), blanks the site host while keeping the/wp-json/...REST path, drops everything after the first line, and removes any request context. Chainedcauseerrors are covered too. The terminal still prints the full, unmodified message.
0.24.0
Minor Changes
-
755b390: Added
hooks/, a new resource for declaring WordPress actions, filters, and scheduled (cron) tasks as plain PHP files, deployed withlps hook push/pull/list(andlps push/pull/diff,hook_push/hook_pull/hook_listMCP tools). One file, one class, public methods attributed with#[Action(hook, priority, acceptedArgs)],#[Filter(hook, priority, acceptedArgs)], or#[Cron(recurrence, hook?)]bind straight toadd_action()/add_filter()/WP-Cron (a#[Cron]job is just an action bound to a schedule instead of an existing WordPress event). Unlike a REST route, a bound hook runs unconditionally for every visitor with no permission check of its own, so every callback is wrapped to catch and log rather than propagate; a filter additionally fails open, returning the original value on a throw. Loopress Full only, same as custom API routes. -
0ec5ebe: Add
lps diff: show what differs between your local tracked files and a WordPress environment, or between two environments with--against. Covers snippets, forms, ACF, API routes, SEO, and Composer, comparing the normalized representation each resource round-trips (so serialization quirks like key order or a re-added<?phptag never show up as drift). Prints a git-style summary (+local-only,-remote-only,~changed, with a per-field list for objects and a line diff for file contents) and exits non-zero when anything differs, so it doubles as a CI drift gate. Plugins and themes keep their existinglps plugin status/lps theme status. -
ad89acb: Add per-resource diff commands, scoping flags, and harden
lps difffor CI use.lps snippet diff,lps form diff,lps acf diff,lps api diff,lps seo diff: run the same normalized comparison aslps difffor a single resource, accept the resource’s[PATH]override, and support--env/--against/--json.lps diffgains--only <resource>...and--skip <resource>...to narrow the run (a project that only tracks some resource types can stop the rest showing as drift).- Form and ACF comparisons now ignore server-rewritten save timestamps (
modified,modified_gmt), the same way redirecthitsis already ignored, so a diff between two independently-configured environments reports real differences only. - Exit code is now 0 in sync, 1 on drift, 2 when a resource could not be compared (a fetch failure no longer looks like “no drift” to a CI gate).
--jsonchange entries now carry a structuredfieldslist for object resources. Resources are compared concurrently. A[PATH]argument combined with--againstis rejected rather than silently ignored.
-
f0fe762: Added
options/, a new resource for tracking individual WordPress options (wp_optionsrows) directly, agnostic of which plugin owns them: no adapter to write, unlikeseo/which has to know Yoast’s vs RankMath’s option names and shape.lps option listshows every option name and autoload flag on the site (never values, so browsing stays cheap and never leaks a value by accident),lps option add <name>starts tracking one as a local file, andlps option pull/push/diff/removework the tracked set from there (pushis upsert-only, it never deletes an untracked option). Two safety rails:active_plugins/template/stylesheetare refused outright (already owned by theplugin/themeresources), and environment-owned or WordPress-generated options (siteurl,home,db_version,cron,rewrite_rules,WPLANG) default to"readonly": truein their local file, tracked and diffable but skipped bypushunless explicitly overridden. Mirrored asoption_push/option_pull/option_list/option_add/option_removeMCP tools. -
6b0fda3: Breaking: removed the
pageresource.lps page pull/push/list/diff, thepage_pull/page_push/page_listMCP tools, thepageDirsetting inloopress.json, and thepagesentry forlps dev/lps diff --onlyare all gone.lps push/pullno longer touch pages. WordPress pages are core content editable in wp-admin or overwp/v2/pagesdirectly; keep them under version control with a general-purpose WP-CLI export if needed.
0.23.0
Minor Changes
-
d811b3f: Add
lps app push [name],lps app pull [path],lps app listandlps app remove <name>for syncing built single-page app bundles between a localapps/<name>/directory (aloopress.app.jsonplus a builtdist/) and WordPress.pushbuilds a sha256 manifest ofdist/, asks the site which files it already has, uploads only the differences, then commits the new build in one step; the front end keeps serving the old build until that commit lands.pullwrites each committed app back toapps/<name>/dist/and removes local app directories that no longer exist remotely (confirmed in a terminal, reported in CI). Adds anappsDirkey toloopress.jsonand an “apps” entry tolps init.New MCP tools mirror the commands:
app_push,app_pull,app_list,app_remove(app_push/app_removeuse the dry-run/confirmToken handshake).This is a Loopress Full plugin feature; the CLI and MCP surface is edition-agnostic.
-
dfb33a3: Pin WordPress.org plugin and theme versions through a lockfile, installed on the site with Composer + WPackagist.
loopress.jsonplugins(and the newthemeskey) now take an exact version or"latest", instead of always"latest".lps plugin pushno longer calls WordPress’s corewp/v2/pluginsendpoint (which only installs the current release); it sends an intent to the site, where Loopress Full renders a plugin-ownedcomposer.jsonand runs Composer against the WPackagist repository, landing plugins inwp-content/plugins/at the pinned version, with no SSH and nocomposer.jsonin your repo.lps plugin pull/lps theme pullnow pin every plugin/theme to the version running on the site.lps plugin add --version <v>pins an exact version;lps theme addmirrors it.- New
lps plugin pushflags:--force(allow downgrades, and take over a plugin installed by hand, replacing its files) and--prune(deactivate active plugins absent fromloopress.json). Removing an entry fromloopress.jsonuninstalls the plugin on the next push (shown in the plan, confirmed). - New
lps plugin status/lps theme status: report drift (missing, wrong version, inactive, untracked) and exit non-zero for CI. - New
lps plugin audit: check pinned plugins against a WordPress vulnerability database (wpvulnerability.net) and the WordPress.org plugin API for health signals. - New
lps themecommand group (add,pull,push,status). Loopress manages installed theme versions only, it never switches the active theme. - When a repo has a
composer.json(fromlps composer init), it is authoritative for plugins/themes and theplugin/themecommands defer tolps composer.lps composer pushnow sends an intent (libraries + WPackagist plugins/themes) instead of a verbatimcomposer.json; the plugin owns the file’s shape.
New MCP tools:
plugin_status,plugin_audit,theme_push,theme_pull,theme_status.plugin_pushgainsforceandprune.Limits: WordPress.org plugins/themes only (premium plugins are tracked but never touched); a downgrade replaces files only and does not undo database migrations; multisite is not supported.
This relies on a Loopress Full plugin feature; see the companion changeset there.
Patch Changes
- b3e9d88: Fix
lps plugin pullrecording the wrong slug for a single-file plugin whose WordPress.org slug differs from its bare file id (e.g. Hello Dolly:hello.phphas slughello-dolly, nothello). The wrong slug made a laterlps plugin push --forcefail atomically with a Composer “could not be found” error instead of installing anything. The slug is now read from the plugin’sPlugin URIheader when it points to a wordpress.org listing, falling back to the bare file id otherwise.
0.22.0
Minor Changes
- 1c3e617: Add
lps promote <from> <to>, which copies every tracked resource from one environment to another by runninglps pullfrom<from>thenlps pushto<to>in one step. It confirms once up front (local tracked files are overwritten with<from>in the process, and the prompt calls out production targets), forwards--dry-runto both halves, and stops before pushing if the pull fails. - 14874f8: Add
lps pull, a top-level command that pulls plugins, composer dependencies, ACF, API routes, forms, pages, SEO, and snippets from WordPress in one run instead of calling each resource’spullcommand separately. It’s the counterpart oflps push.composeris pulled beforepluginsso plugin detection sees the freshcomposer.json, and--yesis forwarded only to the resource pulls that prompt before deleting orphaned local files. - 672395e: Add
lps push, a top-level command that pushes plugins, composer dependencies, ACF, API routes, forms, pages, SEO, and snippets to WordPress in one run instead of calling each resource’spushcommand separately. Production pushes are confirmed once up front rather than once per resource. - 74cb1ab: Add
lps validate, an offline check that the local tracked files are push-ready: every resource JSON parses and is an object, snippet sidecars use a knowntypeand an integerid, API route files are non-empty,loopress.jsonpoints at a configured project, andcomposer.jsonis well formed. It contacts nothing, prints one line per problem, exits non-zero when any is found, and supports--json. Handy as a pre-commit or CI gate beforelps push. - 50fae55:
lps project listnow supports--json, like every otherlistcommand. - 3bfe6a8:
lps acf list,lps form list, andlps seo listnow use the same--jsonmechanism as every other command instead of a one-off-j/--jsonflag. Breaking: the-jshort flag is removed from these three commands, use--jsoninstead.
Patch Changes
- b2fcf46: Internal refactoring: extract a shared
pullDirectoryhelper for the write-then-reconcile-orphans loop duplicated acrossacf/api/form/page/seo/snippetpull commands, and a sharedguardProductionPushhelper for the production-push confirmation duplicated betweenPushCommandand the top-levellps push. No visible behavior change. - cb993db: Internal refactoring: extract shared helpers for the id-based PUT-then-create-on-404 push dance (
form,page,seoredirects,snippet), the Listr push-task loop, ENOENT-tolerant directory listing, and count pluralization, previously duplicated across each resource’spush/pullcommand. No visible behavior change.
0.21.0
Minor Changes
-
182ae2a:
api/route files can now use a bracketed segment,[order_id], anywhere in their path (e.g.api/invoice-pdf/[order_id].php,api/orders/[order_id]/items/[item_id].php) to capture a dynamic value into$request->get_param(...), the same convention as Astro/Next.js dynamic routes, without a catch-all segment. The segment name must start with a letter or underscore (it becomes a PHP identifier internally). The generated class name PascalCases each path segment and joins them with_(InvoicePdf_OrderId, notInvoicePdfOrderId), so two differently nested files can never collide on the same class name.lps api push/pull/listnow support route files nested in subdirectories, needed for the above. Internal, breaking: the upload endpoint (PUT loopress/v1/api-files) now takesfilenameas a body field instead of a URL path segment (avoids depending on how a given host handles a percent-encoded slash in a URL), so the CLI and the WordPress plugin must be upgraded together, an old CLI against a new plugin (or the reverse) will fail to push. -
c365613: Add
lps dev, which watches snippets, pages, API routes, and the plugin manifest, pushing each change to thelocalenvironment automatically. Supports--only/--skipto limit which resource types are watched. -
421a1e8:
--jsonnow works consistently acrosssnippet/page/api/plugin/composerpush/pull/list andstatus: each command returns a structured result (shape documented per command) instead of the interactive progress UI, and errors come through as{error: {message, name}}with a non-zero exit code.snippet list/page list/api listpreviously had their own one-off--json/-jflag; they now use the same oclif mechanism as everything else. First step towards a@loopress/mcpserver that wrapslpsinstead of duplicating its logic, seeobsidian/Product/Loopress MCP.md.
Patch Changes
- 67a932e:
lps api pushnow rejects a file missingdeclare(strict_types=1);(or containing it more than once) before making any network call, mirroring the server’s own check instead of failing only after the round-trip. - 182ae2a:
lps api push’s server-side PHP syntax check now distinguishes “verified, no error” from “couldn’t verify here” (exec()disabled, common on managed hosts, or another local condition preventing the check from running), instead of treating both as silent success. The CLI now reports when the check was skipped for the second case instead of staying indistinguishable from a fully verified push.
0.20.1
Patch Changes
-
6091e29: Bug fixes from the QA backlog (5th pass), lot 1:
lps doctorandlps project confignow detect Application Passwords being disabled on the target site by reading thewp-json/index instead of probingwp-admin/authorize-application.php, which sits behind the admin login wall and never reached the check that would report the feature disabled.lps page pushcan now recreate a page whose local id no longer exists on the site (deleted, or pushed to a site where that id never existed): the fallback create no longer sends the stale id, which WordPress core previously rejected with “Cannot create existing post”.- Server error messages are no longer hidden on a 404: a legitimate applicative 404 from a Loopress controller (e.g.
lps composer pullon a site with nocomposer.lockpushed yet) now shows the real server message instead of a generic “is the plugin installed?” one, andlps composer pulltreats that specific case as “no lock yet” and writescomposer.jsonalone instead of failing.
0.20.0
Minor Changes
- 886fa43: Add
lps page pull,lps page push, andlps page listto sync WordPress pages as files in Git. Talks directly to WordPress core’s own REST API (wp/v2/pages), no Loopress plugin required. Each page is a pair of files:<id>-<slug>.htmlfor the raw Gutenberg block content, a real file you can open and edit directly, and<id>-<slug>.jsonfor everything else (title, slug, status, parent, menu_order, template, meta, excerpt…), filtered down to only the fields WordPress actually accepts back on write so readonly/computed noise that changes on every edit regardless of actual content (_links,guid,modified) doesn’t produce a diff on every pull.parentround-trips as the source site’s raw page id and isn’t remapped across environments. - 9bbd03f:
lps project confignow detects when Loopress Full isn’t installed on the target site and offers to install it automatically. Since Loopress Full is never distributed on wordpress.org and normally requires a manual zip upload in wp-admin, this closes that gap: after confirming, the CLI downloads the latest release, creates a temporary administrator account (the only way to get plugin-install rights from an application password), drives the wp-admin upload flow headlessly to install and activate the plugin, then removes the temporary account. If the automated install fails for any reason, the temporary account is still cleaned up and the CLI falls back to printing the downloaded zip’s local path plus the direct upload URL so the install can be finished by hand. - 9a0bb9d: Add
lps project rotateto replace the WordPress application password for the current (or--env) environment: it creates a new one, verifies it authenticates on its own, then revokes the old one, never the other way around, so a bad new credential can’t lock you out. Every other command now also does this silently in the background once the stored credential is older than 90 days: best-effort, skipped during--dry-run, and a failed attempt (site unreachable) just retries on the next run instead of blocking the current command.
0.19.0
Minor Changes
-
7fa0350: Explicit environment targeting and safer destructive operations (CLI backlog lot 3)
- New
--env <name>flag on every project-aware command: targets an environment by name for a single run, taking priority overlps project switch, with an error listing the available environments when the name does not exist.lps status --env <name>previews what would be targeted. - New
--yes(-y) flag on commands that ask for confirmation. - Production guard: push commands targeting an environment named
productionnow ask for confirmation in a terminal, and require--yesin non-interactive runs. - Pull commands now list local files that would be deleted (because they no longer exist on WordPress) and ask before removing them.
--yesskips the question; without a TTY the previous behavior (remove and warn) is kept so existing scripts do not break. - The CLI never hangs on a prompt in CI: confirmations take their default answer and log it, and commands that require interactive input (
lps init,lps project config) fail immediately with instructions.
- New
-
01300ca: Diagnostics and onboarding (CLI backlog lot 4)
- New
lps doctorcommand: checks that the site is reachable, the Loopress plugin installed, and the credentials valid, each with a corrective action, and shows the plugin version when exposed. Exits non-zero when a check fails, so it can guard a CI deploy. lps initnow offers to runlps project configinline when no project is configured yet, proposes the other feature directories (ACF, SEO, Forms, custom API routes) via an optional multi-select, and ends with a summary of everything configured plus the next useful command.- The CLI now warns when a newer version is available (background npm check, at most once a day, never blocking), pointing to the npm update command.
- New
Patch Changes
-
827af5b:
lps api pushnow rejects a route file whose name contains characters outside[a-z0-9-](e.g. uppercase letters, underscores) with a clear “Invalid filename” error before attempting the push. Previously the request reached WordPress, whose REST router matches the filename against the same allowlist and returned a generic 404 that the CLI reported as “Is the required plugin installed and up to date on the site?”, a misleading message for what was actually an invalid filename. -
d529f5e: Timeouts and dependency cleanup (CLI backlog lot 1)
- Calls to the Loopress cloud API now time out after 30s with an actionable error message instead of hanging indefinitely.
lps composer pushwaits up to 10 minutes for the server-sidecomposer installinstead of failing after 30s, shows a wait message, and explains on a real timeout that the install may still be running on the server.- Removed unused dependencies:
@oclif/plugin-pluginsand@oclif/test.
-
adc5b73: Internal refactoring, shared helpers (CLI backlog lot 2). No visible behavior change.
- One shared orphan-file detection used by the five pull commands (acf, api, form, seo, snippet).
- One shared directory loader (per-file parse and validation, corrupted files skipped with a warning, missing directory means empty) used by acf push and api push.
- PushCommand now carries failedCount and the Listr task failure reporting used by the six push commands.
- Single API_URL definition, shared toSlug() helper, snippet and seo file-format functions moved to utils/.
-
5658d6c: Fix three bugs found during manual QA. Pushing a snippet with a location unsupported by the active provider (WPCode) used to create the snippet anyway with the location silently defaulted, while still reporting failure to the CLI; since the CLI never learned the resulting id, retrying the push (the natural reaction to a reported failure) created another duplicate snippet each time.
WPCodeSnippetProvidernow validates the location before any write, so an invalid location is rejected with nothing created. A pushedapi/route file with a real PHP parse error was accepted and listed as present bylps api push/list, while the route silently 404d at request time;ApiFilesController::push_file()now runs a real PHP syntax check before writing, rejecting with a clear 400. SEO endpoints (lps seo) returned a generic 500 for client-actionable conditions (multiple SEO plugins active, redirects unsupported by the active plugin) instead of the 409/400 used by the equivalent snippets/forms guards; dedicated exceptions now map these correctly, andget_settings()/update_settings()(which had no error handling at all before this) are covered too.Also fixes
lps composer init’s generated scaffold, which requiredcomposer/installerswithout allow-listing it, so Composer 2.2+’s non-interactive plugin-trust gate blocked every realcomposer pushthrough it. Error messages surfaced from a WordPress REST failure now include the server’s full detail (e.g. the actual Composer trace) instead of just a generic summary, making failures like this one diagnosable from the CLI’s own output.
0.18.0
Minor Changes
- 2b6420c: Adds custom API routes: a versioned
api/folder (lps api push/pull/liston the CLI) lets a project expose its own WP REST endpoints without hand-writing a plugin, deployed straight towp-content/loopress/api/on Loopress Full and registered underloopress-api/v1/<filename>. One file, one class, one method per HTTP verb (get/post/put/patch/delete), resolved by filename convention (kebab-case -> PascalCase). Defaults tomanage_options+ Application Password auth like every other sync route, with explicit per-file overrides forpermission()(e.g. an anonymous headless form endpoint) andheaders()(CORS, including the OPTIONS preflight). Filenames are allowlisted against path traversal, deployed files get an auto-injectedABSPATHguard against direct HTTP access, and a bad file (parse error, class collision, throwingpermission()/headers()) is skipped and logged rather than fatalingrest_api_initor breaking other routes. Full only, like snippet sync: Light stays ACF+SEO only. - 9383f6e: Adds a WordPress form-sync feature:
lps form list/pull/pushon the CLI, backed by new REST routes underloopress/v1/formson Loopress Full (Light stays ACF+SEO only). The plugin side introduces a genericFormProviderabstraction, mirroring the existing snippet sync (Code Snippets/WPCode), with WPForms as the first supported plugin; more WordPress form plugins can be added as additional providers later. Forms are addressed by numeric id (no ACF-style stable key), pulled/pushed as one<id>-<slug>.jsonfile per form, with orphan cleanup on pull and the same PUT-then-404-fallback-to-create dance aslps snippet push.
0.17.0
Minor Changes
- 31790ce: Add
lps seo pull,lps seo push, andlps seo listto sync SEO configuration as JSON files in Git: site-wide Titles & Meta settings (including per-post-type schema defaults), per-post SEO meta, and redirects. Works with either RankMath or Yoast SEO, whichever is active on the site, exactly one must be active, if both are active at once or neither is, everyseocommand fails with a clear error instead of guessing which plugin’s data is authoritative. Redirects are only available when RankMath is active: Yoast’s equivalent is a Premium-only feature this doesn’t support,seo pullskips them quietly when unsupported andseo pushfails clearly per file if you have local redirect files the active plugin can’t take.
0.16.0
Minor Changes
-
6f5712c: Add
lps acf pull/lps acf push/lps acf list, which sync ACF (Advanced Custom Fields) field groups, post types, taxonomies, and options pages between the WordPress site and local JSON files, the same git-based workflow already available for snippets and Composer dependencies.Backed by new
loopress/v1/acf/*endpoints on the WordPress plugin. Requires ACF to be installed and active; options pages additionally require ACF PRO. -
6c557ec: Add
lps composer init, which scaffolds a composer.json wired to WPackagist (repository,composer/installers, installer-paths) so WordPress.org plugins and themes can be added and installed through Composer instead of the native plugin API.lps composer pullnow also pullscomposer.json(previously onlycomposer.lock), backed by a newGET loopress/v1/composer/jsonendpoint on the WordPress plugin, so local composer.json stays in sync with packages added or removed through the Loopress admin page.
Patch Changes
- 9589ec1: From Beta to Alpha to reflect more status of the tool
0.15.0
Minor Changes
- 69b5050: Replaced
lps project syncwithlps project pushandlps project pull, matching thepush/pullnaming used bysnippet,pluginandcomposer.lps project pushcreates/links local projects and environments on your Loopress account and pushes credentials (whatsyncdid for local-to-remote).lps project pullfetches projects and environments already on your account that aren’t configured locally yet, now works even when no project is configured locally.
Patch Changes
- 83a65be: Stopped sending personal data to Sentry when reporting a crash. Command-line argument values (WordPress URLs, application passwords, tokens, emails) are now redacted, only flag names are kept for debugging context. Also disabled
sendDefaultPiiexplicitly and set a staticserverNameinstead of the machine’s real hostname.
0.14.0
Minor Changes
- 42c3956: Use oclif native config system
- 9a8f6b5:
lps plugincommands now use WordPress core’s nativewp/v2/pluginsREST API instead of a custom Loopress endpoint. As a result, plugin version pinning is no longer supported:lps plugin addno longer accepts a[version]argument, andloopress.jsonalways stores"latest"for managed plugins. Pin an exact version through thecomposercommand group and wpackagist instead. - b27bd0c: Delay sentry load to improve performances
Patch Changes
- 13e0495: Move WordPress app-password relay from website to API
0.13.0
Minor Changes
- 6a18588: Add application password creation with open browser
- 01b3005: Add snippet publish command
0.12.0
Minor Changes
- 8a3e52f: Sync projects and snippet files during push/pull
0.11.0
Minor Changes
- 9754a14: Remove snippet provider and put it directl in plugins during init
- 0ab8400: Improve remove UX and synchronization with backend
- 616b2ab: Add
lps project syncto push locally configured projects, environments and credentials to your Loopress account - 908875c: Add upsert mechanism on snippet push
- 85b2fcf: Improve ux of project switch with separators
0.10.0
Minor Changes
- Publish schemas on NPM
0.9.0
Minor Changes
- f5bae31: Add list2 to improve UX
- 34a4a22: Setup sentry to monitor the CLI
0.8.0
Minor Changes
- 540c702: Improve structure with fable
- 6122ddc: Align better sidecar file with WPCode api
0.7.0
Minor Changes
- 56cca02: Refine command descriptions. Improve compatibility with WPCode. Add unit tests
0.6.0
Minor Changes
- 18edf51: Add composer command
0.5.0
Minor Changes
- 138acfd: Remove style commands in favor of snippets for custom CSS
- 6aba757: Use sidekick files to store snippets state
- 249b128: Add init command
- 7f4ac7c: CLI can read projectId in loopress.json
- 6aba757: Record deployments in Loopress console
0.4.0
Minor Changes
- 2d33230: Add commands to manage plugins
- 2d33230: Add cli-plugins command as wrapper around oclif plugin plugins
- a3e2a67: Activate plugin after installation
0.3.0
Minor Changes
- Add detection by id for push/pull of snippets
0.2.0
Minor Changes
- 7a3642f: Add full snippet pull with various files
Patch Changes
- 7b8b35d: Add ascii art of the logo
0.1.1
Patch Changes
- Re-gnerate readme
0.1.0
Minor Changes
- f66babd: Initial release
@loopress/wordpress-plugin
2026.18.0
Minor Changes
- 0bf479c: Dynamic blocks (Loopress Full):
lps block push | pull | list | diff | rollback | rmand theblock_*MCP tools deployblocks/<slug>/folders (block.json+render.php, optionallystyle.cssandview.js) registered as the server-rendered blockloopress/<slug>. Each block’s CSS and JS load only where it renders, it works inside a Query Loop, and arender.phpthat throws renders nothing instead of breaking the page.lps push,lps pull,lps diffandlps validateinclude them. - 4613ded: Loopress Light no longer breaks the aggregate commands.
GET loopress/v1/capabilitiesnow lists the site’smodules, andlps pull,lps push(dry run included),lps diffand solps promoteskip a Full-only step (“needs Loopress Full”) when the project has no files for it. When it does,lps pushrefuses before pushing anything,lps pullandlps diffreport it.lps pushandlps theme pushskip templates and parts when the project has none, even withactivateChildTheme(uselps theme activatefor an empty child), andlps theme template pushalone refuses with “Nothing to push”. The removed SEO module warning is gone. - da7d681: The plugin now exposes
loopress/v1/capabilities(edition, version, environment type, ACF/form/snippet integrations, active theme and Loopress child theme), and the CLI requires it: an older plugin is asked to update.lps pushrefuses upfront, with the fix, when the project uses snippets, forms or ACF the site can’t hold, instead of failing at that step after pushing everything before it. Newlps theme activate(the Loopress child theme by default, which starts from its parent’s theme mods and Global Styles) and"activateChildTheme": truein loopress.json to keep the child active onlps theme template push. Global Styles keep the parent’s file name while the child is active.lps content copyactivates locally the theme and plugins active on the source.lps doctorreports the site’s capabilities.
2026.17.1
Patch Changes
- ac984c0:
lps diff, push dry runs and the outside-edit check no longer report drift on ACF field groups or Global Styles that are in sync. ACF stores every setting with its default (wrapper,aria-label…), which a hand- or agent-written field group leaves out: the local files are now compared as the site would store them, through a new read-only plugin route (POST loopress/v1/acf/normalize/<type>) that runs them through ACF’s own validation without saving anything. Global Styles now compare a preset list and its origin-keyed form (palette: [...]andpalette: {custom: [...]}) as the same, and avar:preset|color|basereference as itsvar(--wp--preset--color--base)expansion, the two forms WordPress rewrites on save. With a plugin older than this release, ACF files are compared as written, as before. If the site can’t be read for the outside-edit check after an earlier push, the push now stops instead of skipping the check.
2026.17.0
Minor Changes
- fcf44bc: A Loopress link in the WordPress top bar, for administrators. Hovering it shows the plugin version, the environment type, and the last push.
- ad06a9a:
lps page pushcan now take over a page with the same slug that Loopress did not create, instead of refusing it. It lists those pages and asks once, or takes them over directly with--overwrite(required in CI and through the MCP server). The page keeps its URL and ID, so menus, links and SEO data keep working, and its original content is saved as a WordPress revision first. - df54b2d: A push’s dry run now says what it would change: each local item is
created,changedorunchangedagainst the site, in the human output and underchangesin--json(existing fields are kept).lps acf pushalso names the fields added, changed or removed in a changed field group, andlps theme template pushlists templates, parts and CSS files separately, withremovedfor a file the child theme would drop.lps pushandlps theme pushnow return each step’s own result. The MCP push tools’ previews are these results, and their descriptions now tell the agent so. The plugin’s child theme endpoint reports a hash of each CSS file, so the dry run can tell whether the theme CSS changed. - 33baa84: Loopress Full now discloses its anonymous usage statistics in a wp-admin notice right after activation, with equal “Keep it on” and “Turn it off” buttons, instead of only in the Settings tab. The notice disappears once an administrator answers, either here or in Settings.
2026.16.0
Minor Changes
- 60ef881: Content export and import endpoints for
lps content copy, in both editions. The import deletes the site’s posts and terms, so it only runs on a site whose environment type islocal. After an import, images missing locally are served from the source site’s uploads URL. - 5bb3a54: The last error a hook method threw while running is now kept per file (method, message, time), and returned as
runtimeErrorby the hook list endpoint while the file is still the version that threw it. The same error is written at most once a minute. Until now it only went to the PHP error log.
Patch Changes
-
ef30ef3: Admin page redesign:
- Every block of every tab now sits in its own white card, so all tabs share one look. Recent pushes on Overview is a table, Settings groups both data-sharing toggles in one card, and Dependencies stacks “Add a package” above “Installed packages” (the table no longer overflows its card).
- Overview now repeats every issue the other tabs raise: server configuration, Composer platform problems, security advisories and abandoned packages, code files that failed to load, and Loopress declarations skipped because something registered the same slug first.
- Code tab: “View source” on API routes and hooks opens the file in a drawer on the right, with a copy button, instead of expanding it inline, with PHP syntax colors.
- Config tab: Loopress-owned post types, taxonomies, roles and meta keys stand out in the Source column. When ACF or a form plugin isn’t installed, its section shows a quiet note instead of a red error, and the admin page no longer retries requests the server refused with a 4xx.
- The page header no longer slides under the WordPress admin bar when scrolling.
- The API routes namespace moves from under the routes list to the Settings tab, and to a gear popover in the API routes card header.
- A Check for updates button in the page header asks GitHub right away instead of waiting for the twice-daily check, and offers the update when there is one. Dashboard → Updates’ Check again now really asks GitHub too.
-
b4e6151: Post types from
cpt/and taxonomies fromtaxonomies/are now registered oninitpriority 9 and 8 (was 20 and 19), before the default 10. Plugins that read post types duringinitand cache them, like Rank Math for its sitemap settings, now see them. A slug another plugin registers again later is still reported as a conflict. -
90779f2: API route and hook files are now syntax-checked with PHP’s own parser before they are written, so a broken file is refused at push time on every host, Local by Flywheel and managed hosts included, instead of only where a
phpbinary is on the PATH. A menu item pointing at a page or term missing on the site answers 422 instead of 500, and an empty menu item description no longer reads back as a single space, which showed as drift after every push. A static page holding an empty enclosing shortcode ([tabs][/tabs]) that runsthe_contenton its inner text no longer recurses until a 502: a nestedthe_contentrun for the page being rendered gets nothing back instead of the page. A form in the trash now counts as gone forlps form push, which recreates it, instead of updating it in the trash and reporting a successful push. A Composer sync with no package at all, on a site that never had one, no longer fails with a 500 (“require : Array value found, but an object is required”): an emptyrequireis written as{}.
2026.15.0
Minor Changes
- 03d3da0: The Loopress admin page opens on a new Overview tab, in both editions: what was pushed to the site from your repository, when, by whom and through which application password, and whether each resource changed on the site since its last push, outside the CLI. Before the first push it shows how to connect the CLI, with the site’s URL ready to copy. Once a resource has been pushed, its own WordPress screens (ACF, Menus, WPForms, Code Snippets, WPCode) warn that changes made there are overwritten by the next push. A new Config tab lists, read-only, the configuration Loopress syncs (ACF objects, menus, forms), next to the Code tab for deployed code. Loopress Light’s page gets the Overview and Config tabs instead of a static introduction.
- 33df017: The Loopress admin page is reorganized into three tabs: Dependencies, Code (API routes, hooks and single-page apps, read-only) and Settings. The site’s environment type now shows next to the title, and on a production site installing, updating or removing a Composer package asks for a confirmation first.
DISALLOW_FILE_MODSis now respected: Composer dependencies become read-only in the admin, and every Composer endpoint that writes files answers 403,lps composer pushincluded. Platform and webserver checks moved out of the removed Diagnostics tab into Tools > Site Health, and only show on the Dependencies tab when they find something. The Code tab adds a source view and a copyable URL for each API route, a copyable shortcode for each app, and when each named cron runs next. The update banner is gone, since updates already appear on the Plugins page. - a1a5147: Custom post types can now be declared in code: each
cpt/<slug>.jsonfile in your repository holds theregister_post_type()arguments, and the plugin registers it on every request, in both editions. Arguments WordPress would run as code (register_meta_box_cb, the REST controller classes) are refused, and a slug already registered by a theme or another plugin is skipped, never overwritten. Permalinks are refreshed once after a change. The Config tab lists every post type on the site with where it comes from (Loopress, WordPress, ACF, CPT UI, a theme or plugin), its published count, and a Conflict badge for a skipped Loopress one. - 049d127: Post meta keys can now be declared in code too, in both editions: each
meta/<post-type>.jsonfile maps meta keys to theirregister_post_meta()arguments (type, single, default, REST exposure),postandpageincluded. Arguments that would run code (sanitize_callback,auth_callback,show_in_rest.prepare_callback) are refused, so are an unknown type, a default of the wrong type, and an array or object exposed in REST without a schema. A key already registered elsewhere is skipped. Only the keys are synced, never their values. The Config tab lists the registered meta keys per post type, and flags a key sharing its name with an ACF field. - 22894f6: Breaking: the SEO module is removed from both editions, with its
loopress/v1/seo/*REST routes and the redirects list in the Config tab. The plugin no longer reads or writes Rank Math or Yoast SEO data directly. Their site-wide settings stay syncable as a regular tracked option (rank-math-options-titlesorwpseo_titles) through the options endpoints. - cd4b340: Custom user roles can now be declared in code, in both editions: each
roles/<slug>.jsonfile holds a role’s name and capabilities. Loopress adds the role in memory on every request and never writes it into the saved roles, so the file stays the only source of truth and removing it really removes the role. Built-in roles are reserved, and a role already saved on the site is skipped. The Config tab lists every role with its number of users. Loopress Full also registers block patterns frompatterns/<slug>.htmlfiles asloopress/<slug>, so a<!-- wp:pattern {"slug":"loopress/<slug>"} /-->reference in a page, template or part renders the current version. - 66790cb: Taxonomies can now be declared in code too, in both editions: each
taxonomies/<slug>.jsonfile holds theregister_taxonomy()arguments plusobject_type, the post types it attaches to. Same rules as custom post types: arguments that would run code are refused, so are values of the wrong type, a slug already registered elsewhere is skipped, and permalinks are refreshed once after a change. Only the taxonomy is synced, never its terms. The Config tab lists every taxonomy on the site with its source, the post types it’s attached to and its number of terms. - b87fa13: Loopress Full now removes, every hour, any temporary administrator account left behind by an interrupted
lps project configinstall, once it is more than 15 minutes old. Until then, administrators see a wp-admin notice naming it. Only accounts matching both thelps-temp-username prefix and the@lps-temp.invalidemail are touched. - 7139fd8: The Loopress child theme now carries the project’s theme CSS. Its
style.cssis loaded on the site and in the editor, inlined when small, and eachblocks/<namespace>/<block>.cssis loaded withwp_enqueue_block_style(), only on pages that render that block. Only while the child is the active theme. Fonts and images go to the child’sassets/, nothing else can be written there.
Patch Changes
- cb1fb49: The usage ping now sends the site’s UTC offset (for example
+02:00) when no city is picked under Settings → General, instead of an empty timezone. WordPress defaults to an offset, so most sites sent nothing.
2026.14.0
Minor Changes
- cb29aee: Form sync (WPForms) now ships in Loopress Light too, not only Loopress Full:
lps form pull/push/listwork against a site running the wordpress.org edition. Every value in a pushed form (field labels, descriptions, the HTML field’s content) is now stripped of active content, the same way ACF and SEO already were: script and style tags, inline event handlers, andjavascript:URLs are removed, other HTML is kept as is. - cc5ede1: Options that store a page ID (EDD’s checkout pages in
edd_settings, WooCommerce’swoocommerce_shop_page_id…) can now be synced between environments. Declare them in the option file with"refs": {"purchase_page": "page"}, thenlps option pullwrites the page’s path instead of its ID, andlps option pushturns it back into the ID of that page on the target. A page missing on the target fails the push for that option with a clear message. - bc3e474: Loopress Full now sends an anonymous usage ping, at most twice a day, alongside its update check: a random site identifier, the Loopress, WordPress and PHP versions, the site language, timezone, and environment type. Never the site URL, an email, content, or the plugin list. On by default, switch it off under Settings → Send anonymous usage statistics. Loopress Light is unchanged and sends nothing.
Patch Changes
- cc5ede1: Menus and SEO meta now find child pages. A menu item pointing to
account/profileused to fail on push withNo "page" post with slug "profile" was found, and SEO meta for a child page did too.lps menu pullnow writes a child page’s full path (account/profile), so menu files pulled before this change get a one-time diff. A bare slug still works when exactly one page has it; when several do (account/profileandteam/profile), the push fails with a 409 listing the paths to choose from. - 426df4b:
full-width: truenow reaches the true edge on Twenty Twenty-Five’s page template: the side padding of the template’s own wrappers around the content (<main>and its group) is dropped too, on any block theme. The header and footer keep theirs, and the template’s vertical spacing is unchanged. - 72457e5: Listing options no longer fails with a fatal error when WordPress cannot reach its database at that moment: the list comes back empty instead.
- a40d6da: Composer package search (Loopress Full) now says “Unable to fetch versions for this package.” when the version lookup fails, instead of the misleading “No stable versions found.”
- 426df4b: Template and part file names now accept
_, like the post type and taxonomy slugs the template hierarchy embeds:templates/taxonomy-download_tag.htmlortemplates/single-my_type.htmlno longer stoplps theme template push. Page file names keep their stricter rule. - a40d6da: Marked as tested up to WordPress 7.1, and cleared the remaining WordPress Plugin Check errors so the plugin keeps showing up in wordpress.org searches.
2026.13.0
Minor Changes
- 4d1c8af: Apps can now stop other sites from embedding their files: set
"crossOrigin": "same-origin"(or"same-site") inloopress.app.json, and onlps app pushthe plugin writes an.htaccessin the app’s folder that sendsCross-Origin-Resource-Policywith every file. It overrides a site-widecross-originvalue set by a security plugin, and is removed when the setting is dropped.lps app pullkeeps the setting, andlps app pushwarns when it was not applied (plugin too old, or an app.htaccessthe site owner manages, which is left untouched). Apache and LiteSpeed only (nginx ignores.htaccess). - 20a406c: Block templates and parts:
templates/<slug>.htmlandparts/<slug>.htmlare written bylps template pushas the files of a child theme of the active block theme,<parent>-loopress, the parent staying untouched. Any template of the hierarchy can be overridden, and a template with atitle/postTypesheader becomes a page template a static page picks with itstemplateheader (no header or footer part gives a bare page). Edits made in the Site Editor are never overwritten and show up as drift inlps template diffandlps diff. Part oflps push(before pages),lps dev,lps initandlps validate. MCP gainstemplate_push,template_listandtemplate_diff.
Patch Changes
- 4d1c8af:
lps app pullno longer overwrites an existingloopress.app.json: files go into itsassetsDirand itsentryis kept, so an app with a custom build folder or an explicit entry can still be pushed after a pull. The.htaccessfiles Loopress writes underwp-content/loopress/(apps/,vendor/) are now updated when a new plugin version changes them (for example thenosniffheader on apps), instead of only being written once. The rewrite is atomic (temp file + rename), so a request never sees half-written rules. A file whose first# Loopress:line was removed is left alone. - eb417c3: The Loopress Light readme now lists menu and option sync, which Light already ships, along with the option read/write safeguards.
2026.12.0
Minor Changes
- d043549: A nested
api/orders/index.phpis now the/ordersroute instead of being silently ignored after a successful push. A rootapi/index.phpis refused at push time (it’s the directory’s anti-listing guard, and/is the namespace discovery index), and two files resolving to the same route (orders.phpandorders/index.php) are refused at push, or skipped and reported bylps api listwhen deployed another way. Pushing a publishedpages/home.htmlnow makes it the site’s front page (Settings > Reading), replacing the previous one; switching it back to draft reverts the site to its latest posts.
Patch Changes
- bb32133: Serializes every write to
wp-content/loopress/api/andhooks/(batch pushes, single-file pushes, deletions) behind a per-directory lock. Two overlappinglps api push/lps hook pushruns shared one staging directory: the second could end up committing a staging directory holding only its own files, deleting every other route or hook from the site. A second push now waits for the first and starts from its result. If the lock itself can’t be taken (for examplewp-content/loopress/not writable), the request now fails with a JSON 500 carrying the reason instead of a PHP fatal error.
2026.11.0
Minor Changes
- b60df46: Adds static pages (Loopress Full only): a
loopress/v1/pagesendpoint that creates or updates a WordPress page from hand-written HTML, stored in a hidden post meta and rendered inside the active theme throughthe_content(nowpautop). Pages managed this way get a “Managed by Loopress” badge and can no longer be edited in wp-admin; trashing stays possible. A push is refused, with nothing written, on a page Loopress didn’t create, a trashed page, a slug WordPress would rename, or HTML over theloopress_max_file_bytes/loopress_max_files_total_byteslimits.
Patch Changes
- b60df46: Adds
#[Hidden], a class-level attribute for route files: every verb in the file is excluded from the/wp-json/{namespace}discovery index (show_in_index => false, WP core’s own flag) while the route keeps dispatching normally. Useful for a route whose response shape isn’t a fixed REST resource, a GraphQL endpoint or an HTML-serving page.
2026.10.1
Patch Changes
- 71e7243: Wraps route verb callbacks (
get(),post(), etc.) in a try/catch so an uncaught exception returns a clean 500 response instead of fataling the request. WP core’s own dispatch (WP_REST_Server::respond_to_request()) has no try/catch around the route callback, unlikepermission_callbackandheaders(), whichRouteLoaderalready wrapped for the same reason.
2026.10.0
Minor Changes
-
c15b4b9: Makes
api pushandhook pushatomic (#236). Previously,pushFile()sent one independent HTTP PUT per file: a push of N files that failed partway through (network error, a staleexpectedRevision, a class collision, a syntax error on file K) left files1..K-1already live on the site andK..Nuntouched, a mixed state discoverable only after the fact vialps diff.AbstractFilesDirectorygains a batch staging API (beginBatch/stageWrite/stageDelete/commitBatch/abortBatch): a batch stages into an inactive sibling directory seeded with a copy of every currently live file, andcommitBatch()swaps it into place with a directoryrename(), atomic on the filesystems WordPress actually runs on.AbstractFilesControllergains aPOST {routePath}/batchendpoint that validates and stages every file (and every--pruned filename) of one push, and only callscommitBatch()once every single one has succeeded; any failure aborts the batch and leaves the live directory completely untouched.api push/hook pushnow send one batch request instead of N sequential PUTs, folding--pruneinto the same atomic swap rather than a separate round of DELETEs after the fact. The single-filePUT/DELETEendpoints are unchanged, still used bylps <resource> rmand any direct API integration. -
9b3647d: Loopress Full now updates through WordPress’s native Plugins page flow (the “update available” notice, “Update now” link, and bulk updater), instead of requiring a manual re-upload of
loopress-full.zip. WordPress fetches the zip directly from the matching GitHub release, using the same GitHub lookup the update-available notice already relied on. -
1607fc5: Adds a
menuresource for syncing WordPress navigation menus and menu locations. The plugin exposesloopress/v1/menusandloopress/v1/menu-locationsREST endpoints, resolving menu items by identity (post type/taxonomy + slug) rather than raw object ids so they remain portable between environments. The CLI gainslps menu list/pull/push/diff, wired intolps push/lps pull/lps diff/lps initand local file validation, and the MCP server gains matchingmenu_push/menu_pull/menu_listtools.
Patch Changes
-
6f5bc6b: Extends #234’s conditional write to the
acfresource:lps acf pushnow closes the WordPress-side race a write can still land in for field groups, post types, taxonomies, and options pages, the same way it was already closed foroptionin #235.GET /acf/{type}/{key}now returns arevision(a content hash of the exported object, excluding fields ACF itself computes such asmodified).acf pushreads it right before writing each object, and sends it back asexpectedRevision;POST /acf/{type}refuses the write (412) if the object’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The remaining 7 resource-state-backed resources (snippet, form, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
d2be612: Extends #234’s conditional-write pattern (introduced for
optionin #235) to the two file-based resources,apiandhook, which share the sameAbstractFilesController/AbstractFilesDirectorybase classes.GET /api-filesandGET /hook-filesnow return arevision(a content hash) per file, andPUTreturns one too on a successful push.api pushandhook pushcompute each file’s expected revision from the remote listing already read for the push’s own rollback snapshot, no extra network round trip, and send it back asexpectedRevision; the server refuses the write (412) if the file’s revision no longer matches, instead of silently overwriting whatever changed it in between. A file with no remote counterpart yet (a first push) has nothing to condition on, so it falls back to today’s unconditional create. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero.The remaining 6 resource-state-backed resources (snippet, form, acf, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout.
-
e4f4b0d: Fixes a fatal error in
lps snippet list/pull/pushwhenever the Code Snippets plugin is active.CodeSnippetsSnippetProvidercalledSnippet::is_trashed(), a method the current WPackagist release of Code Snippets (3.10.2) no longer has: trash state is exposed as thetrashedproperty onCode_Snippets\Model\Snippetinstead. Both call sites (isTrashed()andtrashedIds()) now read the property, and the static-analysis stub is updated to match the real class. -
9bd5f61: Extends #234’s conditional write to
form:lps form pushnow closes the same WordPress-side race thatoption pushalready closed.GET /forms/{id}(and the create/update responses) now return arevision(a content hash of the form’s canonical state as reported by whichever form plugin is currently active, minus itsidand its own save-bookkeeping fields).form pushreads it right before writing each existing form, and sends it back asexpectedRevision;PUT /forms/{id}refuses the write (412) if the form’s revision no longer matches, instead of silently overwriting whatever changed it in between. A form with no local id yet (a first push) has nothing to condition on, so it falls back to today’s unconditional create, same asoption push. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The remaining resource-state-backed resources (snippet, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
1d05edd: Continues #234’s rollout:
lps menu pushnow closes the same WordPress-side race thatoption pushclosed in #235, this time for themenuresource.GET /menus/{slug}(and the upsert response) now return arevision(a content hash of the menu’snameanditems).menu pushreads it right before pushing each menu, and sends it back asexpectedRevision;POST /menusrefuses the write (412) if the menu’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. A menu that doesn’t exist remotely yet has nothing to condition on, so a first push is unchanged. The other 7 resource-state-backed resources (snippet, form, acf, api, hook, seo, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
bc2359c: Proof of concept for #234:
lps option pushnow closes the WordPress-side race a write can still land in, on the one resource this lands on first (option).GET /options/{name}now returns arevision(a content hash of the value).option pushreads it right before writing each option, and sends it back asexpectedRevision;PUT /options/{name}refuses the write (412) if the option’s revision no longer matches, instead of silently overwriting whatever changed it in between. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap: it shrinks the window down to this one request’s own execution time, not to zero. The other 8 resource-state-backed resources (snippet, form, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234 as the remaining rollout. -
2e05f74: Closes the same race for
seothat PR #235 closed foroption(#234):lps seo pushnow conditions its writes on the active SEO plugin’s (RankMath or Yoast) state at read time, instead of silently overwriting a change a different actor made on WordPress in between.GET /seo/settingsnow returns{revision, settings}(a content hash of the settings, wrapped so the hash never gets written back into wp_options as if it were a real Titles & Meta field), andGET /seo/post-meta/{type}/{slug}now includes arevisionalongsidemeta,slug, andtitle(a content hash of that post’s provider-prefixed SEO meta as a whole, since a write replaces the whole set, not just the keys a particular push happens to mention).seo pushreads the relevant revision right before writing each settings file or post-meta file, and sends it back asexpectedRevision;PUT /seo/settingsandPOST /seo/post-meta/{type}refuse the write (412) if the revision no longer matches, the same optimistic-concurrency checkoptionuses. The revision is bookkeeping, never persisted to local files:settings.jsonand post-meta files on disk are unchanged in shape.The remaining resources tracked in #234 (snippet, form, acf, api, hook, menu, theme-styles) aren’t covered yet.
-
f1e8e61: Closes the WordPress-side race
lps snippet pushcould still land in (#234), extending the conditional write introduced foroptionto thesnippetresource.GET /snippets/{id}(and thecreate/updateresponses) now return arevision, a content hash of every field an update can change (code, active state, location, priority, and the rest).snippet pushreads it right before writing each already-known snippet, and sends it back asexpectedRevision;PUT /snippets/{id}refuses the write (412) if the snippet’s revision no longer matches, instead of silently overwriting whatever changed it in between. A first push (no known remote id yet) is unaffected, it still falls through to a plain create. This is optimistic concurrency control within a single request, not a database-level atomic compare-and-swap. The remaining resource-state-backed resources (form, acf, api, hook, seo, menu, theme-styles) aren’t covered yet, tracked in #234. -
e4f4b0d: Fixes the stale-revision (412) error message showing literal
"instead of quotes in the CLI’s terminal output. The message is only ever thrown into a REST JSON body and then read aslpsstderr, never rendered as HTML, so it should not have been run throughesc_html(). Affects options, snippets, forms, ACF objects, menus, and SEO post meta/settings.
2026.9.2
Patch Changes
-
406eae1: Security:
POST /loopress/v1/composer/syncno longer installs from a client-suppliedcomposer.lock.The endpoint used to write the uploaded
lockto disk verbatim and runcomposer install, which replayed it as-is: a crafted lock could pointdist.url/source.urlat attacker-controlled hosts (server-side request forgery) and regeneratevendor/composer/autoload_files.phpfrom those archives, which the plugin then loads on every request (remote code execution). The lock is now advisory only. Composer always runsupdateand resolves the plugin-renderedcomposer.jsonagainst its own Packagist and WPackagist repositories; the uploaded lock is only parsed afterwards to report which resolved versions moved (lockDriftin the sync response).Also: every Composer invocation now passes
--no-scripts, and thelocksync argument is capped at 5 MB. -
b28d36c: Security: bound the size of
api/andhooks/files so one file can no longer take down/wp-json/.AbstractFileLoadertokenises (token_get_all()), thenrequires, every file in the directory on every request. With no size limit, a large enough file (or a large enough pile of them) exhausted memory, an uncatchableE_ERRORthat 500s every REST route on the site, and the same blob would 500 the push that wrote it (LP-SEC-02 / F19 / F20).Now:
PUT /api-filesand/hook-filesreject acontentover 512 KB with413, before anything tokenises it.- The loader skips any on-disk file over 512 KB (recorded in the load-errors option, surfaced in the admin tab), so a file planted outside the CLI breaks only itself.
- The loader also stops once the directory as a whole passes an 8 MB budget, a last-resort guard against a runaway directory.
- Both limits are filterable:
loopress_max_file_bytesandloopress_max_files_total_bytes(each passed the subdir,'api'or'hooks'). list_filesreturns{filename, error}withoutcontentfor an over-limit file instead of reading it back.FileWriter::withGuard()fails cleanly if the guard-placement regex bails on its backtrack limit, rather than misplacing the guard.
-
d198555: Security: add
DELETE /loopress/v1/api-filesand/hook-filesso a deployed route or hook can be removed through the product.Route and hook files live under
wp-content/, outside the plugin directory, so deactivating the plugin never removed them. Before this, a file pushed by a leaked application password could only be cleared over SSH/SFTP (F3).DELETEtakesfilenamein the query string, validated by the same rules asPUT(including the hooks-only “last segment must not beindex”), returns404when the file is absent and200 {"filename", "deleted": true}otherwise, and clears any stale boot-time load-error entry for the removed slug.uninstall.php(Full edition) already removed the entirewp-content/loopress/tree; a comment now spells out that this is deliberate (api, hooks, vendor, apps, everything), so nothing Loopress wrote survives an uninstall. -
69bdc0c: Security: surface which custom API routes run with no authentication (F1).
#[Permission(public: true)]on a route’s class or a verb method makes that code callable by anyone, with no login. There was no signal for it anywhere. Now theGET /api-fileslist and thePUT /api-filespush response carrypublic: truefor such a route (detected lexically by a newPermissionScanner, the file is never executed), and the plugin’s API Routes admin tab shows a red Public badge on the row.Hooks are unaffected: they have no permission concept, so
HookFilesControllerdoes not add the flag.Detection has two documented blind spots it does not flag: an aliased import of the attribute, and a
permission()method that returnstrue. -
de7f283: Security: guard the generic
/loopress/v1/options/{name}resource so a deployment token is not “read every secret / rewrite site behaviour over HTTP” (F10, F11).GET /options/{name}now returns403for a name that looks like a stored secret (matchessecret,password,_pass,token,_key,_api_key,apikey,^auth_,nonce,salt,private_key,credential). This is a best-effort denylist; the real control for a specific site is the newloopress_option_readablefilter, or tracking only the options it needs.PUT/DELETE /options/{name}now return403for a curated set of behaviour-changing core options (default_role,users_can_register,siteurl,home,cron,uninstall_plugins,mailserver_*,db_version,initial_db_version) and for anyloopress_*option (owned by the plugin’s own settings). Re-allow a specific name with the newloopress_option_writablefilter.- New
ProtectedOptionException(403), distinct fromReservedOptionNameException(409, still used only for the 3 names owned by theplugin/themeresources).
A server-enforced allowlist (“only options declared in loopress.json are readable/writable”) is the stronger fix and is tracked separately: it needs the tracked list synced to the server.
-
b43b2c7: Security: the SEO, Forms and ACF sync resources no longer write unvalidated request bodies straight into storage other plugins render (F12, F13).
- New
SyncSanitizer::stripActiveContent()neutralises script/style blocks, inline event handlers andjavascript:/data:text-htmlURLs in a string, leaving benign HTML and text byte-for-byte so a pull/edit/push round trip stays stable. Applied recursively toPUT /seo/settings,POST /seo/post-meta, and every string in an/acfimport object, all of which are rendered into the public<head>or the block editor. POST/PUT /formsno longer overwrites a form’ssettings.notifications/settings.confirmationsunless the body carries"allowNotifications": true. When allowed, recipient addresses are validated withis_emailandsender_addressmust be on the site’s own domain, and message bodies are stripped of active content. NewFormNotificationException(422). This closes the confirmed notification-hijack: repointing every submission to an attacker inbox with a spoofed sender.POST/PUT /seo/redirectsrejects aurlTothat points off this site unless the body carries"allowExternal": true, and constrainsstatustoactive/inactive. NewInvalidRedirectException(422).
- New
2026.9.1
Patch Changes
- 755b390: Added
hooks/, a new resource for declaring WordPress actions, filters, and scheduled (cron) tasks as plain PHP files, deployed withlps hook push/pull/list(andlps push/pull/diff,hook_push/hook_pull/hook_listMCP tools). One file, one class, public methods attributed with#[Action(hook, priority, acceptedArgs)],#[Filter(hook, priority, acceptedArgs)], or#[Cron(recurrence, hook?)]bind straight toadd_action()/add_filter()/WP-Cron (a#[Cron]job is just an action bound to a schedule instead of an existing WordPress event). Unlike a REST route, a bound hook runs unconditionally for every visitor with no permission check of its own, so every callback is wrapped to catch and log rather than propagate; a filter additionally fails open, returning the original value on a throw. Loopress Full only, same as custom API routes. - f0fe762: Added
options/, a new resource for tracking individual WordPress options (wp_optionsrows) directly, agnostic of which plugin owns them: no adapter to write, unlikeseo/which has to know Yoast’s vs RankMath’s option names and shape.lps option listshows every option name and autoload flag on the site (never values, so browsing stays cheap and never leaks a value by accident),lps option add <name>starts tracking one as a local file, andlps option pull/push/diff/removework the tracked set from there (pushis upsert-only, it never deletes an untracked option). Two safety rails:active_plugins/template/stylesheetare refused outright (already owned by theplugin/themeresources), and environment-owned or WordPress-generated options (siteurl,home,db_version,cron,rewrite_rules,WPLANG) default to"readonly": truein their local file, tracked and diffable but skipped bypushunless explicitly overridden. Mirrored asoption_push/option_pull/option_list/option_add/option_removeMCP tools. - 55b5d6d: Removed the snippet provider migration feature: the Snippets admin tab and the
GET/POST /loopress/v1/snippets/migration/{direction}REST routes are gone. Code Snippets and WPCode each ship their own import/export, so moving snippets between them no longer needs a Loopress screen. Snippet sync (lps snippet pull/push/listand the/loopress/v1/snippetsroutes) is unchanged. - 1da6d44: Internal refactor: deduplicated REST error-response handling (
AcfController, part ofComposerController), the filesystem bootstrap snippets shared byApiDirectory/AppsDirectory/LoopressEnvironment,composer.lock/JSON-output parsing inComposerService, and the “exactly one active provider” arbitration duplicated identically across the SEO/Snippets/Forms services. No behavior change.
2026.9.0
Minor Changes
- e505f00: Add single-page app hosting (Loopress Full only). A built SPA bundle uploaded over the REST API lands in
wp-content/loopress/apps/<name>/: assets are PUT one at a time (per-file size cap, only changed files sent), then one POST/commitflips theloopress_appsoption so the front end serves the new build atomically. The[loopress_app name="..."]shortcode enqueues the build’s content-hashed entry files and prints the mount point the SPA attaches to. Files from the immediately previous build are kept one generation so an in-flight visitor does not 404 on a lazy chunk. Hash routing only; static assets only (.phpand other server-executable extensions are rejected). A read-only “Apps” tab in the plugin admin lists what has been deployed. - d213206: Add Composer-backed plugin/theme installs (Loopress Full only).
POST /loopress/v1/composer/synctakes{ intent, lock, force }, renders a plugin-ownedcomposer.jsonfrom the intent (libraries + WPackagist plugins/themes), runs Composer against the WPackagist repository, and returns the effectivecomposer.json,composer.lock, and the list of removed packages. It returns 422 (unmanaged_plugins_present) when the intent references a plugin/theme folder installed by hand, unlessforceis set.LoopressEnvironmentscaffolds and migrates the sitecomposer.jsonwith the WPackagist repository,composer/installers, and the installer-paths that place plugins/themes underwp-content/.
Patch Changes
- eb79278: Fixed
wp-content/loopress/vendor/(the site-wide Composer dependencies managed by the Dependencies feature) being fully reachable over HTTP: unlike the plugin’s own bundledvendor/, it had no.htaccessor anti-listingindex.phpat all, exposingvendor/composer/installed.json(the full dependency tree and exact versions) to anyone who requested it. It now gets the same deny-all.htaccesswritten on first use. Since.htaccessonly works where the webserver actually reads it (nginx ignores it outright, and some Apache hosts disableAllowOverride), the Composer diagnostics panel now also makes a live HTTP check against that file and surfaces avendor_publicly_accessiblewarning if it’s still reachable.
2026.8.1
Patch Changes
-
b48b4b5:
api/route files no longer need their class name to match a formula derived from the filename (kebab-case filename -> PascalCase class): the class is now discovered by reading the file itself (PHP’s own tokenizer, never executed to find out), so it can be named anything. Internal, breaking: this replaces the old naming convention entirely, no transition period.lps api pushnow also rejects a file immediately (before anything is written) if it declares zero or more than one class, or if its class name collides with WordPress core, another active plugin, or anotherapi/file already on the site, instead of only failing silently at the next site boot. A file that still fails to load at boot (a stale collision from a plugin activated later, for instance) now shows a warning in the plugin’s API Routes admin tab, with the actual reason, clearing itself automatically once the file loads clean again. -
8e43b47: Admin page styles are now enqueued (
wp_add_inline_style) instead of echoed as raw<style>tags, and the Loopress top-level menu item moved from position 6 to 100 so it stops competing with WordPress core’s own menu hierarchy. Both were flagged by the wordpress.org plugin review.
2026.8.0
Minor Changes
-
182ae2a:
api/route files: the anti-listingindex.phpis now recreated on every boot if missing, not only whenlps api pushhappens to write a file, so a Git-based deploy that never goes throughlps api pushstill gets it. Also logs (without blocking the route) when a file is missing its ABSPATH guard, which is expected for any file deployed outsidelps api pushsince the guard is only ever injected at push time and stripped again on pull. -
182ae2a:
api/route files can now use a bracketed segment,[order_id], anywhere in their path (e.g.api/invoice-pdf/[order_id].php,api/orders/[order_id]/items/[item_id].php) to capture a dynamic value into$request->get_param(...), the same convention as Astro/Next.js dynamic routes, without a catch-all segment. The segment name must start with a letter or underscore (it becomes a PHP identifier internally). The generated class name PascalCases each path segment and joins them with_(InvoicePdf_OrderId, notInvoicePdfOrderId), so two differently nested files can never collide on the same class name.lps api push/pull/listnow support route files nested in subdirectories, needed for the above. Internal, breaking: the upload endpoint (PUT loopress/v1/api-files) now takesfilenameas a body field instead of a URL path segment (avoids depending on how a given host handles a percent-encoded slash in a URL), so the CLI and the WordPress plugin must be upgraded together, an old CLI against a new plugin (or the reverse) will fail to push. -
182ae2a: New
wp-content/loopress/lib/directory, autoloaded under theLoopressLib\namespace (via the same Composer setup aswp-content/loopress/vendor/), for code shared betweenapi/route files, permission checks and formatters reused across several files, without turning that shared code into a route itself. Existing sites get the new autoload entry migrated intocomposer.jsonautomatically, with adump-autoloadto make it take effect immediately rather than only on the next unrelated Composer operation. -
182ae2a:
api/route files can now declare#[Permission]on a verb method or on the class, for per-verb authorization (a publicget()next to an admin-onlypost(), for example) instead of onepermission()covering every verb. Resolution order: attribute on the verb, attribute on the class, the file’spermission()method, the closedmanage_optionsdefault.#[Permission(callback: ...)]can point to a shared static method, reusable across several route files, with the same fail-closed behavior on a throw aspermission(). Combining more than one ofpublic,capability, orcallbackon the same attribute is rejected (the route fails to register, logged, rather than silently picking one). -
67a932e:
api/route files:permission()is now called directly by WordPress as the route’spermission_callback(permission(WP_REST_Request $request): bool), instead of being called at registration to produce a callable. Breaking: the oldpermission(): callable { return fn(): bool => ...; }form no longer works, update any custom route that overridespermission(). A throw insidepermission()now fails closed (denies that request) instead of skipping the whole route file at boot.Also logs when an
api/file has no public HTTP verb method (get/post/put/patch/delete), previously silent and indistinguishable from a route that intentionally has none yet. -
182ae2a:
lps api push’s server-side PHP syntax check now distinguishes “verified, no error” from “couldn’t verify here” (exec()disabled, common on managed hosts, or another local condition preventing the check from running), instead of treating both as silent success. The CLI now reports when the check was skipped for the second case instead of staying indistinguishable from a fully verified push.
2026.7.16
Patch Changes
-
af9c109: Bug fixes from the QA backlog (5th pass), lot 1:
- SEO sync (
YoastService/RankMathService) no longer writes arbitrary metadata keys from the request body: the write loop is now bounded to the provider’s own prefix, symmetrically with the delete loop right below it. Not exploitable beyond the existingmanage_optionstrust boundary, but closes a defense-in-depth gap where a bug elsewhere, or a future feature reusing this endpoint, could silently overwrite metadata belonging to another plugin (ACF, FluentCRM, etc.) on a published post or page. uninstall.phpno longer fatals whenvendor/is missing (a dev checkout symlinked intowp-content/plugins/withoutcomposer install). The cleanup was already best-effort, it’s now skipped gracefully instead of fataling.- Corrected an outdated claim that ACF options pages require ACF PRO, in both a code comment and the error message shown when an object type isn’t registered. Confirmed working with Secure Custom Fields (the free fork recommended by WordPress.org) during the 5th QA pass; the error message now points at the real fix (
acf_add_options_page()) instead of a PRO requirement.
- SEO sync (
2026.7.15
Patch Changes
- 6dac6c3: Fixed a fatal error on every Loopress Light install (
Class "DI\ContainerBuilder" not found): the Light build shipped an empty Composerrequire, but shared code (ContainerFactory,WpHttpClient) depends onphp-di/php-di,nyholm/psr7, andpsr/http-client. The Light build now keeps those, and only excludes the genuinely Full-only packages (composer/composer,sentry/sentry). - 6dac6c3:
LOOPRESS_VERSIONis now read from the plugin’s ownVersion:header viaget_file_data()instead of a hardcoded literal, removing a second copy thatscripts/sync-version.jshad to keep in sync and could drift from the header.
2026.7.14
Patch Changes
- dd36268:
LOOPRESS_VERSIONis now read from the plugin’s ownVersion:header viaget_file_data()instead of a hardcoded literal, removing a second copy thatscripts/sync-version.jshad to keep in sync and could drift from the header.
2026.7.13
Patch Changes
- fae069f: Fixed errors reported by the WordPress Plugin Check tool: escaped the exception message in
WpHttpClient::sendRequest(), and bumped the readme’s “Requires at least” header to 6.2 so the RankMath redirects queries can keep using the%iprepare placeholder.
2026.7.12
Patch Changes
-
7e28650:
api/route files can nowusepackages installed via the Composer feature (wp-content/loopress/vendor/) directly, without a manualrequire_onceof the autoloader. Previously this only happened to work by coincidence, as a side effect of the unrelated Dependencies feature loading it first for its own diagnostics;RouteLoadernow loads it explicitly and independently, and a broken user autoloader is caught and logged instead of breaking everyapi/route. -
827af5b: The REST namespace
api/route files register under (loopress-api/v1by default, e.g.hello.phpbecomesloopress-api/v1/hello) is now configurable from the plugin’s Settings tab instead of hardcoded. Existing sites keep the same default namespace and routes; only sites that explicitly set a custom value change behavior. -
5658d6c: Fix three bugs found during manual QA. Pushing a snippet with a location unsupported by the active provider (WPCode) used to create the snippet anyway with the location silently defaulted, while still reporting failure to the CLI; since the CLI never learned the resulting id, retrying the push (the natural reaction to a reported failure) created another duplicate snippet each time.
WPCodeSnippetProvidernow validates the location before any write, so an invalid location is rejected with nothing created. A pushedapi/route file with a real PHP parse error was accepted and listed as present bylps api push/list, while the route silently 404d at request time;ApiFilesController::push_file()now runs a real PHP syntax check before writing, rejecting with a clear 400. SEO endpoints (lps seo) returned a generic 500 for client-actionable conditions (multiple SEO plugins active, redirects unsupported by the active plugin) instead of the 409/400 used by the equivalent snippets/forms guards; dedicated exceptions now map these correctly, andget_settings()/update_settings()(which had no error handling at all before this) are covered too.Also fixes
lps composer init’s generated scaffold, which requiredcomposer/installerswithout allow-listing it, so Composer 2.2+’s non-interactive plugin-trust gate blocked every realcomposer pushthrough it. Error messages surfaced from a WordPress REST failure now include the server’s full detail (e.g. the actual Composer trace) instead of just a generic summary, making failures like this one diagnosable from the CLI’s own output.
2026.7.11
Patch Changes
- 2b6420c: Adds custom API routes: a versioned
api/folder (lps api push/pull/liston the CLI) lets a project expose its own WP REST endpoints without hand-writing a plugin, deployed straight towp-content/loopress/api/on Loopress Full and registered underloopress-api/v1/<filename>. One file, one class, one method per HTTP verb (get/post/put/patch/delete), resolved by filename convention (kebab-case -> PascalCase). Defaults tomanage_options+ Application Password auth like every other sync route, with explicit per-file overrides forpermission()(e.g. an anonymous headless form endpoint) andheaders()(CORS, including the OPTIONS preflight). Filenames are allowlisted against path traversal, deployed files get an auto-injectedABSPATHguard against direct HTTP access, and a bad file (parse error, class collision, throwingpermission()/headers()) is skipped and logged rather than fatalingrest_api_initor breaking other routes. Full only, like snippet sync: Light stays ACF+SEO only. - 063616e: Loopress Full’s Sentry error reporting is now opt-in. Until an admin decides either way, a banner (“Send crash reports to Loopress?”) shows on every tab of the admin page with Allow/Deny buttons; once decided, a switch in the new Settings tab reflects and lets you change the choice. The Sentry PHP SDK’s global handlers don’t install at all until consent is given. Backed by
GET/PUT loopress/v1/sentry/consent, storing the choice in a WordPress option. A new “Reset all settings to default” button (DELETE loopress/v1/settings, global to all Loopress settings, not just Sentry) clears it and brings the banner back. Existing installs upgrading into this send nothing until an admin opts in. - 063616e: Loopress Full now initializes the Sentry PHP SDK on boot, reporting PHP errors and exceptions from the plugin’s own code so they can be triaged across every install. Filtered via a
before_sendcallback that only keeps events whose stack trace passes through this plugin’s own files, so a site’s Sentry project never fills up with errors from its theme or other plugins. Loopress Light doesn’t have this:src/Sentry/is stripped at build time like the other Full-only features. Currently a scaffold pending the real Sentry project DSN, an unset DSN is a documented no-op for the SDK, so this ships inert until then. - f542f91: Code snippet sync (Code Snippets, WPCode) moved from Loopress Light to Loopress Full, alongside Composer dependency management. wordpress.org’s final decision on the appeal rejected Loopress Light’s snippet sync REST endpoints as a remote arbitrary-code-deployment mechanism, regardless of the authentication and capability checks in front of them. Loopress Light now syncs only ACF field groups and SEO settings (Yoast, RankMath);
lps snippet pull/pushand the snippet migration UI require Loopress Full. REST routes are unchanged (loopress/v1/snippets*), so existing CLI versions keep working against Loopress Full. - 9383f6e: Adds a WordPress form-sync feature:
lps form list/pull/pushon the CLI, backed by new REST routes underloopress/v1/formson Loopress Full (Light stays ACF+SEO only). The plugin side introduces a genericFormProviderabstraction, mirroring the existing snippet sync (Code Snippets/WPCode), with WPForms as the first supported plugin; more WordPress form plugins can be added as additional providers later. Forms are addressed by numeric id (no ACF-style stable key), pulled/pushed as one<id>-<slug>.jsonfile per form, with orphan cleanup on pull and the same PUT-then-404-fallback-to-create dance aslps snippet push.
2026.7.10
Patch Changes
- 512e72b: Loopress Full now checks GitHub for newer releases and shows an admin notice (“Loopress Full X is available, you are running Y”) with a link to the release when one exists. Read-only for now: no download or install, you still update manually via loopress.dev, same as before. Backed by a new
GET loopress/v1/updateendpoint. Loopress Light doesn’t have this: WordPress.org reserves update-checking for its own review-and-update flow, so it stays out of that edition. - 512e72b: Remove wp-admin’s default left padding on the Loopress admin page only (scoped to that page, every other wp-admin screen is unaffected), so the plugin’s own page layout controls its own spacing instead of being padded twice.
2026.7.9
Patch Changes
-
6f5712c: Add
lps acf pull/lps acf push/lps acf list, which sync ACF (Advanced Custom Fields) field groups, post types, taxonomies, and options pages between the WordPress site and local JSON files, the same git-based workflow already available for snippets and Composer dependencies.Backed by new
loopress/v1/acf/*endpoints on the WordPress plugin. Requires ACF to be installed and active; options pages additionally require ACF PRO. -
6c557ec: Add
lps composer init, which scaffolds a composer.json wired to WPackagist (repository,composer/installers, installer-paths) so WordPress.org plugins and themes can be added and installed through Composer instead of the native plugin API.lps composer pullnow also pullscomposer.json(previously onlycomposer.lock), backed by a newGET loopress/v1/composer/jsonendpoint on the WordPress plugin, so local composer.json stays in sync with packages added or removed through the Loopress admin page.
2026.7.8
Patch Changes
- 3296877: Loopress Light now links to the full documentation (https://docs.loopress.dev/wordpress-plugin/) instead of the bare loopress.dev when mentioning Loopress Full, in both the readme (description and FAQ) and a new note on the plugin’s own settings page, and states explicitly that Loopress Full is free. Also fixed ambiguous wording that read as “not distributed from loopress.dev” when the opposite is true.
2026.7.7
Patch Changes
- 3acd857: Fixed a WordPress Plugin Check text domain mismatch on both editions: the source carried
loopress, a leftover text domain from before the light/full split that matches neither edition’s real slug.scripts/build-flavor.cjsnow rewrites theText Domainheader and translation calls toloopress-light/loopress-fullper edition at build time. Also realigned bothreadme.txtchangelogs withCHANGELOG.md, the source of truth: they had drifted with fabricated2026.8.0and2026.6.0entries that don’t exist in the real release history.
2026.7.6
Patch Changes
- 31a82f6: The WordPress plugin now builds two editions from the same codebase: Loopress Light (
loopress-light.zip, wordpress.org) keeps snippet synchronization (Code Snippets / WPCode); Loopress Full (loopress-full.zip, loopress.dev only) adds Composer dependency management (install, remove, audit, diagnostics, CLI sync) on top. Loopress Light contains no Composer code, even inactive; Loopress Full fully replaces Loopress Light on activation, both editions never run side by side. REST routes are unchanged (loopress/v1/snippets*andloopress/v1/composer*, the latter only present in Loopress Full), so existing CLI versions keep working.
2026.7.5
Patch Changes
- 887b59f: Fixed all errors reported by the WordPress Plugin Check tool: escaped dynamic exception messages across the Composer and snippet provider services, added the missing
ABSPATHdirect-access guard inloopress.php, removed the trackedassets/.gitkeephidden file (theprebuildscript now creates the directory itself), and bumped the readme’s “Tested up to” header to 7.0.
2026.7.4
Patch Changes
- a85959e: Fix two bugs in the Code Snippets provider:
deleteSnippet()was missing the leading slash required byWP_REST_Server::dispatch(), soDELETE /wp-json/loopress/v1/snippets/{id}always failed with “Snippet not found” instead of deleting anything. Separately,getSnippets()/getSnippet()returned trashed snippets indistinguishably from active ones (Code Snippets’ own REST API doesn’t filter them out), causinglps snippet pullto re-import a snippet the user had just deleted from wp-admin.
2026.7.3
Patch Changes
- 9a8f6b5: Removed
PluginService,PluginController, and theloopress/v1/plugins*REST routes. Plugin management now relies entirely on WordPress core’s nativewp/v2/pluginsREST API, which the Loopress CLI calls directly.
2026.7.2
Patch Changes
- 3f04ab4: Replace md5 hash function by native hash function with sha256
- 13c5fb9: Add tab on the UI and diagnostics panel
- 1dc1228: Unify snippet route
2026.7.1
Patch Changes
- 6122ddc: Align better sidecar file with WPCode api
2026.7.0
Minor Changes
- 25ece79: 1st release of the plugin containing basic UI fordependencies installation. It also contains endpoints to let the CLI interact with Wordpress
@loopress/mcp
0.33.0
Minor Changes
- 0bf479c: Dynamic blocks (Loopress Full):
lps block push | pull | list | diff | rollback | rmand theblock_*MCP tools deployblocks/<slug>/folders (block.json+render.php, optionallystyle.cssandview.js) registered as the server-rendered blockloopress/<slug>. Each block’s CSS and JS load only where it renders, it works inside a Query Loop, and arender.phpthat throws renders nothing instead of breaking the page.lps push,lps pull,lps diffandlps validateinclude them.
Patch Changes
- 47d5a68: Consistency fixes from the 2026-10-27 audit.
lps apiandlps hookhelp examples pass the directory as a positional argument (there is no--pathflag), and the public-route warning links to docs.loopress.dev.lps --helpdescribes theapp,form,hook,menu,optionandpagetopics. The MCP server reports its real version, and its missing-project error nameslps init. Docs: API route and hook pushes are all or nothing, the declared resources appear in the workflow page, plus the snippet sidecar fields, theproject configprompts and the push exceptions that do delete. - 4613ded: Loopress Light no longer breaks the aggregate commands.
GET loopress/v1/capabilitiesnow lists the site’smodules, andlps pull,lps push(dry run included),lps diffand solps promoteskip a Full-only step (“needs Loopress Full”) when the project has no files for it. When it does,lps pushrefuses before pushing anything,lps pullandlps diffreport it.lps pushandlps theme pushskip templates and parts when the project has none, even withactivateChildTheme(uselps theme activatefor an empty child), andlps theme template pushalone refuses with “Nothing to push”. The removed SEO module warning is gone. - Updated dependencies [47d5a68]
- Updated dependencies [0bf479c]
- Updated dependencies [cbe0bd9]
- Updated dependencies [b038a67]
- Updated dependencies [4613ded]
- Updated dependencies [f24e50f]
- Updated dependencies [da7d681]
- Updated dependencies [b698800]
- @loopress/cli@0.33.0
0.32.2
Patch Changes
- ac984c0:
lps diff, push dry runs and the outside-edit check no longer report drift on ACF field groups or Global Styles that are in sync. ACF stores every setting with its default (wrapper,aria-label…), which a hand- or agent-written field group leaves out: the local files are now compared as the site would store them, through a new read-only plugin route (POST loopress/v1/acf/normalize/<type>) that runs them through ACF’s own validation without saving anything. Global Styles now compare a preset list and its origin-keyed form (palette: [...]andpalette: {custom: [...]}) as the same, and avar:preset|color|basereference as itsvar(--wp--preset--color--base)expansion, the two forms WordPress rewrites on save. With a plugin older than this release, ACF files are compared as written, as before. If the site can’t be read for the outside-edit check after an earlier push, the push now stops instead of skipping the check. - Updated dependencies [ac984c0]
- @loopress/cli@0.32.2
0.32.1
Patch Changes
- 26fb36c:
lps validateandlps hook pushno longer reject a hook whose method outputs a template (?> <p>...</p> <?phpinside the class) as “code outside the class”: only inline HTML at the top level of the file is flagged, as the plugin does. - Updated dependencies [26fb36c]
- @loopress/cli@0.32.1
0.32.0
Minor Changes
- 35310b3: The MCP server now tells a connecting agent what a Loopress project is and links the documentation of each file format, so an agent writes a Loopress hook instead of plain WordPress code.
lps validate(and the MCPvalidate_localtool) now runs the same checks on hook and API route files as push: a valid file name,declare(strict_types=1);exactly once, and no code outside the class (a hand-writtenABSPATHguard included, the plugin adds its own), anddeclare(strict_types=1);as the first statement. Those problems now surface before push; PHP syntax and class-name collisions are still checked by the server at push. - ad06a9a:
lps page pushcan now take over a page with the same slug that Loopress did not create, instead of refusing it. It lists those pages and asks once, or takes them over directly with--overwrite(required in CI and through the MCP server). The page keeps its URL and ID, so menus, links and SEO data keep working, and its original content is saved as a WordPress revision first. - ad938a6:
lps pushandlps pull(and solps promote,lps content copy,push_allandpull_all) now include Global Styles once the project tracks them, on a site whose active theme is a block theme, and skip them with a one-line note otherwise. Theme versions stay withlps theme push. ACF, forms and snippets are skipped when the project has no directory for them, solps push --dry-run(and thepush_allpreview) no longer fails with a 409 on a site without a form plugin, andlps diffno longer reports such a resource as inconclusive. - df54b2d: A push’s dry run now says what it would change: each local item is
created,changedorunchangedagainst the site, in the human output and underchangesin--json(existing fields are kept).lps acf pushalso names the fields added, changed or removed in a changed field group, andlps theme template pushlists templates, parts and CSS files separately, withremovedfor a file the child theme would drop.lps pushandlps theme pushnow return each step’s own result. The MCP push tools’ previews are these results, and their descriptions now tell the agent so. The plugin’s child theme endpoint reports a hash of each CSS file, so the dry run can tell whether the theme CSS changed.
Patch Changes
- Updated dependencies [3f3beb9]
- Updated dependencies [35310b3]
- Updated dependencies [ad06a9a]
- Updated dependencies [ad938a6]
- Updated dependencies [df54b2d]
- Updated dependencies [515153d]
- @loopress/cli@0.32.0
0.31.0
Minor Changes
- 60ef881: New
content_copytool, the equivalent oflps content copy. - ad4500c:
push_all,page_pushand every push tool with a matching_rollbacktool take an optionaloverwriteboolean (the CLI’s--overwrite): without it, a confirmed push over a change made on the site outside Loopress fails and names what it would overwrite.
Patch Changes
- 90779f2:
project_promotetakes an optionaloverwriteboolean, passed tolps promote --overwrite. - Updated dependencies [60ef881]
- Updated dependencies [5bb3a54]
- Updated dependencies [ad4500c]
- Updated dependencies [90779f2]
- @loopress/cli@0.31.0
0.30.0
Minor Changes
- a1a5147: New
cpt_push,cpt_pull,cpt_list,cpt_rmandcpt_rollbacktools for custom post types, andproject_diffacceptscpt. - 049d127: New
meta_push,meta_pull,meta_list,meta_rmandmeta_rollbacktools, andproject_diffacceptsmeta. - 22894f6: Breaking: the
seo_list,seo_pull,seo_pushandseo_rollbacktools are removed, following the removal oflps seo. Track SEO plugin settings withoption_add(rank-math-options-titlesorwpseo_titles) instead. - cd4b340: New
role_*andpattern_*tools (push, pull, list, rm, rollback), andproject_diffacceptsroleandpattern. - 66790cb: New
taxonomy_push,taxonomy_pull,taxonomy_list,taxonomy_rmandtaxonomy_rollbacktools, andproject_diffacceptstaxonomy. - 7139fd8:
template_pushnow also pushes the theme CSS (theme/css/,theme/blocks/) to the Loopress child theme.
Patch Changes
- aa60465: Commands no longer remember an environment. Without
--env, they target the project’slocalenvironment, or its only environment when it has just one. A project with several environments and nolocalone needs--envon every command.lps project switchnow picks a project only, andlps project listtags the default environment with[default]instead of an arrow on the switched one (isDefaultreplacesisCurrentper environment in--json). An existingconfig.jsonkeeps working: theenvfield ofcurrentProjectis ignored and dropped on the next write. - Updated dependencies [a1a5147]
- Updated dependencies [95d9f8c]
- Updated dependencies [aa60465]
- Updated dependencies [049d127]
- Updated dependencies [22894f6]
- Updated dependencies [03d3da0]
- Updated dependencies [cd4b340]
- Updated dependencies [3bbe20d]
- Updated dependencies [b87fa13]
- Updated dependencies [66790cb]
- Updated dependencies [7139fd8]
- Updated dependencies [5fdd7ee]
- @loopress/cli@0.30.0
0.29.0
Minor Changes
-
cc5ede1:
lps plugin pullno longer adds every plugin installed on the site. Plugins the project already tracks are refreshed, and the others are offered as a list to tick, unticked by default, so a pull from production doesn’t bring its cache, security or backup plugins into the project. Without a terminal (--yes,--json, CI, the MCP server), only tracked plugins and those named with the new--plugin <slug>flag are written; the MCPplugin_pulltool takes them asplugins.Plugins under
require-devincomposer.jsonare now installed on thelocalenvironment bylps plugin pushandlps composer push, and still never on a remote site: use it for Query Monitor, or for a premium plugin’s free edition while production runs the paid one. -
bee1b73: Breaking: everything theme related now lives under
lps theme.lps template push/list/diffis nowlps theme template push/list/diff, and the versions-onlylps theme push/pullis nowlps theme version push/pull.lps theme pushnow pushes the whole topic in order (versions, then templates and parts, then Global Styles), andlps theme pullpulls versions and Global Styles.lps theme style pushaccepts--yes. In the MCP server,theme_push/theme_pullnow wrap the whole topic, the newtheme_version_push/theme_version_pullkeep the versions-only behavior, and thetemplate_*tool names are unchanged. Thetemplateandpartresource names forlps diff --only/--skipare unchanged.
Patch Changes
- cc5ede1: A push’s
--dry-runnow reads the target site, the same waylps diffdoes, instead of staying local. It used to announce ACF, SEO, forms or snippets as pushed on a site that would refuse them (plugin missing), and the real push then failed; the dry run now fails the same way. The MCP server’s preview and pre-apply check, built on the dry run, benefit too.lps push --dry-runalso says “would push” instead of “pushed”. - 69ed247:
lps acf,lps seo,lps form,lps menuandlps theme stylepush/pullnow accept--json. The MCP server runs every command with--json, soacf_push,acf_pull,seo_push,seo_pull,form_push,form_pull,menu_push,menu_pull,theme_styles_pushandtheme_styles_pullalways failed with “Nonexistent flag: –json”. A test now checks that every command the MCP server can call supports--json. - 69ed247: The MCP server now refuses a change to
productioneven whenenvis omitted andproductionis the active environment. Before, only an explicitenv: "production"was refused, sohook_rm,api_rm, a*_rollbackor apushwithprunecould reach production without any human confirmation. A mutating tool called withoutenvnow resolves the active environment first and pins it, so the confirmed call also applies to the environment that was previewed, even if the active one is switched in between.lps status --jsonreports the resolvedenvironmentname for this. - 69ed247:
project_diffnow acceptstemplate,partandpageinonly/skip, likelps diff --only. - Updated dependencies [cc5ede1]
- Updated dependencies [426df4b]
- Updated dependencies [69ed247]
- Updated dependencies [69ed247]
- Updated dependencies [cc5ede1]
- Updated dependencies [69ed247]
- Updated dependencies [cc5ede1]
- Updated dependencies [b77aafe]
- Updated dependencies [e57f3a0]
- Updated dependencies [426df4b]
- Updated dependencies [bee1b73]
- Updated dependencies [a3c5097]
- @loopress/cli@0.29.0
0.28.0
Minor Changes
- cd9479d:
lps app pushnow builds each app before pushing it: whenapps/<name>/package.jsonhas abuildscript, it runs with the app’s package manager (frompackageManager, else the nearest lockfile, else npm), on--dry-runtoo, so a forgotten rebuild no longer ships a staledist/reported as “up to date”. A failing build fails that app and uploads nothing. Pass--no-build(MCP:noBuild) when CI already built. Dependencies are never installed for you. - 20a406c: Block templates and parts:
templates/<slug>.htmlandparts/<slug>.htmlare written bylps template pushas the files of a child theme of the active block theme,<parent>-loopress, the parent staying untouched. Any template of the hierarchy can be overridden, and a template with atitle/postTypesheader becomes a page template a static page picks with itstemplateheader (no header or footer part gives a bare page). Edits made in the Site Editor are never overwritten and show up as drift inlps template diffandlps diff. Part oflps push(before pages),lps dev,lps initandlps validate. MCP gainstemplate_push,template_listandtemplate_diff. - 61350a3: Breaking:
lps theme-styles pull/push/diff/rollbackis nowlps theme style pull/push/diff/rollback, grouped with the other theme commands. Thetheme-stylesresource name forlps diff --only/--skipand the MCP tool names (theme_styles_*) are unchanged, and existing rollback snapshots still work.
Patch Changes
- Updated dependencies [cd9479d]
- Updated dependencies [4d1c8af]
- Updated dependencies [4d1c8af]
- Updated dependencies [eb417c3]
- Updated dependencies [20a406c]
- Updated dependencies [b1c46ac]
- Updated dependencies [61350a3]
- @loopress/cli@0.28.0
0.27.0
Patch Changes
-
bb32133:
lps devnow also watcheshooks/and pushes hook files on change. A request the Loopress plugin refuses on purpose with a 403 (for example reading a secret-looking option) now shows the plugin’s own reason instead of “Authentication failed, check your credentials”, and HTML entities in server messages (") are decoded. Errors from the Loopress cloud API now show the API’s own message (validation errors included) instead of a generic “Request failed with status code 400”.lps validatenow checks JSON files at any depth (ACF’sacf/<type>/, SEO’spost-meta/andredirects/were silently skipped before), tracked option files (a missingname,autoloadorvalueis reported), and the theme styles directory.lps plugin addandlps theme addsupport--json, and the MCP server gets matchingplugin_addandtheme_addtools (localloopress.jsonedit only).Rollback snapshots (
.loopress/snapshots/, inside your project) hold the environment’s full remote state; the.loopress/directory now writes its own.gitignoreso they are never committed by accident.lps push,lps pullandlps promotewith--jsonnow print a single valid JSON document: the progress output of the resource commands they run goes to stderr instead of polluting stdout, which broke the MCPpush_all,pull_allandproject_promotetools. A failed aggregate run now lists each failed resource and its reason. -
e042cde: Fix the MCP server on Windows: every tool failed with
ENOENTbecause it spawnedlpswithout a shell, and Windows installs it aslps.cmd, which Node refuses to run directly. The server now starts the installed CLI’sbin/run.jswith its own Node, falling back tolpsonPATHthroughtinyexec, which also works on Windows.LPS_BINalso accepts a.jsfile.@loopress/clinow exports itspackage.json, and the MCP server now declares Node 22 or later, like the CLI.lps initnow adds* text=auto eol=lfto.gitattributes, so a checkout on Windows (CRLF by default) keeps the same bytes as whatpullwrites. -
Updated dependencies [1cd51fc]
-
Updated dependencies [bb32133]
-
Updated dependencies [d043549]
-
Updated dependencies [bb32133]
-
Updated dependencies [e042cde]
- @loopress/cli@0.27.0
0.26.0
Minor Changes
- b60df46: Adds
lps page push [SLUG],lps page listandlps page difffor static HTML pages (pages/<slug>.html,pageDirinloopress.json), with an optional<!-- title: ... / status: draft|publish -->header. Pages are part oflps push,lps diff --only page,lps devandlps init, and are checked bylps validate. The MCP server gets matchingpage_push,page_listandpage_difftools, and a CLI command that exits non-zero with a regular JSON payload (a diff reporting drift) is now returned as a result instead of an error. Requires Loopress Full.
0.25.0
Minor Changes
-
3ec619c:
lps doctornow supports--json, returning a structured{project, environment, url, checks, ok, pluginVersion}result instead of throwing on failure (its human-readable output and exit code are unchanged).The MCP server gains three read-only tools that were missing from CLI parity:
project_diff(drift between local files and an environment, or between two environments),project_doctor(connectivity, plugin and credential diagnostics), andvalidate_local(checks local tracked files without contacting WordPress). -
1607fc5: Adds a
menuresource for syncing WordPress navigation menus and menu locations. The plugin exposesloopress/v1/menusandloopress/v1/menu-locationsREST endpoints, resolving menu items by identity (post type/taxonomy + slug) rather than raw object ids so they remain portable between environments. The CLI gainslps menu list/pull/push/diff, wired intolps push/lps pull/lps diff/lps initand local file validation, and the MCP server gains matchingmenu_push/menu_pull/menu_listtools. -
10d18ec:
lps push,lps pull, andlps promotenow support--json(each returns a structured per-resource result instead of erroring with “Nonexistent flag: –json”). This was a real bug in the MCP server’spush_allandpull_alltools, which always shell out with--jsonand so always failed.The MCP server also gains
project_promote, the tool forlps promotethat CLI parity was missing. -
ce19505: Adds
lps <resource> rollbackfor the 9 resource-state-backed resources (snippet, form, acf, api, hook, seo, menu, option, theme-styles).lps <resource> pushnow writes a snapshot of the environment’s state (before and about-to-be-pushed) to a local, gitignored cache (.loopress/snapshots/<resource>/, capped at the last 10 per resource) right before every real push.lps <resource> rollbackrestores the most recent snapshot (or--to <id>),--listshows what’s available. It refuses to overwrite a change made to the environment since the original push unless explicitly confirmed (interactive prompt, or--yes), the same shape as the production-push guard. No WordPress-side capability is required: restoring materializes the archived state into the resource’s own local file layout, then delegates to the existing push command.The MCP server gains a matching
<resource>_rollbacktool per resource, following the existing confirmToken handshake (list: truebypasses it as a plain read). -
4432466: New
lps theme-styles pull/push/diffcommand group syncs the active block theme’s Global Styles customizations (Site Editor > Styles: colors, typography, spacing) as a localtheme/<stylesheet>-global-styles.jsonfile, reusing WordPress core’s ownwp/v2/themesandwp/v2/global-styles/<id>REST endpoints (no new PHP controller needed). Classic (non-block) themes fail the command clearly instead of syncing nothing silently. This resource is included automatically inlps diff, but deliberately left out of the aggregatelps push/lps pull(likelps theme), so a classic-theme site’s routine sync isn’t broken by this opt-in feature.The MCP server gains matching
theme_styles_pullandtheme_styles_pushtools, andproject_diffnow coverstheme-stylestoo.
Patch Changes
-
d7aa371: The confirm-token handshake every mutating MCP tool uses now catches remote-state staleness, not just local-file staleness. Previously only a file swapped between the preview and confirmed calls was protected against (via a frozen working-tree copy); a remote change on WordPress in that same window (e.g. someone else editing the environment between a
snippet_rollbackpreview and its confirm) could still be silently overwritten, since the confirmed call trusted the preview’s drift check without re-checking it.The confirmed call now re-runs the same
--dry-runonce more right before applying, and refuses (STALE_PREVIEW) if it reports anything different from what the preview reported, instead of applying against stale data. This applies to every mutating tool generically (push, rm, prune, rollback, …), not just rollback: each tool’s own--dry-runoutput is compared as-is, so tools whose preview doesn’t inspect remote state see no behavior change.
0.24.1
Patch Changes
-
cedc316: Security: the mutating-tool confirm handshake now applies from a frozen copy of the working tree, not a fresh disk read (F28).
The two-call handshake bound the
confirmTokento the CLI arguments, but the apply step re-read the files from disk, up to 5 minutes after the preview the caller approved. A file swapped in that window (a benignsnippets/foo.phpfor a web shell) was pushed under an approval given against the old content.The preview call now copies the working directory to a temp dir (skipping
node_modules,.git,vendor,dist), runs the dry-run against that copy, and the apply runs from the same copy. The snapshot is removed after the apply and on token expiry or eviction. Known gap, documented: anlps --pathoutside the working directory, or aloopress.jsonthat maps a resource directory to an absolute path elsewhere, is still read live.
0.24.0
Minor Changes
- 755b390: Added
hooks/, a new resource for declaring WordPress actions, filters, and scheduled (cron) tasks as plain PHP files, deployed withlps hook push/pull/list(andlps push/pull/diff,hook_push/hook_pull/hook_listMCP tools). One file, one class, public methods attributed with#[Action(hook, priority, acceptedArgs)],#[Filter(hook, priority, acceptedArgs)], or#[Cron(recurrence, hook?)]bind straight toadd_action()/add_filter()/WP-Cron (a#[Cron]job is just an action bound to a schedule instead of an existing WordPress event). Unlike a REST route, a bound hook runs unconditionally for every visitor with no permission check of its own, so every callback is wrapped to catch and log rather than propagate; a filter additionally fails open, returning the original value on a throw. Loopress Full only, same as custom API routes. - f0fe762: Added
options/, a new resource for tracking individual WordPress options (wp_optionsrows) directly, agnostic of which plugin owns them: no adapter to write, unlikeseo/which has to know Yoast’s vs RankMath’s option names and shape.lps option listshows every option name and autoload flag on the site (never values, so browsing stays cheap and never leaks a value by accident),lps option add <name>starts tracking one as a local file, andlps option pull/push/diff/removework the tracked set from there (pushis upsert-only, it never deletes an untracked option). Two safety rails:active_plugins/template/stylesheetare refused outright (already owned by theplugin/themeresources), and environment-owned or WordPress-generated options (siteurl,home,db_version,cron,rewrite_rules,WPLANG) default to"readonly": truein their local file, tracked and diffable but skipped bypushunless explicitly overridden. Mirrored asoption_push/option_pull/option_list/option_add/option_removeMCP tools. - 6b0fda3: Breaking: removed the
pageresource.lps page pull/push/list/diff, thepage_pull/page_push/page_listMCP tools, thepageDirsetting inloopress.json, and thepagesentry forlps dev/lps diff --onlyare all gone.lps push/pullno longer touch pages. WordPress pages are core content editable in wp-admin or overwp/v2/pagesdirectly; keep them under version control with a general-purpose WP-CLI export if needed.
0.23.0
Minor Changes
-
d811b3f: Add
lps app push [name],lps app pull [path],lps app listandlps app remove <name>for syncing built single-page app bundles between a localapps/<name>/directory (aloopress.app.jsonplus a builtdist/) and WordPress.pushbuilds a sha256 manifest ofdist/, asks the site which files it already has, uploads only the differences, then commits the new build in one step; the front end keeps serving the old build until that commit lands.pullwrites each committed app back toapps/<name>/dist/and removes local app directories that no longer exist remotely (confirmed in a terminal, reported in CI). Adds anappsDirkey toloopress.jsonand an “apps” entry tolps init.New MCP tools mirror the commands:
app_push,app_pull,app_list,app_remove(app_push/app_removeuse the dry-run/confirmToken handshake).This is a Loopress Full plugin feature; the CLI and MCP surface is edition-agnostic.
-
dfb33a3: Pin WordPress.org plugin and theme versions through a lockfile, installed on the site with Composer + WPackagist.
loopress.jsonplugins(and the newthemeskey) now take an exact version or"latest", instead of always"latest".lps plugin pushno longer calls WordPress’s corewp/v2/pluginsendpoint (which only installs the current release); it sends an intent to the site, where Loopress Full renders a plugin-ownedcomposer.jsonand runs Composer against the WPackagist repository, landing plugins inwp-content/plugins/at the pinned version, with no SSH and nocomposer.jsonin your repo.lps plugin pull/lps theme pullnow pin every plugin/theme to the version running on the site.lps plugin add --version <v>pins an exact version;lps theme addmirrors it.- New
lps plugin pushflags:--force(allow downgrades, and take over a plugin installed by hand, replacing its files) and--prune(deactivate active plugins absent fromloopress.json). Removing an entry fromloopress.jsonuninstalls the plugin on the next push (shown in the plan, confirmed). - New
lps plugin status/lps theme status: report drift (missing, wrong version, inactive, untracked) and exit non-zero for CI. - New
lps plugin audit: check pinned plugins against a WordPress vulnerability database (wpvulnerability.net) and the WordPress.org plugin API for health signals. - New
lps themecommand group (add,pull,push,status). Loopress manages installed theme versions only, it never switches the active theme. - When a repo has a
composer.json(fromlps composer init), it is authoritative for plugins/themes and theplugin/themecommands defer tolps composer.lps composer pushnow sends an intent (libraries + WPackagist plugins/themes) instead of a verbatimcomposer.json; the plugin owns the file’s shape.
New MCP tools:
plugin_status,plugin_audit,theme_push,theme_pull,theme_status.plugin_pushgainsforceandprune.Limits: WordPress.org plugins/themes only (premium plugins are tracked but never touched); a downgrade replaces files only and does not undo database migrations; multisite is not supported.
This relies on a Loopress Full plugin feature; see the companion changeset there.
Patch Changes
- b3e9d88: Fix
lps plugin pullrecording the wrong slug for a single-file plugin whose WordPress.org slug differs from its bare file id (e.g. Hello Dolly:hello.phphas slughello-dolly, nothello). The wrong slug made a laterlps plugin push --forcefail atomically with a Composer “could not be found” error instead of installing anything. The slug is now read from the plugin’sPlugin URIheader when it points to a wordpress.org listing, falling back to the bare file id otherwise.
0.22.0
Minor Changes
- 2867b83: The MCP server now exposes
acf,seoandformtools (_push,_pull,_list), matching the CLI.acf_*takes an optionaltypearray andseo_*an optionalpostTypearray to scope the operation. - 479525f: The MCP server now exposes
push_allandpull_all, wrappinglps pushandlps pullso an agent can sync every resource in one call instead of eight.push_alluses the same dry-run/confirmToken handshake as the other mutating tools.
0.21.0
Minor Changes
-
ddce917: Initial release of
@loopress/mcp: an MCP (Model Context Protocol) stdio server exposinglpsoperations (snippet/page/apipush/pull/list,plugin/composerpush/pull,project_status) as tool calls for AI agents, plus aloopress://project/configresource. It wraps the existinglpsbinary as a subprocess rather than depending on@loopress/cliinternals, seeobsidian/Product/Loopress MCP.md.Mutating tools (
*_push) use a two-call handshake: called withoutconfirmToken, they run a--dry-runpreview and return a single-use, 5-minuteconfirmToken; called again with that token, they apply exactly what was previewed. There is no way to skip the preview in a single call, including for aproductionenvironment.