Official boilerplate for building plugins for Onda — the AI-first terminal for macOS.
This template gives you a TypeScript-strict scaffold, a typed bridge to the Onda host API, hot-reload via symlink, and a one-command pack-to-zip pipeline.
An Onda plugin is a small JavaScript bundle that runs inside Onda in an isolated Web Worker. Plugins extend Onda by:
- registering commands in the Command Palette
- adding status bar items and side panels
- subscribing to terminal output in real time
- shelling out, making HTTP calls, reading/writing files (with explicit permission)
- shipping custom themes, keybindings, and context-menu actions
Plugins live at ~/.config/onda/plugins/<plugin-id>/ and consist of a manifest.json plus a bundled main.js. Onda loads them lazily on activation events you declare in the manifest.
See the canonical capability reference in the Onda repo (onda-plugins/README.md) or the inline JSDoc in types/onda.d.ts.
# Using GitHub CLI
gh repo create my-onda-plugin --template mindfullabai/onda-plugin-template --public --clone
cd my-onda-plugin
# Or clone directly
git clone https://github.com/mindfullabai/onda-plugin-template.git my-onda-plugin
cd my-onda-plugin && rm -rf .git && git init- Rename the plugin in
manifest.json(id,name,description,author). - Update
package.json(name,author,homepage). - Edit
src/index.tsto do whatever your plugin should do.
The plugin id in manifest.json should be globally unique. Reverse-DNS (com.acme.git-status) or namespaced (acme.git-status) are both fine. It becomes the directory name on the user's disk.
npm install
npm run buildThis produces dist/main.js (a single bundled IIFE Worker script).
npm run link:localThis creates a symlink at ~/.config/onda/plugins/<your-plugin-id> pointing to this repo. Open Onda → Settings (⌘,) → Plugins, enable your plugin, then ⌘R to reload after each rebuild.
For watch mode:
npm run devnpm run pack
# → release/<plugin-id>-<version>.zipThe zip contains manifest.json, dist/main.js, README.md, and LICENSE. Users can extract it into ~/.config/onda/plugins/, or you can submit it to the Onda plugin registry (see Publishing).
.
├── manifest.json # Plugin metadata, capabilities, contributions
├── package.json
├── tsconfig.json # strict TS, ES2022, WebWorker lib
├── tsup.config.ts # IIFE bundle, browser target, single file
├── src/
│ └── index.ts # Plugin entry — assigns self.__ondaPlugin
├── types/
│ └── onda.d.ts # Hand-written types for the onda.* bridge
├── scripts/
│ ├── link-local.mjs # Symlink into ~/.config/onda/plugins
│ └── pack.mjs # Build zip artifact for distribution
├── examples/
│ ├── hello-world/ # Minimal command + notification
│ └── git-status-banner/ # Statusbar item, exec, periodic poll
└── .github/workflows/build.yml
Onda discovers your plugin via a global assignment, not an ES module export:
self.__ondaPlugin = {
async onActivate(onda) {
// onda.* is the bridge — see types/onda.d.ts
},
async onDeactivate() { /* optional cleanup */ }
};The bundler (tsup) is configured to emit a single classic-script IIFE because Onda loads main.js as a classic Worker, not an ES module Worker. Don't switch to format: 'esm' unless you know the host build has been updated to use new Worker(url, { type: 'module' }).
Every onda.foo.bar() call you make is gated by the capabilities array in manifest.json. Calling onda.exec.run(...) without "exec" in the capability list throws at the bridge level. Add capabilities as you need them and document why in the manifest so users can grant trust intentionally.
| Capability | What it unlocks |
|---|---|
commands |
Command Palette entries |
keybindings |
Global / focus-gated keyboard shortcuts |
statusbar |
Status bar items (left/right) |
panel |
Side panels with HTML content |
notifications |
Toast notifications |
contextmenu |
File panel / terminal right-click items |
storage |
Persistent key-value store, namespaced per plugin |
themes |
Register custom color themes |
terminal:read |
Read full terminal buffer |
terminal:write |
Inject keystrokes into terminals |
terminal:subscribe |
Real-time output events with optional regex filters |
dialog |
Modal dialogs with input fields |
clipboard |
Read/write system clipboard |
filesystem:read |
Read files |
filesystem:write |
Write files |
http |
Outbound HTTP (use httpDomains to scope) |
exec |
Run shell commands (use execPatterns to scope) |
onda.commands.register('acme.hello', {
title: 'Say Hello',
category: 'Acme',
handler: async () => {
onda.notifications.show({ type: 'success', message: 'Hello!' });
},
});// manifest.json: capabilities += ["terminal:subscribe"]
const sub = await onda.terminal.subscribe({
terminalId: 'active',
patterns: ['ERROR', 'WARN'],
});
onda.on('terminal:output', (e) => {
if (e.data.includes('ERROR')) {
onda.notifications.show({ type: 'error', message: 'Build error detected' });
}
});await onda.statusBar.addItem({
id: 'acme-status',
text: 'Acme',
icon: 'wrench',
position: 'right',
onClick: 'acme.toggle-panel',
});
await onda.panel.register({
id: 'acme-panel',
title: 'Acme',
position: 'right',
width: 320,
});
await onda.panel.setContent('acme-panel', '<div style="padding:16px;color:#eee">Hello</div>');
onda.commands.register('acme.toggle-panel', {
title: 'Toggle Acme Panel',
handler: () => onda.panel.toggle('acme-panel'),
});// manifest.json: capabilities += ["exec"]
const { stdout, code } = await onda.exec.run('git status --porcelain');
if (code === 0 && stdout.trim() === '') {
onda.notifications.show({ message: 'Working tree clean' });
}See the examples/ directory for full plugins you can copy.
The Onda plugin registry at https://onda.sh/publish is coming soon (tracked in Onda's roadmap as P4.11). Until then:
- Run
npm run packto producerelease/<id>-<version>.zip. - Distribute via GitHub Releases — the workflow in
.github/workflows/build.ymlbuilds and attaches the zip automatically on every push and tag. - Users install by extracting the zip into
~/.config/onda/plugins/.
When the registry launches, you'll submit by signing in with GitHub and pointing at a release tag — no rebuild required.
strict,noUncheckedIndexedAccess,noImplicitOverrideare all on.- The
types/onda.d.tsfile is hand-maintained against the Onda 1.x bridge. If your build of Onda exposes new APIs, extend the file locally and consider opening a PR upstream. - Don't import from
'../types/onda'at runtime — they're type-only. Useimport type.
- "Plugin failed to activate": open Onda DevTools (⌘⌥I) and check the Console — Worker errors include stack traces, prefixed with
[PluginWorker]. - "Capability denied": add the capability to
manifest.jsonand reload. - Changes don't show up: did you
npm run build? did you ⌘R the renderer? did you restart Onda after editingmanifest.json(manifest changes require a full restart, code changes don't). self is not definedin tests: tests run in Node, not a Worker. Mockself.__ondaPluginor run unit tests against pure functions extracted from the entry.
MIT — see LICENSE.
{ "activationEvents": [ "onStartup", // lazy boot after UI mount (default) "onCommand:my-plugin.do-thing", // only when user runs the command "onView:my-plugin.panel", // only when panel becomes visible "*" // eager boot — use sparingly ] }