create-bridgething 0.11.0 → 0.12.1

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 (37) hide show
  1. package/README.md +16 -12
  2. package/dist/index.mjs +476 -142
  3. package/package.json +20 -5
  4. package/template/_claude/skills/bridgething/SKILL.md +12 -17
  5. package/template/_claude/skills/bridgething/reference/develop.md +86 -66
  6. package/template/_claude/skills/bridgething/reference/extension.md +183 -0
  7. package/template/_claude/skills/bridgething/reference/sdk.md +86 -105
  8. package/template/_claude/skills/bridgething/reference/ship.md +19 -38
  9. package/template/_gitignore +1 -0
  10. package/template/package.json +6 -5
  11. package/template/scripts/bridgething.ts +1086 -0
  12. package/template/scripts/push.ts +212 -193
  13. package/template/settings/main.tsx +14 -13
  14. package/template/src/App.tsx +3 -6
  15. package/template/src/daemon.ts +14 -0
  16. package/template/tsconfig.json +1 -1
  17. package/template/vite.config.ts +5 -3
  18. package/template/vite.settings.config.ts +3 -3
  19. package/template-extension/_claude_append.md +23 -0
  20. package/template-extension/extension/main.ts +20 -0
  21. package/template-launcher/_claude_append.md +14 -30
  22. package/template-launcher/src/App.tsx +2 -5
  23. package/template-overlay/_claude_append.md +31 -45
  24. package/template-overlay/vite.overlay.config.ts +1 -1
  25. package/template-source/CLAUDE.md +104 -0
  26. package/template-source/README.md +57 -0
  27. package/template-source/_bunversion +1 -0
  28. package/template-source/_github/workflows/ci.yml +28 -0
  29. package/template-source/_github/workflows/publish.yml +33 -0
  30. package/template-source/_gitignore +7 -0
  31. package/template-source/_prettierrc.js +10 -0
  32. package/template-source/apps/.gitkeep +0 -0
  33. package/template-source/package.json +27 -0
  34. package/template-source/source.json +8 -0
  35. package/template-versions.json +6 -0
  36. package/template/CLAUDE.md +0 -95
  37. package/template/README.md +0 -70
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "create-bridgething",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
- "description": "Scaffold a new bridgething webapp (React + Vite + Tailwind v4)",
5
+ "description": "Scaffold a bridgething app source: webapps for the Spotify Car Thing, published as a catalog.v1 on GitHub Pages.",
6
6
  "license": "MIT",
7
7
  "author": "Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com> (https://github.com/JoeyEamigh)",
8
8
  "homepage": "https://bridgething.com",
@@ -14,7 +14,14 @@
14
14
  "bugs": {
15
15
  "url": "https://github.com/JoeyEamigh/bridgething/issues"
16
16
  },
17
- "keywords": ["bridgething", "spotify", "car-thing", "superbird", "create", "scaffold"],
17
+ "keywords": [
18
+ "bridgething",
19
+ "spotify",
20
+ "car-thing",
21
+ "superbird",
22
+ "create",
23
+ "scaffold"
24
+ ],
18
25
  "publishConfig": {
19
26
  "access": "public",
20
27
  "registry": "https://registry.npmjs.org/"
@@ -24,19 +31,27 @@
24
31
  },
25
32
  "files": [
26
33
  "dist",
34
+ "template-versions.json",
27
35
  "template",
36
+ "template-source",
37
+ "template-extension",
28
38
  "template-launcher",
29
39
  "template-overlay"
30
40
  ],
31
41
  "scripts": {
32
- "prebuild": "bun scripts/sync-push.ts",
42
+ "prebuild": "bun scripts/sync-shared.ts",
33
43
  "build": "tsdown",
34
44
  "dev": "tsdown --watch",
35
- "typecheck": "tsc --noEmit"
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "bun test"
36
47
  },
37
48
  "devDependencies": {
49
+ "@bridgething/extension": "0.12.1",
50
+ "@msgpack/msgpack": "^3.1.3",
51
+ "@types/bun": "^1.4.0",
38
52
  "@types/node": "^26.1.1",
39
53
  "@types/uuid": "^11.0.0",
54
+ "esbuild": "^0.28.2",
40
55
  "tsdown": "^0.21.10"
41
56
  },
