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,126 @@
1
+ // flow-live-status — pushes the flow runner's live progress into the sim it
2
+ // is driving, over the same ws bridge the steps execute on. the engine's
3
+ // flow-run-store renders it in the shell devtools "test" tab and replies
4
+ // with the pause intent, which the runner honors between steps.
5
+ //
6
+ // pushes are best-effort: an engine without the `flowStatus` handler (an
7
+ // older shell) throws "unknown command type" once and the reporter disables
8
+ // itself for the rest of the run. live status must never fail a test.
9
+
10
+ import type { WsBridge } from './ws-bridge'
11
+
12
+ export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
13
+
14
+ type PlanStep = { index: number; name: string; target?: string }
15
+
16
+ const PAUSE_POLL_MS = 400
17
+
18
+ function replyPaused(reply: unknown) {
19
+ if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
20
+ const control = reply.control
21
+ return (
22
+ typeof control === 'object' &&
23
+ control != null &&
24
+ 'paused' in control &&
25
+ control.paused === true
26
+ )
27
+ }
28
+
29
+ export class FlowLiveStatusReporter {
30
+ private disabled = false
31
+ private paused = false
32
+ private lastSimId: string | undefined
33
+ private plannedSteps: PlanStep[] | null = null
34
+ private readonly completedSteps = new Map<
35
+ number,
36
+ { index: number; status: LiveStepStatus; durationMs?: number; error?: string }
37
+ >()
38
+ private readonly runId = `flr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
39
+
40
+ constructor(
41
+ private bridge: WsBridge,
42
+ private flowName: string | null,
43
+ private flowSource: string,
44
+ private getSimId?: () => string | undefined,
45
+ ) {}
46
+
47
+ private async send(payload: Record<string, unknown>, simId: string | undefined) {
48
+ const reply: unknown = await this.bridge.send({
49
+ type: 'flowStatus',
50
+ simId,
51
+ status: { runId: this.runId, flowName: this.flowName, ...payload },
52
+ })
53
+ this.paused = replyPaused(reply)
54
+ }
55
+
56
+ private async push(payload: Record<string, unknown>): Promise<void> {
57
+ if (this.disabled) return
58
+ try {
59
+ const simId = this.getSimId?.()
60
+ if (
61
+ simId !== this.lastSimId &&
62
+ this.lastSimId != null &&
63
+ this.plannedSteps &&
64
+ payload.phase !== 'plan'
65
+ ) {
66
+ await this.send(
67
+ {
68
+ phase: 'plan',
69
+ state: 'running',
70
+ steps: this.plannedSteps,
71
+ source: this.flowSource,
72
+ },
73
+ simId,
74
+ )
75
+ for (const step of this.completedSteps.values()) {
76
+ await this.send({ phase: 'step', state: 'running', step }, simId)
77
+ }
78
+ }
79
+ this.lastSimId = simId
80
+ await this.send(payload, simId)
81
+ } catch (error) {
82
+ this.disabled = true
83
+ console.log(
84
+ `[flow] live status unavailable (${error instanceof Error ? error.message.slice(0, 120) : error}) — continuing without the devtools rail`,
85
+ )
86
+ }
87
+ }
88
+
89
+ async plan(steps: PlanStep[]): Promise<void> {
90
+ this.plannedSteps = steps.map((step) => ({ ...step }))
91
+ this.completedSteps.clear()
92
+ await this.push({ phase: 'plan', state: 'running', steps, source: this.flowSource })
93
+ }
94
+
95
+ async step(
96
+ index: number,
97
+ status: LiveStepStatus,
98
+ extra?: { durationMs?: number; error?: string },
99
+ ): Promise<void> {
100
+ const step = { index, status, ...extra }
101
+ await this.push({
102
+ phase: 'step',
103
+ state: 'running',
104
+ step,
105
+ })
106
+ this.completedSteps.set(index, step)
107
+ }
108
+
109
+ async end(state: 'passed' | 'failed'): Promise<void> {
110
+ await this.push({ phase: 'end', state })
111
+ }
112
+
113
+ // park while the devtools pause toggle is on. checked between top-level
114
+ // steps only, so a pause never interrupts a step mid-gesture.
115
+ async waitWhilePaused(): Promise<void> {
116
+ if (this.disabled) return
117
+ while (this.paused) {
118
+ console.log('[flow] paused from devtools — waiting…')
119
+ while (this.paused && !this.disabled) {
120
+ await new Promise((resolve) => setTimeout(resolve, PAUSE_POLL_MS))
121
+ await this.push({ phase: 'heartbeat', state: 'paused' })
122
+ }
123
+ if (!this.disabled) console.log('[flow] resumed')
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,140 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import { dirname, join, resolve } from 'path'
4
+
5
+ export type FlowDraftStep = Record<string, unknown>
6
+
7
+ export interface FlowCandidate {
8
+ step: FlowDraftStep
9
+ summary: string
10
+ source: string
11
+ recordedAt: string
12
+ }
13
+
14
+ export interface FlowSessionState {
15
+ version: 1
16
+ startedAt: string
17
+ updatedAt: string
18
+ steps: FlowDraftStep[]
19
+ candidate: FlowCandidate | null
20
+ }
21
+
22
+ const FLOW_SESSION_VERSION = 1 as const
23
+
24
+ export function getFlowSessionPath() {
25
+ return resolve(
26
+ process.env.RNX_FLOW_SESSION_PATH || join(tmpdir(), 'rnx-flow-session.json'),
27
+ )
28
+ }
29
+
30
+ function writeFlowSession(state: FlowSessionState) {
31
+ const filepath = getFlowSessionPath()
32
+ mkdirSync(dirname(filepath), { recursive: true })
33
+ writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n')
34
+ }
35
+
36
+ export function readFlowSession(): FlowSessionState | null {
37
+ const filepath = getFlowSessionPath()
38
+ if (!existsSync(filepath)) return null
39
+
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial<FlowSessionState>
42
+ if (
43
+ parsed.version !== FLOW_SESSION_VERSION ||
44
+ !Array.isArray(parsed.steps) ||
45
+ typeof parsed.startedAt !== 'string' ||
46
+ typeof parsed.updatedAt !== 'string'
47
+ ) {
48
+ rmSync(filepath, { force: true })
49
+ return null
50
+ }
51
+
52
+ return {
53
+ version: FLOW_SESSION_VERSION,
54
+ startedAt: parsed.startedAt,
55
+ updatedAt: parsed.updatedAt,
56
+ steps: parsed.steps as FlowDraftStep[],
57
+ candidate: parsed.candidate ?? null,
58
+ }
59
+ } catch {
60
+ rmSync(filepath, { force: true })
61
+ return null
62
+ }
63
+ }
64
+
65
+ export function hasActiveFlowSession() {
66
+ return readFlowSession() !== null
67
+ }
68
+
69
+ export function startFlowSession() {
70
+ const now = new Date().toISOString()
71
+ const state: FlowSessionState = {
72
+ version: FLOW_SESSION_VERSION,
73
+ startedAt: now,
74
+ updatedAt: now,
75
+ steps: [],
76
+ candidate: null,
77
+ }
78
+ writeFlowSession(state)
79
+ return { path: getFlowSessionPath(), state }
80
+ }
81
+
82
+ export function clearFlowSession() {
83
+ rmSync(getFlowSessionPath(), { force: true })
84
+ }
85
+
86
+ export function finalizeFlowSession() {
87
+ clearFlowSession()
88
+ }
89
+
90
+ export function rememberFlowCandidate(input: {
91
+ step: FlowDraftStep
92
+ summary: string
93
+ source: string
94
+ }) {
95
+ const state = readFlowSession()
96
+ if (!state) return { active: false as const }
97
+
98
+ // a pending candidate that was never `maestro keep`'d is silently dropped
99
+ // when a new action arrives — that's the "skip mistakes" model. surface
100
+ // the replaced action so the caller can warn (a kept action is never
101
+ // lost; an unkept one is).
102
+ const replaced = state.candidate
103
+ const next: FlowSessionState = {
104
+ ...state,
105
+ updatedAt: new Date().toISOString(),
106
+ candidate: {
107
+ step: input.step,
108
+ summary: input.summary,
109
+ source: input.source,
110
+ recordedAt: new Date().toISOString(),
111
+ },
112
+ }
113
+ writeFlowSession(next)
114
+ return { active: true as const, candidate: next.candidate, replaced }
115
+ }
116
+
117
+ export function keepFlowCandidate() {
118
+ const state = readFlowSession()
119
+ if (!state)
120
+ return { active: false as const, kept: false as const, reason: 'no-session' }
121
+ if (!state.candidate) {
122
+ return { active: true as const, kept: false as const, reason: 'no-candidate' }
123
+ }
124
+
125
+ const keptCandidate = state.candidate
126
+ const next: FlowSessionState = {
127
+ ...state,
128
+ updatedAt: new Date().toISOString(),
129
+ steps: [...state.steps, keptCandidate.step],
130
+ candidate: null,
131
+ }
132
+ writeFlowSession(next)
133
+
134
+ return {
135
+ active: true as const,
136
+ kept: true as const,
137
+ candidate: keptCandidate,
138
+ stepCount: next.steps.length,
139
+ }
140
+ }
@@ -0,0 +1,143 @@
1
+ import { cliCommandMetas } from '@rnx/skills/cli'
2
+ import {
3
+ renderCliCommandHelp,
4
+ renderCliGroupHelp,
5
+ renderCliIndexHelp,
6
+ } from '@rnx/skills/cli/renderers'
7
+ import { formatFlagHelp } from 'sootsim-engine/settings/cli-flags'
8
+ import {
9
+ DEFAULT_SOOTSIM_BRIDGE_PORT,
10
+ DEFAULT_SOOTSIM_BRIDGE_URL,
11
+ } from '../src/bridge-constants'
12
+ import {
13
+ getRnxCommandAvailability,
14
+ getRnxFirstPositional,
15
+ getRnxCommandRegistration,
16
+ RNX_COMMAND_REGISTRY,
17
+ } from './command-registry'
18
+ import type { RnxBackendKind } from './command-contract'
19
+
20
+ export interface RnxHelpOptions {
21
+ backend: RnxBackendKind
22
+ command?: string | null
23
+ commandArgs?: string[]
24
+ prefer?: 'command' | 'verb'
25
+ group?: string | null
26
+ }
27
+
28
+ const HELP_CONTEXT = {
29
+ bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
30
+ defaultShellUrl: DEFAULT_SOOTSIM_BRIDGE_URL,
31
+ }
32
+
33
+ function backendLabel(command: string, args: readonly string[]): string {
34
+ const available = (['node', 'browser'] satisfies RnxBackendKind[]).filter(
35
+ (backend) => getRnxCommandAvailability(command, args, backend).available,
36
+ )
37
+ return `available backends: ${available.join(', ')}`
38
+ }
39
+
40
+ function renderPreviewHelp(command: 'web' | 'ios' | 'android'): string {
41
+ return `rnx ${command} — select or ensure the current branch's existing ${command} preview
42
+
43
+ usage:
44
+ rnx ${command}
45
+
46
+ The browser backend selects the Factory-owned ${command} iframe. It never launches a
47
+ host process or Cloudflare runtime.`
48
+ }
49
+
50
+ function renderFactoryBrowserLeaves(command: string): string | null {
51
+ switch (command) {
52
+ case 'do':
53
+ return `Factory browser leaves:
54
+ rnx do navigate /route`
55
+ case 'shell':
56
+ return `Factory browser leaves:
57
+ rnx shell appearance light|dark`
58
+ case 'assert':
59
+ return `Factory browser leaves:
60
+ rnx assert find (--testid <id>|--text <text>|--role <role>) [--exists|--empty] [--contains <text>]`
61
+ case 'screenshot':
62
+ return `Factory browser leaves:
63
+ rnx screenshot
64
+ rnx screenshot ask <question>
65
+ rnx screenshot appearance light|dark /route --by <query-kind> --value <target>
66
+ rnx screenshot motion <task-id> /route --trigger-by <kind> --trigger-value <target> --result-by <kind> --result-value <target> [--duration-ms <100..2000>]`
67
+ default:
68
+ return null
69
+ }
70
+ }
71
+
72
+ function renderRootAvailability(backend: RnxBackendKind): string {
73
+ const visibleCommands = new Set([
74
+ ...cliCommandMetas.filter((meta) => !meta.hidden).map((meta) => meta.id),
75
+ 'web',
76
+ 'ios',
77
+ 'android',
78
+ ])
79
+ const visibleRegistry = RNX_COMMAND_REGISTRY.filter((entry) =>
80
+ visibleCommands.has(entry.name),
81
+ )
82
+ const available = visibleRegistry
83
+ .filter((entry) => getRnxCommandAvailability(entry.name, [], backend).available)
84
+ .map((entry) => entry.name)
85
+ const unavailable = visibleRegistry
86
+ .filter((entry) => !getRnxCommandAvailability(entry.name, [], backend).available)
87
+ .map((entry) => entry.name)
88
+
89
+ return `backend availability (${backend}):
90
+ available: ${available.join(', ')}
91
+ unavailable: ${unavailable.join(', ')}
92
+
93
+ Run \`rnx <command> --help\` for leaf availability and usage.`
94
+ }
95
+
96
+ export function renderRnxHelp(options: RnxHelpOptions): string {
97
+ const command = options.command ?? null
98
+ const commandArgs = options.commandArgs ?? []
99
+ if (!command) {
100
+ return `${renderCliIndexHelp(HELP_CONTEXT, formatFlagHelp()).trimEnd()}
101
+
102
+ browser preview selectors:
103
+ rnx web select the existing Factory web preview
104
+ rnx ios select the existing Factory iOS preview
105
+ rnx android select the existing Factory Android preview
106
+
107
+ ${renderRootAvailability(options.backend)}`
108
+ }
109
+
110
+ let body: string | null
111
+ if (command === 'web' || command === 'ios' || command === 'android') {
112
+ body = renderPreviewHelp(command)
113
+ } else if (options.group === command && !options.prefer) {
114
+ body = renderCliGroupHelp(command, HELP_CONTEXT, formatFlagHelp())
115
+ } else {
116
+ const subVerb = getRnxFirstPositional(commandArgs)
117
+ const grouping = new Set(['do', 'get', 'debug', 'wait'])
118
+ const helpName = options.prefer === 'verb' && subVerb ? subVerb : command
119
+ body = renderCliCommandHelp(helpName, HELP_CONTEXT, {
120
+ prefer: options.prefer,
121
+ group: options.group,
122
+ })
123
+ if (!body && grouping.has(command)) {
124
+ body = renderCliGroupHelp(command, HELP_CONTEXT, formatFlagHelp())
125
+ }
126
+ }
127
+
128
+ if (!body) return `unknown command: ${command}`
129
+
130
+ const registration = getRnxCommandRegistration(command)
131
+ if (!registration) return body
132
+ const current = getRnxCommandAvailability(command, commandArgs, options.backend)
133
+ const currentLine = current.available
134
+ ? `${options.backend} backend: available`
135
+ : `${options.backend} backend: ${current.reason}`
136
+ const factoryLeaves =
137
+ options.backend === 'browser' ? renderFactoryBrowserLeaves(command) : null
138
+ return `${body.trimEnd()}
139
+
140
+ ${factoryLeaves ? `${factoryLeaves}\n\n` : ''}${backendLabel(command, commandArgs)}
141
+
142
+ ${currentLine}`
143
+ }
package/cli/help.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { BETA_TAGLINE, IS_BETA } from '../src/beta'
2
+ import { getCliVersion } from '../src/cli-version'
3
+ import { rnxPublicBrand } from '../src/public-brand'
4
+ import { renderRnxHelp } from './help-core'
5
+ import { maybeHint } from './hints'
6
+ import { getRuntimeSummary } from './runtime-summary'
7
+
8
+ function versionSummaryLine(): string {
9
+ return `${rnxPublicBrand.commandName} CLI v${getCliVersion()} · ${getRuntimeSummary().primary}`
10
+ }
11
+
12
+ export function printHelp(): void {
13
+ const betaLine = IS_BETA ? `\n${BETA_TAGLINE}\n` : ''
14
+ console.log(
15
+ `${versionSummaryLine()}\n${betaLine}${renderRnxHelp({ backend: 'node' })}\n`,
16
+ )
17
+ maybeHint('clean-uninstall')
18
+ }
19
+
20
+ export function printGroupHelp(group: string): boolean {
21
+ const output = renderRnxHelp({
22
+ backend: 'node',
23
+ command: group,
24
+ group,
25
+ })
26
+ if (output.startsWith('unknown command:')) return false
27
+ console.log(`${output}\n`)
28
+ return true
29
+ }
30
+
31
+ export function printCommandHelp(
32
+ name: string,
33
+ options: { prefer?: 'command' | 'verb'; group?: string | null } = {},
34
+ ): void {
35
+ const command = options.group ?? name
36
+ const output = renderRnxHelp({
37
+ backend: 'node',
38
+ command,
39
+ commandArgs: options.group ? [name] : [],
40
+ prefer: options.prefer,
41
+ group: options.group,
42
+ })
43
+ console.log(`${output}\n`)
44
+ }
@@ -0,0 +1,19 @@
1
+ import { getCliInspectVerbMetaByPath } from '@rnx/skills/cli'
2
+ import type { ParsedArgs } from './parse-args'
3
+
4
+ const HIDDEN_RUNTIME_ALIAS_GROUPS = ['do', 'get'] as const
5
+
6
+ export function resolveHiddenRuntimeAlias(parsed: ParsedArgs): ParsedArgs {
7
+ if (parsed.command) return parsed
8
+
9
+ const attempted = parsed.commandArgs.find((arg) => !arg.startsWith('-'))
10
+ if (!attempted) return parsed
11
+
12
+ for (const group of HIDDEN_RUNTIME_ALIAS_GROUPS) {
13
+ if (getCliInspectVerbMetaByPath(group, attempted)) {
14
+ return { ...parsed, command: group }
15
+ }
16
+ }
17
+
18
+ return parsed
19
+ }
package/cli/hints.ts ADDED
@@ -0,0 +1,214 @@
1
+ // configurable, identity-scoped hint registry for the rnx CLI.
2
+ //
3
+ // two problems this solves:
4
+ // 1. the CLI scatters "hint: ..." console.logs across many command files,
5
+ // each with slightly different wording.
6
+ // 2. agents that loop over the same command (e.g. describe, find) see the
7
+ // same hint every invocation, which becomes noise.
8
+ //
9
+ // each hint declares its own frequency (always, once-per-identity, or a
10
+ // cooldown), so hints that are informational-but-stale-quickly can still
11
+ // re-fire while advice-like hints stay suppressed for the rest of the
12
+ // CLI identity.
13
+ //
14
+ // suppression is keyed on `getCliIdentityKey()` so two agents / two terminals
15
+ // don't share shown-sets.
16
+
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
18
+ import { tmpdir } from 'os'
19
+ import { dirname, join } from 'path'
20
+ import { getCliIdentityKey } from './current-sim'
21
+
22
+ export type HintFrequency =
23
+ | 'always'
24
+ | 'once-per-identity'
25
+ | 'once-ever'
26
+ | { cooldownMs: number }
27
+
28
+ export interface HintSpec<Args extends unknown[] = unknown[]> {
29
+ frequency: HintFrequency
30
+ // returned string(s) are printed as-is, prefixed with two spaces.
31
+ // return null to skip (useful when render wants to decide conditionally).
32
+ render: (...args: Args) => string | string[] | null
33
+ }
34
+
35
+ const registry = new Map<string, HintSpec<any[]>>()
36
+
37
+ export function defineHint<Args extends unknown[]>(id: string, spec: HintSpec<Args>) {
38
+ registry.set(id, spec as HintSpec<any[]>)
39
+ }
40
+
41
+ interface HintStateFile {
42
+ version: 1
43
+ shown: Record<string, number> // id -> last-shown timestamp (ms)
44
+ }
45
+
46
+ const STATE_VERSION = 1 as const
47
+
48
+ function identityStatePath(): string {
49
+ return join(tmpdir(), `rnx-cli-hints-${getCliIdentityKey()}.json`)
50
+ }
51
+
52
+ function globalStatePath(): string {
53
+ return join(tmpdir(), `rnx-cli-hints-global.json`)
54
+ }
55
+
56
+ function readState(path: string): HintStateFile {
57
+ if (!existsSync(path)) return { version: STATE_VERSION, shown: {} }
58
+ try {
59
+ const parsed = JSON.parse(readFileSync(path, 'utf8'))
60
+ if (parsed?.version !== STATE_VERSION || !parsed?.shown) {
61
+ return { version: STATE_VERSION, shown: {} }
62
+ }
63
+ return parsed as HintStateFile
64
+ } catch {
65
+ return { version: STATE_VERSION, shown: {} }
66
+ }
67
+ }
68
+
69
+ function writeState(path: string, state: HintStateFile) {
70
+ try {
71
+ mkdirSync(dirname(path), { recursive: true })
72
+ writeFileSync(path, JSON.stringify(state) + '\n')
73
+ } catch {
74
+ // best-effort only; losing shown state just means a hint re-fires once.
75
+ }
76
+ }
77
+
78
+ function modeFromEnv(): 'normal' | 'always' | 'off' {
79
+ const v = (process.env.RNX_HINTS || '').toLowerCase()
80
+ if (v === 'off' || v === '0' || v === 'false') return 'off'
81
+ if (v === 'always' || v === 'verbose') return 'always'
82
+ return 'normal'
83
+ }
84
+
85
+ function shouldShow(id: string, frequency: HintFrequency): boolean {
86
+ const mode = modeFromEnv()
87
+ if (mode === 'off') return false
88
+ if (mode === 'always') return true
89
+ if (frequency === 'always') return true
90
+
91
+ const now = Date.now()
92
+ if (frequency === 'once-per-identity') {
93
+ const state = readState(identityStatePath())
94
+ if (state.shown[id]) return false
95
+ state.shown[id] = now
96
+ writeState(identityStatePath(), state)
97
+ return true
98
+ }
99
+ if (frequency === 'once-ever') {
100
+ const state = readState(globalStatePath())
101
+ if (state.shown[id]) return false
102
+ state.shown[id] = now
103
+ writeState(globalStatePath(), state)
104
+ return true
105
+ }
106
+ if (typeof frequency === 'object' && 'cooldownMs' in frequency) {
107
+ const state = readState(identityStatePath())
108
+ const last = state.shown[id] ?? 0
109
+ if (now - last < frequency.cooldownMs) return false
110
+ state.shown[id] = now
111
+ writeState(identityStatePath(), state)
112
+ return true
113
+ }
114
+ return true
115
+ }
116
+
117
+ // print a registered hint. no-ops if suppressed for this identity/cooldown.
118
+ // returns whether it was actually shown.
119
+ export function maybeHint(id: string, ...args: unknown[]): boolean {
120
+ const spec = registry.get(id)
121
+ if (!spec) {
122
+ if (process.env.RNX_HINTS_DEBUG) {
123
+ console.error(` [hints] no hint registered for id "${id}"`)
124
+ }
125
+ return false
126
+ }
127
+ if (!shouldShow(id, spec.frequency)) return false
128
+
129
+ const body = spec.render(...args)
130
+ if (body == null) return false
131
+ const lines = Array.isArray(body) ? body : [body]
132
+ // always stderr. every hint is advice about how to drive the CLI, never part
133
+ // of a command's result, and several commands (`describe`, `find`, every
134
+ // `do` verb) put a JSON document on stdout — a hint printed there corrupts it.
135
+ for (const line of lines) console.error(` hint: ${line}`)
136
+ return true
137
+ }
138
+
139
+ export function resetHintsForIdentity() {
140
+ rmSync(identityStatePath(), { force: true })
141
+ }
142
+
143
+ export function resetGlobalHints() {
144
+ rmSync(globalStatePath(), { force: true })
145
+ }
146
+
147
+ export function listHints(): Array<{ id: string; frequency: HintFrequency }> {
148
+ return Array.from(registry.entries()).map(([id, spec]) => ({
149
+ id,
150
+ frequency: spec.frequency,
151
+ }))
152
+ }
153
+
154
+ // register the repo-wide hints here. keeping them centralised makes it easy
155
+ // to audit wording + frequency policy in one place.
156
+ defineHint<[number]>('app-still-loading', {
157
+ // re-fires once a minute because node count may genuinely change as the
158
+ // bundle boots — telling the user about it again is useful, not noisy.
159
+ frequency: { cooldownMs: 60_000 },
160
+ render: (nodeCount) =>
161
+ `app may still be loading (${nodeCount} nodes). run \`rnx wait ready\` first.`,
162
+ })
163
+
164
+ defineHint<[string]>('wait-selector-for-missing-testid', {
165
+ // advice is stable; once per CLI identity is enough.
166
+ frequency: 'once-per-identity',
167
+ render: (testId) => `rnx wait selector ${testId}`,
168
+ })
169
+
170
+ defineHint<[string[]]>('prefer-cli-over-eval', {
171
+ frequency: 'once-per-identity',
172
+ render: (suggestions) => {
173
+ if (!suggestions.length) return null
174
+ return [`try the CLI shortcut instead:`, ...suggestions.map((s) => ` ${s}`)]
175
+ },
176
+ })
177
+
178
+ defineHint<[]>('describe-use-filters', {
179
+ // shown when a describe run emits >=80 lines without any filter active —
180
+ // gentle nudge that there are better tools.
181
+ frequency: 'once-per-identity',
182
+ render: () => [
183
+ `describe output is long. narrow it with:`,
184
+ ` rnx describe --only '*Bottom Sheet*'`,
185
+ ` rnx describe --testid-like 'swap-*'`,
186
+ ` rnx describe --subtree <testID>`,
187
+ ],
188
+ })
189
+
190
+ defineHint<[]>('describe-filter-context', {
191
+ // filtered describe is useful search output, but it intentionally drops
192
+ // surrounding tree context. once per identity is enough to prevent agents
193
+ // treating a search hit as route proof.
194
+ frequency: 'once-per-identity',
195
+ render: () => [
196
+ `filtered describe is search output, not current-route proof.`,
197
+ `run \`rnx describe\` without filters after navigation; clipped nodes are marked \`(clipped:...)\`.`,
198
+ ],
199
+ })
200
+
201
+ defineHint<[string]>('subtree-root-not-found', {
202
+ frequency: 'always',
203
+ render: (id) =>
204
+ `no node with testID/id "${id}" — try \`rnx find --testid ${id}\` to discover available ids.`,
205
+ })
206
+
207
+ defineHint<[]>('clean-uninstall', {
208
+ // shown once on the first `rnx --help` run so users learn the teardown path.
209
+ frequency: 'once-ever',
210
+ render: () => [
211
+ `to remove the background daemon, run \`rnx daemon uninstall\`. it keeps your runtimes and device profiles; add \`--purge\` to delete ~/.rnx too.`,
212
+ `the optional desktop app and its macOS preference plist are separate from the daemon.`,
213
+ ],
214
+ })