@putdotio/rokit 1.0.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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/rokit.mjs +341 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) put.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<p>
|
|
3
|
+
<img src="https://static.put.io/images/putio-boncuk.png" width="72">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<h1>rokit</h1>
|
|
7
|
+
|
|
8
|
+
<p>A tiny CLI companion for Roku device harness work.</p>
|
|
9
|
+
|
|
10
|
+
<p>
|
|
11
|
+
<a href="https://github.com/putdotio/rokit/actions/workflows/ci.yml?query=branch%3Amain" style="text-decoration:none;"><img src="https://img.shields.io/github/actions/workflow/status/putdotio/rokit/ci.yml?branch=main&style=flat&label=ci&colorA=000000&colorB=000000" alt="CI"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@putdotio/rokit" style="text-decoration:none;"><img src="https://img.shields.io/npm/v/%40putdotio%2Frokit?style=flat&label=npm&logo=npm&colorA=000000&colorB=000000" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/putdotio/rokit/blob/main/LICENSE" style="text-decoration:none;"><img src="https://img.shields.io/github/license/putdotio/rokit?style=flat&label=license&colorA=000000&colorB=000000" alt="License"></a>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add -D @putdotio/rokit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`rokit` wraps Roku platform primitives for live-device proof loops. It does not
|
|
24
|
+
own app-specific journeys, content IDs, credentials, or product assertions.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Create `.rokit/.env` or export environment variables in the app repo:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
ROKIT_TARGET=<roku-ip>
|
|
32
|
+
ROKIT_PASSWORD=<developer-mode-password>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm exec rokit check
|
|
39
|
+
pnpm exec rokit launch dev
|
|
40
|
+
pnpm exec rokit press Down Select
|
|
41
|
+
pnpm exec rokit query /query/active-app
|
|
42
|
+
pnpm exec rokit screenshot artifacts/live/player.png
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
rokit check
|
|
49
|
+
rokit device-info
|
|
50
|
+
rokit active-app
|
|
51
|
+
rokit launch <app-id> [--param key=value]
|
|
52
|
+
rokit press <key> [key...]
|
|
53
|
+
rokit query <ecp-path>
|
|
54
|
+
rokit screenshot <output-path>
|
|
55
|
+
rokit install <zip-path>
|
|
56
|
+
rokit --version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `check` confirms the Roku ECP endpoint responds and the developer installer
|
|
60
|
+
is reachable.
|
|
61
|
+
- `device-info` prints enhanced Roku device metadata as JSON.
|
|
62
|
+
- `active-app` prints the foreground app.
|
|
63
|
+
- `launch` opens an app and waits until it is active. Use repeated `--param`
|
|
64
|
+
values for deeplink parameters.
|
|
65
|
+
- `press` sends Roku remote keys through ECP.
|
|
66
|
+
- `query` prints a raw ECP response such as `/query/sgnodes/all`.
|
|
67
|
+
- `screenshot` saves a developer screenshot. It requires `ROKIT_PASSWORD`.
|
|
68
|
+
- `install` publishes an existing ZIP through `roku-deploy`. It requires
|
|
69
|
+
`ROKIT_PASSWORD`.
|
|
70
|
+
|
|
71
|
+
## Environment
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ROKIT_TARGET=<roku-ip>
|
|
75
|
+
ROKIT_PASSWORD=<developer-mode-password>
|
|
76
|
+
ROKIT_USERNAME=rokudev
|
|
77
|
+
ROKIT_TIMEOUT_MS=10000
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`ROKU_DEV_TARGET` and `ROKU_DEV_PASSWORD` are accepted as fallbacks for app
|
|
81
|
+
repos that already use Roku dev naming.
|
|
82
|
+
|
|
83
|
+
Keep `.rokit/` local. Device IPs, Developer Mode passwords, signing keys, user
|
|
84
|
+
tokens, and app-specific media identifiers do not belong in git.
|
|
85
|
+
|
|
86
|
+
## Boundaries
|
|
87
|
+
|
|
88
|
+
`rokit` is the generic Roku harness layer:
|
|
89
|
+
|
|
90
|
+
- device info
|
|
91
|
+
- install/publish
|
|
92
|
+
- launch and deeplink parameters
|
|
93
|
+
- remote keypresses
|
|
94
|
+
- raw ECP queries
|
|
95
|
+
- screenshots
|
|
96
|
+
|
|
97
|
+
App repositories should keep their own scenario commands for product behavior,
|
|
98
|
+
such as opening a specific route, asserting playback state, generating review
|
|
99
|
+
HTML, or checking app-specific UI nodes.
|
|
100
|
+
|
|
101
|
+
## Docs
|
|
102
|
+
|
|
103
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
104
|
+
- [Security](./SECURITY.md)
|
|
105
|
+
|
|
106
|
+
## Repo Internals
|
|
107
|
+
|
|
108
|
+
- [Agent guide](./AGENTS.md)
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
[MIT](./LICENSE)
|
package/dist/rokit.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import * as rokuDeploy from "roku-deploy";
|
|
6
|
+
//#region src/xml.ts
|
|
7
|
+
const readXmlTag = (xml, tag) => {
|
|
8
|
+
return new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml)?.[1]?.trim();
|
|
9
|
+
};
|
|
10
|
+
const readXmlAttribute = (attributes, name) => {
|
|
11
|
+
return new RegExp(`${name}="([^"]*)"`).exec(attributes)?.[1];
|
|
12
|
+
};
|
|
13
|
+
const readActiveApp = (xml) => {
|
|
14
|
+
const match = /<app\s+([^>]*)>([^<]*)<\/app>/.exec(xml);
|
|
15
|
+
if (!match) throw new Error("active app response did not include an app node");
|
|
16
|
+
const attributes = match[1] ?? "";
|
|
17
|
+
return {
|
|
18
|
+
id: readXmlAttribute(attributes, "id") ?? "",
|
|
19
|
+
name: match[2]?.trim() ?? "",
|
|
20
|
+
type: readXmlAttribute(attributes, "type") ?? "",
|
|
21
|
+
version: readXmlAttribute(attributes, "version") ?? ""
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/roku.ts
|
|
26
|
+
const ecpPort = 8060;
|
|
27
|
+
const remoteKeySet = new Set([
|
|
28
|
+
"Home",
|
|
29
|
+
"Rev",
|
|
30
|
+
"Fwd",
|
|
31
|
+
"Play",
|
|
32
|
+
"Select",
|
|
33
|
+
"Left",
|
|
34
|
+
"Right",
|
|
35
|
+
"Down",
|
|
36
|
+
"Up",
|
|
37
|
+
"Back",
|
|
38
|
+
"InstantReplay",
|
|
39
|
+
"Info",
|
|
40
|
+
"Backspace",
|
|
41
|
+
"Search",
|
|
42
|
+
"Enter",
|
|
43
|
+
"VolumeDown",
|
|
44
|
+
"VolumeMute",
|
|
45
|
+
"VolumeUp",
|
|
46
|
+
"PowerOff",
|
|
47
|
+
"ChannelUp",
|
|
48
|
+
"ChannelDown",
|
|
49
|
+
"InputTuner",
|
|
50
|
+
"InputHDMI1",
|
|
51
|
+
"InputHDMI2",
|
|
52
|
+
"InputHDMI3",
|
|
53
|
+
"InputHDMI4"
|
|
54
|
+
]);
|
|
55
|
+
const checkDevice = async (context) => {
|
|
56
|
+
const deviceInfo = await fetchText(context, "/query/device-info");
|
|
57
|
+
const installerStatus = await fetchInstallerStatus(context);
|
|
58
|
+
return {
|
|
59
|
+
ecp: `http://${context.target}:${ecpPort}`,
|
|
60
|
+
installerStatus,
|
|
61
|
+
model: readXmlTag(deviceInfo, "model-name") ?? "unknown model",
|
|
62
|
+
name: readXmlTag(deviceInfo, "friendly-device-name") ?? readXmlTag(deviceInfo, "friendlyName") ?? "unknown"
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const getDeviceInfo = async (context) => await rokuDeploy.getDeviceInfo({
|
|
66
|
+
enhance: true,
|
|
67
|
+
host: context.target,
|
|
68
|
+
remotePort: ecpPort,
|
|
69
|
+
timeout: context.timeoutMs
|
|
70
|
+
});
|
|
71
|
+
const queryActiveApp = async (context) => readActiveApp(await fetchText(context, "/query/active-app"));
|
|
72
|
+
const launchApp = async (context, appId, params = /* @__PURE__ */ new Map()) => {
|
|
73
|
+
const url = ecpUrl(context, `/launch/${encodeURIComponent(appId)}`);
|
|
74
|
+
for (const [key, value] of params) url.searchParams.set(key, value);
|
|
75
|
+
await postOk(context, url);
|
|
76
|
+
return await waitForActiveApp(context, appId);
|
|
77
|
+
};
|
|
78
|
+
const pressKey = async (context, key) => {
|
|
79
|
+
validateRemoteKey(key);
|
|
80
|
+
await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
|
|
81
|
+
};
|
|
82
|
+
const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
|
|
83
|
+
const installPackage = async (context, zipPath) => {
|
|
84
|
+
const resolvedZip = resolve(zipPath);
|
|
85
|
+
const extension = extname(resolvedZip);
|
|
86
|
+
return (await rokuDeploy.publish({
|
|
87
|
+
host: context.target,
|
|
88
|
+
outDir: dirname(resolvedZip),
|
|
89
|
+
outFile: basename(resolvedZip, extension),
|
|
90
|
+
password: context.password,
|
|
91
|
+
rootDir: process.cwd(),
|
|
92
|
+
username: context.username
|
|
93
|
+
})).message;
|
|
94
|
+
};
|
|
95
|
+
const takeScreenshot = async (context, outputPath) => {
|
|
96
|
+
const resolvedOutput = resolve(outputPath);
|
|
97
|
+
const extension = extname(resolvedOutput);
|
|
98
|
+
return await rokuDeploy.takeScreenshot({
|
|
99
|
+
host: context.target,
|
|
100
|
+
outDir: dirname(resolvedOutput),
|
|
101
|
+
outFile: basename(resolvedOutput, extension),
|
|
102
|
+
password: context.password
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const validateRemoteKey = (key) => {
|
|
106
|
+
if (key.startsWith("Lit_")) return;
|
|
107
|
+
if (!remoteKeySet.has(key)) throw new Error(`unsupported remote key: ${key}`);
|
|
108
|
+
};
|
|
109
|
+
const waitForActiveApp = async (context, appId, timeoutMs = 1e4) => {
|
|
110
|
+
const start = Date.now();
|
|
111
|
+
let lastApp;
|
|
112
|
+
while (Date.now() - start < timeoutMs) {
|
|
113
|
+
lastApp = await queryActiveApp(context);
|
|
114
|
+
if (lastApp.id === appId) return lastApp;
|
|
115
|
+
await sleep(500);
|
|
116
|
+
}
|
|
117
|
+
const last = lastApp ? `${lastApp.id} ${lastApp.name}` : "unknown";
|
|
118
|
+
throw new Error(`expected active app ${appId}, got ${last}`);
|
|
119
|
+
};
|
|
120
|
+
const fetchInstallerStatus = async (context) => {
|
|
121
|
+
return (await fetch(`http://${context.target}`, { signal: AbortSignal.timeout(context.timeoutMs) })).status;
|
|
122
|
+
};
|
|
123
|
+
const fetchText = async (context, path) => {
|
|
124
|
+
const response = await fetch(ecpUrl(context, path), { signal: AbortSignal.timeout(context.timeoutMs) });
|
|
125
|
+
if (!response.ok) throw new Error(`GET ${path} returned HTTP ${response.status}`);
|
|
126
|
+
return await response.text();
|
|
127
|
+
};
|
|
128
|
+
const postOk = async (context, url) => {
|
|
129
|
+
const response = await fetch(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
signal: AbortSignal.timeout(context.timeoutMs)
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) throw new Error(`POST ${url.pathname} returned HTTP ${response.status}`);
|
|
134
|
+
};
|
|
135
|
+
const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
|
|
136
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
137
|
+
setTimeout(resolve, ms);
|
|
138
|
+
});
|
|
139
|
+
const envPath = join(join(process.cwd(), ".rokit"), ".env");
|
|
140
|
+
const loadLocalEnv = () => {
|
|
141
|
+
if (existsSync(envPath)) process.loadEnvFile(envPath);
|
|
142
|
+
};
|
|
143
|
+
const loadEnv = () => ({
|
|
144
|
+
password: process.env.ROKIT_PASSWORD ?? process.env.ROKU_DEV_PASSWORD,
|
|
145
|
+
target: process.env.ROKIT_TARGET ?? process.env.ROKU_DEV_TARGET,
|
|
146
|
+
timeoutMs: parseTimeout(process.env.ROKIT_TIMEOUT_MS),
|
|
147
|
+
username: process.env.ROKIT_USERNAME ?? "rokudev"
|
|
148
|
+
});
|
|
149
|
+
const requireTarget = (env) => {
|
|
150
|
+
const target = env.target?.trim();
|
|
151
|
+
if (!target) return fail("ROKIT_TARGET is not set");
|
|
152
|
+
return normalizeTarget(target);
|
|
153
|
+
};
|
|
154
|
+
const requirePassword = (env) => {
|
|
155
|
+
const password = env.password;
|
|
156
|
+
if (!password) return fail("ROKIT_PASSWORD is not set");
|
|
157
|
+
return password;
|
|
158
|
+
};
|
|
159
|
+
const fail = (message) => {
|
|
160
|
+
console.error(message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
};
|
|
163
|
+
const formatErrorMessage = (error) => {
|
|
164
|
+
if (error instanceof Error) return error.message;
|
|
165
|
+
return String(error);
|
|
166
|
+
};
|
|
167
|
+
const normalizeTarget = (target) => target.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "");
|
|
168
|
+
const parseTimeout = (value) => {
|
|
169
|
+
if (value === void 0) return 1e4;
|
|
170
|
+
const timeout = Number(value);
|
|
171
|
+
if (!Number.isFinite(timeout) || timeout <= 0) fail(`Invalid ROKIT_TIMEOUT_MS: ${value}`);
|
|
172
|
+
return timeout;
|
|
173
|
+
};
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/cli.ts
|
|
176
|
+
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
177
|
+
const main = async (argv = process.argv.slice(2)) => {
|
|
178
|
+
const firstArg = argv[0];
|
|
179
|
+
if (!firstArg || firstArg === "--help" || firstArg === "-h") {
|
|
180
|
+
printHelp();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (firstArg === "--version" || firstArg === "-v") {
|
|
184
|
+
console.log(packageJson.version);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
loadLocalEnv();
|
|
188
|
+
const env = loadEnv();
|
|
189
|
+
const target = requireTarget(env);
|
|
190
|
+
const context = {
|
|
191
|
+
password: env.password,
|
|
192
|
+
target,
|
|
193
|
+
timeoutMs: env.timeoutMs,
|
|
194
|
+
username: env.username
|
|
195
|
+
};
|
|
196
|
+
const command = parseCommand(argv);
|
|
197
|
+
try {
|
|
198
|
+
await runCommand(context, command);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
fail(formatErrorMessage(error));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const runCommand = async (context, command) => {
|
|
204
|
+
if (command.name === "check") {
|
|
205
|
+
const summary = await checkDevice(context);
|
|
206
|
+
console.log(`device: ${summary.name} (${summary.model})`);
|
|
207
|
+
console.log(`ecp: ${summary.ecp}`);
|
|
208
|
+
console.log(`developer installer HTTP status: ${summary.installerStatus}`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (command.name === "device-info") {
|
|
212
|
+
console.log(JSON.stringify(await getDeviceInfo(context), null, 2));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (command.name === "active-app") {
|
|
216
|
+
const app = await queryActiveApp(context);
|
|
217
|
+
console.log(`active app: ${app.id} ${app.name} ${app.version}`.trim());
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (command.name === "launch") {
|
|
221
|
+
const app = await launchApp(context, command.args.appId, command.args.params);
|
|
222
|
+
console.log(`launched: ${app.id} ${app.name} ${app.version}`.trim());
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (command.name === "press") {
|
|
226
|
+
for (const key of command.keys) {
|
|
227
|
+
await pressKey(context, key);
|
|
228
|
+
console.log(`pressed: ${key}`);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (command.name === "query") {
|
|
233
|
+
console.log(await queryEcp(context, command.path));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (command.name === "screenshot") {
|
|
237
|
+
const password = requirePassword(context);
|
|
238
|
+
mkdirSync(dirname(command.outputPath), { recursive: true });
|
|
239
|
+
console.log(`screenshot: ${await takeScreenshot({
|
|
240
|
+
...context,
|
|
241
|
+
password
|
|
242
|
+
}, command.outputPath)}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (command.name === "install") {
|
|
246
|
+
const password = requirePassword(context);
|
|
247
|
+
console.log(await installPackage({
|
|
248
|
+
...context,
|
|
249
|
+
password
|
|
250
|
+
}, command.zipPath));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const parseCommand = (argv) => {
|
|
254
|
+
const [name, ...args] = argv;
|
|
255
|
+
if (name === "check") return { name };
|
|
256
|
+
if (name === "device-info") return { name };
|
|
257
|
+
if (name === "active-app") return { name };
|
|
258
|
+
if (name === "launch") return {
|
|
259
|
+
name,
|
|
260
|
+
args: parseLaunchArgs(args)
|
|
261
|
+
};
|
|
262
|
+
if (name === "press") {
|
|
263
|
+
if (args.length === 0) fail("usage: rokit press <key> [key...]");
|
|
264
|
+
return {
|
|
265
|
+
name,
|
|
266
|
+
keys: args
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (name === "query") {
|
|
270
|
+
const path = args[0];
|
|
271
|
+
if (!path) fail("usage: rokit query <ecp-path>");
|
|
272
|
+
return {
|
|
273
|
+
name,
|
|
274
|
+
path
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (name === "screenshot") {
|
|
278
|
+
const outputPath = args[0];
|
|
279
|
+
if (!outputPath) fail("usage: rokit screenshot <output-path>");
|
|
280
|
+
return {
|
|
281
|
+
name,
|
|
282
|
+
outputPath
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (name === "install") {
|
|
286
|
+
const zipPath = args[0];
|
|
287
|
+
if (!zipPath) fail("usage: rokit install <zip-path>");
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
zipPath
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return fail(`Unknown command: ${name ?? ""}`);
|
|
294
|
+
};
|
|
295
|
+
const parseLaunchArgs = (args) => {
|
|
296
|
+
const appId = args[0];
|
|
297
|
+
if (!appId) fail("usage: rokit launch <app-id> [--param key=value]");
|
|
298
|
+
const params = /* @__PURE__ */ new Map();
|
|
299
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
300
|
+
const arg = args[index];
|
|
301
|
+
if (arg !== "--param") fail(`Unknown launch option: ${arg ?? ""}`);
|
|
302
|
+
const pair = args[index + 1];
|
|
303
|
+
if (!pair) fail("usage: rokit launch <app-id> [--param key=value]");
|
|
304
|
+
const equalsIndex = pair.indexOf("=");
|
|
305
|
+
if (equalsIndex <= 0) fail(`Invalid launch param: ${pair}`);
|
|
306
|
+
params.set(pair.slice(0, equalsIndex), pair.slice(equalsIndex + 1));
|
|
307
|
+
index += 1;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
appId,
|
|
311
|
+
params
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
const printHelp = () => {
|
|
315
|
+
console.log(`rokit - Roku device harness helper
|
|
316
|
+
|
|
317
|
+
usage:
|
|
318
|
+
rokit check
|
|
319
|
+
rokit device-info
|
|
320
|
+
rokit active-app
|
|
321
|
+
rokit launch <app-id> [--param key=value]
|
|
322
|
+
rokit press <key> [key...]
|
|
323
|
+
rokit query <ecp-path>
|
|
324
|
+
rokit screenshot <output-path>
|
|
325
|
+
rokit install <zip-path>
|
|
326
|
+
rokit --version
|
|
327
|
+
|
|
328
|
+
environment:
|
|
329
|
+
ROKIT_TARGET=<roku-ip>
|
|
330
|
+
ROKIT_PASSWORD=<developer-mode-password>
|
|
331
|
+
ROKIT_USERNAME=rokudev
|
|
332
|
+
ROKIT_TIMEOUT_MS=10000
|
|
333
|
+
|
|
334
|
+
compatibility:
|
|
335
|
+
ROKU_DEV_TARGET and ROKU_DEV_PASSWORD are accepted as fallbacks.`);
|
|
336
|
+
};
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/rokit.ts
|
|
339
|
+
await main();
|
|
340
|
+
//#endregion
|
|
341
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@putdotio/rokit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny CLI companion for Roku device harness work.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"harness",
|
|
8
|
+
"roku",
|
|
9
|
+
"tv"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/putdotio/rokit#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/putdotio/rokit/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "put.io <devs@put.io>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/putdotio/rokit.git"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"rokit": "dist/rokit.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vp pack src/rokit.ts",
|
|
35
|
+
"check": "vp check .",
|
|
36
|
+
"clean": "rm -rf .turbo coverage dist",
|
|
37
|
+
"prepack": "vp pack src/rokit.ts",
|
|
38
|
+
"smoke": "vp pack src/rokit.ts && node dist/rokit.mjs --version && node dist/rokit.mjs --help >/dev/null",
|
|
39
|
+
"test": "vp test --passWithNoTests",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"verify": "vp check . && tsc --noEmit && vp pack src/rokit.ts && vp test --passWithNoTests && npm pack --dry-run"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"roku-deploy": "3.17.3"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^25.7.0",
|
|
48
|
+
"typescript": "^6.0.2",
|
|
49
|
+
"vite-plus": "^0.1.20",
|
|
50
|
+
"vitest": "^4.1.6"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=24.14.0 <25"
|
|
54
|
+
},
|
|
55
|
+
"packageManager": "pnpm@11.0.0"
|
|
56
|
+
}
|