@aoagents/ao-web 0.5.0 → 0.7.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 (265) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +196 -182
  3. package/.next/app-path-routes-manifest.json +19 -17
  4. package/.next/build-manifest.json +6 -6
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +21 -21
  7. package/.next/react-loadable-manifest.json +5 -3
  8. package/.next/required-server-files.json +4 -5
  9. package/.next/server/app/_not-found/page.js +2 -2
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +8 -8
  14. package/.next/server/app/api/backlog/route.js +1 -1
  15. package/.next/server/app/api/backlog/route.js.nft.json +1 -1
  16. package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/browse-directory/route.js +1 -1
  18. package/.next/server/app/api/browse-directory/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/filesystem/browse/route.js +1 -1
  20. package/.next/server/app/api/filesystem/browse/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/issues/route.js +1 -1
  22. package/.next/server/app/api/issues/route.js.nft.json +1 -1
  23. package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/observability/route.js +1 -1
  25. package/.next/server/app/api/observability/route.js.nft.json +1 -1
  26. package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/orchestrators/route.js +1 -1
  28. package/.next/server/app/api/orchestrators/route.js.nft.json +1 -1
  29. package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/projects/[id]/route.js +5 -1
  31. package/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
  32. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/projects/reload/route.js +1 -1
  34. package/.next/server/app/api/projects/reload/route.js.nft.json +1 -1
  35. package/.next/server/app/api/projects/reload/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/route.js +1 -1
  37. package/.next/server/app/api/projects/route.js.nft.json +1 -1
  38. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/prs/[id]/merge/route.js +1 -1
  40. package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -1
  41. package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/runtime/terminal/route.js +1 -1
  43. package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
  45. package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/sessions/[id]/message/route.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/sessions/[id]/remap/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
  54. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  55. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  57. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/sessions/[id]/send/route.js +1 -1
  60. package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
  61. package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/sessions/patches/route.js +1 -1
  63. package/.next/server/app/api/sessions/patches/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/patches/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/sessions/route.js +1 -1
  66. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/setup-labels/route.js +1 -1
  69. package/.next/server/app/api/setup-labels/route.js.nft.json +1 -1
  70. package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/spawn/route.js +1 -1
  72. package/.next/server/app/api/spawn/route.js.nft.json +1 -1
  73. package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/update/route.js +1 -0
  75. package/.next/server/app/api/update/route.js.nft.json +1 -0
  76. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -0
  77. package/.next/server/app/api/verify/route.js +1 -1
  78. package/.next/server/app/api/verify/route.js.nft.json +1 -1
  79. package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/version/route.js +1 -0
  81. package/.next/server/app/api/version/route.js.nft.json +1 -0
  82. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
  83. package/.next/server/app/api/webhooks/[...slug]/route.js +1 -1
  84. package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -1
  85. package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/apple-icon/route.js +1 -1
  87. package/.next/server/app/apple-icon/route.js.nft.json +1 -1
  88. package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/apple-icon.body +0 -0
  90. package/.next/server/app/dev/terminal-test/page.js +3 -3
  91. package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -1
  92. package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app/dev/terminal-test.html +1 -1
  94. package/.next/server/app/dev/terminal-test.rsc +9 -9
  95. package/.next/server/app/icon/route.js +1 -1
  96. package/.next/server/app/icon/route.js.nft.json +1 -1
  97. package/.next/server/app/icon/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/icon-192/route.js +1 -1
  99. package/.next/server/app/icon-192/route.js.nft.json +1 -1
  100. package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/icon-512/route.js +1 -1
  102. package/.next/server/app/icon-512/route.js.nft.json +1 -1
  103. package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/icon.body +0 -0
  105. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  106. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  107. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/manifest.webmanifest.body +1 -1
  109. package/.next/server/app/orchestrators/page.js +2 -2
  110. package/.next/server/app/orchestrators/page.js.nft.json +1 -1
  111. package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app/page.js +2 -2
  113. package/.next/server/app/page.js.nft.json +1 -1
  114. package/.next/server/app/page_client-reference-manifest.js +1 -1
  115. package/.next/server/app/projects/[projectId]/page.js +2 -2
  116. package/.next/server/app/projects/[projectId]/page.js.nft.json +1 -1
  117. package/.next/server/app/projects/[projectId]/page_client-reference-manifest.js +1 -1
  118. package/.next/server/app/projects/[projectId]/sessions/[id]/page.js +2 -2
  119. package/.next/server/app/projects/[projectId]/sessions/[id]/page.js.nft.json +1 -1
  120. package/.next/server/app/projects/[projectId]/sessions/[id]/page_client-reference-manifest.js +1 -1
  121. package/.next/server/app/projects/[projectId]/settings/page.js +2 -2
  122. package/.next/server/app/projects/[projectId]/settings/page.js.nft.json +1 -1
  123. package/.next/server/app/projects/[projectId]/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/prs/page.js +2 -2
  125. package/.next/server/app/prs/page.js.nft.json +1 -1
  126. package/.next/server/app/prs/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/sessions/[id]/page.js +2 -2
  128. package/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  129. package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/test-direct/page.js +2 -2
  131. package/.next/server/app/test-direct/page.js.nft.json +1 -1
  132. package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -1
  133. package/.next/server/app/test-direct.html +1 -1
  134. package/.next/server/app/test-direct.rsc +9 -9
  135. package/.next/server/app-paths-manifest.json +19 -17
  136. package/.next/server/chunks/1271.js +1 -1
  137. package/.next/server/chunks/1876.js +2 -2
  138. package/.next/server/chunks/303.js +3 -0
  139. package/.next/server/chunks/3714.js +1 -1
  140. package/.next/server/chunks/4520.js +1 -1
  141. package/.next/server/chunks/6013.js +884 -0
  142. package/.next/server/chunks/6848.js +1 -0
  143. package/.next/server/chunks/7167.js +1 -1
  144. package/.next/server/chunks/7173.js +1 -1
  145. package/.next/server/chunks/7227.js +604 -0
  146. package/.next/server/middleware-build-manifest.js +1 -1
  147. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  148. package/.next/server/pages/404.html +1 -1
  149. package/.next/server/pages/500.html +1 -1
  150. package/.next/server/server-reference-manifest.json +1 -1
  151. package/.next/static/9nr0fNWbZcuWTqhM2HhrH/_buildManifest.js +1 -0
  152. package/.next/static/chunks/1654.ac304fc9e36ec94a.js +1 -0
  153. package/.next/static/chunks/3764.cdef4e76dbc23af8.js +1 -0
  154. package/.next/static/chunks/3780-7bdc52d8370adf2f.js +1 -0
  155. package/.next/static/chunks/{5795-b96fd46c8c7344fc.js → 5795-a4dd81606df09bc4.js} +1 -1
  156. package/.next/static/chunks/6231-57dd9c1e306c7069.js +1 -0
  157. package/.next/static/chunks/app/_not-found/page-8b5044bdc951ae98.js +1 -0
  158. package/.next/static/chunks/app/api/backlog/route-8b5044bdc951ae98.js +1 -0
  159. package/.next/static/chunks/app/api/browse-directory/route-8b5044bdc951ae98.js +1 -0
  160. package/.next/static/chunks/app/api/filesystem/browse/route-8b5044bdc951ae98.js +1 -0
  161. package/.next/static/chunks/app/api/issues/route-8b5044bdc951ae98.js +1 -0
  162. package/.next/static/chunks/app/api/observability/route-8b5044bdc951ae98.js +1 -0
  163. package/.next/static/chunks/app/api/orchestrators/route-8b5044bdc951ae98.js +1 -0
  164. package/.next/static/chunks/app/api/projects/[id]/route-8b5044bdc951ae98.js +1 -0
  165. package/.next/static/chunks/app/api/projects/reload/route-8b5044bdc951ae98.js +1 -0
  166. package/.next/static/chunks/app/api/projects/route-8b5044bdc951ae98.js +1 -0
  167. package/.next/static/chunks/app/api/prs/[id]/merge/route-8b5044bdc951ae98.js +1 -0
  168. package/.next/static/chunks/app/api/runtime/terminal/route-8b5044bdc951ae98.js +1 -0
  169. package/.next/static/chunks/app/api/sessions/[id]/kill/route-8b5044bdc951ae98.js +1 -0
  170. package/.next/static/chunks/app/api/sessions/[id]/message/route-8b5044bdc951ae98.js +1 -0
  171. package/.next/static/chunks/app/api/sessions/[id]/remap/route-8b5044bdc951ae98.js +1 -0
  172. package/.next/static/chunks/app/api/sessions/[id]/restore/route-8b5044bdc951ae98.js +1 -0
  173. package/.next/static/chunks/app/api/sessions/[id]/route-8b5044bdc951ae98.js +1 -0
  174. package/.next/static/chunks/app/api/sessions/[id]/send/route-8b5044bdc951ae98.js +1 -0
  175. package/.next/static/chunks/app/api/sessions/patches/route-8b5044bdc951ae98.js +1 -0
  176. package/.next/static/chunks/app/api/sessions/route-8b5044bdc951ae98.js +1 -0
  177. package/.next/static/chunks/app/api/setup-labels/route-8b5044bdc951ae98.js +1 -0
  178. package/.next/static/chunks/app/api/spawn/route-8b5044bdc951ae98.js +1 -0
  179. package/.next/static/chunks/app/api/update/route-8b5044bdc951ae98.js +1 -0
  180. package/.next/static/chunks/app/api/verify/route-8b5044bdc951ae98.js +1 -0
  181. package/.next/static/chunks/app/api/version/route-8b5044bdc951ae98.js +1 -0
  182. package/.next/static/chunks/app/api/webhooks/[...slug]/route-8b5044bdc951ae98.js +1 -0
  183. package/.next/static/chunks/app/apple-icon/route-8b5044bdc951ae98.js +1 -0
  184. package/.next/static/chunks/app/dev/terminal-test/page-9aa423dfd54c8325.js +1 -0
  185. package/.next/static/chunks/app/{error-da1d10c96ff5dd29.js → error-65c526052680c0dc.js} +1 -1
  186. package/.next/static/chunks/app/{global-error-ca06d2b1be2d4ae0.js → global-error-63dcb797b2c3ee60.js} +1 -1
  187. package/.next/static/chunks/app/icon/route-8b5044bdc951ae98.js +1 -0
  188. package/.next/static/chunks/app/icon-192/route-8b5044bdc951ae98.js +1 -0
  189. package/.next/static/chunks/app/icon-512/route-8b5044bdc951ae98.js +1 -0
  190. package/.next/static/chunks/app/{layout-bb6db479523cb3d6.js → layout-36ab0168ddb22083.js} +1 -1
  191. package/.next/static/chunks/app/loading-8b5044bdc951ae98.js +1 -0
  192. package/.next/static/chunks/app/manifest.webmanifest/route-8b5044bdc951ae98.js +1 -0
  193. package/.next/static/chunks/app/{not-found-824d5d3c6e296eeb.js → not-found-a693bed1f9e1893f.js} +1 -1
  194. package/.next/static/chunks/app/orchestrators/{page-f07983413ed1a44b.js → page-376a92db51deb112.js} +1 -1
  195. package/.next/static/chunks/app/page-587d546e62c0796f.js +1 -0
  196. package/.next/static/chunks/app/projects/[projectId]/loading-8b5044bdc951ae98.js +1 -0
  197. package/.next/static/chunks/app/projects/[projectId]/page-bd8fc2a1decb649d.js +1 -0
  198. package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-bd33f6ffda513080.js +1 -0
  199. package/.next/static/chunks/app/projects/[projectId]/settings/page-11facc471a63de50.js +1 -0
  200. package/.next/static/chunks/app/prs/page-f34f66ad51106080.js +1 -0
  201. package/.next/static/chunks/app/sessions/[id]/{error-8de5b24e86eeae7b.js → error-df65e7b626bbb713.js} +1 -1
  202. package/.next/static/chunks/app/sessions/[id]/loading-8b5044bdc951ae98.js +1 -0
  203. package/.next/static/chunks/app/sessions/[id]/{not-found-824d5d3c6e296eeb.js → not-found-a693bed1f9e1893f.js} +1 -1
  204. package/.next/static/chunks/app/sessions/[id]/page-3ea4aa79275ea449.js +1 -0
  205. package/.next/static/chunks/app/test-direct/page-edfc701a9300105b.js +1 -0
  206. package/.next/static/chunks/{main-app-690acf9d5d2050c9.js → main-app-decbc53736801215.js} +1 -1
  207. package/.next/static/chunks/{webpack-d2dfbd3e9262b74e.js → webpack-ecf0988dbb79e19b.js} +1 -1
  208. package/.next/static/css/b93232cd4a58743d.css +1 -0
  209. package/dist-server/direct-terminal-ws.js +12 -2
  210. package/dist-server/mux-websocket.js +319 -76
  211. package/dist-server/start-all.js +28 -6
  212. package/dist-server/tmux-utils.js +124 -2
  213. package/next.config.js +26 -0
  214. package/package.json +26 -12
  215. package/.next/server/chunks/1172.js +0 -1
  216. package/.next/server/chunks/3131.js +0 -3
  217. package/.next/server/chunks/801.js +0 -658
  218. package/.next/server/chunks/9223.js +0 -440
  219. package/.next/static/YR6Xi4DC5A7S7E2PoZuif/_buildManifest.js +0 -1
  220. package/.next/static/chunks/1383.c891a8ba72ee600c.js +0 -1
  221. package/.next/static/chunks/3764.89a5955e46eb74b4.js +0 -1
  222. package/.next/static/chunks/3780-52c4733ce6591b8d.js +0 -1
  223. package/.next/static/chunks/4465-17154f7a01abfe85.js +0 -1
  224. package/.next/static/chunks/app/_not-found/page-3b8a01e726e988c8.js +0 -1
  225. package/.next/static/chunks/app/api/backlog/route-3b8a01e726e988c8.js +0 -1
  226. package/.next/static/chunks/app/api/browse-directory/route-3b8a01e726e988c8.js +0 -1
  227. package/.next/static/chunks/app/api/filesystem/browse/route-3b8a01e726e988c8.js +0 -1
  228. package/.next/static/chunks/app/api/issues/route-3b8a01e726e988c8.js +0 -1
  229. package/.next/static/chunks/app/api/observability/route-3b8a01e726e988c8.js +0 -1
  230. package/.next/static/chunks/app/api/orchestrators/route-3b8a01e726e988c8.js +0 -1
  231. package/.next/static/chunks/app/api/projects/[id]/route-3b8a01e726e988c8.js +0 -1
  232. package/.next/static/chunks/app/api/projects/reload/route-3b8a01e726e988c8.js +0 -1
  233. package/.next/static/chunks/app/api/projects/route-3b8a01e726e988c8.js +0 -1
  234. package/.next/static/chunks/app/api/prs/[id]/merge/route-3b8a01e726e988c8.js +0 -1
  235. package/.next/static/chunks/app/api/runtime/terminal/route-3b8a01e726e988c8.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/[id]/kill/route-3b8a01e726e988c8.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/[id]/message/route-3b8a01e726e988c8.js +0 -1
  238. package/.next/static/chunks/app/api/sessions/[id]/remap/route-3b8a01e726e988c8.js +0 -1
  239. package/.next/static/chunks/app/api/sessions/[id]/restore/route-3b8a01e726e988c8.js +0 -1
  240. package/.next/static/chunks/app/api/sessions/[id]/route-3b8a01e726e988c8.js +0 -1
  241. package/.next/static/chunks/app/api/sessions/[id]/send/route-3b8a01e726e988c8.js +0 -1
  242. package/.next/static/chunks/app/api/sessions/patches/route-3b8a01e726e988c8.js +0 -1
  243. package/.next/static/chunks/app/api/sessions/route-3b8a01e726e988c8.js +0 -1
  244. package/.next/static/chunks/app/api/setup-labels/route-3b8a01e726e988c8.js +0 -1
  245. package/.next/static/chunks/app/api/spawn/route-3b8a01e726e988c8.js +0 -1
  246. package/.next/static/chunks/app/api/verify/route-3b8a01e726e988c8.js +0 -1
  247. package/.next/static/chunks/app/api/webhooks/[...slug]/route-3b8a01e726e988c8.js +0 -1
  248. package/.next/static/chunks/app/apple-icon/route-3b8a01e726e988c8.js +0 -1
  249. package/.next/static/chunks/app/dev/terminal-test/page-5819e40b3d4754ef.js +0 -1
  250. package/.next/static/chunks/app/icon/route-3b8a01e726e988c8.js +0 -1
  251. package/.next/static/chunks/app/icon-192/route-3b8a01e726e988c8.js +0 -1
  252. package/.next/static/chunks/app/icon-512/route-3b8a01e726e988c8.js +0 -1
  253. package/.next/static/chunks/app/loading-3b8a01e726e988c8.js +0 -1
  254. package/.next/static/chunks/app/manifest.webmanifest/route-3b8a01e726e988c8.js +0 -1
  255. package/.next/static/chunks/app/page-6aa506a579ac9949.js +0 -1
  256. package/.next/static/chunks/app/projects/[projectId]/loading-3b8a01e726e988c8.js +0 -1
  257. package/.next/static/chunks/app/projects/[projectId]/page-49eb5b990c74ca8f.js +0 -1
  258. package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-2450704c6b66a4b4.js +0 -1
  259. package/.next/static/chunks/app/projects/[projectId]/settings/page-d1da671e72a7bd5e.js +0 -1
  260. package/.next/static/chunks/app/prs/page-2332a7180a47f28c.js +0 -1
  261. package/.next/static/chunks/app/sessions/[id]/loading-3b8a01e726e988c8.js +0 -1
  262. package/.next/static/chunks/app/sessions/[id]/page-b60b49ccbafe51c9.js +0 -1
  263. package/.next/static/chunks/app/test-direct/page-eb366dde03fab6a7.js +0 -1
  264. package/.next/static/css/fcafd381715071b8.css +0 -1
  265. /package/.next/static/{YR6Xi4DC5A7S7E2PoZuif → 9nr0fNWbZcuWTqhM2HhrH}/_ssgManifest.js +0 -0
