@drift-beacon/plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/bin/dbplugin.js +11 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +38 -0
  6. package/dist/dev.d.ts +38 -0
  7. package/dist/dev.js +8 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +9 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +8 -0
  12. package/dist/internal/actions.d.ts +35 -0
  13. package/dist/internal/actions.js +1 -0
  14. package/dist/internal/collection.d.ts +15 -0
  15. package/dist/internal/collection.js +52 -0
  16. package/dist/internal/compatibility.d.ts +26 -0
  17. package/dist/internal/compatibility.js +52 -0
  18. package/dist/internal/json-equal.d.ts +3 -0
  19. package/dist/internal/json-equal.js +20 -0
  20. package/dist/internal/manifest.d.ts +51 -0
  21. package/dist/internal/manifest.js +73 -0
  22. package/dist/internal/models.d.ts +62 -0
  23. package/dist/internal/models.js +387 -0
  24. package/dist/internal/package-layout.d.ts +21 -0
  25. package/dist/internal/package-layout.js +19 -0
  26. package/dist/internal/protocol.d.ts +96 -0
  27. package/dist/internal/protocol.js +8 -0
  28. package/dist/internal/rows.d.ts +41 -0
  29. package/dist/internal/rows.js +1 -0
  30. package/dist/internal.d.ts +17 -0
  31. package/dist/internal.js +13 -0
  32. package/dist/main.d.ts +92 -0
  33. package/dist/main.js +14 -0
  34. package/dist/pack.d.ts +24 -0
  35. package/dist/pack.js +131 -0
  36. package/dist/prepare.d.ts +28 -0
  37. package/dist/prepare.js +121 -0
  38. package/dist/types.d.ts +193 -0
  39. package/dist/types.js +10 -0
  40. package/dist/ui.d.ts +25 -0
  41. package/dist/ui.js +265 -0
  42. package/dist/version.d.ts +6 -0
  43. package/dist/version.js +5 -0
  44. package/dist/vite.d.ts +8 -0
  45. package/dist/vite.js +370 -0
  46. package/package.json +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Drift Beacon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # @drift-beacon/plugin
