@tantawowa/hosanna-tools 3.8.0 → 3.10.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.
Files changed (104) hide show
  1. package/README.md +108 -20
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +87 -142
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +3 -9
  6. package/dist/index.js +3 -12
  7. package/dist/index.js.map +1 -1
  8. package/dist/run/apple-tv.js +14 -12
  9. package/dist/run/apple-tv.js.map +1 -1
  10. package/dist/run/destination-picker.d.ts +24 -0
  11. package/dist/run/destination-picker.js +180 -0
  12. package/dist/run/destination-picker.js.map +1 -0
  13. package/dist/run/ios-device.js +7 -6
  14. package/dist/run/ios-device.js.map +1 -1
  15. package/dist/run/ios-simulator.js +7 -6
  16. package/dist/run/ios-simulator.js.map +1 -1
  17. package/dist/run/native-build.d.ts +3 -0
  18. package/dist/run/native-build.js +8 -0
  19. package/dist/run/native-build.js.map +1 -1
  20. package/dist/run/planner.js +10 -8
  21. package/dist/run/planner.js.map +1 -1
  22. package/dist/run/run-config.d.ts +9 -0
  23. package/dist/run/run-config.js +43 -3
  24. package/dist/run/run-config.js.map +1 -1
  25. package/dist/run/run-controller.js +10 -1
  26. package/dist/run/run-controller.js.map +1 -1
  27. package/dist/support-tools/android-metro-build.d.ts +3 -0
  28. package/dist/support-tools/android-metro-build.js +57 -2
  29. package/dist/support-tools/android-metro-build.js.map +1 -1
  30. package/dist/support-tools/apple-bundler.js +20 -8
  31. package/dist/support-tools/apple-bundler.js.map +1 -1
  32. package/dist/support-tools/build-config-resolver.d.ts +1 -0
  33. package/dist/support-tools/build-config-resolver.js +38 -9
  34. package/dist/support-tools/build-config-resolver.js.map +1 -1
  35. package/dist/support-tools/command-debugger.d.ts +1 -0
  36. package/dist/support-tools/command-debugger.js +6 -0
  37. package/dist/support-tools/command-debugger.js.map +1 -1
  38. package/dist/support-tools/dev-runner.d.ts +1 -0
  39. package/dist/support-tools/dev-runner.js +3 -3
  40. package/dist/support-tools/dev-runner.js.map +1 -1
  41. package/dist/support-tools/mcp-server/{cli-test-recorder.d.ts → rasp-capture.d.ts} +7 -7
  42. package/dist/support-tools/mcp-server/{cli-test-recorder.js → rasp-capture.js} +58 -58
  43. package/dist/support-tools/mcp-server/rasp-capture.js.map +1 -0
  44. package/dist/support-tools/mcp-server/rasp-exporter.d.ts +12 -12
  45. package/dist/support-tools/mcp-server/rasp-exporter.js +23 -23
  46. package/dist/support-tools/mcp-server/rasp-exporter.js.map +1 -1
  47. package/dist/support-tools/mcp-server/rasp-input-capture.d.ts +60 -0
  48. package/dist/support-tools/mcp-server/rasp-input-capture.js +100 -0
  49. package/dist/support-tools/mcp-server/rasp-input-capture.js.map +1 -0
  50. package/dist/support-tools/mcp-server/roku-device-client.d.ts +9 -0
  51. package/dist/support-tools/mcp-server/roku-device-client.js +17 -2
  52. package/dist/support-tools/mcp-server/roku-device-client.js.map +1 -1
  53. package/dist/support-tools/mcp-server/roku-log-client.d.ts +2 -0
  54. package/dist/support-tools/mcp-server/roku-log-client.js +16 -2
  55. package/dist/support-tools/mcp-server/roku-log-client.js.map +1 -1
  56. package/dist/support-tools/mcp-server/tools.js +30 -679
  57. package/dist/support-tools/mcp-server/tools.js.map +1 -1
  58. package/dist/support-tools/mcp-server/types.d.ts +4 -1
  59. package/dist/support-tools/mcp-server/types.js +3 -1
  60. package/dist/support-tools/mcp-server/types.js.map +1 -1
  61. package/dist/testing/client.d.ts +215 -0
  62. package/dist/testing/client.js +1029 -0
  63. package/dist/testing/client.js.map +1 -0
  64. package/dist/testing/config.d.ts +5 -0
  65. package/dist/testing/config.js +55 -0
  66. package/dist/testing/config.js.map +1 -0
  67. package/dist/testing/global-setup.d.ts +10 -0
  68. package/dist/testing/global-setup.js +39 -0
  69. package/dist/testing/global-setup.js.map +1 -0
  70. package/dist/testing/index.d.ts +7 -0
  71. package/dist/testing/index.js +26 -0
  72. package/dist/testing/index.js.map +1 -0
  73. package/dist/testing/launcher.d.ts +23 -0
  74. package/dist/testing/launcher.js +675 -0
  75. package/dist/testing/launcher.js.map +1 -0
  76. package/dist/testing/target.d.ts +16 -0
  77. package/dist/testing/target.js +40 -0
  78. package/dist/testing/target.js.map +1 -0
  79. package/dist/testing/types.d.ts +266 -0
  80. package/dist/testing/types.js +3 -0
  81. package/dist/testing/types.js.map +1 -0
  82. package/dist/testing/vitest-fixture.d.mts +28 -0
  83. package/dist/testing/vitest-fixture.mjs +87 -0
  84. package/dist/testing/vitest-fixture.mjs.map +1 -0
  85. package/dist/testing/vitest-reporter.d.mts +35 -0
  86. package/dist/testing/vitest-reporter.mjs +176 -0
  87. package/dist/testing/vitest-reporter.mjs.map +1 -0
  88. package/dist/testing/vitest.d.ts +1 -0
  89. package/dist/testing/vitest.js +8 -0
  90. package/dist/testing/vitest.js.map +1 -0
  91. package/dist/vite.d.ts +19 -0
  92. package/dist/vite.js +98 -5
  93. package/dist/vite.js.map +1 -1
  94. package/package.json +26 -1
  95. package/dist/support-tools/mcp-server/cli-test-recorder.js.map +0 -1
  96. package/dist/support-tools/mcp-server/cli-test-runner.d.ts +0 -37
  97. package/dist/support-tools/mcp-server/cli-test-runner.js +0 -332
  98. package/dist/support-tools/mcp-server/cli-test-runner.js.map +0 -1
  99. package/dist/support-tools/mcp-server/test-reporter.d.ts +0 -19
  100. package/dist/support-tools/mcp-server/test-reporter.js +0 -248
  101. package/dist/support-tools/mcp-server/test-reporter.js.map +0 -1
  102. package/dist/support-tools/mcp-server/test-runner.d.ts +0 -222
  103. package/dist/support-tools/mcp-server/test-runner.js +0 -686
  104. package/dist/support-tools/mcp-server/test-runner.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Documentation
