parasor 0.1.0

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 (633) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/THIRD-PARTY-NOTICES.md +138 -0
  4. package/bin/parasor.mjs +3 -0
  5. package/node_modules/@parasor/shared/dist/client.d.ts +8 -0
  6. package/node_modules/@parasor/shared/dist/client.d.ts.map +1 -0
  7. package/node_modules/@parasor/shared/dist/client.js +2 -0
  8. package/node_modules/@parasor/shared/dist/client.js.map +1 -0
  9. package/node_modules/@parasor/shared/dist/drops.d.ts +24 -0
  10. package/node_modules/@parasor/shared/dist/drops.d.ts.map +1 -0
  11. package/node_modules/@parasor/shared/dist/drops.js +2 -0
  12. package/node_modules/@parasor/shared/dist/drops.js.map +1 -0
  13. package/node_modules/@parasor/shared/dist/file-uploads.d.ts +56 -0
  14. package/node_modules/@parasor/shared/dist/file-uploads.d.ts.map +1 -0
  15. package/node_modules/@parasor/shared/dist/file-uploads.js +13 -0
  16. package/node_modules/@parasor/shared/dist/file-uploads.js.map +1 -0
  17. package/node_modules/@parasor/shared/dist/ide-commands.d.ts +8 -0
  18. package/node_modules/@parasor/shared/dist/ide-commands.d.ts.map +1 -0
  19. package/node_modules/@parasor/shared/dist/ide-commands.js +59 -0
  20. package/node_modules/@parasor/shared/dist/ide-commands.js.map +1 -0
  21. package/node_modules/@parasor/shared/dist/pane-commands.d.ts +7 -0
  22. package/node_modules/@parasor/shared/dist/pane-commands.d.ts.map +1 -0
  23. package/node_modules/@parasor/shared/dist/pane-commands.js +47 -0
  24. package/node_modules/@parasor/shared/dist/pane-commands.js.map +1 -0
  25. package/node_modules/@parasor/shared/dist/pane-model.d.ts +63 -0
  26. package/node_modules/@parasor/shared/dist/pane-model.d.ts.map +1 -0
  27. package/node_modules/@parasor/shared/dist/pane-model.js +89 -0
  28. package/node_modules/@parasor/shared/dist/pane-model.js.map +1 -0
  29. package/node_modules/@parasor/shared/dist/panes.d.ts +33 -0
  30. package/node_modules/@parasor/shared/dist/panes.d.ts.map +1 -0
  31. package/node_modules/@parasor/shared/dist/panes.js +2 -0
  32. package/node_modules/@parasor/shared/dist/panes.js.map +1 -0
  33. package/node_modules/@parasor/shared/dist/runtime.d.ts +180 -0
  34. package/node_modules/@parasor/shared/dist/runtime.d.ts.map +1 -0
  35. package/node_modules/@parasor/shared/dist/runtime.js +2 -0
  36. package/node_modules/@parasor/shared/dist/runtime.js.map +1 -0
  37. package/node_modules/@parasor/shared/dist/state.d.ts +192 -0
  38. package/node_modules/@parasor/shared/dist/state.d.ts.map +1 -0
  39. package/node_modules/@parasor/shared/dist/state.js +10 -0
  40. package/node_modules/@parasor/shared/dist/state.js.map +1 -0
  41. package/node_modules/@parasor/shared/dist/terminal.d.ts +191 -0
  42. package/node_modules/@parasor/shared/dist/terminal.d.ts.map +1 -0
  43. package/node_modules/@parasor/shared/dist/terminal.js +174 -0
  44. package/node_modules/@parasor/shared/dist/terminal.js.map +1 -0
  45. package/node_modules/@parasor/shared/dist/types.d.ts +13 -0
  46. package/node_modules/@parasor/shared/dist/types.d.ts.map +1 -0
  47. package/node_modules/@parasor/shared/dist/types.js +13 -0
  48. package/node_modules/@parasor/shared/dist/types.js.map +1 -0
  49. package/node_modules/@parasor/shared/dist/worktree-local-files.d.ts +14 -0
  50. package/node_modules/@parasor/shared/dist/worktree-local-files.d.ts.map +1 -0
  51. package/node_modules/@parasor/shared/dist/worktree-local-files.js +41 -0
  52. package/node_modules/@parasor/shared/dist/worktree-local-files.js.map +1 -0
  53. package/node_modules/@parasor/shared/dist/ws-events.d.ts +152 -0
  54. package/node_modules/@parasor/shared/dist/ws-events.d.ts.map +1 -0
  55. package/node_modules/@parasor/shared/dist/ws-events.js +2 -0
  56. package/node_modules/@parasor/shared/dist/ws-events.js.map +1 -0
  57. package/node_modules/@parasor/shared/package.json +13 -0
  58. package/package.json +61 -0
  59. package/server/agent-detector/agent-state-store.d.ts +23 -0
  60. package/server/agent-detector/agent-state-store.d.ts.map +1 -0
  61. package/server/agent-detector/agent-state-store.js +110 -0
  62. package/server/agent-detector/agent-state-store.js.map +1 -0
  63. package/server/agent-detector/detector.d.ts +80 -0
  64. package/server/agent-detector/detector.d.ts.map +1 -0
  65. package/server/agent-detector/detector.js +219 -0
  66. package/server/agent-detector/detector.js.map +1 -0
  67. package/server/agent-detector/event-map.d.ts +25 -0
  68. package/server/agent-detector/event-map.d.ts.map +1 -0
  69. package/server/agent-detector/event-map.js +136 -0
  70. package/server/agent-detector/event-map.js.map +1 -0
  71. package/server/agent-detector/manual-agent-tracker.d.ts +19 -0
  72. package/server/agent-detector/manual-agent-tracker.d.ts.map +1 -0
  73. package/server/agent-detector/manual-agent-tracker.js +123 -0
  74. package/server/agent-detector/manual-agent-tracker.js.map +1 -0
  75. package/server/agent-detector/output-eligibility.d.ts +3 -0
  76. package/server/agent-detector/output-eligibility.d.ts.map +1 -0
  77. package/server/agent-detector/output-eligibility.js +39 -0
  78. package/server/agent-detector/output-eligibility.js.map +1 -0
  79. package/server/application/files/errors.d.ts +28 -0
  80. package/server/application/files/errors.d.ts.map +1 -0
  81. package/server/application/files/errors.js +55 -0
  82. package/server/application/files/errors.js.map +1 -0
  83. package/server/application/files/local-filesystem.d.ts +36 -0
  84. package/server/application/files/local-filesystem.d.ts.map +1 -0
  85. package/server/application/files/local-filesystem.js +171 -0
  86. package/server/application/files/local-filesystem.js.map +1 -0
  87. package/server/application/files/project-file-queries.d.ts +16 -0
  88. package/server/application/files/project-file-queries.d.ts.map +1 -0
  89. package/server/application/files/project-file-queries.js +126 -0
  90. package/server/application/files/project-file-queries.js.map +1 -0
  91. package/server/application/integrations/errors.d.ts +16 -0
  92. package/server/application/integrations/errors.d.ts.map +1 -0
  93. package/server/application/integrations/errors.js +31 -0
  94. package/server/application/integrations/errors.js.map +1 -0
  95. package/server/application/integrations/hook-notify.d.ts +32 -0
  96. package/server/application/integrations/hook-notify.d.ts.map +1 -0
  97. package/server/application/integrations/hook-notify.js +118 -0
  98. package/server/application/integrations/hook-notify.js.map +1 -0
  99. package/server/application/integrations/open-url.d.ts +11 -0
  100. package/server/application/integrations/open-url.d.ts.map +1 -0
  101. package/server/application/integrations/open-url.js +23 -0
  102. package/server/application/integrations/open-url.js.map +1 -0
  103. package/server/application/ports.d.ts +11 -0
  104. package/server/application/ports.d.ts.map +1 -0
  105. package/server/application/ports.js +2 -0
  106. package/server/application/ports.js.map +1 -0
  107. package/server/application/workspace/errors.d.ts +21 -0
  108. package/server/application/workspace/errors.d.ts.map +1 -0
  109. package/server/application/workspace/errors.js +21 -0
  110. package/server/application/workspace/errors.js.map +1 -0
  111. package/server/application/workspace/pane-commands.d.ts +47 -0
  112. package/server/application/workspace/pane-commands.d.ts.map +1 -0
  113. package/server/application/workspace/pane-commands.js +193 -0
  114. package/server/application/workspace/pane-commands.js.map +1 -0
  115. package/server/application/workspace/project-commands.d.ts +34 -0
  116. package/server/application/workspace/project-commands.d.ts.map +1 -0
  117. package/server/application/workspace/project-commands.js +81 -0
  118. package/server/application/workspace/project-commands.js.map +1 -0
  119. package/server/application/workspace/project-queries.d.ts +21 -0
  120. package/server/application/workspace/project-queries.d.ts.map +1 -0
  121. package/server/application/workspace/project-queries.js +216 -0
  122. package/server/application/workspace/project-queries.js.map +1 -0
  123. package/server/application/workspace/session-commands.d.ts +24 -0
  124. package/server/application/workspace/session-commands.d.ts.map +1 -0
  125. package/server/application/workspace/session-commands.js +123 -0
  126. package/server/application/workspace/session-commands.js.map +1 -0
  127. package/server/application/workspace/session-queries.d.ts +14 -0
  128. package/server/application/workspace/session-queries.d.ts.map +1 -0
  129. package/server/application/workspace/session-queries.js +47 -0
  130. package/server/application/workspace/session-queries.js.map +1 -0
  131. package/server/application/workspace/worktree-commands.d.ts +98 -0
  132. package/server/application/workspace/worktree-commands.d.ts.map +1 -0
  133. package/server/application/workspace/worktree-commands.js +323 -0
  134. package/server/application/workspace/worktree-commands.js.map +1 -0
  135. package/server/application/workspace/worktree-local-files.d.ts +11 -0
  136. package/server/application/workspace/worktree-local-files.d.ts.map +1 -0
  137. package/server/application/workspace/worktree-local-files.js +223 -0
  138. package/server/application/workspace/worktree-local-files.js.map +1 -0
  139. package/server/application/workspace/worktree-reconcile.d.ts +21 -0
  140. package/server/application/workspace/worktree-reconcile.d.ts.map +1 -0
  141. package/server/application/workspace/worktree-reconcile.js +49 -0
  142. package/server/application/workspace/worktree-reconcile.js.map +1 -0
  143. package/server/auth/origin.d.ts +17 -0
  144. package/server/auth/origin.d.ts.map +1 -0
  145. package/server/auth/origin.js +93 -0
  146. package/server/auth/origin.js.map +1 -0
  147. package/server/auth/pairing-token.d.ts +28 -0
  148. package/server/auth/pairing-token.d.ts.map +1 -0
  149. package/server/auth/pairing-token.js +46 -0
  150. package/server/auth/pairing-token.js.map +1 -0
  151. package/server/auth/token-exchange.d.ts +11 -0
  152. package/server/auth/token-exchange.d.ts.map +1 -0
  153. package/server/auth/token-exchange.js +27 -0
  154. package/server/auth/token-exchange.js.map +1 -0
  155. package/server/auth/token.d.ts +18 -0
  156. package/server/auth/token.d.ts.map +1 -0
  157. package/server/auth/token.js +115 -0
  158. package/server/auth/token.js.map +1 -0
  159. package/server/bootstrap/create-app-server.d.ts +49 -0
  160. package/server/bootstrap/create-app-server.d.ts.map +1 -0
  161. package/server/bootstrap/create-app-server.js +154 -0
  162. package/server/bootstrap/create-app-server.js.map +1 -0
  163. package/server/bootstrap/project-runtime.d.ts +29 -0
  164. package/server/bootstrap/project-runtime.d.ts.map +1 -0
  165. package/server/bootstrap/project-runtime.js +253 -0
  166. package/server/bootstrap/project-runtime.js.map +1 -0
  167. package/server/bootstrap/pty-env.d.ts +3 -0
  168. package/server/bootstrap/pty-env.d.ts.map +1 -0
  169. package/server/bootstrap/pty-env.js +15 -0
  170. package/server/bootstrap/pty-env.js.map +1 -0
  171. package/server/bootstrap/reconcile-state.d.ts +16 -0
  172. package/server/bootstrap/reconcile-state.d.ts.map +1 -0
  173. package/server/bootstrap/reconcile-state.js +73 -0
  174. package/server/bootstrap/reconcile-state.js.map +1 -0
  175. package/server/bootstrap/runtime-loops.d.ts +51 -0
  176. package/server/bootstrap/runtime-loops.d.ts.map +1 -0
  177. package/server/bootstrap/runtime-loops.js +189 -0
  178. package/server/bootstrap/runtime-loops.js.map +1 -0
  179. package/server/bootstrap/runtime-port.d.ts +4 -0
  180. package/server/bootstrap/runtime-port.d.ts.map +1 -0
  181. package/server/bootstrap/runtime-port.js +52 -0
  182. package/server/bootstrap/runtime-port.js.map +1 -0
  183. package/server/bootstrap/safety-gate.d.ts +22 -0
  184. package/server/bootstrap/safety-gate.d.ts.map +1 -0
  185. package/server/bootstrap/safety-gate.js +43 -0
  186. package/server/bootstrap/safety-gate.js.map +1 -0
  187. package/server/bootstrap/shutdown-marker.d.ts +35 -0
  188. package/server/bootstrap/shutdown-marker.d.ts.map +1 -0
  189. package/server/bootstrap/shutdown-marker.js +100 -0
  190. package/server/bootstrap/shutdown-marker.js.map +1 -0
  191. package/server/bootstrap/shutdown-runtime.d.ts +62 -0
  192. package/server/bootstrap/shutdown-runtime.d.ts.map +1 -0
  193. package/server/bootstrap/shutdown-runtime.js +66 -0
  194. package/server/bootstrap/shutdown-runtime.js.map +1 -0
  195. package/server/bootstrap/startup-banner.d.ts +34 -0
  196. package/server/bootstrap/startup-banner.d.ts.map +1 -0
  197. package/server/bootstrap/startup-banner.js +114 -0
  198. package/server/bootstrap/startup-banner.js.map +1 -0
  199. package/server/bootstrap/wire-runtime.d.ts +42 -0
  200. package/server/bootstrap/wire-runtime.d.ts.map +1 -0
  201. package/server/bootstrap/wire-runtime.js +205 -0
  202. package/server/bootstrap/wire-runtime.js.map +1 -0
  203. package/server/cli/help.d.ts +3 -0
  204. package/server/cli/help.d.ts.map +1 -0
  205. package/server/cli/help.js +39 -0
  206. package/server/cli/help.js.map +1 -0
  207. package/server/cli/hook-client.d.ts +12 -0
  208. package/server/cli/hook-client.d.ts.map +1 -0
  209. package/server/cli/hook-client.js +81 -0
  210. package/server/cli/hook-client.js.map +1 -0
  211. package/server/cli/hook.d.ts +3 -0
  212. package/server/cli/hook.d.ts.map +1 -0
  213. package/server/cli/hook.js +152 -0
  214. package/server/cli/hook.js.map +1 -0
  215. package/server/cli/main.d.ts +2 -0
  216. package/server/cli/main.d.ts.map +1 -0
  217. package/server/cli/main.js +133 -0
  218. package/server/cli/main.js.map +1 -0
  219. package/server/cli/notify.d.ts +19 -0
  220. package/server/cli/notify.d.ts.map +1 -0
  221. package/server/cli/notify.js +77 -0
  222. package/server/cli/notify.js.map +1 -0
  223. package/server/cli/open.d.ts +2 -0
  224. package/server/cli/open.d.ts.map +1 -0
  225. package/server/cli/open.js +47 -0
  226. package/server/cli/open.js.map +1 -0
  227. package/server/cli/probe-daemon-version.d.ts +46 -0
  228. package/server/cli/probe-daemon-version.d.ts.map +1 -0
  229. package/server/cli/probe-daemon-version.js +167 -0
  230. package/server/cli/probe-daemon-version.js.map +1 -0
  231. package/server/cli/pty-host.d.ts +36 -0
  232. package/server/cli/pty-host.d.ts.map +1 -0
  233. package/server/cli/pty-host.js +390 -0
  234. package/server/cli/pty-host.js.map +1 -0
  235. package/server/cli/qr.d.ts +2 -0
  236. package/server/cli/qr.d.ts.map +1 -0
  237. package/server/cli/qr.js +70 -0
  238. package/server/cli/qr.js.map +1 -0
  239. package/server/cli/restart-confirm.d.ts +33 -0
  240. package/server/cli/restart-confirm.d.ts.map +1 -0
  241. package/server/cli/restart-confirm.js +107 -0
  242. package/server/cli/restart-confirm.js.map +1 -0
  243. package/server/cli/restart.d.ts +17 -0
  244. package/server/cli/restart.d.ts.map +1 -0
  245. package/server/cli/restart.js +145 -0
  246. package/server/cli/restart.js.map +1 -0
  247. package/server/cli/service-darwin.d.ts +70 -0
  248. package/server/cli/service-darwin.d.ts.map +1 -0
  249. package/server/cli/service-darwin.js +562 -0
  250. package/server/cli/service-darwin.js.map +1 -0
  251. package/server/cli/service-linux.d.ts +81 -0
  252. package/server/cli/service-linux.d.ts.map +1 -0
  253. package/server/cli/service-linux.js +412 -0
  254. package/server/cli/service-linux.js.map +1 -0
  255. package/server/cli/service.d.ts +33 -0
  256. package/server/cli/service.d.ts.map +1 -0
  257. package/server/cli/service.js +133 -0
  258. package/server/cli/service.js.map +1 -0
  259. package/server/cli/shim-installer.d.ts +35 -0
  260. package/server/cli/shim-installer.d.ts.map +1 -0
  261. package/server/cli/shim-installer.js +662 -0
  262. package/server/cli/shim-installer.js.map +1 -0
  263. package/server/cli/shim-open.d.ts +2 -0
  264. package/server/cli/shim-open.d.ts.map +1 -0
  265. package/server/cli/shim-open.js +128 -0
  266. package/server/cli/shim-open.js.map +1 -0
  267. package/server/cli/shutdown-deps.d.ts +41 -0
  268. package/server/cli/shutdown-deps.d.ts.map +1 -0
  269. package/server/cli/shutdown-deps.js +158 -0
  270. package/server/cli/shutdown-deps.js.map +1 -0
  271. package/server/cli/stop.d.ts +11 -0
  272. package/server/cli/stop.d.ts.map +1 -0
  273. package/server/cli/stop.js +123 -0
  274. package/server/cli/stop.js.map +1 -0
  275. package/server/cli/unknown-command.d.ts +8 -0
  276. package/server/cli/unknown-command.d.ts.map +1 -0
  277. package/server/cli/unknown-command.js +14 -0
  278. package/server/cli/unknown-command.js.map +1 -0
  279. package/server/debug/agent-status-recorder.d.ts +57 -0
  280. package/server/debug/agent-status-recorder.d.ts.map +1 -0
  281. package/server/debug/agent-status-recorder.js +209 -0
  282. package/server/debug/agent-status-recorder.js.map +1 -0
  283. package/server/debug/terminal-trace-recorder.d.ts +41 -0
  284. package/server/debug/terminal-trace-recorder.d.ts.map +1 -0
  285. package/server/debug/terminal-trace-recorder.js +112 -0
  286. package/server/debug/terminal-trace-recorder.js.map +1 -0
  287. package/server/fonts/catalog.d.ts +35 -0
  288. package/server/fonts/catalog.d.ts.map +1 -0
  289. package/server/fonts/catalog.js +76 -0
  290. package/server/fonts/catalog.js.map +1 -0
  291. package/server/fonts/installer.d.ts +45 -0
  292. package/server/fonts/installer.d.ts.map +1 -0
  293. package/server/fonts/installer.js +201 -0
  294. package/server/fonts/installer.js.map +1 -0
  295. package/server/fonts/routes.d.ts +4 -0
  296. package/server/fonts/routes.d.ts.map +1 -0
  297. package/server/fonts/routes.js +91 -0
  298. package/server/fonts/routes.js.map +1 -0
  299. package/server/fs/drops.d.ts +50 -0
  300. package/server/fs/drops.d.ts.map +1 -0
  301. package/server/fs/drops.js +137 -0
  302. package/server/fs/drops.js.map +1 -0
  303. package/server/fs/file-uploads.d.ts +57 -0
  304. package/server/fs/file-uploads.d.ts.map +1 -0
  305. package/server/fs/file-uploads.js +214 -0
  306. package/server/fs/file-uploads.js.map +1 -0
  307. package/server/fs/file-watcher.d.ts +22 -0
  308. package/server/fs/file-watcher.d.ts.map +1 -0
  309. package/server/fs/file-watcher.js +105 -0
  310. package/server/fs/file-watcher.js.map +1 -0
  311. package/server/fs/git-watcher.d.ts +76 -0
  312. package/server/fs/git-watcher.d.ts.map +1 -0
  313. package/server/fs/git-watcher.js +356 -0
  314. package/server/fs/git-watcher.js.map +1 -0
  315. package/server/fs/media.d.ts +35 -0
  316. package/server/fs/media.d.ts.map +1 -0
  317. package/server/fs/media.js +271 -0
  318. package/server/fs/media.js.map +1 -0
  319. package/server/fs/service.d.ts +97 -0
  320. package/server/fs/service.d.ts.map +1 -0
  321. package/server/fs/service.js +306 -0
  322. package/server/fs/service.js.map +1 -0
  323. package/server/fs/upload-staging.d.ts +76 -0
  324. package/server/fs/upload-staging.d.ts.map +1 -0
  325. package/server/fs/upload-staging.js +283 -0
  326. package/server/fs/upload-staging.js.map +1 -0
  327. package/server/fs/watcher-lifecycle.d.ts +24 -0
  328. package/server/fs/watcher-lifecycle.d.ts.map +1 -0
  329. package/server/fs/watcher-lifecycle.js +105 -0
  330. package/server/fs/watcher-lifecycle.js.map +1 -0
  331. package/server/index.d.ts +17 -0
  332. package/server/index.d.ts.map +1 -0
  333. package/server/index.js +510 -0
  334. package/server/index.js.map +1 -0
  335. package/server/ipc/socket-server.d.ts +28 -0
  336. package/server/ipc/socket-server.d.ts.map +1 -0
  337. package/server/ipc/socket-server.js +206 -0
  338. package/server/ipc/socket-server.js.map +1 -0
  339. package/server/lib/git-exec.d.ts +38 -0
  340. package/server/lib/git-exec.d.ts.map +1 -0
  341. package/server/lib/git-exec.js +96 -0
  342. package/server/lib/git-exec.js.map +1 -0
  343. package/server/lib/open-in-ide.d.ts +22 -0
  344. package/server/lib/open-in-ide.d.ts.map +1 -0
  345. package/server/lib/open-in-ide.js +90 -0
  346. package/server/lib/open-in-ide.js.map +1 -0
  347. package/server/lib/open-in-os.d.ts +20 -0
  348. package/server/lib/open-in-os.d.ts.map +1 -0
  349. package/server/lib/open-in-os.js +51 -0
  350. package/server/lib/open-in-os.js.map +1 -0
  351. package/server/lib/path.d.ts +2 -0
  352. package/server/lib/path.d.ts.map +1 -0
  353. package/server/lib/path.js +10 -0
  354. package/server/lib/path.js.map +1 -0
  355. package/server/lib/promise-mutex.d.ts +7 -0
  356. package/server/lib/promise-mutex.d.ts.map +1 -0
  357. package/server/lib/promise-mutex.js +26 -0
  358. package/server/lib/promise-mutex.js.map +1 -0
  359. package/server/lib/sd-notify.d.ts +8 -0
  360. package/server/lib/sd-notify.d.ts.map +1 -0
  361. package/server/lib/sd-notify.js +27 -0
  362. package/server/lib/sd-notify.js.map +1 -0
  363. package/server/net/local-machine.d.ts +7 -0
  364. package/server/net/local-machine.d.ts.map +1 -0
  365. package/server/net/local-machine.js +79 -0
  366. package/server/net/local-machine.js.map +1 -0
  367. package/server/net/reachable-host.d.ts +12 -0
  368. package/server/net/reachable-host.d.ts.map +1 -0
  369. package/server/net/reachable-host.js +25 -0
  370. package/server/net/reachable-host.js.map +1 -0
  371. package/server/network/endpoints.d.ts +14 -0
  372. package/server/network/endpoints.d.ts.map +1 -0
  373. package/server/network/endpoints.js +104 -0
  374. package/server/network/endpoints.js.map +1 -0
  375. package/server/network/qr.d.ts +16 -0
  376. package/server/network/qr.d.ts.map +1 -0
  377. package/server/network/qr.js +89 -0
  378. package/server/network/qr.js.map +1 -0
  379. package/server/port-forwarder/forwarder.d.ts +47 -0
  380. package/server/port-forwarder/forwarder.d.ts.map +1 -0
  381. package/server/port-forwarder/forwarder.js +159 -0
  382. package/server/port-forwarder/forwarder.js.map +1 -0
  383. package/server/port-scanner/scanner.d.ts +24 -0
  384. package/server/port-scanner/scanner.d.ts.map +1 -0
  385. package/server/port-scanner/scanner.js +193 -0
  386. package/server/port-scanner/scanner.js.map +1 -0
  387. package/server/pty/connection-lifecycle.d.ts +50 -0
  388. package/server/pty/connection-lifecycle.d.ts.map +1 -0
  389. package/server/pty/connection-lifecycle.js +113 -0
  390. package/server/pty/connection-lifecycle.js.map +1 -0
  391. package/server/pty/daemon-connect.d.ts +52 -0
  392. package/server/pty/daemon-connect.d.ts.map +1 -0
  393. package/server/pty/daemon-connect.js +62 -0
  394. package/server/pty/daemon-connect.js.map +1 -0
  395. package/server/pty/handshake-timeout-race.d.ts +22 -0
  396. package/server/pty/handshake-timeout-race.d.ts.map +1 -0
  397. package/server/pty/handshake-timeout-race.js +23 -0
  398. package/server/pty/handshake-timeout-race.js.map +1 -0
  399. package/server/pty/headless-replay-snapshot.d.ts +28 -0
  400. package/server/pty/headless-replay-snapshot.d.ts.map +1 -0
  401. package/server/pty/headless-replay-snapshot.js +285 -0
  402. package/server/pty/headless-replay-snapshot.js.map +1 -0
  403. package/server/pty/headless-terminal-state-cache.d.ts +31 -0
  404. package/server/pty/headless-terminal-state-cache.d.ts.map +1 -0
  405. package/server/pty/headless-terminal-state-cache.js +119 -0
  406. package/server/pty/headless-terminal-state-cache.js.map +1 -0
  407. package/server/pty/hello-ack-validator.d.ts +15 -0
  408. package/server/pty/hello-ack-validator.d.ts.map +1 -0
  409. package/server/pty/hello-ack-validator.js +30 -0
  410. package/server/pty/hello-ack-validator.js.map +1 -0
  411. package/server/pty/host-daemon/bootstrap.d.ts +100 -0
  412. package/server/pty/host-daemon/bootstrap.d.ts.map +1 -0
  413. package/server/pty/host-daemon/bootstrap.js +611 -0
  414. package/server/pty/host-daemon/bootstrap.js.map +1 -0
  415. package/server/pty/host-daemon/daemon.d.ts +111 -0
  416. package/server/pty/host-daemon/daemon.d.ts.map +1 -0
  417. package/server/pty/host-daemon/daemon.js +648 -0
  418. package/server/pty/host-daemon/daemon.js.map +1 -0
  419. package/server/pty/host-daemon/entry.d.ts +3 -0
  420. package/server/pty/host-daemon/entry.d.ts.map +1 -0
  421. package/server/pty/host-daemon/entry.js +72 -0
  422. package/server/pty/host-daemon/entry.js.map +1 -0
  423. package/server/pty/host-daemon/lockfile.d.ts +12 -0
  424. package/server/pty/host-daemon/lockfile.d.ts.map +1 -0
  425. package/server/pty/host-daemon/lockfile.js +120 -0
  426. package/server/pty/host-daemon/lockfile.js.map +1 -0
  427. package/server/pty/host-daemon/mode-marker.d.ts +77 -0
  428. package/server/pty/host-daemon/mode-marker.d.ts.map +1 -0
  429. package/server/pty/host-daemon/mode-marker.js +228 -0
  430. package/server/pty/host-daemon/mode-marker.js.map +1 -0
  431. package/server/pty/host-daemon/orphan-cleanup.d.ts +42 -0
  432. package/server/pty/host-daemon/orphan-cleanup.d.ts.map +1 -0
  433. package/server/pty/host-daemon/orphan-cleanup.js +84 -0
  434. package/server/pty/host-daemon/orphan-cleanup.js.map +1 -0
  435. package/server/pty/host-daemon/paths.d.ts +10 -0
  436. package/server/pty/host-daemon/paths.d.ts.map +1 -0
  437. package/server/pty/host-daemon/paths.js +56 -0
  438. package/server/pty/host-daemon/paths.js.map +1 -0
  439. package/server/pty/host-daemon/server-connection.d.ts +44 -0
  440. package/server/pty/host-daemon/server-connection.d.ts.map +1 -0
  441. package/server/pty/host-daemon/server-connection.js +100 -0
  442. package/server/pty/host-daemon/server-connection.js.map +1 -0
  443. package/server/pty/host-daemon/service-detection.d.ts +24 -0
  444. package/server/pty/host-daemon/service-detection.d.ts.map +1 -0
  445. package/server/pty/host-daemon/service-detection.js +16 -0
  446. package/server/pty/host-daemon/service-detection.js.map +1 -0
  447. package/server/pty/host-daemon/socket-ready.d.ts +22 -0
  448. package/server/pty/host-daemon/socket-ready.d.ts.map +1 -0
  449. package/server/pty/host-daemon/socket-ready.js +33 -0
  450. package/server/pty/host-daemon/socket-ready.js.map +1 -0
  451. package/server/pty/host-daemon/spawn-daemon.d.ts +45 -0
  452. package/server/pty/host-daemon/spawn-daemon.d.ts.map +1 -0
  453. package/server/pty/host-daemon/spawn-daemon.js +155 -0
  454. package/server/pty/host-daemon/spawn-daemon.js.map +1 -0
  455. package/server/pty/host-daemon/terminate-daemon.d.ts +16 -0
  456. package/server/pty/host-daemon/terminate-daemon.d.ts.map +1 -0
  457. package/server/pty/host-daemon/terminate-daemon.js +128 -0
  458. package/server/pty/host-daemon/terminate-daemon.js.map +1 -0
  459. package/server/pty/host-protocol/frames.d.ts +110 -0
  460. package/server/pty/host-protocol/frames.d.ts.map +1 -0
  461. package/server/pty/host-protocol/frames.js +255 -0
  462. package/server/pty/host-protocol/frames.js.map +1 -0
  463. package/server/pty/host-protocol/messages.d.ts +140 -0
  464. package/server/pty/host-protocol/messages.d.ts.map +1 -0
  465. package/server/pty/host-protocol/messages.js +117 -0
  466. package/server/pty/host-protocol/messages.js.map +1 -0
  467. package/server/pty/host.d.ts +273 -0
  468. package/server/pty/host.d.ts.map +1 -0
  469. package/server/pty/host.js +184 -0
  470. package/server/pty/host.js.map +1 -0
  471. package/server/pty/in-process-host.d.ts +224 -0
  472. package/server/pty/in-process-host.d.ts.map +1 -0
  473. package/server/pty/in-process-host.js +1183 -0
  474. package/server/pty/in-process-host.js.map +1 -0
  475. package/server/pty/osc7-lifecycle.d.ts +8 -0
  476. package/server/pty/osc7-lifecycle.d.ts.map +1 -0
  477. package/server/pty/osc7-lifecycle.js +22 -0
  478. package/server/pty/osc7-lifecycle.js.map +1 -0
  479. package/server/pty/osc7-parser.d.ts +23 -0
  480. package/server/pty/osc7-parser.d.ts.map +1 -0
  481. package/server/pty/osc7-parser.js +91 -0
  482. package/server/pty/osc7-parser.js.map +1 -0
  483. package/server/pty/remote-host.d.ts +188 -0
  484. package/server/pty/remote-host.d.ts.map +1 -0
  485. package/server/pty/remote-host.js +810 -0
  486. package/server/pty/remote-host.js.map +1 -0
  487. package/server/pty/request-correlator.d.ts +59 -0
  488. package/server/pty/request-correlator.d.ts.map +1 -0
  489. package/server/pty/request-correlator.js +75 -0
  490. package/server/pty/request-correlator.js.map +1 -0
  491. package/server/pty/scrollback-log.d.ts +130 -0
  492. package/server/pty/scrollback-log.d.ts.map +1 -0
  493. package/server/pty/scrollback-log.js +344 -0
  494. package/server/pty/scrollback-log.js.map +1 -0
  495. package/server/pty/scrollback-sanitize.d.ts +2 -0
  496. package/server/pty/scrollback-sanitize.d.ts.map +1 -0
  497. package/server/pty/scrollback-sanitize.js +54 -0
  498. package/server/pty/scrollback-sanitize.js.map +1 -0
  499. package/server/pty/session-mirror.d.ts +67 -0
  500. package/server/pty/session-mirror.d.ts.map +1 -0
  501. package/server/pty/session-mirror.js +112 -0
  502. package/server/pty/session-mirror.js.map +1 -0
  503. package/server/pty/session-policy.d.ts +85 -0
  504. package/server/pty/session-policy.d.ts.map +1 -0
  505. package/server/pty/session-policy.js +186 -0
  506. package/server/pty/session-policy.js.map +1 -0
  507. package/server/pty/version-mismatch-recovery.d.ts +71 -0
  508. package/server/pty/version-mismatch-recovery.d.ts.map +1 -0
  509. package/server/pty/version-mismatch-recovery.js +63 -0
  510. package/server/pty/version-mismatch-recovery.js.map +1 -0
  511. package/server/routes/debug-agent-status.d.ts +5 -0
  512. package/server/routes/debug-agent-status.d.ts.map +1 -0
  513. package/server/routes/debug-agent-status.js +22 -0
  514. package/server/routes/debug-agent-status.js.map +1 -0
  515. package/server/routes/debug-diagnostics.d.ts +8 -0
  516. package/server/routes/debug-diagnostics.d.ts.map +1 -0
  517. package/server/routes/debug-diagnostics.js +34 -0
  518. package/server/routes/debug-diagnostics.js.map +1 -0
  519. package/server/routes/debug-terminal-trace.d.ts +16 -0
  520. package/server/routes/debug-terminal-trace.d.ts.map +1 -0
  521. package/server/routes/debug-terminal-trace.js +545 -0
  522. package/server/routes/debug-terminal-trace.js.map +1 -0
  523. package/server/routes/drops.d.ts +13 -0
  524. package/server/routes/drops.d.ts.map +1 -0
  525. package/server/routes/drops.js +126 -0
  526. package/server/routes/drops.js.map +1 -0
  527. package/server/routes/file-uploads.d.ts +9 -0
  528. package/server/routes/file-uploads.d.ts.map +1 -0
  529. package/server/routes/file-uploads.js +119 -0
  530. package/server/routes/file-uploads.js.map +1 -0
  531. package/server/routes/files.d.ts +9 -0
  532. package/server/routes/files.d.ts.map +1 -0
  533. package/server/routes/files.js +441 -0
  534. package/server/routes/files.js.map +1 -0
  535. package/server/routes/filesystem.d.ts +6 -0
  536. package/server/routes/filesystem.d.ts.map +1 -0
  537. package/server/routes/filesystem.js +76 -0
  538. package/server/routes/filesystem.js.map +1 -0
  539. package/server/routes/git.d.ts +38 -0
  540. package/server/routes/git.d.ts.map +1 -0
  541. package/server/routes/git.js +585 -0
  542. package/server/routes/git.js.map +1 -0
  543. package/server/routes/healthz.d.ts +8 -0
  544. package/server/routes/healthz.d.ts.map +1 -0
  545. package/server/routes/healthz.js +24 -0
  546. package/server/routes/healthz.js.map +1 -0
  547. package/server/routes/hook.d.ts +14 -0
  548. package/server/routes/hook.d.ts.map +1 -0
  549. package/server/routes/hook.js +86 -0
  550. package/server/routes/hook.js.map +1 -0
  551. package/server/routes/ide-commands.d.ts +9 -0
  552. package/server/routes/ide-commands.d.ts.map +1 -0
  553. package/server/routes/ide-commands.js +23 -0
  554. package/server/routes/ide-commands.js.map +1 -0
  555. package/server/routes/lib/resolve-worktree.d.ts +30 -0
  556. package/server/routes/lib/resolve-worktree.d.ts.map +1 -0
  557. package/server/routes/lib/resolve-worktree.js +35 -0
  558. package/server/routes/lib/resolve-worktree.js.map +1 -0
  559. package/server/routes/open.d.ts +4 -0
  560. package/server/routes/open.d.ts.map +1 -0
  561. package/server/routes/open.js +21 -0
  562. package/server/routes/open.js.map +1 -0
  563. package/server/routes/pane-commands.d.ts +9 -0
  564. package/server/routes/pane-commands.d.ts.map +1 -0
  565. package/server/routes/pane-commands.js +23 -0
  566. package/server/routes/pane-commands.js.map +1 -0
  567. package/server/routes/projects.d.ts +15 -0
  568. package/server/routes/projects.d.ts.map +1 -0
  569. package/server/routes/projects.js +363 -0
  570. package/server/routes/projects.js.map +1 -0
  571. package/server/routes/server-notices.d.ts +4 -0
  572. package/server/routes/server-notices.d.ts.map +1 -0
  573. package/server/routes/server-notices.js +29 -0
  574. package/server/routes/server-notices.js.map +1 -0
  575. package/server/routes/service-config.d.ts +11 -0
  576. package/server/routes/service-config.d.ts.map +1 -0
  577. package/server/routes/service-config.js +64 -0
  578. package/server/routes/service-config.js.map +1 -0
  579. package/server/routes/sessions.d.ts +7 -0
  580. package/server/routes/sessions.d.ts.map +1 -0
  581. package/server/routes/sessions.js +218 -0
  582. package/server/routes/sessions.js.map +1 -0
  583. package/server/service/caffeinate.d.ts +33 -0
  584. package/server/service/caffeinate.d.ts.map +1 -0
  585. package/server/service/caffeinate.js +72 -0
  586. package/server/service/caffeinate.js.map +1 -0
  587. package/server/state/app-state.d.ts +167 -0
  588. package/server/state/app-state.d.ts.map +1 -0
  589. package/server/state/app-state.js +335 -0
  590. package/server/state/app-state.js.map +1 -0
  591. package/server/state/project-manager.d.ts +30 -0
  592. package/server/state/project-manager.d.ts.map +1 -0
  593. package/server/state/project-manager.js +128 -0
  594. package/server/state/project-manager.js.map +1 -0
  595. package/server/state/server-notices.d.ts +12 -0
  596. package/server/state/server-notices.d.ts.map +1 -0
  597. package/server/state/server-notices.js +32 -0
  598. package/server/state/server-notices.js.map +1 -0
  599. package/server/state/worktree-cache.d.ts +25 -0
  600. package/server/state/worktree-cache.d.ts.map +1 -0
  601. package/server/state/worktree-cache.js +53 -0
  602. package/server/state/worktree-cache.js.map +1 -0
  603. package/server/ws/events.d.ts +45 -0
  604. package/server/ws/events.d.ts.map +1 -0
  605. package/server/ws/events.js +134 -0
  606. package/server/ws/events.js.map +1 -0
  607. package/server/ws/keepalive.d.ts +27 -0
  608. package/server/ws/keepalive.d.ts.map +1 -0
  609. package/server/ws/keepalive.js +63 -0
  610. package/server/ws/keepalive.js.map +1 -0
  611. package/server/ws/terminal-attach.d.ts +13 -0
  612. package/server/ws/terminal-attach.d.ts.map +1 -0
  613. package/server/ws/terminal-attach.js +233 -0
  614. package/server/ws/terminal-attach.js.map +1 -0
  615. package/server/ws/terminal-flow.d.ts +17 -0
  616. package/server/ws/terminal-flow.d.ts.map +1 -0
  617. package/server/ws/terminal-flow.js +64 -0
  618. package/server/ws/terminal-flow.js.map +1 -0
  619. package/server/ws/terminal.d.ts +69 -0
  620. package/server/ws/terminal.d.ts.map +1 -0
  621. package/server/ws/terminal.js +311 -0
  622. package/server/ws/terminal.js.map +1 -0
  623. package/web/assets/EditorPane-CzzT3iYY.js +123 -0
  624. package/web/assets/SymbolsNerdFontMono-Regular-CwEZqMeU.woff2 +0 -0
  625. package/web/assets/TerminalPane-CjbYzePr.js +68 -0
  626. package/web/assets/TerminalPane-DkTCHfhq.css +1 -0
  627. package/web/assets/file-icons-JBi09j0r.js +6 -0
  628. package/web/assets/index-CTTkRpnn.css +2 -0
  629. package/web/assets/index-CmhewzMp.js +34 -0
  630. package/web/assets/session-resume-7f-tB-ZU.js +2 -0
  631. package/web/assets/terminal-trace-PuuFRybC.js +2803 -0
  632. package/web/assets/useVirtualKeyboard-DgJb9u9d.js +1 -0
  633. package/web/index.html +52 -0
