@buoy-gg/tv-remote 7.0.31

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,58 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2024-present Buoy. All rights reserved.
4
+
5
+ This software and its source code are proprietary and confidential.
6
+
7
+ NOTICE: This is NOT open source software. This software is licensed,
8
+ not sold, and is protected by copyright laws and international treaties.
9
+
10
+ TERMS AND CONDITIONS:
11
+
12
+ 1. LICENSE GRANT
13
+ Subject to the terms of this Agreement and payment of applicable fees,
14
+ Buoy grants you a limited, non-exclusive, non-transferable license
15
+ to use the compiled software packages in your applications.
16
+
17
+ 2. RESTRICTIONS
18
+ You may NOT:
19
+ - Copy, modify, or distribute the source code
20
+ - Reverse engineer, decompile, or disassemble the software
21
+ - Remove or alter any proprietary notices or labels
22
+ - Sublicense, rent, lease, or lend the software
23
+ - Use the software to create competing products
24
+ - Share access credentials with unauthorized parties
25
+
26
+ 3. OWNERSHIP
27
+ React Buoy retains all right, title, and interest in the software,
28
+ including all intellectual property rights. This license does not
29
+ grant you any rights to trademarks or service marks.
30
+
31
+ 4. TERMINATION
32
+ This license is effective until terminated. Your rights under this
33
+ license will terminate automatically without notice if you fail to
34
+ comply with any of its terms. Upon termination, you must cease all
35
+ use and destroy all copies of the software.
36
+
37
+ 5. DISCLAIMER OF WARRANTIES
38
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
39
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
40
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
41
+
42
+ 6. LIMITATION OF LIABILITY
43
+ IN NO EVENT SHALL BUOY BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
44
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN
45
+ CONNECTION WITH THIS LICENSE OR THE USE OF THE SOFTWARE.
46
+
47
+ 7. GOVERNING LAW
48
+ This Agreement shall be governed by and construed in accordance with
49
+ the laws of the United States, without regard to its conflict of
50
+ law provisions.
51
+
52
+ For licensing inquiries and subscription information:
53
+ - Website: https://buoy.gg
54
+ - Email: AustinLovesWorking@gmail.com
55
+
56
+ Unauthorized reproduction or distribution of this software, or any
57
+ portion of it, may result in severe civil and criminal penalties,
58
+ and will be prosecuted to the maximum extent possible under the law.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @buoy-gg/tv-remote
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@buoy-gg/tv-remote?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/tv-remote) [![npm downloads](https://img.shields.io/npm/dm/@buoy-gg/tv-remote?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/tv-remote)
4
+
5
+ **Press the TV remote from Buoy Desktop — drive your Apple TV / Android TV app's D-pad, record press sequences as macros, and replay them across every connected device.**
6
+
7
+ Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside your React Native app. Install it and the TV Remote panel lights up in Buoy Desktop for that device.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @buoy-gg/core @buoy-gg/tv-remote
13
+ ```
14
+
15
+ ```tsx
16
+ import { FloatingDevTools } from "@buoy-gg/core";
17
+
18
+ export default function App() {
19
+ return (
20
+ <>
21
+ {/* your app */}
22
+ <FloatingDevTools headless /> {/* TV apps run headless — no bubble */}
23
+ </>
24
+ );
25
+ }
26
+ ```
27
+
28
+ ## What this package does — and deliberately does not
29
+
30
+ This package **only observes**. It captures the remote events your app receives
31
+ (`up`/`down`/`left`/`right`/`select`/`menu`/`playPause`) and streams them to Buoy Desktop.
32
+
33
+ **Presses are injected from the host**, by Buoy Desktop shelling out to `adb shell input keyevent`
34
+ (Android TV) or `idb ui key` (Apple TV simulator). That is the only honest path:
35
+
36
+ - On tvOS, the two things an app *can* do in-process are post the internal `onHWKeyEvent`
37
+ notification (fires the JS event, but focus never moves) and `requestTVFocus` (jumps focus,
38
+ but fires no event and traverses nothing). Each is half of a press, and the missing half is
39
+ the half QA cares about.
40
+ - On Android, directional focus navigation for unconsumed D-pad keys happens in `ViewRootImpl`,
41
+ *above* the Activity — so an in-process `dispatchKeyEvent` would fire JS events **without
42
+ moving focus** exactly on the screens that are broken.
43
+
44
+ Host-side injection goes through `InputManager` / the simulator's HID layer — the same pipeline
45
+ as a physical remote, driving the real focus engine.
46
+
47
+ The package also renders **nothing**. On Android TV any focusable view in an overlay becomes a
48
+ D-pad stop in the host app's focus order, so a visible tool would alter the navigation it is
49
+ supposed to be testing.
50
+
51
+ ## Support matrix
52
+
53
+ | Target | Drive-able? | Via |
54
+ |---|---|---|
55
+ | Android TV emulator | ✅ full fidelity | `adb -s <serial> shell input keyevent` |
56
+ | Android TV device | ✅ full fidelity | same, over `adb connect <ip>:5555` |
57
+ | Apple TV simulator | ✅ arrows / select / menu — ❌ play-pause | `idb ui key --udid <udid>` |
58
+ | Apple TV device | ❌ replay unsupported — **record only** | (future: an XCUITest `XCUIRemote` companion) |
59
+
60
+ Capture is pure JS and works on every target, including retail hardware — so the
61
+ **record-on-retail** workflow holds: press the physical remote on a rack device, get a macro,
62
+ replay it against emulators and simulators.
63
+
64
+ `idb` is not part of Xcode. Without it on `PATH` the Apple TV lane is disabled with an install
65
+ hint (`brew install idb-companion` + `pipx install fb-idb`).
66
+
67
+ ## API
68
+
69
+ Everything is driven from Buoy Desktop; these exports exist for custom integrations.
70
+
71
+ ```ts
72
+ import {
73
+ arm, // start capturing
74
+ disarm, // stop capturing, hand the Menu key back to the platform
75
+ clear,
76
+ setMenuCapture, // tvOS: route Menu to JS (opt-in, always undone on disarm)
77
+ getEventsSince, // (seq) => events newer than seq
78
+ getState,
79
+ subscribe,
80
+ tvRemoteSyncAdapter, // auto-discovered by @buoy-gg/core
81
+ } from "@buoy-gg/tv-remote";
82
+ ```
83
+
84
+ On non-TV platforms `getState().supported` is `false`, the native listener is never attached,
85
+ and the package costs one boolean.
86
+
87
+ ## Docs
88
+
89
+ Full documentation: <https://buoy.gg/buoy/latest/docs/tools/tv-remote>
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "arm", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _tvEventStore.arm;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "clear", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _tvEventStore.clear;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "disarm", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _tvEventStore.disarm;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "getEventsSince", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _tvEventStore.getEventsSince;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "getState", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _tvEventStore.getState;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "setMenuCapture", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _tvEventStore.setMenuCapture;
40
+ }
41
+ });
42
+ Object.defineProperty(exports, "subscribe", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _tvEventStore.subscribe;
46
+ }
47
+ });
48
+ Object.defineProperty(exports, "tvRemoteSyncAdapter", {
49
+ enumerable: true,
50
+ get: function () {
51
+ return _tvRemoteSyncAdapter.tvRemoteSyncAdapter;
52
+ }
53
+ });
54
+ var _tvRemoteSyncAdapter = require("./sync/tvRemoteSyncAdapter");
55
+ var _tvEventStore = require("./store/tvEventStore");
56
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_tvRemoteSyncAdapter","require","_tvEventStore"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,IAAAA,oBAAA,GAAAC,OAAA;AAKA,IAAAC,aAAA,GAAAD,OAAA","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.arm = arm;
7
+ exports.clear = clear;
8
+ exports.disarm = disarm;
9
+ exports.getEventsSince = getEventsSince;
10
+ exports.getState = getState;
11
+ exports.setMenuCapture = setMenuCapture;
12
+ exports.subscribe = subscribe;
13
+ var ReactNative = _interopRequireWildcard(require("react-native"));
14
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
+ /**
16
+ * tvEventStore — the whole device-side half of the TV remote tool.
17
+ *
18
+ * CONTRACT: this store only ever OBSERVES. It never injects, synthesizes, or
19
+ * fakes a remote press. Presses come from the host (Buoy Desktop shells out to
20
+ * `adb shell input keyevent` / `idb ui key`), because those go through the real
21
+ * platform focus engine; anything we could fake in-process would move focus
22
+ * without firing events, or fire events without moving focus. See
23
+ * TV_REMOTE_DRIVER_PLAN.md §2.
24
+ *
25
+ * It is also deliberately UI-less. On Android TV any focusable view we rendered
26
+ * would become a D-pad stop in the host app's focus order, so the tool would
27
+ * alter the navigation it is supposed to be replaying.
28
+ *
29
+ * Safe to ship in non-TV apps: the native event emitter is never touched unless
30
+ * `Platform.isTV` is true, so the cost on a phone is one boolean.
31
+ */
32
+
33
+ /** One captured remote event, as it goes over the wire to the dashboard. */
34
+
35
+ /**
36
+ * Ring buffer depth. Replay polls `getEventsSince` every few dozen ms with a
37
+ * ~2s budget per step, so 50 is many times more than any echo needs; it exists
38
+ * so a human scrubbing the echo strip has some scrollback.
39
+ */
40
+ const MAX_EVENTS = 50;
41
+
42
+ /**
43
+ * Continuous gestures are not presses and are never recorded — the macro format
44
+ * is discrete keys. Dropping them here keeps them out of the ring buffer so
45
+ * they can't push real presses out of it during a replay.
46
+ */
47
+ const IGNORED_EVENT_TYPES = new Set(["pan", "swipeUp", "swipeDown", "swipeLeft", "swipeRight"]);
48
+ /**
49
+ * `TVEventHandler` / `TVEventControl` only exist on the react-native-tvos fork.
50
+ * A namespace read (rather than a named import) keeps this module importable on
51
+ * stock react-native, where those exports are absent.
52
+ */
53
+ const RN = ReactNative;
54
+ const isTV = RN.Platform?.isTV === true;
55
+ let events = [];
56
+ let seq = 0;
57
+ let armed = false;
58
+ let menuCaptureArmed = false;
59
+ let subscription = null;
60
+ const listeners = new Set();
61
+ function emitChange() {
62
+ listeners.forEach(listener => {
63
+ listener();
64
+ });
65
+ }
66
+ function handleEvent(raw) {
67
+ // Disarming removes the subscription, but don't take that on trust: a native
68
+ // emitter that delivered one more event after `remove()` would land a stray
69
+ // press in the buffer, and a stray press is exactly what a replay would
70
+ // mistake for a step's echo.
71
+ if (!armed) return;
72
+ const event = raw;
73
+ const eventType = event?.eventType;
74
+ if (typeof eventType !== "string") return;
75
+ if (IGNORED_EVENT_TYPES.has(eventType)) return;
76
+
77
+ /**
78
+ * Dedupe by ignoring key DOWN, never key UP. tvOS emits keyAction 1 (up)
79
+ * ONLY — taps reach the gesture recognizer at StateEnded — so filtering to
80
+ * "down" would swallow every tvOS event. Android's down-emit is
81
+ * feature-flagged off by default, but dropping it here is the correct
82
+ * invariant either way: one press must produce exactly one echo.
83
+ */
84
+ if (event?.eventKeyAction === 0) return;
85
+ seq += 1;
86
+ const next = {
87
+ seq,
88
+ t: Date.now(),
89
+ eventType,
90
+ eventKeyAction: event?.eventKeyAction
91
+ };
92
+ events = events.length >= MAX_EVENTS ? [...events.slice(1), next] : [...events, next];
93
+ emitChange();
94
+ }
95
+
96
+ /** Start pushing remote events into the buffer. Idempotent. */
97
+ function arm() {
98
+ if (armed) return;
99
+ armed = true;
100
+ if (isTV && RN.TVEventHandler && !subscription) {
101
+ subscription = RN.TVEventHandler.addListener(handleEvent);
102
+ }
103
+ emitChange();
104
+ }
105
+
106
+ /**
107
+ * Stop capturing and hand the Menu key back to the platform. Every teardown
108
+ * path funnels through here — leaving menu capture on would leave the app
109
+ * unable to navigate back, which is an App Store behavior change.
110
+ */
111
+ function disarm() {
112
+ if (subscription) {
113
+ subscription.remove();
114
+ subscription = null;
115
+ }
116
+ if (menuCaptureArmed) setMenuCapture(false);
117
+ if (!armed) return;
118
+ armed = false;
119
+ emitChange();
120
+ }
121
+
122
+ /**
123
+ * tvOS: route the Menu key to JS so it can be recorded, instead of letting it
124
+ * pop the navigation stack (and background the app at the root). This changes
125
+ * how the app under test behaves, so it is opt-in per recording session and is
126
+ * always undone on disarm.
127
+ */
128
+ function setMenuCapture(on) {
129
+ if (!isTV || RN.Platform.OS !== "ios" || !RN.TVEventControl) return;
130
+ if (on === menuCaptureArmed) return;
131
+ if (on) {
132
+ RN.TVEventControl.enableTVMenuKey();
133
+ } else {
134
+ RN.TVEventControl.disableTVMenuKey();
135
+ }
136
+ menuCaptureArmed = on;
137
+ emitChange();
138
+ }
139
+ function clear() {
140
+ if (events.length === 0) return;
141
+ events = [];
142
+ emitChange();
143
+ }
144
+
145
+ /** Events strictly newer than `sinceSeq` — the replay echo cursor. */
146
+ function getEventsSince(sinceSeq) {
147
+ if (!Number.isFinite(sinceSeq)) return events.slice();
148
+ return events.filter(event => event.seq > sinceSeq);
149
+ }
150
+ function getState() {
151
+ return {
152
+ supported: isTV,
153
+ platform: RN.Platform?.OS ?? "unknown",
154
+ captureArmed: armed,
155
+ listening: subscription !== null,
156
+ menuCaptureArmed,
157
+ seq,
158
+ recent: events.slice()
159
+ };
160
+ }
161
+ function subscribe(listener) {
162
+ listeners.add(listener);
163
+ return () => {
164
+ listeners.delete(listener);
165
+ };
166
+ }
167
+ //# sourceMappingURL=tvEventStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ReactNative","_interopRequireWildcard","require","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","MAX_EVENTS","IGNORED_EVENT_TYPES","Set","RN","isTV","Platform","events","seq","armed","menuCaptureArmed","subscription","listeners","emitChange","forEach","listener","handleEvent","raw","event","eventType","eventKeyAction","next","Date","now","length","slice","arm","TVEventHandler","addListener","disarm","remove","setMenuCapture","on","OS","TVEventControl","enableTVMenuKey","disableTVMenuKey","clear","getEventsSince","sinceSeq","Number","isFinite","filter","getState","supported","platform","captureArmed","listening","recent","subscribe","add","delete"],"sourceRoot":"../../../src","sources":["store/tvEventStore.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.tvRemoteSyncAdapter = void 0;
7
+ var _tvEventStore = require("../store/tvEventStore");
8
+ /**
9
+ * tvRemoteSyncAdapter — streams captured TV remote events to Buoy Desktop via
10
+ * @buoy-gg/external-sync.
11
+ *
12
+ * Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
13
+ * package (same as every other tool's adapter). Auto-discovery in @buoy-gg/core
14
+ * picks it up by export name and announces the "tv-remote" capability.
15
+ *
16
+ * `getEventsSince` exists because snapshots are throttled 200ms trailing-edge
17
+ * per tool, which is too coarse to time a replay step's echo. Replay polls this
18
+ * awaited action instead and gets the real latency.
19
+ */
20
+
21
+ const tvRemoteSyncAdapter = exports.tvRemoteSyncAdapter = {
22
+ version: 1,
23
+ getSnapshot() {
24
+ return (0, _tvEventStore.getState)();
25
+ },
26
+ subscribe(onChange) {
27
+ return (0, _tvEventStore.subscribe)(onChange);
28
+ },
29
+ actions: {
30
+ arm() {
31
+ (0, _tvEventStore.arm)();
32
+ return (0, _tvEventStore.getState)();
33
+ },
34
+ disarm() {
35
+ (0, _tvEventStore.disarm)();
36
+ return (0, _tvEventStore.getState)();
37
+ },
38
+ /** Cursor-paged echo poll. `seq: 0` returns everything buffered. */
39
+ getEventsSince(params) {
40
+ const since = typeof params?.seq === "number" ? params.seq : 0;
41
+ const state = (0, _tvEventStore.getState)();
42
+ return {
43
+ seq: state.seq,
44
+ events: (0, _tvEventStore.getEventsSince)(since)
45
+ };
46
+ },
47
+ setMenuCapture(params) {
48
+ (0, _tvEventStore.setMenuCapture)(params?.on === true);
49
+ return (0, _tvEventStore.getState)();
50
+ },
51
+ clear() {
52
+ (0, _tvEventStore.clear)();
53
+ return (0, _tvEventStore.getState)();
54
+ }
55
+ }
56
+ };
57
+ //# sourceMappingURL=tvRemoteSyncAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_tvEventStore","require","tvRemoteSyncAdapter","exports","version","getSnapshot","getState","subscribe","onChange","actions","arm","disarm","getEventsSince","params","since","seq","state","events","setMenuCapture","on","clear"],"sourceRoot":"../../../src","sources":["sync/tvRemoteSyncAdapter.ts"],"mappings":";;;;;;AAaA,IAAAA,aAAA,GAAAC,OAAA;AAbA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAcO,MAAMC,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,GAAG;EACjCE,OAAO,EAAE,CAAC;EAEVC,WAAWA,CAAA,EAAkB;IAC3B,OAAO,IAAAC,sBAAQ,EAAC,CAAC;EACnB,CAAC;EAEDC,SAASA,CAACC,QAAoB,EAAc;IAC1C,OAAO,IAAAD,uBAAS,EAACC,QAAQ,CAAC;EAC5B,CAAC;EAEDC,OAAO,EAAE;IACPC,GAAGA,CAAA,EAAkB;MACnB,IAAAA,iBAAG,EAAC,CAAC;MACL,OAAO,IAAAJ,sBAAQ,EAAC,CAAC;IACnB,CAAC;IAEDK,MAAMA,CAAA,EAAkB;MACtB,IAAAA,oBAAM,EAAC,CAAC;MACR,OAAO,IAAAL,sBAAQ,EAAC,CAAC;IACnB,CAAC;IAED;IACAM,cAAcA,CAACC,MAAyB,EAGtC;MACA,MAAMC,KAAK,GAAG,OAAOD,MAAM,EAAEE,GAAG,KAAK,QAAQ,GAAGF,MAAM,CAACE,GAAG,GAAG,CAAC;MAC9D,MAAMC,KAAK,GAAG,IAAAV,sBAAQ,EAAC,CAAC;MACxB,OAAO;QAAES,GAAG,EAAEC,KAAK,CAACD,GAAG;QAAEE,MAAM,EAAE,IAAAL,4BAAc,EAACE,KAAK;MAAE,CAAC;IAC1D,CAAC;IAEDI,cAAcA,CAACL,MAAyB,EAAiB;MACvD,IAAAK,4BAAc,EAACL,MAAM,EAAEM,EAAE,KAAK,IAAI,CAAC;MACnC,OAAO,IAAAb,sBAAQ,EAAC,CAAC;IACnB,CAAC;IAEDc,KAAKA,CAAA,EAAkB;MACrB,IAAAA,mBAAK,EAAC,CAAC;MACP,OAAO,IAAAd,sBAAQ,EAAC,CAAC;IACnB;EACF;AACF,CAAC","ignoreList":[]}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @buoy-gg/tv-remote
5
+ *
6
+ * The device-side half of Buoy's TV remote driver: it CAPTURES the D-pad /
7
+ * select / menu / play-pause events the app actually receives and streams them
8
+ * to Buoy Desktop, which is where presses are injected from (adb / idb) and
9
+ * where macros are recorded and replayed.
10
+ *
11
+ * This package renders nothing and injects nothing. See the README for why.
12
+ */
13
+
14
+ // =============================================================================
15
+ // EXTERNAL SYNC (device → desktop dashboard)
16
+ // =============================================================================
17
+ export { tvRemoteSyncAdapter } from "./sync/tvRemoteSyncAdapter";
18
+
19
+ // =============================================================================
20
+ // CAPTURE STORE (arm/disarm + types)
21
+ // =============================================================================
22
+ export { arm, disarm, clear, setMenuCapture, getEventsSince, getState, subscribe } from "./store/tvEventStore";
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["tvRemoteSyncAdapter","arm","disarm","clear","setMenuCapture","getEventsSince","getState","subscribe"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,SAASA,mBAAmB,QAAQ,4BAA4B;;AAEhE;AACA;AACA;AACA,SACEC,GAAG,EACHC,MAAM,EACNC,KAAK,EACLC,cAAc,EACdC,cAAc,EACdC,QAAQ,EACRC,SAAS,QACJ,sBAAsB","ignoreList":[]}
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * tvEventStore — the whole device-side half of the TV remote tool.
5
+ *
6
+ * CONTRACT: this store only ever OBSERVES. It never injects, synthesizes, or
7
+ * fakes a remote press. Presses come from the host (Buoy Desktop shells out to
8
+ * `adb shell input keyevent` / `idb ui key`), because those go through the real
9
+ * platform focus engine; anything we could fake in-process would move focus
10
+ * without firing events, or fire events without moving focus. See
11
+ * TV_REMOTE_DRIVER_PLAN.md §2.
12
+ *
13
+ * It is also deliberately UI-less. On Android TV any focusable view we rendered
14
+ * would become a D-pad stop in the host app's focus order, so the tool would
15
+ * alter the navigation it is supposed to be replaying.
16
+ *
17
+ * Safe to ship in non-TV apps: the native event emitter is never touched unless
18
+ * `Platform.isTV` is true, so the cost on a phone is one boolean.
19
+ */
20
+
21
+ import * as ReactNative from "react-native";
22
+
23
+ /** One captured remote event, as it goes over the wire to the dashboard. */
24
+
25
+ /**
26
+ * Ring buffer depth. Replay polls `getEventsSince` every few dozen ms with a
27
+ * ~2s budget per step, so 50 is many times more than any echo needs; it exists
28
+ * so a human scrubbing the echo strip has some scrollback.
29
+ */
30
+ const MAX_EVENTS = 50;
31
+
32
+ /**
33
+ * Continuous gestures are not presses and are never recorded — the macro format
34
+ * is discrete keys. Dropping them here keeps them out of the ring buffer so
35
+ * they can't push real presses out of it during a replay.
36
+ */
37
+ const IGNORED_EVENT_TYPES = new Set(["pan", "swipeUp", "swipeDown", "swipeLeft", "swipeRight"]);
38
+ /**
39
+ * `TVEventHandler` / `TVEventControl` only exist on the react-native-tvos fork.
40
+ * A namespace read (rather than a named import) keeps this module importable on
41
+ * stock react-native, where those exports are absent.
42
+ */
43
+ const RN = ReactNative;
44
+ const isTV = RN.Platform?.isTV === true;
45
+ let events = [];
46
+ let seq = 0;
47
+ let armed = false;
48
+ let menuCaptureArmed = false;
49
+ let subscription = null;
50
+ const listeners = new Set();
51
+ function emitChange() {
52
+ listeners.forEach(listener => {
53
+ listener();
54
+ });
55
+ }
56
+ function handleEvent(raw) {
57
+ // Disarming removes the subscription, but don't take that on trust: a native
58
+ // emitter that delivered one more event after `remove()` would land a stray
59
+ // press in the buffer, and a stray press is exactly what a replay would
60
+ // mistake for a step's echo.
61
+ if (!armed) return;
62
+ const event = raw;
63
+ const eventType = event?.eventType;
64
+ if (typeof eventType !== "string") return;
65
+ if (IGNORED_EVENT_TYPES.has(eventType)) return;
66
+
67
+ /**
68
+ * Dedupe by ignoring key DOWN, never key UP. tvOS emits keyAction 1 (up)
69
+ * ONLY — taps reach the gesture recognizer at StateEnded — so filtering to
70
+ * "down" would swallow every tvOS event. Android's down-emit is
71
+ * feature-flagged off by default, but dropping it here is the correct
72
+ * invariant either way: one press must produce exactly one echo.
73
+ */
74
+ if (event?.eventKeyAction === 0) return;
75
+ seq += 1;
76
+ const next = {
77
+ seq,
78
+ t: Date.now(),
79
+ eventType,
80
+ eventKeyAction: event?.eventKeyAction
81
+ };
82
+ events = events.length >= MAX_EVENTS ? [...events.slice(1), next] : [...events, next];
83
+ emitChange();
84
+ }
85
+
86
+ /** Start pushing remote events into the buffer. Idempotent. */
87
+ export function arm() {
88
+ if (armed) return;
89
+ armed = true;
90
+ if (isTV && RN.TVEventHandler && !subscription) {
91
+ subscription = RN.TVEventHandler.addListener(handleEvent);
92
+ }
93
+ emitChange();
94
+ }
95
+
96
+ /**
97
+ * Stop capturing and hand the Menu key back to the platform. Every teardown
98
+ * path funnels through here — leaving menu capture on would leave the app
99
+ * unable to navigate back, which is an App Store behavior change.
100
+ */
101
+ export function disarm() {
102
+ if (subscription) {
103
+ subscription.remove();
104
+ subscription = null;
105
+ }
106
+ if (menuCaptureArmed) setMenuCapture(false);
107
+ if (!armed) return;
108
+ armed = false;
109
+ emitChange();
110
+ }
111
+
112
+ /**
113
+ * tvOS: route the Menu key to JS so it can be recorded, instead of letting it
114
+ * pop the navigation stack (and background the app at the root). This changes
115
+ * how the app under test behaves, so it is opt-in per recording session and is
116
+ * always undone on disarm.
117
+ */
118
+ export function setMenuCapture(on) {
119
+ if (!isTV || RN.Platform.OS !== "ios" || !RN.TVEventControl) return;
120
+ if (on === menuCaptureArmed) return;
121
+ if (on) {
122
+ RN.TVEventControl.enableTVMenuKey();
123
+ } else {
124
+ RN.TVEventControl.disableTVMenuKey();
125
+ }
126
+ menuCaptureArmed = on;
127
+ emitChange();
128
+ }
129
+ export function clear() {
130
+ if (events.length === 0) return;
131
+ events = [];
132
+ emitChange();
133
+ }
134
+
135
+ /** Events strictly newer than `sinceSeq` — the replay echo cursor. */
136
+ export function getEventsSince(sinceSeq) {
137
+ if (!Number.isFinite(sinceSeq)) return events.slice();
138
+ return events.filter(event => event.seq > sinceSeq);
139
+ }
140
+ export function getState() {
141
+ return {
142
+ supported: isTV,
143
+ platform: RN.Platform?.OS ?? "unknown",
144
+ captureArmed: armed,
145
+ listening: subscription !== null,
146
+ menuCaptureArmed,
147
+ seq,
148
+ recent: events.slice()
149
+ };
150
+ }
151
+ export function subscribe(listener) {
152
+ listeners.add(listener);
153
+ return () => {
154
+ listeners.delete(listener);
155
+ };
156
+ }
157
+ //# sourceMappingURL=tvEventStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ReactNative","MAX_EVENTS","IGNORED_EVENT_TYPES","Set","RN","isTV","Platform","events","seq","armed","menuCaptureArmed","subscription","listeners","emitChange","forEach","listener","handleEvent","raw","event","eventType","has","eventKeyAction","next","t","Date","now","length","slice","arm","TVEventHandler","addListener","disarm","remove","setMenuCapture","on","OS","TVEventControl","enableTVMenuKey","disableTVMenuKey","clear","getEventsSince","sinceSeq","Number","isFinite","filter","getState","supported","platform","captureArmed","listening","recent","subscribe","add","delete"],"sourceRoot":"../../../src","sources":["store/tvEventStore.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * tvRemoteSyncAdapter — streams captured TV remote events to Buoy Desktop via
5
+ * @buoy-gg/external-sync.
6
+ *
7
+ * Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
8
+ * package (same as every other tool's adapter). Auto-discovery in @buoy-gg/core
9
+ * picks it up by export name and announces the "tv-remote" capability.
10
+ *
11
+ * `getEventsSince` exists because snapshots are throttled 200ms trailing-edge
12
+ * per tool, which is too coarse to time a replay step's echo. Replay polls this
13
+ * awaited action instead and gets the real latency.
14
+ */
15
+
16
+ import { arm, clear, disarm, getEventsSince, getState, setMenuCapture, subscribe } from "../store/tvEventStore";
17
+ export const tvRemoteSyncAdapter = {
18
+ version: 1,
19
+ getSnapshot() {
20
+ return getState();
21
+ },
22
+ subscribe(onChange) {
23
+ return subscribe(onChange);
24
+ },
25
+ actions: {
26
+ arm() {
27
+ arm();
28
+ return getState();
29
+ },
30
+ disarm() {
31
+ disarm();
32
+ return getState();
33
+ },
34
+ /** Cursor-paged echo poll. `seq: 0` returns everything buffered. */
35
+ getEventsSince(params) {
36
+ const since = typeof params?.seq === "number" ? params.seq : 0;
37
+ const state = getState();
38
+ return {
39
+ seq: state.seq,
40
+ events: getEventsSince(since)
41
+ };
42
+ },
43
+ setMenuCapture(params) {
44
+ setMenuCapture(params?.on === true);
45
+ return getState();
46
+ },
47
+ clear() {
48
+ clear();
49
+ return getState();
50
+ }
51
+ }
52
+ };
53
+ //# sourceMappingURL=tvRemoteSyncAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["arm","clear","disarm","getEventsSince","getState","setMenuCapture","subscribe","tvRemoteSyncAdapter","version","getSnapshot","onChange","actions","params","since","seq","state","events","on"],"sourceRoot":"../../../src","sources":["sync/tvRemoteSyncAdapter.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SACEA,GAAG,EACHC,KAAK,EACLC,MAAM,EACNC,cAAc,EACdC,QAAQ,EACRC,cAAc,EACdC,SAAS,QAGJ,uBAAuB;AAE9B,OAAO,MAAMC,mBAAmB,GAAG;EACjCC,OAAO,EAAE,CAAC;EAEVC,WAAWA,CAAA,EAAkB;IAC3B,OAAOL,QAAQ,CAAC,CAAC;EACnB,CAAC;EAEDE,SAASA,CAACI,QAAoB,EAAc;IAC1C,OAAOJ,SAAS,CAACI,QAAQ,CAAC;EAC5B,CAAC;EAEDC,OAAO,EAAE;IACPX,GAAGA,CAAA,EAAkB;MACnBA,GAAG,CAAC,CAAC;MACL,OAAOI,QAAQ,CAAC,CAAC;IACnB,CAAC;IAEDF,MAAMA,CAAA,EAAkB;MACtBA,MAAM,CAAC,CAAC;MACR,OAAOE,QAAQ,CAAC,CAAC;IACnB,CAAC;IAED;IACAD,cAAcA,CAACS,MAAyB,EAGtC;MACA,MAAMC,KAAK,GAAG,OAAOD,MAAM,EAAEE,GAAG,KAAK,QAAQ,GAAGF,MAAM,CAACE,GAAG,GAAG,CAAC;MAC9D,MAAMC,KAAK,GAAGX,QAAQ,CAAC,CAAC;MACxB,OAAO;QAAEU,GAAG,EAAEC,KAAK,CAACD,GAAG;QAAEE,MAAM,EAAEb,cAAc,CAACU,KAAK;MAAE,CAAC;IAC1D,CAAC;IAEDR,cAAcA,CAACO,MAAyB,EAAiB;MACvDP,cAAc,CAACO,MAAM,EAAEK,EAAE,KAAK,IAAI,CAAC;MACnC,OAAOb,QAAQ,CAAC,CAAC;IACnB,CAAC;IAEDH,KAAKA,CAAA,EAAkB;MACrBA,KAAK,CAAC,CAAC;MACP,OAAOG,QAAQ,CAAC,CAAC;IACnB;EACF;AACF,CAAC","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @buoy-gg/tv-remote
3
+ *
4
+ * The device-side half of Buoy's TV remote driver: it CAPTURES the D-pad /
5
+ * select / menu / play-pause events the app actually receives and streams them
6
+ * to Buoy Desktop, which is where presses are injected from (adb / idb) and
7
+ * where macros are recorded and replayed.
8
+ *
9
+ * This package renders nothing and injects nothing. See the README for why.
10
+ */
11
+ export { tvRemoteSyncAdapter } from "./sync/tvRemoteSyncAdapter";
12
+ export { arm, disarm, clear, setMenuCapture, getEventsSince, getState, subscribe, } from "./store/tvEventStore";
13
+ export type { TvRemoteEvent, TvRemoteState } from "./store/tvEventStore";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAKjE,OAAO,EACL,GAAG,EACH,MAAM,EACN,KAAK,EACL,cAAc,EACd,cAAc,EACd,QAAQ,EACR,SAAS,GACV,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * tvEventStore — the whole device-side half of the TV remote tool.
3
+ *
4
+ * CONTRACT: this store only ever OBSERVES. It never injects, synthesizes, or
5
+ * fakes a remote press. Presses come from the host (Buoy Desktop shells out to
6
+ * `adb shell input keyevent` / `idb ui key`), because those go through the real
7
+ * platform focus engine; anything we could fake in-process would move focus
8
+ * without firing events, or fire events without moving focus. See
9
+ * TV_REMOTE_DRIVER_PLAN.md §2.
10
+ *
11
+ * It is also deliberately UI-less. On Android TV any focusable view we rendered
12
+ * would become a D-pad stop in the host app's focus order, so the tool would
13
+ * alter the navigation it is supposed to be replaying.
14
+ *
15
+ * Safe to ship in non-TV apps: the native event emitter is never touched unless
16
+ * `Platform.isTV` is true, so the cost on a phone is one boolean.
17
+ */
18
+ /** One captured remote event, as it goes over the wire to the dashboard. */
19
+ export interface TvRemoteEvent {
20
+ /** Monotonic counter — the cursor `getEventsSince` pages from. */
21
+ seq: number;
22
+ /** Capture time (ms epoch). */
23
+ t: number;
24
+ /** Raw RN event type: "up" | "down" | "left" | "right" | "select" | "playPause" | "menu" | "longSelect" | … */
25
+ eventType: string;
26
+ /** 0 = key down, 1 = key up. tvOS only ever emits 1. */
27
+ eventKeyAction?: number;
28
+ }
29
+ export interface TvRemoteState {
30
+ /** False on phones/tablets/web — the tool renders as unavailable. */
31
+ supported: boolean;
32
+ platform: string;
33
+ captureArmed: boolean;
34
+ /**
35
+ * Whether a native listener is actually attached. Distinct from
36
+ * `captureArmed` on purpose: if the RN build has no TVEventHandler, arming
37
+ * would otherwise look successful while nothing was ever captured, and the
38
+ * desktop would report every replay step as swallowed input.
39
+ */
40
+ listening: boolean;
41
+ /** tvOS only: Menu is being routed to JS instead of popping the stack. */
42
+ menuCaptureArmed: boolean;
43
+ seq: number;
44
+ recent: TvRemoteEvent[];
45
+ }
46
+ /** Start pushing remote events into the buffer. Idempotent. */
47
+ export declare function arm(): void;
48
+ /**
49
+ * Stop capturing and hand the Menu key back to the platform. Every teardown
50
+ * path funnels through here — leaving menu capture on would leave the app
51
+ * unable to navigate back, which is an App Store behavior change.
52
+ */
53
+ export declare function disarm(): void;
54
+ /**
55
+ * tvOS: route the Menu key to JS so it can be recorded, instead of letting it
56
+ * pop the navigation stack (and background the app at the root). This changes
57
+ * how the app under test behaves, so it is opt-in per recording session and is
58
+ * always undone on disarm.
59
+ */
60
+ export declare function setMenuCapture(on: boolean): void;
61
+ export declare function clear(): void;
62
+ /** Events strictly newer than `sinceSeq` — the replay echo cursor. */
63
+ export declare function getEventsSince(sinceSeq: number): TvRemoteEvent[];
64
+ export declare function getState(): TvRemoteState;
65
+ export declare function subscribe(listener: () => void): () => void;
66
+ //# sourceMappingURL=tvEventStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tvEventStore.d.ts","sourceRoot":"","sources":["../../../src/store/tvEventStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,+GAA+G;IAC/G,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,gBAAgB,EAAE,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAwFD,+DAA+D;AAC/D,wBAAgB,GAAG,IAAI,IAAI,CAO1B;AAED;;;;GAIG;AACH,wBAAgB,MAAM,IAAI,IAAI,CAS7B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAUhD;AAED,wBAAgB,KAAK,IAAI,IAAI,CAI5B;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAGhE;AAED,wBAAgB,QAAQ,IAAI,aAAa,CAUxC;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK1D"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * tvRemoteSyncAdapter — streams captured TV remote events to Buoy Desktop via
3
+ * @buoy-gg/external-sync.
4
+ *
5
+ * Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
6
+ * package (same as every other tool's adapter). Auto-discovery in @buoy-gg/core
7
+ * picks it up by export name and announces the "tv-remote" capability.
8
+ *
9
+ * `getEventsSince` exists because snapshots are throttled 200ms trailing-edge
10
+ * per tool, which is too coarse to time a replay step's echo. Replay polls this
11
+ * awaited action instead and gets the real latency.
12
+ */
13
+ import { type TvRemoteEvent, type TvRemoteState } from "../store/tvEventStore";
14
+ export declare const tvRemoteSyncAdapter: {
15
+ version: number;
16
+ getSnapshot(): TvRemoteState;
17
+ subscribe(onChange: () => void): () => void;
18
+ actions: {
19
+ arm(): TvRemoteState;
20
+ disarm(): TvRemoteState;
21
+ /** Cursor-paged echo poll. `seq: 0` returns everything buffered. */
22
+ getEventsSince(params?: {
23
+ seq?: number;
24
+ }): {
25
+ seq: number;
26
+ events: TvRemoteEvent[];
27
+ };
28
+ setMenuCapture(params?: {
29
+ on?: boolean;
30
+ }): TvRemoteState;
31
+ clear(): TvRemoteState;
32
+ };
33
+ };
34
+ //# sourceMappingURL=tvRemoteSyncAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tvRemoteSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/tvRemoteSyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,mBAAmB;;mBAGf,aAAa;wBAIR,MAAM,IAAI,GAAG,MAAM,IAAI;;eAKlC,aAAa;kBAKV,aAAa;QAKvB,oEAAoE;gCAC5C;YAAE,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG;YACzC,GAAG,EAAE,MAAM,CAAC;YACZ,MAAM,EAAE,aAAa,EAAE,CAAC;SACzB;gCAMuB;YAAE,EAAE,CAAC,EAAE,OAAO,CAAA;SAAE,GAAG,aAAa;iBAK/C,aAAa;;CAKzB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@buoy-gg/tv-remote",
3
+ "version": "7.0.31",
4
+ "description": "TV remote capture for React Native TV apps — records the D-pad/select/menu events the app actually receives, so Buoy Desktop can drive and replay real remote presses. Part of Buoy devtools.",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "lib/module/index.js",
9
+ "source": "src/index.tsx",
10
+ "files": [
11
+ "lib"
12
+ ],
13
+ "sideEffects": false,
14
+ "peerDependencies": {
15
+ "react": "*",
16
+ "react-native": "*"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.1.0",
20
+ "@types/react-native": "^0.73.0",
21
+ "typescript": "~5.8.3"
22
+ },
23
+ "react-native-builder-bob": {
24
+ "source": "src",
25
+ "output": "lib",
26
+ "targets": [
27
+ "commonjs",
28
+ "module",
29
+ [
30
+ "typescript",
31
+ {
32
+ "project": "tsconfig.build.json"
33
+ }
34
+ ]
35
+ ]
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Buoy-gg/buoy.git",
40
+ "directory": "packages/tv-remote"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/LovesWorking/react-native-buoy/issues"
44
+ },
45
+ "homepage": "https://buoy.gg/buoy/latest/docs/tools/tv-remote",
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "tag": "latest"
49
+ },
50
+ "keywords": [
51
+ "react-native",
52
+ "expo",
53
+ "devtools",
54
+ "debugging",
55
+ "react-native-devtools",
56
+ "tvos",
57
+ "android-tv",
58
+ "react-native-tvos",
59
+ "remote",
60
+ "d-pad",
61
+ "focus"
62
+ ],
63
+ "scripts": {
64
+ "build": "bob build",
65
+ "typecheck": "tsc --noEmit",
66
+ "clean": "rimraf lib",
67
+ "test": "pnpm run typecheck",
68
+ "postinstall": "echo \"Run pnpm build to compile this package\""
69
+ }
70
+ }