@putdotio/rokit 1.6.0 → 2.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.
@@ -1,5 +1,6 @@
1
1
  import * as rokuDeploy from "roku-deploy";
2
- import { basename, dirname, extname, resolve } from "node:path";
2
+ import { createSocket } from "node:dgram";
3
+ import { basename, dirname, extname, isAbsolute, resolve } from "node:path";
3
4
  //#region src/xml.ts
4
5
  const readXmlTag = (xml, tag) => {
5
6
  return new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml)?.[1]?.trim();
@@ -70,6 +71,9 @@ const isNamedNodeVisible = (xml, nodeName) => {
70
71
  const attributes = readNamedNodeAttributes(xml, nodeName);
71
72
  return attributes !== void 0 && !attributes.includes("visible=\"false\"");
72
73
  };
74
+ const isCompleteSceneGraph = (xml) => !xml.includes("<All_Nodes>") || xml.includes("</sgnodes>") && (readSceneGraphStatus(xml).status !== void 0 || readSceneGraphFailure(xml) !== void 0);
75
+ const escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
76
+ const sceneGraphContainsText = (xml, text) => xml.includes(escapeXmlAttribute(text));
73
77
  const assertSceneGraphNumberNear = (actual, expected, label, tolerance = 1) => {
74
78
  if (actual === void 0 || Math.abs(actual - expected) > tolerance) throw new Error(`expected ${label} ${expected}, got ${actual ?? "missing"}`);
75
79
  };
@@ -158,6 +162,50 @@ const getDeviceInfo = async (context) => await rokuDeploy.getDeviceInfo({
158
162
  timeout: context.timeoutMs
159
163
  });
160
164
  const queryActiveApp = async (context) => readActiveApp(await fetchText(context, "/query/active-app"));
