peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -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,189 @@
1
+ // shared CLI argument parser — handles global flags, commands, and per-command flags
2
+
3
+ import { RNX_COMMAND_NAMES, RNX_NODE_COMMAND_NAMES } from './command-registry'
4
+
5
+ export interface ParsedArgs {
6
+ command: string | null
7
+ commandArgs: string[]
8
+ globalFlags: Record<string, string | boolean | number>
9
+ help: boolean
10
+ version: boolean
11
+ verbose: boolean
12
+ warnings: string[]
13
+ }
14
+
15
+ const GLOBAL_FLAGS: Record<
16
+ string,
17
+ { type: 'string' | 'boolean' | 'number'; short?: string }
18
+ > = {
19
+ '--help': { type: 'boolean', short: '-h' },
20
+ '--version': { type: 'boolean', short: '-V' },
21
+ '--verbose': { type: 'boolean', short: '-v' },
22
+ '--port': { type: 'number', short: '-p' },
23
+ '--device': { type: 'string', short: '-d' },
24
+ '--theme': { type: 'string', short: '-t' },
25
+ '--headless': { type: 'boolean' },
26
+ '--driver': { type: 'string' },
27
+ '--sim': { type: 'string' },
28
+ // documented aliases for --sim (CLAUDE.md / skills / engine docs say
29
+ // `--session <id>`). normalized to the sim target in bin.ts.
30
+ '--session': { type: 'string' },
31
+ '--tab': { type: 'string' },
32
+ }
33
+
34
+ // every spelling parseRnxArgs consumes before the command name, mapped to
35
+ // whether it takes a value. a command that rejects unknown flags uses this to
36
+ // tell a genuinely unknown flag apart from a global one typed after the
37
+ // command, where nothing reads it, and to show how to pass that one: telling
38
+ // someone to write `rnx --verbose <value>` earns them a second error.
39
+ export const GLOBAL_FLAG_TAKES_VALUE: ReadonlyMap<string, boolean> = new Map(
40
+ Object.entries(GLOBAL_FLAGS).flatMap(([name, def]): [string, boolean][] => {
41
+ const takesValue = def.type !== 'boolean'
42
+ return def.short
43
+ ? [
44
+ [name, takesValue],
45
+ [def.short, takesValue],
46
+ ]
47
+ : [[name, takesValue]]
48
+ }),
49
+ )
50
+
51
+ // top-level verbs that route into runInspect. the surface is intentionally
52
+ // tiny — everything else lives under one of the grouping verbs:
53
+ // - `rnx get <noun>` reads
54
+ // - `rnx do <action>` writes
55
+ // - `rnx debug <tool>` instrumentation (handled by runDebug)
56
+ // `describe` and `find` stay at root as the most common entrypoints.
57
+ // `list` stays at root because it's a sim-management read.
58
+ export const TOP_LEVEL_RUNTIME_COMMANDS = new Set([
59
+ 'list',
60
+ 'describe',
61
+ 'find',
62
+ 'get',
63
+ 'do',
64
+ 'wait',
65
+ 'network',
66
+ 'logs',
67
+ 'shell',
68
+ ])
69
+
70
+ // grouping verbs that take a sub-verb (`rnx wait ready`,
71
+ // `rnx do tap-id`). agents and humans very often type the hyphenated
72
+ // form (`rnx wait-ready`, `rnx do-tap-id`) — it reads as one
73
+ // command and matches the typed-tool names (rnx_wait_ready). bin.ts
74
+ // normalizes `<verb>-<rest>` → `<verb> <rest>` for these so the intuitive
75
+ // spelling just works instead of failing as "unknown command".
76
+ export const HYPHENATED_GROUPING_VERBS = new Set(['do', 'get', 'wait', 'shell', 'debug'])
77
+
78
+ // known commands — if first positional arg matches, it's a command.
79
+ // every entry here must have a `cliCommandMetas` entry in
80
+ // `packages/rnx-skills/src/cli/meta.ts` (or sit on the internal
81
+ // allowlist in `test/sootsimCliRegistry.test.ts`), so help, website docs,
82
+ // and agent skills can never drift from what the parser accepts.
83
+ export const COMMANDS = new Set(RNX_NODE_COMMAND_NAMES)
84
+
85
+ export function parseRnxArgs(args: string[]): ParsedArgs {
86
+ const result: ParsedArgs = {
87
+ command: null,
88
+ commandArgs: [],
89
+ globalFlags: {},
90
+ help: false,
91
+ version: false,
92
+ verbose: false,
93
+ warnings: [],
94
+ }
95
+
96
+ let i = 0
97
+
98
+ // parse global flags and detect command
99
+ while (i < args.length) {
100
+ const arg = args[i]
101
+
102
+ // explicit separator — strip it, everything after is command args
103
+ if (arg === '--') {
104
+ result.commandArgs.push(...args.slice(i + 1))
105
+ break
106
+ }
107
+
108
+ // check for global flag
109
+ const flagEntry = Object.entries(GLOBAL_FLAGS).find(
110
+ ([name, def]) => name === arg || def.short === arg,
111
+ )
112
+
113
+ if (flagEntry) {
114
+ const [name, def] = flagEntry
115
+ const key = name.replace(/^--/, '')
116
+ if (def.type === 'boolean') {
117
+ result.globalFlags[key] = true
118
+ i++
119
+ } else {
120
+ const val = args[i + 1]
121
+ if (val === undefined || val.startsWith('-')) {
122
+ // missing value — treat as error, pass through
123
+ result.warnings.push(` warning: ${arg} requires a value`)
124
+ i++
125
+ continue
126
+ }
127
+ if (def.type === 'number') {
128
+ const num = Number(val)
129
+ if (Number.isNaN(num)) {
130
+ result.warnings.push(` warning: ${arg} requires a number, got "${val}"`)
131
+ i += 2
132
+ continue
133
+ }
134
+ result.globalFlags[key] = num
135
+ } else {
136
+ result.globalFlags[key] = val
137
+ }
138
+ i += 2
139
+ }
140
+ continue
141
+ }
142
+
143
+ // first non-flag arg — is it a command?
144
+ if (!result.command && !arg.startsWith('-')) {
145
+ // hyphenated grouping-verb spelling (`wait-ready`, `do-tap-id`):
146
+ // agents and humans type it constantly (it reads as one command and
147
+ // mirrors the typed tool names). split only the FIRST hyphen so
148
+ // multi-segment sub-verbs survive (`do-tap-id` → `do` `tap-id`).
149
+ // without this it fell through to the unrecognized branch as
150
+ // "unknown command: wait-ready" with empty stdout.
151
+ const dash = arg.indexOf('-')
152
+ if (dash > 0) {
153
+ const verb = arg.slice(0, dash)
154
+ const sub = arg.slice(dash + 1)
155
+ if (HYPHENATED_GROUPING_VERBS.has(verb) && sub.length > 0) {
156
+ result.command = verb
157
+ const rest = args.slice(i + 1)
158
+ result.commandArgs = [sub, ...(rest[0] === '--' ? rest.slice(1) : rest)]
159
+ break
160
+ }
161
+ }
162
+ if (RNX_COMMAND_NAMES.has(arg)) {
163
+ result.command = arg
164
+ const rest = args.slice(i + 1)
165
+ // strip leading -- separator if present
166
+ result.commandArgs = rest[0] === '--' ? rest.slice(1) : rest
167
+ break
168
+ }
169
+ // not a recognized command — leave it as command args so bin.ts can
170
+ // print a focused unknown-command message.
171
+ result.commandArgs = args.slice(i)
172
+ break
173
+ }
174
+
175
+ // unrecognized flag — pass through to command
176
+ result.commandArgs.push(arg)
177
+ i++
178
+ }
179
+
180
+ result.help = !!result.globalFlags['help']
181
+ result.version = !!result.globalFlags['version']
182
+ result.verbose = !!result.globalFlags['verbose']
183
+
184
+ return result
185
+ }
186
+
187
+ export function parseArgs(argv: string[]): ParsedArgs {
188
+ return parseRnxArgs(argv.slice(2))
189
+ }
package/cli/privacy.ts ADDED
@@ -0,0 +1,53 @@
1
+ import {
2
+ readPrivacyPreferences,
3
+ writePrivacyPreferences,
4
+ type PrivacyPreferences,
5
+ } from '../src/home-paths'
6
+ import { isAgentEnv } from './commands/inspect/env'
7
+ import { confirm } from './prompt'
8
+
9
+ function isCi(): boolean {
10
+ return (
11
+ process.env.CI === '1' ||
12
+ process.env.CI === 'true' ||
13
+ process.env.GITHUB_ACTIONS === 'true'
14
+ )
15
+ }
16
+
17
+ export function canPromptForPrivacyChoice(): boolean {
18
+ return process.stdin.isTTY === true && !isCi() && !isAgentEnv()
19
+ }
20
+
21
+ export function diagnosticsAllowedForCurrentProcess(
22
+ preferences: PrivacyPreferences = readPrivacyPreferences(),
23
+ ): boolean {
24
+ const doNotTrack = process.env.DO_NOT_TRACK
25
+ if (doNotTrack === '1' || doNotTrack === 'true') return false
26
+ if (!canPromptForPrivacyChoice()) return false
27
+ return preferences.configured && preferences.productAnalytics
28
+ }
29
+
30
+ export async function ensurePrivacyChoice(options: {
31
+ prompt: boolean
32
+ }): Promise<PrivacyPreferences> {
33
+ const current = readPrivacyPreferences()
34
+ if (current.configured) return current
35
+
36
+ if (!options.prompt || !canPromptForPrivacyChoice()) {
37
+ return {
38
+ productAnalytics: false,
39
+ crashReports: false,
40
+ configured: false,
41
+ }
42
+ }
43
+
44
+ const allowed = await confirm(
45
+ 'Share anonymous package incompatibilities and error classes to improve rnx?',
46
+ false,
47
+ )
48
+ // the engine's historical analytics and crash transports carry broader
49
+ // payloads, so crashReports stays off. productAnalytics is now consent for
50
+ // the bounded CLI diagnostic transport only.
51
+ writePrivacyPreferences({ productAnalytics: allowed, crashReports: false })
52
+ return readPrivacyPreferences()
53
+ }
package/cli/prompt.ts ADDED
@@ -0,0 +1,56 @@
1
+ // minimal readline prompts — no external dependencies
2
+
3
+ import { createInterface } from 'readline'
4
+ import { accent, bold, muted } from './ui'
5
+
6
+ const rl = () =>
7
+ createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ })
11
+
12
+ function ask(question: string): Promise<string> {
13
+ return new Promise((resolve) => {
14
+ const r = rl()
15
+ r.question(question, (answer) => {
16
+ r.close()
17
+ resolve(answer.trim())
18
+ })
19
+ })
20
+ }
21
+
22
+ export async function confirm(message: string, defaultYes = true): Promise<boolean> {
23
+ const hint = defaultYes ? 'Y/n' : 'y/N'
24
+ // non-interactive stdin (piped, /dev/null, closed) — readline's question
25
+ // callback would never fire, leaving the await unsettled and the process
26
+ // exiting 0 with a `Detected unsettled top-level await` warning. degrade
27
+ // to the default instead so scripted invocations behave predictably.
28
+ if (!process.stdin.isTTY) {
29
+ console.log(
30
+ ` ${message} (${hint}) ${defaultYes ? 'y' : 'n'} ${muted('(non-interactive)')}`,
31
+ )
32
+ return defaultYes
33
+ }
34
+ const answer = await ask(
35
+ ` ${accent('?')} ${bold(message)} ${muted(`(${hint})`)} ${accent('›')} `,
36
+ )
37
+ if (answer === '') return defaultYes
38
+ return answer.toLowerCase().startsWith('y')
39
+ }
40
+
41
+ export async function select(message: string, options: string[]): Promise<number> {
42
+ console.log(`\n ${accent('?')} ${bold(message)}\n`)
43
+ for (let i = 0; i < options.length; i++) {
44
+ console.log(` ${accent(String(i + 1))} ${options[i]}`)
45
+ }
46
+ console.log()
47
+ // non-interactive stdin — pick the first option rather than hanging.
48
+ if (!process.stdin.isTTY) {
49
+ console.log(` Choose (1-${options.length}): 1 ${muted('(non-interactive)')}`)
50
+ return 0
51
+ }
52
+ const answer = await ask(` ${muted(`Choose 1-${options.length}`)} ${accent('›')} `)
53
+ const num = parseInt(answer, 10)
54
+ if (num >= 1 && num <= options.length) return num - 1
55
+ return 0 // default to first
56
+ }
@@ -0,0 +1,108 @@
1
+ import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
2
+ import { authHeaderValue, resolveCliAuth, type CliAuth } from './auth'
3
+ import { resolveDefaultUploadOrigin } from './commands/upload'
4
+ import { rnxExit } from './run-rnx'
5
+
6
+ type BillingSubscriptionSnapshot = {
7
+ plan?: 'free' | 'personal' | 'team'
8
+ entitlements?: {
9
+ desktopRecording?: boolean
10
+ }
11
+ }
12
+
13
+ async function billingOriginFor(auth: CliAuth, originOverride?: string): Promise<string> {
14
+ // an explicit override (e.g. --preview-origin https://contrast.localhost:3000 from
15
+ // `maestro test --preview`) wins over whatever origin the shared auth session
16
+ // happens to remember. this keeps local preview-upload flows pointed
17
+ // at the local billing endpoint even when the desktop session token
18
+ // was last minted against contrast.
19
+ if (originOverride) return originOverride.replace(/\/$/, '')
20
+ if (auth.kind === 'session' && auth.origin) {
21
+ return auth.origin.replace(/\/$/, '')
22
+ }
23
+ const shared = readSharedDesktopAuthSession()
24
+ if (shared?.origin) {
25
+ return shared.origin.replace(/\/$/, '')
26
+ }
27
+ // api-key auth (RNX_API_KEY) carries no origin.
28
+ // resolve it the same way the upload itself does — probe a local Contrast
29
+ // stack on :3000, else fall back to prod. this keeps the entitlement
30
+ // check pointed at the *same* origin the recording will upload to (the
31
+ // engine resolves loopback sims to the local :3000 stack); checking prod
32
+ // billing with a key minted against the local stack would 401 here even
33
+ // though the upload would have succeeded.
34
+ return (await resolveDefaultUploadOrigin()).replace(/\/$/, '')
35
+ }
36
+
37
+ export interface RecordingEntitlementOptions {
38
+ /** override the origin used to check billing. when the CLI is running
39
+ * against a local Contrast stack (e.g. `maestro test --preview --preview-origin
40
+ * https://contrast.localhost:3000`), the recording entitlement needs to hit the
41
+ * same origin that'll receive the upload — not the remote contrast
42
+ * origin stored in the shared desktop session. */
43
+ originOverride?: string
44
+ /** allow GitHub installation/action auth to record for pr preview uploads.
45
+ * this is intentionally opt-in so local `rnx record` stays tied to
46
+ * the user's paid/trial desktop entitlement. */
47
+ allowGitHubAuth?: boolean
48
+ }
49
+
50
+ export async function ensureCliRecordingEntitlement(
51
+ commandLabel: string,
52
+ opts: RecordingEntitlementOptions = {},
53
+ ): Promise<void> {
54
+ const auth = resolveCliAuth()
55
+ if (!auth) {
56
+ process.stderr.write(
57
+ `\n rnx ${commandLabel} needs auth before it can check recording access.\n\n` +
58
+ ` pick one:\n` +
59
+ ` • run \`rnx login\`\n` +
60
+ ` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
61
+ )
62
+ rnxExit(1)
63
+ }
64
+
65
+ if (auth.kind === 'github' && opts.allowGitHubAuth) return
66
+
67
+ const billingOrigin = await billingOriginFor(auth, opts.originOverride)
68
+ let res: Response
69
+ try {
70
+ res = await fetch(`${billingOrigin}/api/sootsim/billing/subscription`, {
71
+ headers: { authorization: authHeaderValue(auth) },
72
+ })
73
+ } catch (error) {
74
+ process.stderr.write(
75
+ `\n rnx ${commandLabel} couldn't verify recording access.\n` +
76
+ ` ${error instanceof Error ? error.message : String(error)}\n\n`,
77
+ )
78
+ rnxExit(1)
79
+ }
80
+
81
+ if (res.status === 401) {
82
+ process.stderr.write(
83
+ `\n rnx ${commandLabel} couldn't verify recording access with the current auth against ${billingOrigin}.\n` +
84
+ ` sign in again for that origin, or provide a valid RNX_API_KEY.\n\n`,
85
+ )
86
+ rnxExit(1)
87
+ }
88
+
89
+ if (!res.ok) {
90
+ const text = await res.text().catch(() => '')
91
+ process.stderr.write(
92
+ `\n rnx ${commandLabel} couldn't verify recording access (${res.status}).\n` +
93
+ ` ${text || 'unexpected billing response'}\n\n`,
94
+ )
95
+ rnxExit(1)
96
+ }
97
+
98
+ const data = (await res.json()) as BillingSubscriptionSnapshot
99
+ if (data.entitlements?.desktopRecording === true) return
100
+
101
+ const plan = data.plan ?? 'free'
102
+ process.stderr.write(
103
+ `\n rnx ${commandLabel} recording requires Personal, Team, or an active trial.\n` +
104
+ ` current plan: ${plan}.\n` +
105
+ ` free keeps basic 3d mode; the trial unlocks the full Personal surface.\n\n`,
106
+ )
107
+ rnxExit(1)
108
+ }
@@ -0,0 +1,2 @@
1
+ export * from '@rnx/skills/cli'
2
+ export * from './command-registry'
@@ -0,0 +1,63 @@
1
+ // resolve rnx'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 the rnxsim package name
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 }