@putdotio/rokit 1.7.0 → 2.0.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.
package/dist/rokit.mjs CHANGED
@@ -1,9 +1,33 @@
1
1
  #!/usr/bin/env node
2
- import { C as waitForMediaPlayerState, S as waitForActiveApp, T as waitForSceneGraphNode, a as installPackage, b as takeScreenshot, c as pressKey, d as queryMediaPlayer, h as querySceneGraph, i as getDeviceInfo, l as queryActiveApp, n as assertSceneGraphNode, r as checkDevice, s as launchApp, u as queryEcp } from "./roku-B61mpmWt.mjs";
2
+ import { C as takeScreenshot, D as waitForMediaPlayerState, E as waitForActiveApp, G as readSceneGraphFailure, K as readSceneGraphStatus, S as resolvePackageOutputPath, T as validateRemoteKey, _ as querySceneGraph, a as getDeviceInfo, c as launchApp, d as queryActiveApp, f as queryEcp, i as discoverRokuDevices, k as waitForSceneGraphNode, l as packageChannel, n as assertSceneGraphNode, o as installPackage, p as queryMediaPlayer, r as checkDevice, u as pressKey, w as validateEcpPath } from "./roku-BxnS6Axs.mjs";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, join } from "node:path";
5
- import { existsSync, mkdirSync } from "node:fs";
6
- const envPath = join(join(process.cwd(), ".rokit"), ".env");
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { NodeRuntime } from "@effect/platform-node";
6
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
7
+ import { Effect, Schema } from "effect";
8
+ //#region src/errors.ts
9
+ var InvalidInput = class extends Schema.TaggedErrorClass()("InvalidInput", { message: Schema.String }) {};
10
+ var MissingTarget = class extends Schema.TaggedErrorClass()("MissingTarget", {}) {
11
+ get message() {
12
+ return "ROKIT_TARGET is not set";
13
+ }
14
+ };
15
+ var MissingPassword = class extends Schema.TaggedErrorClass()("MissingPassword", {}) {
16
+ get message() {
17
+ return "ROKIT_PASSWORD is not set";
18
+ }
19
+ };
20
+ var UnexpectedRokitFailure = class extends Schema.TaggedErrorClass()("UnexpectedRokitFailure", { message: Schema.String }) {};
21
+ const renderError = (error) => error.message;
22
+ const normalizeError = (error) => {
23
+ if (isRokitError(error)) return error;
24
+ return UnexpectedRokitFailure.make({ message: error instanceof Error ? error.message : String(error) });
25
+ };
26
+ const isRokitError = (error) => error instanceof InvalidInput || error instanceof MissingPassword || error instanceof MissingTarget || error instanceof UnexpectedRokitFailure;
27
+ //#endregion
28
+ //#region src/runtime.ts
29
+ const appDir = process.cwd();
30
+ const envPath = join(join(appDir, ".rokit"), ".env");
7
31
  const loadLocalEnv = () => {
8
32
  if (existsSync(envPath)) process.loadEnvFile(envPath);
9
33
  };
@@ -15,17 +39,41 @@ const loadEnv = () => ({
15
39
  });
16
40
  const requireTarget = (env) => {
17
41
  const target = env.target?.trim();
18
- if (!target) return fail("ROKIT_TARGET is not set");
42
+ if (!target) throw MissingTarget.make({});
19
43
  return normalizeTarget(target);
20
44
  };
21
45
  const requirePassword = (env) => {
22
46
  const password = env.password;
23
- if (!password) return fail("ROKIT_PASSWORD is not set");
47
+ if (!password) throw MissingPassword.make({});
24
48
  return password;
25
49
  };
26
- var RokitCliError = class extends Error {};
50
+ const resolveOutputPath = (path, label) => {
51
+ rejectUnsafeInput(path, label);
52
+ const resolved = resolve(appDir, path);
53
+ const relativePath = relative(appDir, resolved);
54
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) fail(`${label} must stay within the current working directory`);
55
+ return resolved;
56
+ };
57
+ const resolveFileOutputPath = (path, label) => {
58
+ const resolved = resolveOutputPath(path, label);
59
+ if (resolved === appDir) fail(`${label} must name a file within the current working directory`);
60
+ return resolved;
61
+ };
62
+ const rejectUnsafeInput = (value, label) => {
63
+ if ([...value].some((character) => {
64
+ const code = character.charCodeAt(0);
65
+ return code < 32 || code === 127;
66
+ })) fail(`${label} contains control characters`);
67
+ };
68
+ const rejectUnsafeEcpPath = (value) => {
69
+ try {
70
+ validateEcpPath(value);
71
+ } catch (error) {
72
+ fail(formatErrorMessage(error));
73
+ }
74
+ };
27
75
  const fail = (message) => {
28
- throw new RokitCliError(message);
76
+ throw InvalidInput.make({ message });
29
77
  };