165
+ const discoverRokuDevices = async (timeoutMs = 3e3) => await new Promise((resolveDevices, reject) => {
166
+ const socket = createSocket("udp4");
167
+ const devices = /* @__PURE__ */ new Map();
168
+ const request = [
169
+ "M-SEARCH * HTTP/1.1",
170
+ "HOST: 239.255.255.250:1900",
171
+ "MAN: \"ssdp:discover\"",
172
+ "MX: 1",
173
+ "ST: roku:ecp",
174
+ "",
175
+ ""
176
+ ].join("\r\n");
177
+ const finish = () => {
178
+ socket.close();
179
+ resolveDevices([...devices.values()]);
180
+ };
181
+ const timer = setTimeout(finish, timeoutMs);
182
+ socket.on("message", (message) => {
183
+ const headers = readSsdpHeaders(message.toString("utf8"));
184
+ const location = headers.get("location");
185
+ if (!location) return;
186
+ devices.set(location, {
187
+ location,
188
+ server: headers.get("server"),
189
+ target: readLocationTarget(location),
190
+ usn: headers.get("usn")
191
+ });
192
+ });
193
+ socket.on("error", (error) => {
194
+ clearTimeout(timer);
195
+ socket.close();
196
+ reject(error);
197
+ });
198
+ socket.bind(() => {
199
+ socket.setBroadcast(true);
200
+ socket.send(Buffer.from(request), 1900, "239.255.255.250", (error) => {
201
+ if (error) {
202
+ clearTimeout(timer);
203
+ socket.close();
204
+ reject(error);
205
+ }
206
+ });
207
+ });
208
+ });
161
209
  const waitForActiveApp = async (context, appId, timeoutMs = 1e4) => {
162
210
  const start = Date.now();
163
211
  let lastApp;
@@ -188,9 +236,23 @@ const pressKey = async (context, key) => {
188
236
  validateRemoteKey(key);
189
237
  await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
190
238
  };
191
- const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
239
+ const queryEcp = async (context, path) => {
240
+ const safePath = validateEcpPath(path);
241
+ return await fetchText(context, safePath.startsWith("/") ? safePath : `/${safePath}`);
242
+ };
192
243
  const queryMediaPlayerXml = async (context) => await queryEcp(context, "/query/media-player");
193
244
  const queryMediaPlayer = async (context) => readMediaPlayerInfo(await queryMediaPlayerXml(context));
245
+ const queryMediaPlayerXmlSafe = async (context) => {
246
+ try {
247
+ return await queryMediaPlayerXml(context);
248
+ } catch {
249
+ return;
250
+ }
251
+ };
252
+ const queryMediaPlayerSafe = async (context) => {
253
+ const xml = await queryMediaPlayerXmlSafe(context);
254
+ return xml === void 0 ? void 0 : readMediaPlayerInfo(xml);
255
+ };
194
256
  const readMediaPlayerInfo = (xml) => {
195
257
  const playerAttributes = /<player(?:\s+([^>]*))?>/.exec(xml)?.[1] ?? "";
196
258
  const formatAttributes = /<format(?:\s+([^>]*))?\/>/.exec(xml)?.[1] ?? "";
@@ -216,7 +278,12 @@ const readMediaPlayerInfo = (xml) => {
216
278
  const readMediaPlayerState = (xml) => readMediaPlayerInfo(xml).state;
217
279
  const readMediaPlayerPositionMs = (xml) => readMediaPlayerInfo(xml).positionMs;
218
280
  const readMediaPlayerContainer = (xml) => readMediaPlayerInfo(xml).container;
219
- const isActiveMediaPlayerState = (state) => state === "buffer" || state === "pause" || state === "play";
281
+ const isActiveMediaPlayerState = (state) => state === "buffer" || state === "buffering" || state === "pause" || state === "play";
282
+ const assertMediaPlayerContainer = async (context, expectedContainer) => {
283
+ const mediaPlayer = await queryMediaPlayer(context);
284
+ if (mediaPlayer.container !== expectedContainer) throw new Error(`expected media-player container ${expectedContainer}, got ${mediaPlayer.container ?? "unknown"}`);
285
+ return mediaPlayer;
286
+ };
220
287
  const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4) => {
221
288
  const start = Date.now();
222
289
  let lastState;
@@ -237,14 +304,22 @@ const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4)
237
304
  };
238
305
  const querySceneGraph = async (context, options = {}) => {
239
306
  const attempts = options.attempts ?? 1;
307
+ const requireComplete = options.requireComplete ?? false;
240
308
  const retryDelayMs = options.retryDelayMs ?? 500;
309
+ let lastXml = "";
241
310
  let lastError;
242
- for (let attempt = 0; attempt < attempts; attempt += 1) try {
243
- return await queryEcp(context, "/query/sgnodes/all");
244
- } catch (error) {
245
- lastError = error;
311
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
312
+ try {
313
+ const xml = await queryEcp(context, "/query/sgnodes/all");
314
+ if (!requireComplete || isCompleteSceneGraph(xml)) return xml;
315
+ lastXml = xml;
316
+ } catch (error) {
317
+ lastError = error;
318
+ lastXml = "";
319
+ }
246
320
  if (attempt < attempts - 1) await sleep(retryDelayMs);
247
321
  }
322
+ if (lastXml !== "") throw new Error("SceneGraph query returned incomplete XML");
248
323
  throw new Error(`SceneGraph query failed: ${formatErrorMessage(lastError)}`);
249
324
  };
250
325
  const assertSceneGraphNode = async (context, nodeName, expectation) => {
@@ -305,6 +380,39 @@ const takeScreenshot = async (context, outputPath) => {
305
380
  password: context.password
306
381
  });
307
382
  };