42
57
  "peerDependencies": {
@@ -1,25 +1,20 @@
1
1
  ---
2
2
  name: bridgething
3
- description: Build, run, and ship a bridgething webapp for the Spotify Car Thing. Covers @bridgething/client (the SDK a webapp uses to reach the on-device daemon - now-playing, artwork, storage, settings, networking, all 18 surfaces), the loop for running the app and driving its 800x480 screen and physical controls from a browser you control, and installing the built app onto a device or packaging it as a shareable zip. Use whenever calling the daemon, running or previewing the app, pressing its controls, or pushing it to hardware.
3
+ description: Build, run, and ship a bridgething webapp for the Spotify Car Thing. Use for @bridgething/client calls (now-playing, transport, artwork, storage, user settings, networking, every surface), running the dev server and driving the 800x480 screen and its physical controls, pushing the built app to a device or packaging a shareable zip, and writing the desktop-side Deno extension a webapp can ship.
4
4
  ---
5
5
 
6
6
  # Building a bridgething webapp
7
7
 
8
- A bridgething webapp is a single-page React UI that runs full-screen in the
9
- device's chromium kiosk and reaches the on-device daemon through one typed
10
- client. You build it against a browser on your own machine and push it to real
11
- hardware only for final checks.
12
-
13
- Three jobs. Read the reference file for the one in front of you:
14
-
15
- - **Call the daemon** - now-playing, transport, artwork, storage, settings,
16
- networking, and the full 18-surface index: [reference/sdk.md](reference/sdk.md)
17
- - **Run and drive the app** - dev server, screenshot the 800x480 screen, press
18
- the physical controls (locally, then over CDP on the device):
8
+ - Calling the daemon, from now-playing to networking, plus every surface the SDK
9
+ ships: [reference/sdk.md](reference/sdk.md)
10
+ - Running the app, screenshotting the 800x480 screen, and pressing the physical
11
+ controls locally and over CDP on the device:
19
12
  [reference/develop.md](reference/develop.md)
20
- - **Install and share** - build, push onto a connected Car Thing, package a zip
21
- anyone can install, and keep the device on the latest bridgething release:
22
- [reference/ship.md](reference/ship.md)
13
+ - Installing onto a connected Car Thing, packaging a zip, and updating the
14
+ device: [reference/ship.md](reference/ship.md)
15
+ - The desktop-side Deno extension, its `ctx` contract, and its permission model,
16
+ when the project has an `extension/` directory:
17
+ [reference/extension.md](reference/extension.md)
23
18
 
24
- The device shape, the control-to-event mapping, and the hard constraints live in
25
- CLAUDE.md, which is always loaded. This skill is the how-to on top of it.
19
+ CLAUDE.md holds the device shape, the control-to-event mapping, and the manifest
20
+ fields.
@@ -1,39 +1,46 @@
1
1
  # Running and driving the app
2
2
 
3
- The device is an 800x480 landscape screen with five physical controls plus touch.
4
- To develop effectively you (1) run the app, (2) see it at the real size, and
5
- (3) press the buttons. The fast loop drives a browser you control on your own
6
- machine; you only need the physical Car Thing for final validation.
3
+ ## Start the dev server
7
4
 
8
- ## Contents
5
+ ```bash
6
+ bun run dev
7
+ ```
9
8
 
10
- - Start the dev server
11
- - Drive it with Playwright (recommended for agents)
12
- - Alternative: your own browser + DevTools
13
- - Drive the real device over CDP
14
- - Input reference
9
+ Vite serves at `http://localhost:5173/` with hot reload. The page reaches the
10
+ daemon on the Car Thing plugged in over USB through the dev server's
11
+ `/__bridgething` proxy, so now-playing, config, and geo are the real thing rather
12
+ than fixtures. `SUPERBIRD_HOST=bridgething-<serial>.local` targets another
13
+ device, and `BRIDGETHING_DAEMON_URL=ws://host:8891` targets a daemon anywhere
14
+ else.
15
15
 
16
- ## Start the dev server
16
+ ## Show it on the Car Thing with hot reload
17
17
 
18
18
  ```bash
19
- bun run dev
19
+ bun run dev:device
20
20
  ```
21
21
 
22
- Vite serves at `http://localhost:5173/` with hot reload.
22
+ On start it installs the app once if the daemon has never seen it, makes it the
23
+ active webapp so config, permissions, and forwards are yours, then points the
24
+ kiosk at `http://<your usb address>:5173/`. Every save from then on hot-reloads
25
+ on the real display. Ctrl-C hands the screen back to the installed build.
23
26
 
24
- ## Drive it with Playwright (recommended for agents)
27
+ ## The extension half
25
28
 
26
- Playwright is **not** installed by default (a human clicking around does not need
27
- it). Add it only when you want to see and drive the app programmatically:
29
+ When `public/manifest.json` declares `extension`, both dev commands also bundle
30
+ `extension/main.ts`, run it under Deno with the manifest's permissions, and link
31
+ it to the same Car Thing. Every save rebuilds and restarts it, and `ctx.log.*`
32
+ prints in the vite terminal. The contract is in
33
+ [reference/extension.md](extension.md).
34
+
35
+ ## Drive it with Playwright
36
+
37
+ To screenshot and drive the app from a script, add Playwright:
28
38
 
29
39
  ```bash
30
40
  bun add -d playwright
31
41
  bunx playwright install chromium
32
42
  ```
33
43
 
34
- Then a short script gives you a locked 800x480 window you can screenshot and send
35
- input to:
36
-
37
44
  ```ts
38
45
  import { chromium } from 'playwright';
39
46
 
@@ -44,50 +51,47 @@ await page.goto('http://localhost:5173/');
44
51
  await page.screenshot({ path: 'screen.png' }); // look at the result
45
52
 
46
53
  // physical controls -> the exact events the device sends:
47
- await page.keyboard.press('1'); // preset 1 (also '2' '3' '4')
48
- await page.keyboard.press('m'); // Mode button
49
- await page.keyboard.press('Escape'); // Back button
50
- await page.mouse.wheel(120, 0); // rotary wheel: HORIZONTAL scroll (deltaX)
54
+ await page.keyboard.press('1'); // preset 1 (also '2' '3' '4')
55
+ await page.keyboard.press('m'); // Mode button
56
+ await page.keyboard.press('Escape'); // Back button
57
+ await page.mouse.wheel(120, 0); // rotary wheel: HORIZONTAL scroll (deltaX)
51
58
  // touch: page.tap('selector') or page.mouse.click(x, y)
52
59
  ```
53
60
 
54
- Loop: edit code -> vite hot-reloads -> screenshot -> adjust. Keep the viewport at
55
- exactly `{ width: 800, height: 480 }` so what you see matches the device.
61
+ Loop: edit code, let vite hot-reload, screenshot, adjust. Keep the viewport at
62
+ `{ width: 800, height: 480 }` so what you see matches the device.
56
63
 
57
- ## Alternative: your own browser + DevTools
64
+ ## Your own browser and DevTools
58
65
 
59
- Open `http://localhost:5173/` in your normal browser, then in DevTools' device
60
- toolbar set a custom device of **800 x 480**. You get real DevTools plus the same
61
- keys (type `1`-`4`, `m`, `Escape`; shift+wheel for the rotary's horizontal
62
- scroll). This is the recommended manual path.
66
+ Open `http://localhost:5173/`, then set a custom device of 800 x 480 in DevTools'
67
+ device toolbar. Type `1` through `4`, `m`, and `Escape`, and hold shift while
68
+ scrolling for the rotary's horizontal wheel.
63
69
 
64
70
  ## Drive the real device over CDP
65
71
 
66
- To see and drive the app on real hardware, talk CDP to the device's own chromium.
67
- CDP input goes straight into the page (never through the physical buttons), so the
68
- launcher gesture and on-device browser-nav side effects never interfere.
72
+ CDP input goes into the page, so the launcher gesture and browser-nav side
73
+ effects stay out of the way.
69
74
 
70
- Install the app onto the device first so the kiosk is showing it (build + push).
71
- CDP is served at `bridgething.local:9222`, reachable directly over USB:
75
+ Get the app on the kiosk first with `bun run dev:device` or `bun run push`.
76
+ Chromium listens on loopback, but the image proxies it, so CDP answers on
77
+ `bridgething.local:9222` over USB:
72
78
 
73
79
  ```bash
74
- # device answers at bridgething.local over USB; root has no password
75
80
  curl -s http://bridgething.local:9222/json/version >/dev/null && echo cdp-up
76
81
  ```
77
82
 
78
- Use the `webSocketDebuggerUrl` exactly as returned. It comes back as an IP rather
79
- than the hostname you asked for, and it is already correct - do not rewrite it.
83
+ Use the `webSocketDebuggerUrl` exactly as returned. It comes back as an IP
84
+ rather than the hostname you asked for, and that address is the correct one.
80
85
 
81
- **Playwright's `connectOverCDP` hangs against the device's embedded chromium** -
82
- it never finishes the websocket upgrade (a quirk of the device's `cast_shell`
83
- build; a plain websocket connects fine). Drive it with a raw CDP websocket
84
- instead. Connect to the `page` target from `/json` (the page-level endpoint, not
85
- the browser-level one). This script screenshots the live device and presses a
86
- button; it is the whole loop:
86
+ Playwright's `connectOverCDP` never finishes the websocket upgrade against the
87
+ device's embedded chromium, so drive it with a raw CDP websocket. Connect to the
88
+ `page` target from `/json`, not the browser-level endpoint.
87
89
 
88
90
  ```ts
89
91
  // drive.ts -> run with: bun drive.ts
90
- const targets = (await fetch('http://bridgething.local:9222/json').then(r => r.json())) as any[];
92
+ const targets = (await fetch('http://bridgething.local:9222/json').then(r =>
93
+ r.json(),
94
+ )) as any[];
91
95
  const page = targets.find(t => t.type === 'page');
92
96
  const ws = new WebSocket(page.webSocketDebuggerUrl);
93
97
  await new Promise<void>((res, rej) => {
@@ -99,45 +103,61 @@ let id = 0;
99
103
  const pending = new Map<number, (v: any) => void>();
100
104
  ws.onmessage = ev => {
101
105
  const m = JSON.parse(ev.data as string);
102
- if (m.id && pending.has(m.id)) (pending.get(m.id)!(m.result), pending.delete(m.id));
106
+ if (m.id && pending.has(m.id))
107
+ (pending.get(m.id)!(m.result), pending.delete(m.id));
103
108
  };
104
109
  const send = (method: string, params: any = {}) =>
105
- new Promise<any>(res => (pending.set(++id, res), ws.send(JSON.stringify({ id, method, params }))));
110
+ new Promise<any>(
111
+ res => (
112
+ pending.set(++id, res),
113
+ ws.send(JSON.stringify({ id, method, params }))
114
+ ),
115
+ );
106
116
 
107
117
  // screenshot
108
118
  const { data } = await send('Page.captureScreenshot', { format: 'png' });
109
119
  await Bun.write('device.png', Buffer.from(data, 'base64'));
110
120
 
111
- // read what is on screen (great for asserting a change happened)
112
- const text = await send('Runtime.evaluate', { expression: 'document.body.innerText', returnByValue: true });
121
+ // read what is on screen, to assert a change happened
122
+ const text = await send('Runtime.evaluate', {
123
+ expression: 'document.body.innerText',
124
+ returnByValue: true,
125
+ });
113
126
  console.log(text.result.value);
114
127
 
115
128
  // press a button: keyDown + keyUp. key/code/text must match a real key.
116
129
  const key = (type: string, k: string, code: string, vk: number) =>
117
- send('Input.dispatchKeyEvent', { type, key: k, code, text: k.length === 1 ? k : '', windowsVirtualKeyCode: vk });
118
- await key('keyDown', '3', 'Digit3', 51); // preset 3
130
+ send('Input.dispatchKeyEvent', {
131
+ type,
132
+ key: k,
133
+ code,
134
+ text: k.length === 1 ? k : '',
135
+ windowsVirtualKeyCode: vk,
136
+ });
137
+ await key('keyDown', '3', 'Digit3', 51); // preset 3
119
138
  await key('keyUp', '3', 'Digit3', 51);
120
139
 
121
140
  // rotary wheel = horizontal scroll:
122
- await send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: 400, y: 240, deltaX: 120, deltaY: 0 });
141
+ await send('Input.dispatchMouseEvent', {
142
+ type: 'mouseWheel',
143
+ x: 400,
144
+ y: 240,
145
+ deltaX: 120,
146
+ deltaY: 0,
147
+ });
123
148
 
124
149
  process.exit(0);
125
150
  ```
126
151
 
127
- Read the screenshot and the `innerText` to confirm the app reacted. Prefer the
128
- local loop for fast iteration; use the device to confirm real behavior (real
129
- now-playing, real artwork, the actual screen).
152
+ Iterate in the local loop, then confirm real now-playing, real artwork, and the
153
+ actual screen on the device.
130
154
 
131
155
  ## Input reference
132
156
 
133
- The controls arrive as ordinary browser events. Listen for them with a `keydown`
134
- handler and a `wheel` handler on `window`. Make horizontal wheel scroll move
135
- through your primary list; it is the control users reach for first.
136
-
137
- | Control | Browser event | CDP `key`/`code`/vk |
138
- | ------------ | ------------------------------------ | ---------------------- |
139
- | Preset 1-4 | `keydown` key `"1"` `"2"` `"3"` `"4"` | `Digit1`-`Digit4` / 49-52 |
140
- | Mode (M) | `keydown` key `"m"` | `KeyM` / 77 |
141
- | Back | `keydown` key `"Escape"` | `Escape` / 27 |
142
- | Rotary wheel | `wheel` with `deltaX` (horizontal) | mouseWheel `deltaX` |
143
- | Touch | pointer / touch events | `Input.dispatchTouchEvent` |
157
+ | Control | Browser event | CDP `key`/`code`/vk |
158
+ | ------------ | ------------------------------------- | -------------------------- |
159
+ | Preset 1-4 | `keydown` key `"1"` `"2"` `"3"` `"4"` | `Digit1`-`Digit4` / 49-52 |
160
+ | Mode (M) | `keydown` key `"m"` | `KeyM` / 77 |
161
+ | Back | `keydown` key `"Escape"` | `Escape` / 27 |
162
+ | Rotary wheel | `wheel` with `deltaX` (horizontal) | mouseWheel `deltaX` |
163
+ | Touch | pointer / touch events | `Input.dispatchTouchEvent` |
@@ -0,0 +1,183 @@
1
+ # Extensions (`@bridgething/extension`)
2
+
3
+ An extension is the desktop-side half of a webapp: a Deno process the bridgething
4
+ desktop app spawns, one per installed and enabled app. It has whatever host
5
+ access the manifest asks for, and reaches every connected Car Thing through the
6
+ daemon's forward surface.
7
+
8
+ ## Shape of an extension
9
+
10
+ ```ts
11
+ import { asJson, defineExtension, json } from '@bridgething/extension';
12
+
13
+ let timer: ReturnType<typeof setInterval> | undefined;
14
+
15
+ defineExtension({
16
+ start(ctx) {
17
+ ctx.on('device', event => ctx.log.info(event.type, event.device.name));
18
+ ctx.on('message', (device, message) =>
19
+ device.send(json({ echo: asJson(message) })),
20
+ );
21
+ timer = setInterval(() => ctx.broadcast(json({ tick: Date.now() })), 5_000);
22
+ },
23
+ stop() {
24
+ clearInterval(timer);
25
+ },
26
+ });
27
+ ```
28
+
29
+ `defineExtension` runs `start` after the host's `hello`, sends `ready` when the
30
+ returned promise settles, and on `stop`, or when the host closes stdin, runs
31
+ `stop` and exits. The extension runs device or no device.
32
+
33
+ - Stdout is the protocol. Log with `ctx.log.*` and keep `console.error` on stderr
34
+ for scratch debugging.
35
+ - Register listeners synchronously at the top of `start`. The host replays every
36
+ live device immediately after it calls `start`, so a listener added after an
37
+ `await` misses them.
38
+
39
+ ## The `ctx` contract
40
+
41
+ Read `node_modules/@bridgething/extension/dist/*.d.ts` for the exact signatures.
42
+
43
+ Identity:
44
+
45
+ - `ctx.api` is the host protocol revision, `1`.
46
+ - `ctx.webapp` is `{ id, name, version }` from `manifest.json`.
47
+ - `ctx.dataDir` is a directory this extension owns. The kv store lives there.
48
+
49
+ Devices:
50
+
51
+ - `ctx.devices` is the connected devices, recomputed on read.
52
+ - `ctx.device(id)` returns a handle for any id, connected or not. It never
53
+ throws.
54
+ - `ctx.on('device', event => ...)` streams `event.type` of `connected`,
55
+ `disconnected`, or `active`, with `event.device` as the handle.
56
+ - A handle carries `id`, `name`, `active`, `connected`, `config`, and `send`, and
57
+ survives a disconnect with `connected` false.
58
+
59
+ Messages:
60
+
61
+ - `ctx.device(id).send(message)` and `ctx.broadcast(message)`. Broadcast reaches
62
+ every connected device where this webapp is active.
63
+ - `ctx.on('message', (device, message) => ...)` takes forwards coming back.
64
+ - Both directions are fire and forget. Build your own correlation for
65
+ request and reply.
66
+ - The daemon delivers a forward to the active webapp, so check `device.active`. A
67
+ send to a backgrounded app is dropped silently.
68
+
69
+ Config:
70
+
71
+ - `ctx.config(device)` is a snapshot of that device's settings, keyed by
72
+ `manifest.json` config key. Values are strings.
73
+ - `ctx.on('config', (device, key, value) => ...)` fires when a settings page
74
+ writes, from any companion. `value` is `null` when the user cleared the key,
75
+ and the key leaves the snapshot. An empty string is a real empty value.
76
+
77
+ Storage, auth, logging:
78
+
79
+ - `ctx.kv.get(key)`, `.set(key, value)`, `.delete(key)`, and `.list()` are a
80
+ persistent JSON store scoped to this extension. `get` resolves `undefined` for
81
+ a missing key.
82
+ - `ctx.auth.authorize(url)` opens the system browser and resolves with the full
83
+ callback URL, query string included. It rejects when the user backs out.
84
+ - `ctx.log.debug/info/warn/error(...args)` writes into the host's log: the vite
85
+ terminal in development, the desktop app's log once installed.
86
+ - `kv` and `auth` failures reject with an `ExtensionError` carrying a `kind` of
87
+ `host-error`, `disconnected`, or `write-failed`.
88
+
89
+ Persist eagerly as state changes. The host drops your stdin the moment it sends
90
+ `stop`, so a `ctx.kv` or `ctx.auth` call made from `stop` rejects with
91
+ `kind: 'disconnected'`. Use `stop` to release timers, sockets, and children.
92
+
93
+ ## Forward messages
94
+
95
+ A forward is `{ encoding, data }`, the same type the webapp SDK uses:
96
+
97
+ | encoding | reading | writing |
98
+ | -------- | ----------------------------------- | -------------------------------------- |
99
+ | `text` | `asText(message)` -> `string` | `send('hi')` or `send(text('hi'))` |
100
+ | `json` | `asJson<T>(message)` -> `T` | `send(json(value))` |
101
+ | `binary` | `asBinary(message)` -> `Uint8Array` | `send(bytes)` or `send(binary(bytes))` |
102
+
103
+ The narrowing helpers return `undefined` when the encoding does not match, so
104
+ `asJson(message) ?? fallback` is the normal shape. Wrap a plain object in
105
+ `json(...)` before sending, because `send` cannot tell one apart from a
106
+ `ForwardMessage`. Binary always arrives as a `Uint8Array`.
107
+
108
+ ## Manifest block and permissions
109
+
110
+ ```json
111
+ "extension": {
112
+ "entry": "extension/desktop.mjs",
113
+ "permissions": ["all"],
114
+ "api": 1
115
+ }
116
+ ```
117
+
118
+ - `entry` is the path inside the built bundle. It must exist, or the daemon
119
+ refuses the install.
120
+ - `api` is the host protocol revision. `1` is frozen.
121
+ - `permissions` are Deno permission descriptors, handed to the runtime as flags.
122
+
123
+ Descriptor grammar, one string per entry: `all`, `net`, `net:<host[:port]>`,
124
+ `read`, `read:<path>`, `write`, `write:<path>`, `run`, `run:<binary>`, `env`,
125
+ `env:<VAR>`, `sys`, `sys:<kind>`, `ffi`, `ffi:<path>`. A path may start with `~`.
126
+
127
+ Narrow `["all"]` before you publish. The dev loop runs the extension with exactly
128
+ the declared flags, so it fails on your machine the moment you use something you
129
+ did not declare. The store card lists the permissions and the desktop app makes
130
+ the user confirm them at install. A version declaring `extension` must publish
131
+ from a public GitHub repo.
132
+
133
+ ## The dev loop
134
+
135
+ ```bash
136
+ bun run dev # page in your browser + extension under deno, both on the connected Car Thing
137
+ bun run dev:device # the same, with the page on the Car Thing's own screen
138
+ bun run build # webapp + settings page + dist/extension/desktop.mjs
139
+ bun run share # zips dist/, extension included
140
+ ```
141
+
142
+ Both dev commands:
143
+
144
+ 1. Make this app the active webapp on the Car Thing, installing it once if the
145
+ device has never seen it, so forwards route.
146
+ 2. Bundle `extension/main.ts` with esbuild and run it under Deno with the
147
+ manifest's permissions, from the gitignored `.dev-extension/`, which also
148
+ holds the kv store. Every save rebuilds and restarts it. A build error prints
149
+ and leaves the previous build running.
150
+ 3. Link to the daemon over USB as an extension host, so the extension gets
151
+ `device.connected`, `device.active`, `config.changed`, and every forward the
152
+ page sends. With the Car Thing unplugged it links up when one appears.
153
+
154
+ `ctx.log.*` and stderr both print in the vite terminal. A crash restarts with
155
+ backoff, or immediately on the next save; a tight loop shows as `crashed` in the
156
+ desktop app's row. Ctrl-C stops the extension and, under `dev:device`, hands the
157
+ screen back. Disable this app's extension in the desktop app while you develop,
158
+ or the page hears both copies.
159
+
160
+ Under `bun run dev`, `ctx.auth.authorize(url)` ends on bridgething.com's callback
161
+ page. Copy that page's address and paste it at
162
+ `http://localhost:5173/__extension/authorize` to deliver it to the extension. The
163
+ desktop app captures the callback itself once the app is installed there.
164
+
165
+ The runtime is the `deno` npm package, pinned to the version the desktop app
166
+ downloads and fetched into `node_modules` on first run. `BRIDGETHING_DENO=/path`
167
+ overrides it.
168
+
169
+ ## Shipping
170
+
171
+ `bun run push` installs the webapp on a Car Thing, and the extension stays on the
172
+ desktop. Once someone installs the app through the desktop app, that app runs the
173
+ extension from the zip. Phones have no extension host, and the desktop app can be
174
+ closed while the device is on a phone, so make the webapp useful with nothing
175
+ attached.
176
+
177
+ `npm:`, `jsr:`, and `node:` specifiers work in the bundle, as do `Deno.Command`
178
+ and unix sockets, subject to the permissions you declared. Deno resolves them at
179
+ runtime only when it is not standing in a project: a `package.json` or
180
+ `deno.json` anywhere above the bundle switches it to node_modules resolution and
181
+ it refuses `npm:` with _"Could not find a matching package"_. The dev loop and
182
+ the desktop app run the bundle from a data directory with
183
+ `DENO_NO_PACKAGE_JSON=1`. Set that variable when you run the bundle by hand.