trek-plugin-sdk
The SDK for building TREK plugins.
The path is four commands: create → dev → status → publish. Everything
else the CLI can do is a step one of those already does for you.
Scaffold a plugin
npx trek-plugin-sdk # no command? a guided menu of everything below
npx trek-plugin-sdk create # interactive wizard (id, type, icon, permissions, egress)
npx trek-plugin-sdk create my-plugin --type widget # or non-interactive
cd my-plugin
The wizard asks for the id, type, icon (validated against lucide), permissions,
egress hosts and required addons, and offers to initialize a git repo and install
dependencies for you. In a non-interactive shell (CI, pipes) every command stays
flag-driven with plain output — no prompts, and machine output (entry JSON,
pack --json, PR URLs) stays on stdout.
What you get runs and packs immediately — but it is not publishable yet: the
README is a template and there is no screenshot. That is deliberate. Writing those
is your job, and status tells you exactly what is left.
Develop with a live reload loop
dev runs your plugin locally — no full TREK needed. It injects a ctx that
enforces exactly the permissions your manifest grants (an ungranted call throws
PERMISSION_DENIED, so you catch missing grants), backs db:own with a real
SQLite file, serves your routes and your page/widget UI, and reloads on save.
npx trek-plugin-sdk dev # http://localhost:4317 — dashboard, routes, UI
Open /preview to see a page/widget rendered in a real sandboxed frame with a
theme/accent/appearance toggle (trek.invoke() is proxied to your routes). Hit a route
as an unauthenticated request with ?_anon=1. Drop a dev-fixtures.json (trips, users,
config) next to your manifest to feed ctx.trips / ctx.users.
Know where you are
npx trek-plugin-sdk status # never fails — it's a map, not a gate
status runs every check the TREK-Plugins registry enforces that can be answered
without a network — which is nearly all of them — and prints the whole journey as a
checklist grouped by stage (Manifest, Code, Docs, Release, Repo): what passes, what
does not, and the one command to run next. Run it whenever you are not sure what
to do; it deliberately never exits non-zero.
validate is the same checks with an exit code. It is the form for scripts and CI;
status is the form for humans. A plugin that passes validate will pass every
registry gate that does not require the tag and the release to exist — those four
run in preflight, which publish does for you.
Build a native UI (page / widget)
The UI is a sandboxed, opaque-origin iframe that can't load TREK's stylesheet — so the
SDK ships it. Put one line in your client/index.html <head>:
<!-- trek:ui -->
dev and pack expand it into the inlined design kit: token-driven styles that
follow the host's theme and accent (glass, cards, .trek-btn, .trek-input,
.trek-chip, .trek-row, hover), plus a window.trek bridge:
trek.onContext((ctx) => { /* ctx.theme, ctx.tokens, ctx.appearance, ctx.user, ctx.tripId */ })
const data = await trek.invoke('/status') // calls your own route, host-proxied
trek.notify('success', 'Saved')
Keep UI state for this browser tab
The opaque frame cannot use browser storage directly. The bridge provides host-managed, JSON-only session state instead. It survives an iframe remount and page reload in the same browser tab, but is not durable data. Do not use it to store secrets.
// Default: this user + this plugin + this browser tab.
await trek.session.set('dismissed-onboarding', true)
const dismissed = await trek.session.get('dismissed-onboarding')
// Explicitly partition state by the trip in view (fails without a trip context).
await trek.session.set('filters', ['flight', 'hotel'], { scope: 'trip' })
get() returns undefined for a missing key. set, get, remove, and
clear accept { scope: 'plugin' | 'trip' }; plugin is the default. Use your
plugin's own logical key (for example reservation:88:expanded) for any finer
partitioning. Each plugin or individual trip scope is limited to 32 keys,
64 characters per key, and 1 KiB per JSON value.
The kit applies the theme, mirrors the appearance flags (reduced-motion,
no-transparency) and auto-reports your height. It also upgrades any native
<select> into a host-styled, keyboard-accessible dropdown that matches TREK —
the OS-drawn popup never could. Write a plain <select> and it just works; add
data-trek-native to opt a field out. See the
Plugin Development wiki
for the full component + token reference.
Write a plugin
const { definePlugin } = require('trek-plugin-sdk')
module.exports = definePlugin({
async onLoad(ctx) {
await ctx.db.migrate('001', 'CREATE TABLE cache (k TEXT PRIMARY KEY, v TEXT)')
},
routes: [
{ method: 'GET', path: '/status', auth: true, async handler(req, ctx) {
return { status: 200, headers: { 'content-type': 'application/json' }, body: '{"ok":true}' }
}},
],
})
Your plugin runs in an isolated child process. ctx is the only way to reach
TREK, and it grants exactly the permissions your trek-plugin.json declares — an
ungranted call throws PERMISSION_DENIED.
Test without a running TREK
import { createMockHost } from 'trek-plugin-sdk/testing'
const { ctx, broadcasts } = createMockHost({
grants: ['db:read:trips', 'ws:broadcast:trip'],
trips: { 1: { members: [42], data: { id: 1, name: 'Japan' } } },
})
// the mock enforces the SAME permission model, so you can prove your plugin
// degrades gracefully when a permission is missing.
Capture the screenshot
The registry requires a screenshot that resolves to a real image, and the store card
shows it. shot boots the dev server, renders your plugin in the same themed frame
TREK uses, and writes a 1600×900 docs/screenshot.png:
npm i -D playwright && npx playwright install chromium # once
npx trek-plugin-sdk shot # --dark for the dark theme
Playwright is deliberately not a dependency of this SDK — it ships a browser, and
most authors never need one. An integration plugin has no UI to render, so shot
cannot help; screenshot the TREK surface your plugin changes instead.
Publish — one command
Commit and push your plugin to its public GitHub repo, then:
npx trek-plugin-sdk publish --repo you/repo --tag v1.0.0
publish is the whole release, in five steps and in this order:
- check — every registry gate that can be checked locally
- pack — build
plugin.zip - release — git tag, push, and cut the GitHub release with the artifact
- preflight — the gates that need the tag and the release to exist
- submit — open the PR against the TREK-Plugins registry
Step 1 comes first for a reason: a GitHub release is effectively immutable, because
the registry pins its sha256. If a check fails, nothing is packed, tagged, pushed
or released — so you can fix it and re-run against the same version. (--no-checks
skips step 1; it is an escape hatch for a re-run, never right on a first publish.)
Needs git and an authenticated gh. It prints the PR URL at the end.
Updating a listed plugin: bump version in the manifest, commit, and run
publish again with the new tag — it detects the existing entry and prepends the
new version, newest-first.
Prefer to drive the steps yourself? They still exist individually — pack,
release (pack → GitHub release → entry), preflight, submit (opens the PR),
and entry (just prints the JSON).
Signing (recommended — publish offers it)
In a terminal, publish asks whether to sign, and creates the key for you if you
have none — you don't have to know --sign or keygen exist. Scripts and CI are never
prompted: pass --sign.
A signature proves the artifact came from you, not just that its bytes match what the registry saw. Signing is dependency-free Ed25519 over the artifact bytes.
It is a one-way door you may walk through late:
- Unsigned throughout is fine — the sha256 pin is the only guarantee, and TREK accepts it.
- Unsigned → signed later breaks nobody. Nothing is pinned until a signed version installs, so adding a key at v1.4.0 is a real option, not a lost cause.
- Signed → unsigned is refused forever, on every instance that already has the plugin.
publishrefuses that at step 1, before anything is tagged or released.
So back the key up (~/.trek-plugin/signing.key). Losing it means you cannot ship an
update to your own plugin without a registry maintainer override.
npx trek-plugin-sdk publish --repo you/repo --tag v1.1.0 --sign # or just answer the prompt
Exports
definePlugin(def)+ all the plugin types (PluginContext,PluginRoute,PluginJob,PhotoProvider,CalendarSource).PLUGIN_API_VERSION— embed asapiVersionin your manifest.validateManifest(json)— the manifest rules the server loader uses.createMockHost(opts)(fromtrek-plugin-sdk/testing).TREK_UI_CSS,TREK_THEME_JS,TREK_UI_MARKER,injectTrekUi(html)— the design kit, for authors who inline it themselves (a bundler, a custom build). Most plugins just use the<!-- trek:ui -->marker instead.
Commands
Run any of these with npx trek-plugin-sdk <command> (or the short trek-plugin
bin if you install the package). trek-plugin help <command> — or
trek-plugin <command> --help — prints a full page for any of them.
The path:
create [name] [--type t] [--template blank|notification-channel] [--interactive]— scaffold a plugin; a wizard if you omit the name.dev [dir] [--port 4317]— run locally with a real request loop, SQLitedb:own, and hot reload.status [dir]— where am I? what's left? Every offline registry gate as a checklist, plus the next command. Never fails.publish [dir] --repo o/n --tag vX [--sign [key]] [--no-checks] [--no-preflight]— the lot: check → pack → release → preflight → open the PR. In a terminal it offers to sign (and makes you a key); scripts pass--sign.
Also:
validate [dir]— the gate: the same checks asstatus, but it exits non-zero.pack [dir] [--out plugin.zip] [--json]— build the artifact, printsha256+size. Refuses a plugin that could not load; does not enforce the publish gates, because packing is how you sideload a plugin to try it.shot [dir] [--port 4317] [--out docs/screenshot.png] [--dark] [--no-serve]— capturedocs/screenshot.png. Needs Playwright.keygen [--key file]— create an Ed25519 signing key.sign [zip] [--key file]— print a signature + public key for an artifact.entry --repo o/n --tag vX [--merge f] [--sign [key]] [--out f]— emit the registry entry JSON.preflight [dir] --repo o/n --tag vX [--entry f] [--all]— the registry checks that need the network: the tag resolves to the pinned commit, the released artifact downloads and hashes, the id is not bound to another owner, and an update does not drop or rotate a published signing key.submit [dir] --repo o/n --tag vX [--registry o/n] [--draft]— open the registry PR for you.release [dir] --repo o/n --tag vX [--sign [key]] [--merge f]— pack → GitHub release → entry, without opening the PR.
Update notice
Both CLIs (trek-plugin and create-trek-plugin) tell you when a newer SDK has been
published. This matters more than the usual "you're on an old version" nag: the
registry entry format, the manifest rules and the permission catalog all move with the
TREK host, so a stale SDK can pack and submit an entry that today's registry CI
rejects.
It is powered by update-notifier,
the standard for npm CLIs:
- At most once every 24 hours, a detached background process asks
registry.npmjs.orgfor this package'slatestversion and caches the answer under$XDG_CONFIG_HOME/configstore/(or~/.config/…). - Your command never waits for it — the notice is printed from that cache, so a fresh install learns about an update on a later run.
- The notice goes to stderr, so
pack --jsonandentrykeep piping clean JSON.
The request is an unauthenticated GET for a public package — the same one npm install
makes (npm's servers see your IP, as for any download). TREK has no telemetry, and this
isn't any. To turn it off:
export NO_UPDATE_NOTIFIER=1
It is already silent in CI (any CI env var), under NODE_ENV=test, and whenever
stdout isn't a terminal (i.e. when piped or redirected).
The SDK tooling in this repo is MIT. Your plugin is your own code under your own license.