@@ -0,0 +1,1183 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import * as pty from "node-pty";
5
+ import { PromiseMutex } from "../lib/promise-mutex.js";
6
+ import { HeadlessTerminalStateCache } from "./headless-terminal-state-cache.js";
7
+ import { stripQueryEscapes } from "./scrollback-sanitize.js";
8
+ import * as sessionPolicy from "./session-policy.js";
9
+ const DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES = 256 * 1024;
10
+ const DEFAULT_HEADLESS_REPLAY_SCROLLBACK_LINES = 10_000;
11
+ const DEFAULT_HEADLESS_REPLAY_MAX_BYTES = DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES;
12
+ const DEFAULT_HEADLESS_STATE_MAX_SESSIONS = 8;
13
+ const DEFAULT_HEADLESS_STATE_TTL_MS = 10 * 60_000;
14
+ function readPositiveIntegerEnv(name) {
15
+ const raw = process.env[name];
16
+ if (!raw)
17
+ return null;
18
+ const value = Number(raw);
19
+ if (!Number.isSafeInteger(value) || value <= 0)
20
+ return null;
21
+ return value;
22
+ }
23
+ function readBooleanEnv(name) {
24
+ const raw = process.env[name];
25
+ if (raw === undefined)
26
+ return null;
27
+ const value = raw.trim().toLowerCase();
28
+ if (value === "1" || value === "true" || value === "yes" || value === "on")
29
+ return true;
30
+ if (value === "0" || value === "false" || value === "no" || value === "off")
31
+ return false;
32
+ return null;
33
+ }
34
+ function readHeadlessReplayEnabled() {
35
+ return (readBooleanEnv("PARASOR_HEADLESS_REPLAY") ??
36
+ readBooleanEnv("PARASOR_EXPERIMENT_HEADLESS_REPLAY") ??
37
+ true);
38
+ }
39
+ function utf8Tail(text, maxBytes) {
40
+ const buf = Buffer.from(text, "utf8");
41
+ if (buf.length <= maxBytes)
42
+ return text;
43
+ const slice = buf.subarray(buf.length - maxBytes);
44
+ let start = 0;
45
+ while (start < slice.length && start < 3 && (slice[start] & 0xc0) === 0x80) {
46
+ start++;
47
+ }
48
+ return slice.subarray(start).toString("utf8");
49
+ }
50
+ export class InProcessPtyHost {
51
+ store;
52
+ scrollbackLog;
53
+ daemonContext;
54
+ uploadsDir;
55
+ sessions = new Map();
56
+ globalDataListeners = [];
57
+ inputListeners = [];
58
+ ptyEnv = {};
59
+ inProcessLegacyReplayMaxBytes = readPositiveIntegerEnv("PARASOR_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES") ??
60
+ DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES;
61
+ headlessReplayEnabled = readHeadlessReplayEnabled();
62
+ headlessReplayScrollbackLines = readPositiveIntegerEnv("PARASOR_HEADLESS_REPLAY_SCROLLBACK_LINES") ??
63
+ DEFAULT_HEADLESS_REPLAY_SCROLLBACK_LINES;
64
+ headlessReplayMaxBytes = readPositiveIntegerEnv("PARASOR_HEADLESS_REPLAY_MAX_BYTES") ??
65
+ DEFAULT_HEADLESS_REPLAY_MAX_BYTES;
66
+ headlessStateCache = this.headlessReplayEnabled
67
+ ? new HeadlessTerminalStateCache({
68
+ cols: 80,
69
+ rows: 24,
70
+ scrollbackLines: this.headlessReplayScrollbackLines,
71
+ maxBytes: this.headlessReplayMaxBytes,
72
+ maxSessions: readPositiveIntegerEnv("PARASOR_HEADLESS_STATE_MAX_SESSIONS") ??
73
+ DEFAULT_HEADLESS_STATE_MAX_SESSIONS,
74
+ ttlMs: readPositiveIntegerEnv("PARASOR_HEADLESS_STATE_TTL_MS") ??
75
+ DEFAULT_HEADLESS_STATE_TTL_MS,
76
+ })
77
+ : null;
78
+ /**
79
+ * Attach fencing monotonic counter -- incremented per attach (initClient or
80
+ * attachClient), stamped on the resulting `AttachedClient` entry, and
81
+ * compared on `detachClient(..., expectedToken)`. Plain integer is fine
82
+ * because tokens are scoped to this host instance and the comparison
83
+ * is identity, not magnitude.
84
+ */
85
+ nextAttachToken = 1;
86
+ mintAttachToken(caller) {
87
+ if (caller !== undefined)
88
+ return caller;
89
+ return this.nextAttachToken++;
90
+ }
91
+ onSessionExit = null;
92
+ constructor(store,
93
+ /**
94
+ * Disk-backed scrollback log. When null (daemon-side host, contract
95
+ * tests), this host records no scrollback and every replay path
96
+ * returns empty. That is intentional: the daemon forwards OUTPUT to
97
+ * the client over IPC and the client-side `RemotePtyHost` owns its
98
+ * own `ScrollbackLog` -- keeping a redundant copy on the daemon side
99
+ * would just double the disk footprint. `getScrollback()` and the
100
+ * `initClient` legacy replay degrade to "no replay" in that mode.
101
+ */
102
+ scrollbackLog = null, daemonContext = null,
103
+ /**
104
+ * Canonical absolute path of `<rootDir>/uploads`. When supplied, every
105
+ * spawned PTY's env gets `PARASOR_UPLOAD_DIR=<uploadsDir>/<sessionId>`
106
+ * -- the per-session subdir, NOT the shared root -- so the Claude shim's
107
+ * `--add-dir` allowlists only that PTY's own drops (upload staging isolation codex
108
+ * Shared-root isolation review: the shared-root variant let session A read session
109
+ * B's uploads via `--add-dir`). Null in tests that don't exercise the
110
+ * upload pipeline.
111
+ */
112
+ uploadsDir = null) {
113
+ this.store = store;
114
+ this.scrollbackLog = scrollbackLog;
115
+ this.daemonContext = daemonContext;
116
+ this.uploadsDir = uploadsDir;
117
+ }
118
+ /**
119
+ * Merge env vars into the per-PTY environment. Multiple calls accumulate
120
+ * (later calls override individual keys but preserve everything else),
121
+ * so the server can set the static shim PATH early during init and add
122
+ * the dynamic PARASOR_PORT later once probePort has resolved the actual
123
+ * listening port.
124
+ */
125
+ setPtyEnv(env) {
126
+ this.ptyEnv = { ...this.ptyEnv, ...env };
127
+ }
128
+ buildSessionEnv(sessionId, projectId) {
129
+ const env = {};
130
+ for (const [k, v] of Object.entries(process.env)) {
131
+ if (v !== undefined)
132
+ env[k] = v;
133
+ }
134
+ Object.assign(env, this.ptyEnv, {
135
+ PROMPT_EOL_MARK: "",
136
+ TERM: "xterm-256color",
137
+ COLORTERM: "truecolor",
138
+ TERM_PROGRAM: "parasor",
139
+ PARASOR_SESSION_ID: sessionId,
140
+ PARASOR_PROJECT_ID: projectId,
141
+ });
142
+ if (this.uploadsDir) {
143
+ // Per-session subdir, eagerly created so the Claude wrapper's
144
+ // `--add-dir` flag references a real path even when the user never
145
+ // drops a file. The drops route's `acquire(sessionId)` is
146
+ // idempotent -- a later upload re-mkdirs the same path.
147
+ const sessionUploadDir = join(this.uploadsDir, sessionId);
148
+ mkdirSync(sessionUploadDir, { recursive: true, mode: 0o700 });
149
+ env.PARASOR_UPLOAD_DIR = sessionUploadDir;
150
+ }
151
+ return env;
152
+ }
153
+ /**
154
+ * Create a session stub. The PTY is NOT spawned here -- it is deferred
155
+ * until the first WebSocket attach provides real viewport dimensions.
156
+ * This is the whole point of the deferred-spawn model: spawning at the
157
+ * exact client dims eliminates the SIGWINCH-driven prompt redraw that
158
+ * otherwise walks zsh's prompt down one row on every new-session open.
159
+ */
160
+ async create(input) {
161
+ const id = randomUUID();
162
+ const mutex = new PromiseMutex();
163
+ const release = await mutex.acquire();
164
+ try {
165
+ const { spawnCmd, spawnArgs } = this.resolveCommand(input.command);
166
+ const info = {
167
+ id,
168
+ projectId: input.projectId,
169
+ pid: null,
170
+ state: "spawning",
171
+ generation: 1,
172
+ title: input.title ?? basename(spawnCmd),
173
+ ...(input.title !== undefined && { titleManual: true }),
174
+ command: input.command,
175
+ cwd: input.cwd,
176
+ shell: spawnCmd,
177
+ createdAt: Date.now(),
178
+ };
179
+ // Build the SessionRecord stub. PID/PGID stay null until
180
+ // spawnProcess fills them on first WS attach. State is "running"
181
+ // from creation: from the daemon's perspective this slot is
182
+ // claimed and counts toward orphan reconciliation. If the daemon
183
+ // crashes between create() and spawnProcess, the next daemon's
184
+ // reconcile sees state=running + pid=null and transitions it to
185
+ // "lost" (orphan-cleanup.ts: no-pid path).
186
+ const record = this.daemonContext
187
+ ? {
188
+ id,
189
+ projectId: input.projectId,
190
+ command: input.command,
191
+ cwd: input.cwd,
192
+ pid: null,
193
+ pgid: null,
194
+ argv: [spawnCmd, ...spawnArgs],
195
+ startedAt: new Date().toISOString(),
196
+ state: "running",
197
+ exitCode: null,
198
+ exitSignal: null,
199
+ daemonPid: this.daemonContext.pid,
200
+ daemonStartedAt: this.daemonContext.startedAt,
201
+ }
202
+ : null;
203
+ const managed = {
204
+ info,
205
+ process: null,
206
+ ptySize: null,
207
+ currentGeneration: 1,
208
+ attachedClients: new Map(),
209
+ outputPaused: false,
210
+ mutex,
211
+ bootstrapInput: input.bootstrapInput ?? null,
212
+ record,
213
+ };
214
+ this.sessions.set(id, managed);
215
+ this.store.mutateSessions((s) => {
216
+ s.sessions.push(structuredClone(info));
217
+ if (record) {
218
+ s.sessionRecords.push(structuredClone(record));
219
+ }
220
+ });
221
+ return info;
222
+ }
223
+ finally {
224
+ release();
225
+ }
226
+ }
227
+ async restart(id) {
228
+ const managed = this.sessions.get(id);
229
+ if (!managed)
230
+ throw new Error(`Session ${id} not found`);
231
+ const release = await managed.mutex.acquire();
232
+ try {
233
+ if (managed.info.state !== "ended") {
234
+ throw new Error(`Session ${id} is not ended; cannot restart`);
235
+ }
236
+ const nextGen = managed.currentGeneration + 1;
237
+ managed.currentGeneration = nextGen;
238
+ this.scrollbackLog?.bumpGeneration(id, nextGen);
239
+ managed.info = {
240
+ ...managed.info,
241
+ pid: null,
242
+ state: "spawning",
243
+ generation: nextGen,
244
+ endedAt: undefined,
245
+ endReason: undefined,
246
+ };
247
+ managed.process = null;
248
+ managed.ptySize = null;
249
+ managed.attachedClients.clear();
250
+ // Reset the SessionRecord too: clear PID/PGID, re-arm state to
251
+ // "running" so the record participates in orphan reconciliation
252
+ // again, and refresh startedAt so the window starts now.
253
+ // daemonPid/daemonStartedAt stay this generation's -- restart()
254
+ // is by definition same-daemon (a different daemon would have
255
+ // already marked the record orphaned at boot).
256
+ if (managed.record && this.daemonContext) {
257
+ managed.record = {
258
+ ...managed.record,
259
+ pid: null,
260
+ pgid: null,
261
+ state: "running",
262
+ exitCode: null,
263
+ exitSignal: null,
264
+ startedAt: new Date().toISOString(),
265
+ };
266
+ this.persistRecord(managed.record);
267
+ }
268
+ this.persistSession(managed.info);
269
+ return managed.info;
270
+ }
271
+ finally {
272
+ release();
273
+ }
274
+ }
275
+ get(id) {
276
+ return this.sessions.get(id)?.info;
277
+ }
278
+ getScrollback(id) {
279
+ if (!this.sessions.has(id))
280
+ return null;
281
+ const tail = this.scrollbackLog?.readTail(id) ?? "";
282
+ return tail || null;
283
+ }
284
+ list() {
285
+ return [...this.sessions.values()].map((s) => s.info);
286
+ }
287
+ getForegroundProcess(id) {
288
+ const managed = this.sessions.get(id);
289
+ if (!managed?.process)
290
+ return null;
291
+ try {
292
+ const name = managed.process.process;
293
+ return name ? basename(name) : null;
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ setTitle(id, title, titleManual = false) {
300
+ const managed = this.sessions.get(id);
301
+ if (!managed)
302
+ return false;
303
+ const currentManual = managed.info.titleManual === true;
304
+ if (managed.info.title === title && currentManual === titleManual) {
305
+ return false;
306
+ }
307
+ managed.info = titleManual
308
+ ? { ...managed.info, title, titleManual: true }
309
+ : (() => {
310
+ const { titleManual: _drop, ...rest } = managed.info;
311
+ return { ...rest, title };
312
+ })();
313
+ this.persistSession(managed.info);
314
+ return true;
315
+ }
316
+ setPinned(id, pinned) {
317
+ const managed = this.sessions.get(id);
318
+ if (!managed)
319
+ return false;
320
+ const current = managed.info.pinned === true;
321
+ if (current === pinned)
322
+ return false;
323
+ const next = pinned
324
+ ? { ...managed.info, pinned: true }
325
+ : (() => {
326
+ const { pinned: _drop, ...rest } = managed.info;
327
+ return rest;
328
+ })();
329
+ managed.info = next;
330
+ this.persistSession(managed.info);
331
+ return true;
332
+ }
333
+ listByProject(projectId) {
334
+ return this.list().filter((s) => s.projectId === projectId);
335
+ }
336
+ onSessionInput(listener) {
337
+ this.inputListeners.push(listener);
338
+ }
339
+ write(id, data, generation) {
340
+ const managed = this.sessions.get(id);
341
+ if (!managed?.process)
342
+ return;
343
+ /*
344
+ * PTY generation gate generation gate. Drop input tagged with a stale generation --
345
+ * happens when the previous PTY's TUI sent a DECRQM-style query and
346
+ * the terminal's reply is in-flight on the WS while we auto-resume
347
+ * a new shell. Without this, the reply lands on the new shell's
348
+ * stdin and visible fragments (e.g. "2026;2$y" from a DECRPM mode
349
+ * 2026 response) leak onto the prompt.
350
+ *
351
+ * `0` and `undefined` are both "no gating" sentinels -- used by
352
+ * legacy non-WS callers (osc7/port-detect taps), pre-init-ack
353
+ * queued web INPUT (web's `currentGenerationRef` defaults to 0
354
+ * before the init-ack populates it), and the daemon-IPC path when
355
+ * the legacy WRITE codec lacked a generation field. Aligns with
356
+ * the daemon's `handleWrite` semantics so in-process and daemon
357
+ * modes behave identically (PTY generation gate parity).
358
+ */
359
+ if (sessionPolicy.shouldDropStaleInput(generation, managed.currentGeneration)) {
360
+ if (process.env.PARASOR_INPUT_DEBUG === "1") {
361
+ // eslint-disable-next-line no-console
362
+ console.error(`[input] drop stale gen=${generation} current=${managed.currentGeneration} session=${id.slice(0, 8)} bytes=${JSON.stringify(data.slice(0, 32))}`);
363
+ }
364
+ return;
365
+ }
366
+ for (const listener of this.inputListeners) {
367
+ listener(id, data);
368
+ }
369
+ managed.process.write(data);
370
+ }
371
+ /**
372
+ * Resize the underlying PTY. The explicit resize caller is authoritative,
373
+ * but duplicate claims for the current PTY size must not issue another
374
+ * SIGWINCH.
375
+ */
376
+ resize(id, cols, rows) {
377
+ const managed = this.sessions.get(id);
378
+ if (!managed?.process)
379
+ return;
380
+ if (managed.ptySize?.cols === cols && managed.ptySize.rows === rows) {
381
+ return;
382
+ }
383
+ if (process.env.PARASOR_RESIZE_DEBUG === "1") {
384
+ // eslint-disable-next-line no-console
385
+ console.error(`[resize] session=${id.slice(0, 8)} -> ${cols}x${rows}`);
386
+ }
387
+ try {
388
+ managed.process.resize(cols, rows);
389
+ managed.ptySize = { cols, rows };
390
+ }
391
+ catch {
392
+ // node-pty rejects invalid dims -- ignore
393
+ }
394
+ }
395
+ /**
396
+ * Force a SIGWINCH without changing dims so a TUI repaints from scratch.
397
+ * Used when a client surface returns to the foreground (iOS background
398
+ * tab freeze, mobile visibilitychange). node-pty's resize() is a no-op
399
+ * when new dims equal current ones, so we briefly bump rows and snap
400
+ * back -- INK collapses the two calls into a single repaint.
401
+ */
402
+ refresh(id) {
403
+ const managed = this.sessions.get(id);
404
+ if (!managed?.process)
405
+ return;
406
+ const proc = managed.process;
407
+ try {
408
+ // Read current rows via ptyProc internals isn't exposed, but bumping
409
+ // by +1 and snapping back is safe regardless of the current value --
410
+ // we don't need to know it.
411
+ const anyProc = proc;
412
+ const rows = anyProc.rows ?? 24;
413
+ const cols = anyProc.cols ?? 80;
414
+ proc.resize(cols, rows + 1);
415
+ proc.resize(cols, rows);
416
+ }
417
+ catch {
418
+ // ignore
419
+ }
420
+ }
421
+ pauseOutput(id, clientId) {
422
+ const managed = this.sessions.get(id);
423
+ if (!managed?.process)
424
+ return;
425
+ const client = managed.attachedClients.get(clientId);
426
+ if (!client)
427
+ return;
428
+ client.flowPaused = true;
429
+ this.applyOutputPauseState(managed);
430
+ }
431
+ resumeOutput(id, clientId) {
432
+ const managed = this.sessions.get(id);
433
+ if (!managed?.process)
434
+ return;
435
+ const client = managed.attachedClients.get(clientId);
436
+ if (!client)
437
+ return;
438
+ client.flowPaused = false;
439
+ this.applyOutputPauseState(managed);
440
+ }
441
+ shouldPauseOutput(managed) {
442
+ const flowPausedFlags = [...managed.attachedClients.values()].map((client) => client.flowPaused);
443
+ return sessionPolicy.shouldPauseOutputForClients(flowPausedFlags);
444
+ }
445
+ applyOutputPauseState(managed) {
446
+ const shouldPause = this.shouldPauseOutput(managed);
447
+ if (shouldPause === managed.outputPaused)
448
+ return;
449
+ managed.outputPaused = shouldPause;
450
+ try {
451
+ if (shouldPause) {
452
+ managed.process?.pause();
453
+ }
454
+ else {
455
+ managed.process?.resume();
456
+ }
457
+ }
458
+ catch {
459
+ // ignore node-pty pause/resume failures; detach/reconnect can race exit
460
+ }
461
+ }
462
+ /**
463
+ * Attach a WebSocket client. Spawns the PTY on first call (using the
464
+ * client's viewport dims), replays scrollback to the fresh listener,
465
+ * and adds the listener to the broadcast set. A subsequent call with
466
+ * the same clientId replaces the existing listener in place (no
467
+ * duplicate broadcast).
468
+ */
469
+ async initClient(id, clientId, cols, rows, listener, callerToken) {
470
+ const managed = this.sessions.get(id);
471
+ if (!managed)
472
+ return { ok: false };
473
+ const release = await managed.mutex.acquire();
474
+ try {
475
+ if (managed.info.state === "ended") {
476
+ // Either auto-resume (silent re-spawn with scrollback retained) or
477
+ // refuse -- the client then renders the error pane.
478
+ if (!this.autoResumeIfSafe(managed, cols, rows))
479
+ return { ok: false };
480
+ }
481
+ else if (managed.info.state === "spawning") {
482
+ this.spawnProcess(managed, cols, rows);
483
+ }
484
+ const attachToken = this.mintAttachToken(callerToken);
485
+ managed.attachedClients.set(clientId, {
486
+ kind: "string",
487
+ listener,
488
+ attachToken,
489
+ flowPaused: false,
490
+ });
491
+ this.applyOutputPauseState(managed);
492
+ /*
493
+ * Replay scrollback to just this client so its xterm has state.
494
+ * Terminal query escapes (XTVERSION / DA / DSR) are stripped here
495
+ * because re-parsing them on the fresh xterm would make it emit
496
+ * the response again -- which then lands in the idle shell's
497
+ * readline buffer as visible garbage. The live path is untouched;
498
+ * the single legitimate response-per-query still happens when the
499
+ * app first sends the query.
500
+ *
501
+ * Attach fencing: if the listener throws (e.g. ws.send fails because
502
+ * the WS just closed), the Promise rejects before the caller can
503
+ * capture `attachToken`, so cleanupTerminalRelay later sees an
504
+ * undefined token and skips detach. Roll back the just-inserted
505
+ * entry inline -- the token-equality guard preserves a concurrent
506
+ * fresher attach (defensive: mutex makes this unreachable today).
507
+ */
508
+ const replayText = this.scrollbackLog?.readTail(id) ?? "";
509
+ if (replayText) {
510
+ try {
511
+ listener(stripQueryEscapes(replayText));
512
+ }
513
+ catch (err) {
514
+ const current = managed.attachedClients.get(clientId);
515
+ if (current?.attachToken === attachToken) {
516
+ managed.attachedClients.delete(clientId);
517
+ this.applyOutputPauseState(managed);
518
+ }
519
+ throw err;
520
+ }
521
+ }
522
+ return { ok: true, attachToken };
523
+ }
524
+ finally {
525
+ release();
526
+ }
527
+ }
528
+ /**
529
+ * capable attach.
530
+ *
531
+ * Held under `managed.mutex` for the entire negotiation so that any
532
+ * concurrent PTY broadcast queues behind us -- the live chunk listener
533
+ * is registered last, after we have already decided the replay strategy
534
+ * and snapshotted the chunk ring. This prevents init-ack / replay /
535
+ * live OUTPUT from interleaving on the wire.
536
+ *
537
+ * Falls back to JSON-path semantics when `capabilities.binary === false`
538
+ * (the WS handler will not call us in that case, but the contract
539
+ * stays well-defined).
540
+ */
541
+ async attachClient(id, clientId, cols, rows, capabilities, sink) {
542
+ const managed = this.sessions.get(id);
543
+ if (!managed)
544
+ return { ok: false };
545
+ const release = await managed.mutex.acquire();
546
+ try {
547
+ if (managed.info.state === "ended") {
548
+ if (!this.autoResumeIfSafe(managed, cols, rows))
549
+ return { ok: false };
550
+ }
551
+ else if (managed.info.state === "spawning") {
552
+ this.spawnProcess(managed, cols, rows);
553
+ }
554
+ const negotiated = {
555
+ binary: capabilities.binary === true,
556
+ // chunkedReplay requires binary -- guarded explicitly so an
557
+ // ill-formed client (false/true) is canonicalised before we
558
+ // commit it into the response and the chunk listener.
559
+ chunkedReplay: capabilities.binary === true && capabilities.chunkedReplay === true,
560
+ };
561
+ const generation = managed.currentGeneration;
562
+ const ringSnapshot = this.scrollbackLog?.ringState(id, generation) ?? {
563
+ generation,
564
+ lastDeliveredSeq: null,
565
+ oldestSeq: null,
566
+ };
567
+ let replay = "none";
568
+ let chunks;
569
+ let fullReplay;
570
+ let replayDiagnostics;
571
+ // Translate the wire-side string `lastSeen.seq` into BigInt for
572
+ // ring comparison. Malformed inputs (NaN, negative, non-decimal)
573
+ // collapse to "no cursor" so we never throw on the hot path.
574
+ const lastSeenForRing = sessionPolicy.parseLastSeen(capabilities.lastSeen);
575
+ // Disk tail is the single source of truth for full replay. Read it
576
+ // lazily and at most once: the common delta-replay reconnect never
577
+ // needs it, and readTail does a flush + synchronous readFileSync of
578
+ // up to the 4 MB tail under the session mutex.
579
+ let diskTailCache;
580
+ const getDiskTail = () => {
581
+ if (diskTailCache === undefined) {
582
+ diskTailCache = this.scrollbackLog?.readTail(id) ?? "";
583
+ }
584
+ return diskTailCache;
585
+ };
586
+ if (negotiated.chunkedReplay && this.scrollbackLog) {
587
+ const decision = this.scrollbackLog.readSince(id, lastSeenForRing);
588
+ if (decision.kind === "delta") {
589
+ replay = "delta";
590
+ chunks = decision.chunks.map((c) => ({
591
+ generation,
592
+ seq: c.seq,
593
+ data: c.data,
594
+ }));
595
+ }
596
+ else if (decision.kind === "full") {
597
+ const tail = getDiskTail();
598
+ replay = "full";
599
+ const resolved = await this.buildFullReplay(id, tail, cols, rows);
600
+ fullReplay = resolved.fullReplay;
601
+ replayDiagnostics = resolved.replayDiagnostics;
602
+ }
603
+ else {
604
+ if (lastSeenForRing) {
605
+ replay = "none";
606
+ }
607
+ else {
608
+ const tail = getDiskTail();
609
+ replay = tail ? "full" : "none";
610
+ if (replay === "full") {
611
+ const resolved = await this.buildFullReplay(id, tail, cols, rows);
612
+ fullReplay = resolved.fullReplay;
613
+ replayDiagnostics = resolved.replayDiagnostics;
614
+ }
615
+ }
616
+ }
617
+ }
618
+ else if (negotiated.binary) {
619
+ // Binary OUTPUT but no chunked replay -- emit the disk tail once
620
+ // via the legacy `replay` envelope so the new xterm has state,
621
+ // then live OUTPUT continues.
622
+ const tail = getDiskTail();
623
+ if (tail) {
624
+ replay = "full";
625
+ const resolved = await this.buildFullReplay(id, tail, cols, rows);
626
+ fullReplay = resolved.fullReplay;
627
+ replayDiagnostics = resolved.replayDiagnostics;
628
+ }
629
+ }
630
+ const attachToken = this.mintAttachToken(undefined);
631
+ managed.attachedClients.set(clientId, {
632
+ kind: "chunk",
633
+ listener: sink.onChunk,
634
+ onExit: sink.onExit,
635
+ attachToken,
636
+ flowPaused: false,
637
+ });
638
+ this.applyOutputPauseState(managed);
639
+ return {
640
+ ok: true,
641
+ capabilities: negotiated,
642
+ serverState: {
643
+ generation: ringSnapshot.generation,
644
+ lastDeliveredSeq: ringSnapshot.lastDeliveredSeq === null
645
+ ? null
646
+ : ringSnapshot.lastDeliveredSeq.toString(),
647
+ oldestSeq: ringSnapshot.oldestSeq === null
648
+ ? null
649
+ : ringSnapshot.oldestSeq.toString(),
650
+ },
651
+ replay,
652
+ chunks,
653
+ fullReplay,
654
+ replayDiagnostics,
655
+ attachToken,
656
+ };
657
+ }
658
+ finally {
659
+ release();
660
+ }
661
+ }
662
+ async buildFullReplay(id, tail, cols, rows) {
663
+ const rawBytes = Buffer.byteLength(tail, "utf8");
664
+ if (this.headlessReplayEnabled && this.headlessStateCache) {
665
+ try {
666
+ const headlessSnapshot = (await this.headlessStateCache.snapshot(id, { cols, rows })) ??
667
+ (await this.headlessStateCache.rebuild(id, tail, { cols, rows }));
668
+ if (!headlessSnapshot) {
669
+ throw new Error("empty headless replay snapshot");
670
+ }
671
+ const snapshot = headlessSnapshot.snapshot;
672
+ return {
673
+ fullReplay: snapshot.text,
674
+ replayDiagnostics: {
675
+ source: headlessSnapshot.source,
676
+ rawBytes: snapshot.rawBytes,
677
+ replayBytes: snapshot.snapshotBytes,
678
+ headlessDurationMs: snapshot.durationMs,
679
+ headlessBufferLines: snapshot.bufferLines,
680
+ headlessEmittedLines: snapshot.emittedLines,
681
+ scrollbackLines: this.headlessReplayScrollbackLines,
682
+ maxBytes: this.headlessReplayMaxBytes,
683
+ },
684
+ };
685
+ }
686
+ catch (err) {
687
+ console.warn(`[terminal] in-process headless replay snapshot failed for session=${id.slice(0, 8)}: ${err.message}`);
688
+ const fullReplay = stripQueryEscapes(utf8Tail(tail, this.inProcessLegacyReplayMaxBytes));
689
+ return {
690
+ fullReplay,
691
+ replayDiagnostics: {
692
+ source: "headless-fallback",
693
+ rawBytes,
694
+ replayBytes: Buffer.byteLength(fullReplay, "utf8"),
695
+ scrollbackLines: this.headlessReplayScrollbackLines,
696
+ maxBytes: this.headlessReplayMaxBytes,
697
+ },
698
+ };
699
+ }
700
+ }
701
+ const fullReplay = stripQueryEscapes(utf8Tail(tail, this.inProcessLegacyReplayMaxBytes));
702
+ return {
703
+ fullReplay,
704
+ replayDiagnostics: {
705
+ source: "raw-tail",
706
+ rawBytes,
707
+ replayBytes: Buffer.byteLength(fullReplay, "utf8"),
708
+ maxBytes: this.inProcessLegacyReplayMaxBytes,
709
+ },
710
+ };
711
+ }
712
+ updateHeadlessState(sessionId, data) {
713
+ void this.headlessStateCache
714
+ ?.writeExisting(sessionId, data)
715
+ .catch((err) => {
716
+ console.warn(`[terminal] in-process headless state update failed for session=${sessionId.slice(0, 8)}: ${err.message}`);
717
+ this.headlessStateCache?.delete(sessionId);
718
+ });
719
+ }
720
+ detachClient(id, clientId, expectedToken) {
721
+ const managed = this.sessions.get(id);
722
+ if (!managed)
723
+ return;
724
+ if (expectedToken !== undefined) {
725
+ const entry = managed.attachedClients.get(clientId);
726
+ if (!entry || entry.attachToken !== expectedToken)
727
+ return;
728
+ }
729
+ managed.attachedClients.delete(clientId);
730
+ this.applyOutputPauseState(managed);
731
+ }
732
+ async dispose(id) {
733
+ const managed = this.sessions.get(id);
734
+ if (!managed)
735
+ return;
736
+ const release = await managed.mutex.acquire();
737
+ try {
738
+ if (managed.process) {
739
+ managed.info.state = "ended"; // prevent exit callback from acting
740
+ try {
741
+ managed.process.kill();
742
+ }
743
+ catch {
744
+ // ignore kill errors
745
+ }
746
+ managed.process = null;
747
+ }
748
+ this.sessions.delete(id);
749
+ this.scrollbackLog?.remove(id);
750
+ this.store.mutateSessions((s) => {
751
+ s.sessions = s.sessions.filter((sess) => sess.id !== id);
752
+ s.sessionRecords = s.sessionRecords.filter((rec) => rec.id !== id);
753
+ });
754
+ }
755
+ finally {
756
+ release();
757
+ }
758
+ }
759
+ async disposeAll() {
760
+ const ids = [...this.sessions.keys()];
761
+ await Promise.all(ids.map((id) => this.dispose(id)));
762
+ }
763
+ /**
764
+ * Graceful-shutdown variant: kill every live PTY and mark each session
765
+ * as graceful, but keep the session in state so it can be restored
766
+ * (with scrollback) on the next server start. Unlike dispose, does not
767
+ * remove sessions from the store. The optional `reason` lets the
768
+ * daemon shutdown path distinguish itself from the server shutdown
769
+ * path so the UI can pick the correct error/recovery branch -- a
770
+ * "daemon-graceful" exit means the PTY children themselves are gone
771
+ * (the daemon owned them), while a "server-graceful" exit in daemon
772
+ * mode would be unusual (server unit shutdown but the daemon's PTYs
773
+ * survive).
774
+ */
775
+ async shutdownAll(reason = { type: "server-graceful" }) {
776
+ const entries = [...this.sessions.values()];
777
+ await Promise.all(entries.map(async (managed) => {
778
+ const release = await managed.mutex.acquire();
779
+ try {
780
+ if (managed.info.state === "ended")
781
+ return;
782
+ managed.info = {
783
+ ...managed.info,
784
+ state: "ended",
785
+ pid: null,
786
+ endedAt: Date.now(),
787
+ endReason: reason,
788
+ };
789
+ if (managed.process) {
790
+ try {
791
+ managed.process.kill();
792
+ }
793
+ catch {
794
+ // ignore
795
+ }
796
+ managed.process = null;
797
+ }
798
+ this.persistSession(managed.info);
799
+ // Mirror the shutdown into the SessionRecord: from the
800
+ // orphan-reconcile point of view this PTY is gone (state
801
+ // "exited", PID null). exitSignal records the signal that
802
+ // would have been delivered -- node-pty's `kill()` with no
803
+ // argument sends SIGHUP (controlling-terminal hangup), so we
804
+ // mirror that here. We do *not* remove the record -- the next
805
+ // daemon boot will see state="exited" and pass it through
806
+ // unchanged (orphan-cleanup ignores already-terminal records).
807
+ if (managed.record && this.daemonContext) {
808
+ managed.record = {
809
+ ...managed.record,
810
+ pid: null,
811
+ pgid: null,
812
+ state: "exited",
813
+ exitSignal: "SIGHUP",
814
+ };
815
+ this.persistRecord(managed.record);
816
+ }
817
+ }
818
+ finally {
819
+ release();
820
+ }
821
+ }));
822
+ // Ensure any buffered scrollback writes are on disk before shutdown
823
+ // completes; the marker file is written after this returns.
824
+ this.scrollbackLog?.flushAll();
825
+ }
826
+ /**
827
+ * Restore a session from persisted AppState. Sessions that were running
828
+ * or spawning at server/daemon exit time carry no `endReason` yet --
829
+ * fill it in based on whether the previous shutdown was graceful
830
+ * (marker present) or a crash (marker absent). The host context picks
831
+ * the correct prefix: `daemon-*` when this is the host instance owned
832
+ * by the `parasor-pty-host` daemon (PTY children belonged to the
833
+ * daemon), `server-*` otherwise. Sessions that already have
834
+ * `endReason` (either from an earlier process exit or from
835
+ * `shutdownAll`) keep it.
836
+ */
837
+ loadPersistedSession(session, wasGracefulShutdown) {
838
+ const fallback = sessionPolicy.deriveLoadFallbackEndReason(this.daemonContext !== null, wasGracefulShutdown);
839
+ const endReason = session.endReason ?? fallback;
840
+ // Pick up any pre-existing record for this session from AppState so
841
+ // restart() / dispose() / shutdownAll() round-trip correctly. If the
842
+ // store has no matching record (in-process upgrade scenario, or the
843
+ // previous shutdown was a crash that lost the record), leave it null
844
+ // -- orphan-cleanup will not touch a record that does not exist.
845
+ const existingRecord = this.store.get().sessionRecords.find((r) => r.id === session.id) ?? null;
846
+ const managed = {
847
+ info: { ...session, state: "ended", pid: null, endReason },
848
+ process: null,
849
+ ptySize: null,
850
+ currentGeneration: session.generation,
851
+ attachedClients: new Map(),
852
+ outputPaused: false,
853
+ mutex: new PromiseMutex(),
854
+ bootstrapInput: null,
855
+ record: existingRecord ? structuredClone(existingRecord) : null,
856
+ };
857
+ this.sessions.set(session.id, managed);
858
+ // The persisted copy from state.json may still carry `state: "running"`
859
+ // (crash) or lack endReason; sync the corrected info so REST, WS
860
+ // snapshots, and the on-disk state all agree.
861
+ this.persistSession(managed.info);
862
+ }
863
+ onSessionData(callback) {
864
+ this.globalDataListeners.push(callback);
865
+ }
866
+ /**
867
+ * Attempt a silent re-spawn, preserving the previous scrollback (so
868
+ * the user sees continuity) plus a visible separator line that says
869
+ * exactly when the restart happened. Caller must hold managed.mutex.
870
+ * Returns true if re-spawn was initiated; false if the session is not
871
+ * auto-resumable (caller should surface an error).
872
+ */
873
+ autoResumeIfSafe(managed, cols, rows) {
874
+ if (!sessionPolicy.isAutoResumable(managed.info.command, managed.info.endReason)) {
875
+ return false;
876
+ }
877
+ const separator = sessionPolicy.buildRestartSeparator();
878
+ this.scrollbackLog?.append(managed.info.id, separator);
879
+ this.updateHeadlessState(managed.info.id, separator);
880
+ const nextGen = managed.currentGeneration + 1;
881
+ managed.currentGeneration = nextGen;
882
+ this.scrollbackLog?.bumpGeneration(managed.info.id, nextGen);
883
+ managed.info = {
884
+ ...managed.info,
885
+ pid: null,
886
+ state: "spawning",
887
+ generation: nextGen,
888
+ endedAt: undefined,
889
+ endReason: undefined,
890
+ };
891
+ managed.attachedClients.clear();
892
+ this.persistSession(managed.info);
893
+ // Record participates in orphan reconciliation again. Same rule as
894
+ // restart(): same daemon, so daemonPid/daemonStartedAt stay.
895
+ if (managed.record && this.daemonContext) {
896
+ managed.record = {
897
+ ...managed.record,
898
+ pid: null,
899
+ pgid: null,
900
+ state: "running",
901
+ exitCode: null,
902
+ exitSignal: null,
903
+ startedAt: new Date().toISOString(),
904
+ };
905
+ this.persistRecord(managed.record);
906
+ }
907
+ this.spawnProcess(managed, cols, rows);
908
+ return true;
909
+ }
910
+ /**
911
+ * Spawn the underlying PTY process and wire up its handlers. Called
912
+ * from `initClient` (deferred spawn) and from the test-only eager path.
913
+ * Caller must hold managed.mutex.
914
+ */
915
+ spawnProcess(managed, cols, rows) {
916
+ const { spawnCmd, spawnArgs } = this.resolveCommand(managed.info.command);
917
+ const proc = pty.spawn(spawnCmd, spawnArgs, {
918
+ name: "xterm-256color",
919
+ cols,
920
+ rows,
921
+ cwd: managed.info.cwd,
922
+ env: this.buildSessionEnv(managed.info.id, managed.info.projectId),
923
+ });
924
+ managed.process = proc;
925
+ managed.ptySize = { cols, rows };
926
+ managed.outputPaused = false;
927
+ managed.info = {
928
+ ...managed.info,
929
+ pid: proc.pid,
930
+ state: "running",
931
+ };
932
+ this.persistSession(managed.info);
933
+ // node-pty calls setsid -> the child is its own session leader and
934
+ // the leader's pid == pgid. We can persist the same value as both
935
+ // pid and pgid; the daemon shutdown path uses kill(-pgid, ...) to
936
+ // kill the whole process group atomically (by design).
937
+ if (managed.record && this.daemonContext) {
938
+ managed.record = {
939
+ ...managed.record,
940
+ pid: proc.pid,
941
+ pgid: proc.pid,
942
+ state: "running",
943
+ // Refresh startedAt so the record's "running" window is the
944
+ // actual spawn time (not the create-stub time).
945
+ startedAt: new Date().toISOString(),
946
+ };
947
+ this.persistRecord(managed.record);
948
+ }
949
+ this.attachProcHandlers(managed, managed.currentGeneration);
950
+ const bootstrapInput = managed.bootstrapInput;
951
+ if (bootstrapInput) {
952
+ managed.bootstrapInput = null;
953
+ this.write(managed.info.id, bootstrapInput, managed.currentGeneration);
954
+ }
955
+ }
956
+ attachProcHandlers(managed, generationAtSpawn) {
957
+ const id = managed.info.id;
958
+ /*
959
+ * PTY generation gate: per-spawn batching state. `pendingData` and `flushScheduled`
960
+ * MUST live in this closure, not on `managed`, otherwise an old-gen
961
+ * setImmediate flush that fires AFTER auto-resume has spawned a new
962
+ * PTY can consume bytes from the new PTY's onData handler (which
963
+ * appends to the shared `managed.pendingData`) and either (a) misroute
964
+ * them under the old generation through globalDataListeners or (b)
965
+ * drop them entirely because the `generationStillCurrent` gate fires
966
+ * `false` for the merged batch. With a per-spawn closure each generation
967
+ * has an isolated buffer and an isolated flush slot, so old-gen flushes
968
+ * see only old-gen bytes and new-gen flushes see only new-gen bytes.
969
+ */
970
+ let pendingData = "";
971
+ let flushScheduled = false;
972
+ managed.process?.onData((data) => {
973
+ pendingData += data;
974
+ if (!flushScheduled) {
975
+ flushScheduled = true;
976
+ setImmediate(() => {
977
+ const batch = pendingData;
978
+ pendingData = "";
979
+ flushScheduled = false;
980
+ // : 1 broadcast = 1 chunk. Compute the
981
+ // (gen, seq) pair once per flush so every chunk-flavored
982
+ // listener observes the same coordinates. The buffer is shared
983
+ // across listeners -- none of them should mutate it.
984
+ //
985
+ // PTY generation gate: tag with the spawn-time generation, NOT the live
986
+ // `managed.currentGeneration`. If auto-resume bumped the gen
987
+ // between data-arrival and this setImmediate flush, the live
988
+ // counter is already pointing at the new PTY, but every byte
989
+ // in `batch` was emitted by the old one. Using the live gen
990
+ // here would mis-tag old terminal-mode-query responses with
991
+ // the new generation, defeating the whole point of PTY generation gate.
992
+ const generation = generationAtSpawn;
993
+ /*
994
+ * PTY generation gate: if auto-resume bumped the gen between data-arrival
995
+ * and this flush, skip the chunk-ring append and the live
996
+ * client broadcast. The new ring (allocated by
997
+ * `bumpGeneration`) must not be overwritten by an
998
+ * `appendChunk(id, OLDGEN, ...)` that would reset it to
999
+ * fresh state with seq=0. Newly-attached clients of the
1000
+ * new session must not see old-PTY noise.
1001
+ *
1002
+ * PTY generation gate: also skip the disk `scrollbackLog.append()`
1003
+ * when stale. Otherwise auto-resume leaves a residue of old-PTY
1004
+ * bytes in the on-disk rehydration tail -- symmetric to the
1005
+ * daemon-side drop in remote-host.ts:551. Daemon-IPC propagation
1006
+ * (globalDataListeners) still fires below with `generation`, so
1007
+ * the remote-side gate decides whether to keep or drop.
1008
+ */
1009
+ const generationStillCurrent = generation === managed.currentGeneration;
1010
+ let seq = null;
1011
+ let batchBuf = null;
1012
+ if (generationStillCurrent) {
1013
+ this.scrollbackLog?.append(id, batch);
1014
+ this.updateHeadlessState(id, batch);
1015
+ let hasChunkClient = false;
1016
+ for (const client of managed.attachedClients.values()) {
1017
+ if (client.kind === "chunk") {
1018
+ hasChunkClient = true;
1019
+ break;
1020
+ }
1021
+ }
1022
+ if (hasChunkClient && this.scrollbackLog) {
1023
+ batchBuf = Buffer.from(batch, "utf8");
1024
+ seq = this.scrollbackLog.appendChunk(id, generation, batchBuf);
1025
+ }
1026
+ // Broadcast to every attached client; isolate listener faults
1027
+ // so one throwing client can't starve the others.
1028
+ for (const client of managed.attachedClients.values()) {
1029
+ try {
1030
+ if (client.kind === "string") {
1031
+ client.listener(batch);
1032
+ }
1033
+ else if (seq !== null && batchBuf) {
1034
+ client.listener(generation, seq, batchBuf);
1035
+ }
1036
+ }
1037
+ catch {
1038
+ // ignore listener errors to keep broadcast loop alive
1039
+ }
1040
+ }
1041
+ }
1042
+ // same isolation as attached-client listeners.
1043
+ // A throwing global listener (debug recorder, scrollback log) used
1044
+ // to leak as an uncaught exception inside setImmediate.
1045
+ for (const listener of this.globalDataListeners) {
1046
+ try {
1047
+ listener(id, batch, generation);
1048
+ }
1049
+ catch {
1050
+ // ignore listener errors
1051
+ }
1052
+ }
1053
+ });
1054
+ }
1055
+ });
1056
+ // node-pty's onExit handler treats the callback
1057
+ // return value as fire-and-forget. If we hand it an async function
1058
+ // and any await inside rejects (mutex.acquire, persistSession,
1059
+ // persistRecord), the rejection becomes an unhandledRejection. Wrap
1060
+ // the body in a sync function that explicitly catches the inner
1061
+ // promise so onExit cannot bubble.
1062
+ managed.process?.onExit(({ exitCode, signal }) => {
1063
+ void (async () => {
1064
+ const release = await managed.mutex.acquire();
1065
+ try {
1066
+ if (!this.sessions.has(id))
1067
+ return;
1068
+ if (managed.currentGeneration !== generationAtSpawn)
1069
+ return;
1070
+ // shutdownAll/dispose already set state=ended and recorded the
1071
+ // authoritative endReason; don't overwrite it with the kill signal
1072
+ // that happens to fire afterwards.
1073
+ if (managed.info.state === "ended")
1074
+ return;
1075
+ const endReason = sessionPolicy.deriveEndReason(signal, exitCode);
1076
+ managed.info = {
1077
+ ...managed.info,
1078
+ state: "ended",
1079
+ pid: null,
1080
+ endedAt: Date.now(),
1081
+ endReason,
1082
+ };
1083
+ managed.process = null;
1084
+ // : notify binary-capable clients via the EXIT
1085
+ // chunk-listener channel. Legacy string clients learn about
1086
+ // exit via /ws/events SESSION_END (existing behavior).
1087
+ const numericExit = typeof exitCode === "number" && Number.isFinite(exitCode)
1088
+ ? exitCode
1089
+ : 0;
1090
+ for (const client of managed.attachedClients.values()) {
1091
+ if (client.kind === "chunk" && client.onExit) {
1092
+ try {
1093
+ client.onExit(numericExit);
1094
+ }
1095
+ catch {
1096
+ // ignore listener faults; broadcast loop is dead anyway
1097
+ }
1098
+ }
1099
+ }
1100
+ this.persistSession(managed.info);
1101
+ // Record the natural exit. node-pty exposes `signal` as a
1102
+ // numeric POSIX signal (or undefined when the child exited
1103
+ // normally); the SessionRecord schema stores the signal *name*
1104
+ // and a `number | null` exit code. deriveRecordExit coerces both
1105
+ // (null exit code when killed, `SIG<n>` for unmapped signals).
1106
+ if (managed.record && this.daemonContext) {
1107
+ const { exitCode: recordExitCode, exitSignal } = sessionPolicy.deriveRecordExit(exitCode, signal);
1108
+ managed.record = {
1109
+ ...managed.record,
1110
+ pid: null,
1111
+ pgid: null,
1112
+ state: "exited",
1113
+ exitCode: recordExitCode,
1114
+ exitSignal,
1115
+ };
1116
+ this.persistRecord(managed.record);
1117
+ }
1118
+ this.onSessionExit?.(id, generationAtSpawn, endReason);
1119
+ }
1120
+ finally {
1121
+ release();
1122
+ }
1123
+ })().catch(() => {
1124
+ // Last-resort guard -- anything thrown inside the body (e.g. a
1125
+ // future store.mutateSessions() rejection) gets swallowed so node-pty
1126
+ // doesn't see an unhandledRejection-shaped event.
1127
+ });
1128
+ });
1129
+ }
1130
+ persistSession(info) {
1131
+ this.store.mutateSessions((s) => {
1132
+ const idx = s.sessions.findIndex((sess) => sess.id === info.id);
1133
+ if (idx >= 0) {
1134
+ s.sessions[idx] = structuredClone(info);
1135
+ }
1136
+ else {
1137
+ s.sessions.push(structuredClone(info));
1138
+ }
1139
+ });
1140
+ }
1141
+ /**
1142
+ * Upsert a SessionRecord into AppState. Mirror of `persistSession` for
1143
+ * the daemon-orphan-tracking side of the data model. Caller must hold
1144
+ * managed.mutex (record updates are interleaved with session updates,
1145
+ * single-record-per-session invariant guards against torn writes).
1146
+ */
1147
+ persistRecord(record) {
1148
+ this.store.mutateSessions((s) => {
1149
+ const idx = s.sessionRecords.findIndex((rec) => rec.id === record.id);
1150
+ if (idx >= 0) {
1151
+ s.sessionRecords[idx] = structuredClone(record);
1152
+ }
1153
+ else {
1154
+ s.sessionRecords.push(structuredClone(record));
1155
+ }
1156
+ });
1157
+ }
1158
+ /**
1159
+ * Test-only eager spawn. Production code path is always deferred via
1160
+ * `initClient`. This helper exists so unit tests that need a live PTY
1161
+ * without a WebSocket can still drive the manager.
1162
+ */
1163
+ async testEagerSpawn(id, cols = 80, rows = 24) {
1164
+ const managed = this.sessions.get(id);
1165
+ if (!managed)
1166
+ throw new Error(`Session ${id} not found`);
1167
+ const release = await managed.mutex.acquire();
1168
+ try {
1169
+ if (managed.info.state !== "spawning")
1170
+ return;
1171
+ this.spawnProcess(managed, cols, rows);
1172
+ }
1173
+ finally {
1174
+ release();
1175
+ }
1176
+ }
1177
+ resolveCommand(command) {
1178
+ return sessionPolicy.resolveSessionCommand(command, {
1179
+ bashRcPath: this.ptyEnv.PARASOR_BASH_RC,
1180
+ });
1181
+ }
1182
+ }
1183
+ //# sourceMappingURL=in-process-host.js.map