skybridge 0.0.0-dev.fd77b0f → 0.0.0-dev.fd947a0

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 (445) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/run.js +5 -0
  4. package/dist/cli/detect-port.d.ts +18 -0
  5. package/dist/cli/detect-port.js +61 -0
  6. package/dist/cli/detect-port.js.map +1 -0
  7. package/dist/cli/header.d.ts +4 -0
  8. package/dist/cli/header.js +6 -0
  9. package/dist/cli/header.js.map +1 -0
  10. package/dist/cli/run-command.d.ts +2 -0
  11. package/dist/cli/run-command.js +43 -0
  12. package/dist/cli/run-command.js.map +1 -0
  13. package/dist/cli/telemetry.d.ts +7 -0
  14. package/dist/cli/telemetry.js +123 -0
  15. package/dist/cli/telemetry.js.map +1 -0
  16. package/dist/cli/tunnel-control-server.d.ts +9 -0
  17. package/dist/cli/tunnel-control-server.js +31 -0
  18. package/dist/cli/tunnel-control-server.js.map +1 -0
  19. package/dist/cli/tunnel-control-server.test.js +39 -0
  20. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  21. package/dist/cli/tunnel-handler.d.ts +3 -0
  22. package/dist/cli/tunnel-handler.js +48 -0
  23. package/dist/cli/tunnel-handler.js.map +1 -0
  24. package/dist/cli/tunnel-handler.test.js +105 -0
  25. package/dist/cli/tunnel-handler.test.js.map +1 -0
  26. package/dist/cli/tunnel.d.ts +57 -0
  27. package/dist/cli/tunnel.js +154 -0
  28. package/dist/cli/tunnel.js.map +1 -0
  29. package/dist/cli/tunnel.test.js +190 -0
  30. package/dist/cli/tunnel.test.js.map +1 -0
  31. package/dist/cli/types.d.ts +5 -0
  32. package/dist/cli/types.js.map +1 -0
  33. package/dist/cli/use-execute-steps.d.ts +11 -0
  34. package/dist/cli/use-execute-steps.js +36 -0
  35. package/dist/cli/use-execute-steps.js.map +1 -0
  36. package/dist/cli/use-messages.d.ts +3 -0
  37. package/dist/cli/use-messages.js +11 -0
  38. package/dist/cli/use-messages.js.map +1 -0
  39. package/dist/cli/use-nodemon.d.ts +2 -0
  40. package/dist/cli/use-nodemon.js +73 -0
  41. package/dist/cli/use-nodemon.js.map +1 -0
  42. package/dist/cli/use-open-browser.d.ts +1 -0
  43. package/dist/cli/use-open-browser.js +44 -0
  44. package/dist/cli/use-open-browser.js.map +1 -0
  45. package/dist/cli/use-tunnel.d.ts +14 -0
  46. package/dist/cli/use-tunnel.js +131 -0
  47. package/dist/cli/use-tunnel.js.map +1 -0
  48. package/dist/cli/use-typescript-check.d.ts +9 -0
  49. package/dist/cli/use-typescript-check.js +94 -0
  50. package/dist/cli/use-typescript-check.js.map +1 -0
  51. package/dist/commands/build.d.ts +9 -0
  52. package/dist/commands/build.js +102 -0
  53. package/dist/commands/build.js.map +1 -0
  54. package/dist/commands/dev.d.ts +12 -0
  55. package/dist/commands/dev.js +80 -0
  56. package/dist/commands/dev.js.map +1 -0
  57. package/dist/commands/start.d.ts +9 -0
  58. package/dist/commands/start.js +49 -0
  59. package/dist/commands/start.js.map +1 -0
  60. package/dist/commands/telemetry/disable.d.ts +5 -0
  61. package/dist/commands/telemetry/disable.js +14 -0
  62. package/dist/commands/telemetry/disable.js.map +1 -0
  63. package/dist/commands/telemetry/enable.d.ts +5 -0
  64. package/dist/commands/telemetry/enable.js +14 -0
  65. package/dist/commands/telemetry/enable.js.map +1 -0
  66. package/dist/commands/telemetry/status.d.ts +5 -0
  67. package/dist/commands/telemetry/status.js +14 -0
  68. package/dist/commands/telemetry/status.js.map +1 -0
  69. package/dist/server/asset-base-url-transform-plugin.d.ts +10 -0
  70. package/dist/server/asset-base-url-transform-plugin.js +33 -0
  71. package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
  72. package/dist/server/asset-base-url-transform-plugin.test.js +84 -0
  73. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
  74. package/dist/server/content-helpers.d.ts +27 -0
  75. package/dist/server/content-helpers.js +46 -0
  76. package/dist/server/content-helpers.js.map +1 -0
  77. package/dist/server/content-helpers.test.js +70 -0
  78. package/dist/server/content-helpers.test.js.map +1 -0
  79. package/dist/server/express.d.ts +11 -0
  80. package/dist/server/express.js +101 -0
  81. package/dist/server/express.js.map +1 -0
  82. package/dist/server/express.test.js +430 -0
  83. package/dist/server/express.test.js.map +1 -0
  84. package/dist/server/index.d.ts +6 -0
  85. package/dist/server/index.js +4 -0
  86. package/dist/server/index.js.map +1 -0
  87. package/dist/{src/server → server}/inferUtilityTypes.d.ts +6 -6
  88. package/dist/server/inferUtilityTypes.js.map +1 -0
  89. package/dist/server/metric.d.ts +14 -0
  90. package/dist/server/metric.js +62 -0
  91. package/dist/server/metric.js.map +1 -0
  92. package/dist/server/middleware.d.ts +124 -0
  93. package/dist/server/middleware.js +93 -0
  94. package/dist/server/middleware.js.map +1 -0
  95. package/dist/server/middleware.test-d.js +75 -0
  96. package/dist/server/middleware.test-d.js.map +1 -0
  97. package/dist/server/middleware.test.js +493 -0
  98. package/dist/server/middleware.test.js.map +1 -0
  99. package/dist/server/server.d.ts +196 -0
  100. package/dist/server/server.js +468 -0
  101. package/dist/server/server.js.map +1 -0
  102. package/dist/{src/server → server}/templateHelper.d.ts +5 -7
  103. package/dist/server/templateHelper.js +11 -0
  104. package/dist/server/templateHelper.js.map +1 -0
  105. package/dist/server/templates.generated.d.ts +4 -0
  106. package/dist/server/templates.generated.js +47 -0
  107. package/dist/server/templates.generated.js.map +1 -0
  108. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  109. package/dist/server/tunnel-proxy-router.js +110 -0
  110. package/dist/server/tunnel-proxy-router.js.map +1 -0
  111. package/dist/server/tunnel-proxy-router.test.js +229 -0
  112. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  113. package/dist/server/viewsDevServer.d.ts +14 -0
  114. package/dist/server/viewsDevServer.js +45 -0
  115. package/dist/server/viewsDevServer.js.map +1 -0
  116. package/dist/{src/test → test}/utils.d.ts +13 -21
  117. package/dist/{src/test → test}/utils.js +42 -37
  118. package/dist/test/utils.js.map +1 -0
  119. package/dist/test/view.test.js +523 -0
  120. package/dist/test/view.test.js.map +1 -0
  121. package/dist/version.d.ts +1 -0
  122. package/dist/version.js +3 -0
  123. package/dist/version.js.map +1 -0
  124. package/dist/web/bridges/apps-sdk/adaptor.d.ts +24 -0
  125. package/dist/web/bridges/apps-sdk/adaptor.js +96 -0
  126. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -0
  127. package/dist/web/bridges/apps-sdk/bridge.d.ts +10 -0
  128. package/dist/{src/web/bridges/apps-sdk-bridge.js → web/bridges/apps-sdk/bridge.js} +2 -2
  129. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -0
  130. package/dist/web/bridges/apps-sdk/index.d.ts +5 -0
  131. package/dist/web/bridges/apps-sdk/index.js +5 -0
  132. package/dist/web/bridges/apps-sdk/index.js.map +1 -0
  133. package/dist/web/bridges/apps-sdk/types.d.ts +131 -0
  134. package/dist/{src/web → web/bridges/apps-sdk}/types.js +0 -1
  135. package/dist/web/bridges/apps-sdk/types.js.map +1 -0
  136. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +2 -0
  137. package/dist/{src/web/bridges/hooks/use-apps-sdk-bridge.js → web/bridges/apps-sdk/use-apps-sdk-context.js} +3 -3
  138. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -0
  139. package/dist/web/bridges/get-adaptor.d.ts +2 -0
  140. package/dist/web/bridges/get-adaptor.js +8 -0
  141. package/dist/web/bridges/get-adaptor.js.map +1 -0
  142. package/dist/web/bridges/index.d.ts +5 -0
  143. package/dist/web/bridges/index.js +6 -0
  144. package/dist/web/bridges/index.js.map +1 -0
  145. package/dist/web/bridges/mcp-app/adaptor.d.ts +48 -0
  146. package/dist/web/bridges/mcp-app/adaptor.js +263 -0
  147. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -0
  148. package/dist/web/bridges/mcp-app/bridge.d.ts +26 -0
  149. package/dist/web/bridges/mcp-app/bridge.js +102 -0
  150. package/dist/web/bridges/mcp-app/bridge.js.map +1 -0
  151. package/dist/web/bridges/mcp-app/index.d.ts +4 -0
  152. package/dist/web/bridges/mcp-app/index.js +4 -0
  153. package/dist/web/bridges/mcp-app/index.js.map +1 -0
  154. package/dist/web/bridges/mcp-app/types.d.ts +8 -0
  155. package/dist/web/bridges/mcp-app/types.js +2 -0
  156. package/dist/web/bridges/mcp-app/types.js.map +1 -0
  157. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +7 -0
  158. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +7 -0
  159. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -0
  160. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +26 -0
  161. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -0
  162. package/dist/web/bridges/types.d.ts +111 -0
  163. package/dist/web/bridges/types.js +2 -0
  164. package/dist/web/bridges/types.js.map +1 -0
  165. package/dist/web/bridges/use-host-context.d.ts +2 -0
  166. package/dist/web/bridges/use-host-context.js +8 -0
  167. package/dist/web/bridges/use-host-context.js.map +1 -0
  168. package/dist/web/components/modal-provider.d.ts +4 -0
  169. package/dist/web/components/modal-provider.js +45 -0
  170. package/dist/web/components/modal-provider.js.map +1 -0
  171. package/dist/web/create-store.js +38 -0
  172. package/dist/web/create-store.js.map +1 -0
  173. package/dist/web/create-store.test.js +129 -0
  174. package/dist/web/create-store.test.js.map +1 -0
  175. package/dist/{src/web → web}/data-llm.d.ts +1 -1
  176. package/dist/{src/web → web}/data-llm.js +7 -5
  177. package/dist/web/data-llm.js.map +1 -0
  178. package/dist/web/data-llm.test.js +142 -0
  179. package/dist/web/data-llm.test.js.map +1 -0
  180. package/dist/{src/web → web}/generate-helpers.d.ts +22 -19
  181. package/dist/{src/web → web}/generate-helpers.js +20 -18
  182. package/dist/web/generate-helpers.js.map +1 -0
  183. package/dist/{src/web → web}/generate-helpers.test-d.js +26 -26
  184. package/dist/web/generate-helpers.test-d.js.map +1 -0
  185. package/dist/web/generate-helpers.test.js.map +1 -0
  186. package/dist/{src/web → web}/helpers/state.d.ts +2 -2
  187. package/dist/web/helpers/state.js +45 -0
  188. package/dist/web/helpers/state.js.map +1 -0
  189. package/dist/{src/web → web}/helpers/state.test.js +9 -9
  190. package/dist/web/helpers/state.test.js.map +1 -0
  191. package/dist/{src/web → web}/hooks/index.d.ts +3 -3
  192. package/dist/{src/web → web}/hooks/index.js +2 -2
  193. package/dist/web/hooks/index.js.map +1 -0
  194. package/dist/{src/web → web}/hooks/test/utils.d.ts +8 -2
  195. package/dist/web/hooks/test/utils.js +64 -0
  196. package/dist/web/hooks/test/utils.js.map +1 -0
  197. package/dist/{src/web → web}/hooks/use-call-tool.d.ts +2 -1
  198. package/dist/{src/web → web}/hooks/use-call-tool.js +2 -2
  199. package/dist/web/hooks/use-call-tool.js.map +1 -0
  200. package/dist/web/hooks/use-call-tool.test-d.js.map +1 -0
  201. package/dist/{src/web → web}/hooks/use-call-tool.test.js +0 -5
  202. package/dist/web/hooks/use-call-tool.test.js.map +1 -0
  203. package/dist/web/hooks/use-display-mode.d.ts +4 -0
  204. package/dist/web/hooks/use-display-mode.js +9 -0
  205. package/dist/web/hooks/use-display-mode.js.map +1 -0
  206. package/dist/web/hooks/use-display-mode.test-d.js +8 -0
  207. package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
  208. package/dist/web/hooks/use-display-mode.test.js.map +1 -0
  209. package/dist/web/hooks/use-files.d.ts +7 -0
  210. package/dist/web/hooks/use-files.js +10 -0
  211. package/dist/web/hooks/use-files.js.map +1 -0
  212. package/dist/web/hooks/use-files.test.d.ts +1 -0
  213. package/dist/web/hooks/use-files.test.js +54 -0
  214. package/dist/web/hooks/use-files.test.js.map +1 -0
  215. package/dist/{src/web → web}/hooks/use-layout.d.ts +2 -2
  216. package/dist/{src/web → web}/hooks/use-layout.js +4 -4
  217. package/dist/web/hooks/use-layout.js.map +1 -0
  218. package/dist/web/hooks/use-layout.test.d.ts +1 -0
  219. package/dist/{src/web → web}/hooks/use-layout.test.js +7 -6
  220. package/dist/web/hooks/use-layout.test.js.map +1 -0
  221. package/dist/web/hooks/use-open-external.d.ts +3 -0
  222. package/dist/web/hooks/use-open-external.js +8 -0
  223. package/dist/web/hooks/use-open-external.js.map +1 -0
  224. package/dist/web/hooks/use-open-external.test.d.ts +1 -0
  225. package/dist/{src/web → web}/hooks/use-open-external.test.js +27 -12
  226. package/dist/web/hooks/use-open-external.test.js.map +1 -0
  227. package/dist/web/hooks/use-request-modal.d.ts +9 -0
  228. package/dist/web/hooks/use-request-modal.js +16 -0
  229. package/dist/web/hooks/use-request-modal.js.map +1 -0
  230. package/dist/web/hooks/use-request-modal.test.d.ts +1 -0
  231. package/dist/{src/web → web}/hooks/use-request-modal.test.js +5 -1
  232. package/dist/web/hooks/use-request-modal.test.js.map +1 -0
  233. package/dist/web/hooks/use-send-follow-up-message.d.ts +2 -0
  234. package/dist/web/hooks/use-send-follow-up-message.js +8 -0
  235. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -0
  236. package/dist/web/hooks/use-set-open-in-app-url.d.ts +1 -0
  237. package/dist/web/hooks/use-set-open-in-app-url.js +8 -0
  238. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -0
  239. package/dist/web/hooks/use-set-open-in-app-url.test.d.ts +1 -0
  240. package/dist/web/hooks/use-set-open-in-app-url.test.js +43 -0
  241. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -0
  242. package/dist/{src/web → web}/hooks/use-tool-info.js +4 -4
  243. package/dist/web/hooks/use-tool-info.js.map +1 -0
  244. package/dist/web/hooks/use-tool-info.test-d.d.ts +1 -0
  245. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -0
  246. package/dist/web/hooks/use-tool-info.test.d.ts +1 -0
  247. package/dist/{src/web → web}/hooks/use-tool-info.test.js +5 -5
  248. package/dist/web/hooks/use-tool-info.test.js.map +1 -0
  249. package/dist/{src/web → web}/hooks/use-user.d.ts +1 -1
  250. package/dist/web/hooks/use-user.js +35 -0
  251. package/dist/web/hooks/use-user.js.map +1 -0
  252. package/dist/web/hooks/use-user.test.d.ts +1 -0
  253. package/dist/{src/web → web}/hooks/use-user.test.js +33 -4
  254. package/dist/web/hooks/use-user.test.js.map +1 -0
  255. package/dist/web/hooks/use-view-state.d.ts +4 -0
  256. package/dist/web/hooks/use-view-state.js +32 -0
  257. package/dist/web/hooks/use-view-state.js.map +1 -0
  258. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  259. package/dist/web/hooks/use-view-state.test.js +177 -0
  260. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  261. package/dist/{src/web → web}/index.d.ts +1 -2
  262. package/dist/{src/web → web}/index.js +1 -2
  263. package/dist/web/index.js.map +1 -0
  264. package/dist/web/mount-view.d.ts +1 -0
  265. package/dist/{src/web/mount-widget.js → web/mount-view.js} +11 -3
  266. package/dist/web/mount-view.js.map +1 -0
  267. package/dist/web/plugin/data-llm.test.d.ts +1 -0
  268. package/dist/web/plugin/data-llm.test.js.map +1 -0
  269. package/dist/web/plugin/plugin.d.ts +5 -0
  270. package/dist/web/plugin/plugin.js +156 -0
  271. package/dist/web/plugin/plugin.js.map +1 -0
  272. package/dist/web/plugin/scan-views.d.ts +16 -0
  273. package/dist/web/plugin/scan-views.js +88 -0
  274. package/dist/web/plugin/scan-views.js.map +1 -0
  275. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  276. package/dist/web/plugin/scan-views.test.js +99 -0
  277. package/dist/web/plugin/scan-views.test.js.map +1 -0
  278. package/dist/{src/web → web}/plugin/transform-data-llm.js +1 -1
  279. package/dist/web/plugin/transform-data-llm.js.map +1 -0
  280. package/dist/web/plugin/transform-data-llm.test.d.ts +1 -0
  281. package/dist/web/plugin/transform-data-llm.test.js.map +1 -0
  282. package/dist/web/plugin/validate-view.d.ts +1 -0
  283. package/dist/web/plugin/validate-view.js +9 -0
  284. package/dist/web/plugin/validate-view.js.map +1 -0
  285. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  286. package/dist/web/plugin/validate-view.test.js +24 -0
  287. package/dist/web/plugin/validate-view.test.js.map +1 -0
  288. package/dist/{src/web → web}/proxy.js +0 -1
  289. package/dist/web/proxy.js.map +1 -0
  290. package/dist/web/types.d.ts +16 -0
  291. package/dist/web/types.js +2 -0
  292. package/dist/web/types.js.map +1 -0
  293. package/package.json +74 -39
  294. package/tsconfig.base.json +33 -0
  295. package/dist/src/server/devtoolsStaticServer.d.ts +0 -15
  296. package/dist/src/server/devtoolsStaticServer.js +0 -38
  297. package/dist/src/server/devtoolsStaticServer.js.map +0 -1
  298. package/dist/src/server/index.d.ts +0 -5
  299. package/dist/src/server/index.js +0 -4
  300. package/dist/src/server/index.js.map +0 -1
  301. package/dist/src/server/inferUtilityTypes.js.map +0 -1
  302. package/dist/src/server/server.d.ts +0 -74
  303. package/dist/src/server/server.js +0 -82
  304. package/dist/src/server/server.js.map +0 -1
  305. package/dist/src/server/templateHelper.js +0 -30
  306. package/dist/src/server/templateHelper.js.map +0 -1
  307. package/dist/src/server/templates/development.hbs +0 -13
  308. package/dist/src/server/templates/production.hbs +0 -7
  309. package/dist/src/server/widgetsDevServer.d.ts +0 -12
  310. package/dist/src/server/widgetsDevServer.js +0 -38
  311. package/dist/src/server/widgetsDevServer.js.map +0 -1
  312. package/dist/src/test/utils.js.map +0 -1
  313. package/dist/src/test/widget.test.js +0 -146
  314. package/dist/src/test/widget.test.js.map +0 -1
  315. package/dist/src/web/bridges/adaptors/apps-sdk-adaptor.d.ts +0 -13
  316. package/dist/src/web/bridges/adaptors/apps-sdk-adaptor.js +0 -33
  317. package/dist/src/web/bridges/adaptors/apps-sdk-adaptor.js.map +0 -1
  318. package/dist/src/web/bridges/adaptors/mcp-app-adaptor.d.ts +0 -16
  319. package/dist/src/web/bridges/adaptors/mcp-app-adaptor.js +0 -115
  320. package/dist/src/web/bridges/adaptors/mcp-app-adaptor.js.map +0 -1
  321. package/dist/src/web/bridges/apps-sdk-bridge.d.ts +0 -10
  322. package/dist/src/web/bridges/apps-sdk-bridge.js.map +0 -1
  323. package/dist/src/web/bridges/hooks/use-adaptor.d.ts +0 -2
  324. package/dist/src/web/bridges/hooks/use-adaptor.js +0 -8
  325. package/dist/src/web/bridges/hooks/use-adaptor.js.map +0 -1
  326. package/dist/src/web/bridges/hooks/use-apps-sdk-bridge.d.ts +0 -2
  327. package/dist/src/web/bridges/hooks/use-apps-sdk-bridge.js.map +0 -1
  328. package/dist/src/web/bridges/hooks/use-bridge.d.ts +0 -2
  329. package/dist/src/web/bridges/hooks/use-bridge.js +0 -8
  330. package/dist/src/web/bridges/hooks/use-bridge.js.map +0 -1
  331. package/dist/src/web/bridges/hooks/use-mcp-app-bridge.d.ts +0 -5
  332. package/dist/src/web/bridges/hooks/use-mcp-app-bridge.js +0 -7
  333. package/dist/src/web/bridges/hooks/use-mcp-app-bridge.js.map +0 -1
  334. package/dist/src/web/bridges/hooks/use-mcp-app-bridge.test.js +0 -41
  335. package/dist/src/web/bridges/hooks/use-mcp-app-bridge.test.js.map +0 -1
  336. package/dist/src/web/bridges/index.d.ts +0 -4
  337. package/dist/src/web/bridges/index.js +0 -5
  338. package/dist/src/web/bridges/index.js.map +0 -1
  339. package/dist/src/web/bridges/mcp-app-bridge.d.ts +0 -38
  340. package/dist/src/web/bridges/mcp-app-bridge.js +0 -162
  341. package/dist/src/web/bridges/mcp-app-bridge.js.map +0 -1
  342. package/dist/src/web/bridges/types.d.ts +0 -57
  343. package/dist/src/web/bridges/types.js.map +0 -1
  344. package/dist/src/web/create-store.js +0 -25
  345. package/dist/src/web/create-store.js.map +0 -1
  346. package/dist/src/web/create-store.test.js +0 -70
  347. package/dist/src/web/create-store.test.js.map +0 -1
  348. package/dist/src/web/data-llm.js.map +0 -1
  349. package/dist/src/web/data-llm.test.js +0 -76
  350. package/dist/src/web/data-llm.test.js.map +0 -1
  351. package/dist/src/web/generate-helpers.js.map +0 -1
  352. package/dist/src/web/generate-helpers.test-d.js.map +0 -1
  353. package/dist/src/web/generate-helpers.test.js.map +0 -1
  354. package/dist/src/web/helpers/state.js +0 -40
  355. package/dist/src/web/helpers/state.js.map +0 -1
  356. package/dist/src/web/helpers/state.test.js.map +0 -1
  357. package/dist/src/web/hooks/index.js.map +0 -1
  358. package/dist/src/web/hooks/test/utils.js +0 -40
  359. package/dist/src/web/hooks/test/utils.js.map +0 -1
  360. package/dist/src/web/hooks/use-call-tool.js.map +0 -1
  361. package/dist/src/web/hooks/use-call-tool.test-d.js.map +0 -1
  362. package/dist/src/web/hooks/use-call-tool.test.js.map +0 -1
  363. package/dist/src/web/hooks/use-display-mode.d.ts +0 -4
  364. package/dist/src/web/hooks/use-display-mode.js +0 -10
  365. package/dist/src/web/hooks/use-display-mode.js.map +0 -1
  366. package/dist/src/web/hooks/use-display-mode.test.js.map +0 -1
  367. package/dist/src/web/hooks/use-files.d.ts +0 -10
  368. package/dist/src/web/hooks/use-files.js +0 -7
  369. package/dist/src/web/hooks/use-files.js.map +0 -1
  370. package/dist/src/web/hooks/use-files.test.js +0 -29
  371. package/dist/src/web/hooks/use-files.test.js.map +0 -1
  372. package/dist/src/web/hooks/use-layout.js.map +0 -1
  373. package/dist/src/web/hooks/use-layout.test.js.map +0 -1
  374. package/dist/src/web/hooks/use-open-external.d.ts +0 -1
  375. package/dist/src/web/hooks/use-open-external.js +0 -8
  376. package/dist/src/web/hooks/use-open-external.js.map +0 -1
  377. package/dist/src/web/hooks/use-open-external.test.js.map +0 -1
  378. package/dist/src/web/hooks/use-openai-global.d.ts +0 -3
  379. package/dist/src/web/hooks/use-openai-global.js +0 -6
  380. package/dist/src/web/hooks/use-openai-global.js.map +0 -1
  381. package/dist/src/web/hooks/use-request-modal.d.ts +0 -9
  382. package/dist/src/web/hooks/use-request-modal.js +0 -14
  383. package/dist/src/web/hooks/use-request-modal.js.map +0 -1
  384. package/dist/src/web/hooks/use-request-modal.test.js.map +0 -1
  385. package/dist/src/web/hooks/use-send-follow-up-message.d.ts +0 -1
  386. package/dist/src/web/hooks/use-send-follow-up-message.js +0 -8
  387. package/dist/src/web/hooks/use-send-follow-up-message.js.map +0 -1
  388. package/dist/src/web/hooks/use-tool-info.js.map +0 -1
  389. package/dist/src/web/hooks/use-tool-info.test-d.js.map +0 -1
  390. package/dist/src/web/hooks/use-tool-info.test.js.map +0 -1
  391. package/dist/src/web/hooks/use-user.js +0 -19
  392. package/dist/src/web/hooks/use-user.js.map +0 -1
  393. package/dist/src/web/hooks/use-user.test.js.map +0 -1
  394. package/dist/src/web/hooks/use-widget-state.d.ts +0 -4
  395. package/dist/src/web/hooks/use-widget-state.js +0 -32
  396. package/dist/src/web/hooks/use-widget-state.js.map +0 -1
  397. package/dist/src/web/hooks/use-widget-state.test.js +0 -61
  398. package/dist/src/web/hooks/use-widget-state.test.js.map +0 -1
  399. package/dist/src/web/index.js.map +0 -1
  400. package/dist/src/web/mount-widget.d.ts +0 -1
  401. package/dist/src/web/mount-widget.js.map +0 -1
  402. package/dist/src/web/plugin/data-llm.test.js.map +0 -1
  403. package/dist/src/web/plugin/plugin.d.ts +0 -2
  404. package/dist/src/web/plugin/plugin.js +0 -39
  405. package/dist/src/web/plugin/plugin.js.map +0 -1
  406. package/dist/src/web/plugin/transform-data-llm.js.map +0 -1
  407. package/dist/src/web/plugin/transform-data-llm.test.js.map +0 -1
  408. package/dist/src/web/proxy.js.map +0 -1
  409. package/dist/src/web/types.d.ts +0 -149
  410. package/dist/src/web/types.js.map +0 -1
  411. package/dist/vitest.config.d.ts +0 -2
  412. package/dist/vitest.config.js +0 -8
  413. package/dist/vitest.config.js.map +0 -1
  414. /package/dist/{src/test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
  415. /package/dist/{src/web/bridges/hooks/use-mcp-app-bridge.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
  416. /package/dist/{src/web/create-store.test.d.ts → cli/tunnel.test.d.ts} +0 -0
  417. /package/dist/{src/web/bridges → cli}/types.js +0 -0
  418. /package/dist/{src/web/data-llm.test.d.ts → server/asset-base-url-transform-plugin.test.d.ts} +0 -0
  419. /package/dist/{src/web/generate-helpers.test-d.d.ts → server/content-helpers.test.d.ts} +0 -0
  420. /package/dist/{src/web/generate-helpers.test.d.ts → server/express.test.d.ts} +0 -0
  421. /package/dist/{src/server → server}/inferUtilityTypes.js +0 -0
  422. /package/dist/{src/web/helpers/state.test.d.ts → server/middleware.test-d.d.ts} +0 -0
  423. /package/dist/{src/web/hooks/use-call-tool.test-d.d.ts → server/middleware.test.d.ts} +0 -0
  424. /package/dist/{src/web/hooks/use-call-tool.test.d.ts → server/tunnel-proxy-router.test.d.ts} +0 -0
  425. /package/dist/{src/web/hooks/use-display-mode.test.d.ts → test/view.test.d.ts} +0 -0
  426. /package/dist/{src/web/hooks/use-files.test.d.ts → web/bridges/mcp-app/use-mcp-app-context.test.d.ts} +0 -0
  427. /package/dist/{src/web → web}/create-store.d.ts +0 -0
  428. /package/dist/{src/web/hooks/use-layout.test.d.ts → web/create-store.test.d.ts} +0 -0
  429. /package/dist/{src/web/plugin → web}/data-llm.test.d.ts +0 -0
  430. /package/dist/{src/web/hooks/use-open-external.test.d.ts → web/generate-helpers.test-d.d.ts} +0 -0
  431. /package/dist/{src/web/hooks/use-request-modal.test.d.ts → web/generate-helpers.test.d.ts} +0 -0
  432. /package/dist/{src/web → web}/generate-helpers.test.js +0 -0
  433. /package/dist/{src/web/hooks/use-tool-info.test-d.d.ts → web/helpers/state.test.d.ts} +0 -0
  434. /package/dist/{src/web/hooks/use-tool-info.test.d.ts → web/hooks/use-call-tool.test-d.d.ts} +0 -0
  435. /package/dist/{src/web → web}/hooks/use-call-tool.test-d.js +0 -0
  436. /package/dist/{src/web/hooks/use-user.test.d.ts → web/hooks/use-call-tool.test.d.ts} +0 -0
  437. /package/dist/{src/web/hooks/use-widget-state.test.d.ts → web/hooks/use-display-mode.test-d.d.ts} +0 -0
  438. /package/dist/{src/web/plugin/transform-data-llm.test.d.ts → web/hooks/use-display-mode.test.d.ts} +0 -0
  439. /package/dist/{src/web → web}/hooks/use-display-mode.test.js +0 -0
  440. /package/dist/{src/web → web}/hooks/use-tool-info.d.ts +0 -0
  441. /package/dist/{src/web → web}/hooks/use-tool-info.test-d.js +0 -0
  442. /package/dist/{src/web → web}/plugin/data-llm.test.js +0 -0
  443. /package/dist/{src/web → web}/plugin/transform-data-llm.d.ts +0 -0
  444. /package/dist/{src/web → web}/plugin/transform-data-llm.test.js +0 -0
  445. /package/dist/{src/web → web}/proxy.d.ts +0 -0
@@ -0,0 +1,48 @@
1
+ export function createTunnelHandler(manager) {
2
+ return (req, res) => {
3
+ if (req.url === "/__skybridge/tunnel" && req.method === "POST") {
4
+ manager.start();
5
+ sendJson(res, 200, manager.getState());
6
+ return;
7
+ }
8
+ if (req.url === "/__skybridge/tunnel" && req.method === "DELETE") {
9
+ manager.stop();
10
+ sendJson(res, 200, manager.getState());
11
+ return;
12
+ }
13
+ if (req.url === "/__skybridge/tunnel/events" && req.method === "GET") {
14
+ writeSseHead(res);
15
+ writeSse(res, "state", manager.getState());
16
+ const onState = (s) => {
17
+ writeSse(res, "state", s);
18
+ };
19
+ const onActivity = (a) => {
20
+ writeSse(res, "activity", a);
21
+ };
22
+ manager.on("state", onState);
23
+ manager.on("activity", onActivity);
24
+ req.on("close", () => {
25
+ manager.off("state", onState);
26
+ manager.off("activity", onActivity);
27
+ });
28
+ return;
29
+ }
30
+ res.writeHead(404).end();
31
+ };
32
+ }
33
+ function sendJson(res, status, data) {
34
+ res.writeHead(status, { "Content-Type": "application/json" });
35
+ res.end(JSON.stringify(data));
36
+ }
37
+ function writeSseHead(res) {
38
+ res.writeHead(200, {
39
+ "Content-Type": "text/event-stream",
40
+ "Cache-Control": "no-cache, no-transform",
41
+ Connection: "keep-alive",
42
+ });
43
+ res.flushHeaders?.();
44
+ }
45
+ function writeSse(res, event, data) {
46
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
47
+ }
48
+ //# sourceMappingURL=tunnel-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-handler.js","sourceRoot":"","sources":["../../src/cli/tunnel-handler.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAQ,EAAE;QACzD,IAAI,GAAG,CAAC,GAAG,KAAK,qBAAqB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/D,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,qBAAqB,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACjE,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,KAAK,4BAA4B,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YACrE,YAAY,CAAC,GAAG,CAAC,CAAC;YAClB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3C,MAAM,OAAO,GAAG,CAAC,CAAc,EAAE,EAAE;gBACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC,CAAC;YACF,MAAM,UAAU,GAAG,CAAC,CAAiB,EAAE,EAAE;gBACvC,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;YAC/B,CAAC,CAAC;YACF,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7B,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACnC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACnB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9B,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa;IAClE,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC9D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,YAAY,CAAC,GAAmB;IACvC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;QACjB,cAAc,EAAE,mBAAmB;QACnC,eAAe,EAAE,wBAAwB;QACzC,UAAU,EAAE,YAAY;KACzB,CAAC,CAAC;IACH,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC;AACvB,CAAC;AAED,SAAS,QAAQ,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAa;IACjE,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { TunnelActivity, TunnelManager, TunnelState } from \"./tunnel.js\";\n\nexport function createTunnelHandler(manager: TunnelManager) {\n return (req: IncomingMessage, res: ServerResponse): void => {\n if (req.url === \"/__skybridge/tunnel\" && req.method === \"POST\") {\n manager.start();\n sendJson(res, 200, manager.getState());\n return;\n }\n if (req.url === \"/__skybridge/tunnel\" && req.method === \"DELETE\") {\n manager.stop();\n sendJson(res, 200, manager.getState());\n return;\n }\n if (req.url === \"/__skybridge/tunnel/events\" && req.method === \"GET\") {\n writeSseHead(res);\n writeSse(res, \"state\", manager.getState());\n const onState = (s: TunnelState) => {\n writeSse(res, \"state\", s);\n };\n const onActivity = (a: TunnelActivity) => {\n writeSse(res, \"activity\", a);\n };\n manager.on(\"state\", onState);\n manager.on(\"activity\", onActivity);\n req.on(\"close\", () => {\n manager.off(\"state\", onState);\n manager.off(\"activity\", onActivity);\n });\n return;\n }\n res.writeHead(404).end();\n };\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n}\n\nfunction writeSseHead(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n });\n res.flushHeaders?.();\n}\n\nfunction writeSse(res: ServerResponse, event: string, data: unknown): void {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n}\n"]}
@@ -0,0 +1,105 @@
1
+ import { EventEmitter } from "node:events";
2
+ import http from "node:http";
3
+ import { Readable } from "node:stream";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { TunnelManager } from "./tunnel.js";
6
+ import { createTunnelHandler } from "./tunnel-handler.js";
7
+ let openServer;
8
+ afterEach(() => openServer?.close());
9
+ function makeFakeChild() {
10
+ const child = new EventEmitter();
11
+ child.stdout = new Readable({ read() { } });
12
+ child.stderr = new Readable({ read() { } });
13
+ child.kill = vi.fn(() => true);
14
+ return child;
15
+ }
16
+ async function listen(handler) {
17
+ const server = http.createServer(handler);
18
+ await new Promise((resolve) => server.listen(0, resolve));
19
+ const port = server.address().port;
20
+ return { port, server };
21
+ }
22
+ async function listenWithHandler() {
23
+ const child = makeFakeChild();
24
+ const manager = new TunnelManager({
25
+ getPort: () => 3000,
26
+ spawn: () => child,
27
+ });
28
+ const { port, server } = await listen(createTunnelHandler(manager));
29
+ openServer = server;
30
+ return { port, child, manager };
31
+ }
32
+ describe("createTunnelHandler", () => {
33
+ it("POST /__skybridge/tunnel starts the tunnel and returns the current state", async () => {
34
+ const { port, child } = await listenWithHandler();
35
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
36
+ method: "POST",
37
+ });
38
+ expect(res.status).toBe(200);
39
+ expect(await res.json()).toEqual({
40
+ status: "starting",
41
+ message: "Starting tunnel…",
42
+ });
43
+ expect(child.kill).not.toHaveBeenCalled();
44
+ });
45
+ it("POST /__skybridge/tunnel is idempotent — second call does not respawn", async () => {
46
+ const { port, manager } = await listenWithHandler();
47
+ const startSpy = vi.spyOn(manager, "start");
48
+ await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
49
+ method: "POST",
50
+ });
51
+ await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
52
+ method: "POST",
53
+ });
54
+ expect(startSpy).toHaveBeenCalledTimes(2);
55
+ // Manager.start() is internally idempotent (verified in tunnel.test.ts).
56
+ });
57
+ it("DELETE /__skybridge/tunnel stops the tunnel", async () => {
58
+ const { port, child } = await listenWithHandler();
59
+ await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
60
+ method: "POST",
61
+ });
62
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
63
+ method: "DELETE",
64
+ });
65
+ expect(res.status).toBe(200);
66
+ expect(await res.json()).toEqual({ status: "idle" });
67
+ expect(child.kill).toHaveBeenCalled();
68
+ });
69
+ it("GET /__skybridge/tunnel/events streams the current state on connect", async () => {
70
+ const { port, child } = await listenWithHandler();
71
+ await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
72
+ method: "POST",
73
+ });
74
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
75
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel/events`);
76
+ expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
77
+ expect(res.body).toBeTruthy();
78
+ const reader = res.body.getReader();
79
+ const { value } = await reader.read();
80
+ const chunk = new TextDecoder().decode(value);
81
+ expect(chunk).toContain("event: state");
82
+ expect(chunk).toContain('"status":"connected"');
83
+ expect(chunk).toContain('"url":"https://abc.tunnel.example"');
84
+ await reader.cancel();
85
+ });
86
+ it("GET /__skybridge/tunnel/events sends the current error state on connect", async () => {
87
+ const { port, child } = await listenWithHandler();
88
+ await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
89
+ method: "POST",
90
+ });
91
+ child.stderr.emit("data", Buffer.from("boom: tunnel auth failed\n"));
92
+ child.emit("close", 1);
93
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel/events`);
94
+ expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
95
+ expect(res.body).toBeTruthy();
96
+ const reader = res.body.getReader();
97
+ const { value } = await reader.read();
98
+ const chunk = new TextDecoder().decode(value);
99
+ expect(chunk).toContain("event: state");
100
+ expect(chunk).toContain('"status":"error"');
101
+ expect(chunk).toContain("boom: tunnel auth failed");
102
+ await reader.cancel();
103
+ });
104
+ });
105
+ //# sourceMappingURL=tunnel-handler.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-handler.test.js","sourceRoot":"","sources":["../../src/cli/tunnel-handler.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport http from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { TunnelManager } from \"./tunnel.js\";\nimport { createTunnelHandler } from \"./tunnel-handler.js\";\n\nlet openServer: http.Server | undefined;\nafterEach(() => openServer?.close());\n\ntype FakeChild = EventEmitter & {\n stdout: Readable;\n stderr: Readable;\n kill: ReturnType<typeof vi.fn<() => boolean>>;\n};\n\nfunction makeFakeChild(): FakeChild {\n const child = new EventEmitter() as FakeChild;\n child.stdout = new Readable({ read() {} });\n child.stderr = new Readable({ read() {} });\n child.kill = vi.fn<() => boolean>(() => true);\n return child;\n}\n\nasync function listen(handler: http.RequestListener) {\n const server = http.createServer(handler);\n await new Promise<void>((resolve) => server.listen(0, resolve));\n const port = (server.address() as { port: number }).port;\n return { port, server };\n}\n\nasync function listenWithHandler() {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n const { port, server } = await listen(createTunnelHandler(manager));\n openServer = server;\n return { port, child, manager };\n}\n\ndescribe(\"createTunnelHandler\", () => {\n it(\"POST /__skybridge/tunnel starts the tunnel and returns the current state\", async () => {\n const { port, child } = await listenWithHandler();\n\n const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n expect(res.status).toBe(200);\n expect(await res.json()).toEqual({\n status: \"starting\",\n message: \"Starting tunnel…\",\n });\n expect(child.kill).not.toHaveBeenCalled();\n });\n\n it(\"POST /__skybridge/tunnel is idempotent — second call does not respawn\", async () => {\n const { port, manager } = await listenWithHandler();\n const startSpy = vi.spyOn(manager, \"start\");\n\n await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n expect(startSpy).toHaveBeenCalledTimes(2);\n // Manager.start() is internally idempotent (verified in tunnel.test.ts).\n });\n\n it(\"DELETE /__skybridge/tunnel stops the tunnel\", async () => {\n const { port, child } = await listenWithHandler();\n await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"DELETE\",\n });\n expect(res.status).toBe(200);\n expect(await res.json()).toEqual({ status: \"idle\" });\n expect(child.kill).toHaveBeenCalled();\n });\n\n it(\"GET /__skybridge/tunnel/events streams the current state on connect\", async () => {\n const { port, child } = await listenWithHandler();\n await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n const res = await fetch(\n `http://localhost:${port}/__skybridge/tunnel/events`,\n );\n expect(res.headers.get(\"content-type\")).toMatch(/text\\/event-stream/);\n expect(res.body).toBeTruthy();\n\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const { value } = await reader.read();\n const chunk = new TextDecoder().decode(value);\n\n expect(chunk).toContain(\"event: state\");\n expect(chunk).toContain('\"status\":\"connected\"');\n expect(chunk).toContain('\"url\":\"https://abc.tunnel.example\"');\n\n await reader.cancel();\n });\n\n it(\"GET /__skybridge/tunnel/events sends the current error state on connect\", async () => {\n const { port, child } = await listenWithHandler();\n await fetch(`http://localhost:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n child.stderr.emit(\"data\", Buffer.from(\"boom: tunnel auth failed\\n\"));\n child.emit(\"close\", 1);\n\n const res = await fetch(\n `http://localhost:${port}/__skybridge/tunnel/events`,\n );\n expect(res.headers.get(\"content-type\")).toMatch(/text\\/event-stream/);\n expect(res.body).toBeTruthy();\n\n const reader = (res.body as ReadableStream<Uint8Array>).getReader();\n const { value } = await reader.read();\n const chunk = new TextDecoder().decode(value);\n\n expect(chunk).toContain(\"event: state\");\n expect(chunk).toContain('\"status\":\"error\"');\n expect(chunk).toContain(\"boom: tunnel auth failed\");\n\n await reader.cancel();\n });\n});\n"]}
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { Readable } from "node:stream";
3
+ export type TunnelState = {
4
+ status: "idle";
5
+ } | {
6
+ status: "starting";
7
+ message: string;
8
+ } | {
9
+ status: "connected";
10
+ url: string;
11
+ } | {
12
+ status: "error";
13
+ message: string;
14
+ };
15
+ export type ParsedStdoutEvent = {
16
+ kind: "connected";
17
+ url: string;
18
+ } | {
19
+ kind: "starting";
20
+ message: string;
21
+ };
22
+ export declare function parseStdoutLine(line: string): ParsedStdoutEvent | null;
23
+ export type TunnelActivity = {
24
+ time: string;
25
+ text: string;
26
+ level: "log" | "error";
27
+ };
28
+ export type TunnelChildProcess = {
29
+ stdout: Pick<Readable, "on"> | null;
30
+ stderr: Pick<Readable, "on"> | null;
31
+ kill: (signal?: NodeJS.Signals | number) => boolean;
32
+ on(event: "error", listener: (err: Error) => void): unknown;
33
+ on(event: "close", listener: (code: number | null) => void): unknown;
34
+ };
35
+ export type SpawnFn = (port: number) => TunnelChildProcess;
36
+ export declare class TunnelManager extends EventEmitter {
37
+ private state;
38
+ private child;
39
+ private timeout;
40
+ private stderrBuffer;
41
+ private connected;
42
+ private readonly getPort;
43
+ private readonly spawnFn;
44
+ constructor(opts: {
45
+ getPort: () => number;
46
+ spawn?: SpawnFn;
47
+ });
48
+ getState(): TunnelState;
49
+ subscribe(listener: (state: TunnelState) => void): () => void;
50
+ start(): void;
51
+ stop(): void;
52
+ private handleStdout;
53
+ private handleStderr;
54
+ private setState;
55
+ private emitActivity;
56
+ private clearConnectTimeout;
57
+ }
@@ -0,0 +1,154 @@
1
+ import { EventEmitter } from "node:events";
2
+ import spawn from "cross-spawn";
3
+ const FORWARDING_RE = /Forwarding:\s+(https?:\/\/\S+)\s*->\s*(\S+)/;
4
+ export function parseStdoutLine(line) {
5
+ const trimmed = line.trim();
6
+ if (!trimmed) {
7
+ return null;
8
+ }
9
+ const match = trimmed.match(FORWARDING_RE);
10
+ if (match?.[1]) {
11
+ return { kind: "connected", url: match[1].replace(/\/$/, "") };
12
+ }
13
+ return { kind: "starting", message: trimmed };
14
+ }
15
+ const CONNECT_TIMEOUT_MS = 60_000;
16
+ const STDERR_BUFFER_BYTES = 1024;
17
+ const defaultSpawn = (port) => spawn("npx", ["--yes", "alpic", "tunnel", "--port", String(port), "--plain"], {
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ });
20
+ export class TunnelManager extends EventEmitter {
21
+ state = { status: "idle" };
22
+ child = null;
23
+ timeout = null;
24
+ stderrBuffer = "";
25
+ connected = false;
26
+ getPort;
27
+ spawnFn;
28
+ constructor(opts) {
29
+ super();
30
+ this.getPort = opts.getPort;
31
+ this.spawnFn = opts.spawn ?? defaultSpawn;
32
+ // Multiple SSE subscribers (CLI, devtools, ad-hoc curl) can each register
33
+ // state + activity listeners; the default cap of 10 is easy to hit.
34
+ this.setMaxListeners(0);
35
+ }
36
+ getState() {
37
+ return this.state;
38
+ }
39
+ subscribe(listener) {
40
+ listener(this.state);
41
+ this.on("state", listener);
42
+ return () => {
43
+ this.off("state", listener);
44
+ };
45
+ }
46
+ start() {
47
+ if (this.state.status === "starting" || this.state.status === "connected") {
48
+ return;
49
+ }
50
+ this.connected = false;
51
+ this.stderrBuffer = "";
52
+ this.setState({ status: "starting", message: "Starting tunnel…" });
53
+ const child = this.spawnFn(this.getPort());
54
+ this.child = child;
55
+ this.timeout = setTimeout(() => {
56
+ if (!this.connected) {
57
+ this.setState({
58
+ status: "error",
59
+ message: "Tunnel connection timed out after one minute",
60
+ });
61
+ // Detach before killing so the imminent `close` event is treated as
62
+ // stale and does not overwrite the timeout error message.
63
+ this.child = null;
64
+ child.kill();
65
+ }
66
+ }, CONNECT_TIMEOUT_MS);
67
+ child.stdout?.on("data", (data) => {
68
+ this.handleStdout(data);
69
+ });
70
+ child.stderr?.on("data", (data) => {
71
+ this.handleStderr(data);
72
+ });
73
+ child.on("error", (err) => {
74
+ // Stale event from a child we've already replaced via stop()+start().
75
+ if (child !== this.child) {
76
+ return;
77
+ }
78
+ this.clearConnectTimeout();
79
+ this.setState({ status: "error", message: err.message });
80
+ });
81
+ child.on("close", (code) => {
82
+ // Stale event from a child we've already replaced via stop()+start().
83
+ if (child !== this.child) {
84
+ return;
85
+ }
86
+ this.clearConnectTimeout();
87
+ if (code !== 0 && code !== null) {
88
+ const detail = this.stderrBuffer.trim() || `exited with code ${code}`;
89
+ this.setState({ status: "error", message: detail });
90
+ }
91
+ else {
92
+ this.setState({ status: "idle" });
93
+ }
94
+ this.child = null;
95
+ });
96
+ }
97
+ stop() {
98
+ this.clearConnectTimeout();
99
+ if (this.child) {
100
+ this.child.kill();
101
+ this.child = null;
102
+ }
103
+ this.setState({ status: "idle" });
104
+ }
105
+ handleStdout(data) {
106
+ const lines = data.toString().split("\n");
107
+ for (const raw of lines) {
108
+ const parsed = parseStdoutLine(raw);
109
+ if (!parsed) {
110
+ continue;
111
+ }
112
+ if (parsed.kind === "connected") {
113
+ this.connected = true;
114
+ this.clearConnectTimeout();
115
+ this.setState({ status: "connected", url: parsed.url });
116
+ }
117
+ else if (this.connected) {
118
+ this.emitActivity(parsed.message, "log");
119
+ }
120
+ else {
121
+ this.setState({ status: "starting", message: parsed.message });
122
+ }
123
+ }
124
+ }
125
+ handleStderr(data) {
126
+ const text = data.toString().trim();
127
+ if (!text) {
128
+ return;
129
+ }
130
+ this.stderrBuffer = (this.stderrBuffer + text).slice(-STDERR_BUFFER_BYTES);
131
+ for (const line of text.split("\n").filter(Boolean)) {
132
+ this.emitActivity(line, "error");
133
+ }
134
+ }
135
+ setState(next) {
136
+ this.state = next;
137
+ this.emit("state", next);
138
+ }
139
+ emitActivity(text, level) {
140
+ const activity = {
141
+ time: new Date().toISOString(),
142
+ text,
143
+ level,
144
+ };
145
+ this.emit("activity", activity);
146
+ }
147
+ clearConnectTimeout() {
148
+ if (this.timeout) {
149
+ clearTimeout(this.timeout);
150
+ this.timeout = null;
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../../src/cli/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,MAAM,aAAa,CAAC;AAYhC,MAAM,aAAa,GAAG,6CAA6C,CAAC;AAEpE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAC3C,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAkBjC,MAAM,YAAY,GAAY,CAAC,IAAI,EAAE,EAAE,CACrC,KAAK,CACH,KAAK,EACL,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,EAC/D;IACE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;CAClC,CACF,CAAC;AAEJ,MAAM,OAAO,aAAc,SAAQ,YAAY;IACrC,KAAK,GAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACxC,KAAK,GAA+B,IAAI,CAAC;IACzC,OAAO,GAA0B,IAAI,CAAC;IACtC,YAAY,GAAG,EAAE,CAAC;IAClB,SAAS,GAAG,KAAK,CAAC;IACT,OAAO,CAAe;IACtB,OAAO,CAAU;IAElC,YAAY,IAAgD;QAC1D,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,YAAY,CAAC;QAC1C,0EAA0E;QAC1E,oEAAoE;QACpE,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,SAAS,CAAC,QAAsC;QAC9C,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC3B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC9B,CAAC,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAEnE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QAEnB,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACpB,IAAI,CAAC,QAAQ,CAAC;oBACZ,MAAM,EAAE,OAAO;oBACf,OAAO,EAAE,8CAA8C;iBACxD,CAAC,CAAC;gBACH,oEAAoE;gBACpE,0DAA0D;gBAC1D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;gBAClB,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,CAAC;QACH,CAAC,EAAE,kBAAkB,CAAC,CAAC;QAEvB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACxC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACxC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;YAC/B,sEAAsE;YACtE,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,EAAE;YACxC,sEAAsE;YACtE,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC3B,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,oBAAoB,IAAI,EAAE,CAAC;gBACtE,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACtD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACpC,CAAC;YACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI;QACF,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,CAAC;IAEO,YAAY,CAAC,IAAY;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1C,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS;YACX,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAChC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;gBACtB,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBAC3B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;YAC1D,CAAC;iBAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC1B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;IACH,CAAC;IAEO,YAAY,CAAC,IAAY;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,mBAAmB,CAAC,CAAC;QAC3E,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,IAAiB;QAChC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC;IAEO,YAAY,CAAC,IAAY,EAAE,KAAsB;QACvD,MAAM,QAAQ,GAAmB;YAC/B,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,IAAI;YACJ,KAAK;SACN,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;IAEO,mBAAmB;QACzB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;CACF","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Readable } from \"node:stream\";\nimport spawn from \"cross-spawn\";\n\nexport type TunnelState =\n | { status: \"idle\" }\n | { status: \"starting\"; message: string }\n | { status: \"connected\"; url: string }\n | { status: \"error\"; message: string };\n\nexport type ParsedStdoutEvent =\n | { kind: \"connected\"; url: string }\n | { kind: \"starting\"; message: string };\n\nconst FORWARDING_RE = /Forwarding:\\s+(https?:\\/\\/\\S+)\\s*->\\s*(\\S+)/;\n\nexport function parseStdoutLine(line: string): ParsedStdoutEvent | null {\n const trimmed = line.trim();\n if (!trimmed) {\n return null;\n }\n\n const match = trimmed.match(FORWARDING_RE);\n if (match?.[1]) {\n return { kind: \"connected\", url: match[1].replace(/\\/$/, \"\") };\n }\n return { kind: \"starting\", message: trimmed };\n}\n\nconst CONNECT_TIMEOUT_MS = 60_000;\nconst STDERR_BUFFER_BYTES = 1024;\n\nexport type TunnelActivity = {\n time: string;\n text: string;\n level: \"log\" | \"error\";\n};\n\nexport type TunnelChildProcess = {\n stdout: Pick<Readable, \"on\"> | null;\n stderr: Pick<Readable, \"on\"> | null;\n kill: (signal?: NodeJS.Signals | number) => boolean;\n on(event: \"error\", listener: (err: Error) => void): unknown;\n on(event: \"close\", listener: (code: number | null) => void): unknown;\n};\n\nexport type SpawnFn = (port: number) => TunnelChildProcess;\n\nconst defaultSpawn: SpawnFn = (port) =>\n spawn(\n \"npx\",\n [\"--yes\", \"alpic\", \"tunnel\", \"--port\", String(port), \"--plain\"],\n {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n },\n );\n\nexport class TunnelManager extends EventEmitter {\n private state: TunnelState = { status: \"idle\" };\n private child: ReturnType<SpawnFn> | null = null;\n private timeout: NodeJS.Timeout | null = null;\n private stderrBuffer = \"\";\n private connected = false;\n private readonly getPort: () => number;\n private readonly spawnFn: SpawnFn;\n\n constructor(opts: { getPort: () => number; spawn?: SpawnFn }) {\n super();\n this.getPort = opts.getPort;\n this.spawnFn = opts.spawn ?? defaultSpawn;\n // Multiple SSE subscribers (CLI, devtools, ad-hoc curl) can each register\n // state + activity listeners; the default cap of 10 is easy to hit.\n this.setMaxListeners(0);\n }\n\n getState(): TunnelState {\n return this.state;\n }\n\n subscribe(listener: (state: TunnelState) => void): () => void {\n listener(this.state);\n this.on(\"state\", listener);\n return () => {\n this.off(\"state\", listener);\n };\n }\n\n start(): void {\n if (this.state.status === \"starting\" || this.state.status === \"connected\") {\n return;\n }\n\n this.connected = false;\n this.stderrBuffer = \"\";\n this.setState({ status: \"starting\", message: \"Starting tunnel…\" });\n\n const child = this.spawnFn(this.getPort());\n this.child = child;\n\n this.timeout = setTimeout(() => {\n if (!this.connected) {\n this.setState({\n status: \"error\",\n message: \"Tunnel connection timed out after one minute\",\n });\n // Detach before killing so the imminent `close` event is treated as\n // stale and does not overwrite the timeout error message.\n this.child = null;\n child.kill();\n }\n }, CONNECT_TIMEOUT_MS);\n\n child.stdout?.on(\"data\", (data: Buffer) => {\n this.handleStdout(data);\n });\n child.stderr?.on(\"data\", (data: Buffer) => {\n this.handleStderr(data);\n });\n\n child.on(\"error\", (err: Error) => {\n // Stale event from a child we've already replaced via stop()+start().\n if (child !== this.child) {\n return;\n }\n this.clearConnectTimeout();\n this.setState({ status: \"error\", message: err.message });\n });\n\n child.on(\"close\", (code: number | null) => {\n // Stale event from a child we've already replaced via stop()+start().\n if (child !== this.child) {\n return;\n }\n this.clearConnectTimeout();\n if (code !== 0 && code !== null) {\n const detail = this.stderrBuffer.trim() || `exited with code ${code}`;\n this.setState({ status: \"error\", message: detail });\n } else {\n this.setState({ status: \"idle\" });\n }\n this.child = null;\n });\n }\n\n stop(): void {\n this.clearConnectTimeout();\n if (this.child) {\n this.child.kill();\n this.child = null;\n }\n this.setState({ status: \"idle\" });\n }\n\n private handleStdout(data: Buffer): void {\n const lines = data.toString().split(\"\\n\");\n for (const raw of lines) {\n const parsed = parseStdoutLine(raw);\n if (!parsed) {\n continue;\n }\n if (parsed.kind === \"connected\") {\n this.connected = true;\n this.clearConnectTimeout();\n this.setState({ status: \"connected\", url: parsed.url });\n } else if (this.connected) {\n this.emitActivity(parsed.message, \"log\");\n } else {\n this.setState({ status: \"starting\", message: parsed.message });\n }\n }\n }\n\n private handleStderr(data: Buffer): void {\n const text = data.toString().trim();\n if (!text) {\n return;\n }\n this.stderrBuffer = (this.stderrBuffer + text).slice(-STDERR_BUFFER_BYTES);\n for (const line of text.split(\"\\n\").filter(Boolean)) {\n this.emitActivity(line, \"error\");\n }\n }\n\n private setState(next: TunnelState): void {\n this.state = next;\n this.emit(\"state\", next);\n }\n\n private emitActivity(text: string, level: \"log\" | \"error\"): void {\n const activity: TunnelActivity = {\n time: new Date().toISOString(),\n text,\n level,\n };\n this.emit(\"activity\", activity);\n }\n\n private clearConnectTimeout(): void {\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = null;\n }\n }\n}\n"]}
@@ -0,0 +1,190 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { Readable } from "node:stream";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { parseStdoutLine, TunnelManager } from "./tunnel.js";
5
+ describe("parseStdoutLine", () => {
6
+ it("returns a connected event when the forwarding line is seen", () => {
7
+ const result = parseStdoutLine("Forwarding: https://abc.tunnel.example -> http://localhost:3000");
8
+ expect(result).toEqual({
9
+ kind: "connected",
10
+ url: "https://abc.tunnel.example",
11
+ });
12
+ });
13
+ it("strips a trailing slash from the forwarded URL", () => {
14
+ const result = parseStdoutLine("Forwarding: https://abc.tunnel.example/ -> http://localhost:3000");
15
+ expect(result).toEqual({
16
+ kind: "connected",
17
+ url: "https://abc.tunnel.example",
18
+ });
19
+ });
20
+ it("returns a starting event for any other non-empty line", () => {
21
+ expect(parseStdoutLine("Opening tunnel...")).toEqual({
22
+ kind: "starting",
23
+ message: "Opening tunnel...",
24
+ });
25
+ });
26
+ it("returns null for empty lines", () => {
27
+ expect(parseStdoutLine("")).toBeNull();
28
+ expect(parseStdoutLine(" ")).toBeNull();
29
+ });
30
+ });
31
+ function makeFakeChild() {
32
+ const child = new EventEmitter();
33
+ child.stdout = new Readable({ read() { } });
34
+ child.stderr = new Readable({ read() { } });
35
+ child.kill = vi.fn(() => true);
36
+ return child;
37
+ }
38
+ describe("TunnelManager", () => {
39
+ beforeEach(() => {
40
+ vi.useFakeTimers();
41
+ });
42
+ afterEach(() => {
43
+ vi.useRealTimers();
44
+ });
45
+ it("starts idle", () => {
46
+ const manager = new TunnelManager({
47
+ getPort: () => 3000,
48
+ spawn: () => makeFakeChild(),
49
+ });
50
+ expect(manager.getState()).toEqual({ status: "idle" });
51
+ });
52
+ it("transitions idle -> starting -> connected as stdout reports progress", () => {
53
+ const child = makeFakeChild();
54
+ const manager = new TunnelManager({
55
+ getPort: () => 3000,
56
+ spawn: () => child,
57
+ });
58
+ const states = [];
59
+ manager.on("state", (s) => states.push(s.status));
60
+ manager.start();
61
+ child.stdout.emit("data", Buffer.from("Opening tunnel...\n"));
62
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
63
+ expect(states).toEqual(["starting", "starting", "connected"]);
64
+ expect(manager.getState()).toEqual({
65
+ status: "connected",
66
+ url: "https://abc.tunnel.example",
67
+ });
68
+ });
69
+ it("emits activity events for stdout lines after connect", () => {
70
+ const child = makeFakeChild();
71
+ const manager = new TunnelManager({
72
+ getPort: () => 3000,
73
+ spawn: () => child,
74
+ });
75
+ const activity = [];
76
+ manager.on("activity", (a) => activity.push({ text: a.text, level: a.level }));
77
+ manager.start();
78
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
79
+ child.stdout.emit("data", Buffer.from("GET /widgets/foo\n"));
80
+ child.stderr.emit("data", Buffer.from("connection wobble\n"));
81
+ expect(activity).toEqual([
82
+ { text: "GET /widgets/foo", level: "log" },
83
+ { text: "connection wobble", level: "error" },
84
+ ]);
85
+ });
86
+ it("calling start twice does not spawn twice", () => {
87
+ const spawn = vi.fn(() => makeFakeChild());
88
+ const manager = new TunnelManager({ getPort: () => 3000, spawn });
89
+ manager.start();
90
+ manager.start();
91
+ expect(spawn).toHaveBeenCalledTimes(1);
92
+ });
93
+ it("transitions to error when the connection times out", () => {
94
+ const child = makeFakeChild();
95
+ const manager = new TunnelManager({
96
+ getPort: () => 3000,
97
+ spawn: () => child,
98
+ });
99
+ manager.start();
100
+ vi.advanceTimersByTime(60_000);
101
+ expect(manager.getState().status).toBe("error");
102
+ expect(child.kill).toHaveBeenCalled();
103
+ });
104
+ it("preserves the timeout error message when the killed child later emits close", () => {
105
+ const child = makeFakeChild();
106
+ const manager = new TunnelManager({
107
+ getPort: () => 3000,
108
+ spawn: () => child,
109
+ });
110
+ manager.start();
111
+ vi.advanceTimersByTime(60_000);
112
+ // The killed child emits a non-zero close after the timeout fired.
113
+ child.emit("close", 1);
114
+ expect(manager.getState()).toEqual({
115
+ status: "error",
116
+ message: "Tunnel connection timed out after one minute",
117
+ });
118
+ });
119
+ it("stop() kills the subprocess and goes idle", () => {
120
+ const child = makeFakeChild();
121
+ const manager = new TunnelManager({
122
+ getPort: () => 3000,
123
+ spawn: () => child,
124
+ });
125
+ manager.start();
126
+ manager.stop();
127
+ expect(child.kill).toHaveBeenCalled();
128
+ expect(manager.getState()).toEqual({ status: "idle" });
129
+ });
130
+ it("subscribers get the current state on subscribe", () => {
131
+ const child = makeFakeChild();
132
+ const manager = new TunnelManager({
133
+ getPort: () => 3000,
134
+ spawn: () => child,
135
+ });
136
+ manager.start();
137
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
138
+ let received;
139
+ const unsubscribe = manager.subscribe((s) => {
140
+ received = s;
141
+ });
142
+ expect(received).toEqual({
143
+ status: "connected",
144
+ url: "https://abc.tunnel.example",
145
+ });
146
+ unsubscribe();
147
+ });
148
+ it("ignores deferred close from a child that was replaced via stop()+start()", () => {
149
+ const childA = makeFakeChild();
150
+ const childB = makeFakeChild();
151
+ let spawnCount = 0;
152
+ const manager = new TunnelManager({
153
+ getPort: () => 3000,
154
+ spawn: () => (spawnCount++ === 0 ? childA : childB),
155
+ });
156
+ manager.start();
157
+ manager.stop();
158
+ manager.start();
159
+ childB.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
160
+ expect(manager.getState()).toEqual({
161
+ status: "connected",
162
+ url: "https://abc.tunnel.example",
163
+ });
164
+ // Stale close from childA arrives only now — must not clobber state.
165
+ childA.emit("close", null);
166
+ expect(manager.getState()).toEqual({
167
+ status: "connected",
168
+ url: "https://abc.tunnel.example",
169
+ });
170
+ });
171
+ it("ignores deferred error from a child that was replaced via stop()+start()", () => {
172
+ const childA = makeFakeChild();
173
+ const childB = makeFakeChild();
174
+ let spawnCount = 0;
175
+ const manager = new TunnelManager({
176
+ getPort: () => 3000,
177
+ spawn: () => (spawnCount++ === 0 ? childA : childB),
178
+ });
179
+ manager.start();
180
+ manager.stop();
181
+ manager.start();
182
+ childB.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
183
+ childA.emit("error", new Error("late spawn failure"));
184
+ expect(manager.getState()).toEqual({
185
+ status: "connected",
186
+ url: "https://abc.tunnel.example",
187
+ });
188
+ });
189
+ });
190
+ //# sourceMappingURL=tunnel.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.test.js","sourceRoot":"","sources":["../../src/cli/tunnel.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { Readable } from \"node:stream\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { parseStdoutLine, TunnelManager, type TunnelState } from \"./tunnel.js\";\n\ndescribe(\"parseStdoutLine\", () => {\n it(\"returns a connected event when the forwarding line is seen\", () => {\n const result = parseStdoutLine(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\",\n );\n expect(result).toEqual({\n kind: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n });\n\n it(\"strips a trailing slash from the forwarded URL\", () => {\n const result = parseStdoutLine(\n \"Forwarding: https://abc.tunnel.example/ -> http://localhost:3000\",\n );\n expect(result).toEqual({\n kind: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n });\n\n it(\"returns a starting event for any other non-empty line\", () => {\n expect(parseStdoutLine(\"Opening tunnel...\")).toEqual({\n kind: \"starting\",\n message: \"Opening tunnel...\",\n });\n });\n\n it(\"returns null for empty lines\", () => {\n expect(parseStdoutLine(\"\")).toBeNull();\n expect(parseStdoutLine(\" \")).toBeNull();\n });\n});\n\ntype FakeChild = EventEmitter & {\n stdout: Readable;\n stderr: Readable;\n kill: ReturnType<typeof vi.fn<() => boolean>>;\n};\n\nfunction makeFakeChild(): FakeChild {\n const child = new EventEmitter() as FakeChild;\n child.stdout = new Readable({ read() {} });\n child.stderr = new Readable({ read() {} });\n child.kill = vi.fn<() => boolean>(() => true);\n return child;\n}\n\ndescribe(\"TunnelManager\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"starts idle\", () => {\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => makeFakeChild(),\n });\n expect(manager.getState()).toEqual({ status: \"idle\" });\n });\n\n it(\"transitions idle -> starting -> connected as stdout reports progress\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n const states: string[] = [];\n manager.on(\"state\", (s: TunnelState) => states.push(s.status));\n\n manager.start();\n child.stdout.emit(\"data\", Buffer.from(\"Opening tunnel...\\n\"));\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n expect(states).toEqual([\"starting\", \"starting\", \"connected\"]);\n expect(manager.getState()).toEqual({\n status: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n });\n\n it(\"emits activity events for stdout lines after connect\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n const activity: Array<{ text: string; level: string }> = [];\n manager.on(\"activity\", (a: { text: string; level: string }) =>\n activity.push({ text: a.text, level: a.level }),\n );\n\n manager.start();\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n child.stdout.emit(\"data\", Buffer.from(\"GET /widgets/foo\\n\"));\n child.stderr.emit(\"data\", Buffer.from(\"connection wobble\\n\"));\n\n expect(activity).toEqual([\n { text: \"GET /widgets/foo\", level: \"log\" },\n { text: \"connection wobble\", level: \"error\" },\n ]);\n });\n\n it(\"calling start twice does not spawn twice\", () => {\n const spawn = vi.fn(() => makeFakeChild());\n const manager = new TunnelManager({ getPort: () => 3000, spawn });\n manager.start();\n manager.start();\n expect(spawn).toHaveBeenCalledTimes(1);\n });\n\n it(\"transitions to error when the connection times out\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n manager.start();\n\n vi.advanceTimersByTime(60_000);\n\n expect(manager.getState().status).toBe(\"error\");\n expect(child.kill).toHaveBeenCalled();\n });\n\n it(\"preserves the timeout error message when the killed child later emits close\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n manager.start();\n\n vi.advanceTimersByTime(60_000);\n // The killed child emits a non-zero close after the timeout fired.\n child.emit(\"close\", 1);\n\n expect(manager.getState()).toEqual({\n status: \"error\",\n message: \"Tunnel connection timed out after one minute\",\n });\n });\n\n it(\"stop() kills the subprocess and goes idle\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n manager.start();\n manager.stop();\n expect(child.kill).toHaveBeenCalled();\n expect(manager.getState()).toEqual({ status: \"idle\" });\n });\n\n it(\"subscribers get the current state on subscribe\", () => {\n const child = makeFakeChild();\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => child,\n });\n manager.start();\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n let received: TunnelState | undefined;\n const unsubscribe = manager.subscribe((s) => {\n received = s;\n });\n expect(received).toEqual({\n status: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n unsubscribe();\n });\n\n it(\"ignores deferred close from a child that was replaced via stop()+start()\", () => {\n const childA = makeFakeChild();\n const childB = makeFakeChild();\n let spawnCount = 0;\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => (spawnCount++ === 0 ? childA : childB),\n });\n\n manager.start();\n manager.stop();\n manager.start();\n childB.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n expect(manager.getState()).toEqual({\n status: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n\n // Stale close from childA arrives only now — must not clobber state.\n childA.emit(\"close\", null);\n expect(manager.getState()).toEqual({\n status: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n });\n\n it(\"ignores deferred error from a child that was replaced via stop()+start()\", () => {\n const childA = makeFakeChild();\n const childB = makeFakeChild();\n let spawnCount = 0;\n const manager = new TunnelManager({\n getPort: () => 3000,\n spawn: () => (spawnCount++ === 0 ? childA : childB),\n });\n\n manager.start();\n manager.stop();\n manager.start();\n childB.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n childA.emit(\"error\", new Error(\"late spawn failure\"));\n expect(manager.getState()).toEqual({\n status: \"connected\",\n url: \"https://abc.tunnel.example\",\n });\n });\n});\n"]}