30
78
  const formatErrorMessage = (error) => {
31
79
  if (error instanceof Error) return error.message;
@@ -41,38 +89,83 @@ const parseTimeout = (value) => {
41
89
  //#endregion
42
90
  //#region src/cli.ts
43
91
  const packageJson = createRequire(import.meta.url)("../package.json");
44
- const main = async (argv = process.argv.slice(2)) => {
92
+ const mainEffect = Effect.fn("mainEffect")(function* (argv = process.argv.slice(2)) {
45
93
  let outputMode = inferOutputMode(argv);
46
- try {
47
- const options = parseGlobalOptions(argv);
48
- outputMode = options.outputMode;
49
- const firstArg = options.args[0];
50
- if (!firstArg || firstArg === "--help" || firstArg === "-h") {
51
- printHelp();
52
- return;
53
- }
54
- if (firstArg === "--version" || firstArg === "-v") {
55
- console.log(packageJson.version);
56
- return;
57
- }
94
+ let fields = [];
95
+ yield* Effect.tryPromise({
96
+ try: async () => {
97
+ await runMain(argv, (mode) => {
98
+ outputMode = mode;
99
+ }, (nextFields) => {
100
+ fields = nextFields;
101
+ });
102
+ },
103
+ catch: normalizeError
104
+ }).pipe(Effect.catch((error) => Effect.sync(() => {
105
+ printError(outputMode, renderError(error), fields);
106
+ process.exitCode = 1;
107
+ })));
108
+ });
109
+ const runMain = async (argv, setOutputMode, setFields) => {
110
+ const options = parseGlobalOptions(argv);
111
+ const fields = options.fields;
112
+ setOutputMode(options.outputMode);
113
+ setFields(fields);
114
+ const firstArg = options.inputJson === void 0 ? options.args[0] : "input-json";
115
+ if (options.inputJson === void 0 && (!firstArg || firstArg === "--help" || firstArg === "-h")) {
116
+ printHelp();
117
+ return;
118
+ }
119
+ if (options.inputJson === void 0 && (firstArg === "--version" || firstArg === "-v")) {
120
+ console.log(packageJson.version);
121
+ return;
122
+ }
123
+ const command = parseCommand(options);
124
+ let context;
125
+ if (commandNeedsTarget(command, options.dryRun)) {
58
126
  loadLocalEnv();
59
127
  const env = loadEnv();
60
- const target = requireTarget(env);
61
- const result = await runCommand({
128
+ context = {
62
129
  password: env.password,
63
- target,
130
+ target: requireTarget(env),
64
131
  timeoutMs: env.timeoutMs,
65
132
  username: env.username
66
- }, parseCommand(options.args));
67
- printResult(options.outputMode, result);
68
- } catch (error) {
69
- printError(outputMode, formatErrorMessage(error));
70
- process.exitCode = 1;
133
+ };
71
134
  }
135
+ const result = await runCommand(context, command, options.dryRun);
136
+ printResult(options.outputMode, result, fields);
72
137
  };
73
- const runCommand = async (context, command) => {
138
+ const runCommand = async (context, command, dryRun) => {
139
+ if (command.name === "describe") return {
140
+ command: command.name,
141
+ data: describeCli(),
142
+ status: "ok"
143
+ };
144
+ if (command.name === "discover") {
145
+ if (dryRun) return dryRunResult(command.name, { timeoutMs: command.timeoutMs ?? 3e3 });
146
+ const devices = await discoverRokuDevices(command.timeoutMs);
147
+ return {
148
+ command: command.name,
149
+ data: { devices },
150
+ message: devices.length === 0 ? "no Roku devices discovered" : devices.map((device) => `${device.target ?? "unknown"} ${device.location}`).join("\n"),
151
+ status: "ok"
152
+ };
153
+ }
154
+ if (command.name === "package") {
155
+ const outputPath = resolveFileOutputPath(command.outputPath, "package output path");
156
+ if (dryRun) return dryRunResult(command.name, { path: resolvePackageOutputPath(outputPath) });
157
+ const result = await packageChannel(outputPath);
158
+ return {
159
+ command: command.name,
160
+ data: result,
161
+ message: `package: ${result.path}`,
162
+ status: "ok"
163
+ };
164
+ }
165
+ if (dryRun && commandSupportsDryRun(command)) return dryRunResult(command.name, dryRunData(command));
166
+ const deviceContext = requireContext(context);
74
167
  if (command.name === "check") {
75
- const summary = await checkDevice(context);
168
+ const summary = await checkDevice(deviceContext);
76
169
  return {
77
170
  command: command.name,
78
171
  data: summary,
@@ -86,11 +179,11 @@ const runCommand = async (context, command) => {
86
179
  }
87
180
  if (command.name === "device-info") return {
88
181
  command: command.name,
89
- data: await getDeviceInfo(context),
182
+ data: await getDeviceInfo(deviceContext),
90
183
  status: "ok"
91
184
  };
92
185
  if (command.name === "active-app") {
93
- const app = await queryActiveApp(context);
186
+ const app = await queryActiveApp(deviceContext);
94
187
  return {
95
188
  command: command.name,
96
189
  data: app,
@@ -99,7 +192,7 @@ const runCommand = async (context, command) => {
99
192
  };
100
193
  }
101
194
  if (command.name === "media-player") {
102
- const mediaPlayer = await queryMediaPlayer(context);
195
+ const mediaPlayer = await queryMediaPlayer(deviceContext);
103
196
  return {
104
197
  command: command.name,
105
198
  data: mediaPlayer,
@@ -108,7 +201,7 @@ const runCommand = async (context, command) => {
108
201
  };
109
202
  }
110
203
  if (command.name === "wait-media-player") {
111
- const mediaPlayer = await waitForMediaPlayerState(context, command.state, command.timeoutMs);
204
+ const mediaPlayer = await waitForMediaPlayerState(deviceContext, command.state, command.timeoutMs);
112
205
  return {
113
206
  command: command.name,
114
207
  data: mediaPlayer,
@@ -117,7 +210,7 @@ const runCommand = async (context, command) => {
117
210
  };
118
211
  }
119
212
  if (command.name === "wait-active") {
120
- const app = await waitForActiveApp(context, command.appId, command.timeoutMs);
213
+ const app = await waitForActiveApp(deviceContext, command.appId, command.timeoutMs);
121
214
  return {
122
215
  command: command.name,
123
216
  data: app,
@@ -126,7 +219,11 @@ const runCommand = async (context, command) => {
126
219
  };
127
220
  }
128
221
  if (command.name === "launch") {
129
- const app = await launchApp(context, command.args.appId, command.args.params);
222
+ if (dryRun) return dryRunResult(command.name, {
223
+ appId: command.args.appId,
224
+ params: Object.fromEntries(command.args.params)
225
+ });
226
+ const app = await launchApp(deviceContext, command.args.appId, command.args.params);
130
227
  return {
131
228
  command: command.name,
132
229
  data: app,
@@ -135,24 +232,43 @@ const runCommand = async (context, command) => {
135
232
  };
136
233
  }
137
234
  if (command.name === "press") {
235
+ if (dryRun) return dryRunResult(command.name, {
236
+ delayMs: command.args.delayMs,
237
+ keys: command.args.keys,
238
+ maxAttempts: command.args.maxAttempts,
239
+ until: command.args.until ? formatNodeData(command.args.until) : void 0
240
+ });
138
241
  const pressed = [];
139
- for (const [index, key] of command.args.keys.entries()) {
140
- if (index > 0 && command.args.delayMs > 0) await sleep(command.args.delayMs);
141
- await pressKey(context, key);
142
- pressed.push(key);
242
+ let attempts = 0;
243
+ while (attempts < command.args.maxAttempts) {
244
+ attempts += 1;
245
+ for (const [index, key] of command.args.keys.entries()) {
246
+ if ((index > 0 || attempts > 1) && command.args.delayMs > 0) await sleep(command.args.delayMs);
247
+ await pressKey(deviceContext, key);
248
+ pressed.push(key);
249
+ }
250
+ if (!command.args.until) break;
251
+ try {
252
+ await assertSceneGraphNode(deviceContext, command.args.until.nodeName, command.args.until.expectation);
253
+ break;
254
+ } catch (error) {
255
+ if (attempts >= command.args.maxAttempts) throw error;
256
+ }
143
257
  }
144
258
  return {
145
259
  command: command.name,
146
260
  data: {
261
+ attempts,
147
262
  delayMs: command.args.delayMs,
148
- keys: pressed
263
+ keys: pressed,
264
+ until: command.args.until ? formatNodeData(command.args.until) : void 0
149
265
  },
150
266
  message: pressed.map((key) => `pressed: ${key}`).join("\n"),
151
267
  status: "ok"
152
268
  };
153
269
  }
154
270
  if (command.name === "query") {
155
- const body = await queryEcp(context, command.path);
271
+ const body = await queryEcp(deviceContext, command.path);
156
272
  return {
157
273
  command: command.name,
158
274
  data: {
@@ -164,7 +280,7 @@ const runCommand = async (context, command) => {
164
280
  };
165
281
  }
166
282
  if (command.name === "sgnodes") {
167
- const body = await querySceneGraph(context);
283
+ const body = await querySceneGraph(deviceContext);
168
284
  return {
169
285
  command: command.name,
170
286
  data: { body },
@@ -173,7 +289,7 @@ const runCommand = async (context, command) => {
173
289
  };
174
290
  }
175
291
  if (command.name === "assert-node") {
176
- await assertSceneGraphNode(context, command.args.nodeName, command.args.expectation);
292
+ await assertSceneGraphNode(deviceContext, command.args.nodeName, command.args.expectation);
177
293
  return {
178
294
  command: command.name,
179
295
  data: formatNodeData(command.args),
@@ -182,7 +298,7 @@ const runCommand = async (context, command) => {
182
298
  };
183
299
  }
184
300
  if (command.name === "wait-node") {
185
- await waitForSceneGraphNode(context, command.args.nodeName, command.args.expectation, command.args.timeoutMs);
301
+ await waitForSceneGraphNode(deviceContext, command.args.nodeName, command.args.expectation, command.args.timeoutMs);
186
302
  return {
187
303
  command: command.name,
188
304
  data: formatNodeData(command.args),
@@ -191,23 +307,59 @@ const runCommand = async (context, command) => {
191
307
  };
192
308
  }
193
309
  if (command.name === "screenshot") {
194
- const password = requirePassword(context);
195
- mkdirSync(dirname(command.outputPath), { recursive: true });
196
- const path = await takeScreenshot({
197
- ...context,
310
+ const path = resolveFileOutputPath(command.outputPath, "screenshot output path");
311
+ if (dryRun) return dryRunResult(command.name, { path });
312
+ const password = requirePassword(deviceContext);
313
+ mkdirSync(dirname(path), { recursive: true });
314
+ const screenshotPath = await takeScreenshot({
315
+ ...deviceContext,
198
316
  password
199
- }, command.outputPath);
317
+ }, path);
318
+ return {
319
+ command: command.name,
320
+ data: { path: screenshotPath },
321
+ message: `screenshot: ${screenshotPath}`,
322
+ status: "ok"
323
+ };
324
+ }
325
+ if (command.name === "snapshot") {
326
+ const data = await collectSnapshot(deviceContext);
327
+ return observationResult(command.name, data);
328
+ }
329
+ if (command.name === "proof") {
330
+ const outputDir = resolveOutputPath(command.outputDir, "proof output directory");
331
+ if (dryRun) return dryRunResult(command.name, {
332
+ outputDir,
333
+ screenshot: command.screenshot
334
+ });
335
+ const data = await writeProof(deviceContext, outputDir, command.screenshot);
336
+ return {
337
+ command: command.name,
338
+ data,
339
+ ...partialObservationMetadata(data.snapshot),
340
+ message: data.artifacts.map((artifact) => `${artifact.kind}: ${artifact.path}`).join("\n"),
341
+ status: "ok"
342
+ };
343
+ }
344
+ if (command.name === "wait-ready") {
345
+ const data = await waitForReady(deviceContext, command);
346
+ const failedObservations = data.sceneGraph.status === "failed" ? ["sceneGraph"] : [];
200
347
  return {
201
348
  command: command.name,
202
- data: { path },
203
- message: `screenshot: ${path}`,
349
+ data,
350
+ ...failedObservations.length > 0 ? {
351
+ failedObservations,
352
+ partial: true
353
+ } : void 0,
354
+ message: `ready: active app ${data.activeApp.id}`,
204
355
  status: "ok"
205
356
  };
206
357
  }
207
358
  if (command.name === "install") {
208
- const password = requirePassword(context);
359
+ if (dryRun) return dryRunResult(command.name, { zipPath: command.zipPath });
360
+ const password = requirePassword(deviceContext);
209
361
  const message = await installPackage({
210
- ...context,
362
+ ...deviceContext,
211
363
  password
212
364
  }, command.zipPath);
213
365
  return {
@@ -217,13 +369,191 @@ const runCommand = async (context, command) => {
217
369
  status: "ok"
218
370
  };
219
371
  }
220
- throw new Error(`unsupported command: ${command.name}`);
372
+ throw new Error("unsupported command");
373
+ };
374
+ const requireContext = (context) => {
375
+ if (!context) return fail("ROKIT_TARGET is not set");
376
+ return context;
377
+ };
378
+ const dryRunResult = (command, data) => ({
379
+ command,
380
+ data,
381
+ dryRun: true,
382
+ message: `dry-run: ${command}`,
383
+ status: "ok"
384
+ });
385
+ const observe = async (read) => {
386
+ try {
387
+ return {
388
+ data: await read(),
389
+ status: "ok"
390
+ };
391
+ } catch (error) {
392
+ return {
393
+ error: { message: renderError(normalizeError(error)) },
394
+ status: "failed"
395
+ };
396
+ }
397
+ };
398
+ const collectSnapshot = async (context) => {
399
+ const sceneGraph = await observe(async () => await querySceneGraph(context));
400
+ const sceneGraphBody = sceneGraph.status === "ok" ? sceneGraph.data : void 0;
401
+ return {
402
+ activeApp: await observe(async () => await queryActiveApp(context)),
403
+ device: await observe(async () => await checkDevice(context)),
404
+ mediaPlayer: await observe(async () => await queryMediaPlayer(context)),
405
+ sceneGraph: sceneGraphBody === void 0 ? sceneGraph : {
406
+ data: {
407
+ failure: readSceneGraphFailure(sceneGraphBody),
408
+ status: readSceneGraphStatus(sceneGraphBody)
409
+ },
410
+ status: "ok"
411
+ }
412
+ };
413
+ };
414
+ const observationResult = (command, data) => ({
415
+ command,
416
+ data,
417
+ ...partialObservationMetadata(data),
418
+ status: "ok"
419
+ });
420
+ const partialObservationMetadata = (snapshot) => {
421
+ const failedObservations = failedSnapshotObservations(snapshot);
422
+ return failedObservations.length === 0 ? {} : {
423
+ failedObservations,
424
+ partial: true
425
+ };
426
+ };
427
+ const failedSnapshotObservations = (snapshot) => {
428
+ const failures = [];
429
+ if (snapshot.activeApp.status === "failed") failures.push("activeApp");
430
+ if (snapshot.device.status === "failed") failures.push("device");
431
+ if (snapshot.mediaPlayer.status === "failed") failures.push("mediaPlayer");
432
+ if (snapshot.sceneGraph.status === "failed") failures.push("sceneGraph");
433
+ return failures;
434
+ };
435
+ const writeProof = async (context, outputDir, includeScreenshot) => {
436
+ mkdirSync(outputDir, { recursive: true });
437
+ const artifacts = [];
438
+ const writeJson = (name, value) => {
439
+ const path = `${outputDir}/${name}.json`;
440
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
441
+ artifacts.push({
442
+ kind: "json",
443
+ path
444
+ });
445
+ return path;
446
+ };
447
+ const snapshot = await collectSnapshot(context);
448
+ writeJson("summary", snapshot);
449
+ const sceneGraph = await observe(async () => await querySceneGraph(context));
450
+ if (sceneGraph.status === "ok") {
451
+ const path = `${outputDir}/sgnodes.xml`;
452
+ writeFileSync(path, sceneGraph.data);
453
+ artifacts.push({
454
+ kind: "xml",
455
+ path
456
+ });
457
+ }
458
+ writeJson("device-info", await observe(async () => await getDeviceInfo(context)));
459
+ writeJson("active-app", await observe(async () => await queryActiveApp(context)));
460
+ writeJson("media-player", await observe(async () => await queryMediaPlayer(context)));
461
+ if (includeScreenshot) {
462
+ const password = requirePassword(context);
463
+ const path = await takeScreenshot({
464
+ ...context,
465
+ password
466
+ }, `${outputDir}/screenshot.png`);
467
+ artifacts.push({
468
+ kind: "screenshot",
469
+ path
470
+ });
471
+ }
472
+ return {
473
+ artifacts,
474
+ outputDir,
475
+ snapshot
476
+ };
477
+ };
478
+ const waitForReady = async (context, command) => {
479
+ const activeApp = await waitForActiveApp(context, command.appId, command.timeoutMs);
480
+ const sceneGraph = await observe(async () => querySceneGraph(context, {
481
+ attempts: 3,
482
+ requireComplete: true
483
+ }));
484
+ if (command.node) await waitForSceneGraphNode(context, command.node.nodeName, command.node.expectation, command.node.timeoutMs ?? command.timeoutMs);
485
+ return {
486
+ activeApp,
487
+ mediaPlayer: command.mediaState ? {
488
+ data: await waitForMediaPlayerState(context, command.mediaState, command.timeoutMs),
489
+ status: "ok"
490
+ } : await observe(async () => queryMediaPlayer(context)),
491
+ sceneGraph: sceneGraph.status === "ok" ? {
492
+ data: {
493
+ failure: readSceneGraphFailure(sceneGraph.data),
494
+ status: readSceneGraphStatus(sceneGraph.data)
495
+ },
496
+ status: "ok"
497
+ } : sceneGraph
498
+ };
499
+ };
500
+ const commandNeedsTarget = (command, dryRun) => {
501
+ if (command.name === "describe" || command.name === "discover" || command.name === "package") return false;
502
+ return !(dryRun && commandSupportsDryRun(command));
503
+ };
504
+ const commandSupportsDryRun = (command) => command.name === "install" || command.name === "launch" || command.name === "package" || command.name === "press" || command.name === "proof" || command.name === "screenshot";
505
+ const dryRunData = (command) => {
506
+ if (command.name === "launch") return {
507
+ appId: command.args.appId,
508
+ params: Object.fromEntries(command.args.params)
509
+ };
510
+ if (command.name === "press") {
511
+ for (const key of command.args.keys) validateRemoteKey(key);
512
+ return {
513
+ delayMs: command.args.delayMs,
514
+ keys: command.args.keys,
515
+ maxAttempts: command.args.maxAttempts,
516
+ until: command.args.until ? formatNodeData(command.args.until) : void 0
517
+ };
518
+ }
519
+ if (command.name === "screenshot") return { path: resolveFileOutputPath(command.outputPath, "screenshot output path") };
520
+ if (command.name === "proof") return {
521
+ outputDir: resolveOutputPath(command.outputDir, "proof output directory"),
522
+ screenshot: command.screenshot
523
+ };
524
+ if (command.name === "package") return { path: resolveOutputPath(command.outputPath, "package output path") };
525
+ if (command.name === "install") return { zipPath: command.zipPath };
526
+ return {};
221
527
  };
222
528
  const parseGlobalOptions = (argv) => {
223
529
  const args = [];
224
- let outputMode = "text";
530
+ let dryRun = false;
531
+ let fields = [];
532
+ let inputJson;
533
+ let outputMode = defaultOutputMode();
225
534
  for (let index = 0; index < argv.length; index += 1) {
226
535
  const arg = argv[index];
536
+ if (arg === "--dry-run") {
537
+ dryRun = true;
538
+ continue;
539
+ }
540
+ if (arg === "--fields") {
541
+ const value = argv[index + 1];
542
+ if (!value) fail("usage: rokit [--fields field[,field...]] <command>");
543
+ fields = value.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
544
+ outputMode = "json";
545
+ index += 1;
546
+ continue;
547
+ }
548
+ if (arg === "--input-json") {
549
+ const value = argv[index + 1];
550
+ if (!value) fail("usage: rokit --input-json '<payload>'");
551
+ if (inputJson !== void 0) fail("usage: rokit --input-json '<payload>'");
552
+ inputJson = value;
553
+ outputMode = "json";
554
+ index += 1;
555
+ continue;
556
+ }
227
557
  if (arg === "--json") {
228
558
  outputMode = "json";
229
559
  continue;
@@ -237,18 +567,27 @@ const parseGlobalOptions = (argv) => {
237
567
  }
238
568
  args.push(arg);
239
569
  }
570
+ if (inputJson !== void 0 && args.length > 0) fail("usage: rokit --input-json '<payload>'");
240
571
  return {
241
572
  args,
573
+ dryRun,
574
+ fields,
575
+ inputJson,
242
576
  outputMode
243
577
  };
244
578
  };
245
579
  const inferOutputMode = (argv) => {
246
580
  if (argv.includes("--json")) return "json";
247
- return argv[argv.indexOf("--output") + 1] === "json" ? "json" : "text";
581
+ const outputIndex = argv.indexOf("--output");
582
+ if (argv[outputIndex + 1] === "json") return "json";
583
+ if (argv[outputIndex + 1] === "text") return "text";
584
+ if (argv.includes("--input-json") || argv.includes("--fields")) return "json";
585
+ return defaultOutputMode();
248
586
  };
249
- const printResult = (outputMode, result) => {
587
+ const defaultOutputMode = () => process.stdout.isTTY ? "text" : "json";
588
+ const printResult = (outputMode, result, fields) => {
250
589
  if (outputMode === "json") {
251
- console.log(JSON.stringify(result, null, 2));
590
+ console.log(JSON.stringify(applyFields(result, fields), null, 2));
252
591
  return;
253
592
  }
254
593
  if (result.message !== void 0) {
@@ -257,7 +596,7 @@ const printResult = (outputMode, result) => {
257
596
  }
258
597
  console.log(JSON.stringify(result.data, null, 2));
259
598
  };
260
- const printError = (outputMode, message) => {
599
+ const printError = (outputMode, message, _fields) => {
261
600
  if (outputMode === "json") {
262
601
  console.error(JSON.stringify({
263
602
  error: { message },
@@ -267,9 +606,15 @@ const printError = (outputMode, message) => {
267
606
  }
268
607
  console.error(message);
269
608
  };
270
- const parseCommand = (argv) => {
271
- const [name, ...args] = argv;
609
+ const parseCommand = (options) => {
610
+ if (options.inputJson !== void 0) return parseInputJson(options.inputJson);
611
+ const [name, ...args] = options.args;
612
+ if (name === "describe") return { name };
272
613
  if (name === "check") return { name };
614
+ if (name === "discover") return {
615
+ name,
616
+ timeoutMs: parseOptionalTimeout(args, "rokit discover")
617
+ };
273
618
  if (name === "device-info") return { name };
274
619
  if (name === "active-app") return { name };
275
620
  if (name === "media-player") return { name };
@@ -282,6 +627,7 @@ const parseCommand = (argv) => {
282
627
  timeoutMs: parseTimeoutOption(args.slice(1), "rokit wait-media-player <state>")
283
628
  };
284
629
  }
630
+ if (name === "wait-ready") return parseWaitReadyArgs(args);
285
631
  if (name === "wait-active") {
286
632
  const appId = args[0];
287
633
  if (!appId) fail("usage: rokit wait-active <app-id> [--timeout-ms <ms>]");
@@ -302,6 +648,7 @@ const parseCommand = (argv) => {
302
648
  if (name === "query") {
303
649
  const path = args[0];
304
650
  if (!path) fail("usage: rokit query <ecp-path>");
651
+ rejectUnsafeEcpPath(path);
305
652
  return {
306
653
  name,
307
654
  path
@@ -320,6 +667,12 @@ const parseCommand = (argv) => {
320
667
  outputPath
321
668
  };
322
669
  }
670
+ if (name === "snapshot") return { name };
671
+ if (name === "proof") return parseProofArgs(args);
672
+ if (name === "package") return {
673
+ name,
674
+ outputPath: parseOutputPath(args, "rokit package --out <zip-path>")
675
+ };
323
676
  if (name === "install") {
324
677
  const zipPath = args[0];
325
678
  if (!zipPath) fail("usage: rokit install <zip-path>");
@@ -371,7 +724,10 @@ const parseNodeCondition = (commandName, args) => {
371
724
  };
372
725
  const parsePressArgs = (args) => {
373
726
  let delayMs = 0;
727
+ let maxAttempts = 1;
728
+ let maxAttemptsWasProvided = false;
374
729
  const keys = [];
730
+ let until;
375
731
  for (let index = 0; index < args.length; index += 1) {
376
732
  const arg = args[index];
377
733
  if (arg === "--delay-ms") {
@@ -381,15 +737,254 @@ const parsePressArgs = (args) => {
381
737
  index += 1;
382
738
  continue;
383
739
  }
740
+ if (arg === "--max") {
741
+ const value = args[index + 1];
742
+ if (!value) fail("usage: rokit press [--delay-ms <ms>] [--until-node ... --max <count>] <key> [key...]");
743
+ maxAttempts = parsePositiveInteger(value, "max attempts");
744
+ maxAttemptsWasProvided = true;
745
+ index += 1;
746
+ continue;
747
+ }
748
+ if (arg === "--until-node") {
749
+ until = parseNodeCondition("press --until-node", args.slice(index + 1));
750
+ if (!maxAttemptsWasProvided) maxAttempts = 8;
751
+ break;
752
+ }
384
753
  if (arg?.startsWith("--")) fail(`Unknown press option: ${arg}`);
385
754
  if (arg !== void 0) keys.push(arg);
386
755
  }
387
756
  if (keys.length === 0) fail("usage: rokit press [--delay-ms <ms>] <key> [key...]");
388
757
  return {
389
758
  delayMs,
390
- keys
759
+ keys,
760
+ maxAttempts,
761
+ until
762
+ };
763
+ };
764
+ const parseOptionalTimeout = (args, usagePrefix) => args.length === 0 ? void 0 : parseTimeoutOption(args, usagePrefix);
765
+ const parseOutputPath = (args, usage) => {
766
+ if (args.length === 1 && args[0] !== void 0 && !args[0].startsWith("--")) return args[0];
767
+ if (args.length === 2 && args[0] === "--out" && args[1] !== void 0) return args[1];
768
+ return fail(`usage: ${usage}`);
769
+ };
770
+ const parseProofArgs = (args) => {
771
+ let outputDir;
772
+ let screenshot = false;
773
+ const assignOutputDir = (nextOutputDir) => {
774
+ if (!nextOutputDir) fail("usage: rokit proof <output-dir> [--screenshot]");
775
+ if (outputDir !== void 0) fail("usage: rokit proof <output-dir> [--screenshot]");
776
+ outputDir = nextOutputDir;
777
+ };
778
+ for (let index = 0; index < args.length; index += 1) {
779
+ const arg = args[index];
780
+ if (arg === "--out") {
781
+ assignOutputDir(args[index + 1]);
782
+ index += 1;
783
+ continue;
784
+ }
785
+ if (arg === "--screenshot") {
786
+ screenshot = true;
787
+ continue;
788
+ }
789
+ if (arg?.startsWith("--")) fail(`Unknown proof option: ${arg}`);
790
+ assignOutputDir(arg);
791
+ }
792
+ if (!outputDir) return fail("usage: rokit proof <output-dir> [--screenshot]");
793
+ return {
794
+ name: "proof",
795
+ outputDir,
796
+ screenshot
797
+ };
798
+ };
799
+ const parseWaitReadyArgs = (args) => {
800
+ const appId = args[0];
801
+ if (!appId) return fail("usage: rokit wait-ready <app-id> [--media-state <state>] [--node ...] [--timeout-ms <ms>]");
802
+ let mediaState;
803
+ let node;
804
+ let timeoutMs;
805
+ for (let index = 1; index < args.length; index += 1) {
806
+ const arg = args[index];
807
+ if (arg === "--media-state") {
808
+ mediaState = args[index + 1];
809
+ if (!mediaState) fail("usage: rokit wait-ready <app-id> --media-state <state>");
810
+ index += 1;
811
+ continue;
812
+ }
813
+ if (arg === "--timeout-ms") {
814
+ timeoutMs = parsePositiveInteger(args[index + 1] ?? "", "timeout");
815
+ index += 1;
816
+ continue;
817
+ }
818
+ if (arg === "--node") {
819
+ node = parseNodeCondition("wait-ready --node", args.slice(index + 1));
820
+ break;
821
+ }
822
+ fail(`Unknown wait-ready option: ${arg ?? ""}`);
823
+ }
824
+ return {
825
+ appId,
826
+ mediaState,
827
+ name: "wait-ready",
828
+ node,
829
+ timeoutMs: timeoutMs ?? node?.timeoutMs
830
+ };
831
+ };
832
+ const parseInputJson = (value) => {
833
+ const parsed = requireRecord(JSON.parse(value), "input JSON");
834
+ const command = readString(parsed, "command");
835
+ if (command === "describe") return { name: "describe" };
836
+ if (command === "discover") return {
837
+ name: "discover",
838
+ timeoutMs: readOptionalNumber(parsed, "timeoutMs")
839
+ };
840
+ if (command === "check" || command === "device-info" || command === "active-app") return { name: command };
841
+ if (command === "media-player" || command === "sgnodes" || command === "snapshot") return { name: command };
842
+ if (command === "wait-active") return {
843
+ appId: readString(parsed, "appId"),
844
+ name: "wait-active",
845
+ timeoutMs: readOptionalNumber(parsed, "timeoutMs")
846
+ };
847
+ if (command === "wait-media-player") return {
848
+ name: "wait-media-player",
849
+ state: readString(parsed, "state"),
850
+ timeoutMs: readOptionalNumber(parsed, "timeoutMs")
851
+ };
852
+ if (command === "wait-ready") return {
853
+ appId: readString(parsed, "appId"),
854
+ mediaState: readOptionalString(parsed, "mediaState"),
855
+ name: "wait-ready",
856
+ node: readOptionalNodeCondition(parsed, "node"),
857
+ timeoutMs: readOptionalNumber(parsed, "timeoutMs")
858
+ };
859
+ if (command === "launch") return {
860
+ args: {
861
+ appId: readString(parsed, "appId"),
862
+ params: readStringMap(parsed, "params")
863
+ },
864
+ name: "launch"
865
+ };
866
+ if (command === "press") {
867
+ const keys = readStringArray(parsed, "keys");
868
+ const until = readOptionalNodeCondition(parsed, "until");
869
+ if (keys.length === 0) fail("input JSON field must include at least one key: keys");
870
+ return {
871
+ args: {
872
+ delayMs: readOptionalNumber(parsed, "delayMs") ?? 0,
873
+ keys,
874
+ maxAttempts: readOptionalNumber(parsed, "maxAttempts") ?? (until === void 0 ? 1 : 8),
875
+ until
876
+ },
877
+ name: "press"
878
+ };
879
+ }
880
+ if (command === "query") {
881
+ const path = readString(parsed, "path");
882
+ rejectUnsafeEcpPath(path);
883
+ return {
884
+ name: "query",
885
+ path
886
+ };
887
+ }
888
+ if (command === "assert-node" || command === "wait-node") return {
889
+ args: {
890
+ expectation: readNodeExpectation(parsed),
891
+ nodeName: readString(parsed, "nodeName"),
892
+ timeoutMs: readOptionalNumber(parsed, "timeoutMs")
893
+ },
894
+ name: command
895
+ };
896
+ if (command === "screenshot") return {
897
+ name: "screenshot",
898
+ outputPath: readString(parsed, "outputPath")
899
+ };
900
+ if (command === "proof") return {
901
+ name: "proof",
902
+ outputDir: readString(parsed, "outputDir"),
903
+ screenshot: readOptionalBoolean(parsed, "screenshot") ?? false
904
+ };
905
+ if (command === "package") return {
906
+ name: "package",
907
+ outputPath: readString(parsed, "outputPath")
908
+ };
909
+ if (command === "install") return {
910
+ name: "install",
911
+ zipPath: readString(parsed, "zipPath")
912
+ };
913
+ return fail(`Unknown command: ${command}`);
914
+ };
915
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
916
+ const requireRecord = (value, label) => {
917
+ if (!isRecord(value)) return fail(`${label} must be an object`);
918
+ return value;
919
+ };
920
+ const readString = (record, key) => {
921
+ const value = record[key];
922
+ if (typeof value !== "string" || value.length === 0) return fail(`input JSON is missing string field: ${key}`);
923
+ return value;
924
+ };
925
+ const readOptionalString = (record, key) => {
926
+ const value = record[key];
927
+ if (value === void 0) return;
928
+ if (typeof value !== "string") return fail(`input JSON field must be a string: ${key}`);
929
+ return value;
930
+ };
931
+ const readOptionalBoolean = (record, key) => {
932
+ const value = record[key];
933
+ if (value === void 0) return;
934
+ if (typeof value !== "boolean") return fail(`input JSON field must be a boolean: ${key}`);
935
+ return value;
936
+ };
937
+ const readOptionalNumber = (record, key) => {
938
+ const value = record[key];
939
+ if (value === void 0) return;
940
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return fail(`input JSON field must be a positive integer: ${key}`);
941
+ return value;
942
+ };
943
+ const readStringArray = (record, key) => {
944
+ const value = record[key];
945
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return fail(`input JSON field must be a string array: ${key}`);
946
+ return value.map((item) => String(item));
947
+ };
948
+ const readStringMap = (record, key) => {
949
+ const value = record[key];
950
+ if (value === void 0) return /* @__PURE__ */ new Map();
951
+ if (!isRecord(value)) return fail(`input JSON field must be an object: ${key}`);
952
+ const params = /* @__PURE__ */ new Map();
953
+ for (const [paramKey, paramValue] of Object.entries(value)) {
954
+ if (typeof paramValue !== "string") return fail(`input JSON map values must be strings: ${key}`);
955
+ params.set(paramKey, paramValue);
956
+ }
957
+ return params;
958
+ };
959
+ const readOptionalNodeCondition = (record, key) => {
960
+ const value = record[key];
961
+ if (value === void 0) return;
962
+ if (!isRecord(value)) return fail(`input JSON field must be an object: ${key}`);
963
+ return {
964
+ expectation: readNodeExpectation(value),
965
+ nodeName: readString(value, "nodeName"),
966
+ timeoutMs: readOptionalNumber(value, "timeoutMs")
391
967
  };
392
968
  };
969
+ const readNodeExpectation = (record) => {
970
+ const expectation = record.expectation;
971
+ if (isRecord(expectation)) {
972
+ if (typeof expectation.attribute === "string" && typeof expectation.value === "string") return {
973
+ attribute: expectation.attribute,
974
+ value: expectation.value
975
+ };
976
+ const state = expectation.state;
977
+ if (state === "visible" || state === "hidden" || state === "absent") {
978
+ const text = expectation.text;
979
+ if (text !== void 0 && typeof text !== "string") return fail("input JSON node expectation text must be a string");
980
+ return text === void 0 ? { state } : {
981
+ state,
982
+ text
983
+ };
984
+ }
985
+ }
986
+ return fail("input JSON is missing a valid node expectation");
987
+ };
393
988
  const parseTimeoutOption = (args, usagePrefix) => {
394
989
  if (args.length === 0) return;
395
990
  if (args.length !== 2 || args[0] !== "--timeout-ms") fail(`usage: ${usagePrefix} [--timeout-ms <ms>]`);
@@ -419,6 +1014,33 @@ const formatMediaPlayerMessage = (mediaPlayer) => {
419
1014
  ].join(" ")}`;
420
1015
  };
421
1016
  const formatMaybeMs = (value) => value === void 0 ? "unknown" : `${value}ms`;
1017
+ const applyFields = (value, fields) => {
1018
+ if (fields.length === 0) return value;
1019
+ const output = {};
1020
+ for (const field of fields) copyField(value, output, field.split("."));
1021
+ preserveStatusMetadata(value, output);
1022
+ return output;
1023
+ };
1024
+ const preserveStatusMetadata = (source, target) => {
1025
+ if (!isRecord(source)) return;
1026
+ for (const key of [
1027
+ "status",
1028
+ "partial",
1029
+ "failedObservations"
1030
+ ]) if (key in source) target[key] = source[key];
1031
+ };
1032
+ const copyField = (source, target, path) => {
1033
+ const [head, ...tail] = path;
1034
+ if (!head || !isRecord(source) || !(head in source)) return;
1035
+ if (tail.length === 0) {
1036
+ target[head] = source[head];
1037
+ return;
1038
+ }
1039
+ const child = target[head];
1040
+ const childTarget = isRecord(child) ? child : {};
1041
+ target[head] = childTarget;
1042
+ copyField(source[head], childTarget, tail);
1043
+ };
422
1044
  const sleep = (ms) => new Promise((resolve) => {
423
1045
  setTimeout(resolve, ms);
424
1046
  });
@@ -441,18 +1063,125 @@ const parseLaunchArgs = (args) => {
441
1063
  params
442
1064
  };
443
1065
  };
1066
+ const describeCli = () => ({
1067
+ automation: {
1068
+ dryRun: true,
1069
+ inputJson: true,
1070
+ nonTtyJsonDefault: true,
1071
+ outputFields: true,
1072
+ schemaIntrospection: true
1073
+ },
1074
+ commands: [
1075
+ commandSchema("describe", "Print machine-readable command schemas.", false, false, []),
1076
+ commandSchema("check", "Check ECP and developer installer reachability.", true, false, []),
1077
+ commandSchema("discover", "Discover Roku ECP devices with SSDP.", false, false, [optionField("timeoutMs", "positive-integer", "Discovery timeout in milliseconds.")]),
1078
+ commandSchema("device-info", "Read enhanced Roku device metadata.", true, false, []),
1079
+ commandSchema("active-app", "Read the foreground app.", true, false, []),
1080
+ commandSchema("media-player", "Read parsed /query/media-player state.", true, false, []),
1081
+ commandSchema("snapshot", "Read a compact state snapshot.", true, false, []),
1082
+ commandSchema("proof", "Write reviewable state artifacts.", true, true, [argumentField("outputDir", "path", "Directory where proof artifacts are written."), optionField("screenshot", "boolean", "Include a developer screenshot.")]),
1083
+ commandSchema("package", "Create a sideload ZIP from the current app root.", false, true, [optionField("outputPath", "path", "ZIP output path inside the current app root.", true)]),
1084
+ commandSchema("install", "Publish an existing ZIP to a developer-enabled Roku.", true, true, [argumentField("zipPath", "path", "Existing sideload ZIP path.")]),
1085
+ commandSchema("launch", "Launch an app by id with optional params.", true, true, [argumentField("appId", "string", "Roku application id."), optionField("params", "record<string,string>", "Launch parameters.", false, true)]),
1086
+ commandSchema("press", "Send remote keys, optionally until a node condition matches.", true, true, [
1087
+ argumentField("keys", "string[]", "Remote keys to send.", true, true),
1088
+ optionField("delayMs", "positive-integer", "Delay between keys in milliseconds."),
1089
+ optionField("maxAttempts", "positive-integer", "Maximum repeated attempts."),
1090
+ optionField("until", "node-condition", "SceneGraph condition that stops the loop.")
1091
+ ]),
1092
+ commandSchema("query", "Read a raw ECP path.", true, false, [argumentField("path", "ecp-path", "Raw ECP path without query string or fragment.")]),
1093
+ commandSchema("sgnodes", "Read raw SceneGraph XML.", true, false, []),
1094
+ commandSchema("assert-node", "Assert one SceneGraph node condition.", true, false, [...nodeConditionFields()], [...nodeConditionInputJsonFields()]),
1095
+ commandSchema("wait-node", "Wait for a SceneGraph node condition.", true, false, [...nodeConditionFields(), optionField("timeoutMs", "positive-integer", "Wait timeout in milliseconds.")], [...nodeConditionInputJsonFields(), optionField("timeoutMs", "positive-integer", "Wait timeout in milliseconds.")]),
1096
+ commandSchema("wait-active", "Wait for a foreground app id.", true, false, [argumentField("appId", "string", "Expected foreground Roku application id."), optionField("timeoutMs", "positive-integer", "Wait timeout in milliseconds.")]),
1097
+ commandSchema("wait-media-player", "Wait for a media-player state.", true, false, [argumentField("state", "string", "Expected media-player state."), optionField("timeoutMs", "positive-integer", "Wait timeout in milliseconds.")]),
1098
+ commandSchema("wait-ready", "Wait for active app plus optional node/media readiness.", true, false, [
1099
+ argumentField("appId", "string", "Expected foreground Roku application id."),
1100
+ optionField("mediaState", "string", "Optional media-player state to wait for."),
1101
+ optionField("node", "node-condition", "Optional SceneGraph node condition."),
1102
+ optionField("timeoutMs", "positive-integer", "Wait timeout in milliseconds.")
1103
+ ]),
1104
+ commandSchema("screenshot", "Write a developer screenshot.", true, true, [argumentField("outputPath", "path", "Screenshot output path inside the current app root.")])
1105
+ ],
1106
+ globalOptions: [
1107
+ globalField("json", "boolean", "Print structured JSON output."),
1108
+ globalField("output", "json|text", "Select structured or human output.", false, ["json", "text"]),
1109
+ globalField("dryRun", "boolean", "Validate mutating commands without side effects."),
1110
+ globalField("fields", "field-mask", "Comma-separated JSON field mask for output trimming."),
1111
+ globalField("inputJson", "json-object", "Command payload matching the described input schema.")
1112
+ ],
1113
+ schemaVersion: 2
1114
+ });
1115
+ const commandSchema = (name, description, requiresTarget, mutates, parameters, inputJsonFields = parameters) => ({
1116
+ description,
1117
+ inputJson: {
1118
+ fields: [{
1119
+ description: "Command name.",
1120
+ name: "command",
1121
+ required: true,
1122
+ type: "string",
1123
+ values: [name]
1124
+ }, ...inputJsonFields],
1125
+ required: ["command", ...inputJsonFields.filter((field) => field.required).map((field) => field.name)]
1126
+ },
1127
+ mutates,
1128
+ name,
1129
+ parameters,
1130
+ requiresTarget
1131
+ });
1132
+ const argumentField = (name, type, description, required = true, repeatable = false, values) => ({
1133
+ description,
1134
+ name,
1135
+ repeatable,
1136
+ required,
1137
+ type,
1138
+ values
1139
+ });
1140
+ const optionField = (name, type, description, required = false, repeatable = false, values) => ({
1141
+ description,
1142
+ name,
1143
+ repeatable,
1144
+ required,
1145
+ type,
1146
+ values
1147
+ });
1148
+ const globalField = (name, type, description, required = false, values) => ({
1149
+ description,
1150
+ name,
1151
+ required,
1152
+ type,
1153
+ values
1154
+ });
1155
+ const nodeConditionFields = () => [
1156
+ argumentField("nodeName", "string", "SceneGraph node name."),
1157
+ argumentField("condition", "visible|hidden|absent|text|attr", "Expected node condition.", true, false, [
1158
+ "visible",
1159
+ "hidden",
1160
+ "absent",
1161
+ "text",
1162
+ "attr"
1163
+ ]),
1164
+ optionField("value", "string", "Text or attr name=value pair for text/attr conditions.")
1165
+ ];
1166
+ const nodeConditionInputJsonFields = () => [argumentField("nodeName", "string", "SceneGraph node name."), argumentField("expectation", "node-expectation-object", "Expected node state, text, or attribute object.")];
444
1167
  const printHelp = () => {
445
1168
  console.log(`rokit - Roku device harness helper
446
1169
 
447
1170
  usage:
1171
+ rokit describe
448
1172
  rokit check
1173
+ rokit discover [--timeout-ms <ms>]
449
1174
  rokit device-info
450
1175
  rokit active-app
451
1176
  rokit media-player
1177
+ rokit snapshot
1178
+ rokit proof <output-dir> [--screenshot]
1179
+ rokit package --out <zip-path>
452
1180
  rokit wait-active <app-id> [--timeout-ms <ms>]
453
1181
  rokit wait-media-player <state> [--timeout-ms <ms>]
1182
+ rokit wait-ready <app-id> [--media-state <state>] [--node <node-name> <condition> [value]] [--timeout-ms <ms>]
454
1183
  rokit launch <app-id> [--param key=value]
455
- rokit press [--delay-ms <ms>] <key> [key...]
1184
+ rokit press [--delay-ms <ms>] [--max <count>] <key> [key...] [--until-node <node-name> <condition> [value]]
456
1185
  rokit query <ecp-path>
457
1186
  rokit sgnodes
458
1187
  rokit assert-node <node-name> <visible|hidden|absent|text|attr> [value]
@@ -464,6 +1193,9 @@ usage:
464
1193
  global options:
465
1194
  --json
466
1195
  --output json | text
1196
+ --dry-run
1197
+ --fields field[,field...]
1198
+ --input-json '<payload>'
467
1199
 
468
1200
  environment:
469
1201
  ROKIT_TARGET=<roku-ip>
@@ -476,6 +1208,6 @@ compatibility:
476
1208
  };
477
1209
  //#endregion
478
1210
  //#region src/rokit.ts
479
- await main();
1211
+ NodeRuntime.runMain(mainEffect());
480
1212
  //#endregion
481
1213
  export {};