2
+
3
+ The plugin SDK for [Drift Beacon](https://driftbeacon.app). A plugin has **main code**, which runs on your Drift Beacon server, and a **UI**, which opens in the web app. This package gives you their APIs, a Vite plugin that builds both, and the `dbplugin` command that packages a release.
4
+
5
+ Plugin API version: **0.1**. SDK `0.1.x` builds plugins for Drift Beacon servers that run API 0.1.
6
+
7
+ ## Contents
8
+
9
+ - [Quick start](#quick-start)
10
+ - [Project layout and commands](#project-layout-and-commands)
11
+ - [Manifest](#manifest)
12
+ - [Main code](#main-code)
13
+ - [UI](#ui)
14
+ - [Workspace data](#workspace-data)
15
+ - [Actions and events](#actions-and-events)
16
+ - [Storage](#storage)
17
+ - [Configuration](#configuration)
18
+ - [MQTT and HTTP routes](#mqtt-and-http-routes)
19
+ - [Errors and limits](#errors-and-limits)
20
+ - [Developing against your server](#developing-against-your-server)
21
+ - [Releasing a plugin](#releasing-a-plugin)
22
+ - [Versions](#versions)
23
+
24
+ ## Quick start
25
+
26
+ ```sh
27
+ mkdir my-plugin && cd my-plugin
28
+ npm init -y
29
+ npm pkg set type=module
30
+ npm install --save-dev @drift-beacon/plugin@0.1 vite typescript @types/node
31
+ ```
32
+
33
+ `package.json` scripts:
34
+
35
+ ```json
36
+ {
37
+ "scripts": {
38
+ "dev": "vite",
39
+ "build": "vite build",
40
+ "release": "dbplugin pack",
41
+ "postinstall": "dbplugin prepare"
42
+ }
43
+ }
44
+ ```
45
+
46
+ `vite.config.ts`:
47
+
48
+ ```ts
49
+ import { driftBeacon } from "@drift-beacon/plugin/vite";
50
+ import { defineConfig } from "vite";
51
+
52
+ export default defineConfig({ plugins: [driftBeacon()] }); // with React: [react(), driftBeacon()]
53
+ ```
54
+
55
+ `tsconfig.json`:
56
+
57
+ ```json
58
+ { "files": [], "references": [{ "path": "./.drift-beacon/tsconfig.main.json" }, { "path": "./.drift-beacon/tsconfig.ui.json" }] }
59
+ ```
60
+
61
+ `manifest.json`:
62
+
63
+ ```json
64
+ {
65
+ "id": "hello",
66
+ "name": "Hello",
67
+ "version": "1.0.0",
68
+ "apiVersion": "0.1",
69
+ "description": "Says hello when a session starts",
70
+ "author": { "name": "You" },
71
+ "category": "plugin",
72
+ "icon": "",
73
+ "configuration": []
74
+ }
75
+ ```
76
+
77
+ `main/src/index.ts`:
78
+
79
+ ```ts
80
+ import { definePlugin } from "@drift-beacon/plugin";
81
+
82
+ export default definePlugin({
83
+ onStart(ctx) {
84
+ ctx.sessions.onStarted((session) => {
85
+ if (session.isMine) ctx.log.info(`Started ${session.activity?.name}`);
86
+ });
87
+ },
88
+ });
89
+ ```
90
+
91
+ `ui/index.html` and `ui/src/main.ts`:
92
+
93
+ ```html
94
+ <!doctype html>
95
+ <html>
96
+ <body>
97
+ <ul id="activities"></ul>
98
+ <script type="module" src="/src/main.ts"></script>
99
+ </body>
100
+ </html>
101
+ ```
102
+
103
+ ```ts
104
+ import { connect } from "@drift-beacon/plugin/ui";
105
+
106
+ const ctx = await connect();
107
+ const list = document.getElementById("activities")!;
108
+ const render = () => {
109
+ list.replaceChildren(
110
+ ...ctx.activities.list().map((activity) => {
111
+ const item = document.createElement("li");
112
+ item.textContent = `${activity.name}${activity.isLive ? " (live)" : ""}`;
113
+ item.onclick = () => void activity.track();
114
+ return item;
115
+ }),
116
+ );
117
+ };
118
+ ctx.onDataChange(render);
119
+ render();
120
+ ```
121
+
122
+ Then `npm run build` writes `dist/`, and `npm run release` writes `releases/hello.zip`.
123
+
124
+ ## Project layout and commands
125
+
126
+ ```
127
+ my-plugin/
128
+ ├── package.json # "type": "module"
129
+ ├── vite.config.ts # plugins: [driftBeacon()]
130
+ ├── tsconfig.json # references .drift-beacon/tsconfig.{main,ui}.json
131
+ ├── manifest.json # identity, API version, icon, settings
132
+ ├── main/src/index.ts # main code: runs on the server
133
+ ├── ui/ # the UI: an ordinary Vite app, shown in an iframe in the web app
134
+ │ ├── index.html
135
+ │ ├── src/…
136
+ │ └── public/ # the manifest icon, plus any static files
137
+ ├── .drift-beacon/ # generated by dbplugin prepare; ignores itself
138
+ └── dist/ # the build: exactly the release package
139
+ ```
140
+
141
+ | Command | What it does |
142
+ |---|---|
143
+ | `vite` (`npm run dev`) | Serves the UI with hot reload and rebuilds main on every save. A Drift Beacon server in development mode applies each build and prints the plugin's status and logs in the same terminal. |
144
+ | `vite build` | Production build into `dist/`: `manifest.json`, `package.json`, `main/index.js` and `ui/`. |
145
+ | `dbplugin pack` | Typechecks main and the UI, builds fresh into a temporary folder and writes `releases/<id>.zip`. Prints `{ id, version, tag, asset, archivePath }` as JSON on stdout. `dist/` is untouched. `--out <dir>` picks another folder. |
146
+ | `dbplugin prepare` | Writes `.drift-beacon/`: typed settings and the two tsconfigs. Runs on install and on every build. |
147
+
148
+ Run `dbplugin` through your package scripts or `npx dbplugin …` inside the plugin folder: it's the copy from your installed SDK.
149
+
150
+ - **Main** is bundled into one ESM file with every dependency except `@drift-beacon/plugin`, which the server supplies at run time. Native modules aren't supported.
151
+ - **The UI** bundles its own dependencies, including `@drift-beacon/plugin/ui`. Use any framework and library versions you like.
152
+ - `driftBeacon()` owns Vite's `root` (`ui/`), `base` and `build.outDir`: don't set them. It works with Vite 7 and 8.
153
+
154
+ | Import | Used by | Contents |
155
+ |---|---|---|
156
+ | `@drift-beacon/plugin` | Main code | `definePlugin`, `PluginError`, `API_VERSION` and every type |
157
+ | `@drift-beacon/plugin/ui` | UI | `connect()`, `PluginError`, `API_VERSION` and the shared types |
158
+ | `@drift-beacon/plugin/vite` | `vite.config.ts` | `driftBeacon()` |
159
+
160
+ ## Manifest
161
+
162
+ | Field | Rules |
163
+ |---|---|
164
+ | `id` | Lowercase letters, digits and hyphens, starting with a letter or digit, at most 64 characters |
165
+ | `version` | Semantic version (`1.2.3`); build metadata is allowed, prereleases are not |
166
+ | `apiVersion` | `major.minor` of the SDK you build with (`"0.1"`) |
167
+ | `name`, `description` | Required text |
168
+ | `author` | `{ name, email? }` |
169
+ | `category` | `plugin`, `utility` or `ui` |
170
+ | `icon` | A file name in `ui/public/`, or `""` |
171
+ | `configuration` | Settings (see [Configuration](#configuration)) |
172
+
173
+ Settings items have `name` (unique), `title`, `type` and optional `description` and `required`:
174
+
175
+ | `type` | Extra fields | Value in `ctx.config` |
176
+ |---|---|---|
177
+ | `string` | `default?`, `placeholder?` | `string` |
178
+ | `number` | `default?`, `minimum?`, `maximum?` | `number` |
179
+ | `boolean` | `default?` | `boolean` |
180
+ | `dropdown` | `default?`, `data: [{ title, value }]` | the selected `value` |
181
+
182
+ ## Main code
183
+
184
+ `main/src/index.ts` default-exports `definePlugin({ onStart(ctx) { … } })`. The server runs one **instance** for each user and workspace that enabled the plugin; each gets its own `ctx`.
185
+
186
+ - **Start.** `onStart(ctx)` may be async and must finish within 15 seconds. If it throws, the instance doesn't start.
187
+ - **Register through `ctx`.** Callbacks, routes and MQTT subscriptions made through `ctx` are released when the instance stops. Keep per-instance state inside `onStart`, never at module scope: the module is shared by every instance.
188
+ - **Stop.** `ctx.onStop(fn)` callbacks run newest first, 5 seconds in total. Afterwards every `ctx` call throws `PluginError` with code `stopped`.
189
+ - **Faults.** An uncaught error or unhandled rejection stops only that instance, which is retried after 5, 30 and 120 seconds.
190
+
191
+ | `ctx` member | Main | UI | |
192
+ |---|:-:|:-:|---|
193
+ | `plugin`, `user`, `workspace` | ✓ | ✓ | Installed identity (`id`, `version`, `apiPath`), the user and the workspace |
194
+ | `config` | ✓ | ✓ | Settings, validated, defaults applied |
195
+ | `activities`, `categories`, `sessions` | ✓ | ✓ | Workspace data and actions |
196
+ | `onDataChange` | ✓ | ✓ | The data changed |
197
+ | `sessions.onStarted`, `onEnded`, `onMarked` | ✓ | | Live session events |
198
+ | `storage` | ✓ | ✓ | Key-value storage |
199
+ | `mqtt`, `routes`, `log`, `onStop` | ✓ | | Main code only |
200
+
201
+ ## UI
202
+
203
+ `connect()` performs a handshake with the web app and resolves to the UI `ctx`. It rejects with `unsupported` when the app doesn't run this UI's API version, and with `unavailable` outside Drift Beacon or after 10 seconds without an answer. `ctx.onDataChange` fires after every update (data, settings or storage).
204
+
205
+ With React, re-render on every update:
206
+
207
+ ```tsx
208
+ import { connect, type UiContext } from "@drift-beacon/plugin/ui";
209
+ import { useSyncExternalStore } from "react";
210
+
211
+ let context: UiContext | null = null;
212
+ let revision = 0;
213
+ const subscribe = (listener: () => void) => (context ? context.onDataChange(listener) : () => {});
214
+
215
+ export function useDriftBeacon(): UiContext {
216
+ useSyncExternalStore(subscribe, () => revision);
217
+ if (!context) throw new Error("Not connected");
218
+ return context;
219
+ }
220
+
221
+ export async function start() {
222
+ context = await connect();
223
+ context.onDataChange(() => {
224
+ revision += 1;
225
+ });
226
+ }
227
+ ```
228
+
229
+ Models keep their identity when their data changes: key `React.memo` and `useMemo` on `model.data`.
230
+
231
+ ## Workspace data
232
+
233
+ `ctx.activities`, `ctx.categories` and `ctx.sessions` return **models**: the row's fields plus links, derived values and actions.
234
+
235
+ ```ts
236
+ const activity = ctx.activities.get(id);
237
+ activity?.category?.name; // links
238
+ activity?.isLive; // the current user has a live session of it
239
+ await activity?.track(); // start a span / mark a point
240
+ activity?.data; // the plain row, a new frozen object whenever it changes
241
+
242
+ ctx.activities.list(); // sorted like the app, archived hidden
243
+ ctx.activities.list({ includeArchived: true });
244
+ ctx.activities.list({ categoryId: null }); // uncategorized
245
+ ctx.categories.list().map((category) => category.activities());
246
+ ctx.sessions.list({ activityId, status: "completed" }); // newest first
247
+ ctx.sessions.live({ mine: true });
248
+ ```
249
+
250
+ - Models read the latest snapshot synchronously; nothing fetches. One model per row: identity stays, `model.data` changes.
251
+ - Lists return the same frozen array until the data changes.
252
+ - A removed row's model keeps its last values with `exists: false`, and its actions reject with `not-found`.
253
+ - `startedAt` and `endedAt` are `Date`s: copy one before changing it.
254
+ - `iconPath` on activities and categories is SVG path data (viewBox `0 0 24 24`), or `null`:
255
+
256
+ ```tsx
257
+ {activity.iconPath && <svg viewBox="0 0 24 24" fill="currentColor"><path d={activity.iconPath} /></svg>}
258
+ ```
259
+
260
+ | Activity | |
261
+ |---|---|
262
+ | `id`, `name`, `description`, `trackingType`, `icon`, `iconPath`, `color`, `categoryId`, `archived`, `sortOrder`, `unit` | Fields (`color` is resolved from the category when needed) |
263
+ | `isSpan`, `isPoint`, `isLive`, `category` | Derived values and links |
264
+ | `sessions(filter?)`, `live({ mine? })` | Its sessions |
265
+ | `start()`, `mark()`, `track()`, `end()` | Actions (`end()` ends the current user's live session of it) |
266
+
267
+ | Session | |
268
+ |---|---|
269
+ | `id`, `activityId`, `type`, `status`, `memberIds`, `startedAt`, `endedAt` | Fields |
270
+ | `isSpan`, `isPoint`, `isLive`, `isMine`, `activity`, `duration(at?)` | Derived values and links |
271
+ | `end()`, `discard()` | Actions on a live span |
272
+
273
+ Categories have `id`, `name`, `description`, `color`, `icon`, `iconPath`, `sortOrder` and `activities(filter?)`.
274
+
275
+ ## Actions and events
276
+
277
+ ```ts
278
+ await ctx.sessions.start(activityId); // or activity.start()
279
+ await ctx.sessions.mark(activityId); // or activity.mark()
280
+ await ctx.sessions.end(sessionId); // or session.end(), activity.end()
281
+ await ctx.sessions.discard(sessionId); // or session.discard()
282
+ ```
283
+
284
+ Actions run as the current user and resolve once `ctx` shows their result. Starting a session ends the user's other live session, as in the app.
285
+
286
+ | | Live session events | Data change events |
287
+ |---|---|---|
288
+ | API | `ctx.sessions.onStarted`, `onEnded(session, reason)`, `onMarked` | `activities/categories/sessions.onChange`, `ctx.onDataChange` |
289
+ | Means | A user **just did** something | The data **is different** now, possibly from a sync burst |
290
+ | Use for | Reacting: start, end, publish, notify | Refreshing what you show |
291
+ | Where | Main code | Main code and UI |
292
+
293
+ Never trigger actions from data changes. Live events fire for everyone's sessions in the workspace: check `session.isMine`.
294
+
295
+ ## Storage
296
+
297
+ Per plugin, user and workspace, shared by main code and the UI. Values must be JSON.
298
+
299
+ ```ts
300
+ const faces = ctx.storage.get<Record<string, string>>("faces") ?? {};
301
+ await ctx.storage.set("faces", { ...faces, [side]: activityId });
302
+ await ctx.storage.remove("draft");
303
+ ctx.storage.onChange((key, value) => { /* changed by main code, a UI or another device */ });
304
+ ```
305
+
306
+ ## Configuration
307
+
308
+ `ctx.config` holds the settings from the manifest's `configuration`. `dbplugin prepare` turns them into types (`.drift-beacon/config.d.ts`), so `ctx.config.mqttTopic` is typed in main code and the UI. A setting is optional unless it is `required` or has a `default`. An instance only runs with valid settings; changing them restarts it.
309
+
310
+ ## MQTT and HTTP routes
311
+
312
+ Main code only.
313
+
314
+ ```ts
315
+ ctx.mqtt.subscribe("zigbee2mqtt/cube/#", ({ topic, payload }) => { /* payload is a string */ });
316
+ await ctx.mqtt.publish("zigbee2mqtt/lamp/set", JSON.stringify({ state: "ON" }));
317
+
318
+ ctx.routes.post("webhook", async ({ body }) => ({ status: 202, body: { ok: true } }));
319
+ ctx.routes.get("status", () => ({ body: { live: ctx.sessions.live({ mine: true }).length } }));
320
+ ```
321
+
322
+ - MQTT uses the workspace's broker; `publish` rejects with `unavailable` without one.
323
+ - Routes are served at `<server>/api/plugins/<plugin id>/api/<name>` (`ctx.plugin.apiPath` is the path up to `/api`). Callers send `Authorization: Bearer <workspace API key>`, which picks the user and workspace. Handlers return `{ status?, body? }`; `body` is sent as JSON.
324
+
325
+ ## Errors and limits
326
+
327
+ Operations reject with `PluginError`. Check `error.code`, not `instanceof`:
328
+
329
+ | Code | Meaning |
330
+ |---|---|
331
+ | `unsupported` | The app doesn't support this API version or request |
332
+ | `invalid` | Bad arguments (tracking a point with `start()`, a value that isn't JSON, …) |
333
+ | `not-found` | The activity or session doesn't exist, or the model was removed |
334
+ | `unavailable` | Something needed isn't there: no MQTT broker, no answer from the app |
335
+ | `stopped` | The instance has stopped |
336
+ | `failed` | Anything else, such as tracking an archived activity |
337
+
338
+ | | Limit |
339
+ |---|---|
340
+ | `onStart` / `onStop` | 15 s / 5 s |
341
+ | Blocking the event loop | The server restarts the plugin host after 10 s |
342
+ | Actions | 10 s, then `unavailable` |
343
+ | Route handlers | 30 s, then 504 |
344
+ | Release package | 32 MiB zipped, 128 MiB unpacked, 2,000 files |
345
+
346
+ ## Developing against your server
347
+
348
+ On a Drift Beacon server you run yourself, set `DEV_PLUGINS_PATH` to the folder that contains your plugin folders and start the server in development mode. It lists each plugin under **Plugins → Development**. Enable it in a workspace, then run `npm run dev` in the plugin folder: every save rebuilds main, the server restarts the plugin, and its status, logs and errors (with stack traces pointing at your source) print in your terminal. The UI updates in place.
349
+
350
+ ## Releasing a plugin
351
+
352
+ Plugins are published on GitHub. In a public repository, each immediate child folder with a `manifest.json` is a plugin. For each release:
353
+
354
+ 1. Bump `version` in `manifest.json` and commit.
355
+ 2. Run `npm run release` (or `npx dbplugin pack`), which writes `releases/<id>.zip`.
356
+ 3. Create a GitHub release tagged `<id>-<version>` (for example `hello-1.0.0`) and attach `<id>.zip`.
357
+
358
+ Users add the repository URL in **Plugins → Store** and install from there. Don't commit `dist/` or `releases/`.
359
+
360
+ ## Versions
361
+
362
+ - `apiVersion` in the manifest must match the SDK: `dbplugin`, `vite build` and the server all check it.
363
+ - Before 1.0, each minor version (`0.1`, `0.2`) may change the API, and a server runs only its exact version. Install the SDK with `@0.1` to stay on it.
364
+ - Minor releases may add fields and new string values (such as a new `trackingType`): ignore what you don't recognise.
365
+
366
+ ## License
367
+
368
+ MIT
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Committed, so pnpm can link the `dbplugin` bin before the SDK is built. The CLI itself is dist/cli.js.
3
+ const cli = new URL("../dist/cli.js", import.meta.url);
4
+ try {
5
+ await import(cli.href);
6
+ } catch (error) {
7
+ // Only a missing dist/cli.js means an unbuilt SDK; a missing import inside it is a real error.
8
+ if (error?.code !== "ERR_MODULE_NOT_FOUND" || error.url !== cli.href) throw error;
9
+ console.error("Build the SDK first: pnpm --filter @drift-beacon/plugin build (in the Drift Beacon core checkout)");
10
+ process.exitCode = 1;
11
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `dbplugin` (Drift Beacon plugins), run through `bin/dbplugin.js`:
3
+ *
4
+ * dbplugin prepare [root] Generate .drift-beacon/ (config types and tsconfigs). Lenient: a manifest
5
+ * problem only warns, because this runs as the plugin's postinstall.
6
+ * dbplugin pack [root] [--out <dir>] Typecheck, build fresh and write <out>/<id>.zip (default releases/).
7
+ * Prints only the result JSON on stdout.
8
+ */
9
+ import path from "node:path";
10
+ import { parseArgs } from "node:util";
11
+ import { pack } from "./pack.js";
12
+ import { prepare } from "./prepare.js";
13
+ const USAGE = "Usage: dbplugin prepare [root]\n dbplugin pack [root] [--out <dir>]";
14
+ try {
15
+ const { values, positionals } = parseArgs({
16
+ allowPositionals: true,
17
+ options: { help: { type: "boolean", short: "h" }, out: { type: "string" } },
18
+ });
19
+ const [command, root = ".", ...extra] = positionals;
20
+ if (values.help) {
21
+ console.log(USAGE);
22
+ }
23
+ else if (command === "prepare" && extra.length === 0 && values.out === undefined) {
24
+ prepare(path.resolve(root), { strict: false });
25
+ }
26
+ else if (command === "pack" && extra.length === 0) {
27
+ const result = await pack(path.resolve(root), values.out === undefined ? {} : { out: path.resolve(values.out) });
28
+ console.log(JSON.stringify(result, null, 2));
29
+ }
30
+ else {
31
+ console.error(USAGE);
32
+ process.exitCode = 1;
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.error(error instanceof Error ? error.message : String(error));
37
+ process.exitCode = 1;
38
+ }
package/dist/dev.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The `pnpm dev` handshake between `driftBeacon()` and the Drift Beacon server. The dev tool writes a
3
+ * marker into the package's `dist/`; the server polls for it, connects to Vite's HMR socket and trusts
4
+ * the socket only after a `welcome` whose `session` equals the marker's. Messages travel as Vite custom
5
+ * events (`{ type: "custom", event, data }`). All wording is rendered on the server; the tool only prints.
6
+ */
7
+ /** File name of the marker in `dist/`. Written as tmp, then renamed, synchronously. Never packed. */
8
+ export declare const DEV_MARKER = ".drift-beacon-dev.json";
9
+ export interface DevMarker {
10
+ readonly v: 1;
11
+ /** `manifest.id`; the server routes logs and the dev UI URL by it. */
12
+ readonly pluginId: string;
13
+ /** Hash of the built package; null until `dist/main/index.js` exists. A new id means a new build. */
14
+ readonly buildId: string | null;
15
+ /** One per `pnpm dev` process and package root; null after `vite build`. */
16
+ readonly session: string | null;
17
+ /** The dev server's UI URL, for example `http://localhost:5174/`. */
18
+ readonly uiUrl: string | null;
19
+ /** The dev server's HMR socket, for example `ws://localhost:5174/`. */
20
+ readonly wsUrl: string | null;
21
+ }
22
+ /** Server → tool, event `drift-beacon:hello`. */
23
+ export interface DevHello {
24
+ readonly v: 1;
25
+ readonly session: string;
26
+ }
27
+ /** Tool → server, event `drift-beacon:welcome`, sent only to the socket that said hello. */
28
+ export interface DevWelcome {
29
+ readonly v: 1;
30
+ readonly session: string;
31
+ readonly pluginId: string;
32
+ }
33
+ /** Server → tool, event `drift-beacon:print`: one line (or block) for the plugin's terminal. */
34
+ export interface DevPrint {
35
+ readonly level: "info" | "success" | "warn" | "error";
36
+ readonly text: string;
37
+ }
38
+ //# sourceMappingURL=dev.d.ts.map
package/dist/dev.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `pnpm dev` handshake between `driftBeacon()` and the Drift Beacon server. The dev tool writes a
3
+ * marker into the package's `dist/`; the server polls for it, connects to Vite's HMR socket and trusts
4
+ * the socket only after a `welcome` whose `session` equals the marker's. Messages travel as Vite custom
5
+ * events (`{ type: "custom", event, data }`). All wording is rendered on the server; the tool only prints.
6
+ */
7
+ /** File name of the marker in `dist/`. Written as tmp, then renamed, synchronously. Never packed. */
8
+ export const DEV_MARKER = ".drift-beacon-dev.json";
@@ -0,0 +1,7 @@
1
+ import type { PluginErrorCode } from "./types.ts";
2
+ /** Rejection from a platform operation. Check `code` rather than `instanceof`, which differs between SDK copies. */
3
+ export declare class PluginError extends Error {
4
+ readonly code: PluginErrorCode;
5
+ constructor(code: PluginErrorCode, message: string);
6
+ }
7
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Rejection from a platform operation. Check `code` rather than `instanceof`, which differs between SDK copies. */
2
+ export class PluginError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "PluginError";
7
+ this.code = code;
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Drift Beacon plugin SDK: the API for main code (runs on the server). Plugin UIs import from `@drift-beacon/plugin/ui`.
3
+ *
4
+ * At run time the plugin host supplies this module, so `driftBeacon()` keeps it external when building main.
5
+ */
6
+ export { PluginError } from "./errors.ts";
7
+ export type * from "./main.ts";
8
+ export { definePlugin } from "./main.ts";
9
+ export type * from "./types.ts";
10
+ export { API_VERSION } from "./version.ts";
11
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Drift Beacon plugin SDK: the API for main code (runs on the server). Plugin UIs import from `@drift-beacon/plugin/ui`.
3
+ *
4
+ * At run time the plugin host supplies this module, so `driftBeacon()` keeps it external when building main.
5
+ */
6
+ export { PluginError } from "./errors.js";
7
+ export { definePlugin } from "./main.js";
8
+ export { API_VERSION } from "./version.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Requests an instance's main code sends to the server through its plugin host. The server runs them
3
+ * as the instance's user in the instance's workspace.
4
+ */
5
+ export type PluginAction = {
6
+ readonly name: "startSession";
7
+ readonly activityId: string;
8
+ } | {
9
+ readonly name: "endSession";
10
+ readonly sessionId: string;
11
+ } | {
12
+ readonly name: "markPoint";
13
+ readonly activityId: string;
14
+ } | {
15
+ readonly name: "discardSession";
16
+ readonly sessionId: string;
17
+ } | {
18
+ readonly name: "storageSet";
19
+ readonly key: string;
20
+ readonly value: unknown;
21
+ } | {
22
+ readonly name: "storageRemove";
23
+ readonly key: string;
24
+ } | {
25
+ readonly name: "mqttSubscribe";
26
+ readonly topic: string;
27
+ } | {
28
+ readonly name: "mqttUnsubscribe";
29
+ readonly topic: string;
30
+ } | {
31
+ readonly name: "mqttPublish";
32
+ readonly topic: string;
33
+ readonly payload: string;
34
+ };
35
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { RowChange } from "./rows.ts";
2
+ /**
3
+ * A collection of plain rows keyed by id, frozen as they arrive. `replace` applies a full set of rows, keeps the
4
+ * object of every unchanged row (so identity only changes when data does) and reports the changes.
5
+ */
6
+ export declare class Collection<T extends {
7
+ readonly id: string;
8
+ }> {
9
+ private rows;
10
+ private cachedList;
11
+ list(): readonly T[];
12
+ get(id: string): T | undefined;
13
+ replace(incoming: readonly T[]): RowChange<T>[];
14
+ }
15
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { jsonEqual } from "./json-equal.js";
2
+ function deepFreeze(value) {
3
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
4
+ for (const child of Object.values(value))
5
+ deepFreeze(child);
6
+ Object.freeze(value);
7
+ }
8
+ return value;
9
+ }
10
+ /**
11
+ * A collection of plain rows keyed by id, frozen as they arrive. `replace` applies a full set of rows, keeps the
12
+ * object of every unchanged row (so identity only changes when data does) and reports the changes.
13
+ */
14
+ export class Collection {
15
+ rows = new Map();
16
+ cachedList = null;
17
+ list() {
18
+ this.cachedList ??= Object.freeze([...this.rows.values()]);
19
+ return this.cachedList;
20
+ }
21
+ get(id) {
22
+ return this.rows.get(id);
23
+ }
24
+ replace(incoming) {
25
+ const changes = [];
26
+ const next = new Map();
27
+ for (const row of incoming) {
28
+ const previous = this.rows.get(row.id);
29
+ if (!previous) {
30
+ const item = deepFreeze(row);
31
+ next.set(row.id, item);
32
+ changes.push({ type: "added", item });
33
+ }
34
+ else if (jsonEqual(previous, row)) {
35
+ next.set(row.id, previous);
36
+ }
37
+ else {
38
+ const item = deepFreeze(row);
39
+ next.set(row.id, item);
40
+ changes.push({ type: "updated", item, previous });
41
+ }
42
+ }
43
+ for (const [id, item] of this.rows) {
44
+ if (!next.has(id))
45
+ changes.push({ type: "removed", item });
46
+ }
47
+ this.rows = next;
48
+ if (changes.length > 0 || next.size !== this.cachedList?.length)
49
+ this.cachedList = null;
50
+ return changes;
51
+ }
52
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * API versions this build of Drift Beacon runs: the newest minor of each supported major.
3
+ * Before 1.0 only the current version is listed, because every 0.x minor may break plugins.
4
+ * From 1.0, the previous major stays listed for at least six months after its successor ships.
5
+ */
6
+ export declare const SUPPORTED_API_VERSIONS: readonly string[];
7
+ export interface ApiVersion {
8
+ readonly major: number;
9
+ readonly minor: number;
10
+ }
11
+ /** Parse a `major.minor` API version; null when malformed. */
12
+ export declare function parseApiVersion(value: unknown): ApiVersion | null;
13
+ export type ApiCompatibility = {
14
+ readonly compatible: true;
15
+ } | {
16
+ readonly compatible: false;
17
+ readonly reason: "invalid" | "needs-newer-app" | "needs-plugin-update";
18
+ readonly message: string;
19
+ };
20
+ /**
21
+ * Whether a plugin built for `pluginVersion` runs on a build supporting `supported`.
22
+ * Before 1.0 the minor must match exactly; from 1.0 the major must match and the plugin's
23
+ * minor must not be newer than the host's.
24
+ */
25
+ export declare function checkApiCompatibility(pluginVersion: unknown, supported?: readonly string[]): ApiCompatibility;
26
+ //# sourceMappingURL=compatibility.d.ts.map