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,580 @@
1
+ // session lifecycle shared between the CLI (`sootsim agent ...`) and electron
2
+ // main (IPC handlers). both contexts spawn the same `sootsim agent-wrapper`
3
+ // subcommand, mutate the same AttachedProjects store, and subscribe to the
4
+ // same events.out FIFO.
5
+ //
6
+ // this file is the canonical implementation; thin wrappers in the CLI and
7
+ // electron layers call these functions for their UX needs.
8
+
9
+ import { spawn, spawnSync } from 'node:child_process'
10
+ import { randomUUID } from 'node:crypto'
11
+ import fs, { constants as fsConstants } from 'node:fs'
12
+ import path from 'node:path'
13
+ import readline from 'node:readline'
14
+ import { rnxSelfInvocation, type RnxSelfInvocation } from '../cli/self-invocation.ts'
15
+ import { IS_STANDALONE } from '../cli/standalone.ts'
16
+ import { parseAgentEventLine, type AgentEvent } from './agent-events.ts'
17
+ import { encodeAgentPromptEnvelope, type AgentPromptEnvelope } from './agent-prompt.ts'
18
+ import {
19
+ findProjectById,
20
+ findSessionById,
21
+ getUserDataDir,
22
+ listSessions,
23
+ updateSessionStatus,
24
+ upsertSession,
25
+ type AgentSession,
26
+ } from './attached-projects.ts'
27
+ import { rnxPublicBrand } from './public-brand.ts'
28
+
29
+ export type Provider = 'codex' | 'claude'
30
+
31
+ // --- fs layout ---
32
+
33
+ export function sessionDir(sessionId: string): string {
34
+ return path.join(getUserDataDir(), 'sessions', sessionId)
35
+ }
36
+ export function promptFifoPath(sessionId: string): string {
37
+ return path.join(sessionDir(sessionId), 'prompt.in')
38
+ }
39
+ export function eventsFifoPath(sessionId: string): string {
40
+ return path.join(sessionDir(sessionId), 'events.out')
41
+ }
42
+ export function transcriptPath(sessionId: string): string {
43
+ return path.join(getUserDataDir(), 'transcripts', `${sessionId}.log`)
44
+ }
45
+
46
+ // --- pid helpers ---
47
+
48
+ /** returns true iff a process with this pid is alive AND owned by us. pid
49
+ * recycling means kill(0) can return true for an unrelated process that
50
+ * happened to take the same number — we also require the sentinel file
51
+ * written by the wrapper at startup to still exist. */
52
+ export function pidIsAlive(pid: number | undefined, sessionId?: string): boolean {
53
+ if (!pid) return false
54
+ try {
55
+ process.kill(pid, 0)
56
+ } catch {
57
+ return false
58
+ }
59
+ if (sessionId) {
60
+ // sentinel: the events.out fifo's session dir. if the session was ended
61
+ // via cmdEnd, the dir was removed; kill(0) returning true on that pid
62
+ // would be a recycled pid from an unrelated process.
63
+ if (!fs.existsSync(sessionDir(sessionId))) return false
64
+ }
65
+ return true
66
+ }
67
+
68
+ // --- invocation resolution ---
69
+
70
+ /** locate the rnx CLI so this process can run a subcommand of it. callers
71
+ * range from the CLI itself to electron main and the vite-plugin daemon.
72
+ * priority:
73
+ * 1. RNX_BIN env var (explicit override)
74
+ * 2. this process already IS the CLI — `rnxSelfInvocation` owns that answer
75
+ * 3. electron prod: bundled binary under process.resourcesPath/bin/
76
+ * 4. workspace build artifacts (dist-bin/<binary> or dist-cli/bin.js)
77
+ * — shared between electron dev, the vite-plugin-hosted daemon, and
78
+ * any other node process that isn't launched with the CLI entry
79
+ * script directly (e.g. argv[1] = .../node_modules/.bin/vite).
80
+ * 5. CLI dev (argv[1] points at the entry script): re-use argv[0] + argv[1]
81
+ */
82
+ export function resolveSootsimInvocation(): RnxSelfInvocation {
83
+ if (process.env.RNX_BIN) {
84
+ return { executable: process.env.RNX_BIN, prefixArgs: [] }
85
+ }
86
+ // the compiled standalone binary cannot be found through argv: bun reports
87
+ // argv[0] as the literal string "bun" and argv[1] as a path inside its
88
+ // virtual filesystem, so both fall through to spawning a `bun` that a
89
+ // shell-installed user does not have.
90
+ if (IS_STANDALONE) return rnxSelfInvocation()
91
+ // electron prod: packaged binary lives under Resources/bin/.
92
+ if (process.versions.electron) {
93
+ const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string })
94
+ .resourcesPath
95
+ if (resourcesPath) {
96
+ const candidates = [
97
+ path.join(resourcesPath, 'bin', 'sootsim'),
98
+ path.join(resourcesPath, 'bin', `sootsim-${process.platform}-${process.arch}`),
99
+ ]
100
+ for (const c of candidates) {
101
+ if (fs.existsSync(c)) return { executable: c, prefixArgs: [] }
102
+ }
103
+ }
104
+ }
105
+
106
+ // workspace build artifacts — tried for every non-CLI-entry node context
107
+ // (electron dev, vite-plugin-hosted daemon, random script) before falling
108
+ // back to argv. prefers the compiled bun binary because `bun dev` runs
109
+ // `watch:cli:binary`; the same dev graph runs `watch:cli` to keep the node
110
+ // fallback and browser-facing dist-lib exports fresh. still warn when a
111
+ // caller starts outside that graph with stale artifacts.
112
+ const workspace = tryWorkspaceSootsim()
113
+ if (workspace) return workspace
114
+
115
+ // argv fallback. only trust it when argv[1] looks like a real entry
116
+ // script — `.js`/`.ts`/`.mjs` etc. launching vite gives us argv[1] =
117
+ // `…/node_modules/.bin/vite` (no extension), which would make us try
118
+ // to run agent-wrapper under the vite shim and silently fail.
119
+ const argv1 = process.argv[1]
120
+ if (argv1 && /\.(ts|tsx|mjs|cjs|js)$/.test(argv1)) {
121
+ return { executable: process.argv[0], prefixArgs: [argv1] }
122
+ }
123
+ // anything else — a bin shim such as `…/node_modules/.bin/vite`, or no
124
+ // argv[1] at all — names no rnx entry script, so say so instead of
125
+ // spawning the host runtime with no arguments.
126
+ throw new Error(
127
+ 'rnx CLI not found. set RNX_BIN to the path of the rnx binary, ' +
128
+ 'or build the workspace CLI via `bun run --cwd packages/sootsim build:cli`.',
129
+ )
130
+ }
131
+
132
+ function tryWorkspaceSootsim(): RnxSelfInvocation | null {
133
+ try {
134
+ const sootsimDir = resolveSootsimPackageDir()
135
+ if (!sootsimDir) return null
136
+ const binaryName = `rnx-${
137
+ process.platform === 'win32' ? 'windows' : process.platform
138
+ }-${process.arch}${process.platform === 'win32' ? '.exe' : ''}`
139
+ const distBinary = path.join(sootsimDir, 'dist-bin', binaryName)
140
+ if (fs.existsSync(distBinary)) return { executable: distBinary, prefixArgs: [] }
141
+ const distBin = path.join(sootsimDir, 'dist-cli', 'bin.js')
142
+ if (fs.existsSync(distBin)) {
143
+ try {
144
+ const src = path.join(sootsimDir, 'cli', 'commands', 'agent-wrapper.ts')
145
+ if (fs.existsSync(src)) {
146
+ const srcMtime = fs.statSync(src).mtimeMs
147
+ const buildMtime = fs.statSync(distBin).mtimeMs
148
+ if (buildMtime < srcMtime) {
149
+ console.warn(
150
+ `[rnx] dist-cli/bin.js is older than agent-wrapper.ts — ` +
151
+ `rebuild with \`bun run --cwd packages/sootsim build:cli\`.`,
152
+ )
153
+ }
154
+ }
155
+ } catch {}
156
+ return { executable: process.execPath, prefixArgs: [distBin] }
157
+ }
158
+ return null
159
+ } catch {
160
+ return null
161
+ }
162
+ }
163
+
164
+ /** locate the workspace sootsim package directory. tries `require.resolve`
165
+ * first (works in most contexts), then walks up from this module file
166
+ * looking for `packages/sootsim/package.json` (works when sootsim is
167
+ * loaded via vite's native TS resolution, where require.resolve doesn't
168
+ * know about the workspace). */
169
+ function resolveSootsimPackageDir(): string | null {
170
+ try {
171
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
172
+ const resolved = require.resolve(`${rnxPublicBrand.packageName}/package.json`)
173
+ return path.dirname(resolved)
174
+ } catch {}
175
+ // walk up from this file: .../packages/sootsim/src/agent-sessions.ts
176
+ // → .../packages/sootsim/. the module path is whatever the runtime
177
+ // gave us; when bundled by esbuild this will be the bundle path, not
178
+ // the source path, which is why we try require.resolve first.
179
+ const here = fileFromImportMeta()
180
+ if (!here) return null
181
+ let cur = path.dirname(here)
182
+ for (let i = 0; i < 8; i++) {
183
+ const pkg = path.join(cur, 'package.json')
184
+ try {
185
+ if (fs.existsSync(pkg)) {
186
+ const parsed = JSON.parse(fs.readFileSync(pkg, 'utf8')) as { name?: string }
187
+ if (parsed.name === rnxPublicBrand.packageName) return cur
188
+ }
189
+ } catch {}
190
+ const parent = path.dirname(cur)
191
+ if (parent === cur) break
192
+ cur = parent
193
+ }
194
+ return null
195
+ }
196
+
197
+ function fileFromImportMeta(): string | null {
198
+ try {
199
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
200
+ const url = (import.meta as unknown as { url?: string }).url
201
+ if (!url || !url.startsWith('file://')) return null
202
+ return decodeURIComponent(url.slice('file://'.length))
203
+ } catch {
204
+ return null
205
+ }
206
+ }
207
+
208
+ // --- lockfile for start races ---
209
+
210
+ async function withStartLock<T>(
211
+ projectId: string,
212
+ provider: Provider,
213
+ fn: () => Promise<T>,
214
+ ): Promise<T> {
215
+ const lockDir = path.join(getUserDataDir(), 'locks')
216
+ fs.mkdirSync(lockDir, { recursive: true })
217
+ try {
218
+ fs.chmodSync(lockDir, 0o700)
219
+ } catch {}
220
+ const lockPath = path.join(lockDir, `start-${projectId}-${provider}.lock`)
221
+ const deadline = Date.now() + 4000
222
+ let fd: number | null = null
223
+ while (fd === null) {
224
+ try {
225
+ fd = fs.openSync(
226
+ lockPath,
227
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
228
+ 0o600,
229
+ )
230
+ } catch (err) {
231
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err
232
+ // staleness check: if the pid in the lockfile is dead, steal it
233
+ try {
234
+ const stale = Number(fs.readFileSync(lockPath, 'utf8').trim())
235
+ if (stale && !isProcessAlive(stale)) {
236
+ fs.unlinkSync(lockPath)
237
+ continue
238
+ }
239
+ } catch {}
240
+ if (Date.now() > deadline) {
241
+ throw new Error(
242
+ `another start is in progress for project=${projectId} provider=${provider} ` +
243
+ `(lock: ${lockPath})`,
244
+ )
245
+ }
246
+ await new Promise((r) => setTimeout(r, 50))
247
+ }
248
+ }
249
+ try {
250
+ fs.writeFileSync(fd, String(process.pid))
251
+ return await fn()
252
+ } finally {
253
+ try {
254
+ fs.closeSync(fd)
255
+ } catch {}
256
+ try {
257
+ fs.unlinkSync(lockPath)
258
+ } catch {}
259
+ }
260
+ }
261
+
262
+ function isProcessAlive(pid: number): boolean {
263
+ try {
264
+ process.kill(pid, 0)
265
+ return true
266
+ } catch {
267
+ return false
268
+ }
269
+ }
270
+
271
+ // --- FIFO helpers ---
272
+
273
+ export function mkfifoSync(p: string): void {
274
+ const parent = path.dirname(p)
275
+ fs.mkdirSync(parent, { recursive: true })
276
+ try {
277
+ fs.chmodSync(parent, 0o700)
278
+ } catch {}
279
+ if (fs.existsSync(p)) {
280
+ try {
281
+ const stat = fs.statSync(p)
282
+ if (stat.isFIFO()) {
283
+ try {
284
+ fs.chmodSync(p, 0o600)
285
+ } catch {}
286
+ return
287
+ }
288
+ fs.unlinkSync(p)
289
+ } catch {
290
+ fs.unlinkSync(p)
291
+ }
292
+ }
293
+ const result = spawnSync('mkfifo', ['-m', '600', p])
294
+ if (result.status !== 0) {
295
+ throw new Error(
296
+ `mkfifo(${p}) failed: ${result.stderr?.toString().trim() || 'unknown error'}`,
297
+ )
298
+ }
299
+ }
300
+
301
+ // --- session lifecycle ---
302
+
303
+ export interface StartSessionOpts {
304
+ projectId: string
305
+ provider?: Provider
306
+ codexBin?: string
307
+ claudeBin?: string
308
+ freshThread?: boolean
309
+ readyTimeoutMs?: number
310
+ }
311
+
312
+ export interface StartSessionResult {
313
+ session: AgentSession
314
+ /** child pid — the same value stored on session.wrapperPid */
315
+ wrapperPid: number
316
+ }
317
+
318
+ export class AgentSessionError extends Error {
319
+ code: string
320
+ constructor(code: string, message: string) {
321
+ super(message)
322
+ this.code = code
323
+ }
324
+ }
325
+
326
+ export async function startSession(opts: StartSessionOpts): Promise<StartSessionResult> {
327
+ const project = findProjectById(opts.projectId)
328
+ if (!project) {
329
+ throw new AgentSessionError('NO_PROJECT', `no project with id=${opts.projectId}`)
330
+ }
331
+ const provider: Provider = opts.provider || project.preferredProvider || 'codex'
332
+
333
+ return withStartLock(project.id, provider, async () => {
334
+ // re-check inside the lock. a concurrent start that slipped through the
335
+ // pre-check can't slip through both the check AND the lock.
336
+ const existingLive = listSessions(project.id).find(
337
+ (s) =>
338
+ s.provider === provider && s.status !== 'ended' && pidIsAlive(s.wrapperPid, s.id),
339
+ )
340
+ if (existingLive) {
341
+ throw new AgentSessionError(
342
+ 'ALREADY_RUNNING',
343
+ `session already running for project=${project.id} provider=${provider} ` +
344
+ `(session ${existingLive.id}, pid ${existingLive.wrapperPid}). ` +
345
+ 'end it first with `rnx agent end <sessionId>`.',
346
+ )
347
+ }
348
+
349
+ // generate the claude session uuid once and persist it so successive
350
+ // wrapper restarts resume the same `~/.claude/projects/<cwd>/<uuid>.jsonl`
351
+ // file. codex has its own thread-id persistence via `thread/list`, so we
352
+ // only need this for claude.
353
+ const claudeSessionUuid = provider === 'claude' ? randomUUID() : undefined
354
+
355
+ const session = upsertSession({
356
+ projectId: project.id,
357
+ provider,
358
+ transport: 'pty',
359
+ cwd: project.cwd,
360
+ status: 'idle',
361
+ claudeSessionUuid,
362
+ })
363
+
364
+ const promptIn = promptFifoPath(session.id)
365
+ const eventsOut = eventsFifoPath(session.id)
366
+ const transcript = transcriptPath(session.id)
367
+ mkfifoSync(promptIn)
368
+ mkfifoSync(eventsOut)
369
+ // transcripts dir locked too
370
+ const transcriptDir = path.dirname(transcript)
371
+ fs.mkdirSync(transcriptDir, { recursive: true })
372
+ try {
373
+ fs.chmodSync(transcriptDir, 0o700)
374
+ } catch {}
375
+
376
+ const { executable, prefixArgs } = resolveSootsimInvocation()
377
+ const wrapperArgs = [
378
+ ...prefixArgs,
379
+ 'agent-wrapper',
380
+ '--session-id',
381
+ session.id,
382
+ '--project-id',
383
+ project.id,
384
+ '--provider',
385
+ provider,
386
+ '--cwd',
387
+ project.cwd,
388
+ '--prompt-in',
389
+ promptIn,
390
+ '--events-out',
391
+ eventsOut,
392
+ '--transcript',
393
+ transcript,
394
+ ]
395
+ if (opts.codexBin) wrapperArgs.push('--codex-bin', opts.codexBin)
396
+ if (opts.claudeBin) wrapperArgs.push('--claude-bin', opts.claudeBin)
397
+ if (opts.freshThread) wrapperArgs.push('--fresh-thread')
398
+ if (claudeSessionUuid) {
399
+ wrapperArgs.push('--claude-session-uuid', claudeSessionUuid)
400
+ }
401
+
402
+ const child = spawn(executable, wrapperArgs, {
403
+ detached: true,
404
+ stdio: 'ignore',
405
+ env: {
406
+ ...process.env,
407
+ SOOTSIM_USER_DATA_DIR: getUserDataDir(),
408
+ },
409
+ })
410
+ child.unref()
411
+
412
+ const readyTimeout = opts.readyTimeoutMs ?? 6000
413
+ const boot = await waitForFirstEvent(
414
+ eventsOut,
415
+ (e) => e.type === 'ready' || e.type === 'error',
416
+ readyTimeout,
417
+ )
418
+ if (!boot || boot.type === 'error') {
419
+ if (child.pid) {
420
+ try {
421
+ process.kill(child.pid, 'SIGTERM')
422
+ } catch {}
423
+ }
424
+ try {
425
+ fs.rmSync(sessionDir(session.id), { recursive: true, force: true })
426
+ } catch {}
427
+ updateSessionStatus(session.id, { status: 'ended' })
428
+ const reason =
429
+ boot && boot.type === 'error'
430
+ ? boot.message
431
+ : `no ready event within ${readyTimeout}ms`
432
+ throw new AgentSessionError('WRAPPER_FAILED', reason)
433
+ }
434
+
435
+ updateSessionStatus(session.id, {
436
+ wrapperPid: child.pid,
437
+ status: 'idle',
438
+ })
439
+ const updated = findSessionById(session.id)!
440
+ return { session: updated, wrapperPid: child.pid! }
441
+ })
442
+ }
443
+
444
+ export async function sendPrompt(
445
+ sessionId: string,
446
+ prompt: AgentPromptEnvelope,
447
+ ): Promise<void> {
448
+ const session = findSessionById(sessionId)
449
+ if (!session) {
450
+ throw new AgentSessionError('NO_SESSION', `no session with id=${sessionId}`)
451
+ }
452
+ if (!pidIsAlive(session.wrapperPid, sessionId)) {
453
+ updateSessionStatus(sessionId, { status: 'ended' })
454
+ throw new AgentSessionError(
455
+ 'NOT_ALIVE',
456
+ `session wrapper is not alive (pid=${session.wrapperPid}). start a new session.`,
457
+ )
458
+ }
459
+ const fifo = promptFifoPath(sessionId)
460
+ if (!fs.existsSync(fifo)) {
461
+ throw new AgentSessionError('NO_FIFO', `prompt FIFO missing: ${fifo}`)
462
+ }
463
+ const fd = fs.openSync(fifo, fsConstants.O_WRONLY)
464
+ try {
465
+ const wireText = encodeAgentPromptEnvelope(prompt)
466
+ if (!wireText) {
467
+ throw new AgentSessionError('EMPTY_PROMPT', 'prompt text is empty')
468
+ }
469
+ fs.writeSync(fd, wireText + '\n')
470
+ } finally {
471
+ fs.closeSync(fd)
472
+ }
473
+ updateSessionStatus(sessionId, {
474
+ lastPrompt: prompt.displayText ?? prompt.text,
475
+ status: 'working',
476
+ })
477
+ }
478
+
479
+ export async function endSession(sessionId: string): Promise<void> {
480
+ const session = findSessionById(sessionId)
481
+ if (!session) {
482
+ throw new AgentSessionError('NO_SESSION', `no session with id=${sessionId}`)
483
+ }
484
+ if (pidIsAlive(session.wrapperPid, sessionId)) {
485
+ try {
486
+ process.kill(session.wrapperPid!, 'SIGTERM')
487
+ } catch {}
488
+ }
489
+ // guard rm: only delete paths we fully own, derived from getUserDataDir()
490
+ const dir = sessionDir(sessionId)
491
+ const base = getUserDataDir()
492
+ if (dir.startsWith(base)) {
493
+ try {
494
+ fs.rmSync(dir, { recursive: true, force: true })
495
+ } catch {}
496
+ }
497
+ updateSessionStatus(sessionId, { status: 'ended', wrapperPid: undefined })
498
+ }
499
+
500
+ // --- event subscription ---
501
+
502
+ /** open events.out and stream events to onEvent. returns an unsubscribe fn.
503
+ * this is a single-reader channel — only ONE caller (the bridge daemon's
504
+ * AgentHost) should hold a live subscription at a time. every other
505
+ * consumer (electron, cli watch, browser shell) routes through the daemon
506
+ * via the agent:* ws protocol, where the daemon fans events out to N
507
+ * subscribers without re-reading the FIFO. */
508
+ export function subscribeEvents(
509
+ sessionId: string,
510
+ onEvent: (event: AgentEvent) => void,
511
+ ): () => void {
512
+ const fifo = eventsFifoPath(sessionId)
513
+ if (!fs.existsSync(fifo)) {
514
+ throw new AgentSessionError('NO_FIFO', `events FIFO missing: ${fifo}`)
515
+ }
516
+ // O_RDWR keeps the FIFO open even if every writer closes. the empty line in
517
+ // unsubscribe wakes any blocking read before destroy closes the fd; without
518
+ // that wake, repeated daemon teardown leaves reads occupying libuv workers
519
+ // and later sessions stop receiving events.
520
+ const fd = fs.openSync(fifo, fsConstants.O_RDWR)
521
+ const stream = fs.createReadStream('', { fd, autoClose: true })
522
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
523
+ rl.on('line', (line) => {
524
+ const event = parseAgentEventLine(line)
525
+ if (event) onEvent(event)
526
+ })
527
+ let closed = false
528
+ return () => {
529
+ if (closed) return
530
+ closed = true
531
+ try {
532
+ fs.writeSync(fd, '\n')
533
+ } catch {}
534
+ try {
535
+ rl.close()
536
+ } catch {}
537
+ try {
538
+ stream.destroy()
539
+ } catch {}
540
+ }
541
+ }
542
+
543
+ // --- wait for a specific event (used by startSession + watch tools) ---
544
+
545
+ async function waitForFirstEvent(
546
+ fifo: string,
547
+ predicate: (e: AgentEvent) => boolean,
548
+ timeoutMs: number,
549
+ ): Promise<AgentEvent | null> {
550
+ const fd = fs.openSync(fifo, fsConstants.O_RDWR | fsConstants.O_NONBLOCK)
551
+ const buf = Buffer.alloc(8192)
552
+ let leftover = ''
553
+ const deadline = Date.now() + timeoutMs
554
+ try {
555
+ while (Date.now() < deadline) {
556
+ let n = 0
557
+ try {
558
+ n = fs.readSync(fd, buf, 0, buf.length, null)
559
+ } catch (err) {
560
+ if ((err as NodeJS.ErrnoException).code !== 'EAGAIN') throw err
561
+ n = 0
562
+ }
563
+ if (n > 0) {
564
+ leftover += buf.subarray(0, n).toString('utf8')
565
+ let idx: number
566
+ while ((idx = leftover.indexOf('\n')) >= 0) {
567
+ const line = leftover.slice(0, idx)
568
+ leftover = leftover.slice(idx + 1)
569
+ const event = parseAgentEventLine(line)
570
+ if (event && predicate(event)) return event
571
+ }
572
+ } else {
573
+ await new Promise((r) => setTimeout(r, 30))
574
+ }
575
+ }
576
+ return null
577
+ } finally {
578
+ fs.closeSync(fd)
579
+ }
580
+ }
package/src/app-url.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { InspectBridge } from '../cli/commands/inspect/core'
2
+
3
+ const APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/
4
+
5
+ export function normalizeAppUrlTarget(target: string): string {
6
+ const trimmed = target.trim()
7
+ if (!trimmed) {
8
+ throw new Error('expected <url-or-route>')
9
+ }
10
+ if (APP_URL_SCHEME_RE.test(trimmed) || trimmed.startsWith('/')) {
11
+ return trimmed
12
+ }
13
+ return `/${trimmed}`
14
+ }
15
+
16
+ export function isAppDeepLinkTarget(target: string): boolean {
17
+ const trimmed = target.trim()
18
+ if (!trimmed) return false
19
+ if (trimmed.startsWith('/')) return true
20
+ if (!APP_URL_SCHEME_RE.test(trimmed)) return false
21
+ return !/^https?:\/\//i.test(trimmed)
22
+ }
23
+
24
+ export async function openAppUrl(bridge: InspectBridge, target: string): Promise<string> {
25
+ const normalized = normalizeAppUrlTarget(target)
26
+ const result: unknown = await bridge.send({
27
+ type: 'openUrl',
28
+ url: normalized,
29
+ })
30
+ if (
31
+ result !== null &&
32
+ typeof result === 'object' &&
33
+ Reflect.get(result, 'ok') === false
34
+ ) {
35
+ const error = Reflect.get(result, 'error')
36
+ throw new Error(typeof error === 'string' ? error : 'deep-link dispatch failed')
37
+ }
38
+ return normalized
39
+ }