create-bridgething 0.1.2

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joey Eamigh
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,21 @@
1
+ # create-bridgething
2
+
3
+ Scaffold a new [bridgething](https://github.com/JoeyEamigh/bridgething) webapp, a
4
+ single-page app that runs on the kiosk chromium of a Spotify Car Thing and talks
5
+ to the on-device daemon. The template ships React + Vite + Tailwind v4 with
6
+ [`@bridgething/client`](https://www.npmjs.com/package/@bridgething/client)
7
+ preinstalled.
8
+
9
+ ```sh
10
+ bun create bridgething my-app
11
+ ```
12
+
13
+ ```sh
14
+ npm create bridgething@latest my-app
15
+ ```
16
+
17
+ The generated project includes `bun run push` to deploy the built webapp to a
18
+ connected Car Thing and `bun run share` to share it with the community.
19
+
20
+ - Full docs: <https://bridgething.com/docs>
21
+ - Source: <https://github.com/JoeyEamigh/bridgething>
package/dist/index.mjs ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, extname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { v7 } from "uuid";
7
+ //#region src/index.ts
8
+ //! `create-bridgething` - copy the bundled template into a new directory,
9
+ //! substitute the project name, print next steps. Opinionated stack:
10
+ //! React 19 + Vite + Tailwind v4 + TypeScript strict, plus
11
+ //! `@bridgething/client` preinstalled.
12
+ const TEMPLATE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "template");
13
+ function parseArgs(argv) {
14
+ const positional = [];
15
+ let install = true;
16
+ let git = true;
17
+ for (const arg of argv) if (arg === "--no-install") install = false;
18
+ else if (arg === "--no-git") git = false;
19
+ else if (arg === "--help" || arg === "-h") {
20
+ printHelp();
21
+ process.exit(0);
22
+ } else if (arg.startsWith("-")) {
23
+ console.error(`unknown flag: ${arg}`);
24
+ printHelp();
25
+ process.exit(1);
26
+ } else positional.push(arg);
27
+ if (positional.length !== 1) {
28
+ printHelp();
29
+ process.exit(1);
30
+ }
31
+ return {
32
+ target: positional[0],
33
+ install,
34
+ git
35
+ };
36
+ }
37
+ function printHelp() {
38
+ console.log(`Usage: create-bridgething <target-dir> [--no-install] [--no-git]
39
+
40
+ Scaffold a new bridgething webapp at <target-dir>.
41
+
42
+ Options:
43
+ --no-install Skip 'bun install' after copying.
44
+ --no-git Skip 'git init' after copying.
45
+ `);
46
+ }
47
+ const BINARY_EXT = new Set([
48
+ ".ttf",
49
+ ".otf",
50
+ ".woff",
51
+ ".woff2",
52
+ ".png",
53
+ ".jpg",
54
+ ".jpeg",
55
+ ".gif",
56
+ ".webp",
57
+ ".avif",
58
+ ".ico",
59
+ ".wasm"
60
+ ]);
61
+ function copyTemplate(src, dest, subs) {
62
+ for (const entry of readdirSync(src)) {
63
+ const srcPath = join(src, entry);
64
+ const destPath = join(dest, entry === "_gitignore" ? ".gitignore" : entry === "_claude" ? ".claude" : entry);
65
+ if (statSync(srcPath).isDirectory()) {
66
+ mkdirSync(destPath, { recursive: true });
67
+ copyTemplate(srcPath, destPath, subs);
68
+ } else if (BINARY_EXT.has(extname(entry).toLowerCase())) copyFileSync(srcPath, destPath);
69
+ else writeFileSync(destPath, readFileSync(srcPath, "utf8").replace(/__PROJECT_NAME__/g, subs.projectName).replace(/__WEBAPP_UUID__/g, subs.webappUuid));
70
+ }
71
+ }
72
+ function copyDir(src, dest) {
73
+ mkdirSync(dest, { recursive: true });
74
+ for (const entry of readdirSync(src)) {
75
+ const srcPath = join(src, entry);
76
+ const destPath = join(dest, entry);
77
+ if (statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
78
+ else copyFileSync(srcPath, destPath);
79
+ }
80
+ }
81
+ function linkAgentAliases(target) {
82
+ try {
83
+ symlinkSync("CLAUDE.md", join(target, "AGENTS.md"));
84
+ } catch {
85
+ copyFileSync(join(target, "CLAUDE.md"), join(target, "AGENTS.md"));
86
+ }
87
+ mkdirSync(join(target, ".agents"), { recursive: true });
88
+ try {
89
+ symlinkSync(join("..", ".claude", "skills"), join(target, ".agents", "skills"), "dir");
90
+ } catch {
91
+ copyDir(join(target, ".claude", "skills"), join(target, ".agents", "skills"));
92
+ }
93
+ }
94
+ function run(cmd, args, cwd) {
95
+ return spawnSync(cmd, args, {
96
+ cwd,
97
+ stdio: "inherit"
98
+ }).status === 0;
99
+ }
100
+ function main() {
101
+ const args = parseArgs(process.argv.slice(2));
102
+ const target = resolve(process.cwd(), args.target);
103
+ const projectName = args.target.replace(/^.*[\\/]/, "");
104
+ if (existsSync(target)) {
105
+ if (readdirSync(target).length > 0) {
106
+ console.error(`error: ${target} exists and is not empty`);
107
+ process.exit(1);
108
+ }
109
+ } else mkdirSync(target, { recursive: true });
110
+ const webappUuid = v7();
111
+ console.log(`scaffolding ${projectName} (${webappUuid}) in ${target}`);
112
+ copyTemplate(TEMPLATE_DIR, target, {
113
+ projectName,
114
+ webappUuid
115
+ });
116
+ console.log(" ✓ template copied");
117
+ linkAgentAliases(target);
118
+ console.log(" ✓ agent guides linked (CLAUDE.md, AGENTS.md, /bridgething skill)");
119
+ if (args.git) if (run("git", ["init", "--quiet"], target)) console.log(" ✓ git initialized");
120
+ else console.warn(" ! git init failed (skipping)");
121
+ if (args.install) {
122
+ console.log(" installing dependencies with bun...");
123
+ if (!run("bun", ["install"], target)) console.warn(" ! bun install failed; install manually with `bun install`");
124
+ else console.log(" ✓ dependencies installed");
125
+ }
126
+ console.log(`
127
+ Done! Next steps:
128
+
129
+ cd ${args.target}
130
+ ${args.install ? "" : " bun install\n"} bun run dev # local dev server (http://localhost:5173/)
131
+
132
+ Open this folder with your coding agent (Claude Code, Codex, opencode, ...).
133
+ It reads CLAUDE.md / AGENTS.md and the /bridgething skill in .claude/skills
134
+ (mirrored at .agents/skills), which goes deep on the client API, running and
135
+ driving the app, and installing and sharing it.
136
+
137
+ bun run build # production bundle into dist/
138
+ bun run push <addr> # build + install onto a connected Car Thing (default bridgething.local)
139
+ bun run share # build first, then zip dist/ to hand to friends
140
+
141
+ The starter App connects to ws://127.0.0.1:8891/ on the device, and to
142
+ ws://<device-ip>:8891/ in dev when you set VITE_BRIDGETHING_URL.
143
+ `);
144
+ }
145
+ main();
146
+ //#endregion
147
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "create-bridgething",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Scaffold a new bridgething webapp (React + Vite + Tailwind v4)",
6
+ "license": "MIT",
7
+ "author": "Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com> (https://github.com/JoeyEamigh)",
8
+ "homepage": "https://bridgething.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/JoeyEamigh/bridgething.git",
12
+ "directory": "packages/create-bridgething"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/JoeyEamigh/bridgething/issues"
16
+ },
17
+ "keywords": ["bridgething", "spotify", "car-thing", "superbird", "create", "scaffold"],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "bin": {
23
+ "create-bridgething": "./dist/index.mjs"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "template"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsdown",
31
+ "dev": "tsdown --watch",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@types/uuid": "^11.0.0",
36
+ "tsdown": "^0.21.10"
37
+ },
38
+ "peerDependencies": {
39
+ "typescript": "^6.0.3"
40
+ },
41
+ "dependencies": {
42
+ "uuid": "^14.0.0"
43
+ }
44
+ }
@@ -0,0 +1,87 @@
1
+ # Building this bridgething webapp
2
+
3
+ You are helping build a **bridgething webapp**: a single-page web UI for the
4
+ Spotify Car Thing. It runs full-screen in a chromium kiosk on the device and
5
+ talks to the on-device daemon over a local WebSocket using `@bridgething/client`.
6
+ This file is always loaded; it holds the constraints. The `/bridgething` skill
7
+ goes deep on the how-to: the client API, running and driving the app, and
8
+ shipping it.
9
+
10
+ ## The device you are targeting
11
+
12
+ - **Screen: 800x480, landscape.** Fixed. Design for exactly this. The window
13
+ never resizes; there is no responsive breakpoint to chase.
14
+ - **One page.** The kiosk shows exactly one webapp at a time. In-app views are
15
+ fine, but there are no browser tabs, popups, or second windows.
16
+ - **Physical controls arrive as ordinary browser events.** Listen with a
17
+ `keydown` handler and a `wheel` handler on `window`.
18
+ - Preset buttons **1 2 3 4** -> `keydown` key `"1"` `"2"` `"3"` `"4"`.
19
+ - **Mode** button -> `keydown` key `"m"`. (Pressing it 5 times fast is a
20
+ system gesture that jumps back to the launcher, so do not build anything
21
+ that depends on rapid repeated `m`.)
22
+ - **Back** button -> `keydown` key `"Escape"`.
23
+ - **Rotary wheel** -> horizontal scroll: `wheel` events with `deltaX`. This is
24
+ the primary way users move through lists; make horizontal wheel scroll work.
25
+ - **Touch** works normally (pointer/touch events).
26
+ - **No keyboard, no mouse.** Those five controls plus touch are the whole input
27
+ surface. There is no text entry unless you build an on-screen one.
28
+
29
+ ## The SDK: `@bridgething/client`
30
+
31
+ ```ts
32
+ import { BridgethingClient } from '@bridgething/client';
33
+ const client = new BridgethingClient(); // auto-connects to the daemon, auto-reconnects
34
+ ```
35
+
36
+ Construct it once and reuse it. It defaults to the on-device daemon; in dev you
37
+ point it at your device with `VITE_BRIDGETHING_URL` (the starter `App.tsx`
38
+ already does this). Do not hardcode a WebSocket URL.
39
+
40
+ It is fully typed with a doc comment on every surface, method, and field. Read
41
+ those declarations directly - you have no editor to hover in:
42
+
43
+ - `node_modules/@bridgething/client/dist/dispatch.generated.d.ts` - every
44
+ `client.<surface>.<method>` and event, with docs.
45
+ - `node_modules/@bridgething/lib/dist/bindings/*.d.ts` - the payload and reply
46
+ types (`PlayerState`, `MediaItem`, ...), with per-field docs.
47
+
48
+ 18 surfaces: `player asset config store capabilities library audio notifications
49
+ phone geo net hardware bluetooth system time voice webapp forward`. Each method
50
+ is one of three shapes:
51
+
52
+ - **events**: `client.<s>.onXxx(handler)` returns an unsubscribe fn; or
53
+ `client.<s>.subscribe({ onXxx, ... })`.
54
+ - **requests**: `await client.<s>.xxx(req)` returns `{ ok: true, response }` or
55
+ `{ ok: false, kind, error }` - always check `.ok`.
56
+ - **commands**: `await client.<s>.xxx(payload)` returns `void` (fire-and-forget).
57
+
58
+ Now-playing and library data come from the **connected phone's** Spotify. Assume
59
+ a phone may not be connected: handle the empty/disconnected state gracefully (the
60
+ starter shows a "connect a phone" placeholder). Fetch artwork by opaque asset id
61
+ via `client.asset.get`; never build image URLs by hand.
62
+
63
+ Run `/bridgething` for the cookbook, the dev loop, and shipping; grep the `.d.ts`
64
+ files for a specific method or type.
65
+
66
+ ## `manifest.json` (in `public/`)
67
+
68
+ - `id` - baked in at scaffold, unique to this project. **Never change it.**
69
+ - `name`, `version`, `description`, `icon` - cosmetic / store metadata.
70
+ - `config` - user-tunable settings (each becomes a `client.config` value).
71
+ - `permissions` - capabilities you need (e.g. `net.fetch`, `net.proxy`, `geo`).
72
+ - `art: { heroPx, thumbPx }` - the pixel sizes you want artwork delivered at.
73
+
74
+ ## Workflow
75
+
76
+ - `bun run dev` - vite dev server. Iterate here. Use `/bridgething` for the loop
77
+ of seeing the screen and pressing the buttons.
78
+ - `bun run build` - production bundle into `dist/`.
79
+ - `bun run push` - build + install onto a connected Car Thing (see `/bridgething`).
80
+
81
+ ## Do not
82
+
83
+ - Fight the 800x480 viewport or assume any other size.
84
+ - Hardcode `ws://...`; use the SDK default + `VITE_BRIDGETHING_URL`.
85
+ - Assume a phone is always connected.
86
+ - Build image URLs by hand; fetch by opaque asset id.
87
+ - Rely on rapid repeated `m` (system launcher gesture).
@@ -0,0 +1,54 @@
1
+ # **PROJECT_NAME**
2
+
3
+ A bridgething webapp scaffolded with `create-bridgething`.
4
+
5
+ Stack: React 19 + Vite + Tailwind v4 + TypeScript strict, with
6
+ [`@bridgething/client`](https://github.com/JoeyEamigh/bridgething) preinstalled.
7
+
8
+ ## Building with an agent
9
+
10
+ Open this folder with your coding agent (Claude Code, Codex, opencode, ...). It
11
+ reads `CLAUDE.md` / `AGENTS.md` and the `/bridgething` skill.
12
+
13
+ ## Develop
14
+
15
+ ```bash
16
+ bun install
17
+ bun run dev
18
+ ```
19
+
20
+ Opens at http://localhost:5173/. The starter `App.tsx` connects to
21
+ `ws://<host>/` (the daemon's local WebSocket on port 8891 of the device
22
+ itself). To dev against a remote device, set:
23
+
24
+ ```bash
25
+ VITE_BRIDGETHING_URL=ws://<device-ip>:8891/ bun run dev
26
+ ```
27
+
28
+ ## Push to a device
29
+
30
+ ```bash
31
+ bun run push
32
+ ```
33
+
34
+ `push` builds `dist/`, rsyncs it into `/var/bridgething/webapps/<manifest-id>/`
35
+ on the Car Thing, and switches the kiosk to your app. Pass an IP address to target
36
+ something other than `bridgething.local` over USB.
37
+
38
+ ## Share
39
+
40
+ ```bash
41
+ bun run build
42
+ bun run share
43
+ ```
44
+
45
+ `share` writes `<name>-<version>.zip` from `dist/`. Anyone with a bridgething Car
46
+ Thing installs it from the app.
47
+
48
+ ## Layout
49
+
50
+ - `src/App.tsx` - starter UI: subscribes to `client.player.onSnapshot`,
51
+ fetches artwork via `client.asset.get`, exposes transport controls.
52
+ - `src/index.css` - `@import "tailwindcss";`.
53
+ - `vite.config.ts` - vite + tailwind plugin, `es2022` target.
54
+ - `index.html` - 800x480 viewport, no overscroll, no tap highlight.
@@ -0,0 +1,24 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Building a bridgething webapp
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):
19
+ [reference/develop.md](reference/develop.md)
20
+ - **Install and share** - build, push onto a connected Car Thing, package a zip
21
+ anyone can install: [reference/ship.md](reference/ship.md)
22
+
23
+ The device shape, the control-to-event mapping, and the hard constraints live in
24
+ CLAUDE.md, which is always loaded. This skill is the how-to on top of it.
@@ -0,0 +1,142 @@
1
+ # Running and driving the app
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.
7
+
8
+ ## Contents
9
+
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
15
+
16
+ ## Start the dev server
17
+
18
+ ```bash
19
+ bun run dev
20
+ ```
21
+
22
+ Vite serves at `http://localhost:5173/` with hot reload.
23
+
24
+ ## Drive it with Playwright (recommended for agents)
25
+
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:
28
+
29
+ ```bash
30
+ bun add -d playwright
31
+ bunx playwright install chromium
32
+ ```
33
+
34
+ Then a short script gives you a locked 800x480 window you can screenshot and send
35
+ input to:
36
+
37
+ ```ts
38
+ import { chromium } from 'playwright';
39
+
40
+ const browser = await chromium.launch();
41
+ const page = await browser.newPage({ viewport: { width: 800, height: 480 } });
42
+ await page.goto('http://localhost:5173/');
43
+
44
+ await page.screenshot({ path: 'screen.png' }); // look at the result
45
+
46
+ // 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)
51
+ // touch: page.tap('selector') or page.mouse.click(x, y)
52
+ ```
53
+
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.
56
+
57
+ ## Alternative: your own browser + DevTools
58
+
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.
63
+
64
+ ## Drive the real device over CDP
65
+
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.
69
+
70
+ Install the app onto the device first so the kiosk is showing it (build + push).
71
+ Then tunnel the device's CDP port to your machine (it is bound to `127.0.0.1` and
72
+ chromium refuses to bind it to anything routable, so a tunnel is mandatory):
73
+
74
+ ```bash
75
+ # device answers at bridgething.local over USB; root has no password
76
+ ssh -o StrictHostKeyChecking=no -fN -L 9223:127.0.0.1:9223 root@bridgething.local
77
+ curl -s http://localhost:9223/json/version >/dev/null && echo tunnel-up
78
+ ```
79
+
80
+ **Playwright's `connectOverCDP` hangs against the device's embedded chromium** -
81
+ it never finishes the websocket upgrade (a quirk of the device's `cast_shell`
82
+ build, not the tunnel: a plain websocket connects fine over the same tunnel).
83
+ Drive it with a raw CDP websocket instead. Connect to the `page` target from
84
+ `/json` (the page-level endpoint, not the browser-level one). This script
85
+ screenshots the live device and presses a button; it is the whole loop:
86
+
87
+ ```ts
88
+ // drive.ts -> run with: bun drive.ts
89
+ const targets = (await fetch('http://localhost:9223/json').then(r => r.json())) as any[];
90
+ const page = targets.find(t => t.type === 'page');
91
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
92
+ await new Promise<void>((res, rej) => {
93
+ ws.onopen = () => res();
94
+ setTimeout(() => rej(new Error('ws open timeout')), 8000);
95
+ });
96
+
97
+ let id = 0;
98
+ const pending = new Map<number, (v: any) => void>();
99
+ ws.onmessage = ev => {
100
+ const m = JSON.parse(ev.data as string);
101
+ if (m.id && pending.has(m.id)) (pending.get(m.id)!(m.result), pending.delete(m.id));
102
+ };
103
+ const send = (method: string, params: any = {}) =>
104
+ new Promise<any>(res => (pending.set(++id, res), ws.send(JSON.stringify({ id, method, params }))));
105
+
106
+ // screenshot
107
+ const { data } = await send('Page.captureScreenshot', { format: 'png' });
108
+ await Bun.write('device.png', Buffer.from(data, 'base64'));
109
+
110
+ // read what is on screen (great for asserting a change happened)
111
+ const text = await send('Runtime.evaluate', { expression: 'document.body.innerText', returnByValue: true });
112
+ console.log(text.result.value);
113
+
114
+ // press a button: keyDown + keyUp. key/code/text must match a real key.
115
+ const key = (type: string, k: string, code: string, vk: number) =>
116
+ send('Input.dispatchKeyEvent', { type, key: k, code, text: k.length === 1 ? k : '', windowsVirtualKeyCode: vk });
117
+ await key('keyDown', '3', 'Digit3', 51); // preset 3
118
+ await key('keyUp', '3', 'Digit3', 51);
119
+
120
+ // rotary wheel = horizontal scroll:
121
+ await send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: 400, y: 240, deltaX: 120, deltaY: 0 });
122
+
123
+ process.exit(0);
124
+ ```
125
+
126
+ Read the screenshot and the `innerText` to confirm the app reacted. Prefer the
127
+ local loop for fast iteration; use the device to confirm real behavior (real
128
+ now-playing, real artwork, the actual screen).
129
+
130
+ ## Input reference
131
+
132
+ The controls arrive as ordinary browser events. Listen for them with a `keydown`
133
+ handler and a `wheel` handler on `window`. Make horizontal wheel scroll move
134
+ through your primary list; it is the control users reach for first.
135
+
136
+ | Control | Browser event | CDP `key`/`code`/vk |
137
+ | ------------ | ------------------------------------ | ---------------------- |
138
+ | Preset 1-4 | `keydown` key `"1"` `"2"` `"3"` `"4"` | `Digit1`-`Digit4` / 49-52 |
139
+ | Mode (M) | `keydown` key `"m"` | `KeyM` / 77 |
140
+ | Back | `keydown` key `"Escape"` | `Escape` / 27 |
141
+ | Rotary wheel | `wheel` with `deltaX` (horizontal) | mouseWheel `deltaX` |
142
+ | Touch | pointer / touch events | `Input.dispatchTouchEvent` |