@@ -6,9 +6,10 @@
6
6
  * every 3s, then broadcast to all subscribed clients via WebSocket.
7
7
  */
8
8
  import { WebSocketServer, WebSocket } from "ws";
9
- import { homedir, userInfo } from "node:os";
10
9
  import { spawn } from "node:child_process";
11
- import { findTmux, resolveTmuxSession, validateSessionId } from "./tmux-utils.js";
10
+ import { connect as netConnect } from "node:net";
11
+ import { findTmux, resolveTmuxSession, resolvePipePath, tmuxHasSession, validateSessionId, } from "./tmux-utils.js";
12
+ import { getEnvDefaults, isWindows } from "@aoagents/ao-core";
12
13
  /**
13
14
  * Manages polling of session patches from Next.js /api/sessions/patches.
14
15
  * Broadcasts to all subscribed callbacks.
@@ -141,15 +142,31 @@ catch (err) {
141
142
  const RING_BUFFER_MAX = 50 * 1024; // 50KB max per terminal
142
143
  const WS_BUFFER_HIGH_WATERMARK = 64 * 1024; // 64KB
143
144
  const MAX_REATTACH_ATTEMPTS = 3;
145
+ /**
146
+ * Grace period a freshly-attached PTY must survive before its successful
147
+ * attach is allowed to reset the re-attach counter. Prevents tight crash
148
+ * loops (e.g. attaching to a tmux session that no longer exists) from
149
+ * gaming the MAX_REATTACH_ATTEMPTS cap by resetting the counter to 0
150
+ * between every failed attempt.
151
+ *
152
+ * 5 s is comfortably longer than the ~40 ms a doomed `tmux attach-session`
153
+ * takes to exit, while still being short enough that a healthy PTY which
154
+ * crashes hours later gets a fresh retry budget.
155
+ */
156
+ const REATTACH_RESET_GRACE_MS = 5_000;
144
157
  /**
145
158
  * TerminalManager manages PTY processes independently of WebSocket connections.
146
159
  * A single manager instance is shared across all mux connections.
147
160
  */
