fez
Extension API

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 build

Edit 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 --workspace

That 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.

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-extensionyou-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 public

Extensions 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.

On this page