4
4
 
5
- - **[MCP debugger & UI tests](src/support-tools/mcp-server/README.md)** — `hst mcp:start`, all MCP tools, commands by use case, examples, `hst test:ui` / `hst test:record`.
5
+ - **[MCP debugger](src/support-tools/mcp-server/README.md)** — `hst mcp:start`, debugger tools, commands by use case, examples, and RASP certification capture.
6
6
  - **[docs/README.md](docs/README.md)** — index of docs in this repo (MCP + source-map note).
7
7
  - **hosanna-ui** (separate repo): `docs/README.md` for agentic debugging and links back to this README.
8
8
 
@@ -94,7 +94,7 @@ Common build-script replacements:
94
94
  | `npx hst secrets:check` | `await secrets.check({ cwd: process.cwd() })` |
95
95
  | `npx hst env check` | `await env.check({ cwd: process.cwd() })` |
96
96
 
97
- Long-running APIs such as `dev.run`, `debugger.start`, `mcp.start`, `test.record`, and RASP capture-style workflows keep the same operational behavior as the corresponding CLI commands: they start services, attach to debuggers, or wait for user/session activity. `mcp.start`, `dev.run`, and `test.ui` are wired for programmatic no-exit behavior through the top-level API.
97
+ Long-running APIs such as `dev.run`, `debugger.start`, `mcp.start`, and RASP-owned certification capture workflows keep the same operational behavior as the corresponding CLI commands: they start services, attach to debuggers, or wait for user/session activity. `mcp.start` and `dev.run` are wired for programmatic no-exit behavior through the top-level API.
98
98
 
99
99
  # Getting started
100
100
 
@@ -189,7 +189,7 @@ Hosanna Tools is a comprehensive CLI toolset for the Hosanna framework that prov
189
189
  The CLI commands are organized into logical groups using colon-separated namespacing:
190
190
 
191
191
  ### Development Commands
192
- - `run` - Canonical cross-platform app launcher. Requires `--platform` and supports web, Roku, iOS, Apple TV, Android, and Android TV launch targets.
192
+ - `run` - Canonical cross-platform app launcher. Bare interactive runs show one destination picker; explicit platform options support web, Roku, iOS, Apple TV, Android, and Android TV launch targets.
193
193
  - `target:list` - List launch targets for humans or agents, including web emulators, sims, and physical devices.
194
194
  - `dev:start` - Run dev processes: vite, generator watch, and optional debugger
195
195
  - `debugger:start` - Start the command debugger WebSocket proxy (only one debugger is needed for multiple apps; if the port is already in use, a friendly message is shown instead of crashing)
@@ -197,6 +197,8 @@ The CLI commands are organized into logical groups using colon-separated namespa
197
197
  Examples:
198
198
 
199
199
  ```bash
200
+ hst run
201
+ hst run --last
200
202
  hst run --platform web
201
203
  hst run --platform roku --target emulator
202
204
  hst run --platform ios --target emulator
@@ -218,7 +220,9 @@ hst target:list --platform apple-tv --target sim --json
218
220
  hst target:list --form-factor tv --json
219
221
  ```
220
222
 
221
- `hst run` is platform-explicit by design, so non-interactive agents do not guess between iOS, Apple TV, Android, Android TV, Roku, or web. `--target emulator` starts or reuses the shared Vite/debugger dev services for any platform and opens a profiled DOM runtime URL for interactive runs; use `--device <preset-id>` to override defaults such as `iphone-15`, `pixel-8`, `apple-tv-4k`, `android-tv-1080p`, or `rokufhd`, and use `--no-open` for server-only launches. `--target sim` launches the vendor virtual device for platforms that have one: Apple Simulator for iOS and Apple TV, Android Emulator for Android and Android TV. `--target device` launches or deploys to physical local hardware. `web` and `browser` remain accepted aliases for the web emulator; `simulator` and `simulated` remain accepted aliases for `sim`; `emulated` remains an accepted alias for the web emulator. Device/sim runs record the target and refuse to overwrite a recorded session or safe-to-kill occupied resource in non-interactive mode unless `--replace` is passed. Preferred run defaults live in `.hosanna-tools/run.json`, and preferred devices live in `.hosanna-tools/devices.json` or `~/.hosanna-tools/devices.json`. Legacy `.hs-devices.json` files are still read for compatibility.
223
+ Bare `hst run` in an interactive terminal discovers runnable web previews, vendor simulators, and online physical devices, then presents one flat destination picker. The last selection is placed first, so pressing Enter repeats it; `hst run --last` skips discovery and immediately uses that remembered destination. The selection is stored in the user-level `~/.hosanna-tools/run.json`, preserving existing defaults. Explicit commands remain deterministic, and non-interactive runs never open the picker.
224
+
225
+ `--target emulator` starts or reuses the shared Vite/debugger dev services for any platform and opens a profiled DOM runtime URL for interactive runs; use `--device <preset-id>` to override defaults such as `iphone-15`, `pixel-8`, `apple-tv-4k`, `android-tv-1080p`, or `rokufhd`, and use `--no-open` for server-only launches. `--target sim` launches the vendor virtual device for platforms that have one: Apple Simulator for iOS and Apple TV, Android Emulator for Android and Android TV. `--target device` launches or deploys to physical local hardware. `web` and `browser` remain accepted aliases for the web emulator; `simulator` and `simulated` remain accepted aliases for `sim`; `emulated` remains an accepted alias for the web emulator. Device/sim runs record the target and refuse to overwrite a recorded session or safe-to-kill occupied resource in non-interactive mode unless `--replace` is passed. Preferred run defaults live in `.hosanna-tools/run.json`, and preferred devices live in `.hosanna-tools/devices.json` or `~/.hosanna-tools/devices.json`. Legacy `.hs-devices.json` files are still read for compatibility.
222
226
 
