pumi

Build a plugin

From an empty folder to a plugin others can install, with the SDK for types and tests.

This page walks through making a plugin, running it while you edit it, type-checking and testing it with the SDK, and sharing it. Plugins is the reference for everything a plugin can add.

You need pumi installed, and Bun for type-checking and tests. pumi runs the plugin on its own Bun, so the plugin itself needs nothing installed.

1. Make it

pumi plugin new hello

This writes a starter plugin in hello/:

File
package.jsonthe manifest, under "pumi": a command, two options, a sidebar panel, a tab menu item and a section in Settings
src/main.tsits logic, in a worker: registers the hello command
src/ui.tsxwhat it draws, in React: the panel and the Settings section
tsconfig.jsonTypeScript settings for tsc, with the SDK (step 3)
AGENTS.mda summary of the manifest and the API, for a coding agent working on it

Its id is owner/name: name is the folder's name and owner is your GitHub user (git config github.user), else your login name. Both are in package.json and can be changed there.

2. Run it

pumi plugin dev hello

This links the folder, loads the plugin, and reloads it each time you save a file in it. Its log prints in the same terminal. Open the palette and run "Say hello", or open the sidebar panel.

If it fails to load, pumi plugin logs hello shows its stderr and why it last stopped.

3. Type-check it with the SDK

At run time, the app gives the plugin import … from "pumi". Outside the app, tsc and bun test need the SDK, the npm package @pumi-dev/sdk. pumi plugin new already lists it, under the name pumi so imports are the same in both places, and sets "jsxImportSource": "pumi" in tsconfig.json so JSX knows pumi's elements (div, text, terminal, …):

"devDependencies": {
  "pumi": "npm:@pumi-dev/[email protected]",
  "@types/bun": "1.3.14",
  "typescript": "5.9.3"
}
cd hello
bun install
bun run check     # tsc --noEmit

A plugin made before the SDK existed gets it the same way: add that pumi line to its devDependencies and jsxImportSource to its tsconfig.json, and drop @types/react, which the SDK brings.

The SDK brings React and its types as peers, at the versions the app runs. Its version is its own: it moves when what a plugin can import changes, and its README says which pumi version it was built from. A plugin that uses an export the installed app does not have yet fails when it runs, not in tsc.

Import
pumithe API: main.ts's objects and ui.tsx's hooks and components
pumi/themewhat a UI theme is drawn with (see Themes)
pumi/jsx-runtimeJSX with the renderer's elements
pumi/testingcreateTestRoot and TestRenderer, for component tests

4. Test it

pumi and pumi/theme have types but no code in the SDK. In a test, stand in for the parts the code under test imports with mock.module, then import it:

// test/panel.test.tsx
import { describe, expect, mock, test } from "bun:test";
mock.module("pumi", () => ({
  fn: { set() {} }, request: {}, self: {},                       // what src/main.ts imports
  useTheme: () => ({ muted: "#888", surface: "#222", hover: "#333" }),
  useOption: () => [], usePlugin: () => "me/hello",
  ChoiceGrid() {}, OptionRow() {}, Row() {}, Section() {},          // the rest of what src/ui.tsx imports
}));
const { createTestRoot, hasNativeTestRenderer } = await import("pumi/testing");
const { Panel } = await import("../src/ui.tsx");

(hasNativeTestRenderer ? describe : describe.skip)("the panel", () => {
  test("counts clicks", () => {
    const { render, renderer } = createTestRoot({ width: 300, height: 100 });
    render(<Panel />);
    expect(renderer.getAllText()).toContain("0 clicks");
  });
});

The mock replaces the whole module, so it must have every name the tested files import; a missing one is a SyntaxError: Export named … not found. Logic that draws nothing is easiest to test as plain functions, without mocks.

Component tests need the app's native test renderer, which release builds of pumi leave out, so with the installed app those tests skip. Type-check and test says how to run them against a debug build.

5. Share it

Push the folder to a GitHub repository. Anyone can then install it:

pumi plugin add owner/repo

pumi installs its dependencies (not its devDependencies, so the SDK stays out). Tag a release to let people pin it with --version. A plugin runs with the user's permissions and no sandbox, so the README should say what it does. Share has the rest.