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,1124 @@
1
+ // shared Maestro YAML playback and live-authoring implementation.
2
+ // discovers a sootsim target when needed, then reuses the shared bridge action layer
3
+
4
+ import * as fs from 'fs'
5
+ import { createHash, randomUUID } from 'node:crypto'
6
+ import { tmpdir } from 'os'
7
+ import * as path from 'path'
8
+ import { scanDevServers } from '../../scripts/dev-server-scanner'
9
+ import { loadOptionalDemoApps } from '../../scripts/optional-demo-registry'
10
+ import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
11
+ import {
12
+ SootSimBridgeFlowRunner,
13
+ type SootSimFlowProfileResult,
14
+ type SootSimFlowTraceStep,
15
+ } from '../bridge-flow-runner'
16
+ import { readCurrentSimId, saveCurrentSimId } from '../current-sim'
17
+ import { findDesktopCompanion } from '../desktop-companion'
18
+ import { parseFlowFile, validateFlowFile } from '../flow-file'
19
+ import { FlowLiveStatusReporter } from '../flow-live-status'
20
+ import {
21
+ exportFlowSession,
22
+ finalizeFlowSession,
23
+ keepFlowCandidate,
24
+ startFlowSession,
25
+ } from '../flow-session'
26
+ import { flushCliTelemetry, trackCliEvent } from '../telemetry'
27
+ import { createBridge, createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
28
+ import {
29
+ buildShellUrl,
30
+ closeSimsBulk,
31
+ resolveShellBaseUrlForBridgePort,
32
+ runOpenCommand,
33
+ terminatePlaywrightHostsForSims,
34
+ } from './control'
35
+ import { printShellPerfReport } from './inspect'
36
+ import { resolveDefaultUploadOrigin } from './upload'
37
+ import type { UploadResult } from './upload'
38
+
39
+ export { parseFlowFile, validateFlowFile } from '../flow-file'
40
+
41
+ let lastPreviewUploadResult: UploadResult | null = null
42
+
43
+ export function getLastFlowPreviewUploadResult(): UploadResult | null {
44
+ return lastPreviewUploadResult
45
+ }
46
+
47
+ // per-step trace of the most recent playback, preview or not — callers
48
+ // Maestro callers fold this into their run registration.
49
+ let lastFlowTraceSteps: SootSimFlowTraceStep[] = []
50
+
51
+ export function getLastFlowTraceSteps(): SootSimFlowTraceStep[] {
52
+ return lastFlowTraceSteps
53
+ }
54
+
55
+ // resolve a frontmatter `app:` value that may be a port, a URL, or a
56
+ // registered demo name (`bluesky`, `3pc`, `uniswap`, …). returns the
57
+ // raw string for ports/URLs untouched; for a registered name, returns
58
+ // the launcher's preferredPort.
59
+ async function resolveAppFrontmatterTarget(raw: string): Promise<string> {
60
+ if (!raw) return ''
61
+ const trimmed = raw.trim()
62
+ if (!trimmed) return ''
63
+ if (/^\d+$/.test(trimmed)) return trimmed
64
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return trimmed
65
+ // try a case-insensitive name match against the OPTIONAL internal demo
66
+ // registry (in-repo only; empty on a published install). unknown strings
67
+ // fall through unchanged so other targets (host:port, raw bundle URLs
68
+ // without a scheme) still reach the legacy resolver.
69
+ const apps = await loadOptionalDemoApps()
70
+ const match = apps.find((app) => app.name.toLowerCase() === trimmed.toLowerCase())
71
+ if (match) return String(match.preferredPort)
72
+ return trimmed
73
+ }
74
+
75
+ // discover a sootsim shell URL — prefer patched servers, fall back to any match.
76
+ // exported so `detox.ts` and `maestro.ts` can reuse the same discovery logic.
77
+ export async function discoverSootsimUrl(): Promise<string | null> {
78
+ const servers = await scanDevServers()
79
+ const patched = servers.find((s) => s.patched)
80
+ if (patched) return `http://localhost:${patched.port}/__soot/`
81
+ const any = servers[0]
82
+ if (any) return `http://localhost:${any.port}/__soot/`
83
+ return null
84
+ }
85
+
86
+ export async function runFlowPlayback(args: string[]): Promise<number> {
87
+ lastPreviewUploadResult = null
88
+ lastFlowTraceSteps = []
89
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
90
+ console.log(`
91
+ sootsim maestro — author and run Maestro YAML flows
92
+
93
+ usage:
94
+ sootsim maestro test <flow.yaml> [options]
95
+ sootsim maestro start
96
+ sootsim maestro keep
97
+ sootsim maestro end [--output <path>] [--validate] [--video]
98
+ sootsim maestro validate <path>
99
+
100
+ options:
101
+ --record record a webm artifact while the flow runs
102
+ --profile capture perf stats while the flow runs
103
+ --out <dir> output directory (default: /tmp/sootsim-recordings)
104
+ --device <model> override the flow frontmatter device for this run
105
+ --slow <ms> delay between steps for natural pacing
106
+ --tail-wait <ms> hold the recording open after the last step so the
107
+ final UI state is captured (default: smart idle up to
108
+ 6000ms for --preview, fixed 2000ms for --record, 0 otherwise)
109
+ --url <url> load this target before running the flow
110
+ --screenshot-paths <mode>
111
+ screenshot path mode: dir (default) or flow
112
+ --no-shell capture every screenshot tenant-only (no status bar,
113
+ keyboard, notification center, or other shell overlays).
114
+ use for clean demo asset sets. per-step \`layers:\`
115
+ still wins over this default.
116
+ --shell-only inverse — capture shell chrome only
117
+ --replace <m>=<f> replace a Metro module with a local file for this run
118
+ --new open a fresh sim before running the flow
119
+ --driver <id> when paired with --new, use playwright or electron;
120
+ browser sims default to isolated playwright
121
+ --headless pass headless=true to the launch driver (useful
122
+ with --driver playwright)
123
+ --headed force a fresh, VISIBLE window for this run (implies
124
+ --new; overrides --headless). use when you want to
125
+ watch the flow run live even if the current sim is
126
+ headless or stale
127
+ --electron prefer opening the target in the desktop companion
128
+ --sim <id> target a specific current sim
129
+ --validate replay the drafted flow before clearing it
130
+ --video replay and record the drafted flow before clearing it
131
+ --preview record events+video, upload, and print /preview/<id>
132
+ --preview-open open the uploaded preview link after a successful run
133
+ --preview-origin <url>
134
+ upload target for --preview (default: auto)
135
+ --preview-public-origin <url>
136
+ public link origin for --preview (default: sootsim.com
137
+ for prod uploads)
138
+ --billing-kind test_run
139
+ meter the chained preview upload as a test run
140
+ --owner <org> associate preview upload with a linked org repo
141
+ --repo <repo> repo name for --owner; user must belong to the team
142
+
143
+ frontmatter (top of flow.yaml between --- markers):
144
+ app: 8081 load this target before the flow starts
145
+ device: iphone-14 set the live device model for this flow run
146
+ electron: true prefer the desktop companion sim for this flow
147
+
148
+ default runtime:
149
+ bridge-backed flow playback against the current sim
150
+ if no current sim exists, flow opens the discovered target first
151
+ recording and profiling both run on the same bridge-backed path
152
+
153
+ examples:
154
+ sootsim maestro test login.yaml
155
+ sootsim maestro test demo.yaml --url 8081 --record --slow 500 --out ./recordings
156
+ sootsim maestro test demo.yaml --sim a2
157
+ sootsim maestro start
158
+ sootsim maestro keep
159
+ sootsim maestro end --output ./.maestro/draft.yaml
160
+ sootsim maestro end --output ./.maestro/draft.yaml --validate
161
+ sootsim maestro end --output ./.maestro/draft.yaml --video
162
+ sootsim maestro validate ./.maestro/draft.yaml
163
+
164
+ flow extension:
165
+ takeScreenshot: hero
166
+ takeScreenshot:
167
+ path: marketing/hero
168
+ withFrame: true
169
+ `)
170
+ process.exit(0)
171
+ }
172
+
173
+ const flowPath = args[0]
174
+ if (!fs.existsSync(flowPath)) {
175
+ console.error(` error: ${flowPath} not found`)
176
+ process.exit(1)
177
+ }
178
+
179
+ const getFlag = (name: string) => args.find((_, i) => args[i - 1] === name)
180
+ const profile = args.includes('--profile')
181
+ // --headed forces a fresh visible window: implies --new and overrides
182
+ // --headless (you asked to watch it run).
183
+ const headed = args.includes('--headed')
184
+ const openInNewSim = args.includes('--new') || headed
185
+ const outDir = getFlag('--out') || '/tmp/sootsim-recordings'
186
+ // when --screenshots isn't explicitly set, default to the --out directory.
187
+ // this matches what most users expect: one flag controls the whole output
188
+ // bundle (recording + screenshots) for a demo run.
189
+ // previously, screenshots silently landed in /tmp/sootsim-flow even when
190
+ // --out was set, requiring a second --screenshots flag to redirect them.
191
+ const screenshotDir =
192
+ getFlag('--screenshots') || (getFlag('--out') ? outDir : '/tmp/sootsim-flow')
193
+ const screenshotPathMode =
194
+ getFlag('--screenshot-paths') === 'flow' ? 'flow' : ('dir' as const)
195
+ // --no-shell makes every takeScreenshot in the flow capture tenant-only
196
+ // (no status bar, keyboard, notification center). useful for demo asset
197
+ // sets where shell chrome would obscure the app or stale overlays would
198
+ // pollute the result. per-step `layers:` still wins over this default.
199
+ const screenshotLayers: 'full' | 'tenant' | 'shell' | undefined = args.includes(
200
+ '--no-shell',
201
+ )
202
+ ? 'tenant'
203
+ : args.includes('--shell-only')
204
+ ? 'shell'
205
+ : undefined
206
+ const deviceOverride = getFlag('--device')?.trim() || ''
207
+ const slow = args.includes('--slow') ? +(getFlag('--slow') || '500') : 0
208
+ const cliElectron = args.includes('--electron')
209
+ const preview = args.includes('--preview')
210
+ // only --preview reads this (start event, entitlement origin, uploader).
211
+ // resolving it unconditionally probed https://contrast.localhost:3000 and
212
+ // charged every non-preview flow the probe's full 2s timeout whenever no
213
+ // local Contrast stack was listening.
214
+ const previewOrigin = preview
215
+ ? await resolveDefaultUploadOrigin(getFlag('--preview-origin'))
216
+ : ''
217
+ const previewPublicOrigin = getFlag('--preview-public-origin')
218
+ const previewOpen = args.includes('--preview-open')
219
+ const previewOwner = getFlag('--owner')?.trim() || ''
220
+ const previewRepo = getFlag('--repo')?.trim() || ''
221
+ const previewBillingKind = getFlag('--billing-kind')?.trim() || ''
222
+ const previewRunScope = preview
223
+ ? `maestro:${
224
+ process.env.GITHUB_RUN_ID
225
+ ? `github:${process.env.GITHUB_RUN_ID}:attempt:${process.env.GITHUB_RUN_ATTEMPT || '1'}:job:${process.env.GITHUB_JOB || 'preview'}`
226
+ : `local:${randomUUID()}`
227
+ }:flow:${createHash('sha256')
228
+ .update(path.relative(process.cwd(), path.resolve(flowPath)))
229
+ .digest('hex')
230
+ .slice(0, 16)}`
231
+ : ''
232
+ if (previewBillingKind && previewBillingKind !== 'test_run') {
233
+ console.error(` error: invalid --billing-kind: ${previewBillingKind}`)
234
+ process.exit(1)
235
+ }
236
+ const replaceArgs = args.flatMap((arg, index) =>
237
+ args[index - 1] === '--replace' ? ['--replace', arg] : [],
238
+ )
239
+ // forward --remap <host:port>=<host:port> to the open command so the sim
240
+ // boots with the guest-fetch network remap (config.network.remap).
241
+ const remapArgs = args.flatMap((arg, index) =>
242
+ args[index - 1] === '--remap' ? ['--remap', arg] : [],
243
+ )
244
+ // --preview implies --record: the preview page layers the mp4 over
245
+ // the canvas during scrub so the viewer sees smooth motion while the
246
+ // real app is catching up. upload chains the video in automatically.
247
+ const record = args.includes('--record') || preview
248
+ // hold the final UI state in the recording so viewers see the result
249
+ // of the flow, not just a frame mid-transition. short flows otherwise
250
+ // cut off ~600ms in. caller can override with --tail-wait <ms>.
251
+ const tailWaitMs = args.includes('--tail-wait')
252
+ ? Math.max(0, +(getFlag('--tail-wait') || '2000'))
253
+ : record
254
+ ? preview
255
+ ? 6000
256
+ : 2000
257
+ : 0
258
+
259
+ // parse flow file (frontmatter + steps). `${...}` templates stay verbatim
260
+ // here — the runner interpolates per step at execution time.
261
+ const flowContent = fs.readFileSync(flowPath, 'utf8')
262
+ const { frontmatter, steps } = parseFlowFile(flowContent)
263
+ if (steps.length === 0) {
264
+ console.error(' error: flow file must contain a YAML array of steps')
265
+ process.exit(1)
266
+ }
267
+ const targetDevice = deviceOverride || frontmatter.device || ''
268
+ const targetTheme = frontmatter.theme || ''
269
+
270
+ const wantElectron = cliElectron || frontmatter.electron === true
271
+ const rawTargetApp =
272
+ frontmatter.app === undefined || frontmatter.app === null
273
+ ? ''
274
+ : typeof frontmatter.app === 'number'
275
+ ? String(frontmatter.app)
276
+ : frontmatter.app
277
+ // accept `app: <demo-name>` (e.g. `app: 3pc`) in addition to ports/URLs.
278
+ // resolve via APPS — the launcher's preferredPort is the explicit target.
279
+ const targetApp = await resolveAppFrontmatterTarget(rawTargetApp)
280
+
281
+ const bridgeArgOptions = {
282
+ stripBooleanFlags: [
283
+ '--record',
284
+ '--profile',
285
+ '--electron',
286
+ '--new',
287
+ '--preview',
288
+ '--preview-open',
289
+ '--headless',
290
+ '--headed',
291
+ ],
292
+ stripValueFlags: [
293
+ '--out',
294
+ '--device',
295
+ '--slow',
296
+ '--tail-wait',
297
+ '--url',
298
+ '--replace',
299
+ '--remap',
300
+ '--screenshots',
301
+ '--screenshot-paths',
302
+ '--preview-origin',
303
+ '--preview-public-origin',
304
+ '--driver',
305
+ '--billing-kind',
306
+ '--owner',
307
+ '--repo',
308
+ ],
309
+ }
310
+ const parsedBridgeArgs = parseBridgeCliArgs(args, bridgeArgOptions)
311
+ const url = getFlag('--url') || ''
312
+ if (wantElectron && !findDesktopCompanion()) {
313
+ console.error(
314
+ ' error: desktop companion not found. install or build it first with `bun run build:electron`',
315
+ )
316
+ process.exit(1)
317
+ }
318
+
319
+ const mode = wantElectron ? 'desktop companion' : 'bridge'
320
+ const appLabel = targetApp ? ` | target: ${targetApp}` : ''
321
+ console.log(`
322
+ sootsim maestro — ${path.basename(flowPath)}
323
+ ${steps.length} steps | ${mode}${appLabel}${record ? ' | recording' : ''}${profile ? ' | profiling' : ''}${slow ? ` | ${slow}ms delay` : ''}
324
+ `)
325
+
326
+ // only meter flows that will result in a cloud preview upload. plain
327
+ // flow playback + local recording stay completely unmetered per the
328
+ // free/pro/enterprise beta plan (local usage is free). server emits
329
+ // the authoritative preview_upload events when the chained upload
330
+ // actually lands.
331
+ if (preview) {
332
+ trackCliEvent({
333
+ event: 'preview_flow_started',
334
+ properties: {
335
+ flowName: path.basename(flowPath),
336
+ stepCount: steps.length,
337
+ mode,
338
+ electron: wantElectron,
339
+ record,
340
+ profile,
341
+ previewOrigin,
342
+ },
343
+ })
344
+ }
345
+
346
+ let openTarget = url || targetApp || ''
347
+ const effectiveOpenInNewSim =
348
+ openInNewSim || (wantElectron && parsedBridgeArgs.simIdSource !== 'flag')
349
+ const driverFlag = getFlag('--driver') || ''
350
+ let closeManagedDriverSimAfterRun = false
351
+ const ownedSimIds = new Set<string>()
352
+
353
+ // if we weren't told exactly where to go, and there's already a primary
354
+ // open tab, just reuse it. opening a fresh tab that auto-discovers some
355
+ // other dev server on the host is almost never what the user wants — they
356
+ // already pointed the current tab at the app they want to drive.
357
+ let reuseActivePrimary = false
358
+ if (!openTarget && !effectiveOpenInNewSim && parsedBridgeArgs.simIdSource === 'none') {
359
+ const probe = createBridgeFromParsed(parsedBridgeArgs)
360
+ try {
361
+ const sims = await probe.listSims()
362
+ const primary = sims.find((b) => b.isPrimary && b.readyState === 'open')
363
+ const firstOpen = sims.find((b) => b.readyState === 'open')
364
+ const active = primary ?? firstOpen
365
+ if (active) {
366
+ reuseActivePrimary = true
367
+ console.log(` reusing active sim: ${active.id}`)
368
+ }
369
+ } catch {
370
+ // bridge not reachable — fall through to discovery
371
+ } finally {
372
+ probe.close()
373
+ }
374
+ }
375
+
376
+ if (
377
+ !openTarget &&
378
+ !reuseActivePrimary &&
379
+ (effectiveOpenInNewSim || parsedBridgeArgs.simIdSource === 'none')
380
+ ) {
381
+ const discovered = await discoverSootsimUrl()
382
+ if (!discovered) {
383
+ console.error(' error: no current sim and no sootsim target found')
384
+ await flushCliTelemetry()
385
+ process.exit(1)
386
+ }
387
+ openTarget = discovered
388
+ }
389
+
390
+ if (openTarget && targetDevice) {
391
+ // build against the shell that owns the bridge port this run will poll.
392
+ // resolve the shell from the already-selected bridge port so the flow
393
+ // cannot drive a sim in another live development stack.
394
+ const url = new URL(
395
+ await buildShellUrl(
396
+ openTarget,
397
+ resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
398
+ ),
399
+ )
400
+ url.searchParams.set('device', targetDevice)
401
+ openTarget = url.toString()
402
+ }
403
+
404
+ if (openTarget) {
405
+ const openArgs = [openTarget]
406
+ openArgs.push(...replaceArgs)
407
+ openArgs.push(...remapArgs)
408
+ if (effectiveOpenInNewSim) {
409
+ openArgs.push('--new')
410
+ }
411
+ openArgs.push('--no-describe')
412
+ if (parsedBridgeArgs.simIdSource === 'flag' && parsedBridgeArgs.simId) {
413
+ openArgs.push('--sim', parsedBridgeArgs.simId)
414
+ }
415
+ if (driverFlag) {
416
+ openArgs.push('--driver', driverFlag)
417
+ closeManagedDriverSimAfterRun = true
418
+ }
419
+ if (args.includes('--headless') && !headed) {
420
+ openArgs.push('--headless')
421
+ }
422
+ await runOpenCommand(openArgs, {
423
+ port: parsedBridgeArgs.wsPort,
424
+ timeoutMs: parsedBridgeArgs.commandTimeoutMs,
425
+ })
426
+ if (closeManagedDriverSimAfterRun) {
427
+ const openedSimId = readCurrentSimId()
428
+ if (openedSimId) ownedSimIds.add(openedSimId)
429
+ }
430
+ }
431
+
432
+ const resolvedBridgeArgs = parseBridgeCliArgs(args, bridgeArgOptions)
433
+ const pinnedSimId =
434
+ resolvedBridgeArgs.simIdSource === 'flag'
435
+ ? resolvedBridgeArgs.simId
436
+ : readCurrentSimId() || resolvedBridgeArgs.simId
437
+ // carry the source through so the target-sim notice reads `via --sim`
438
+ // (not the misleading `primary fallback — no sim pinned`) when the flow
439
+ // was actually pinned with --sim or a `sootsim use` selection.
440
+ const pinnedSimIdSource: typeof resolvedBridgeArgs.simIdSource =
441
+ resolvedBridgeArgs.simIdSource === 'flag' ? 'flag' : pinnedSimId ? 'saved' : 'none'
442
+ const bridge = createBridge(resolvedBridgeArgs.wsPort, {
443
+ commandTimeoutMs: resolvedBridgeArgs.commandTimeoutMs,
444
+ simId: pinnedSimId,
445
+ simIdSource: pinnedSimIdSource,
446
+ })
447
+ let driver: SootSimBridgeFlowRunner
448
+ const applyFlowFrontmatter = async () => {
449
+ const statusBarTime = frontmatter.env?.SOOTSIM_STATUS_BAR_TIME
450
+ if (statusBarTime) {
451
+ if (!/^(?:[1-9]|1[0-2]):[0-5][0-9]$/.test(statusBarTime)) {
452
+ throw new Error('SOOTSIM_STATUS_BAR_TIME must be a 12-hour time such as 9:41')
453
+ }
454
+ await bridge.send({
455
+ type: 'evaluate',
456
+ simId: driver.simId,
457
+ code: `window.dispatchEvent(new CustomEvent('sootsim:statusBarOverride', { detail: { time: ${JSON.stringify(statusBarTime)} } }))`,
458
+ })
459
+ }
460
+ if (targetTheme) {
461
+ await bridge.send({
462
+ type: 'call',
463
+ simId: driver.simId,
464
+ path: 'SootSim.bridges.settings.set',
465
+ args: ['colorScheme', targetTheme],
466
+ })
467
+ }
468
+ if (!targetDevice) return false
469
+ const currentUrl = await bridge.send({
470
+ type: 'evaluate',
471
+ simId: driver.simId,
472
+ code: 'window.location.href',
473
+ })
474
+ if (typeof currentUrl === 'string' && currentUrl.length > 0) {
475
+ const nextUrl = new URL(currentUrl)
476
+ nextUrl.searchParams.set('device', targetDevice)
477
+ driver.setSimRouteHint(nextUrl.toString())
478
+ }
479
+ return Boolean(
480
+ await bridge.send({
481
+ type: 'call',
482
+ simId: driver.simId,
483
+ path: 'SootSim.bridges.settings.set',
484
+ args: ['deviceModel', targetDevice],
485
+ }),
486
+ )
487
+ }
488
+ driver = new SootSimBridgeFlowRunner(bridge, {
489
+ screenshotDir,
490
+ flowDir: path.dirname(path.resolve(flowPath)),
491
+ screenshotPathMode,
492
+ screenshotLayers,
493
+ simId: pinnedSimId,
494
+ recordingOutputDir: record ? outDir : undefined,
495
+ recordingFormat: preview ? 'mp4' : 'webm',
496
+ // preview mode uploads to previewOrigin; entitlement check must
497
+ // talk to the same origin, not whatever the shared desktop sim
498
+ // happens to have cached.
499
+ billingOriginOverride: preview ? previewOrigin : undefined,
500
+ // any recording reached via a github installation token is a ci /
501
+ // pr-preview run, so allow it for preview uploads. local flow recording
502
+ // writes a file and matches `sootsim record --mode video`.
503
+ allowGitHubRecording: preview,
504
+ requireRecordingEntitlement: preview,
505
+ // re-arm the event recorder after every launchApp-with-reload. the
506
+ // recorder lives in-page state and gets wiped by window.location.reload.
507
+ onAfterLaunch: async () => {
508
+ const deviceChanged = await applyFlowFrontmatter()
509
+ if (deviceChanged) await driver.waitForTree(120000)
510
+ if (!preview) return
511
+ await bridge.send({
512
+ type: 'evaluate',
513
+ simId: driver.simId,
514
+ code: `(() => {
515
+ const r = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
516
+ return r?.start?.() ?? false
517
+ })()`,
518
+ })
519
+ },
520
+ })
521
+ if (slow) driver.stepDelay = slow
522
+ // live step rail + pause gate in the sim devtools "test" tab, named by the
523
+ // flow file so the rail identifies what is running.
524
+ driver.liveStatus = new FlowLiveStatusReporter(
525
+ bridge,
526
+ path.basename(flowPath),
527
+ () => driver.simId,
528
+ )
529
+ // maestro default vars + flow-level `env:` frontmatter, defined into the
530
+ // flow's JS context before the first step (upstream withDefaultEnvVars +
531
+ // DefineVariablesCommand).
532
+ driver.js.putEnv('MAESTRO_FILENAME', path.basename(flowPath, path.extname(flowPath)))
533
+ if (frontmatter.env && typeof frontmatter.env === 'object') {
534
+ for (const [key, value] of Object.entries(frontmatter.env)) {
535
+ driver.js.putEnv(key, value)
536
+ }
537
+ }
538
+
539
+ let profileResult: SootSimFlowProfileResult | null = null
540
+ let videoPath: string | null = null
541
+ let videoDurationMs: number | null = null
542
+ let capturedEvents: Array<Record<string, any>> | null = null
543
+ let previewScreenshotPath: string | null = null
544
+ let failureBundleDir: string | null = null
545
+ let exitCode = 0
546
+ const startsWithLaunchApp = !!steps[0]?.launchApp
547
+ try {
548
+ if (!startsWithLaunchApp) {
549
+ await driver.waitForTree(120000)
550
+ }
551
+ const deviceChanged = await applyFlowFrontmatter()
552
+ if (deviceChanged && !startsWithLaunchApp) await driver.waitForTree(120000)
553
+ // preview replay must reproduce the SAME initial conditions the agent
554
+ // explored from. the explore session runs on a fresh ephemeral profile
555
+ // (cold, logged-out), so its recorded steps assume that cold start —
556
+ // e.g. step 1 taps a dev/login button. but the replay tab reuses the
557
+ // explore tab's now-dirtied storage (persisted auth/session), so without
558
+ // a reset the app boots already-authenticated and the recorded login
559
+ // step targets an element that no longer exists — the flow hangs on
560
+ // step 1 ("made no progress … bridge or bundle probably hung"). flows
561
+ // that open with an explicit launchApp manage their own state; every
562
+ // other preview flow gets a clean cold launch here so the recording
563
+ // captures the same journey from the same starting point. app-agnostic,
564
+ // no per-repo configuration — the bot just figures it out.
565
+ // skip when the caller already established a clean cold state (e.g. the
566
+ // contrastbot run.sh pre-replay reset reopens an --ephemeral sim: fresh
567
+ // profile, logged-out, brought to readiness). a second clearState+reload
568
+ // here is redundant and triggers a sim-id rotation on the reload that
569
+ // routes the flow to a stale sim, hanging step 1 ("made no progress …
570
+ // bridge or bundle probably hung"). one reset is enough — the bot
571
+ // signals it did one via SOOTSIM_PREVIEW_RESET_DONE.
572
+ if (preview && !startsWithLaunchApp && !process.env.SOOTSIM_PREVIEW_RESET_DONE) {
573
+ await driver.launchApp({ clearState: true })
574
+ }
575
+ if (record) {
576
+ if (startsWithLaunchApp) {
577
+ driver.prepareRecording()
578
+ } else {
579
+ await driver.startRecording()
580
+ }
581
+ }
582
+ if (profile) {
583
+ await driver.startProfile()
584
+ }
585
+ if (preview) {
586
+ // start the preview event recorder on the tab so agent-driven taps
587
+ // (via sootsim:agentAction) land in the capture stream for replay.
588
+ // some shells expose the recorder via window.SootSim, others via
589
+ // the legacy alias only — try both.
590
+ const started = await bridge.send({
591
+ type: 'evaluate',
592
+ simId: driver.simId,
593
+ code: `(() => {
594
+ const r = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
595
+ return r?.start?.() ?? false
596
+ })()`,
597
+ })
598
+ if (!started) {
599
+ console.warn(
600
+ ' warn: eventRecorder.start() returned false — preview will upload bundle without replay events',
601
+ )
602
+ }
603
+ }
604
+ await driver.runFlow(steps)
605
+ if (profile) {
606
+ profileResult = await driver.stopProfile()
607
+ }
608
+ await driver.waitForRecordingTail({ maxMs: tailWaitMs, smart: preview })
609
+ if (record) {
610
+ videoPath = await driver.stopRecording()
611
+ videoDurationMs = driver.getLastRecordingDurationMs()
612
+ }
613
+ if (preview) {
614
+ capturedEvents = (await bridge.send({
615
+ type: 'evaluate',
616
+ simId: driver.simId,
617
+ code: `(() => {
618
+ const r = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
619
+ return r?.stop?.() ?? []
620
+ })()`,
621
+ })) as Array<Record<string, any>>
622
+ console.log(` captured ${capturedEvents.length} replay events`)
623
+ previewScreenshotPath = path.join(screenshotDir, 'preview-final.png')
624
+ await driver.captureScreenshot(previewScreenshotPath)
625
+ console.log(` final screenshot: ${previewScreenshotPath}`)
626
+ }
627
+
628
+ const screenshotSteps = steps.filter(
629
+ (s: any) => s && typeof s === 'object' && 'takeScreenshot' in s,
630
+ ).length
631
+ console.log(`\n + completed (${steps.length} steps)`)
632
+ if (screenshotSteps > 0) {
633
+ // surface where the assets actually landed so the user doesn't have to
634
+ // grep through verbose [flow] step output for paths
635
+ console.log(` screenshots: ${screenshotSteps} → ${screenshotDir}`)
636
+ }
637
+ if (profileResult) {
638
+ printFlowProfile(profileResult)
639
+ }
640
+ if (videoPath) {
641
+ console.log(` video: ${videoPath}`)
642
+ }
643
+ console.log()
644
+ } catch (err: any) {
645
+ if (profile && !profileResult) {
646
+ try {
647
+ profileResult = await driver.stopProfile()
648
+ } catch {
649
+ // ignore profiling teardown failures while already handling the main error
650
+ }
651
+ }
652
+ if (record && !videoPath) {
653
+ try {
654
+ videoPath = await driver.stopRecording()
655
+ videoDurationMs = driver.getLastRecordingDurationMs()
656
+ } catch (recordErr: any) {
657
+ console.warn(
658
+ ` recording stop failed: ${recordErr?.message || String(recordErr)}`,
659
+ )
660
+ }
661
+ }
662
+ if (preview && !capturedEvents) {
663
+ try {
664
+ capturedEvents = (await bridge.send({
665
+ type: 'evaluate',
666
+ simId: driver.simId,
667
+ code: `(() => {
668
+ const r = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
669
+ return r?.stop?.() ?? []
670
+ })()`,
671
+ })) as Array<Record<string, any>>
672
+ console.log(` captured ${capturedEvents.length} partial replay events`)
673
+ } catch (eventErr: any) {
674
+ console.warn(
675
+ ` event recorder stop failed: ${eventErr?.message || String(eventErr)}`,
676
+ )
677
+ }
678
+ }
679
+ console.error(`\n x failed: ${err.message}\n`)
680
+ if (profileResult) {
681
+ console.log(' partial profile:')
682
+ printFlowProfile(profileResult)
683
+ }
684
+ if (videoPath) {
685
+ console.log(` partial video: ${videoPath}`)
686
+ }
687
+ try {
688
+ // rich per-failure bundle: screenshot + describe.json + a11y +
689
+ // tree + console + failed-requests. dir is named by the failing
690
+ // step so consecutive failing runs don't overwrite each other.
691
+ const failedStep = driver.lastFailedStep
692
+ const bundleName = failedStep
693
+ ? `step-${String(failedStep.index + 1).padStart(2, '0')}-${failedStep.kind}`
694
+ : 'unstaged'
695
+ const bundleDir = path.join(screenshotDir, bundleName)
696
+ await driver.captureFailureBundle(bundleDir, {
697
+ error: err,
698
+ stepIndex: failedStep?.index,
699
+ stepKind: failedStep?.kind,
700
+ stepTarget: failedStep?.target,
701
+ })
702
+ failureBundleDir = bundleDir
703
+ console.log(` failure bundle: ${bundleDir}`)
704
+ console.log(
705
+ ` (contents: screenshot.png, describe.json, a11y.txt, tree.txt, console.json, error.json)`,
706
+ )
707
+ } catch (bundleErr) {
708
+ // fall back to the bare screenshot if the full bundle blew up —
709
+ // at least the viewer has one thing to look at.
710
+ try {
711
+ const errShot = path.join(screenshotDir, 'error.png')
712
+ await driver.captureScreenshot(errShot)
713
+ console.log(` error screenshot: ${errShot}`)
714
+ } catch {}
715
+ console.log(
716
+ ` (failure bundle capture failed: ${
717
+ bundleErr instanceof Error ? bundleErr.message : String(bundleErr)
718
+ })`,
719
+ )
720
+ }
721
+ await printBridgeFailureDiagnostics(bridge, {
722
+ errorsCommand: `sootsim get errors 5${driver.simId ? ` --sim ${driver.simId}` : ''}`,
723
+ warningsCommand: `sootsim get warnings 5${driver.simId ? ` --sim ${driver.simId}` : ''}`,
724
+ requestsCommand: `sootsim get requests 5${driver.simId ? ` --sim ${driver.simId}` : ''}`,
725
+ })
726
+ exitCode = 1
727
+ }
728
+
729
+ await driver.liveStatus?.end(exitCode === 0 ? 'passed' : 'failed')
730
+
731
+ // under --preview, kick the captured events into the preview uploader so the viewer
732
+ // gets a real /preview/<id> with scrubbable replay. failed flows upload the
733
+ // partial recording when available so dashboard runs can link to the trace.
734
+ if (preview && (exitCode === 0 || videoPath || capturedEvents)) {
735
+ try {
736
+ const eventsJsonlGz = await prepareEventsFile(capturedEvents ?? [])
737
+ const uploadArgs = ['--origin', previewOrigin, '--events', eventsJsonlGz]
738
+ if (previewPublicOrigin) {
739
+ uploadArgs.push('--public-origin', previewPublicOrigin)
740
+ }
741
+ // chain the recorded mp4 so preview-page can layer it over the
742
+ // canvas during scrub for smooth motion while the real app
743
+ // catches up.
744
+ if (videoPath) uploadArgs.push('--video', videoPath)
745
+ if (previewScreenshotPath) {
746
+ uploadArgs.push('--screenshot', previewScreenshotPath)
747
+ }
748
+ if (videoPath && videoDurationMs && Number.isFinite(videoDurationMs)) {
749
+ uploadArgs.push('--video-duration-ms', String(Math.round(videoDurationMs)))
750
+ uploadArgs.push('--recorded-duration-ms', String(Math.round(videoDurationMs)))
751
+ }
752
+ const recordingStartedAtMs = driver.getLastRecordingStartedAtMs()
753
+ if (recordingStartedAtMs && Number.isFinite(recordingStartedAtMs)) {
754
+ uploadArgs.push(
755
+ '--recording-started-at-ms',
756
+ String(Math.round(recordingStartedAtMs)),
757
+ )
758
+ }
759
+ if (parsedBridgeArgs.wsPort) {
760
+ uploadArgs.push('--port', String(parsedBridgeArgs.wsPort))
761
+ }
762
+ if (driver.simId) {
763
+ uploadArgs.push('--sim', driver.simId)
764
+ }
765
+ if (previewOpen) uploadArgs.push('--open')
766
+ if (previewOwner) uploadArgs.push('--owner', previewOwner)
767
+ if (previewRepo) uploadArgs.push('--repo', previewRepo)
768
+ uploadArgs.push('--run-scope', previewRunScope)
769
+ if (previewBillingKind) {
770
+ uploadArgs.push('--billing-kind', previewBillingKind)
771
+ }
772
+ console.log(`\n preparing preview upload…`)
773
+ const flowTraceSteps = driver.getFlowTraceSteps()
774
+ if (flowTraceSteps.length > 0) {
775
+ uploadArgs.push(
776
+ '--timeline-events',
777
+ await prepareFlowStepTimelineFile(flowTraceSteps),
778
+ )
779
+ }
780
+ const snapshotManifest = await prepareFlowSnapshotManifest(
781
+ flowTraceSteps,
782
+ recordingStartedAtMs,
783
+ )
784
+ if (snapshotManifest) {
785
+ uploadArgs.push('--snapshot-manifest', snapshotManifest)
786
+ }
787
+ if (failureBundleDir) {
788
+ // failed runs ship the per-step debug bundle with the share so the
789
+ // dashboard's run row can show the failing screen + full context.
790
+ uploadArgs.push('--failure-bundle', failureBundleDir)
791
+ }
792
+ const frameStats = driver.getLastRecordingFrameStats()
793
+ if (frameStats) {
794
+ uploadArgs.push('--trace-frame-stats', await prepareFrameStatsFile(frameStats))
795
+ }
796
+ const { runUpload } = await import('./upload')
797
+ lastPreviewUploadResult = await runUpload(uploadArgs, {})
798
+ } catch (err: any) {
799
+ console.error(` preview upload failed: ${err?.message || err}`)
800
+ exitCode = 1
801
+ }
802
+ }
803
+
804
+ lastFlowTraceSteps = driver.getFlowTraceSteps()
805
+
806
+ bridge.close()
807
+
808
+ if (closeManagedDriverSimAfterRun) {
809
+ if (driver.simId) ownedSimIds.add(driver.simId)
810
+ await closeFlowOwnedSims(parsedBridgeArgs.wsPort, parsedBridgeArgs.commandTimeoutMs, [
811
+ ...ownedSimIds,
812
+ ])
813
+ } else if (driver.simId && driver.simId !== pinnedSimId) {
814
+ // `launchApp clearState` rotates the sim id mid-flow (the shell reconnects
815
+ // under a fresh id). the runner tracks that in-memory, but a directory run
816
+ // (`sootsim maestro test <dir>`) spawns the next flow fresh — it re-reads
817
+ // the saved sim id, which would still point at the pre-rotation sim and
818
+ // fail with "sim not responding / stale orphan". persist the rotated id so
819
+ // the next flow reuses the live sim. only when we're NOT closing it.
820
+ saveCurrentSimId(driver.simId)
821
+ }
822
+
823
+ // flush before returning to caller — caller typically calls
824
+ // process.exit(returnedCode), and node's `exit` hook can't await, so
825
+ // without this the flow telemetry (and any chained upload's events)
826
+ // get silently dropped.
827
+ await flushCliTelemetry()
828
+ return exitCode
829
+ }
830
+
831
+ async function closeFlowOwnedSims(
832
+ wsPort: number,
833
+ commandTimeoutMs: number,
834
+ simIds: string[],
835
+ ): Promise<void> {
836
+ const ids = [...new Set(simIds.filter(Boolean))]
837
+ if (ids.length === 0) return
838
+ const bridge = createBridge(wsPort, { commandTimeoutMs })
839
+ try {
840
+ const sims = await bridge.listSims()
841
+ const openIds = ids.filter((id) =>
842
+ sims.some((sim) => sim.id === id && sim.readyState === 'open'),
843
+ )
844
+ if (openIds.length === 0) return
845
+ const result = await closeSimsBulk(bridge, wsPort, commandTimeoutMs, openIds)
846
+ await terminatePlaywrightHostsForSims(sims, openIds)
847
+ if (result.closed.length > 0) {
848
+ console.log(` closed flow sim(s): ${result.closed.join(', ')}`)
849
+ }
850
+ if (result.remaining.length > 0) {
851
+ console.warn(
852
+ ` warn: flow sim(s) still connected after close: ${result.remaining.join(', ')}`,
853
+ )
854
+ }
855
+ } catch (err) {
856
+ console.warn(
857
+ ` warn: failed to close flow sim(s): ${
858
+ err instanceof Error ? err.message : String(err)
859
+ }`,
860
+ )
861
+ } finally {
862
+ bridge.close()
863
+ }
864
+ }
865
+
866
+ // write the captured event stream to a gzipped jsonl file in a temp dir.
867
+ // returns the absolute path for the upload command to pick up.
868
+ async function prepareEventsFile(events: Array<Record<string, any>>): Promise<string> {
869
+ const { gzipSync } = await import('zlib')
870
+ const lines =
871
+ events.map((e) => JSON.stringify(e)).join('\n') + (events.length ? '\n' : '')
872
+ const gz = gzipSync(Buffer.from(lines, 'utf8'))
873
+ const outPath = path.join(tmpdir(), `sootsim-events-${Date.now()}.jsonl.gz`)
874
+ fs.writeFileSync(outPath, gz)
875
+ console.log(` events: ${events.length} written to ${outPath} (${gz.length} bytes gz)`)
876
+ return outPath
877
+ }
878
+
879
+ async function prepareFlowStepTimelineFile(
880
+ steps: SootSimFlowTraceStep[],
881
+ ): Promise<string> {
882
+ const { gzipSync } = await import('zlib')
883
+ const events = steps.map((step, index) => ({
884
+ schemaVersion: 1,
885
+ t: step.endedAtMs,
886
+ seq: 1_000_000 + index,
887
+ context: 'host',
888
+ kind: 'flow-step',
889
+ id: `flow-step-${step.stepIndex}`,
890
+ data: {
891
+ stepIndex: step.stepIndex,
892
+ stepNumber: step.stepIndex + 1,
893
+ stepName: step.stepName,
894
+ ...(step.targetLabel ? { targetLabel: step.targetLabel } : {}),
895
+ status: step.status,
896
+ durationMs: step.durationMs,
897
+ startedAtMs: step.startedAtMs,
898
+ endedAtMs: step.endedAtMs,
899
+ ...(step.error ? { error: step.error } : {}),
900
+ },
901
+ }))
902
+ const lines = events.map((event) => JSON.stringify(event)).join('\n') + '\n'
903
+ const gz = gzipSync(Buffer.from(lines, 'utf8'))
904
+ const outPath = path.join(tmpdir(), `sootsim-flow-steps-${Date.now()}.jsonl.gz`)
905
+ fs.writeFileSync(outPath, gz)
906
+ console.log(
907
+ ` flow trace: ${events.length} step events written to ${outPath} (${gz.length} bytes gz)`,
908
+ )
909
+ return outPath
910
+ }
911
+
912
+ async function prepareFlowSnapshotManifest(
913
+ steps: SootSimFlowTraceStep[],
914
+ recordingStartedAtMs: number | null,
915
+ ): Promise<string | null> {
916
+ if (!recordingStartedAtMs) return null
917
+ const snapshots = steps.flatMap((step) =>
918
+ step.screenshotPath
919
+ ? [
920
+ {
921
+ id: `flow-step-${step.stepIndex}`,
922
+ label: step.targetLabel ?? `Step ${step.stepIndex + 1}`,
923
+ kind: 'interaction',
924
+ t: Math.max(0, step.endedAtMs - recordingStartedAtMs),
925
+ path: step.screenshotPath,
926
+ },
927
+ ]
928
+ : [],
929
+ )
930
+ if (snapshots.length === 0) return null
931
+ const outPath = path.join(tmpdir(), `sootsim-flow-snapshots-${Date.now()}.json`)
932
+ fs.writeFileSync(outPath, JSON.stringify(snapshots))
933
+ console.log(` flow snapshots: ${snapshots.length} written to ${outPath}`)
934
+ return outPath
935
+ }
936
+
937
+ async function prepareFrameStatsFile(frameStats: unknown): Promise<string> {
938
+ const outPath = path.join(tmpdir(), `sootsim-frame-stats-${Date.now()}.json`)
939
+ fs.writeFileSync(outPath, JSON.stringify(frameStats))
940
+ console.log(` frame stats: written to ${outPath}`)
941
+ return outPath
942
+ }
943
+
944
+ async function runFlowSession(args: string[]) {
945
+ const subcommand = args[0]
946
+ const getFlag = (name: string) => args.find((_, i) => args[i - 1] === name)
947
+
948
+ switch (subcommand) {
949
+ case 'start': {
950
+ const { path, state } = startFlowSession()
951
+ console.log(` flow draft started`)
952
+ console.log(` session: ${path}`)
953
+ console.log(` steps: ${state.steps.length}`)
954
+ return
955
+ }
956
+
957
+ case 'keep':
958
+ case 'good': {
959
+ const result = keepFlowCandidate()
960
+ if (!result.active) {
961
+ console.error(' no active flow draft — run `sootsim maestro start` first')
962
+ process.exit(1)
963
+ }
964
+ if (!result.kept) {
965
+ console.log(' no pending action to keep')
966
+ return
967
+ }
968
+ console.log(` kept: ${result.candidate.summary}`)
969
+ console.log(` steps: ${result.stepCount}`)
970
+ return
971
+ }
972
+
973
+ case 'end': {
974
+ const outputPath =
975
+ getFlag('--output') || (args[1] && !args[1].startsWith('-') ? args[1] : undefined)
976
+ const validate = args.includes('--validate') || args.includes('--video')
977
+ const video = args.includes('--video')
978
+ // ending the draft commits the trailing pending action. the last
979
+ // successful `do` is presumably one the user wants; without this,
980
+ // `maestro start → do → maestro end` (no explicit keep) exported an empty
981
+ // [] and silently dropped the work (F13-4).
982
+ const autoKept = keepFlowCandidate()
983
+ if (autoKept.active && autoKept.kept) {
984
+ console.log(` auto-kept trailing action: ${autoKept.candidate.summary}`)
985
+ }
986
+ const result = exportFlowSession(
987
+ outputPath || (validate ? createDraftValidationPath() : undefined),
988
+ )
989
+ if (!result.active) {
990
+ console.error(' no active flow draft — run `sootsim maestro start` first')
991
+ process.exit(1)
992
+ }
993
+ if (!result.valid) {
994
+ console.error(' flow draft is not valid:')
995
+ for (const issue of result.issues) console.error(` - ${issue}`)
996
+ console.error(
997
+ ' draft preserved — keep at least one real interaction or run `sootsim maestro start` to reset',
998
+ )
999
+ process.exit(1)
1000
+ }
1001
+
1002
+ if (validate) {
1003
+ const playbackArgs = buildFlowValidationArgs(args, result.outputPath)
1004
+ const exitCode = await runFlowPlayback(playbackArgs)
1005
+ if (exitCode !== 0) {
1006
+ console.error(
1007
+ '\n validation failed — draft preserved so you can keep iterating',
1008
+ )
1009
+ process.exitCode = exitCode
1010
+ return
1011
+ }
1012
+
1013
+ finalizeFlowSession()
1014
+ console.log(
1015
+ ` flow draft validated (${result.stepCount} step${result.stepCount === 1 ? '' : 's'})`,
1016
+ )
1017
+ if (result.outputPath) {
1018
+ console.log(` saved: ${result.outputPath}`)
1019
+ }
1020
+ if (video) {
1021
+ console.log(' video: recorded during validation run')
1022
+ }
1023
+ return
1024
+ }
1025
+
1026
+ finalizeFlowSession()
1027
+ console.log(
1028
+ ` flow draft ended (${result.stepCount} step${result.stepCount === 1 ? '' : 's'})`,
1029
+ )
1030
+ if (result.outputPath) {
1031
+ console.log(` saved: ${result.outputPath}`)
1032
+ console.log(` next: sootsim maestro test ${result.outputPath} --record`)
1033
+ return
1034
+ }
1035
+ console.log('')
1036
+ process.stdout.write(result.yaml)
1037
+ return
1038
+ }
1039
+
1040
+ case 'validate': {
1041
+ const flowPath = args[1]
1042
+ if (!flowPath || flowPath.startsWith('-')) {
1043
+ console.error(' usage: sootsim maestro validate <path>')
1044
+ process.exit(1)
1045
+ }
1046
+ const issues = validateFlowFile(flowPath)
1047
+ if (issues.length > 0) {
1048
+ console.error(` x ${flowPath} failed validation:`)
1049
+ for (const issue of issues) console.error(` - ${issue}`)
1050
+ process.exit(1)
1051
+ }
1052
+ console.log(` + ${flowPath} looks valid`)
1053
+ return
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ // the subcommand / flow-file path is the first positional arg, but a
1059
+ // `--sim <id>` pair can sit in front of it: bin.ts prepends the global
1060
+ // `--sim` to every command's args, and `sootsim maestro --sim a2 start` is a
1061
+ // documented form. hoist a leading `--sim <value>` to the end so `args[0]`
1062
+ // is the real subcommand again (downstream getFlag / parseBridgeCliArgs
1063
+ // still find `--sim` wherever it lands). without this, `--sim 6a5 flow
1064
+ // start` parsed `--sim` as the flow path and failed `--sim not found`.
1065
+ export function hoistLeadingSimFlag(args: string[]): string[] {
1066
+ if (args[0] === '--sim' && args.length >= 2) {
1067
+ return [...args.slice(2), '--sim', args[1]]
1068
+ }
1069
+ return args
1070
+ }
1071
+
1072
+ export async function runMaestroAuthoring(rawArgs: string[]): Promise<number> {
1073
+ const args = hoistLeadingSimFlag(rawArgs)
1074
+ const subcommand = args[0]
1075
+ if (
1076
+ subcommand === 'start' ||
1077
+ subcommand === 'keep' ||
1078
+ subcommand === 'good' ||
1079
+ subcommand === 'end' ||
1080
+ subcommand === 'validate'
1081
+ ) {
1082
+ await runFlowSession(args)
1083
+ return 0
1084
+ }
1085
+
1086
+ const exitCode = await runFlowPlayback(args)
1087
+ if (exitCode !== 0) {
1088
+ process.exitCode = exitCode
1089
+ }
1090
+ return exitCode
1091
+ }
1092
+
1093
+ function printFlowProfile(result: SootSimFlowProfileResult) {
1094
+ console.log('')
1095
+ printShellPerfReport(result)
1096
+ }
1097
+ function createDraftValidationPath() {
1098
+ return path.join(tmpdir(), `sootsim-flow-draft-${Date.now()}.yaml`)
1099
+ }
1100
+
1101
+ function buildFlowValidationArgs(args: string[], outputPath: string | null) {
1102
+ if (!outputPath) {
1103
+ throw new Error('validated flow draft requires an output path')
1104
+ }
1105
+
1106
+ const nextArgs = [outputPath]
1107
+ for (let i = 1; i < args.length; i++) {
1108
+ const arg = args[i]
1109
+ if (arg === '--validate' || arg === '--video') continue
1110
+ if (arg === '--output') {
1111
+ i += 1
1112
+ continue
1113
+ }
1114
+ if (i === 1 && !arg.startsWith('-')) {
1115
+ continue
1116
+ }
1117
+ nextArgs.push(arg)
1118
+ }
1119
+
1120
+ if (!nextArgs.includes('--record') && args.includes('--video')) {
1121
+ nextArgs.push('--record')
1122
+ }
1123
+ return nextArgs
1124
+ }