# trek-plugin-sdk The SDK for building [TREK](https://github.com/liketrek/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 ```bash 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. ```bash 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 ```bash 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` ``: ```html ``` `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: ```js 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. ```js // 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 `` and it just works; add `data-trek-native` to opt a field out. See the [Plugin Development wiki](https://github.com/liketrek/TREK/wiki/Plugin-Development) for the full component + token reference. ## Write a plugin ```js 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 ```js 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`: ```bash 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: ```bash npx trek-plugin-sdk publish --repo you/repo --tag v1.0.0 ``` `publish` is the whole release, in five steps and in this order: 1. **check** — every registry gate that can be checked locally 2. **pack** — build `plugin.zip` 3. **release** — git tag, push, and cut the GitHub release with the artifact 4. **preflight** — the gates that need the tag and the release to exist 5. **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. `publish` refuses 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. ```bash 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 as `apiVersion` in your manifest. - `validateManifest(json)` — the manifest rules the server loader uses. - `createMockHost(opts)` (from `trek-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 `` marker instead. ## Commands Run any of these with `npx trek-plugin-sdk ` (or the short `trek-plugin` bin if you install the package). `trek-plugin help ` — or `trek-plugin --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, SQLite `db: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 as `status`, but it exits non-zero. - `pack [dir] [--out plugin.zip] [--json]` — build the artifact, print `sha256` + `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]` — capture `docs/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`](https://github.com/sindresorhus/update-notifier), the standard for npm CLIs: - At most **once every 24 hours**, a detached background process asks `registry.npmjs.org` for this package's `latest` version 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 --json` and `entry` keep 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: ```bash 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.