@putdotio/rokit 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -63,7 +63,7 @@ const context: RokuContext = {
63
63
 
64
64
  await pressKey(context, "Info");
65
65
  await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
66
- await querySceneGraph(context);
66
+ await querySceneGraph(context, { attempts: 3 });
67
67
  ```
68
68
 
69
69
  ## Commands
@@ -88,13 +88,17 @@ rokit --version
88
88
  is reachable.
89
89
  - `device-info` prints enhanced Roku device metadata as JSON.
90
90
  - `active-app` prints the foreground app.
91
- - `wait-active` waits until the requested app is foregrounded.
91
+ - `wait-active` waits until the requested app is foregrounded and tolerates
92
+ transient ECP read failures while polling.
92
93
  - `launch` opens an app and waits until it is active. Use repeated `--param`
93
- values for deeplink parameters.
94
+ values for deeplink parameters. Roku launch responses can race app startup, so
95
+ launch accepts transient timeout/fetch failures and then verifies foreground
96
+ state.
94
97
  - `press` sends Roku remote keys through ECP. Use `--delay-ms` for navigation
95
98
  sequences that need a stable gap between keys.
96
99
  - `query` prints a raw ECP response such as `/query/sgnodes/all`.
97
- - `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`.
100
+ - `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`. Library
101
+ callers can pass retry options to `querySceneGraph`.
98
102
  - `assert-node` checks a named SceneGraph node once.
99
103
  - `wait-node` polls SceneGraph until a named node condition matches.
100
104
  - `screenshot` saves a developer screenshot. It requires `ROKIT_PASSWORD`.
package/dist/index.d.mts CHANGED
@@ -42,6 +42,10 @@ type DeviceSummary = {
42
42
  readonly model: string;
43
43
  readonly name: string;
44
44
  };
45
+ type RetryOptions = {
46
+ readonly attempts?: number;
47
+ readonly retryDelayMs?: number;
48
+ };
45
49
  declare const checkDevice: (context: RokuContext) => Promise<DeviceSummary>;
46
50
  declare const getDeviceInfo: (context: RokuContext) => Promise<rokuDeploy.DeviceInfo>;
47
51
  declare const queryActiveApp: (context: RokuContext) => Promise<ActiveApp>;
@@ -49,7 +53,7 @@ declare const waitForActiveApp: (context: RokuContext, appId: string, timeoutMs?
49
53
  declare const launchApp: (context: RokuContext, appId: string, params?: ReadonlyMap<string, string>) => Promise<ActiveApp>;
50
54
  declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
51
55
  declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
52
- declare const querySceneGraph: (context: RokuContext) => Promise<string>;
56
+ declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
53
57
  declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
54
58
  declare const waitForSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation, timeoutMs?: number) => Promise<void>;
55
59
  declare const installPackage: (context: RokuContext & {
@@ -60,4 +64,4 @@ declare const takeScreenshot: (context: RokuContext & {
60
64
  }, outputPath: string) => Promise<string>;
61
65
  declare const validateRemoteKey: (key: string) => void;
62
66
  //#endregion
63
- export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RokuContext, assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
67
+ export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { S as readXmlTag, _ as isNamedNodeVisible, a as launchApp, b as readActiveApp, c as queryEcp, d as validateRemoteKey, f as waitForActiveApp, g as assertNamedNodeText, h as assertNamedNodeState, i as installPackage, l as querySceneGraph, m as assertNamedNode, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot, v as readNamedNodeAttribute, x as readXmlAttribute, y as readNamedNodeAttributes } from "./roku-D-FxlWPE.mjs";
1
+ import { S as readXmlTag, _ as isNamedNodeVisible, a as launchApp, b as readActiveApp, c as queryEcp, d as validateRemoteKey, f as waitForActiveApp, g as assertNamedNodeText, h as assertNamedNodeState, i as installPackage, l as querySceneGraph, m as assertNamedNode, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot, v as readNamedNodeAttribute, x as readXmlAttribute, y as readNamedNodeAttributes } from "./roku-DxlYPbY1.mjs";
2
2
  export { assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
package/dist/rokit.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as launchApp, c as queryEcp, f as waitForActiveApp, i as installPackage, l as querySceneGraph, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot } from "./roku-D-FxlWPE.mjs";
2
+ import { a as launchApp, c as queryEcp, f as waitForActiveApp, i as installPackage, l as querySceneGraph, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot } from "./roku-DxlYPbY1.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync } from "node:fs";
@@ -110,18 +110,27 @@ const queryActiveApp = async (context) => readActiveApp(await fetchText(context,
110
110
  const waitForActiveApp = async (context, appId, timeoutMs = 1e4) => {
111
111
  const start = Date.now();
112
112
  let lastApp;
113
+ let lastError;
113
114
  while (Date.now() - start < timeoutMs) {
114
- lastApp = await queryActiveApp(context);
115
+ try {
116
+ lastApp = await queryActiveApp(context);
117
+ lastError = void 0;
118
+ } catch (error) {
119
+ lastError = formatErrorMessage(error);
120
+ await sleep(500);
121
+ continue;
122
+ }
115
123
  if (lastApp.id === appId) return lastApp;
116
124
  await sleep(500);
117
125
  }
118
126
  const last = lastApp ? `${lastApp.id} ${lastApp.name}` : "unknown";
119
- throw new Error(`expected active app ${appId}, got ${last}`);
127
+ const errorSuffix = lastError ? `; last ECP error: ${lastError}` : "";
128
+ throw new Error(`expected active app ${appId}, got ${last}${errorSuffix}`);
120
129
  };
121
130
  const launchApp = async (context, appId, params = /* @__PURE__ */ new Map()) => {
122
131
  const url = ecpUrl(context, `/launch/${encodeURIComponent(appId)}`);
123
132
  for (const [key, value] of params) url.searchParams.set(key, value);
124
- await postOk(context, url);
133
+ await postLaunchMaybeAccepted(context, url);
125
134
  return await waitForActiveApp(context, appId);
126
135
  };
127
136
  const pressKey = async (context, key) => {
@@ -129,7 +138,18 @@ const pressKey = async (context, key) => {
129
138
  await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
130
139
  };
131
140
  const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
132
- const querySceneGraph = async (context) => await queryEcp(context, "/query/sgnodes/all");
141
+ const querySceneGraph = async (context, options = {}) => {
142
+ const attempts = options.attempts ?? 1;
143
+ const retryDelayMs = options.retryDelayMs ?? 500;
144
+ let lastError;
145
+ for (let attempt = 0; attempt < attempts; attempt += 1) try {
146
+ return await queryEcp(context, "/query/sgnodes/all");
147
+ } catch (error) {
148
+ lastError = error;
149
+ if (attempt < attempts - 1) await sleep(retryDelayMs);
150
+ }
151
+ throw new Error(`SceneGraph query failed: ${formatErrorMessage(lastError)}`);
152
+ };
133
153
  const assertSceneGraphNode = async (context, nodeName, expectation) => {
134
154
  assertNamedNode(await querySceneGraph(context), nodeName, expectation);
135
155
  };
@@ -189,9 +209,22 @@ const postOk = async (context, url) => {
189
209
  });
190
210
  if (!response.ok) throw new Error(`POST ${url.pathname} returned HTTP ${response.status}`);
191
211
  };
212
+ const postLaunchMaybeAccepted = async (context, url) => {
213
+ try {
214
+ const response = await fetch(url, {
215
+ method: "POST",
216
+ signal: AbortSignal.timeout(context.timeoutMs)
217
+ });
218
+ if (!response.ok && response.status !== 503) throw new Error(`POST ${url.pathname} returned HTTP ${response.status}`);
219
+ } catch (error) {
220
+ const message = formatErrorMessage(error).toLowerCase();
221
+ if (!message.includes("abort") && !message.includes("timeout") && !message.includes("fetch failed")) throw error;
222
+ }
223
+ };
192
224
  const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
193
225
  const sleep = (ms) => new Promise((resolve) => {
194
226
  setTimeout(resolve, ms);
195
227
  });
228
+ const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
196
229
  //#endregion
197
230
  export { readXmlTag as S, isNamedNodeVisible as _, launchApp as a, readActiveApp as b, queryEcp as c, validateRemoteKey as d, waitForActiveApp as f, assertNamedNodeText as g, assertNamedNodeState as h, installPackage as i, querySceneGraph as l, assertNamedNode as m, checkDevice as n, pressKey as o, waitForSceneGraphNode as p, getDeviceInfo as r, queryActiveApp as s, assertSceneGraphNode as t, takeScreenshot as u, readNamedNodeAttribute as v, readXmlAttribute as x, readNamedNodeAttributes as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putdotio/rokit",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A tiny CLI companion for Roku device harness work.",
5
5
  "keywords": [
6
6
  "cli",