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,204 @@
1
+ import { execFileSync } from 'child_process'
2
+ import { readFileSync } from 'fs'
3
+ import { createRequire } from 'module'
4
+
5
+ // look up a process's parent pid. returns null if we can't read it
6
+ // (unsupported platform, process gone, permission denied).
7
+ //
8
+ // used by session-identity to walk one hop up from `process.ppid` — in a
9
+ // Claude Code Bash tool the shell's parent is the agent-level process,
10
+ // which is stable across invocations and distinct between agents.
11
+
12
+ type Lookup = (pid: number) => number | null
13
+ type CommLookup = (pid: number) => string | null
14
+
15
+ let cached: Lookup | null = null
16
+ let cachedComm: CommLookup | null = null
17
+
18
+ export function getParentPid(pid: number): number | null {
19
+ if (!cached) cached = build()
20
+ try {
21
+ return cached(pid)
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ // short command name (comm) for a pid, or null when unreadable. used by the
28
+ // owner-pid walk to stop at effectively-immortal session hosts (tmux/screen
29
+ // servers and the Team Machine daemon), which must never become a sim owner.
30
+ export function getProcessComm(pid: number): string | null {
31
+ if (!cachedComm) cachedComm = buildComm()
32
+ try {
33
+ return cachedComm(pid)
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ function buildComm(): CommLookup {
40
+ if (process.platform === 'linux') {
41
+ return (pid) => {
42
+ try {
43
+ return readFileSync(`/proc/${pid}/comm`, 'utf8').trim() || null
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+ }
49
+ if (process.platform === 'darwin') {
50
+ const ffi = tryBuildDarwinCommFfi()
51
+ if (ffi) return ffi
52
+ return (pid) => readProcessTable().get(pid)?.comm || null
53
+ }
54
+ return () => null
55
+ }
56
+
57
+ function build(): Lookup {
58
+ // on any platform, if the shell exports $PPID and it differs from the
59
+ // node-level process.ppid, it's the shell's own parent — one level up
60
+ // from where we need to be. this is the cheapest and most portable
61
+ // grand-ppid source since it needs no subprocess or FFI.
62
+ const envPpid = Number(process.env.PPID)
63
+ if (Number.isFinite(envPpid) && envPpid > 1 && envPpid !== process.ppid) {
64
+ return () => envPpid
65
+ }
66
+
67
+ if (process.platform === 'linux') return buildLinux()
68
+ if (process.platform === 'darwin') {
69
+ // on Bun: libproc via FFI is ~0.8µs/call. on Node: one `ps` snapshot of
70
+ // the whole tree (see readProcessTable).
71
+ const ffi = tryBuildDarwinFfi()
72
+ if (ffi) return ffi
73
+ return (pid) => {
74
+ const ppid = readProcessTable().get(pid)?.ppid
75
+ return ppid !== undefined && ppid > 0 ? ppid : null
76
+ }
77
+ }
78
+ return () => null
79
+ }
80
+
81
+ // one snapshot of the whole process tree, shared by the ppid and comm lookups.
82
+ // macOS has no /proc and Node cannot call libproc, so each lookup used to shell
83
+ // out to `lsof -R -p <pid>` with a 2000ms budget — measured at 3.5s, 7.1s and
84
+ // 7.9s on a loaded box, so every hop of an ancestor walk timed out, the walk
85
+ // resolved to null, and `contrast ide open` launched a browser host with an
86
+ // empty owner pid that `ide close` could never attribute. `ps -eo pid,ppid,comm`
87
+ // answers for every process at once in ~0.1-0.9s. the short TTL keeps one walk
88
+ // on one snapshot while a long-lived caller still sees the tree change.
89
+ const PROCESS_TABLE_TTL_MS = 2000
90
+ let processTable: { readAt: number; rows: Map<number, { ppid: number; comm: string }> } =
91
+ { readAt: 0, rows: new Map() }
92
+
93
+ function readProcessTable(): Map<number, { ppid: number; comm: string }> {
94
+ const now = Date.now()
95
+ if (processTable.readAt && now - processTable.readAt < PROCESS_TABLE_TTL_MS) {
96
+ return processTable.rows
97
+ }
98
+ const rows = new Map<number, { ppid: number; comm: string }>()
99
+ try {
100
+ const out = execFileSync('ps', ['-eo', 'pid=,ppid=,comm='], {
101
+ encoding: 'utf8',
102
+ timeout: 5000,
103
+ maxBuffer: 8 * 1024 * 1024,
104
+ stdio: ['ignore', 'pipe', 'ignore'],
105
+ })
106
+ for (const line of out.split('\n')) {
107
+ const parsed = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/)
108
+ if (!parsed) continue
109
+ // libproc reports pbi_comm — the executable's basename, truncated to
110
+ // MAXCOMLEN — and the immortal-host patterns are written against that
111
+ // shape, while `ps -o comm` prints the full path. normalize to one.
112
+ rows.set(Number(parsed[1]), {
113
+ ppid: Number(parsed[2]),
114
+ comm: (parsed[3].split('/').at(-1) ?? '').slice(0, PBI_COMM_LEN),
115
+ })
116
+ }
117
+ } catch {
118
+ // an unreadable table means unresolved ancestry, which callers already
119
+ // treat as "no defensible owner" and refuse on.
120
+ }
121
+ processTable = { readAt: now, rows }
122
+ return rows
123
+ }
124
+
125
+ function buildLinux(): Lookup {
126
+ return (pid) => {
127
+ try {
128
+ const raw = readFileSync(`/proc/${pid}/stat`, 'utf8')
129
+ const end = raw.lastIndexOf(')')
130
+ if (end < 0) return null
131
+ const rest = raw
132
+ .slice(end + 1)
133
+ .trim()
134
+ .split(/\s+/)
135
+ const ppid = Number(rest[1])
136
+ return Number.isFinite(ppid) && ppid > 0 ? ppid : null
137
+ } catch {
138
+ return null
139
+ }
140
+ }
141
+ }
142
+
143
+ // PROC_PIDTBSDINFO flavor for proc_pidinfo. struct proc_bsdinfo is 216 bytes;
144
+ // pbi_ppid is at offset 16 (after pbi_flags, pbi_status, pbi_xstatus, pbi_pid,
145
+ // each 4 bytes). source: <sys/proc_info.h> in the macOS SDK.
146
+ const PROC_PIDTBSDINFO = 3
147
+ const PROC_BSDINFO_SIZE = 216
148
+ const PBI_PPID_OFFSET = 16
149
+ // char pbi_comm[MAXCOMLEN] — after the 4-byte flags/status/xstatus/pid/ppid/
150
+ // uid/gid/ruid/rgid/svuid/svgid/rfu_1 fields. source: <sys/proc_info.h>.
151
+ const PBI_COMM_OFFSET = 48
152
+ const PBI_COMM_LEN = 16
153
+
154
+ function tryBuildDarwinFfi(): Lookup | null {
155
+ if (!process.versions?.bun) return null
156
+ try {
157
+ const r = createRequire(import.meta.url)
158
+ const { dlopen, FFIType } = r('bun:ffi')
159
+ const lib = dlopen('/usr/lib/libproc.dylib', {
160
+ proc_pidinfo: {
161
+ args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
162
+ returns: FFIType.i32,
163
+ },
164
+ })
165
+ const buf = Buffer.alloc(PROC_BSDINFO_SIZE)
166
+ return (pid) => {
167
+ const ret = Number(
168
+ lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE),
169
+ )
170
+ if (ret <= 0) return null
171
+ const ppid = buf.readUInt32LE(PBI_PPID_OFFSET)
172
+ return ppid > 0 ? ppid : null
173
+ }
174
+ } catch {
175
+ return null
176
+ }
177
+ }
178
+
179
+ function tryBuildDarwinCommFfi(): CommLookup | null {
180
+ if (!process.versions?.bun) return null
181
+ try {
182
+ const r = createRequire(import.meta.url)
183
+ const { dlopen, FFIType } = r('bun:ffi')
184
+ const lib = dlopen('/usr/lib/libproc.dylib', {
185
+ proc_pidinfo: {
186
+ args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
187
+ returns: FFIType.i32,
188
+ },
189
+ })
190
+ const buf = Buffer.alloc(PROC_BSDINFO_SIZE)
191
+ return (pid) => {
192
+ const ret = Number(
193
+ lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE),
194
+ )
195
+ if (ret <= 0) return null
196
+ const raw = buf.subarray(PBI_COMM_OFFSET, PBI_COMM_OFFSET + PBI_COMM_LEN)
197
+ const end = raw.indexOf(0)
198
+ const comm = raw.subarray(0, end < 0 ? raw.length : end).toString('utf8')
199
+ return comm || null
200
+ }
201
+ } catch {
202
+ return null
203
+ }
204
+ }
@@ -0,0 +1,211 @@
1
+ // shared CLI argument parser — handles global flags, commands, and per-command flags
2
+
3
+ export interface ParsedArgs {
4
+ command: string | null
5
+ commandArgs: string[]
6
+ globalFlags: Record<string, string | boolean | number>
7
+ help: boolean
8
+ version: boolean
9
+ verbose: boolean
10
+ }
11
+
12
+ const GLOBAL_FLAGS: Record<
13
+ string,
14
+ { type: 'string' | 'boolean' | 'number'; short?: string }
15
+ > = {
16
+ '--help': { type: 'boolean', short: '-h' },
17
+ '--version': { type: 'boolean', short: '-V' },
18
+ '--verbose': { type: 'boolean', short: '-v' },
19
+ '--port': { type: 'number', short: '-p' },
20
+ '--device': { type: 'string', short: '-d' },
21
+ '--theme': { type: 'string', short: '-t' },
22
+ '--headless': { type: 'boolean' },
23
+ '--driver': { type: 'string' },
24
+ '--sim': { type: 'string' },
25
+ // documented aliases for --sim (CLAUDE.md / skills / engine docs say
26
+ // `--session <id>`). normalized to the sim target in bin.ts.
27
+ '--session': { type: 'string' },
28
+ '--tab': { type: 'string' },
29
+ }
30
+
31
+ // top-level verbs that route into runInspect. the surface is intentionally
32
+ // tiny — everything else lives under one of the grouping verbs:
33
+ // - `sootsim get <noun>` reads
34
+ // - `sootsim do <action>` writes
35
+ // - `sootsim debug <tool>` instrumentation (handled by runDebug)
36
+ // - `sootsim inspect <sub>` full legacy surface, all subcommands
37
+ // `describe` and `find` stay at root as the most common entrypoints.
38
+ // `list` stays at root because it's a sim-management read.
39
+ export const TOP_LEVEL_RUNTIME_COMMANDS = new Set([
40
+ 'list',
41
+ 'describe',
42
+ 'find',
43
+ 'get',
44
+ 'do',
45
+ 'wait',
46
+ 'network',
47
+ 'logs',
48
+ 'shell',
49
+ ])
50
+
51
+ // grouping verbs that take a sub-verb (`sootsim wait ready`,
52
+ // `sootsim do tap-id`). agents and humans very often type the hyphenated
53
+ // form (`sootsim wait-ready`, `sootsim do-tap-id`) — it reads as one
54
+ // command and matches the typed-tool names (rnx_wait_ready). bin.ts
55
+ // normalizes `<verb>-<rest>` → `<verb> <rest>` for these so the intuitive
56
+ // spelling just works instead of failing as "unknown command".
57
+ export const HYPHENATED_GROUPING_VERBS = new Set(['do', 'get', 'wait', 'shell', 'debug'])
58
+
59
+ // known commands — if first positional arg matches, it's a command
60
+ const COMMANDS = new Set([
61
+ 'detox',
62
+ 'maestro',
63
+ 'record',
64
+ 'film',
65
+ 'storage',
66
+ 'permissions',
67
+ 'perf',
68
+ 'screenshot',
69
+ 'state',
70
+ 'reset',
71
+ 'camera',
72
+ 'mode',
73
+ 'inspect',
74
+ 'debug',
75
+ 'timeline',
76
+ 'what-happened',
77
+ 'shell',
78
+ 'assert',
79
+ 'device',
80
+ 'open',
81
+ 'list',
82
+ 'use',
83
+ 'claim',
84
+ 'close',
85
+ 'compat',
86
+ 'report-issue',
87
+ 'desktop',
88
+ 'login',
89
+ 'logout',
90
+ 'auth',
91
+ 'setup',
92
+ 'serve',
93
+ 'daemon',
94
+ 'runtime',
95
+ 'upgrade',
96
+ 'update',
97
+ 'version',
98
+ 'skill',
99
+ 'config',
100
+ 'cleanup',
101
+ // stage config-plugin fonts into a build's asset manifest (run.sh build path)
102
+ 'app-fonts',
103
+ // internal — not user-facing, spawned as a subprocess by electron main
104
+ // and by `sootsim agent start`.
105
+ 'agent-wrapper',
106
+ // attached projects + local agent sessions.
107
+ 'agent',
108
+ ...TOP_LEVEL_RUNTIME_COMMANDS,
109
+ ])
110
+
111
+ export function parseArgs(argv: string[]): ParsedArgs {
112
+ const args = argv.slice(2)
113
+ const result: ParsedArgs = {
114
+ command: null,
115
+ commandArgs: [],
116
+ globalFlags: {},
117
+ help: false,
118
+ version: false,
119
+ verbose: false,
120
+ }
121
+
122
+ let i = 0
123
+
124
+ // parse global flags and detect command
125
+ while (i < args.length) {
126
+ const arg = args[i]
127
+
128
+ // explicit separator — strip it, everything after is command args
129
+ if (arg === '--') {
130
+ result.commandArgs.push(...args.slice(i + 1))
131
+ break
132
+ }
133
+
134
+ // check for global flag
135
+ const flagEntry = Object.entries(GLOBAL_FLAGS).find(
136
+ ([name, def]) => name === arg || def.short === arg,
137
+ )
138
+
139
+ if (flagEntry) {
140
+ const [name, def] = flagEntry
141
+ const key = name.replace(/^--/, '')
142
+ if (def.type === 'boolean') {
143
+ result.globalFlags[key] = true
144
+ i++
145
+ } else {
146
+ const val = args[i + 1]
147
+ if (val === undefined || val.startsWith('-')) {
148
+ // missing value — treat as error, pass through
149
+ console.error(` warning: ${arg} requires a value`)
150
+ i++
151
+ continue
152
+ }
153
+ if (def.type === 'number') {
154
+ const num = Number(val)
155
+ if (Number.isNaN(num)) {
156
+ console.error(` warning: ${arg} requires a number, got "${val}"`)
157
+ i += 2
158
+ continue
159
+ }
160
+ result.globalFlags[key] = num
161
+ } else {
162
+ result.globalFlags[key] = val
163
+ }
164
+ i += 2
165
+ }
166
+ continue
167
+ }
168
+
169
+ // first non-flag arg — is it a command?
170
+ if (!result.command && !arg.startsWith('-')) {
171
+ // hyphenated grouping-verb spelling (`wait-ready`, `do-tap-id`):
172
+ // agents and humans type it constantly (it reads as one command and
173
+ // mirrors the typed tool names). split only the FIRST hyphen so
174
+ // multi-segment sub-verbs survive (`do-tap-id` → `do` `tap-id`).
175
+ // without this it fell through to the unrecognized branch as
176
+ // "unknown command: wait-ready" with empty stdout.
177
+ const dash = arg.indexOf('-')
178
+ if (dash > 0) {
179
+ const verb = arg.slice(0, dash)
180
+ const sub = arg.slice(dash + 1)
181
+ if (HYPHENATED_GROUPING_VERBS.has(verb) && sub.length > 0) {
182
+ result.command = verb
183
+ const rest = args.slice(i + 1)
184
+ result.commandArgs = [sub, ...(rest[0] === '--' ? rest.slice(1) : rest)]
185
+ break
186
+ }
187
+ }
188
+ if (COMMANDS.has(arg)) {
189
+ result.command = arg
190
+ const rest = args.slice(i + 1)
191
+ // strip leading -- separator if present
192
+ result.commandArgs = rest[0] === '--' ? rest.slice(1) : rest
193
+ break
194
+ }
195
+ // not a recognized command — leave it as command args so bin.ts can
196
+ // print a focused unknown-command message.
197
+ result.commandArgs = args.slice(i)
198
+ break
199
+ }
200
+
201
+ // unrecognized flag — pass through to command
202
+ result.commandArgs.push(arg)
203
+ i++
204
+ }
205
+
206
+ result.help = !!result.globalFlags['help']
207
+ result.version = !!result.globalFlags['version']
208
+ result.verbose = !!result.globalFlags['verbose']
209
+
210
+ return result
211
+ }
package/cli/prompt.ts ADDED
@@ -0,0 +1,51 @@
1
+ // minimal readline prompts — no external dependencies
2
+
3
+ import { createInterface } from 'readline'
4
+
5
+ const rl = () =>
6
+ createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout,
9
+ })
10
+
11
+ function ask(question: string): Promise<string> {
12
+ return new Promise((resolve) => {
13
+ const r = rl()
14
+ r.question(question, (answer) => {
15
+ r.close()
16
+ resolve(answer.trim())
17
+ })
18
+ })
19
+ }
20
+
21
+ export async function confirm(message: string, defaultYes = true): Promise<boolean> {
22
+ const hint = defaultYes ? '[Y/n]' : '[y/N]'
23
+ // non-interactive stdin (piped, /dev/null, closed) — readline's question
24
+ // callback would never fire, leaving the await unsettled and the process
25
+ // exiting 0 with a `Detected unsettled top-level await` warning. degrade
26
+ // to the default instead so scripted invocations behave predictably.
27
+ if (!process.stdin.isTTY) {
28
+ console.log(` ${message} ${hint} ${defaultYes ? 'y' : 'n'} (non-interactive)`)
29
+ return defaultYes
30
+ }
31
+ const answer = await ask(` ${message} ${hint} `)
32
+ if (answer === '') return defaultYes
33
+ return answer.toLowerCase().startsWith('y')
34
+ }
35
+
36
+ export async function select(message: string, options: string[]): Promise<number> {
37
+ console.log(`\n ${message}\n`)
38
+ for (let i = 0; i < options.length; i++) {
39
+ console.log(` ${i + 1}. ${options[i]}`)
40
+ }
41
+ console.log()
42
+ // non-interactive stdin — pick the first option rather than hanging.
43
+ if (!process.stdin.isTTY) {
44
+ console.log(` choose [1-${options.length}]: 1 (non-interactive)`)
45
+ return 0
46
+ }
47
+ const answer = await ask(` choose [1-${options.length}]: `)
48
+ const num = parseInt(answer, 10)
49
+ if (num >= 1 && num <= options.length) return num - 1
50
+ return 0 // default to first
51
+ }
@@ -0,0 +1,107 @@
1
+ import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
2
+ import { authHeaderValue, resolveCliAuth, type CliAuth } from './auth'
3
+ import { resolveDefaultUploadOrigin } from './commands/upload'
4
+
5
+ type BillingSubscriptionSnapshot = {
6
+ plan?: 'free' | 'personal' | 'team'
7
+ entitlements?: {
8
+ desktopRecording?: boolean
9
+ }
10
+ }
11
+
12
+ async function billingOriginFor(auth: CliAuth, originOverride?: string): Promise<string> {
13
+ // an explicit override (e.g. --preview-origin https://contrast.localhost:3000 from
14
+ // `maestro test --preview`) wins over whatever origin the shared auth session
15
+ // happens to remember. this keeps local preview-upload flows pointed
16
+ // at the local billing endpoint even when the desktop session token
17
+ // was last minted against contrast.
18
+ if (originOverride) return originOverride.replace(/\/$/, '')
19
+ if (auth.kind === 'session' && auth.origin) {
20
+ return auth.origin.replace(/\/$/, '')
21
+ }
22
+ const shared = readSharedDesktopAuthSession()
23
+ if (shared?.origin) {
24
+ return shared.origin.replace(/\/$/, '')
25
+ }
26
+ // api-key auth (RNX_API_KEY) carries no origin.
27
+ // resolve it the same way the upload itself does — probe a local Contrast
28
+ // stack on :3000, else fall back to prod. this keeps the entitlement
29
+ // check pointed at the *same* origin the recording will upload to (the
30
+ // engine resolves loopback sims to the local :3000 stack); checking prod
31
+ // billing with a key minted against the local stack would 401 here even
32
+ // though the upload would have succeeded.
33
+ return (await resolveDefaultUploadOrigin()).replace(/\/$/, '')
34
+ }
35
+
36
+ export interface RecordingEntitlementOptions {
37
+ /** override the origin used to check billing. when the CLI is running
38
+ * against a local Contrast stack (e.g. `maestro test --preview --preview-origin
39
+ * https://contrast.localhost:3000`), the recording entitlement needs to hit the
40
+ * same origin that'll receive the upload — not the remote contrast
41
+ * origin stored in the shared desktop session. */
42
+ originOverride?: string
43
+ /** allow GitHub installation/action auth to record for pr preview uploads.
44
+ * this is intentionally opt-in so local `sootsim record` stays tied to
45
+ * the user's paid/trial desktop entitlement. */
46
+ allowGitHubAuth?: boolean
47
+ }
48
+
49
+ export async function ensureCliRecordingEntitlement(
50
+ commandLabel: string,
51
+ opts: RecordingEntitlementOptions = {},
52
+ ): Promise<void> {
53
+ const auth = resolveCliAuth()
54
+ if (!auth) {
55
+ process.stderr.write(
56
+ `\n rnx ${commandLabel} needs auth before it can check recording access.\n\n` +
57
+ ` pick one:\n` +
58
+ ` • run \`rnxsim login\`\n` +
59
+ ` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ if (auth.kind === 'github' && opts.allowGitHubAuth) return
65
+
66
+ const billingOrigin = await billingOriginFor(auth, opts.originOverride)
67
+ let res: Response
68
+ try {
69
+ res = await fetch(`${billingOrigin}/api/sootsim/billing/subscription`, {
70
+ headers: { authorization: authHeaderValue(auth) },
71
+ })
72
+ } catch (error) {
73
+ process.stderr.write(
74
+ `\n rnx ${commandLabel} couldn't verify recording access.\n` +
75
+ ` ${error instanceof Error ? error.message : String(error)}\n\n`,
76
+ )
77
+ process.exit(1)
78
+ }
79
+
80
+ if (res.status === 401) {
81
+ process.stderr.write(
82
+ `\n rnx ${commandLabel} couldn't verify recording access with the current auth against ${billingOrigin}.\n` +
83
+ ` sign in again for that origin, or provide a valid RNX_API_KEY.\n\n`,
84
+ )
85
+ process.exit(1)
86
+ }
87
+
88
+ if (!res.ok) {
89
+ const text = await res.text().catch(() => '')
90
+ process.stderr.write(
91
+ `\n rnx ${commandLabel} couldn't verify recording access (${res.status}).\n` +
92
+ ` ${text || 'unexpected billing response'}\n\n`,
93
+ )
94
+ process.exit(1)
95
+ }
96
+
97
+ const data = (await res.json()) as BillingSubscriptionSnapshot
98
+ if (data.entitlements?.desktopRecording === true) return
99
+
100
+ const plan = data.plan ?? 'free'
101
+ process.stderr.write(
102
+ `\n rnx ${commandLabel} recording requires Personal, Team, or an active trial.\n` +
103
+ ` current plan: ${plan}.\n` +
104
+ ` free keeps basic 3d mode; the trial unlocks the full Personal surface.\n\n`,
105
+ )
106
+ process.exit(1)
107
+ }
@@ -0,0 +1 @@
1
+ export * from '@sootsim/skills/cli'
@@ -0,0 +1,63 @@
1
+ // resolve sootsim's bundled assets (fonts, icons, sounds)
2
+ // regardless of how the package was installed (source checkout, npm install, bunx).
3
+ //
4
+ // uses import.meta.url to derive the package root, then checks both
5
+ // the source layout (public/) and npm-installed layout (public/).
6
+
7
+ import { existsSync, readFileSync } from 'fs'
8
+ import { dirname, resolve } from 'path'
9
+ import { fileURLToPath } from 'url'
10
+ import { rnxPublicBrand } from '../src/public-brand'
11
+
12
+ // when bundled by esbuild, this file lives at dist-cli/bin.js
13
+ // when running from source, this file lives at cli/resolve-assets.ts
14
+ const thisDir = dirname(fileURLToPath(import.meta.url))
15
+
16
+ // walk up to find package root: look for package.json with name "sootsim"
17
+ function findPackageRoot(): string {
18
+ let dir = thisDir
19
+ for (let i = 0; i < 5; i++) {
20
+ try {
21
+ const pkgPath = resolve(dir, 'package.json')
22
+ if (existsSync(pkgPath)) {
23
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
24
+ if (pkg.name === rnxPublicBrand.packageName) return dir
25
+ }
26
+ } catch {}
27
+ dir = dirname(dir)
28
+ }
29
+ // fallback: assume one level up from this file
30
+ return dirname(thisDir)
31
+ }
32
+
33
+ const packageRoot = findPackageRoot()
34
+
35
+ export interface SootSimAssets {
36
+ fontsDir: string
37
+ iconsDir: string
38
+ soundsDir: string
39
+ packageRoot: string
40
+ }
41
+
42
+ export function resolveAssets(): SootSimAssets {
43
+ // both source and npm-installed layouts use public/ at the package root
44
+ const publicDir = resolve(packageRoot, 'public')
45
+
46
+ const fontsDir = resolve(publicDir, 'fonts')
47
+ const iconsDir = resolve(publicDir, 'icons')
48
+ const soundsDir = resolve(publicDir, 'sounds')
49
+
50
+ if (!existsSync(fontsDir)) {
51
+ console.warn(`[rnx] warning: fonts directory not found at ${fontsDir}`)
52
+ }
53
+
54
+ return {
55
+ fontsDir,
56
+ iconsDir,
57
+ soundsDir,
58
+ packageRoot,
59
+ }
60
+ }
61
+
62
+ // re-export packageRoot for use in CLI commands that need SOOTSIM_ROOT
63
+ export { packageRoot }