Rewrite the third-party extensions design against the real catalog - #186
Merged
Conversation
The first draft surveyed four generic plugin architectures and recommended one without reference to how wp-ops actually resolves and runs a command. Rewrite it as a single decision — build-time embedded catalog vs. runtime discovery — with the code that decision breaks named by file and line. The substantive findings the draft missed: - Entry has no source root. ScriptPath is repo-relative and joined against repoRoot() at six sites (dispatch.go's four executors, docs.go's @doc resolution, manifest.Lint's doc check). Nothing external is runnable before that changes, and it is the whole of Phase 1. - The binary embeds its own asset tree and extracts it to a cache dir, so a Homebrew install has no checkout at all — "drop your script into the repo" serves the least common install shape. - catalog.go states "no filesystem scan happens at runtime" as an invariant; runtime discovery inverts it. Restated as a two-tier rule so core keeps its build-time guarantee and a malformed extension can never be fatal. - A @category outside DisplayOrder makes a command invisible in every listing and the picker. Falls back to misc. - mcp-server reads catalog.json off disk from REPO_ROOT, so extensions are invisible to MCP. Made an explicit decision rather than an omission. - Completion runs the scan on every <TAB>, so the mtime cache is load-bearing; budgeted at 5ms. Dropped, each with a reason recorded in an out-of-scope table: the four duplicate-prevention strategies (ext/<vendor>/ keys make full-key collisions impossible, and basename collisions are already handled by ShortName and printAmbiguous), the permissions sandbox block (unenforceable against an exec'd shell script), the package manager and registry, and the test-helper library (core scripts aren't tested either — ext validate covers the real failure mode). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
docs/third-party-extensions.md(added in d498c3e) surveyed four generic plugin architectures and recommended one without reference to how wp-ops actually resolves and runs a command. This rewrites it as a single named decision — build-time embedded catalog vs. runtime discovery — with the code that decision breaks cited by file and line.Documentation only. No code changes.
Findings the first draft missed
Entryhas no source root.ScriptPathis repo-relative and joined againstrepoRoot()at six sites —dispatch.go's four executors,docs.go's@docresolution, andmanifest.Lint's doc check. Nothing external is runnable before that changes, and it is the whole of Phase 1.env.go:45-111), so a Homebrew install has no checkout on disk. "Drop your script into the repo" serves the least common install shape.catalog.gostates "no filesystem scan happens at runtime" as an invariant, and runtime discovery inverts it. Restated as a two-tier rule: core keeps its build-time guarantee, a malformed extension is skipped with a warning and can never be fatal.@categoryoutsideDisplayOrdermakes a command invisible in every listing and the picker, sinceDisplayCategories()filters against that hardcoded slice. Falls back tomisc.mcp-serverreadscatalog.jsonoff disk fromREPO_ROOT, so extensions are invisible to MCP. Now an explicit decision with a recommendation rather than an omission.<TAB>, so the mtime cache is load-bearing, not an optimization. Budgeted at 5ms with a stated fallback if it can't be met.Dropped, with reasons recorded
Each entry below is in an out-of-scope table in the document, so the removals stay reviewable in a diff that deletes 645 lines.
ext/<vendor>/keys make full-key collisions structurally impossible; basename collisions are already handled byShortNameandprintAmbiguouspermissions:sandbox blockexecd shell script — documenting controls that don't exist is worse than documenting nonegit clonecovers distribution;searchneeds a registry that doesn't existtest generate-ciext validatecovers the real failure modeStructure
Adds an up-front "Is this needed?" section separating three possible demands (private client scripts / shareable packs / public ecosystem) with very different price tags, since Phase 1 is worth doing regardless and Phases 2–3 are not. Adds a consumers-that-must-change checklist. Leaves four open questions on the record rather than answering them.
Review notes
This is a design proposal, so the
Entry.Rootsketch and the 5ms completion budget are claims to test during implementation, not measurements. The MCP question (CLI-only extensions vs. mirroring discovery in TypeScript) is the one decision worth settling before Phase 1 starts.