383
+ const packageChannel = async (outputPath, rootDir = process.cwd()) => {
384
+ const options = packageOptions(outputPath, rootDir);
385
+ await rokuDeploy.createPackage(options);
386
+ return { path: rokuDeploy.getOutputZipFilePath(options) };
387
+ };
388
+ const resolvePackageOutputPath = (outputPath, rootDir = process.cwd()) => {
389
+ const options = packageOptions(outputPath, rootDir);
390
+ return rokuDeploy.getOutputZipFilePath(options);
391
+ };
392
+ const packageOptions = (outputPath, rootDir = process.cwd()) => {
393
+ const resolvedRoot = resolve(rootDir);
394
+ const resolvedOutput = isAbsolute(outputPath) ? resolve(outputPath) : resolve(resolvedRoot, outputPath);
395
+ return {
396
+ outDir: dirname(resolvedOutput),
397
+ outFile: basename(resolvedOutput),
398
+ rootDir: resolvedRoot
399
+ };
400
+ };
401
+ const validateEcpPath = (path) => {
402
+ rejectUnsafeInput(path, "ECP path");
403
+ if (path.includes("\\")) throw new Error("ECP path must not include backslashes");
404
+ if (path.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(path)) throw new Error("ECP path must be device-relative");
405
+ if (path.includes("?") || path.includes("#")) throw new Error("ECP path must not include query strings or fragments");
406
+ if (/(^|[/\\])\.\.($|[/\\])/.test(path)) throw new Error("ECP path must not include path traversal");
407
+ if (/%(?:2e|2f|5c)/i.test(path)) throw new Error("ECP path must not include percent-encoded path segments");
408
+ return path;
409
+ };
410
+ const rejectUnsafeInput = (value, label) => {
411
+ if ([...value].some((character) => {
412
+ const code = character.charCodeAt(0);
413
+ return code < 32 || code === 127;
414
+ })) throw new Error(`${label} contains control characters`);
415
+ };
308
416
  const validateRemoteKey = (key) => {
309
417
  if (key.startsWith("Lit_")) return;
310
418
  if (!remoteKeySet.has(key)) throw new Error(`unsupported remote key: ${key}`);
@@ -359,5 +467,21 @@ const sleep = (ms) => new Promise((resolve) => {
359
467
  setTimeout(resolve, ms);
360
468
  });
361
469
  const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
470
+ const readSsdpHeaders = (text) => {
471
+ const headers = /* @__PURE__ */ new Map();
472
+ for (const line of text.split(/\r?\n/)) {
473
+ const separator = line.indexOf(":");
474
+ if (separator <= 0) continue;
475
+ headers.set(line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim());
476
+ }
477
+ return headers;
478
+ };
479
+ const readLocationTarget = (location) => {
480
+ try {
481
+ return new URL(location).hostname;
482
+ } catch {
483
+ return;
484
+ }
485
+ };
362
486
  //#endregion
363
- export { parseSceneGraphNumberList as A, readXmlTag as B, assertNamedNode as C, assertNamedNodeTranslation as D, assertNamedNodeText as E, readNamedNodeTranslation as F, readSceneGraphFailure as I, readSceneGraphStatus as L, readNamedNodeAttributes as M, readNamedNodeBounds as N, assertSceneGraphNumberNear as O, readNamedNodeNumber as P, readActiveApp as R, waitForSceneGraphNode as S, assertNamedNodeState as T, takeScreenshot as _, isActiveMediaPlayerState as a, waitForMediaPlayerState as b, queryActiveApp as c, queryMediaPlayerXml as d, querySceneGraph as f, readMediaPlayerState as g, readMediaPlayerPositionMs as h, installPackage as i, readNamedNodeAttribute as j, isNamedNodeVisible as k, queryEcp as l, readMediaPlayerInfo as m, checkDevice as n, launchApp as o, readMediaPlayerContainer as p, getDeviceInfo as r, pressKey as s, assertSceneGraphNode as t, queryMediaPlayer as u, validateRemoteKey as v, assertNamedNodeSize as w, waitForSceneGraphAssertion as x, waitForActiveApp as y, readXmlAttribute as z };
487
+ export { assertNamedNode as A, readNamedNodeAttribute as B, takeScreenshot as C, waitForMediaPlayerState as D, waitForActiveApp as E, assertSceneGraphNumberNear as F, readSceneGraphFailure as G, readNamedNodeBounds as H, escapeXmlAttribute as I, readActiveApp as J, readSceneGraphStatus as K, isCompleteSceneGraph as L, assertNamedNodeState as M, assertNamedNodeText as N, waitForSceneGraphAssertion as O, assertNamedNodeTranslation as P, isNamedNodeVisible as R, resolvePackageOutputPath as S, validateRemoteKey as T, readNamedNodeNumber as U, readNamedNodeAttributes as V, readNamedNodeTranslation as W, readXmlTag as X, readXmlAttribute as Y, querySceneGraph as _, getDeviceInfo as a, readMediaPlayerPositionMs as b, launchApp as c, queryActiveApp as d, queryEcp as f, queryMediaPlayerXmlSafe as g, queryMediaPlayerXml as h, discoverRokuDevices as i, assertNamedNodeSize as j, waitForSceneGraphNode as k, packageChannel as l, queryMediaPlayerSafe as m, assertSceneGraphNode as n, installPackage as o, queryMediaPlayer as p, sceneGraphContainsText as q, checkDevice as r, isActiveMediaPlayerState as s, assertMediaPlayerContainer as t, pressKey as u, readMediaPlayerContainer as v, validateEcpPath as w, readMediaPlayerState as x, readMediaPlayerInfo as y, parseSceneGraphNumberList as z };
@@ -0,0 +1,60 @@
1
+ # Distribution
2
+
3
+ `rokit` is a public npm package published as `@putdotio/rokit`.
4
+
5
+ ## Local Contract
6
+
7
+ The release path starts with the repo-local verification command:
8
+
9
+ ```bash
10
+ pnpm verify
11
+ ```
12
+
13
+ `verify` runs formatting/lint checks, TypeScript, package bundling, tests, and an npm pack dry run. GitHub Actions calls this same command before release.
14
+
15
+ ## Continuous Release
16
+
17
+ Merges to `main` are considered publishable. The CI workflow runs:
18
+
19
+ 1. `verify` on pull requests and `main` pushes.
20
+ 2. semantic-release on `main` after `verify` passes.
21
+
22
+ semantic-release analyzes conventional commits, publishes to npm, creates GitHub Releases, and writes release metadata when needed.
23
+
24
+ ## Release Credentials
25
+
26
+ The release job uses the `release` GitHub Environment with `deployment: false`.
27
+
28
+ Required protected inputs:
29
+
30
+ - `PUTIO_RELEASE_BOT_CLIENT_ID` as a repository or Environment variable
31
+ - `PUTIO_RELEASE_BOT_PRIVATE_KEY` as an Environment secret
32
+
33
+ The npm package uses Trusted Publishing from GitHub Actions. On npm, configure owner `putdotio`, repository `rokit`, workflow `ci.yml`, and Environment named `release` for the package.
34
+
35
+ During the `@semantic-release/npm` publish step, npm detects the GitHub OIDC identity, mints short-lived publish credentials, and publishes provenance for the release job.
36
+
37
+ Release writes use the `putio-release-bot` installation token. The default `GITHUB_TOKEN` remains read-only, and the release-bot remote is configured only after dependencies are installed.
38
+
39
+ ## Package Contents
40
+
41
+ The npm package includes `dist`, `README.md`, `docs`, `examples`, `AGENTS.md`,
42
+ `CONTRIBUTING.md`, and `SECURITY.md`. The docs and generic live probe are
43
+ included so agents consuming the package can inspect readiness, distribution,
44
+ security, and generic Roku proof mechanics without cloning extra private
45
+ context.
46
+
47
+ ## Release Smoke
48
+
49
+ After a release, confirm the tag and package are visible:
50
+
51
+ ```bash
52
+ gh release list --repo putdotio/rokit --limit 5
53
+ npm view @putdotio/rokit version
54
+ ```
55
+
56
+ Live Roku behavior is not required for npm release. Real-device checks are local/manual because they require a developer-enabled Roku:
57
+
58
+ ```bash
59
+ ROKIT_TARGET=<roku-ip> pnpm live:smoke
60
+ ```
@@ -0,0 +1,80 @@
1
+ # Agent Readiness
2
+
3
+ Status: current as of 2026-05-17
4
+
5
+ `rokit` is a CLI/package repo. There is no long-running app to boot; readiness is based on deterministic package verification plus optional live Roku proof.
6
+
7
+ ## Grade
8
+
9
+ Overall: B+
10
+
11
+ | Dimension | Status | Evidence | Gap |
12
+ | ---------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
13
+ | bootable | pass | `pnpm smoke` builds the CLI and checks `--version` plus `--help` | no server boot surface, by design |
14
+ | testable | pass | `pnpm verify` runs TypeScript, bundle, unit tests, and npm pack dry run | live Roku checks require local hardware |
15
+ | observable | pass | CLI commands print active app, device info, raw ECP state, SceneGraph XML, snapshots, proof bundles, screenshots, compact assertion failures, and JSON output by default in non-TTY runs | no CI hardware lane |
16
+ | verifiable | pass | CI runs `pnpm verify`; `pnpm live:smoke` proves a configured Roku responds; `pnpm live:probe` packages, installs, launches, drives, asserts, screenshots, and proofs a generic channel | no CI hardware lane |
17
+
18
+ ## Layers
19
+
20
+ - Boot: `pnpm smoke`, `rokit describe`
21
+ - Smoke: `pnpm smoke`, `rokit check`
22
+ - Interact: `rokit press`, `rokit launch`, `rokit query`, `rokit sgnodes`
23
+ - E2e: `pnpm live:probe` for a configured developer-enabled Roku
24
+ - Enforce: GitHub Actions `verify`, optional `.git-hooks/pre-push`
25
+ - Observe: ECP responses, SceneGraph XML, `rokit snapshot`, `rokit proof`, screenshots, concise command output
26
+ - Isolate: `.rokit/` and `.env` stay local per worktree
27
+
28
+ ## Agent-Facing CLI Contract
29
+
30
+ - The CLI entrypoint runs through Effect's Node runtime, and expected CLI
31
+ failures are schema-backed errors rendered without stack traces.
32
+ - `rokit describe` exposes the machine-readable command surface, parameters,
33
+ JSON payload fields, and global options without a Roku target.
34
+ - Non-TTY command output defaults to JSON. Use `--output text` when a human
35
+ transcript is required.
36
+ - `--input-json` lets agents provide typed command payloads without translating
37
+ everything into flags.
38
+ - `--fields` trims JSON output for context-window control.
39
+ - `--dry-run` validates mutating commands without touching a device or writing
40
+ files.
41
+ - ECP paths and generated output paths are hardened against common agent
42
+ mistakes: query strings, fragments, traversal, backslashes, control
43
+ characters, encoded path segments, and writes outside the current app root.
44
+
45
+ ## Setup For Agents
46
+
47
+ ```bash
48
+ pnpm install
49
+ pnpm verify
50
+ ```
51
+
52
+ `pnpm install` runs the local Effect source setup outside CI only. When `CI` is
53
+ set to any non-empty value, `scripts/prepare-effect.sh` exits without cloning
54
+ `.repos/effect` so install, pack, publish, and CI/CD workspaces stay predictable.
55
+
56
+ Optional local hook:
57
+
58
+ ```bash
59
+ pnpm hooks:install
60
+ ```
61
+
62
+ Optional live smoke:
63
+
64
+ ```bash
65
+ ROKIT_TARGET=<roku-ip> pnpm live:smoke
66
+ ```
67
+
68
+ Full local live probe:
69
+
70
+ ```bash
71
+ cp .env.example .env
72
+ # Fill ROKIT_TARGET and ROKIT_PASSWORD.
73
+ pnpm live:probe
74
+ ```
75
+
76
+ `pnpm live:probe` reads `.env` and `.env.local`, accepts `ROKU_DEV_TARGET` and
77
+ `ROKU_DEV_PASSWORD` as fallbacks, copies `examples/live-probe-channel` into
78
+ `.rokit/live-probe/app`, packages it, installs it into the Roku developer slot,
79
+ launches `dev`, checks generic SceneGraph labels, sends `Info Back`, and writes
80
+ proof artifacts to `.rokit/live-probe/artifacts`.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: rokit-harness
3
+ description: Use rokit as a generic Roku harness adapter for packaging, installing, launching, key input, ECP/SceneGraph/media-player observation, readiness waits, screenshots, and proof bundles.
4
+ ---
5
+
6
+ # rokit Harness
7
+
8
+ Use this skill when a repo consumes `@putdotio/rokit` for Roku device harness
9
+ work. Keep `rokit` generic and put app journeys, product selectors, fixture
10
+ names, content IDs, account state, and product assertions in the consumer app
11
+ repo.
12
+
13
+ ## Workflow
14
+
15
+ 1. Run `rokit describe` to inspect the command surface instead of guessing
16
+ flags or `--input-json` payload fields.
17
+ 2. Use `--dry-run` before mutating commands such as `package`, `install`,
18
+ `launch`, `press`, `screenshot`, or `proof`.
19
+ 3. Prefer structured output. In automation, rely on the non-TTY JSON default or
20
+ pass `--json` explicitly.
21
+ 4. Use `--fields` to keep observations small when only a few values are needed.
22
+ 5. For live proof, use `rokit snapshot` for a quick state read,
23
+ `rokit proof <output-dir>` for review artifacts, or `pnpm live:probe` in the
24
+ rokit repo for the full generic package/install/launch/input/proof probe.
25
+ 6. Use `rokit wait-ready <app-id>` after launch when the app can race ECP or
26
+ SceneGraph readiness.
27
+ 7. Use `rokit press --until-node ...` for bounded navigation loops instead of
28
+ arbitrary sleeps.
29
+
30
+ ## Boundaries
31
+
32
+ - Device IPs, developer passwords, screenshots, packages, and proof bundles stay
33
+ local or ignored.
34
+ - `rokit` can assert generic Roku and SceneGraph state. Consumer repos own
35
+ product routes, screen names when product-specific, playback/content
36
+ expectations, and review narratives.
37
+ - ECP query paths must be plain paths. Do not include query strings, fragments,
38
+ traversal, backslashes, or encoded path segments.
39
+ - The local Effect source setup is skipped whenever `CI` is set. Do not make CI,
40
+ install, pack, or publish flows depend on cloning `.repos/effect`.
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <component name="MainScene" extends="Scene">
3
+ <children>
4
+ <Rectangle
5
+ id="background"
6
+ width="1280"
7
+ height="720"
8
+ color="0x101820FF" />
9
+ <Label
10
+ id="title"
11
+ text="Rokit Live Probe"
12
+ translation="[120, 140]"
13
+ width="1040"
14
+ height="80"
15
+ horizAlign="center"
16
+ vertAlign="center" />
17
+ <Label
18
+ id="status"
19
+ text="Install, launch, input, and proof OK"
20
+ translation="[120, 250]"
21
+ width="1040"
22
+ height="60"
23
+ horizAlign="center"
24
+ vertAlign="center" />
25
+ </children>
26
+ </component>
@@ -0,0 +1,5 @@
1
+ title=Rokit Live Probe
2
+ major_version=1
3
+ minor_version=0
4
+ build_version=1
5
+ ui_resolutions=hd
@@ -0,0 +1,14 @@
1
+ sub Main()
2
+ screen = CreateObject("roSGScreen")
3
+ port = CreateObject("roMessagePort")
4
+ screen.SetMessagePort(port)
5
+ screen.CreateScene("MainScene")
6
+ screen.Show()
7
+
8
+ while true
9
+ msg = Wait(0, port)
10
+ if Type(msg) = "roSGScreenEvent" and msg.IsScreenClosed() then
11
+ return
12
+ end if
13
+ end while
14
+ end sub
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putdotio/rokit",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "description": "A tiny CLI companion for Roku device harness work.",
5
5
  "keywords": [
6
6
  "cli",
@@ -22,8 +22,13 @@
22
22
  "rokit": "dist/rokit.mjs"
23
23
  },
24
24
  "files": [
25
+ "AGENTS.md",
26
+ "CONTRIBUTING.md",
25
27
  "dist",
26
- "README.md"
28
+ "docs",
29
+ "examples",
30
+ "README.md",
31
+ "SECURITY.md"
27
32
  ],
28
33
  "type": "module",
29
34
  "sideEffects": false,
@@ -43,17 +48,22 @@
43
48
  "clean": "rm -rf .turbo coverage dist",
44
49
  "hooks:install": "git config core.hooksPath .git-hooks",
45
50
  "live:smoke": "vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs check",
51
+ "live:probe": "scripts/live-probe.sh",
52
+ "prepare": "./scripts/prepare-effect.sh",
46
53
  "prepack": "vp pack src/index.ts src/rokit.ts --dts",
47
54
  "smoke": "vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs --version && node dist/rokit.mjs --help >/dev/null",
48
- "test": "vp test --passWithNoTests",
55
+ "test": "vp pack src/index.ts src/rokit.ts --dts && vitest run --passWithNoTests",
49
56
  "typecheck": "tsc --noEmit",
50
- "verify": "vp check . && tsc --noEmit && vp pack src/index.ts src/rokit.ts --dts && vp test --passWithNoTests && npm pack --dry-run"
57
+ "verify": "vp check . && tsc --noEmit && vp run test && npm pack --dry-run"
51
58
  },
52
59
  "dependencies": {
60
+ "@effect/platform-node": "4.0.0-beta.66",
61
+ "effect": "4.0.0-beta.66",
53
62
  "roku-deploy": "3.17.3"
54
63
  },
55
64
  "devDependencies": {
56
- "@types/node": "^25.7.0",
65
+ "@effect/vitest": "4.0.0-beta.66",
66
+ "@types/node": "^24.12.4",
57
67
  "typescript": "^6.0.2",
58
68
  "vite-plus": "^0.1.20",
59
69
  "vitest": "^4.1.6"