223
227
  ### Roku Commands (`roku:*`) - Roku Deployment & Packaging
224
228
  - `roku:run` - Deploy a Roku app to a device (supports .zip file or folder)
@@ -269,6 +273,8 @@ Supported inputs include `file/line: pkg:/components/source_0.brs(7475)`, `at ..
269
273
  ### Build Config Commands (`build-config`) - Runtime Configuration
270
274
  - `build-config:resolve` - Resolve `build-config/base.json`, env/platform overlays, secrets, and optional developer profiles into the canonical runtime `build-config.json`
271
275
 
276
+ Production JavaScript bundles strip `console.*` calls by default. Configure this through the normal overlay chain with `build.stripLogs`: set it to `false` in `build-config/prod.json` to retain production logs, or in `build-config/prod.apple.json` / `build-config/prod.web.json` for a platform-only exception. Non-production environments keep logs unless their overlay explicitly enables stripping. Apple `prod` builds use Xcode `Release`; other environments continue to use `Debug`.
277
+
272
278
  Native builds can resolve config immediately before the platform build:
273
279
 
274
280
  ```bash
@@ -380,9 +386,95 @@ silently break an install.
380
386
 
381
387
  **Cursor and MCP:** Cursor does not magically attach to a terminal `hst mcp:start`. Add Hosanna as a Command MCP server (project **`.cursor/mcp.json`** or **Cursor Settings → MCP → Add Custom MCP**) so Cursor spawns **`hosanna-mcp`** over stdio. Start the command debugger/app explicitly with `hst run --platform web --target web`, `hst run --platform ios --target web`, `hst run --platform android-tv --target web`, or `hst run --platform roku --target device`.
382
388
 
