The dev loop
Scaffold, link, iterate, publish.
Choose the host
For a desktop UI, start with the GUI runtime guide.
Use declarative settings or themes when they cover the interface; choose an
isolated runtime for custom views. fez create my-extension --gui generates
an isolated React navigation view with its manifest contributions and only
the ui permission. It requires a packaged macOS app with isolated
custom-view support; tauri dev does not support this runtime.
Scaffold
fez create my-extension --gui
cd my-extension
npm install
npm run check
npm run buildEdit src/view.tsx. The starter bundles its own React and uses
@fezchat/ui; npm run build runs the included CLI's fez pack and emits
dist/view.js. No global CLI is needed after creation. The name passed to
registerNavView must match fez.guiContributions.nav in package.json.
Other surface combinations use the multi-part template:
fez create my-extension # headless + legacy gui
fez create my-extension --relay --workspaceThat template emits a file per surface, ESM bundles for Node surfaces, and a
legacy h() GUI bundle. GUI-only --gui is the isolated React starter.
Link
npx fez link .In order: it reads your manifest, prints the permission grants your
package asks for (before copying anything), builds, then places every part.
The headless bundle is staged and smoke-imported first — a build that
fails to import is deleted, not installed, so a broken build can never
clobber a working extension. --no-build skips the build step.
npx fez link . --watch stays resident: it rebuilds on save (300 ms
debounce) and re-copies parts, keeping the smoke-import guarantee.
Restart after linking. The TUI, desktop background worker, and sentinel load extensions once at startup — the loop is save → restart the host. For desktop background work, quit and reopen the app; merely closing its window keeps the worker alive. Gui parts also need a restart after CLI linking. The desktop reloads them on install/update from its own gallery.
Both fez link and fez install derive the installation name from the npm
package, independent of your source directory. The @fezchat/ scope is
omitted; other scopes become a prefix (@you/my-extension → you-my-extension).
Test
Run npm run check and npm run build. In the packaged macOS app, select
your extension in the navigation rail: it should show its title, ready,
and Nothing here yet. Switch away and reopen it, and check its colors
when changing themes. After editing, link again and restart the app.
Record the desktop release tested in your README. fez.minFezVersion is
the protocol host version, not the desktop release number; the starter sets
it to the generating CLI's host version.
The repo's conformance evals are the pattern to copy: if you mirror API
types instead of importing @fezchat/extension-api, a type-check against
the host keeps the mirror honest (it may omit, never disagree). For logic,
keep it in plain modules your parts share — fez-polls keeps vote tallying
in one format.ts imported by the TUI command, the desktop card, and the
agent tool, so all three hosts count votes identically.
Publish
Set a scoped name such as @you/my-extension and remove "private": true
from the generated package.json before publishing.
npm run check && npm run build
npm pack --dry-run
npm publish --access publicExtensions are ordinary npm packages. Users install with
fez install @you/my-extension (bare names resolve to the @fezchat scope,
so third-party packages should be installed by full name), or from the
desktop gallery — which downloads the tarball itself, no node or npm on the
user's machine required.
Ship dist/ only (files: ["dist"]), bundle each part fully, and remember
the desktop installer requires an https tarball URL and reads the same
manifest.
Tools have their own decentralized listing flow — fez tool publish signs
an advertisement to the relay, fez tool market browses with install
counts, and installing always prints the exact command that will run before
writing anything. See Tools.