148
- class TerminalManager {
161
+ export class TerminalManager {
149
162
  terminals = new Map();
150
163
  TMUX;
151
164
  constructor(tmuxPath) {
152
- this.TMUX = tmuxPath ?? findTmux();
165
+ const resolved = tmuxPath ?? findTmux();
166
+ if (!resolved) {
167
+ throw new Error("tmux not available on this platform");
168
+ }
169
+ this.TMUX = resolved;
153
170
  }
154
171
  terminalKey(id, projectId) {
155
172
  return projectId ? `${projectId}:${id}` : id;
@@ -189,33 +206,37 @@ class TerminalManager {
189
206
  if (terminal.pty) {
190
207
  return tmuxSessionId;
191
208
  }
209
+ // tmux 3.4 only honours the `=` exact-match prefix on has-session and
210
+ // attach-session; set-option silently ignores it, so we use the bare id
211
+ // here. The `=`-prefixed form is built below for attach-session.
192
212
  // Enable mouse mode
193
- const exactTmuxTarget = `=${tmuxSessionId}`;
194
- const mouseProc = spawn(this.TMUX, ["set-option", "-t", exactTmuxTarget, "mouse", "on"]);
213
+ const mouseProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "mouse", "on"]);
195
214
  mouseProc.on("error", (err) => {
196
215
  console.error(`[MuxServer] Failed to set mouse mode for ${tmuxSessionId}:`, err.message);
197
216
  });
198
217
  // Hide the status bar
199
- const statusProc = spawn(this.TMUX, ["set-option", "-t", exactTmuxTarget, "status", "off"]);
218
+ const statusProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "status", "off"]);
200
219
  statusProc.on("error", (err) => {
201
220
  console.error(`[MuxServer] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
202
221
  });
203
222
  // Build environment
204
- const homeDir = process.env.HOME || homedir();
205
- const currentUser = process.env.USER || userInfo().username;
223
+ const platformDefaults = getEnvDefaults();
224
+ const homeDir = platformDefaults.HOME;
206
225
  const env = {
207
- HOME: homeDir,
208
- SHELL: process.env.SHELL || "/bin/bash",
209
- USER: currentUser,
210
- PATH: process.env.PATH || "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
226
+ HOME: platformDefaults.HOME,
227
+ SHELL: platformDefaults.SHELL,
228
+ USER: platformDefaults.USER,
229
+ PATH: process.env.PATH || platformDefaults.PATH,
211
230
  TERM: "xterm-256color",
212
231
  LANG: process.env.LANG || "en_US.UTF-8",
213
- TMPDIR: process.env.TMPDIR || "/tmp",
232
+ TMPDIR: platformDefaults.TMPDIR,
214
233
  };
215
234
  if (!ptySpawn) {
216
235
  throw new Error("node-pty not available");
217
236
  }
218
- // Spawn PTY
237
+ // Spawn PTY — use `=`-prefixed exact-match target so we never attach to
238
+ // a session whose name happens to be a prefix of the requested id.
239
+ const exactTmuxTarget = `=${tmuxSessionId}`;
219
240
  const pty = ptySpawn(this.TMUX, ["attach-session", "-t", exactTmuxTarget], {
220
241
  name: "xterm-256color",
221
242
  cols: 80,
@@ -224,6 +245,24 @@ class TerminalManager {
224
245
  env,
225
246
  });
226
247
  terminal.pty = pty;
248
+ // Schedule a grace-period reset of the re-attach counter. We only
249
+ // consider an attach "really successful" if the PTY survives long
250
+ // enough to suggest the underlying tmux session is actually usable.
251
+ // The closure-captured `pty` reference is compared with terminal.pty
252
+ // so a stale timer cannot reset the counter for a PTY that has
253
+ // already exited or been replaced by re-attach. Any previously-
254
+ // scheduled timer (from a now-replaced PTY) is cleared so we don't
255
+ // keep its closure references reachable until the timer fires.
256
+ if (terminal.resetTimer) {
257
+ clearTimeout(terminal.resetTimer);
258
+ }
259
+ terminal.resetTimer = setTimeout(() => {
260
+ terminal.resetTimer = undefined;
261
+ if (terminal.pty === pty) {
262
+ terminal.reattachAttempts = 0;
263
+ }
264
+ }, REATTACH_RESET_GRACE_MS);
265
+ terminal.resetTimer.unref();
227
266
  // Wire up data events
228
267
  pty.onData((data) => {
229
268
  // Push to all subscribers — isolate each callback so a throw in one
@@ -246,18 +285,49 @@ class TerminalManager {
246
285
  }
247
286
  });
248
287
  // Handle PTY exit
249
- pty.onExit(({ exitCode }) => {
288
+ //
289
+ // Async: the has-session probe shells out via promisified execFile and
290
+ // must be awaited. node-pty fires onExit on the main thread; a sync
291
+ // probe would freeze the entire web server (every WebSocket, HTTP
292
+ // request, in-flight terminal) for up to the subprocess timeout when
293
+ // tmux is slow to respond.
294
+ pty.onExit(async ({ exitCode }) => {
250
295
  console.log(`[MuxServer] PTY exited for ${id} with code ${exitCode}`);
251
296
  terminal.pty = null;
297
+ // Skip the re-attach loop entirely when the underlying tmux session is
298
+ // gone (e.g. user pressed Ctrl-C in the pane and the launch command
299
+ // exited, taking the only window with it). Without this guard we
300
+ // burn three doomed attach-session spawns and emit a noisy
301
+ // "Max re-attach attempts reached" log line for what is actually a
302
+ // clean user-initiated termination — see issue #1756. The
303
+ // MAX_REATTACH_ATTEMPTS bound from #1640 still covers tmux server
304
+ // hiccups where the session does still exist.
305
+ if (terminal.subscribers.size > 0 &&
306
+ !(await tmuxHasSession(this.TMUX, tmuxSessionId))) {
307
+ console.log(`[MuxServer] tmux session ${tmuxSessionId} is gone, not re-attaching`);
308
+ if (terminal.resetTimer) {
309
+ clearTimeout(terminal.resetTimer);
310
+ terminal.resetTimer = undefined;
311
+ }
312
+ for (const cb of terminal.exitCallbacks) {
313
+ cb(exitCode);
314
+ }
315
+ return;
316
+ }
252
317
  // Re-attach if subscribers are still present, up to MAX_REATTACH_ATTEMPTS.
253
318
  // The cap prevents an unbounded respawn loop when the PTY crashes immediately
254
319
  // after every attach (e.g. resource exhaustion or a broken tmux session).
320
+ // The counter is reset by a delayed timer in open() once the new PTY has
321
+ // survived REATTACH_RESET_GRACE_MS — see the comment on that constant.
322
+ // Resetting here would defeat the cap: when ao stop kills the tmux session
323
+ // out from under a still-subscribed dashboard, attach-session exits ~40 ms
324
+ // after spawn and the loop runs at ~80 spawns/sec, exhausting the system
325
+ // PTY pool in seconds (issue #1639).
255
326
  if (terminal.subscribers.size > 0 && terminal.reattachAttempts < MAX_REATTACH_ATTEMPTS) {
256
327
  terminal.reattachAttempts += 1;
257
328
  console.log(`[MuxServer] Re-attaching to ${id} (attempt ${terminal.reattachAttempts}/${MAX_REATTACH_ATTEMPTS})`);
258
329
  try {
259
330
  this.open(id, projectId, tmuxSessionId);
260
- terminal.reattachAttempts = 0; // reset on successful attach
261
331
  return; // re-attached — don't notify exit
262
332
  }
263
333
  catch (err) {
@@ -317,6 +387,10 @@ class TerminalManager {
317
387
  terminal.exitCallbacks.delete(onExit);
318
388
  // Kill PTY and clean up when the last subscriber leaves
319
389
  if (terminal.subscribers.size === 0) {
390
+ if (terminal.resetTimer) {
391
+ clearTimeout(terminal.resetTimer);
392
+ terminal.resetTimer = undefined;
393
+ }
320
394
  if (terminal.pty) {
321
395
  terminal.pty.kill();
322
396
  terminal.pty = null;
@@ -335,22 +409,163 @@ class TerminalManager {
335
409
  return terminal.buffer.join("");
336
410
  }
337
411
  }
412
+ /**
413
+ * Handle a Windows terminal message by relaying through named pipes.
414
+ * Extracted from the WebSocket connection handler for testability.
415
+ */
416
+ export function handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, deps) {
417
+ const WS_OPEN = 1; // WebSocket.OPEN
418
+ const { id, type, projectId } = msg;
419
+ // MuxProvider keys subscribers under `${projectId}:${id}` when projectId is
420
+ // provided, so every outbound terminal message must echo projectId back —
421
+ // otherwise the client routes by id alone and the subscriber bucket
422
+ // mismatches, leaving the xterm pane blank on /projects/[id]/sessions/[id].
423
+ const echo = projectId ? { projectId } : {};
424
+ // Project-scoped pipe-map key: matches the Unix `subscriptionKey` shape so
425
+ // two projects sharing a sessionId on the same mux connection don't collide
426
+ // on the same socket/buffer entry.
427
+ const pipeKey = projectId ? `${projectId}:${id}` : id;
428
+ // The Unix path validates inside TerminalManager.open(). The Windows pipe
429
+ // relay bypasses TerminalManager entirely, so validate here too — `id`
430
+ // becomes a map key and is constructed into a pipe path downstream.
431
+ if (!validateSessionId(id)) {
432
+ if (ws.readyState === WS_OPEN) {
433
+ ws.send(JSON.stringify({
434
+ ch: "terminal",
435
+ id,
436
+ type: "error",
437
+ message: "invalid session id",
438
+ ...echo,
439
+ }));
440
+ }
441
+ return;
442
+ }
443
+ if (type === "open") {
444
+ if (winPipes.has(pipeKey)) {
445
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
446
+ }
447
+ else {
448
+ const pipePath = deps.resolvePipePath(id, projectId);
449
+ if (!pipePath) {
450
+ throw new Error(`No PTY host pipe found for session ${id}`);
451
+ }
452
+ const pipeSocket = deps.connect(pipePath);
453
+ winPipes.set(pipeKey, pipeSocket);
454
+ winPipeBuffers.set(pipeKey, Buffer.alloc(0));
455
+ pipeSocket.on("error", (err) => {
456
+ winPipes.delete(pipeKey);
457
+ winPipeBuffers.delete(pipeKey);
458
+ pipeSocket.destroy();
459
+ if (ws.readyState === WS_OPEN) {
460
+ ws.send(JSON.stringify({
461
+ ch: "terminal",
462
+ id,
463
+ type: "error",
464
+ message: `PTY host not available: ${err.message}`,
465
+ ...echo,
466
+ }));
467
+ }
468
+ });
469
+ pipeSocket.on("connect", () => {
470
+ if (ws.readyState === WS_OPEN) {
471
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
472
+ }
473
+ pipeSocket.on("data", (chunk) => {
474
+ const existing = winPipeBuffers.get(pipeKey) ?? Buffer.alloc(0);
475
+ let buf = Buffer.concat([existing, chunk]);
476
+ winPipeBuffers.set(pipeKey, buf);
477
+ while (buf.length >= 5) {
478
+ const msgType = buf.readUInt8(0);
479
+ const length = buf.readUInt32BE(1);
480
+ if (buf.length < 5 + length)
481
+ break;
482
+ const payload = buf.subarray(5, 5 + length);
483
+ buf = buf.subarray(5 + length);
484
+ winPipeBuffers.set(pipeKey, buf);
485
+ if (msgType === 0x01 && ws.readyState === WS_OPEN) {
486
+ ws.send(JSON.stringify({
487
+ ch: "terminal",
488
+ id,
489
+ type: "data",
490
+ data: payload.toString("utf-8"),
491
+ ...echo,
492
+ }));
493
+ }
494
+ if (msgType === 0x07) {
495
+ try {
496
+ const status = JSON.parse(payload.toString("utf-8"));
497
+ if (!status.alive && ws.readyState === WS_OPEN) {
498
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
499
+ }
500
+ }
501
+ catch {
502
+ /* ignore parse errors */
503
+ }
504
+ }
505
+ }
506
+ });
507
+ pipeSocket.on("close", () => {
508
+ winPipes.delete(pipeKey);
509
+ winPipeBuffers.delete(pipeKey);
510
+ if (ws.readyState === WS_OPEN) {
511
+ ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
512
+ }
513
+ });
514
+ });
515
+ }
516
+ }
517
+ else if (type === "data" && msg.data !== undefined) {
518
+ const pipeSocket = winPipes.get(pipeKey);
519
+ if (pipeSocket) {
520
+ const inputBuf = Buffer.from(msg.data, "utf-8");
521
+ const header = Buffer.alloc(5);
522
+ header.writeUInt8(0x02, 0);
523
+ header.writeUInt32BE(inputBuf.length, 1);
524
+ pipeSocket.write(Buffer.concat([header, inputBuf]));
525
+ }
526
+ }
527
+ else if (type === "resize" && msg.cols !== undefined && msg.rows !== undefined) {
528
+ const pipeSocket = winPipes.get(pipeKey);
529
+ if (pipeSocket) {
530
+ const resizePayload = Buffer.from(JSON.stringify({ cols: msg.cols, rows: msg.rows }));
531
+ const header = Buffer.alloc(5);
532
+ header.writeUInt8(0x03, 0);
533
+ header.writeUInt32BE(resizePayload.length, 1);
534
+ pipeSocket.write(Buffer.concat([header, resizePayload]));
535
+ }
536
+ }
537
+ else if (type === "close") {
538
+ const pipeSocket = winPipes.get(pipeKey);
539
+ if (pipeSocket) {
540
+ pipeSocket.end();
541
+ winPipes.delete(pipeKey);
542
+ winPipeBuffers.delete(pipeKey);
543
+ }
544
+ }
545
+ }
338
546
  /**
339
547
  * Create a mux WebSocket server (noServer mode).
340
548
  * Returns the WebSocketServer instance for manual upgrade routing.
341
549
  */
342
550
  export function createMuxWebSocket(tmuxPath) {
343
- if (!ptySpawn) {
551
+ // On Windows, we use named pipe relay instead of node-pty/tmux.
552
+ // Allow the server to be created without ptySpawn on Windows.
553
+ if (!ptySpawn && !isWindows()) {
344
554
  console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled");
345
555
  return null;
346
556
  }
347
- const terminalManager = new TerminalManager(tmuxPath);
557
+ // On Windows, terminal I/O goes through named pipe relay — no TerminalManager needed.
558
+ const terminalManager = ptySpawn && !isWindows() ? new TerminalManager(tmuxPath ?? undefined) : null;
348
559
  const nextPort = process.env.PORT || "3000";
349
560
  const broadcaster = new SessionBroadcaster(nextPort);
350
561
  const wss = new WebSocketServer({ noServer: true });
351
562
  wss.on("connection", (ws) => {
352
563
  console.log("[MuxServer] New mux connection");
353
564
  const subscriptions = new Map();
565
+ // Windows: named pipe sockets keyed by session ID
566
+ const winPipes = new Map();
567
+ // Windows: framing buffers keyed by session ID
568
+ const winPipeBuffers = new Map();
354
569
  let sessionUnsubscribe = null;
355
570
  let missedPongs = 0;
356
571
  const MAX_MISSED_PONGS = 3;
@@ -390,71 +605,93 @@ export function createMuxWebSocket(tmuxPath) {
390
605
  const subscriptionKey = projectId ? `${projectId}:${id}` : id;
391
606
  try {
392
607
  if (type === "open") {
393
- // Validate session exists
394
- terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
395
- // Send opened confirmation (idempotent — safe to send on re-open)
396
- const openedMsg = {
397
- ch: "terminal",
398
- id,
399
- type: "opened",
400
- ...(projectId && { projectId }),
401
- };
402
- ws.send(JSON.stringify(openedMsg));
403
- // Subscribe and send history buffer only for new subscribers.
404
- // Skipping the buffer on re-open prevents duplicate output when
405
- // MuxProvider re-sends open for all terminals on reconnect.
406
- if (!subscriptions.has(subscriptionKey)) {
407
- // Send buffered history to catch up the new subscriber
408
- const buffer = terminalManager.getBuffer(id, projectId);
409
- if (buffer) {
410
- const bufferMsg = {
411
- ch: "terminal",
412
- id,
413
- type: "data",
414
- data: buffer,
415
- ...(projectId && { projectId }),
416
- };
417
- ws.send(JSON.stringify(bufferMsg));
418
- }
419
- const unsub = terminalManager.subscribe(id, projectId, (data) => {
420
- const dataMsg = {
421
- ch: "terminal",
422
- id,
423
- type: "data",
424
- data,
425
- ...(projectId && { projectId }),
426
- };
427
- if (ws.readyState === WebSocket.OPEN) {
428
- ws.send(JSON.stringify(dataMsg));
429
- }
430
- }, (exitCode) => {
431
- const exitedMsg = {
432
- ch: "terminal",
433
- id,
434
- type: "exited",
435
- code: exitCode,
436
- ...(projectId && { projectId }),
437
- };
438
- if (ws.readyState === WebSocket.OPEN) {
439
- ws.send(JSON.stringify(exitedMsg));
608
+ if (isWindows()) {
609
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
610
+ }
611
+ else {
612
+ // --- Unix: tmux path with project scoping ---
613
+ if (!terminalManager)
614
+ throw new Error("Terminal manager not available");
615
+ terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
616
+ // Send opened confirmation (idempotent — safe to send on re-open)
617
+ const openedMsg = {
618
+ ch: "terminal",
619
+ id,
620
+ type: "opened",
621
+ ...(projectId && { projectId }),
622
+ };
623
+ ws.send(JSON.stringify(openedMsg));
624
+ // Subscribe and send history buffer only for new subscribers.
625
+ // Skipping the buffer on re-open prevents duplicate output when
626
+ // MuxProvider re-sends open for all terminals on reconnect.
627
+ if (!subscriptions.has(subscriptionKey)) {
628
+ // Send buffered history to catch up the new subscriber
629
+ const buffer = terminalManager.getBuffer(id, projectId);
630
+ if (buffer) {
631
+ const bufferMsg = {
632
+ ch: "terminal",
633
+ id,
634
+ type: "data",
635
+ data: buffer,
636
+ ...(projectId && { projectId }),
637
+ };
638
+ ws.send(JSON.stringify(bufferMsg));
440
639
  }
441
- });
442
- subscriptions.set(subscriptionKey, unsub);
640
+ const unsub = terminalManager.subscribe(id, projectId, (data) => {
641
+ const dataMsg = {
642
+ ch: "terminal",
643
+ id,
644
+ type: "data",
645
+ data,
646
+ ...(projectId && { projectId }),
647
+ };
648
+ if (ws.readyState === WebSocket.OPEN) {
649
+ ws.send(JSON.stringify(dataMsg));
650
+ }
651
+ }, (exitCode) => {
652
+ const exitedMsg = {
653
+ ch: "terminal",
654
+ id,
655
+ type: "exited",
656
+ code: exitCode,
657
+ ...(projectId && { projectId }),
658
+ };
659
+ if (ws.readyState === WebSocket.OPEN) {
660
+ ws.send(JSON.stringify(exitedMsg));
661
+ }
662
+ });
663
+ subscriptions.set(subscriptionKey, unsub);
664
+ }
443
665
  }
444
666
  }
445
667
  else if (type === "data" && "data" in msg) {
446
- terminalManager.write(id, msg.data, projectId);
668
+ if (isWindows()) {
669
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
670
+ }
671
+ else {
672
+ terminalManager?.write(id, msg.data, projectId);
673
+ }
447
674
  }
448
675
  else if (type === "resize" && "cols" in msg && "rows" in msg) {
449
- terminalManager.resize(id, msg.cols, msg.rows, projectId);
676
+ if (isWindows()) {
677
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
678
+ }
679
+ else {
680
+ terminalManager?.resize(id, msg.cols, msg.rows, projectId);
681
+ }
450
682
  }
451
683
  else if (type === "close") {
452
- // Unsubscribe this client only — TerminalManager is shared across
453
- // all mux connections so we must not kill the PTY here.
454
- const unsub = subscriptions.get(subscriptionKey);
455
- if (unsub) {
456
- unsub();
457
- subscriptions.delete(subscriptionKey);
684
+ if (isWindows()) {
685
+ handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
686
+ }
687
+ else {
688
+ // Unsubscribe this client only — TerminalManager is shared across
689
+ // all mux connections so we must not kill the PTY here.
690
+ const unsub = subscriptions.get(subscriptionKey);
691
+ if (unsub) {
692
+ unsub();
693
+ subscriptions.delete(subscriptionKey);
694
+ }
458
695
  }
459
696
  }
460
697
  }
@@ -515,6 +752,12 @@ export function createMuxWebSocket(tmuxPath) {
515
752
  unsub();
516
753
  }
517
754
  subscriptions.clear();
755
+ // Windows: close all open pipe sockets
756
+ for (const pipeSocket of winPipes.values()) {
757
+ pipeSocket.destroy();
758
+ }
759
+ winPipes.clear();
760
+ winPipeBuffers.clear();
518
761
  });
519
762
  // In the ws library, "error" is always followed by "close", so the close
520
763
  // handler below handles all cleanup. Log the error here and nothing more.
@@ -8,6 +8,7 @@ import { resolve, dirname } from "node:path";
8
8
  import { existsSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
+ import { killProcessTree } from "@aoagents/ao-core";
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
13
14
  // Resolve paths relative to the package root (one level up from dist-server/)
@@ -25,6 +26,7 @@ function spawnProcess(label, command, args, opts) {
25
26
  cwd: pkgRoot,
26
27
  stdio: ["ignore", "pipe", "pipe"],
27
28
  env: process.env,
29
+ detached: process.platform !== "win32",
28
30
  });
29
31
  child.stdout?.on("data", (data) => {
30
32
  for (const line of data.toString().split("\n").filter(Boolean)) {
@@ -60,10 +62,14 @@ function spawnProcess(label, command, args, opts) {
60
62
  * Tries the local .bin shim first (fast), then falls back to require.resolve (hoisted deps).
61
63
  */
62
64
  function resolveNextBin() {
63
- const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
64
- if (existsSync(localBin))
65
- return localBin;
66
- // Hoisted node_modules resolve the actual next CLI entry
65
+ // On Windows, .bin/next is a POSIX shell shim that spawn() cannot execute.
66
+ // Skip it and go straight to the JS entry point.
67
+ if (process.platform !== "win32") {
68
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
69
+ if (existsSync(localBin))
70
+ return localBin;
71
+ }
72
+ // Resolve the actual Next.js CLI JS entry point
67
73
  const require = createRequire(resolve(pkgRoot, "package.json"));
68
74
  try {
69
75
  const nextPkg = require.resolve("next/package.json");
@@ -76,7 +82,15 @@ function resolveNextBin() {
76
82
  }
77
83
  // Start Next.js production server
78
84
  const port = process.env["PORT"] || "3000";
79
- spawnProcess("next", resolveNextBin(), ["start", "-p", port]);
85
+ const nextBin = resolveNextBin();
86
+ if (process.platform === "win32" && nextBin !== "next") {
87
+ // On Windows, run the JS entry point via the current node binary.
88
+ // spawn() can't execute .js files directly on Windows.
89
+ spawnProcess("next", process.execPath, [nextBin, "start", "-p", port]);
90
+ }
91
+ else {
92
+ spawnProcess("next", nextBin, ["start", "-p", port]);
93
+ }
80
94
  // Start direct terminal WebSocket server (auto-restart on crash)
81
95
  spawnProcess("direct-terminal", "node", [resolve(__dirname, "direct-terminal-ws.js")], { restart: true });
82
96
  // Graceful shutdown — send SIGTERM to children and wait for them to exit
@@ -104,7 +118,15 @@ function cleanup() {
104
118
  process.exit(0);
105
119
  }
106
120
  });
107
- child.kill("SIGTERM");
121
+ const pid = child.pid;
122
+ if (pid) {
123
+ void killProcessTree(pid, "SIGTERM").catch(() => {
124
+ child.kill("SIGTERM");
125
+ });
126
+ }
127
+ else {
128
+ child.kill("SIGTERM");
129
+ }
108
130
  }
109
131
  }
110
132
  process.on("SIGINT", cleanup);