rnxsim 0.1.313 → 0.1.314

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 (297) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +480 -280
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
@@ -0,0 +1,19 @@
1
+ import { getCliInspectVerbMetaByPath } from '@sootsim/skills/cli'
2
+ import type { ParsedArgs } from './parse-args'
3
+
4
+ const HIDDEN_RUNTIME_ALIAS_GROUPS = ['do', 'get'] as const
5
+
6
+ export function resolveHiddenRuntimeAlias(parsed: ParsedArgs): ParsedArgs {
7
+ if (parsed.command) return parsed
8
+
9
+ const attempted = parsed.commandArgs.find((arg) => !arg.startsWith('-'))
10
+ if (!attempted) return parsed
11
+
12
+ for (const group of HIDDEN_RUNTIME_ALIAS_GROUPS) {
13
+ if (getCliInspectVerbMetaByPath(group, attempted)) {
14
+ return { ...parsed, command: group }
15
+ }
16
+ }
17
+
18
+ return parsed
19
+ }
package/cli/hints.ts ADDED
@@ -0,0 +1,216 @@
1
+ // configurable, identity-scoped hint registry for the sootsim CLI.
2
+ //
3
+ // two problems this solves:
4
+ // 1. the CLI scatters "hint: ..." console.logs across many command files,
5
+ // each with slightly different wording.
6
+ // 2. agents that loop over the same command (e.g. describe, find) see the
7
+ // same hint every invocation, which becomes noise.
8
+ //
9
+ // each hint declares its own frequency (always, once-per-identity, or a
10
+ // cooldown), so hints that are informational-but-stale-quickly can still
11
+ // re-fire while advice-like hints stay suppressed for the rest of the
12
+ // CLI identity.
13
+ //
14
+ // suppression is keyed on `getCliIdentityKey()` so two agents / two terminals
15
+ // don't share shown-sets.
16
+
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
18
+ import { tmpdir } from 'os'
19
+ import { dirname, join } from 'path'
20
+ import { getCliIdentityKey } from './current-sim'
21
+
22
+ export type HintFrequency =
23
+ | 'always'
24
+ | 'once-per-identity'
25
+ | 'once-ever'
26
+ | { cooldownMs: number }
27
+
28
+ export interface HintSpec<Args extends unknown[] = unknown[]> {
29
+ frequency: HintFrequency
30
+ // returned string(s) are printed as-is, prefixed with two spaces.
31
+ // return null to skip (useful when render wants to decide conditionally).
32
+ render: (...args: Args) => string | string[] | null
33
+ }
34
+
35
+ const registry = new Map<string, HintSpec<any[]>>()
36
+
37
+ export function defineHint<Args extends unknown[]>(id: string, spec: HintSpec<Args>) {
38
+ registry.set(id, spec as HintSpec<any[]>)
39
+ }
40
+
41
+ interface HintStateFile {
42
+ version: 1
43
+ shown: Record<string, number> // id -> last-shown timestamp (ms)
44
+ }
45
+
46
+ const STATE_VERSION = 1 as const
47
+
48
+ function identityStatePath(): string {
49
+ return join(tmpdir(), `sootsim-cli-hints-${getCliIdentityKey()}.json`)
50
+ }
51
+
52
+ function globalStatePath(): string {
53
+ return join(tmpdir(), `sootsim-cli-hints-global.json`)
54
+ }
55
+
56
+ function readState(path: string): HintStateFile {
57
+ if (!existsSync(path)) return { version: STATE_VERSION, shown: {} }
58
+ try {
59
+ const parsed = JSON.parse(readFileSync(path, 'utf8'))
60
+ if (parsed?.version !== STATE_VERSION || !parsed?.shown) {
61
+ return { version: STATE_VERSION, shown: {} }
62
+ }
63
+ return parsed as HintStateFile
64
+ } catch {
65
+ return { version: STATE_VERSION, shown: {} }
66
+ }
67
+ }
68
+
69
+ function writeState(path: string, state: HintStateFile) {
70
+ try {
71
+ mkdirSync(dirname(path), { recursive: true })
72
+ writeFileSync(path, JSON.stringify(state) + '\n')
73
+ } catch {
74
+ // best-effort only; losing shown state just means a hint re-fires once.
75
+ }
76
+ }
77
+
78
+ function modeFromEnv(): 'normal' | 'always' | 'off' {
79
+ const v = (process.env.SOOTSIM_HINTS || '').toLowerCase()
80
+ if (v === 'off' || v === '0' || v === 'false') return 'off'
81
+ if (v === 'always' || v === 'verbose') return 'always'
82
+ return 'normal'
83
+ }
84
+
85
+ function shouldShow(id: string, frequency: HintFrequency): boolean {
86
+ const mode = modeFromEnv()
87
+ if (mode === 'off') return false
88
+ if (mode === 'always') return true
89
+ if (frequency === 'always') return true
90
+
91
+ const now = Date.now()
92
+ if (frequency === 'once-per-identity') {
93
+ const state = readState(identityStatePath())
94
+ if (state.shown[id]) return false
95
+ state.shown[id] = now
96
+ writeState(identityStatePath(), state)
97
+ return true
98
+ }
99
+ if (frequency === 'once-ever') {
100
+ const state = readState(globalStatePath())
101
+ if (state.shown[id]) return false
102
+ state.shown[id] = now
103
+ writeState(globalStatePath(), state)
104
+ return true
105
+ }
106
+ if (typeof frequency === 'object' && 'cooldownMs' in frequency) {
107
+ const state = readState(identityStatePath())
108
+ const last = state.shown[id] ?? 0
109
+ if (now - last < frequency.cooldownMs) return false
110
+ state.shown[id] = now
111
+ writeState(identityStatePath(), state)
112
+ return true
113
+ }
114
+ return true
115
+ }
116
+
117
+ // print a registered hint. no-ops if suppressed for this identity/cooldown.
118
+ // returns whether it was actually shown.
119
+ export function maybeHint(id: string, ...args: unknown[]): boolean {
120
+ const spec = registry.get(id)
121
+ if (!spec) {
122
+ if (process.env.SOOTSIM_HINTS_DEBUG) {
123
+ console.error(` [hints] no hint registered for id "${id}"`)
124
+ }
125
+ return false
126
+ }
127
+ if (!shouldShow(id, spec.frequency)) return false
128
+
129
+ const body = spec.render(...args)
130
+ if (body == null) return false
131
+ const lines = Array.isArray(body) ? body : [body]
132
+ // always stderr. every hint is advice about how to drive the CLI, never part
133
+ // of a command's result, and several commands (`describe`, `find`, every
134
+ // `do` verb) put a JSON document on stdout — a hint printed there corrupts it.
135
+ for (const line of lines) console.error(` hint: ${line}`)
136
+ return true
137
+ }
138
+
139
+ export function resetHintsForIdentity() {
140
+ rmSync(identityStatePath(), { force: true })
141
+ }
142
+
143
+ export function resetGlobalHints() {
144
+ rmSync(globalStatePath(), { force: true })
145
+ }
146
+
147
+ export function listHints(): Array<{ id: string; frequency: HintFrequency }> {
148
+ return Array.from(registry.entries()).map(([id, spec]) => ({
149
+ id,
150
+ frequency: spec.frequency,
151
+ }))
152
+ }
153
+
154
+ // register the repo-wide hints here. keeping them centralised makes it easy
155
+ // to audit wording + frequency policy in one place.
156
+ defineHint<[number]>('app-still-loading', {
157
+ // re-fires once a minute because node count may genuinely change as the
158
+ // bundle boots — telling the user about it again is useful, not noisy.
159
+ frequency: { cooldownMs: 60_000 },
160
+ render: (nodeCount) =>
161
+ `app may still be loading (${nodeCount} nodes). run \`rnxsim wait ready\` first.`,
162
+ })
163
+
164
+ defineHint<[string]>('wait-selector-for-missing-testid', {
165
+ // advice is stable; once per CLI identity is enough.
166
+ frequency: 'once-per-identity',
167
+ render: (testId) => `rnxsim wait selector ${testId}`,
168
+ })
169
+
170
+ defineHint<[string[]]>('prefer-cli-over-eval', {
171
+ frequency: 'once-per-identity',
172
+ render: (suggestions) => {
173
+ if (!suggestions.length) return null
174
+ return [`try the CLI shortcut instead:`, ...suggestions.map((s) => ` ${s}`)]
175
+ },
176
+ })
177
+
178
+ defineHint<[]>('describe-use-filters', {
179
+ // shown when a describe run emits >=80 lines without any filter active —
180
+ // gentle nudge that there are better tools.
181
+ frequency: 'once-per-identity',
182
+ render: () => [
183
+ `describe output is long. narrow it with:`,
184
+ ` rnxsim describe --only '*Bottom Sheet*'`,
185
+ ` rnxsim describe --testid-like 'swap-*'`,
186
+ ` rnxsim describe --subtree <testID>`,
187
+ ],
188
+ })
189
+
190
+ defineHint<[]>('describe-filter-context', {
191
+ // filtered describe is useful search output, but it intentionally drops
192
+ // surrounding tree context. once per identity is enough to prevent agents
193
+ // treating a search hit as route proof.
194
+ frequency: 'once-per-identity',
195
+ render: () => [
196
+ `filtered describe is search output, not current-route proof.`,
197
+ `run \`rnxsim describe\` without filters after navigation; clipped nodes are marked \`(clipped:...)\`.`,
198
+ ],
199
+ })
200
+
201
+ defineHint<[string]>('subtree-root-not-found', {
202
+ frequency: 'always',
203
+ render: (id) =>
204
+ `no node with testID/id "${id}" — try \`rnxsim find --testid ${id}\` to discover available ids.`,
205
+ })
206
+
207
+ defineHint<[]>('clean-uninstall', {
208
+ // shown once on the first `sootsim --help` run so users learn the clean
209
+ // teardown path. package-manager uninstall hooks are best-effort, so the
210
+ // hint documents the explicit daemon cleanup command.
211
+ frequency: 'once-ever',
212
+ render: () => [
213
+ `to remove the background daemon and all ~/.sootsim data, run \`rnxsim daemon uninstall\`.`,
214
+ `the optional desktop app and its macOS preference plist are separate from the daemon.`,
215
+ ],
216
+ })
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import { dirname, join, resolve } from 'path'
4
+
5
+ const INSPECT_NOTICE_STATE_VERSION = 1 as const
6
+ const INSPECT_NOTICE_PATH_ENV = 'SOOTSIM_INSPECT_NOTICE_PATH'
7
+ const INSPECT_NOTICE_ENTRY_TTL_MS = 5 * 60 * 1000
8
+
9
+ export const INSPECT_NOTICE_COOLDOWN_MS = 15_000
10
+
11
+ type InspectNoticeKind = 'console' | 'requests'
12
+
13
+ interface InspectNoticeEntry {
14
+ signature: string
15
+ updatedAt: number
16
+ }
17
+
18
+ interface InspectNoticeState {
19
+ version: 1
20
+ entries: Record<string, InspectNoticeEntry>
21
+ }
22
+
23
+ export function getInspectNoticeStatePath() {
24
+ return resolve(
25
+ process.env[INSPECT_NOTICE_PATH_ENV] ||
26
+ join(tmpdir(), 'sootsim-inspect-notice-state.json'),
27
+ )
28
+ }
29
+
30
+ function pruneEntries(
31
+ entries: Record<string, InspectNoticeEntry>,
32
+ nowMs: number,
33
+ ): Record<string, InspectNoticeEntry> {
34
+ return Object.fromEntries(
35
+ Object.entries(entries).filter(([, entry]) => {
36
+ return (
37
+ typeof entry?.signature === 'string' &&
38
+ Number.isFinite(entry?.updatedAt) &&
39
+ nowMs - entry.updatedAt <= INSPECT_NOTICE_ENTRY_TTL_MS
40
+ )
41
+ }),
42
+ )
43
+ }
44
+
45
+ function readInspectNoticeState(nowMs: number): InspectNoticeState {
46
+ const filepath = getInspectNoticeStatePath()
47
+ if (!existsSync(filepath)) {
48
+ return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
49
+ }
50
+
51
+ try {
52
+ const parsed = JSON.parse(
53
+ readFileSync(filepath, 'utf8'),
54
+ ) as Partial<InspectNoticeState>
55
+ if (
56
+ parsed.version !== INSPECT_NOTICE_STATE_VERSION ||
57
+ !parsed.entries ||
58
+ typeof parsed.entries !== 'object'
59
+ ) {
60
+ rmSync(filepath, { force: true })
61
+ return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
62
+ }
63
+ return {
64
+ version: INSPECT_NOTICE_STATE_VERSION,
65
+ entries: pruneEntries(parsed.entries as Record<string, InspectNoticeEntry>, nowMs),
66
+ }
67
+ } catch {
68
+ rmSync(filepath, { force: true })
69
+ return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
70
+ }
71
+ }
72
+
73
+ function writeInspectNoticeState(state: InspectNoticeState) {
74
+ const filepath = getInspectNoticeStatePath()
75
+ mkdirSync(dirname(filepath), { recursive: true })
76
+ writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n')
77
+ }
78
+
79
+ function getInspectNoticeKey(kind: InspectNoticeKind, scope: string) {
80
+ const normalizedScope = scope.trim() || 'default'
81
+ return `${kind}:${normalizedScope}`
82
+ }
83
+
84
+ export function shouldPrintInspectNotice(
85
+ kind: InspectNoticeKind,
86
+ scope: string,
87
+ signature: string,
88
+ opts: { nowMs?: number; cooldownMs?: number } = {},
89
+ ) {
90
+ const nowMs = opts.nowMs ?? Date.now()
91
+ const cooldownMs = opts.cooldownMs ?? INSPECT_NOTICE_COOLDOWN_MS
92
+ const state = readInspectNoticeState(nowMs)
93
+ const key = getInspectNoticeKey(kind, scope)
94
+ const previous = state.entries[key]
95
+
96
+ if (
97
+ previous &&
98
+ previous.signature === signature &&
99
+ nowMs - previous.updatedAt < cooldownMs
100
+ ) {
101
+ return false
102
+ }
103
+
104
+ state.entries[key] = {
105
+ signature,
106
+ updatedAt: nowMs,
107
+ }
108
+ writeInspectNoticeState(state)
109
+ return true
110
+ }
111
+
112
+ export function clearInspectNoticeState() {
113
+ rmSync(getInspectNoticeStatePath(), { force: true })
114
+ }
@@ -0,0 +1,334 @@
1
+ // maestro-compatible flow JS engine — mirrors maestro's GraalJsEngine
2
+ // (maestro-client/src/main/java/maestro/js/GraalJsEngine.kt) on top of
3
+ // node's vm module. one context per flow run, shared by the whole flow
4
+ // including runFlow sub-flows, exactly like upstream:
5
+ //
6
+ // globals: http, output (persistent map), maestro ({copiedText, platform}),
7
+ // json(), relativePoint(), env vars as bare globals (scope-stacked per
8
+ // sub-flow), console.log routed to the flow log. undeclared identifiers
9
+ // evaluate to undefined for `${MY_VAR || 'default'}` patterns (upstream uses
10
+ // a has-trapping Proxy as globalThis's prototype; node/V8 forbids that, so we
11
+ // get the same semantics from a with-scope proxy — see the constructor).
12
+ //
13
+ // `${expr}` string templates are full JS evaluation at command-execution
14
+ // time (maestro-orchestra-models util/Env.kt): regex (?<!\\)\$\{([^$]*)},
15
+ // result stringified, `\${...}` escapes to a literal, blank exprs become ''.
16
+ //
17
+ // divergences (deliberate, sootsim-side ergonomics — keep this list short):
18
+ // 1. envBinding is seeded from the full process.env, not just MAESTRO_*.
19
+ // sootsim flows historically resolve `${SOOTSIM_*}` credentials from the
20
+ // shell environment and `sootsim maestro --env K=V` writes process.env.
21
+ // 2. a *bare identifier* template (`${NAME}`) that evaluates to undefined
22
+ // throws a loud "missing variable" error instead of interpolating the
23
+ // literal text "undefined" into a tap target or login form. complex
24
+ // expressions keep upstream semantics (undefined -> "undefined").
25
+ // 3. `faker` is not implemented — flows using it fail loudly rather than
26
+ // getting a half-faithful reimplementation of datafaker.
27
+
28
+ import { spawnSync } from 'child_process'
29
+ import * as vm from 'vm'
30
+
31
+ const HTTP_TIMEOUT_MS = 300_000 // upstream: okhttp read/write/call timeout 5min
32
+
33
+ // child process source for synchronous http from inside the vm context.
34
+ // maestro's `http` API is synchronous (scripts do `var res = http.post(...)`),
35
+ // and node has no sync fetch — so the request round-trips through a blocking
36
+ // spawnSync of this script. stdin: JSON request; stdout: JSON response.
37
+ const HTTP_CHILD_SRC = `
38
+ let input = '';
39
+ process.stdin.on('data', (c) => { input += c });
40
+ process.stdin.on('end', async () => {
41
+ try {
42
+ const req = JSON.parse(input);
43
+ const res = await fetch(req.url, {
44
+ method: req.method,
45
+ headers: req.headers,
46
+ body: req.body === undefined || req.body === null ? undefined : req.body,
47
+ });
48
+ const body = await res.text();
49
+ const headers = {};
50
+ for (const [k, v] of res.headers.entries()) {
51
+ headers[k] = headers[k] ? headers[k] + ',' + v : v;
52
+ }
53
+ process.stdout.write(JSON.stringify({ ok: res.ok, status: res.status, body, headers }));
54
+ } catch (err) {
55
+ process.stdout.write(JSON.stringify({ __error: (err && err.message) || String(err) }));
56
+ process.exitCode = 1;
57
+ }
58
+ });
59
+ `
60
+
61
+ interface HttpParams {
62
+ body?: string
63
+ headers?: Record<string, string>
64
+ method?: string
65
+ multipartForm?: unknown
66
+ }
67
+
68
+ function executeSyncHttp(
69
+ url: string,
70
+ method: string,
71
+ params?: HttpParams,
72
+ ): { ok: boolean; status: number; body: string; headers: Record<string, string> } {
73
+ if (params?.multipartForm) {
74
+ throw new Error('http: multipartForm is not supported by rnx yet')
75
+ }
76
+ // the http child must be hermetic: when the runner runs under `bun` (or any
77
+ // process with a debug inspector), the inherited BUN_INSPECT*/NODE_OPTIONS
78
+ // env makes the spawned `bun -e` attach to the parent's inspector/IPC socket
79
+ // and exit 1 with no output instead of running our fetch. scrub those so the
80
+ // child is a clean fetch worker regardless of how the parent was launched.
81
+ const childEnv = { ...process.env }
82
+ for (const key of Object.keys(childEnv)) {
83
+ if (key.startsWith('BUN_INSPECT') || key === 'NODE_OPTIONS') {
84
+ delete childEnv[key]
85
+ }
86
+ }
87
+ const child = spawnSync(process.execPath, ['-e', HTTP_CHILD_SRC], {
88
+ input: JSON.stringify({ url, method, headers: params?.headers, body: params?.body }),
89
+ encoding: 'utf8',
90
+ timeout: HTTP_TIMEOUT_MS,
91
+ maxBuffer: 64 * 1024 * 1024,
92
+ env: childEnv,
93
+ })
94
+ if (child.error) {
95
+ throw new Error(`http ${method} ${url} failed: ${child.error.message}`)
96
+ }
97
+ let parsed: unknown
98
+ try {
99
+ parsed = JSON.parse(child.stdout || '')
100
+ } catch {
101
+ throw new Error(
102
+ `http ${method} ${url} failed: no response (${(child.stderr || '').trim().slice(0, 200)})`,
103
+ )
104
+ }
105
+ const result = parsed as {
106
+ __error?: string
107
+ ok: boolean
108
+ status: number
109
+ body: string
110
+ headers: Record<string, string>
111
+ }
112
+ if (result.__error) {
113
+ throw new Error(`http ${method} ${url} failed: ${result.__error}`)
114
+ }
115
+ return result
116
+ }
117
+
118
+ // matches upstream Env.kt: negative-lookbehind for the \ escape, [^$]* body.
119
+ const TEMPLATE_RE = /(?<!\\)\$\{([^$]*)\}/g
120
+ const ESCAPED_TEMPLATE_RE = /\\(\$\{[^$]*\})/g
121
+ const BARE_IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
122
+
123
+ export class MaestroJsContext {
124
+ private readonly context: vm.Context
125
+ private readonly envBinding = new Map<string, string>()
126
+ private readonly envScopeStack: Array<Map<string, string>> = []
127
+ // env keys currently materialized as context globals — cleared and re-set
128
+ // before each evaluation, mirroring upstream syncBindingsToContext.
129
+ private syncedEnvKeys = new Set<string>()
130
+ readonly output: Record<string, unknown> = {}
131
+ readonly maestro: { copiedText: string | null; platform: string }
132
+
133
+ constructor(opts: { platform?: string; onLog?: (msg: string) => void } = {}) {
134
+ this.maestro = { copiedText: null, platform: opts.platform ?? 'ios' }
135
+ const onLog = opts.onLog ?? ((msg: string) => console.log(`[flow] js: ${msg}`))
136
+ const sandbox: Record<string, unknown> = {
137
+ output: this.output,
138
+ maestro: this.maestro,
139
+ http: {
140
+ get: (url: string, params?: HttpParams) => executeSyncHttp(url, 'GET', params),
141
+ post: (url: string, params?: HttpParams) => executeSyncHttp(url, 'POST', params),
142
+ put: (url: string, params?: HttpParams) => executeSyncHttp(url, 'PUT', params),
143
+ delete: (url: string, params?: HttpParams) =>
144
+ executeSyncHttp(url, 'DELETE', params),
145
+ request: (url: string, params?: HttpParams) =>
146
+ executeSyncHttp(url, (params?.method ?? 'GET').toUpperCase(), params),
147
+ },
148
+ console: {
149
+ log: (...args: unknown[]) =>
150
+ onLog(args.map((a) => (typeof a === 'string' ? a : stringifyJs(a))).join(' ')),
151
+ },
152
+ }
153
+ this.context = vm.createContext(sandbox)
154
+ // upstream GraalJsEngine resolves undeclared identifiers to undefined via
155
+ // a has-trapping Proxy installed as globalThis's prototype, enabling the
156
+ // `${VAR || 'default'}` idiom. node/V8's vm forbids a Proxy in the global
157
+ // prototype chain ("Proxy is not allowed in the global prototype chain"),
158
+ // so we get the identical semantics with a `with`-scope proxy instead:
159
+ // evalRaw wraps every script in `with(__maestroScope){…}`, and the proxy's
160
+ // has→true makes any bare identifier resolve through the scope (returning
161
+ // the real global, or undefined for a name that isn't defined) rather than
162
+ // throwing ReferenceError. plus the json/relativePoint helpers.
163
+ vm.runInContext(
164
+ `
165
+ globalThis.__maestroScope = new Proxy(globalThis, {
166
+ has() { return true },
167
+ get(target, key) { return target[key] },
168
+ })
169
+ function json(text) { return JSON.parse(text) }
170
+ function relativePoint(x, y) {
171
+ var xPercent = Math.ceil(x * 100) + '%'
172
+ var yPercent = Math.ceil(y * 100) + '%'
173
+ return xPercent + ',' + yPercent
174
+ }
175
+ `,
176
+ this.context,
177
+ )
178
+ // divergence #1: seed env from the shell so `${SOOTSIM_*}` keeps working.
179
+ for (const [key, value] of Object.entries(process.env)) {
180
+ if (value !== undefined) this.envBinding.set(key, value)
181
+ }
182
+ }
183
+
184
+ putEnv(key: string, value: string) {
185
+ this.envBinding.set(key, value)
186
+ }
187
+
188
+ setCopiedText(text: string | null) {
189
+ this.maestro.copiedText = text
190
+ }
191
+
192
+ enterEnvScope() {
193
+ this.envScopeStack.push(new Map(this.envBinding))
194
+ }
195
+
196
+ leaveEnvScope() {
197
+ const previous = this.envScopeStack.pop()
198
+ if (previous) {
199
+ this.envBinding.clear()
200
+ for (const [k, v] of previous) this.envBinding.set(k, v)
201
+ }
202
+ }
203
+
204
+ // evaluate a JS snippet in the shared context. mirrors upstream
205
+ // evalWithIIFE: the script is embedded in a template literal and run
206
+ // through eval inside an IIFE so var/let/const stay scoped to this
207
+ // evaluation while the last expression's value is returned.
208
+ evaluate(script: string, opts: { env?: Record<string, string> } = {}): unknown {
209
+ const env = opts.env
210
+ if (env && Object.keys(env).length > 0) {
211
+ this.enterEnvScope()
212
+ try {
213
+ for (const [k, v] of Object.entries(env)) this.envBinding.set(k, v)
214
+ return this.evalRaw(script)
215
+ } finally {
216
+ this.leaveEnvScope()
217
+ }
218
+ }
219
+ return this.evalRaw(script)
220
+ }
221
+
222
+ private evalRaw(script: string): unknown {
223
+ this.syncEnvToContext()
224
+ const escaped = script
225
+ .replace(/\\/g, '\\\\')
226
+ .replace(/`/g, '\\`')
227
+ .replace(/\$\{/g, '\\${')
228
+ // `with(__maestroScope)` gives undeclared identifiers the upstream
229
+ // undefined semantics (see constructor); the IIFE + direct eval keep
230
+ // var/let/const scoped to this evaluation while returning the last value.
231
+ return vm.runInContext(
232
+ `(function(){ with(__maestroScope){ return eval(\`${escaped}\`) } })()`,
233
+ this.context,
234
+ )
235
+ }
236
+
237
+ private syncEnvToContext() {
238
+ const globals = vm.runInContext('globalThis', this.context) as Record<string, unknown>
239
+ for (const key of this.syncedEnvKeys) {
240
+ if (!this.envBinding.has(key)) delete globals[key]
241
+ }
242
+ this.syncedEnvKeys = new Set()
243
+ for (const [key, value] of this.envBinding) {
244
+ try {
245
+ globals[key] = value
246
+ this.syncedEnvKeys.add(key)
247
+ } catch {
248
+ // non-writable global (e.g. a frozen builtin name in env) — skip
249
+ }
250
+ }
251
+ }
252
+
253
+ // upstream Env.kt String.evaluateScripts: replace every unescaped `${expr}`
254
+ // with the stringified evaluation result, then unescape `\${...}`.
255
+ evaluateStringTemplate(value: string): string {
256
+ const replaced = value.replace(TEMPLATE_RE, (_match, expr: string) => {
257
+ if (expr.trim().length === 0) return ''
258
+ const result = this.evaluate(expr)
259
+ // divergence #2: a bare `${NAME}` that comes back undefined is a flow
260
+ // bug 100% of the time — fail loudly instead of typing "undefined".
261
+ if (result === undefined && BARE_IDENTIFIER_RE.test(expr.trim())) {
262
+ throw new Error(`missing variable for flow placeholder: ${expr.trim()}`)
263
+ }
264
+ return stringifyJs(result)
265
+ })
266
+ return replaced.replace(ESCAPED_TEMPLATE_RE, (_match, inner: string) => inner)
267
+ }
268
+
269
+ // walk a parsed step and evaluate every string template. nested command
270
+ // lists are skipped — each nested command evaluates its own strings when
271
+ // it executes (matching upstream per-command evaluateScripts timing, and
272
+ // required for values that change between iterations, e.g. copyTextFrom
273
+ // inside repeat).
274
+ interpolateStep<T>(step: T): T {
275
+ return this.interpolateValue(step, null) as T
276
+ }
277
+
278
+ private interpolateValue(value: unknown, parentKey: string | null): unknown {
279
+ if (typeof value === 'string') {
280
+ // evalScript IS the evaluation — the runner template-evaluates it in
281
+ // its handler. interpolating here too would run the script twice.
282
+ if (parentKey === 'evalScript') return value
283
+ return this.evaluateStringTemplate(value)
284
+ }
285
+ if (Array.isArray(value)) {
286
+ if (
287
+ parentKey === 'commands' ||
288
+ parentKey === 'onFlowStart' ||
289
+ parentKey === 'onFlowComplete'
290
+ ) {
291
+ return value
292
+ }
293
+ return value.map((entry) => this.interpolateValue(entry, parentKey))
294
+ }
295
+ if (value && typeof value === 'object') {
296
+ return Object.fromEntries(
297
+ Object.entries(value).map(([key, entry]) => [
298
+ key,
299
+ this.interpolateValue(entry, key),
300
+ ]),
301
+ )
302
+ }
303
+ return value
304
+ }
305
+ }
306
+
307
+ function stringifyJs(value: unknown): string {
308
+ if (value === undefined) return 'undefined'
309
+ if (value === null) return 'null'
310
+ if (typeof value === 'object') {
311
+ try {
312
+ return JSON.stringify(value)
313
+ } catch {
314
+ return String(value)
315
+ }
316
+ }
317
+ return String(value)
318
+ }
319
+
320
+ // upstream Orchestra.evaluateCondition scriptCondition semantics: the value
321
+ // arrives already template-evaluated; false when blank / "false" (any case) /
322
+ // "undefined" / "null" / numerically zero. yaml may also hand us a real
323
+ // boolean or number before stringification — normalize first.
324
+ export function scriptConditionIsTruthy(value: unknown): boolean {
325
+ if (value === undefined || value === null) return false
326
+ if (typeof value === 'boolean') return value
327
+ const text = String(value)
328
+ if (text.trim().length === 0) return false
329
+ if (text.toLowerCase() === 'false') return false
330
+ if (text === 'undefined' || text === 'null') return false
331
+ const num = Number(text)
332
+ if (!Number.isNaN(num) && num === 0) return false
333
+ return true
334
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ buildOpenUrlCommand,
3
+ launchUrl,
4
+ openUrl,
5
+ type OpenUrlCommand,
6
+ type OpenUrlLaunchResult,
7
+ type OpenUrlOptions,
8
+ } from '../src/host/open-url'