Optional plugins for DevTool,
installable at runtime (Settings → Extensions → paste a manifest URL) instead
of being compiled into the main app — see
docs/decisions/architecture/platform-plugin-architecture.md in the main
repo for the full reasoning.
This branch (main) has no plugin code. It's the skeleton every plugin
branch builds on top of:
shared/— a vendored, trimmed copy of the app's shadcn/ui kit and a fewsrc/lib/*utilities. It's a deliberate duplication, not a shared package: an externally-installed plugin bundle can't import the app's ownsrc/components/ui/*(onlyreact/react-dom/react/jsx-runtimeare shared, viawindow.__DEVTOOL_VENDOR__— seeshared/vendor.d.tsandshared/platform.ts).scripts/makePluginConfig.mjs— the shared Vite config factory every plugin'svite.config.tscalls into.- root
package.json/tsconfig.json/vitest.config.ts— the toolchain versions and lint/type rules every plugin branch starts from. catalog.json— the aggregated, install-ready plugin list. Read by starlight-site's/pluginspage viaraw.githubusercontent.com/.../main/catalog.json(release-asset storage doesn't send CORS headers, so it has to be a committed file, not a release asset). Nothing onmainwrites this file — each plugin branch's own release updates only its own entry when it ships (see below), so this stays live withoutmainever needing a release of its own.
Each actual plugin lives on its own long-lived branch, app/<id>/main
(currently app/redis-client/main, app/rabbit-client/main,
app/container-manager/main, app/kafka-explorer/main), forked from this
skeleton. A plugin branch has:
plugins/<id>/— that plugin'sui/,entry.tsx,manifest.*.jsontemplates, and (if it has a sidecar)sidecar/— same layout as before, just one plugin per branch instead of all of them in one tree.- Its own
package.json/ci.yml/release.yml, trimmed to build/test/release only that one plugin. scripts/merge-catalog-entry.mjs— used by that branch'srelease.ymlto update just its own entry inmain'scatalog.jsonon release, without touching any other plugin's entry.
Why branches instead of one monorepo tree: the previous single-tree
design meant one plugin's broken build failed CI/release for every plugin at
once (a single npm run build chain and a single bash loop over every
sidecar, set -e'd together), and every release stamped every plugin with
the same repo-wide tag whether or not it had actually changed. Splitting
each plugin onto its own branch with its own CI/release/tag gives each one
real fault isolation and independent versioning — and a real place to
attach separate push permissions later, if that's ever needed, since GitHub
branch protection is per-branch.
To pull skeleton updates (a shared/ fix, a toolchain bump) into a plugin
branch: git fetch origin main && git merge origin/main on that plugin
branch, same as merging any upstream change.
A plugin bundle is JavaScript only: scripts/makePluginConfig.mjs builds
one ESM file and emits no CSS. Everything on screen is styled by the host
app's already-compiled Tailwind sheet, which is generated by scanning the
host's own src/** (tailwind.config.js → content). That never includes
this repo — a plugin is fetched at runtime from a URL, long after the host was
built.
So a class written only in a plugin lands in the DOM with no rule behind it
and is silently a no-op. This is not theoretical: it had broken all four
plugins at once. bottom-3, pl-1.5, py-5, h-64, m-4, mx-5,
min-h-16, min-h-20, -mt-2.5, align-top and translate-x-full all had
no rule — while max-h-64, sitting beside h-64 in the same file, did. The
container log dialog lost h-[68vh], fell back to height: auto, grew with
every line that streamed in and ran off both ends of the screen.
Write sizing and layout values as an inline style. No build step can drop
one, and it works on whatever host version the user already has.
Two guards, and they cover different halves:
-
plugins/<id>/ui/hostCssClasses.test.ts— scaffolded into every new plugin bycreate-plugin.mjsand run bynpm test. Catches arbitrary-value sizing and layout classes, which no host-side safelist can ever cover. Offline. -
scripts/check-host-classes.mjs— the complete check. Resolves every class a plugin writes against a real host stylesheet, which is the only ground truth:# in the host checkout npm run build # back here node scripts/check-host-classes.mjs ../developer-desktop-utils/dist/assets/*.css
Run it against the oldest host release you still support — a plugin installs into whatever host the user already has.
Two host-side files decide what resolves, and neither is a guarantee:
externalPluginClassnamesSafelist.ts— a snapshot of plugin class names extracted once, when these tools left that repo. Its own comment says it has "no automated re-sync", and that is the whole story behind the breakage above: the classes plugin code added after that snapshot are exactly the ones with no rule.max-h-64was in it andh-64was not.plugin-utilities.css— enumerates the common scales rather than actual usage, so it needs no plugin checkout and cannot go stale. It cannot cover arbitrary values, which is why the guard above gates those.
The same reasoning is why shared/components/ui/code-editor-base.tsx takes a
style prop: a plugin had no other way to set an editor's height that
survives, since tailwind-merge cannot override a rule that was never emitted.
From main, with a clean working tree:
node scripts/create-plugin.mjs # interactive — prompts for whatever's missing
# or, non-interactive:
node scripts/create-plugin.mjs <new-id> --label "Label" --description "..." [--sidecar]This creates app/<new-id>/main, scaffolds plugins/<new-id>/ (ui/,
entry.tsx, manifest templates, the CSS guard described above,
sidecar/ if requested), points
package.json/tsconfig.json/vitest.config.ts/ci.yml/release.yml at
it, runs the same verify steps CI runs (reported, not blocking — a fresh
scaffold has a placeholder UI by design), and adds this
plugin's dependabot.yml entries on main (Dependabot only ever reads
that file from the repo's default branch, using each entry's
target-branch to reach the others). Both commits (new branch + main)
stay local — nothing is pushed automatically.
Then:
- Fill in the real UI (
ui/*.tsx), sidecar (sidecar/src/main.rs) if any, andmanifest.plugin.json's keywords/permissions/icon. - Add real tests beside the scaffolded CSS guard, re-verify:
npm run lint:manifests && npm run typecheck && npm test && npm run build git push -u origin main && git push -u origin app/<new-id>/maingit tag <new-id>-v0.1.0 && git push origin <new-id>-v0.1.0— triggers its first release.
In DevTool: Settings → Extensions, paste the plugin's manifest URL (from
catalog.json's pluginManifestUrl), install, then do the same with
serviceManifestUrl for its sidecar, and restart the app.