383
- ### UI Test Commands (`test:*`) - UI Test Automation
384
- - `test:ui` - Replay UI test recordings against a running Hosanna app
385
- - `test:record` - Interactively record a UI test by navigating the app
389
+ ### Deterministic UI testing
390
+
391
+ Applications own regular Vitest suites. Global setup comes from `@tantawowa/hosanna-tools/testing/vitest`; worker fixtures and matchers come from `@tantawowa/hosanna-tools/testing/vitest/fixture`; shared lifecycle, types, and helpers come from `@tantawowa/hosanna-tools/testing`. There is no HST test CLI or recording/replay format.
392
+
393
+ Add `createHosannaReporter(config)` from `@tantawowa/hosanna-tools/testing/vitest/reporter` to the application's Vitest reporters. Every run then writes a self-contained JSON and HTML report under `test-results/<testRunId>/`, updates `test-results/latest.json` and `test-results/latest.html`, and includes every Vitest result plus any named visual proofs captured by the tests.
394
+
395
+ Browser targets can also record the complete owned browser session as WebM:
396
+
397
+ ```ts
398
+ defineHosannaTestConfig({
399
+ // ...
400
+ video: true,
401
+ // Or: video: { size: { width: 1280, height: 720 } },
402
+ });
403
+ ```
404
+
405
+ The finalized recording is written to
406
+ `test-results/<testRunId>/<platform>-web/run.webm` and embedded in the HTML
407
+ report. Recording is opt-in in the shared tools and ignored for device and
408
+ simulator targets. It records browser video only, without audio. This is a
409
+ review artifact, not a replay format or executable test source.
410
+
411
+ ```ts
412
+ import { defineConfig } from 'vitest/config';
413
+ import { createHosannaReporter } from '@tantawowa/hosanna-tools/testing/vitest/reporter';
414
+ import hosannaTestConfig from './integration/hosanna-test.config';
415
+
416
+ export default defineConfig({
417
+ reporters: ['default', createHosannaReporter(hosannaTestConfig)],
418
+ });
419
+ ```
420
+
421
+ Use `hs.proof.capture(...)` at meaningful acceptance moments. A proof contains the target screenshot and the inspected screen/focus/control state used to understand it; hierarchy and scoped logs are optional. Proofs are evidence in the report, not an assertion by themselves:
422
+
423
+ ```ts
424
+ const poster = hs.image.inCollectionCell({
425
+ collectionId: 'collectionView',
426
+ rowSettingsKey: 'rows.regular',
427
+ cellIndex: 0,
428
+ imageId: 'poster',
429
+ });
430
+ await poster.waitForLoaded({ requireBitmapSize: true });
431
+ await expect(poster).toBeLoadedImage({ requireBitmapSize: true });
432
+
433
+ await hs.proof.capture('programme-focused', {
434
+ controls: {
435
+ programmeCard: hs.control.byId('programmeCard'),
436
+ },
437
+ images: { poster },
438
+ includeLogs: true,
439
+ });
440
+ ```
441
+
442
+ Image readiness requires a non-empty URI, `loadStatus: "ready"`, positive rendered dimensions, and positive decoded bitmap dimensions by default. Explicit `loadWidth`/`loadHeight` assertions remain available for controls that request a fixed decode size; zero is valid when the runtime decodes at the source size. `hs.image.byId(...)`, `hs.image.within(...)`, and semantic `hs.image.inCollectionCell(...)` locators work across browser, Roku, and later native drivers.
443
+
444
+ Screenshots and proofs wait for the command-plane hierarchy and focus to remain stable for 300ms by default, then allow a short platform render-settle window. Consecutive screenshots are also spaced automatically. Override this only for known platform behaviour through the test config's `screenshot` settings or per-capture options; failure diagnostics bypass the stable-frame wait so they can still capture broken states.
445
+
446
+ Custom test ports must be unique. Keep the Vite and browser CDP ports outside the debugger's dynamic app allocation range; the launcher validates this before starting any processes and reports the conflicting range.
447
+
448
+ Keep structural and state assertions as the default correctness checks. Use `hs.screenshot.match(...)` only for an explicit visual regression whose target-specific baseline and tolerance are intentionally maintained. Failure diagnostics and proof artifacts are separated below `test-results/<testRunId>/<platform>-<target>/`.
449
+
450
+ `buildConfig` supplies a non-secret overlay that is merged last for the owned test launch. It may be a target-aware factory, so browser emulation can enable deterministic mocks while a physical Roku keeps its real platform services. The launcher and resolver both reject test overlays for `prod`:
451
+
452
+ ```ts
453
+ defineHosannaTestConfig({
454
+ // ...
455
+ environment: 'test',
456
+ buildConfig: ({ platform, target }) => ({
457
+ remoteDebug: { isEnabled: true },
458
+ ...(target === 'web'
459
+ ? { channelStore: { testMode: { enabled: true } } }
460
+ : {}),
461
+ }),
462
+ });
463
+ ```
464
+
465
+ Use `createHosannaTestTarget(config)` with Vitest's standard conditional APIs. Selectors accept one platform or a set, and distinguish browser, simulator, and physical-device targets:
466
+
467
+ ```ts
468
+ const target = createHosannaTestTarget(config);
469
+
470
+ test.runIf(target.matches({ platform: 'roku', target: 'device' }))('Roku only', async ({ hs }) => {});
471
+ test.runIf(target.matches({ platform: ['ios', 'android'], target: 'sim' }))('mobile simulators', async ({ hs }) => {});
472
+ test.runIf(target.matches({ target: 'web' }))('all browser flavours', async ({ hs }) => {});
473
+ ```
474
+
475
+ Browser Channel Store emulation supports per-test `success`, `cancelled`, and `failed` outcomes when `channelStore.testMode.enabled` is present in the test overlay. Set it from a named precondition with `hs.mocks.channelStore.setPurchaseOutcome(...)`. This API refuses physical-device targets. Roku purchase tests use Roku Pay [billing testing](https://developer.roku.com/dev/docs/billing-testing), a linked [Roku test user](https://developer.roku.com/dev/docs/test-users), and real Channel Store/OS purchase screens; keep them opt-in and serial.
476
+
477
+ For Roku device suites, the launcher builds and deploys before the test launch, then relaunches through ECP with the exact test run ID and a debugger address reachable from the device. Configure the device IP and developer password in the application test config/environment. `HS_TEST_DEBUG_HOST` overrides automatic LAN address selection when needed. Physical Roku input, held keys, deep links, screenshots, and logs use their device transports; the committed Vitest source remains the same API used by browser tests.
386
478
 
387
479
  ## Usage Examples
388
480
 
@@ -439,16 +531,15 @@ hst env prepare-gitignore # Update .gitignore
439
531
  hst debugger:start # Start debug proxy (required)
440
532
  hst mcp:start # Start MCP server for Cursor/Claude
441
533
 
442
- # UI test automation
443
- hst test:record --name "My test" # Record UI test interactively
444
- hst test:ui # Replay all recordings
445
- hst test:ui --suite regression.json # Run test suite
446
- hst test:ui --json --report-dir ./reports # CI mode with reports
534
+ # Application-owned UI tests
535
+ npm run test:ui
536
+ npm run test:ui -- auth/login.test.ts
537
+ HS_TEST_PLATFORM=roku HS_TEST_TARGET=device npm run test:ui
447
538
  ```
448
539
 
449
- ## MCP Debugging & UI Test Automation
540
+ ## MCP Debugging & Deterministic UI Tests
450
541
 
451
- Hosanna Tools includes a powerful MCP (Model Context Protocol) server for AI-assisted debugging and a UI test automation framework.
542
+ Hosanna Tools includes an MCP server for interactive AI-assisted debugging. Deterministic UI tests run in an application's Vitest process and use the separate `@tantawowa/hosanna-tools/testing` export; MCP is only an authoring aid.
452
543
 
453
544
  ### Cursor Settings → MCP (Hosanna debugger)
454
545
 
@@ -501,7 +592,7 @@ After saving, enable the server in the MCP list. **`hst mcp:start`** is still us
501
592
  ### Quick Start
502
593
 
503
594
  ```bash
504
- # 1. Start the debug proxy (required for both MCP and test automation)
595
+ # 1. Start the debug proxy for an interactive MCP debugging session
505
596
  hst debugger:start
506
597
 
507
598
  # 2. Start your Hosanna app with remote debugging enabled
@@ -509,11 +600,8 @@ hst debugger:start
509
600
  # 3. For AI debugging (Cursor, Claude Code):
510
601
  # Add MCP server (see "Cursor Settings → MCP" above) or hst mcp:start in a terminal
511
602
 
512
- # 4. For UI test recording:
513
- hst test:record --name "My test flow"
514
-
515
- # 5. For UI test replay:
516
- hst test:ui
603
+ # 4. Run committed deterministic tests from the application repository:
604
+ npm run test:ui
517
605
  ```
518
606
 
519
607
  ### See also
@@ -1,6 +1,6 @@
1
1
  {
2
- "buildDate": "2026-07-15T14:04:49+00:00",
3
- "buildDateISO": "2026-07-15T14:04:49.432Z",
2
+ "buildDate": "2026-07-17T08:17:53+00:00",
3
+ "buildDateISO": "2026-07-17T08:17:53.381Z",
4
4
  "timeZone": "UTC",
5
- "gitHash": "ab10627"
5
+ "gitHash": "10849ea"
6
6
  }
package/dist/cli.js CHANGED
@@ -144,10 +144,33 @@ async function runRunPlanCommand(command, args) {
144
144
  const executor = await Promise.resolve().then(() => __importStar(require('./run/executor.js')));
145
145
  const positionalArgv = rawCommandPositionals(command);
146
146
  const format = (args.json || args.format === 'json') ? 'json' : 'text';
147
+ let flags = runPlanFlagsFromCliArgs(args, format);
148
+ let chosenDestination;
149
+ const hasExplicitDestination = positionalArgv.length > 0 ||
150
+ rawHasFlag('platform') || rawHasFlag('target') || rawHasFlag('device');
151
+ if (command === 'run' && args.last) {
152
+ if (hasExplicitDestination) {
153
+ throw new Error('--last cannot be combined with an explicit platform, target, or device.');
154
+ }
155
+ const picker = await Promise.resolve().then(() => __importStar(require('./run/destination-picker.js')));
156
+ chosenDestination = picker.requireLastRunDestination(process.cwd());
157
+ flags = Object.assign(Object.assign({}, flags), runDestinationFlags(chosenDestination));
158
+ }
159
+ else if (command === 'run' &&
160
+ !hasExplicitDestination &&
161
+ !flags.nonInteractive &&
162
+ !args['dry-run'] &&
163
+ format === 'text' &&
164
+ process.stdin.isTTY &&
165
+ process.stdout.isTTY) {
166
+ const picker = await Promise.resolve().then(() => __importStar(require('./run/destination-picker.js')));
167
+ chosenDestination = await picker.chooseRunDestination({ cwd: process.cwd(), env: process.env });
168
+ flags = Object.assign(Object.assign({}, flags), runDestinationFlags(chosenDestination));
169
+ }
147
170
  const planInput = {
148
171
  cwd: process.cwd(),
149
172
  argv: positionalArgv,
150
- flags: runPlanFlagsFromCliArgs(args, format),
173
+ flags,
151
174
  env: process.env,
152
175
  };
153
176
  const plan = command === 'build'
@@ -162,6 +185,18 @@ async function runRunPlanCommand(command, args) {
162
185
  }
163
186
  process.exit(0);
164
187
  }
188
+ if (command === 'run' && !flags.nonInteractive && process.stdin.isTTY) {
189
+ const destination = chosenDestination !== null && chosenDestination !== void 0 ? chosenDestination : rememberedExplicitDestination(plan, hasExplicitDestination);
190
+ if (destination) {
191
+ try {
192
+ const config = await Promise.resolve().then(() => __importStar(require('./run/run-config.js')));
193
+ config.saveLastRunDestination({ cwd: process.cwd(), destination });
194
+ }
195
+ catch (err) {
196
+ console.warn(`Unable to remember the run destination: ${err instanceof Error ? err.message : String(err)}`);
197
+ }
198
+ }
199
+ }
165
200
  const result = await executor.executeRunPlan(plan);
166
201
  if (format === 'json') {
167
202
  console.info(JSON.stringify(result, null, 2));
@@ -179,6 +214,21 @@ async function runRunPlanCommand(command, args) {
179
214
  process.exit(1);
180
215
  }
181
216
  }
217
+ function runDestinationFlags(destination) {
218
+ return {
219
+ platform: destination.platform,
220
+ target: destination.target,
221
+ device: destination.device,
222
+ };
223
+ }
224
+ function rememberedExplicitDestination(plan, hasExplicitDestination) {
225
+ if (!hasExplicitDestination)
226
+ return undefined;
227
+ const target = plan.resolved.target === 'emulator' ? 'web' : plan.resolved.target;
228
+ if (target !== 'web' && !plan.resolved.deviceSelector)
229
+ return undefined;
230
+ return Object.assign({ platform: plan.resolved.platform, target }, (plan.resolved.deviceSelector ? { device: plan.resolved.deviceSelector } : {}));
231
+ }
182
232
  const NATIVE_PLATFORM_CHOICES = ['apple', 'ios', 'apple-tv', 'android', 'android-tv', 'all'];
183
233
  function nativeInputFromArgs(args) {
184
234
  return {
@@ -250,7 +300,7 @@ catch (err) {
250
300
  // Let yargs handle all commands including env
251
301
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
252
302
  .version(`${buildVersion}\n git: ${buildGitHash}\n built: ${buildDate}`)
253
- .usage('$0 [command] [options]', `Hosanna Tools CLI\nVersion: ${buildVersion}\nBuild Date: ${buildDate}\n\nCommand Groups:\n compiler:* Compiler lifecycle (install, status, list)\n config hosanna.json management (wizard, show, set)\n env Environment checks and repair (check, fix, prepare-gitignore)\n dev:* Local development execution\n debugger:* Command debugger control\n mcp:* Hosanna MCP server control\n build Cross-platform build orchestration\n run Cross-platform launch orchestration\n native:* Native platform bootstrap and validation\n target:* Run target discovery and inventory\n roku:* Roku deployment, commands & packaging (run, command, package, map-stack)\n device:* Device discovery and run workflows\n generate:* Code and asset generation\n sdk:* Hosanna SDK/framework installation\n build-config:* Build/runtime configuration resolution\n ci:* Continuous integration helpers\n framework:* Framework distribution helpers\n license:* Hosanna framework licensing\n secrets:* Portable .secrets files\n test:* UI testing\n rasp:* RASP cert scripts`)
303
+ .usage('$0 [command] [options]', `Hosanna Tools CLI\nVersion: ${buildVersion}\nBuild Date: ${buildDate}\n\nCommand Groups:\n compiler:* Compiler lifecycle (install, status, list)\n config hosanna.json management (wizard, show, set)\n env Environment checks and repair (check, fix, prepare-gitignore)\n dev:* Local development execution\n debugger:* Command debugger control\n mcp:* Hosanna MCP server control\n build Cross-platform build orchestration\n run Cross-platform launch orchestration\n native:* Native platform bootstrap and validation\n target:* Run target discovery and inventory\n roku:* Roku deployment, commands & packaging (run, command, package, map-stack)\n device:* Device discovery and run workflows\n generate:* Code and asset generation\n sdk:* Hosanna SDK/framework installation\n build-config:* Build/runtime configuration resolution\n ci:* Continuous integration helpers\n framework:* Framework distribution helpers\n license:* Hosanna framework licensing\n secrets:* Portable .secrets files\n rasp:* RASP cert scripts`)
254
304
  .option('verbose', {
255
305
  type: 'boolean',
256
306
  default: false,
@@ -1662,7 +1712,7 @@ catch (err) {
1662
1712
  .parserConfiguration({ 'boolean-negation': false, 'populate--': true })
1663
1713
  .positional('platform', {
1664
1714
  type: 'string',
1665
- describe: 'Launch platform. Defaults from flags, env, .hosanna-tools/run.json, then web.',
1715
+ describe: 'Launch platform. Bare interactive runs show a destination picker; non-interactive runs default to web.',
1666
1716
  })
1667
1717
  .positional('env', {
1668
1718
  type: 'string',
@@ -1714,6 +1764,7 @@ catch (err) {
1714
1764
  .option('open', { type: 'boolean', describe: 'Open the web runtime URL in the default browser; defaults to interactive runs only' })
1715
1765
  .option('no-open', { type: 'boolean', default: false, describe: 'Do not open the default browser for web runtime launches' })
1716
1766
  .option('non-interactive', { type: 'boolean', default: false, describe: 'Fail instead of prompting when a decision is needed' })
1767
+ .option('last', { type: 'boolean', default: false, describe: 'Immediately reuse the last destination selected by an interactive run' })
1717
1768
  .option('json', { type: 'boolean', default: false, describe: 'Print run result as JSON when the command returns' })
1718
1769
  .option('dry-run', { type: 'boolean', default: false, describe: 'Print the resolved RunPlan without executing it' })
1719
1770
  .option('format', { type: 'string', choices: ['text', 'json'], default: 'text', describe: 'Output format for --dry-run and run result' })
@@ -1734,6 +1785,8 @@ catch (err) {
1734
1785
  .option('logs', { type: 'boolean', default: false, describe: 'Enable verbose deploy logging where supported' })
1735
1786
  .option('applySourceMapToPaths', { type: 'boolean', default: true, describe: 'Resolve Roku stack trace paths with source maps during log streaming' })
1736
1787
  .example('$0 run --platform web', 'Start/reuse web dev services for this app')
1788
+ .example('$0 run', 'Choose once from all available web, simulator, and physical-device destinations')
1789
+ .example('$0 run --last', 'Immediately run on the last selected destination')
1737
1790
  .example('$0 run --platform roku --target emulator', 'Start/reuse the Roku TV web emulator')
1738
1791
  .example('$0 run --platform ios --target emulator', 'Start/reuse the iPhone-shaped web emulator profile')
1739
1792
  .example('$0 run --platform android --target emulator --device pixel-8', 'Start/reuse the Android-shaped web emulator profile')
@@ -2003,121 +2056,13 @@ catch (err) {
2003
2056
  appName: args.appName,
2004
2057
  });
2005
2058
  })
2006
- .command('test:ui [recordings..]', 'Replay UI test recordings against a running Hosanna app and exit with pass/fail. ' +
2007
- 'Requires a running debug proxy (hst debugger:start) and a connected app.', yargs => yargs
2008
- .positional('recordings', {
2009
- describe: 'Recording IDs to replay (omit to run all)',
2010
- type: 'string',
2011
- array: true,
2012
- })
2013
- .option('suite', {
2014
- type: 'string',
2015
- describe: 'Path to a test-suite.json file (relative to recordings dir or absolute)',
2016
- })
2017
- .option('appId', {
2018
- type: 'string',
2019
- describe: 'Target app ID (defaults to the one stored in each recording)',
2020
- })
2021
- .option('managementPort', { type: 'number', default: 59150, describe: 'Debug proxy management port' })
2022
- .option('managementHost', { type: 'string', default: 'localhost', describe: 'Debug proxy host' })
2023
- .option('extensionPort', { type: 'number', default: 59153, describe: 'Debug proxy extension WebSocket port' })
2024
- .option('stop-on-failure', { type: 'boolean', default: true, describe: 'Stop each recording on first step failure' })
2025
- .option('step-delay-ms', { type: 'number', default: 500, describe: 'Delay after each action (ms)' })
2026
- .option('focus-assert-retry-ms', {
2027
- type: 'number',
2028
- default: 300,
2029
- describe: 'Delay (ms) between focus polls after a mismatch (0 = no polling)',
2030
- })
2031
- .option('focus-assert-max-retries', {
2032
- type: 'number',
2033
- default: 12,
2034
- describe: 'Max extra GetFocusedView polls per step (skeleton/load); 0 = single read only',
2035
- })
2036
- .option('screenshot-settle-ms', { type: 'number', default: 400, describe: 'Extra delay before screenshot capture (ms)' })
2037
- .option('compare-screenshots', { type: 'boolean', default: true, describe: 'Enable visual regression comparison' })
2038
- .option('screenshot-threshold', { type: 'number', default: 0.15, describe: 'Pixel diff tolerance (0-1)' })
2039
- .option('screenshot-fail-percent', { type: 'number', default: 2.0, describe: 'Max allowed diff % before failure' })
2040
- .option('roku-ip', { type: 'string', describe: 'Roku device IP (env: HOSANNA_ROKU_DEVICE_IP)' })
2041
- .option('roku-password', { type: 'string', describe: 'Roku dev password (env: HOSANNA_ROKU_DEV_PASSWORD)' })
2042
- .option('json', { type: 'boolean', default: false, describe: 'Output results as JSON only' })
2043
- .option('report-dir', { type: 'string', describe: 'Directory for HTML + JUnit XML reports' })
2044
- .option('update-baselines', { type: 'boolean', default: false, describe: 'Re-capture baseline screenshots without asserting (replays actions only)' })
2045
- .option('coverage', { type: 'boolean', default: false, describe: 'Analyze recordings and print navigation coverage (no running app needed)' }), async (args) => {
2046
- if (args.verbose) {
2047
- process.env.HOSANNA_VERBOSE = '1';
2048
- }
2049
- const { runTestUI } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/cli-test-runner.js')));
2050
- await runTestUI({
2051
- recordings: args.recordings,
2052
- suite: args.suite,
2053
- appId: args.appId,
2054
- managementPort: args.managementPort,
2055
- managementHost: args.managementHost,
2056
- extensionPort: args.extensionPort,
2057
- stopOnFailure: args.stopOnFailure,
2058
- stepDelayMs: args.stepDelayMs,
2059
- focusAssertRetryMs: args.focusAssertRetryMs,
2060
- focusAssertMaxRetries: args.focusAssertMaxRetries,
2061
- screenshotSettleMs: args.screenshotSettleMs,
2062
- compareScreenshots: args.compareScreenshots,
2063
- screenshotThreshold: args.screenshotThreshold,
2064
- screenshotFailPercent: args.screenshotFailPercent,
2065
- rokuIp: args.rokuIp,
2066
- rokuPassword: args.rokuPassword,
2067
- json: args.json,
2068
- verbose: args.verbose,
2069
- reportDir: args.reportDir,
2070
- updateBaselines: args.updateBaselines,
2071
- coverage: args.coverage,
2072
- });
2073
- })
2074
- .command('test:record', 'Interactively record a UI test by navigating the app. ' +
2075
- 'Key events from the running app are captured via the debug proxy.', yargs => yargs
2076
- .option('name', {
2077
- type: 'string',
2078
- demandOption: true,
2079
- describe: 'Name for this test recording',
2080
- })
2081
- .option('description', {
2082
- type: 'string',
2083
- describe: 'Optional description of what this test covers',
2084
- })
2085
- .option('appId', {
2086
- type: 'string',
2087
- describe: 'Target app ID (defaults to first connected app)',
2088
- })
2089
- .option('managementPort', { type: 'number', default: 59150, describe: 'Debug proxy management port' })
2090
- .option('managementHost', { type: 'string', default: 'localhost', describe: 'Debug proxy host' })
2091
- .option('extensionPort', { type: 'number', default: 59153, describe: 'Debug proxy extension WebSocket port' })
2092
- .option('roku-ip', { type: 'string', describe: 'Roku device IP for screenshots (env: HOSANNA_ROKU_DEVICE_IP)' })
2093
- .option('roku-password', { type: 'string', describe: 'Roku dev password for screenshots (env: HOSANNA_ROKU_DEV_PASSWORD)' })
2094
- .option('screenshot-on-select', { type: 'boolean', default: true, describe: 'Auto-capture screenshot on Select key press' })
2095
- .example('$0 test:record --name "Home screen navigation"', 'Record key presses while navigating')
2096
- .example('$0 test:record --name "Details test" --roku-ip 192.168.1.10', 'Record with Roku screenshot support'), async (args) => {
2097
- if (args.verbose) {
2098
- process.env.HOSANNA_VERBOSE = '1';
2099
- }
2100
- const { runTestRecord } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/cli-test-recorder.js')));
2101
- await runTestRecord({
2102
- name: args.name,
2103
- description: args.description,
2104
- appId: args.appId,
2105
- managementPort: args.managementPort,
2106
- managementHost: args.managementHost,
2107
- extensionPort: args.extensionPort,
2108
- rokuIp: args.rokuIp,
2109
- rokuPassword: args.rokuPassword,
2110
- screenshotOnSelect: args.screenshotOnSelect,
2111
- verbose: args.verbose,
2112
- });
2113
- })
2114
- .command('rasp:export <recordingId> <name>', 'Convert a saved test recording into a Roku Automation Scripting Platform (RASP) ' +
2059
+ .command('rasp:export <captureId> <name>', 'Convert a saved RASP input capture into a Roku Automation Scripting Platform (RASP) ' +
2115
2060
  'YAML script suitable for Roku certification testing. Output goes to ' +
2116
2061
  'rasp-scripts/<name>.rasp in the current project by default.', yargs => yargs
2117
- .positional('recordingId', {
2062
+ .positional('captureId', {
2118
2063
  type: 'string',
2119
2064
  demandOption: true,
2120
- describe: 'ID of the saved recording (filename without .json), e.g. "signin_1736888000000"',
2065
+ describe: 'ID of the saved capture (filename without .json), e.g. "signin_1736888000000"',
2121
2066
  })
2122
2067
  .positional('name', {
2123
2068
  type: 'string',
@@ -2127,11 +2072,11 @@ catch (err) {
2127
2072
  .option('out', {
2128
2073
  type: 'string',
2129
2074
  alias: 'o',
2130
- describe: 'Override the output path. Default: rasp-scripts/<name>.rasp next to recordings dir.',
2075
+ describe: 'Override the output path. Default: rasp-scripts/<name>.rasp next to the capture directory.',
2131
2076
  })
2132
2077
  .option('channel-name', {
2133
2078
  type: 'string',
2134
- describe: 'Channel name for the launch step (overrides recording.debugContext.channel.name).',
2079
+ describe: 'Channel name for the launch step (overrides capture.debugContext.channel.name).',
2135
2080
  })
2136
2081
  .option('channel-mode', {
2137
2082
  type: 'string',
@@ -2189,7 +2134,7 @@ catch (err) {
2189
2134
  describe: 'Master gate for credential auto-injection. When true (default), the exporter ' +
2190
2135
  'watches for OK presses landing on a TextInputView classified as email or password ' +
2191
2136
  '(via inputType / secureMode or field id) and injects the matching half of the RFI credential flow. ' +
2192
- 'Use --no-useWithRfi to disable injection entirely (recording flows through verbatim).',
2137
+ 'Use --no-useWithRfi to disable injection entirely (capture flows through verbatim).',
2193
2138
  })
2194
2139
  .option('login-text', {
2195
2140
  type: 'string',
@@ -2224,8 +2169,8 @@ catch (err) {
2224
2169
  }
2225
2170
  const path = await Promise.resolve().then(() => __importStar(require('path')));
2226
2171
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2227
- const { getRecordingsDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/test-runner.js')));
2228
- const { exportRaspFromRecording, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
2172
+ const { getRaspCapturesDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-input-capture.js')));
2173
+ const { exportRaspFromCapture, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
2229
2174
  // Parses a minimal RASP YAML fragment containing only `- press: <key>` and
2230
2175
  // `- text: <value>` ops (with `LOGIN` / `PASSWORD` reserved tokens). Comments
2231
2176
  // (#…) and blank lines are allowed. Anything else throws.
@@ -2252,16 +2197,16 @@ catch (err) {
2252
2197
  }
2253
2198
  return ops;
2254
2199
  };
2255
- const recordingId = args.recordingId;
2200
+ const captureId = args.captureId;
2256
2201
  const scriptName = args.name.replace(/[^a-zA-Z0-9_-]/g, '_');
2257
- const recordingsDir = getRecordingsDir();
2258
- const recordingPath = path.join(recordingsDir, `${recordingId}.json`);
2259
- let recording;
2202
+ const capturesDir = getRaspCapturesDir();
2203
+ const capturePath = path.join(capturesDir, `${captureId}.json`);
2204
+ let capture;
2260
2205
  try {
2261
- recording = JSON.parse(fs.readFileSync(recordingPath, 'utf-8'));
2206
+ capture = JSON.parse(fs.readFileSync(capturePath, 'utf-8'));
2262
2207
  }
2263
2208
  catch (_c) {
2264
- console.error(`Recording "${recordingId}" not found in ${recordingsDir}.`);
2209
+ console.error(`RASP capture "${captureId}" not found in ${capturesDir}.`);
2265
2210
  process.exit(1);
2266
2211
  }
2267
2212
  // Parse repeatable --text-field flags into a map.
@@ -2279,7 +2224,7 @@ catch (err) {
2279
2224
  const credentialFlow = credentialFlowPath
2280
2225
  ? parseCredentialFlowFile(credentialFlowPath)
2281
2226
  : undefined;
2282
- const result = exportRaspFromRecording(recording, {
2227
+ const result = exportRaspFromCapture(capture, {
2283
2228
  channelName: args.channelName,
2284
2229
  channelMode: args.channelMode,
2285
2230
  contentId: args.contentId,
@@ -2304,7 +2249,7 @@ catch (err) {
2304
2249
  process.stdout.write(result.yaml);
2305
2250
  }
2306
2251
  console.error(`✅ Wrote ${outPath}`);
2307
- console.error(` ${result.stats.recordedSteps} recorded steps → ${result.stats.raspOps} RASP ops` +
2252
+ console.error(` ${result.stats.capturedSteps} captured steps → ${result.stats.raspOps} RASP ops` +
2308
2253
  ` (auto-pauses: ${result.stats.autoPausesInserted}, text injections: ${result.stats.textInjections}` +
2309
2254
  `, keyboard steps dropped: ${result.stats.keyboardStepsDropped}, verify ops: ${result.stats.verifyOpsEmitted})`);
2310
2255
  for (const d of result.rfiDetections) {
@@ -2327,7 +2272,7 @@ catch (err) {
2327
2272
  }
2328
2273
  })
2329
2274
  .command('rasp:capture <name>', 'Record a UI session and export it directly to a RASP script in one step. ' +
2330
- 'Navigate the app, press Ctrl+C to stop — the recording is saved and the RASP script is written automatically.', yargs => yargs
2275
+ 'Navigate the app, press Ctrl+C to stop — the capture is saved and the RASP script is written automatically.', yargs => yargs
2331
2276
  .positional('name', {
2332
2277
  type: 'string',
2333
2278
  demandOption: true,
@@ -2412,10 +2357,10 @@ catch (err) {
2412
2357
  default: false,
2413
2358
  describe: 'Print the YAML to stdout after writing.',
2414
2359
  })
2415
- .option('keep-recording', {
2360
+ .option('keep-capture', {
2416
2361
  type: 'boolean',
2417
2362
  default: false,
2418
- describe: 'Keep the temporary test-recordings JSON/screenshots after the RASP script is written.',
2363
+ describe: 'Keep the temporary RASP capture JSON/screenshots after the script is written.',
2419
2364
  })
2420
2365
  .example('$0 rasp:capture login --channel-name "Criterion"', 'Record and export to rasp-scripts/login.rasp')
2421
2366
  .example('$0 rasp:capture login --channel-name "Criterion" --login-text "user@example.com" --password-text "MyPass1!"', 'Default credential flow with custom text')
@@ -2423,13 +2368,13 @@ catch (err) {
2423
2368
  var _a, _b;
2424
2369
  const path = await Promise.resolve().then(() => __importStar(require('path')));
2425
2370
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
2426
- const { cleanupSavedRecordingArtifacts, runTestRecord } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/cli-test-recorder.js')));
2427
- const { getRecordingsDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/test-runner.js')));
2428
- const { exportRaspFromRecording, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
2371
+ const { cleanupRaspCaptureArtifacts, runRaspCapture } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-capture.js')));
2372
+ const { getRaspCapturesDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-input-capture.js')));
2373
+ const { exportRaspFromCapture, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
2429
2374
  const scriptName = args.name.replace(/[^a-zA-Z0-9_-]/g, '_');
2430
2375
  const raspScriptsDir = path.join(process.cwd(), 'rasp-scripts');
2431
2376
  const outPath = (_a = args.out) !== null && _a !== void 0 ? _a : path.join(raspScriptsDir, `${scriptName}.rasp`);
2432
- const recordingsDir = getRecordingsDir();
2377
+ const capturesDir = getRaspCapturesDir();
2433
2378
  const textFields = {};
2434
2379
  const tfArgs = (_b = args['text-field']) !== null && _b !== void 0 ? _b : [];
2435
2380
  for (const tf of tfArgs) {
@@ -2467,7 +2412,7 @@ catch (err) {
2467
2412
  const credentialFlow = credentialFlowPath
2468
2413
  ? parseCredentialFlowFile(credentialFlowPath)
2469
2414
  : undefined;
2470
- await runTestRecord({
2415
+ await runRaspCapture({
2471
2416
  name: args.name,
2472
2417
  appId: args.appId,
2473
2418
  managementPort: args.managementPort,
@@ -2475,8 +2420,8 @@ catch (err) {
2475
2420
  extensionPort: args.extensionPort,
2476
2421
  rokuIp: args.rokuIp,
2477
2422
  rokuPassword: args.rokuPassword,
2478
- onSaved: async (recordingId, recording) => {
2479
- const result = exportRaspFromRecording(recording, {
2423
+ onSaved: async (captureId, capture) => {
2424
+ const result = exportRaspFromCapture(capture, {
2480
2425
  channelName: args.channelName,
2481
2426
  channelMode: args.channelMode,
2482
2427
  contentId: args.contentId,
@@ -2495,7 +2440,7 @@ catch (err) {
2495
2440
  if (args.print)
2496
2441
  process.stdout.write(result.yaml);
2497
2442
  console.log(`✅ RASP script written: ${outPath}`);
2498
- console.log(` ${result.stats.recordedSteps} steps → ${result.stats.raspOps} RASP ops` +
2443
+ console.log(` ${result.stats.capturedSteps} steps → ${result.stats.raspOps} RASP ops` +
2499
2444
  ` (pauses: ${result.stats.autoPausesInserted}, text injections: ${result.stats.textInjections})`);
2500
2445
  for (const d of result.rfiDetections) {
2501
2446
  const icon = d.type === 'login' ? '📧' : '🔑';
@@ -2514,15 +2459,15 @@ catch (err) {
2514
2459
  console.error(` Script has validation errors — review before submitting to Roku cert.`);
2515
2460
  process.exitCode = 1;
2516
2461
  }
2517
- if (!args.keepRecording) {
2462
+ if (!args.keepCapture) {
2518
2463
  try {
2519
- const cleanup = cleanupSavedRecordingArtifacts(recordingsDir, recordingId);
2464
+ const cleanup = cleanupRaspCaptureArtifacts(capturesDir, captureId);
2520
2465
  if (cleanup.jsonDeleted || cleanup.artifactDirDeleted) {
2521
- console.log(`🧹 Cleaned up temporary recording: ${recordingId}`);
2466
+ console.log(`🧹 Cleaned up temporary capture: ${captureId}`);
2522
2467
  }
2523
2468
  }
2524
2469
  catch (err) {
2525
- console.warn(`⚠ RASP script was written, but temporary recording cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
2470
+ console.warn(`⚠ RASP script was written, but temporary capture cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
2526
2471
  }
2527
2472
  }
2528
2473
  },