@dylanpiercey/work 0.1.0 → 0.2.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 (522) hide show
  1. package/README.md +45 -5
  2. package/bin/work.mjs +449 -10
  3. package/dist/_BO3_WJb5.js +169 -0
  4. package/dist/{_Bq7to522.js → _CzOrFOdN.js} +527 -56
  5. package/dist/_DiOv9AKj.js +2 -0
  6. package/dist/index.mjs +8916 -7891
  7. package/dist/public/assets/{_Dytf99zx.js → _3hcTpTUc.js} +1 -1
  8. package/dist/public/assets/_3hcTpTUc.js.br +0 -0
  9. package/dist/public/assets/_88jVvZ6C.js +1 -0
  10. package/dist/public/assets/{_DrIgZJzN.js → _B0FtZL3L.js} +1 -1
  11. package/dist/public/assets/_B0FtZL3L.js.br +0 -0
  12. package/dist/public/assets/{_C0_s45qg.js → _B0rj5kOY.js} +1 -1
  13. package/dist/public/assets/_B0rj5kOY.js.br +0 -0
  14. package/dist/public/assets/{_BNW5Jiec.js → _B3aw0q8p.js} +1 -1
  15. package/dist/public/assets/_B3aw0q8p.js.br +0 -0
  16. package/dist/public/assets/{_Bhx319Hi.js → _B42XfAog.js} +1 -1
  17. package/dist/public/assets/_B42XfAog.js.br +0 -0
  18. package/dist/public/assets/_B5Ih3Lwt.js +1 -0
  19. package/dist/public/assets/_B6jh-_o_.js +1 -0
  20. package/dist/public/assets/_B8FHVXDZ.js +1 -0
  21. package/dist/public/assets/_BBJYNk03.js +1 -0
  22. package/dist/public/assets/{_D4q8XSMI.js → _BBk5_vEh.js} +1 -1
  23. package/dist/public/assets/_BBk5_vEh.js.br +0 -0
  24. package/dist/public/assets/_BCS_gQxR.js +15 -0
  25. package/dist/public/assets/_BCS_gQxR.js.br +0 -0
  26. package/dist/public/assets/{_BvXp6HpX.js → _BDBxSbjY.js} +1 -1
  27. package/dist/public/assets/_BDBxSbjY.js.br +0 -0
  28. package/dist/public/assets/_BDkqB224.js +1 -0
  29. package/dist/public/assets/{_SGDxiwE1.js → _BEtl4C9q.js} +1 -1
  30. package/dist/public/assets/_BEtl4C9q.js.br +0 -0
  31. package/dist/public/assets/{_DDl8KSWJ.js → _BJZwk-Pf.js} +1 -1
  32. package/dist/public/assets/_BJZwk-Pf.js.br +0 -0
  33. package/dist/public/assets/{_CpjSCB1I.js → _BM8qop28.js} +1 -1
  34. package/dist/public/assets/_BM8qop28.js.br +0 -0
  35. package/dist/public/assets/_BNL2e5az.js +1 -0
  36. package/dist/public/assets/{_CEXHZd73.js → _BODl5Jdc.js} +1 -1
  37. package/dist/public/assets/_BODl5Jdc.js.br +0 -0
  38. package/dist/public/assets/{_De7P3jUH.js → _BOe3voZI.js} +1 -1
  39. package/dist/public/assets/_BOe3voZI.js.br +0 -0
  40. package/dist/public/assets/{_eoa1EOU-.js → _BPW-OoaM.js} +1 -1
  41. package/dist/public/assets/_BPW-OoaM.js.br +0 -0
  42. package/dist/public/assets/{_BE_Jyvqd.js → _BSHUa-H0.js} +1 -1
  43. package/dist/public/assets/_BSHUa-H0.js.br +0 -0
  44. package/dist/public/assets/_BTXURtlK.js +1 -0
  45. package/dist/public/assets/{_Bp8-V8yl.js → _BU4lH-F3.js} +1 -1
  46. package/dist/public/assets/_BU4lH-F3.js.br +0 -0
  47. package/dist/public/assets/{_wcshV0sZ.js → _BVGSrb9C.js} +1 -1
  48. package/dist/public/assets/_BVGSrb9C.js.br +0 -0
  49. package/dist/public/assets/{_CTINBWaG.js → _BXyDScMX.js} +1 -1
  50. package/dist/public/assets/_BXyDScMX.js.br +0 -0
  51. package/dist/public/assets/{_Bm4SHrNp.js → _BZXtyNwD.js} +1 -1
  52. package/dist/public/assets/_BZXtyNwD.js.br +0 -0
  53. package/dist/public/assets/{_DfI982PI.js → _B_iUZxSk.js} +1 -1
  54. package/dist/public/assets/_B_iUZxSk.js.br +0 -0
  55. package/dist/public/assets/_B_klvcQs.js +1 -0
  56. package/dist/public/assets/{_CHuLFr1f.js → _BaNsw5pr.js} +1 -1
  57. package/dist/public/assets/_BaNsw5pr.js.br +0 -0
  58. package/dist/public/assets/_BbPq34k8.js +1 -0
  59. package/dist/public/assets/{_CAkPJLeF.js → _BfDaM2KU.js} +1 -1
  60. package/dist/public/assets/{_CAkPJLeF.js.br → _BfDaM2KU.js.br} +0 -0
  61. package/dist/public/assets/_Bk1naXp4.js +1 -0
  62. package/dist/public/assets/{_BOGkzMx9.js → _BkNKadGd.js} +1 -1
  63. package/dist/public/assets/_BkNKadGd.js.br +0 -0
  64. package/dist/public/assets/_BkztnqUW.js +1 -0
  65. package/dist/public/assets/_BkztnqUW.js.br +0 -0
  66. package/dist/public/assets/_BlNBDTvP.js +1 -0
  67. package/dist/public/assets/_BlNBDTvP.js.br +0 -0
  68. package/dist/public/assets/_BlUmx1wN.js +1 -0
  69. package/dist/public/assets/_Ble8b9gd.js +1 -0
  70. package/dist/public/assets/{_BDa-iwuj.js → _BpEvAl9E.js} +1 -1
  71. package/dist/public/assets/_BpEvAl9E.js.br +0 -0
  72. package/dist/public/assets/_Bqyvdrqf.js +1 -0
  73. package/dist/public/assets/_BsA2bpua.js +1 -0
  74. package/dist/public/assets/_BsA2bpua.js.br +0 -0
  75. package/dist/public/assets/_BsAVia13.js +1 -0
  76. package/dist/public/assets/_BtxLmMRq.js +1 -0
  77. package/dist/public/assets/{_ClldMY37.js → _Byfemjjt.js} +1 -1
  78. package/dist/public/assets/_Byfemjjt.js.br +0 -0
  79. package/dist/public/assets/{_MCwIJAuJ.js → _Bz94qqXL.js} +1 -1
  80. package/dist/public/assets/_Bz94qqXL.js.br +0 -0
  81. package/dist/public/assets/_C-HIZYIZ.js +1 -0
  82. package/dist/public/assets/{_D4quFMVQ.js → _C-UGjoFi.js} +1 -1
  83. package/dist/public/assets/_C-UGjoFi.js.br +0 -0
  84. package/dist/public/assets/{_wXDh-Z2N.js → _C-jXvDAe.js} +1 -1
  85. package/dist/public/assets/_C-jXvDAe.js.br +0 -0
  86. package/dist/public/assets/{_Ct3RF80L.js → _C2b1kmvc.js} +1 -1
  87. package/dist/public/assets/_C2b1kmvc.js.br +0 -0
  88. package/dist/public/assets/{_CIb7xw_1.js → _C3PIXMJF.js} +1 -1
  89. package/dist/public/assets/_C3PIXMJF.js.br +0 -0
  90. package/dist/public/assets/_C6eXJBpX.js +1 -0
  91. package/dist/public/assets/{_C654p96Z.js → _C7Xd05b1.js} +1 -1
  92. package/dist/public/assets/_C7Xd05b1.js.br +0 -0
  93. package/dist/public/assets/_C8jPP8tm.js +1 -0
  94. package/dist/public/assets/{_CamWDaoG.js → _CB9BGKKP.js} +1 -1
  95. package/dist/public/assets/_CB9BGKKP.js.br +0 -0
  96. package/dist/public/assets/{_bcsuBP7T.js → _CD44YUtO.js} +1 -1
  97. package/dist/public/assets/_CD44YUtO.js.br +0 -0
  98. package/dist/public/assets/{_D6kktUFn.js → _CEXagH_p.js} +1 -1
  99. package/dist/public/assets/_CEXagH_p.js.br +0 -0
  100. package/dist/public/assets/_CFOiLJdu.js +1 -0
  101. package/dist/public/assets/_CFOiLJdu.js.br +0 -0
  102. package/dist/public/assets/{_D1uRW1qz.js → _CGrClLsS.js} +1 -1
  103. package/dist/public/assets/_CGrClLsS.js.br +0 -0
  104. package/dist/public/assets/_CHJuzFIS.js +1 -0
  105. package/dist/public/assets/{_DC-78WeM.js → _CJ7DGNLC.js} +1 -1
  106. package/dist/public/assets/_CJ7DGNLC.js.br +0 -0
  107. package/dist/public/assets/_CJG7LF4o.js +1 -0
  108. package/dist/public/assets/_CK0tdQ6M.js +1591 -0
  109. package/dist/public/assets/_CK0tdQ6M.js.br +0 -0
  110. package/dist/public/assets/{_CMSBfO6u.js → _CKcab_6c.js} +1 -1
  111. package/dist/public/assets/_CKcab_6c.js.br +0 -0
  112. package/dist/public/assets/{_8G2Mv7jG.js → _CKpSd2DN.js} +1 -1
  113. package/dist/public/assets/_CKpSd2DN.js.br +0 -0
  114. package/dist/public/assets/{_zSAfqXcm.js → _CKy2HduV.js} +1 -1
  115. package/dist/public/assets/_CKy2HduV.js.br +0 -0
  116. package/dist/public/assets/{_BUvqweu0.js → _CLikwAg5.js} +1 -1
  117. package/dist/public/assets/_CLikwAg5.js.br +0 -0
  118. package/dist/public/assets/{_CxAPL49a.js → _CM-AIl7T.js} +1 -1
  119. package/dist/public/assets/_CM-AIl7T.js.br +0 -0
  120. package/dist/public/assets/_CMwrJ1ju.js +1 -0
  121. package/dist/public/assets/_CMwrJ1ju.js.br +0 -0
  122. package/dist/public/assets/_CQG-EbMi.js +1 -0
  123. package/dist/public/assets/_CUl36Gcx.js +1 -0
  124. package/dist/public/assets/{_BwP07uVS.js → _CUq2djIT.js} +1 -1
  125. package/dist/public/assets/_CUq2djIT.js.br +0 -0
  126. package/dist/public/assets/{_DFWOMF-B.js → _CWSGdqC2.js} +1 -1
  127. package/dist/public/assets/_CWSGdqC2.js.br +0 -0
  128. package/dist/public/assets/_CXK3AUqn.js +1 -0
  129. package/dist/public/assets/{_d5u7EI97.js → _CYYRiJuo.js} +1 -1
  130. package/dist/public/assets/_CYYRiJuo.js.br +0 -0
  131. package/dist/public/assets/_CYr8GcJI.js +1 -0
  132. package/dist/public/assets/{_8dvK2NDJ.js → _CaFhYr8X.js} +1 -1
  133. package/dist/public/assets/_CaFhYr8X.js.br +0 -0
  134. package/dist/public/assets/_CbzGfu_I.js +1 -0
  135. package/dist/public/assets/{_CZ_OWtIT.js → _Cd85Xtd1.js} +1 -1
  136. package/dist/public/assets/_Cd85Xtd1.js.br +0 -0
  137. package/dist/public/assets/_Ch-bwiFD.js +1 -0
  138. package/dist/public/assets/_ClGAsZdo.js +1 -0
  139. package/dist/public/assets/_ClGAsZdo.js.br +0 -0
  140. package/dist/public/assets/_ClhDmYWO.js +1 -0
  141. package/dist/public/assets/_ClhDmYWO.js.br +0 -0
  142. package/dist/public/assets/{_9kX9D09B.js → _Cm85V2m3.js} +1 -1
  143. package/dist/public/assets/_Cm85V2m3.js.br +0 -0
  144. package/dist/public/assets/_Cp4ZjGeg.js +1 -0
  145. package/dist/public/assets/{_C_37_B6u.js → _CsRnEvC3.js} +1 -1
  146. package/dist/public/assets/_CsRnEvC3.js.br +0 -0
  147. package/dist/public/assets/_CtFo1511.js +1 -0
  148. package/dist/public/assets/{_DcMIlvz-.js → _CtIpOiCk.js} +1 -1
  149. package/dist/public/assets/_CtIpOiCk.js.br +0 -0
  150. package/dist/public/assets/{_D4rOX-Fa.js → _CwOgQx7H.js} +1 -1
  151. package/dist/public/assets/_CwOgQx7H.js.br +0 -0
  152. package/dist/public/assets/_CwtVecVY.js +1 -0
  153. package/dist/public/assets/_Cxuln-KJ.js +1 -0
  154. package/dist/public/assets/_CyYNC4Ow.js +1 -0
  155. package/dist/public/assets/{_Dd6X3e7w.js → _Cy_g0woP.js} +1 -1
  156. package/dist/public/assets/_Cy_g0woP.js.br +0 -0
  157. package/dist/public/assets/{_COcePo2y.js → _CzDRlITT.js} +1 -1
  158. package/dist/public/assets/_CzDRlITT.js.br +0 -0
  159. package/dist/public/assets/_Czq9fyjZ.js +1 -0
  160. package/dist/public/assets/{_B582D2rg.js → _D1Rh4MKD.js} +1 -1
  161. package/dist/public/assets/_D2edy8B-.js +1 -0
  162. package/dist/public/assets/_D2edy8B-.js.br +0 -0
  163. package/dist/public/assets/_D2pdXUO3.js +1 -0
  164. package/dist/public/assets/_D4M5BFEU.js +1 -0
  165. package/dist/public/assets/{_D0aoEFRx.js → _D749nUfQ.js} +1 -1
  166. package/dist/public/assets/_D749nUfQ.js.br +0 -0
  167. package/dist/public/assets/_D7mcWce0.js +2 -0
  168. package/dist/public/assets/_D7mcWce0.js.br +0 -0
  169. package/dist/public/assets/{_BpoeUr5j.js → _D81sIY4I.js} +1 -1
  170. package/dist/public/assets/_D81sIY4I.js.br +0 -0
  171. package/dist/public/assets/{_D9sS4J_w.js → _D8Opg0DA.js} +1 -1
  172. package/dist/public/assets/_D8Opg0DA.js.br +0 -0
  173. package/dist/public/assets/{_B7GwDeKK.js → _DCJrZQ6J.js} +1 -1
  174. package/dist/public/assets/_DEPHQNx4.js +1 -0
  175. package/dist/public/assets/_DEtYVOe1.js +1 -0
  176. package/dist/public/assets/_DJ4gN7dQ.js +1 -0
  177. package/dist/public/assets/_DJ9NEjwG.js +1 -0
  178. package/dist/public/assets/{_xyw_DS5P.js → _DJUeUZKF.js} +1 -1
  179. package/dist/public/assets/_DJUeUZKF.js.br +0 -0
  180. package/dist/public/assets/_DLZswhbS.js +1 -0
  181. package/dist/public/assets/{_BywcP-As.js → _DMEtxMrc.js} +1 -1
  182. package/dist/public/assets/_DMEtxMrc.js.br +0 -0
  183. package/dist/public/assets/{_DqhphdU4.js → _DOr6ubI-.js} +1 -1
  184. package/dist/public/assets/_DOr6ubI-.js.br +0 -0
  185. package/dist/public/assets/{_ch_yXAap.js → _DQu5M2GV.js} +1 -1
  186. package/dist/public/assets/_DQu5M2GV.js.br +0 -0
  187. package/dist/public/assets/_DUvup9ew.js +1 -0
  188. package/dist/public/assets/{_CEJUcePr.js → _DWrhUk5T.js} +1 -1
  189. package/dist/public/assets/_DWrhUk5T.js.br +0 -0
  190. package/dist/public/assets/{_Bv3qNE1b.js → _DXOx4zKx.js} +1 -1
  191. package/dist/public/assets/_D_1b7pob.js +1 -0
  192. package/dist/public/assets/_Daap2skI.js +1 -0
  193. package/dist/public/assets/{_CUXajOKj.js → _DcktT8yN.js} +1 -1
  194. package/dist/public/assets/_DcktT8yN.js.br +0 -0
  195. package/dist/public/assets/{_D2un443j.js → _Ddc9Y9a_.js} +1 -1
  196. package/dist/public/assets/_Ddc9Y9a_.js.br +0 -0
  197. package/dist/public/assets/{_BBWaxDkO.js → _Dfjlzo9W.js} +1 -1
  198. package/dist/public/assets/_Dfjlzo9W.js.br +0 -0
  199. package/dist/public/assets/_Dh30YzVM.js +1 -0
  200. package/dist/public/assets/_DhohfuPc.js +1 -0
  201. package/dist/public/assets/_DjQz8h-q.js +1 -0
  202. package/dist/public/assets/_DjX1BGKn.js +1 -0
  203. package/dist/public/assets/_DjiaxzEs.js +1 -0
  204. package/dist/public/assets/_Dkg-QEnz.js +1 -0
  205. package/dist/public/assets/{_Bx1CW-sF.js → _Dl46-33v.js} +1 -1
  206. package/dist/public/assets/_Dl46-33v.js.br +0 -0
  207. package/dist/public/assets/_DlvIY6aE.js +1 -0
  208. package/dist/public/assets/{_DxEm-Gwj.js → _Dmi6AQP3.js} +1 -1
  209. package/dist/public/assets/_Dmi6AQP3.js.br +0 -0
  210. package/dist/public/assets/{_BbbOvNRH.js → _Dmj0vHNs.js} +1 -1
  211. package/dist/public/assets/{_BbbOvNRH.js.br → _Dmj0vHNs.js.br} +0 -0
  212. package/dist/public/assets/{_DSVLaMWS.js → _DnYboxB9.js} +1 -1
  213. package/dist/public/assets/_DnYboxB9.js.br +0 -0
  214. package/dist/public/assets/{_Cz5BPBG8.js → _Do6MGycg.js} +1 -1
  215. package/dist/public/assets/_Do6MGycg.js.br +0 -0
  216. package/dist/public/assets/{_DkCaEf_h.js → _DtZ-vebM.js} +1 -1
  217. package/dist/public/assets/_DtZ-vebM.js.br +0 -0
  218. package/dist/public/assets/_DuNisSjT.js +1 -0
  219. package/dist/public/assets/_Dvk-TlMq.js +1 -0
  220. package/dist/public/assets/_Dvk-TlMq.js.br +0 -0
  221. package/dist/public/assets/_DwdnhMjL.js +1 -0
  222. package/dist/public/assets/_DwydmxSi.js +1 -0
  223. package/dist/public/assets/_DzgHDUu0.js +1 -0
  224. package/dist/public/assets/{_Dp3IZdH8.js → _G-e2JHHS.js} +1 -1
  225. package/dist/public/assets/_G-e2JHHS.js.br +0 -0
  226. package/dist/public/assets/_IOS0xG1B.js +1 -0
  227. package/dist/public/assets/_IiZnk_jc.js +1 -0
  228. package/dist/public/assets/{_DaMVRI8f.js → _KYdxhRVL.js} +1 -1
  229. package/dist/public/assets/_KYdxhRVL.js.br +0 -0
  230. package/dist/public/assets/_LY1sEEvJ.js +1 -0
  231. package/dist/public/assets/_MATnTPAL.js +1 -0
  232. package/dist/public/assets/_MATnTPAL.js.br +0 -0
  233. package/dist/public/assets/_OVl4e0V3.js +1 -0
  234. package/dist/public/assets/{_Bf8WYPfh.js → _Sv1HjkeK.js} +1 -1
  235. package/dist/public/assets/_Sv1HjkeK.js.br +0 -0
  236. package/dist/public/assets/_Syg4UyQU.js +86 -0
  237. package/dist/public/assets/_Syg4UyQU.js.br +0 -0
  238. package/dist/public/assets/_TJCslr2g.js +1 -0
  239. package/dist/public/assets/_UcXY-7XH.js +2 -0
  240. package/dist/public/assets/_UcXY-7XH.js.br +0 -0
  241. package/dist/public/assets/{_j2tDSMvo.js → _VM9JbUGP.js} +1 -1
  242. package/dist/public/assets/_VM9JbUGP.js.br +0 -0
  243. package/dist/public/assets/_YOjtJWMK.js +1 -0
  244. package/dist/public/assets/_YOjtJWMK.js.br +10 -0
  245. package/dist/public/assets/_YPt61819.js +1 -0
  246. package/dist/public/assets/{_DbvaGqOl.js → _Zh4-3iG-.js} +1 -1
  247. package/dist/public/assets/_Zh4-3iG-.js.br +0 -0
  248. package/dist/public/assets/{_D4dJ2qEE.js → _ZptG1LQC.js} +1 -1
  249. package/dist/public/assets/_brnakAIx.js +1 -0
  250. package/dist/public/assets/_bsxJ1YDE.js +1 -0
  251. package/dist/public/assets/_f5wPpUkq.js +1 -0
  252. package/dist/public/assets/_fPqLhj85.js +1 -0
  253. package/dist/public/assets/_g3oXW6NQ.js +1 -0
  254. package/dist/public/assets/_iHPPeFV7.js +1 -0
  255. package/dist/public/assets/_iJwEKWn1.js +1 -0
  256. package/dist/public/assets/_jtFrcV5d.js +1 -0
  257. package/dist/public/assets/{_Bv3e3rg8.js → _kQ9qYC7a.js} +1 -1
  258. package/dist/public/assets/_kQ9qYC7a.js.br +0 -0
  259. package/dist/public/assets/_kQki_UDN.js +1 -0
  260. package/dist/public/assets/_lRRU15CX.js +1 -0
  261. package/dist/public/assets/_lRRU15CX.js.br +0 -0
  262. package/dist/public/assets/_mKx7UwEC.js +1 -0
  263. package/dist/public/assets/_nnKb_fyX.js +6 -0
  264. package/dist/public/assets/_nnKb_fyX.js.br +0 -0
  265. package/dist/public/assets/_p44jCyhe.js +1 -0
  266. package/dist/public/assets/_pAx6IPLY.js +1 -0
  267. package/dist/public/assets/_psqSIrEM.js +1 -0
  268. package/dist/public/assets/{_CyBCIknf.js → _w_IRrfdL.js} +1 -1
  269. package/dist/public/assets/_w_IRrfdL.js.br +0 -0
  270. package/dist/public/assets/_wxSNlWQ_.js +1 -0
  271. package/dist/public/assets/_xPBJn0Hx.js +1 -0
  272. package/dist/public/assets/_xPBJn0Hx.js.br +0 -0
  273. package/dist/public/assets/_yP2G9NCR.js +1 -0
  274. package/dist/public/assets/app-shell-CVYlktwa.css +1 -0
  275. package/dist/public/assets/app-shell-CVYlktwa.css.br +0 -0
  276. package/dist/public/assets/files-view-SzdFekvc.js +2 -0
  277. package/dist/public/assets/manage-workspace-DIY2p0qY.js +2 -0
  278. package/dist/public/assets/new-dBLsQZ5Y.js +1 -0
  279. package/dist/public/assets/new-workspace-D6YmZcwM.js +2 -0
  280. package/dist/public/assets/review-view-Dr5gLqyl.js +2 -0
  281. package/dist/public/assets/settings-dBLsQZ5Y.js +1 -0
  282. package/dist/public/assets/settings-page-COg15zf5.js +2 -0
  283. package/dist/public/assets/styles-DqimleOa.css +1 -0
  284. package/dist/public/assets/styles-DqimleOa.css.br +0 -0
  285. package/dist/public/assets/w-dBLsQZ5Y.js +1 -0
  286. package/dist/public/assets/w._-BbJyr8oS.js +1 -0
  287. package/dist/public/assets/w._-Bbhvn-OV.js +1 -0
  288. package/dist/public/assets/w._-dBLsQZ5Y.js +1 -0
  289. package/dist/rtc-helper.mjs +1170 -0
  290. package/lib/preview/media.mjs +5 -4
  291. package/lib/setup/doctor.d.mts +23 -1
  292. package/lib/setup/doctor.mjs +75 -9
  293. package/lib/setup/firewall.d.mts +76 -0
  294. package/lib/setup/firewall.mjs +598 -0
  295. package/lib/setup/tailscale.d.mts +53 -0
  296. package/lib/setup/tailscale.mjs +179 -0
  297. package/package.json +3 -2
  298. package/dist/_CkwTsHKB.js +0 -2
  299. package/dist/public/assets/_1RHVsAcK.js +0 -1
  300. package/dist/public/assets/_4CShjaLP.js +0 -1
  301. package/dist/public/assets/_68JtNDO1.js +0 -1
  302. package/dist/public/assets/_8G2Mv7jG.js.br +0 -0
  303. package/dist/public/assets/_8dvK2NDJ.js.br +0 -0
  304. package/dist/public/assets/_94L2SIJF.js +0 -1
  305. package/dist/public/assets/_9AnDb3YH.js +0 -1
  306. package/dist/public/assets/_9kX9D09B.js.br +0 -0
  307. package/dist/public/assets/_9lErVegn.js +0 -1
  308. package/dist/public/assets/_B0oyGUU5.js +0 -1
  309. package/dist/public/assets/_B0oyGUU5.js.br +0 -0
  310. package/dist/public/assets/_B1mPnLlp.js +0 -1
  311. package/dist/public/assets/_B4fi2SSA.js +0 -1
  312. package/dist/public/assets/_B7Qx25UP.js +0 -1
  313. package/dist/public/assets/_B7b8EVjA.js +0 -1
  314. package/dist/public/assets/_BBWaxDkO.js.br +0 -0
  315. package/dist/public/assets/_BDa-iwuj.js.br +0 -0
  316. package/dist/public/assets/_BEKHKNmM.js +0 -1
  317. package/dist/public/assets/_BE_Jyvqd.js.br +0 -0
  318. package/dist/public/assets/_BFU-kJkk.js +0 -1
  319. package/dist/public/assets/_BIJ2O2oU.js +0 -1
  320. package/dist/public/assets/_BJ59XAZ9.js +0 -1
  321. package/dist/public/assets/_BKqRYfOI.js +0 -15
  322. package/dist/public/assets/_BKqRYfOI.js.br +0 -0
  323. package/dist/public/assets/_BKr9Dzz1.js +0 -1
  324. package/dist/public/assets/_BN7xvxS_.js +0 -1
  325. package/dist/public/assets/_BNQTVBlH.js +0 -1
  326. package/dist/public/assets/_BNW5Jiec.js.br +0 -0
  327. package/dist/public/assets/_BOGkzMx9.js.br +0 -0
  328. package/dist/public/assets/_BUEpqQ_m.js +0 -1
  329. package/dist/public/assets/_BUvqweu0.js.br +0 -0
  330. package/dist/public/assets/_BZKB4fhr.js +0 -1
  331. package/dist/public/assets/_BZKB4fhr.js.br +0 -0
  332. package/dist/public/assets/_BZo9BjDF.js +0 -1
  333. package/dist/public/assets/_BdUNnSlC.js +0 -1
  334. package/dist/public/assets/_Bf8WYPfh.js.br +0 -0
  335. package/dist/public/assets/_Bhx319Hi.js.br +0 -0
  336. package/dist/public/assets/_Bm4SHrNp.js.br +0 -0
  337. package/dist/public/assets/_Bmrqq8Ro.js +0 -2
  338. package/dist/public/assets/_Bmrqq8Ro.js.br +0 -0
  339. package/dist/public/assets/_BnHZ6Npj.js +0 -1
  340. package/dist/public/assets/_BoDxRJLS.js +0 -1
  341. package/dist/public/assets/_BoDxRJLS.js.br +0 -0
  342. package/dist/public/assets/_BoPk1gXL.js +0 -1
  343. package/dist/public/assets/_Bo_Ru664.js +0 -1
  344. package/dist/public/assets/_Bp8-V8yl.js.br +0 -0
  345. package/dist/public/assets/_BpoeUr5j.js.br +0 -0
  346. package/dist/public/assets/_Bq2G_Tid.js +0 -1
  347. package/dist/public/assets/_BsyoLB_D.js +0 -1
  348. package/dist/public/assets/_Bv3e3rg8.js.br +0 -0
  349. package/dist/public/assets/_BvXp6HpX.js.br +0 -0
  350. package/dist/public/assets/_BwP07uVS.js.br +0 -0
  351. package/dist/public/assets/_Bx1CW-sF.js.br +0 -0
  352. package/dist/public/assets/_BywcP-As.js.br +0 -0
  353. package/dist/public/assets/_Bzgtwt3G.js +0 -1
  354. package/dist/public/assets/_Bzgtwt3G.js.br +0 -0
  355. package/dist/public/assets/_C0_s45qg.js.br +0 -0
  356. package/dist/public/assets/_C0ePgF6b.js +0 -1
  357. package/dist/public/assets/_C0oVtTqO.js +0 -1
  358. package/dist/public/assets/_C0oVtTqO.js.br +0 -0
  359. package/dist/public/assets/_C654p96Z.js.br +0 -0
  360. package/dist/public/assets/_C8_1Y2-z.js +0 -1
  361. package/dist/public/assets/_C8_1Y2-z.js.br +0 -0
  362. package/dist/public/assets/_C8v1AZFP.js +0 -1
  363. package/dist/public/assets/_CB0D5cRH.js +0 -1
  364. package/dist/public/assets/_CD05JUTh.js +0 -1
  365. package/dist/public/assets/_CEJUcePr.js.br +0 -0
  366. package/dist/public/assets/_CEXHZd73.js.br +0 -0
  367. package/dist/public/assets/_CHcUMVR7.js +0 -1
  368. package/dist/public/assets/_CHuLFr1f.js.br +0 -0
  369. package/dist/public/assets/_CIb7xw_1.js.br +0 -0
  370. package/dist/public/assets/_CMSBfO6u.js.br +0 -0
  371. package/dist/public/assets/_CN7GPyBo.js +0 -1
  372. package/dist/public/assets/_COcePo2y.js.br +0 -0
  373. package/dist/public/assets/_CTINBWaG.js.br +0 -0
  374. package/dist/public/assets/_CTWrnAbz.js +0 -1
  375. package/dist/public/assets/_CUXajOKj.js.br +0 -0
  376. package/dist/public/assets/_CVru3vzC.js +0 -1
  377. package/dist/public/assets/_CYjE__ml.js +0 -1
  378. package/dist/public/assets/_CYjE__ml.js.br +0 -0
  379. package/dist/public/assets/_CZ_OWtIT.js.br +0 -0
  380. package/dist/public/assets/_C_37_B6u.js.br +0 -0
  381. package/dist/public/assets/_C_CIQL7J.js +0 -1
  382. package/dist/public/assets/_C_CIQL7J.js.br +0 -0
  383. package/dist/public/assets/_C_WdpYbK.js +0 -1
  384. package/dist/public/assets/_CaiH3VN9.js +0 -1
  385. package/dist/public/assets/_CamWDaoG.js.br +0 -0
  386. package/dist/public/assets/_CdDElA0V.js +0 -1
  387. package/dist/public/assets/_CfnsClzZ.js +0 -1
  388. package/dist/public/assets/_Cgo1fRhJ.js +0 -1
  389. package/dist/public/assets/_Cgo1fRhJ.js.br +0 -0
  390. package/dist/public/assets/_CjhGrlf3.js +0 -1
  391. package/dist/public/assets/_CjlbL4C_.js +0 -1
  392. package/dist/public/assets/_CjogTUXK.js +0 -1
  393. package/dist/public/assets/_ClldMY37.js.br +0 -0
  394. package/dist/public/assets/_CmFq6Gtp.js +0 -1
  395. package/dist/public/assets/_CoAPOKiP.js +0 -1
  396. package/dist/public/assets/_CoUUgh9P.js +0 -1
  397. package/dist/public/assets/_CpjSCB1I.js.br +0 -0
  398. package/dist/public/assets/_Ct3RF80L.js.br +0 -0
  399. package/dist/public/assets/_CtYy5eZs.js +0 -1
  400. package/dist/public/assets/_CxAPL49a.js.br +0 -0
  401. package/dist/public/assets/_CyBCIknf.js.br +0 -0
  402. package/dist/public/assets/_Cz5BPBG8.js.br +0 -0
  403. package/dist/public/assets/_D-eEFXaI.js +0 -1
  404. package/dist/public/assets/_D0aoEFRx.js.br +0 -0
  405. package/dist/public/assets/_D1uRW1qz.js.br +0 -0
  406. package/dist/public/assets/_D2oIr2T2.js +0 -1
  407. package/dist/public/assets/_D2un443j.js.br +0 -0
  408. package/dist/public/assets/_D3MPx4-f.js +0 -1
  409. package/dist/public/assets/_D4q8XSMI.js.br +0 -0
  410. package/dist/public/assets/_D4quFMVQ.js.br +0 -0
  411. package/dist/public/assets/_D4rOX-Fa.js.br +0 -0
  412. package/dist/public/assets/_D5edrSyA.js +0 -1
  413. package/dist/public/assets/_D6kktUFn.js.br +0 -0
  414. package/dist/public/assets/_D80-qtVY.js +0 -1
  415. package/dist/public/assets/_D9-3EVA4.js +0 -1
  416. package/dist/public/assets/_D9sS4J_w.js.br +0 -0
  417. package/dist/public/assets/_DBLpxTDi.js +0 -1
  418. package/dist/public/assets/_DByHQ7nW.js +0 -1591
  419. package/dist/public/assets/_DByHQ7nW.js.br +0 -0
  420. package/dist/public/assets/_DC-78WeM.js.br +0 -0
  421. package/dist/public/assets/_DDl8KSWJ.js.br +0 -0
  422. package/dist/public/assets/_DFWOMF-B.js.br +0 -0
  423. package/dist/public/assets/_DKb-rt9n.js +0 -1
  424. package/dist/public/assets/_DMnSJCF_.js +0 -1
  425. package/dist/public/assets/_DOOIlrPh.js +0 -1
  426. package/dist/public/assets/_DPW-0yKP.js +0 -1
  427. package/dist/public/assets/_DR3BmvWU.js +0 -1
  428. package/dist/public/assets/_DSVLaMWS.js.br +0 -0
  429. package/dist/public/assets/_DYNGNKLk.js +0 -1
  430. package/dist/public/assets/_DZnEgCbj.js +0 -1
  431. package/dist/public/assets/_DaMVRI8f.js.br +0 -0
  432. package/dist/public/assets/_DbvaGqOl.js.br +0 -0
  433. package/dist/public/assets/_DcMIlvz-.js.br +0 -0
  434. package/dist/public/assets/_Dd6X3e7w.js.br +0 -0
  435. package/dist/public/assets/_De7P3jUH.js.br +0 -0
  436. package/dist/public/assets/_DeiFEAar.js +0 -1
  437. package/dist/public/assets/_DfI982PI.js.br +0 -0
  438. package/dist/public/assets/_DfZ6emcw.js +0 -1
  439. package/dist/public/assets/_Dj1lZ1pa.js +0 -86
  440. package/dist/public/assets/_Dj1lZ1pa.js.br +0 -0
  441. package/dist/public/assets/_DkCaEf_h.js.br +0 -0
  442. package/dist/public/assets/_DonAis0J.js +0 -1
  443. package/dist/public/assets/_Dp3IZdH8.js.br +0 -0
  444. package/dist/public/assets/_DqYih4xk.js +0 -1
  445. package/dist/public/assets/_DqYih4xk.js.br +0 -0
  446. package/dist/public/assets/_DqhphdU4.js.br +0 -0
  447. package/dist/public/assets/_DrIgZJzN.js.br +0 -0
  448. package/dist/public/assets/_Dt4hHR05.js +0 -1
  449. package/dist/public/assets/_DtTbTraA.js +0 -1
  450. package/dist/public/assets/_DwMxAJKP.js +0 -1
  451. package/dist/public/assets/_DxEm-Gwj.js.br +0 -0
  452. package/dist/public/assets/_Dytf99zx.js.br +0 -0
  453. package/dist/public/assets/_EAgv5jR9.js +0 -1
  454. package/dist/public/assets/_Iecpd0kS.js +0 -1
  455. package/dist/public/assets/_Iecpd0kS.js.br +0 -0
  456. package/dist/public/assets/_MCwIJAuJ.js.br +0 -0
  457. package/dist/public/assets/_NM79Lu3L.js +0 -6
  458. package/dist/public/assets/_NM79Lu3L.js.br +0 -0
  459. package/dist/public/assets/_QpNKKuS9.js +0 -1
  460. package/dist/public/assets/_SGDxiwE1.js.br +0 -0
  461. package/dist/public/assets/_TA8pE7z9.js +0 -1
  462. package/dist/public/assets/_bE4sNDeU.js +0 -1
  463. package/dist/public/assets/_bcsuBP7T.js.br +0 -0
  464. package/dist/public/assets/_cdcAU4yY.js +0 -1
  465. package/dist/public/assets/_ch_yXAap.js.br +0 -0
  466. package/dist/public/assets/_d5u7EI97.js.br +0 -0
  467. package/dist/public/assets/_eoa1EOU-.js.br +0 -0
  468. package/dist/public/assets/_j2tDSMvo.js.br +0 -0
  469. package/dist/public/assets/_jNhIFgbc.js +0 -1
  470. package/dist/public/assets/_kDYiTaag.js +0 -1
  471. package/dist/public/assets/_lNMVVbUx.js +0 -1
  472. package/dist/public/assets/_nwJ7Hu3S.js +0 -1
  473. package/dist/public/assets/_tmDtUACa.js +0 -1
  474. package/dist/public/assets/_tmDtUACa.js.br +0 -0
  475. package/dist/public/assets/_wXDh-Z2N.js.br +0 -0
  476. package/dist/public/assets/_wcshV0sZ.js.br +0 -0
  477. package/dist/public/assets/_x-NwqgCn.js +0 -1
  478. package/dist/public/assets/_xyw_DS5P.js.br +0 -0
  479. package/dist/public/assets/_zSAfqXcm.js.br +0 -0
  480. package/dist/public/assets/app-shell-ChQoVC5b.css +0 -1
  481. package/dist/public/assets/app-shell-ChQoVC5b.css.br +0 -0
  482. package/dist/public/assets/files-view-CLtLp_BW.js +0 -2
  483. package/dist/public/assets/files-view-CNHQapdI.css +0 -1
  484. package/dist/public/assets/files-view-CNHQapdI.css.br +0 -0
  485. package/dist/public/assets/group.css.ts-BG82htKb.css +0 -1
  486. package/dist/public/assets/group.css.ts-BG82htKb.css.br +0 -0
  487. package/dist/public/assets/icon-folder-DJOHuEpD.css +0 -1
  488. package/dist/public/assets/icon-folder-DJOHuEpD.css.br +0 -0
  489. package/dist/public/assets/icon-search-DsJuFXUJ.css +0 -1
  490. package/dist/public/assets/icon-search-DsJuFXUJ.css.br +0 -0
  491. package/dist/public/assets/manage-workspace-NBcFDyrB.js +0 -2
  492. package/dist/public/assets/manage-workspace-f_Np32TD.css +0 -1
  493. package/dist/public/assets/modal-view-KVNBpNYi.css +0 -1
  494. package/dist/public/assets/modal-view-KVNBpNYi.css.br +0 -0
  495. package/dist/public/assets/new-DHI77GvL.js +0 -1
  496. package/dist/public/assets/new-workspace-BFE-HpD_.css +0 -1
  497. package/dist/public/assets/new-workspace-BFE-HpD_.css.br +0 -0
  498. package/dist/public/assets/new-workspace-BRzimdd5.js +0 -2
  499. package/dist/public/assets/port-chip-CUUIDOVM.css +0 -1
  500. package/dist/public/assets/port-chip-CUUIDOVM.css.br +0 -0
  501. package/dist/public/assets/prompt-dialog-BGs6RXOV.css +0 -1
  502. package/dist/public/assets/prompt-dialog-BGs6RXOV.css.br +0 -0
  503. package/dist/public/assets/quick-open-Gx4so1bl.css +0 -1
  504. package/dist/public/assets/quick-open-Gx4so1bl.css.br +0 -0
  505. package/dist/public/assets/repo-picker-CUhd0vJN.css +0 -1
  506. package/dist/public/assets/repo-picker-CUhd0vJN.css.br +0 -4
  507. package/dist/public/assets/review-view-DFEK9VBa.js +0 -2
  508. package/dist/public/assets/review-view-deUY3OE5.css +0 -1
  509. package/dist/public/assets/review-view-deUY3OE5.css.br +0 -0
  510. package/dist/public/assets/section-CziSOSBR.css +0 -1
  511. package/dist/public/assets/section-CziSOSBR.css.br +0 -0
  512. package/dist/public/assets/settings-DHI77GvL.js +0 -1
  513. package/dist/public/assets/settings-page-OOtPOzIn.js +0 -2
  514. package/dist/public/assets/settings-page-wZhd7FLi.css +0 -1
  515. package/dist/public/assets/settings-page-wZhd7FLi.css.br +0 -0
  516. package/dist/public/assets/skill-row-BoCyFIXg.css +0 -1
  517. package/dist/public/assets/sso-auth-DV76tTpE.css +0 -1
  518. package/dist/public/assets/sso-auth-DV76tTpE.css.br +0 -0
  519. package/dist/public/assets/w-DHI77GvL.js +0 -1
  520. package/dist/public/assets/w._-DHI77GvL.js +0 -1
  521. package/dist/public/assets/w._-DsTgq3-C.js +0 -1
  522. package/dist/public/assets/w._-igEOSPR-.js +0 -1
package/README.md CHANGED
@@ -80,13 +80,50 @@ from then on. A refused request gets a page saying which setting it needs.
80
80
  Everything (pages, API, and the `/term` websockets) serves from the single
81
81
  app port, so route the whole hostname to it.
82
82
 
83
+ **Tailscale** is the zero-config way. On a machine logged in to Tailscale
84
+ (1.52 or newer), `work setup` offers to run `tailscale serve --bg <port>`,
85
+ which puts work at `https://<machine>.<tailnet>.ts.net/` for your devices on
86
+ the tailnet alone (never Funnel, which would publish it to the internet),
87
+ and adds that name to `WORK_ALLOWED_HOSTS`: open it on a phone signed in to
88
+ the same tailnet. `work setup --tailscale` does it without asking and
89
+ `--no-tailscale` skips the question; `--yes` alone leaves Tailscale's config
90
+ as it is. A serve config already pointing at work only has its name
91
+ allowed, and `work doctor` says which of these holds.
92
+
93
+ **Direct connections.** Behind a proxy, a tunnel or a relay, terminals
94
+ move to a direct WebRTC connection to this machine when it is measurably
95
+ faster than the path through the proxy (Settings → Terminal → Direct
96
+ connection: Auto by default, Always, or Off, one setting for every device). Pages,
97
+ the API and sign-in still go through your proxy; the direct path is set up
98
+ over the app's authenticated events socket, carries terminal traffic
99
+ alone, encrypted (DTLS), to UDP port `WORK_RTC_PORT` (by default the app's
100
+ port number), and ends when that login does. That port is held by a helper
101
+ process of work's own, only while direct connections are in use: it
102
+ starts at the first one and exits ten minutes after the last. Proxy-side inspection or
103
+ logging does not see terminal traffic that goes direct: turn the feature
104
+ off (per device in Settings, or `WORK_RTC=off` for the machine) where it
105
+ must. On the LAN the direct path needs nothing more; from another network
106
+ (a phone on cellular) the UDP port has to be reachable through your router.
107
+ Settings → Terminal can allow work to open it on the router when needed
108
+ (UPnP, off by default): only after a device failed to connect without it,
109
+ leased, and given back once unused. A host firewall has to let the port
110
+ in too: `work doctor` says when one likely drops it (firewalld's
111
+ FedoraServer zone does, for one), with the command that admits it, and
112
+ `work setup` offers to add that rule, named `work` so it is removed as one
113
+ (`--firewall` adds it without asking, `--no-firewall` skips the question).
114
+ macOS's firewall admits programs rather than ports, so there it is the
115
+ node the service runs that `work doctor` checks and `work setup` lets in. `WORK_RTC_STUN` names the STUN servers that find this machine's
116
+ public address (Cloudflare's by default; empty for none).
117
+
83
118
  **Opening workspace ports remotely.** The port chips in the sidebar link to
84
119
  `http://<host>:<port>`, which only works where the machine is reachable
85
- directly. Through a tunnel, route a wildcard hostname to the app port as
86
- well and work proxies it — HTTP and websockets, so HMR works — to
87
- `localhost:<port>`. `WORK_PORT_HOSTS` picks the hostname shape the chips
88
- link to; `sub` is always accepted, `dash` only when picked, and only for a
89
- name of three labels or more:
120
+ directly — under a tailnet name too, since `tailscale serve` has no
121
+ wildcard names: there a chip reaches a dev server that listens beyond
122
+ loopback (`--host`), and one on loopback says so. Through a tunnel, route
123
+ a wildcard hostname to the app port as well and work proxies it — HTTP and
124
+ websockets, so HMR works — to `localhost:<port>`. `WORK_PORT_HOSTS` picks
125
+ the hostname shape the chips link to; `sub` is always accepted, `dash` only
126
+ when picked, and only for a name of three labels or more:
90
127
 
91
128
  - `sub` (default): `https://3000.work.example.com`. Needs a wildcard
92
129
  `*.work.example.com` route and a certificate that covers it (your own
@@ -147,6 +184,9 @@ targeting that branch.
147
184
  | `WORK_PORT_HOSTS` | `sub` | Port-chip hostname shape: `sub` / `dash` (see above) |
148
185
  | `WORK_USER` / `WORK_HOST` | current user/host | Branch-name prefix parts |
149
186
  | `WORK_BRANCH_PREFIX` | `<user>-<host>` | Whole branch-name prefix, overriding the parts |
187
+ | `WORK_RTC` | on | `off` turns direct connections off (see Access) |
188
+ | `WORK_RTC_PORT` | the app port | UDP port of direct connections |
189
+ | `WORK_RTC_STUN` | Cloudflare's STUN | Their STUN servers, comma-separated (empty: none) |
150
190
 
151
191
  The usage panel (and `work usage`) reads plan/quota windows straight from each
152
192
  agent CLI's own credentials on disk — Claude, Codex, Cursor, Grok, and Copilot
package/bin/work.mjs CHANGED
@@ -2,6 +2,11 @@
2
2
  // work CLI — ships with the npm package (no dependencies; plain node).
3
3
  //
4
4
  // work setup install missing tools, sign in, install/refresh the background service
5
+ // (--tailscale serves work on the tailnet with `tailscale serve`, which
6
+ // it otherwise offers when Tailscale is logged in; --no-tailscale skips that;
7
+ // --firewall adds a `work` rule for direct connections' UDP port to a host
8
+ // firewall that likely drops it, or on macOS lets the service's node in,
9
+ // which it otherwise offers; --no-firewall skips that)
5
10
  // work doctor report machine readiness (--json, --fix to install what's missing)
6
11
  // work start run the server in the foreground
7
12
  // work update upgrade the npm package and refresh the service
@@ -26,8 +31,23 @@ import { execFileSync, spawnSync } from "node:child_process";
26
31
  import fs from "node:fs";
27
32
  import os from "node:os";
28
33
  import path from "node:path";
34
+ import readline from "node:readline/promises";
35
+ import tty from "node:tty";
29
36
  import { fileURLToPath } from "node:url";
30
- import { doctor, formatDoctor, setupPath } from "../lib/setup/doctor.mjs";
37
+ import {
38
+ doctor,
39
+ formatDoctor,
40
+ setupPath,
41
+ tailscaleProbe,
42
+ tilde,
43
+ which,
44
+ } from "../lib/setup/doctor.mjs";
45
+ import { allowHost, stateText } from "../lib/setup/tailscale.mjs";
46
+ import {
47
+ applyPlan,
48
+ firewallProbe,
49
+ serviceNode,
50
+ } from "../lib/setup/firewall.mjs";
31
51
  import { installTool, repairGhHelpers } from "../lib/setup/install.mjs";
32
52
  import { bridgeCommands, bridgeIfNeeded } from "../lib/setup/bridge.mjs";
33
53
  import { dialHost, listenStream } from "../lib/setup/listen.mjs";
@@ -638,17 +658,24 @@ function until(iso) {
638
658
 
639
659
  async function setup() {
640
660
  await bridge();
641
- await fix(await doctor());
661
+ const d = await doctor(doctorOpts());
662
+ await fix(d);
663
+ // Before the service is written: a tailnet name to answer goes into the
664
+ // WORK_ALLOWED_HOSTS it carries.
665
+ const tailnet = await tailscale(d.tailscale);
666
+ await firewall();
642
667
 
643
668
  const running = installService();
644
669
  const port = serviceEnv().PORT;
645
- maybeTunnel(port);
670
+ if (!tailnet) maybeTunnel(port);
646
671
  if (running) {
647
672
  if (await waitForServer()) log(`done — http://localhost:${port}`);
648
673
  else
649
674
  warn(
650
675
  `service started but nothing answered on port ${port} within 30s — check the logs`,
651
676
  );
677
+ if (tailnet)
678
+ log(`on your phone and other devices on the tailnet: ${tailnet}`);
652
679
  } else {
653
680
  // A fresh install's shell may not have mise on PATH yet — name the shim.
654
681
  const shim = path.join(
@@ -725,28 +752,195 @@ async function update() {
725
752
  "@dylanpiercey/work@latest",
726
753
  ]);
727
754
  // Re-exec the (possibly new) CLI to finish with fresh code.
728
- sh("work", ["setup", ...(yes ? ["--yes"] : [])]);
755
+ sh("work", [
756
+ "setup",
757
+ ...(yes ? ["--yes"] : []),
758
+ ...args.filter((a) =>
759
+ ["--tailscale", "--no-tailscale", "--firewall", "--no-firewall"].includes(
760
+ a,
761
+ ),
762
+ ),
763
+ ]);
729
764
  }
730
765
 
731
766
  /** Machine readiness from lib/setup — the same probe the Settings page runs. */
732
767
  async function doctorCmd() {
733
- let d = await doctor();
768
+ let d = await doctor(doctorOpts());
734
769
  if (args.includes("--fix")) {
735
770
  await fix(d);
736
- d = await doctor();
771
+ d = await doctor(doctorOpts());
737
772
  }
773
+ const installs = await workInstalls();
738
774
  if (args.includes("--json")) {
739
- console.log(JSON.stringify(d, null, 2));
775
+ console.log(JSON.stringify({ ...d, work: installs }, null, 2));
740
776
  } else {
741
777
  console.log(formatDoctor(d));
742
778
  const access = accessNote();
743
779
  if (access) warn(access);
780
+ for (const note of installNotes(installs)) warn(note);
744
781
  }
745
782
  // exitCode, not exit(): a pipe on macOS is written asynchronously, and
746
783
  // exiting at once can cut the JSON short for whatever reads it.
747
784
  if (!d.ready) process.exitCode = 1;
748
785
  }
749
786
 
787
+ /**
788
+ * This `work`, every other one on PATH (the first is what `work` runs),
789
+ * each with its version, and the version the running service reports
790
+ * (null when none answers). A package manager's global left behind — a
791
+ * Homebrew node's under mise's — shadows the one kept up to date, and the
792
+ * doctor it runs is then old code describing a machine it no longer knows.
793
+ */
794
+ async function workInstalls() {
795
+ const self = realpathOr(fileURLToPath(import.meta.url));
796
+ const onPath = [];
797
+ const seen = new Set();
798
+ for (const dir of (process.env.PATH || "").split(path.delimiter)) {
799
+ if (!dir) continue;
800
+ const file = path.join(dir, "work");
801
+ let real;
802
+ try {
803
+ if (!fs.statSync(file).isFile()) continue;
804
+ real = fs.realpathSync(file);
805
+ } catch {
806
+ continue;
807
+ }
808
+ if (seen.has(real)) continue;
809
+ seen.add(real);
810
+ onPath.push({ path: file, real, version: installVersion(real, file) });
811
+ }
812
+ let service = null;
813
+ try {
814
+ const res = await fetch(`${serverUrl()}/api/boot`, {
815
+ signal: AbortSignal.timeout(2000),
816
+ });
817
+ if (res.ok) service = (await res.json()).version ?? null;
818
+ } catch {}
819
+ return { self: { path: self, version: pkg.version }, onPath, service };
820
+ }
821
+
822
+ function realpathOr(file) {
823
+ try {
824
+ return fs.realpathSync(file);
825
+ } catch {
826
+ return file;
827
+ }
828
+ }
829
+
830
+ /** An install's version: an npm global's bin links into the package, whose
831
+ * package.json says; a shim (mise's) is asked. */
832
+ function installVersion(real, file) {
833
+ let dir = path.dirname(real);
834
+ for (let i = 0; i < 3; i++) {
835
+ try {
836
+ const p = JSON.parse(fs.readFileSync(path.join(dir, "package.json")));
837
+ if (p.name === "@dylanpiercey/work") return p.version ?? null;
838
+ } catch {}
839
+ dir = path.dirname(dir);
840
+ }
841
+ const out = spawnSync(file, ["version"], { encoding: "utf8", timeout: 5000 });
842
+ return /^\d+\.\d+\.\d+\S*/.exec((out.stdout || "").trim())?.[0] ?? null;
843
+ }
844
+
845
+ /** How to remove an install: the npm beside the node whose global it is. */
846
+ function removeHint(real) {
847
+ const m =
848
+ /^(.*)\/lib\/node_modules\/@dylanpiercey\/work\/bin\/work\.mjs$/.exec(real);
849
+ return m
850
+ ? `${m[1]}/bin/npm uninstall -g @dylanpiercey/work`
851
+ : `remove ${real}`;
852
+ }
853
+
854
+ function newest(versions) {
855
+ const parts = (v) => (v || "0").split(/[.-]/).map((n) => Number(n) || 0);
856
+ return versions.reduce((a, b) => {
857
+ const [x, y] = [parts(a), parts(b)];
858
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i] ? a : b;
859
+ return a;
860
+ }, null);
861
+ }
862
+
863
+ /** The doctor's warnings about which `work` this is. */
864
+ function installNotes({ self, onPath, service }) {
865
+ const notes = [];
866
+ if (service && service !== self.version) {
867
+ const theirs = onPath.find((w) => w.version === service);
868
+ notes.push(
869
+ `this work is ${self.version} (${tilde(self.path)}), but the running service is ${service}, so what this doctor says is an older or newer release's${theirs ? `: the service's is ${tilde(theirs.path)}` : ""}`,
870
+ );
871
+ }
872
+ if (onPath.length > 1) {
873
+ // A stray is one at another release than the service's (or, with no
874
+ // service answering, than the newest here).
875
+ const current = service ?? newest(onPath.map((w) => w.version));
876
+ const lines = onPath.map(
877
+ (w, i) =>
878
+ ` ${tilde(w.path)} → ${w.version ?? "unknown version"}${i === 0 ? " (what \`work\` runs)" : ""}${w.version === current ? "" : `\n remove: ${removeHint(w.real)}`}`,
879
+ );
880
+ notes.push(`more than one work is on PATH:\n${lines.join("\n")}`);
881
+ }
882
+ return notes;
883
+ }
884
+
885
+ /** What the doctor's Tailscale probe checks against: the port and allowed
886
+ * names the service runs with (or will, after `work setup`). */
887
+ function doctorOpts() {
888
+ const env = serviceEnv();
889
+ return {
890
+ port: env.PORT,
891
+ allowedHosts: env.WORK_ALLOWED_HOSTS || "",
892
+ rtcPort: env.WORK_RTC_PORT || "",
893
+ listen: env.WORK_LISTEN,
894
+ rtc: env.WORK_RTC || "",
895
+ firewall: serviceFirewall(),
896
+ };
897
+ }
898
+
899
+ /**
900
+ * The host firewall as the service meets it. macOS's admits programs, so
901
+ * the node that counts is the one the service runs, which need not be this
902
+ * shell's: asked of the running server first (`GET /api/direct`, its own
903
+ * reading), else read here for the node its definition runs
904
+ * (`serviceNode`), else for this one's, which the result says
905
+ * (`nodeFrom`). `fresh` skips the server's few minutes of keeping it.
906
+ */
907
+ async function serviceFirewall({ fresh = false } = {}) {
908
+ const env = serviceEnv();
909
+ try {
910
+ const res = await fetch(
911
+ `${serverUrl()}/api/direct${fresh ? "?fresh=1" : ""}`,
912
+ { signal: AbortSignal.timeout(3000) },
913
+ );
914
+ const live = res.ok ? await res.json() : null;
915
+ if (live && "firewall" in live)
916
+ return (
917
+ live.firewall && {
918
+ ...live.firewall,
919
+ node: live.firewall.node ?? live.node,
920
+ nodeFrom: "service",
921
+ }
922
+ );
923
+ } catch {}
924
+ let definition = null;
925
+ try {
926
+ definition = fs.readFileSync(serviceFile(), "utf8");
927
+ } catch {}
928
+ const fromUnit =
929
+ definition &&
930
+ serviceNode(definition, { PATH: installedServiceEnv().PATH || "" });
931
+ const node = fromUnit || process.execPath;
932
+ const f = await firewallProbe({
933
+ rtcPort: env.WORK_RTC_PORT,
934
+ appPort: env.PORT,
935
+ listen: env.WORK_LISTEN,
936
+ rtc: env.WORK_RTC,
937
+ node,
938
+ });
939
+ return (
940
+ f && { ...f, node: f.node ?? node, nodeFrom: fromUnit ? "unit" : "shell" }
941
+ );
942
+ }
943
+
750
944
  /** Said when the service listens beyond this machine with nothing declared
751
945
  * to answer there: every other machine is refused (server/access.ts), which
752
946
  * is safe, but likely not what the listening was for. */
@@ -956,6 +1150,9 @@ function serviceEnv() {
956
1150
  "WORK_ALLOWED_HOSTS",
957
1151
  "WORK_LISTEN",
958
1152
  "WORK_TRUSTED_PROXIES",
1153
+ "WORK_RTC",
1154
+ "WORK_RTC_PORT",
1155
+ "WORK_RTC_STUN",
959
1156
  ]) {
960
1157
  // `in` rather than a truthy check, so `WORK_ALLOWED_HOSTS= work setup`
961
1158
  // can deliberately clear a setting the installed service still carries.
@@ -1312,8 +1509,250 @@ function maybeTunnel(port = appPort) {
1312
1509
  if (yes || have("cloudflared")) return;
1313
1510
  console.log(`
1314
1511
  To reach work from your phone or other devices, put it behind something
1315
- that terminates TLS and authenticates, e.g. a Cloudflare Tunnel with
1512
+ that terminates TLS and authenticates, e.g. Tailscale (\`work setup
1513
+ --tailscale\` runs \`tailscale serve\` for you) or a Cloudflare Tunnel with
1316
1514
  Access (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
1317
- pointing at http://localhost:${port}, or Tailscale. The server itself has
1318
- no auth of its own.`);
1515
+ pointing at http://localhost:${port}. The server itself has no auth of its
1516
+ own.`);
1517
+ }
1518
+
1519
+ /**
1520
+ * `tailscale serve` in front of work: https://<machine>.<tailnet>.ts.net/,
1521
+ * TLS and the tailnet's own sign-in in the local daemon, proxied to work's
1522
+ * port, so requests arrive from this machine under that name — which goes
1523
+ * into WORK_ALLOWED_HOSTS. Tailnet only, never Funnel: Funnel publishes to
1524
+ * the internet, and work has no login of its own. Tailscale's config is
1525
+ * changed only on an explicit yes, the prompt's or `--tailscale`; `--yes`
1526
+ * is how the installer, `work update` and the app's own update run setup
1527
+ * unattended, and none of them asked for it. A serve config already
1528
+ * pointing at work only has its name allowed. Returns the URL work is
1529
+ * served at, or null.
1530
+ */
1531
+ async function tailscale(ts) {
1532
+ if (args.includes("--no-tailscale")) return null;
1533
+ const asked = args.includes("--tailscale");
1534
+ const port = serviceEnv().PORT;
1535
+ if (!ts?.installed) {
1536
+ if (asked)
1537
+ warn(
1538
+ "Tailscale is not installed: https://tailscale.com/download, then work setup --tailscale",
1539
+ );
1540
+ return null;
1541
+ }
1542
+ if (ts.serve) {
1543
+ if (!allowTailnet(ts.serve)) return null;
1544
+ if (ts.running) return servedAt(ts.serve.url);
1545
+ // A serve config outlives a logout, and serves again once it is up.
1546
+ log(
1547
+ `Tailscale is ${stateText(ts)}: work is at ${ts.serve.url} once it is up`,
1548
+ );
1549
+ return null;
1550
+ }
1551
+ if (!ts.running) {
1552
+ if (asked)
1553
+ warn(
1554
+ `Tailscale is ${stateText(ts)}: ${ts.state === null ? "start tailscaled" : "run `tailscale up`"}, then work setup --tailscale`,
1555
+ );
1556
+ return null;
1557
+ }
1558
+ const input = yes ? null : terminal();
1559
+ try {
1560
+ const refusal = serveRefusal(ts, port);
1561
+ if (refusal) {
1562
+ if (asked || input) warn(refusal);
1563
+ return null;
1564
+ }
1565
+ const target = serveTarget(port);
1566
+ const command = `tailscale serve --bg ${target}`;
1567
+ if (!asked) {
1568
+ if (!input) {
1569
+ log(
1570
+ `Tailscale is logged in: \`work setup --tailscale\` serves work at https://${ts.name}/ on your tailnet`,
1571
+ );
1572
+ return null;
1573
+ }
1574
+ console.log(`
1575
+ Tailscale is logged in here as ${ts.name}.
1576
+ \`${command}\` puts work at https://${ts.name}/
1577
+ for your devices on the tailnet (only them: never Funnel), proxied by the
1578
+ local Tailscale daemon to port ${port}.${ts.https === false ? "\nHTTPS certificates are off on your tailnet: tailscale prints a link that\nturns them on." : ""}`);
1579
+ if (!(await confirm(input, `Run \`${command}\`? [y/N] `))) {
1580
+ log(
1581
+ "left Tailscale as it was (work setup --tailscale serves work later; --no-tailscale skips this question)",
1582
+ );
1583
+ return null;
1584
+ }
1585
+ } else if (ts.https === false && !input) {
1586
+ // tailscale serve would wait, unattended, for someone to follow the
1587
+ // link it prints.
1588
+ warn(
1589
+ "HTTPS certificates are off on your tailnet: turn them on (https://login.tailscale.com/admin/dns), then run work setup --tailscale",
1590
+ );
1591
+ return null;
1592
+ }
1593
+ log(command);
1594
+ spawnSync(which("tailscale"), ["serve", "--bg", target], {
1595
+ stdio: "inherit",
1596
+ });
1597
+ } finally {
1598
+ if (input && input !== process.stdin) input.destroy();
1599
+ }
1600
+ // Its exit status is no proof: it exits 0 having only printed how to
1601
+ // turn HTTPS on, when it does not wait for that.
1602
+ const after = await tailscaleProbe({ port, allowedHosts: "" });
1603
+ if (!after.serve) {
1604
+ warn(
1605
+ "tailscale serve did not serve work (see above); once it does, run work setup again",
1606
+ );
1607
+ return null;
1608
+ }
1609
+ return allowTailnet(after.serve) ? servedAt(after.serve.url) : null;
1610
+ }
1611
+
1612
+ /**
1613
+ * A rule in the host firewall for direct connections' UDP port (and the
1614
+ * app's TCP port too when the service listens beyond this machine), named
1615
+ * `work` so it is found and removed as one: a firewalld service enabled in
1616
+ * the active zone, or ufw rules with that comment (lib/setup/firewall.mjs);
1617
+ * on macOS, whose firewall admits programs, the service's node let in
1618
+ * (`serviceFirewall`; "Block all incoming connections" is only said).
1619
+ * Added with sudo, and only on an explicit yes, the prompt's or
1620
+ * `--firewall`: `--yes` is how the installer, `work update` and the app's
1621
+ * own update run setup unattended, and none of them asked for it. What it
1622
+ * runs is printed as it runs.
1623
+ */
1624
+ async function firewall() {
1625
+ if (args.includes("--no-firewall")) return;
1626
+ const asked = args.includes("--firewall");
1627
+ const env = serviceEnv();
1628
+ const f = await serviceFirewall();
1629
+ if (!f) {
1630
+ if (asked && env.WORK_RTC !== "off")
1631
+ log("no active host firewall: nothing to add");
1632
+ return;
1633
+ }
1634
+ if (f.admits) {
1635
+ if (asked) log(`${f.what} already admits UDP ${f.port}`);
1636
+ return;
1637
+ }
1638
+ const drops = `${f.what} ${f.exact ? "drops" : "likely drops"} UDP ${f.port}${f.why ? ` (${f.why})` : ""}, which direct connections from your other devices use`;
1639
+ if (!f.plan.length) {
1640
+ if (asked || !yes) warn(`${drops}: ${f.fix}`);
1641
+ return;
1642
+ }
1643
+ // macOS's firewall admits a program: the service's node, as found.
1644
+ const adds =
1645
+ f.kind === "macos" ? `let ${f.node} in` : "add a rule named `work`";
1646
+ if (f.kind === "macos" && f.nodeFrom === "shell")
1647
+ warn(
1648
+ `neither a running service nor its definition said which node it runs: this is for this shell's, ${f.node}`,
1649
+ );
1650
+ const input = yes || asked ? null : terminal();
1651
+ try {
1652
+ if (!asked) {
1653
+ if (!input) {
1654
+ log(`${drops}: \`work setup --firewall\` can ${adds}`);
1655
+ return;
1656
+ }
1657
+ console.log(`
1658
+ ${drops}.
1659
+ \`work setup\` can ${adds} for it, with sudo:
1660
+ ${f.plan.map((cmd) => cmd.join(" ")).join("\n ")}`);
1661
+ if (!(await confirm(input, "Add it? [y/N] "))) {
1662
+ log(
1663
+ "left the firewall as it was (work setup --firewall adds it later; --no-firewall skips this question)",
1664
+ );
1665
+ return;
1666
+ }
1667
+ }
1668
+ } finally {
1669
+ if (input && input !== process.stdin) input.destroy();
1670
+ }
1671
+ const applied = applyPlan(f.plan, log);
1672
+ if (!applied.ok) {
1673
+ warn(
1674
+ `\`${applied.failed?.join(" ")}\` failed, so the rule may be part-made: run the rest of\n ${f.plan.map((c) => c.join(" ")).join("\n ")}`,
1675
+ );
1676
+ return;
1677
+ }
1678
+ const after = await serviceFirewall({ fresh: true });
1679
+ if (after?.admits) log(`${after.what} admits UDP ${after.port}`);
1680
+ else warn(`${f.what} still looks shut to UDP ${f.port}: see work doctor`);
1681
+ }
1682
+
1683
+ function servedAt(url) {
1684
+ log(`Tailscale serves work at ${url} (tailnet only)`);
1685
+ return url;
1686
+ }
1687
+
1688
+ /** Why `tailscale serve --bg <port>` is not run here, or null. */
1689
+ function serveRefusal(ts, port) {
1690
+ if (!ts.serveCli)
1691
+ return `tailscale ${ts.version || "(unknown version)"} predates \`tailscale serve --bg <port>\` (1.52): update Tailscale, then run work setup --tailscale`;
1692
+ // It would replace whatever serves the name's root now.
1693
+ if (ts.rootTaken)
1694
+ return `https://${ts.name}/ already serves ${ts.rootTaken}, which work leaves alone. To serve work beside it: tailscale serve --bg --https=8443 ${port}, then work setup`;
1695
+ if (!serveTarget(port))
1696
+ return `work listens on ${serviceEnv().WORK_LISTEN} alone, and tailscale serve proxies to loopback: WORK_LISTEN= work setup --tailscale`;
1697
+ return null;
1698
+ }
1699
+
1700
+ /** What `tailscale serve` proxies to: work's port on loopback, where
1701
+ * requests are this machine's (server/access.ts). Null when work listens
1702
+ * on one other address only, which loopback does not reach. */
1703
+ function serveTarget(port) {
1704
+ const listen = serviceEnv().WORK_LISTEN;
1705
+ if (listen === "::1") return `http://[::1]:${port}`;
1706
+ return ["127.0.0.1", "0.0.0.0", "::", "*"].includes(listen)
1707
+ ? String(port)
1708
+ : null;
1709
+ }
1710
+
1711
+ /** Put the name tailscale serve serves work under in WORK_ALLOWED_HOSTS,
1712
+ * for installService() to write — or, when Funnel publishes that name to
1713
+ * the internet, take it out, so work refuses the internet there. False
1714
+ * then. */
1715
+ function allowTailnet(serve) {
1716
+ const current = serviceEnv().WORK_ALLOWED_HOSTS || "";
1717
+ const next = allowHost(current, serve.host, !serve.funnel);
1718
+ if (next !== current) {
1719
+ process.env.WORK_ALLOWED_HOSTS = next;
1720
+ log(
1721
+ `${serve.funnel ? "refusing" : "allowing"} ${serve.host} (WORK_ALLOWED_HOSTS=${next})`,
1722
+ );
1723
+ }
1724
+ if (serve.funnel) {
1725
+ warn(
1726
+ `Tailscale Funnel publishes ${serve.url} to the internet, and work has no login, so work refuses that name. Turn Funnel off (tailscale funnel --https=${new URL(serve.url).port || 443} off), then run work setup again`,
1727
+ );
1728
+ return false;
1729
+ }
1730
+ return true;
1731
+ }
1732
+
1733
+ /** Where to ask the user something: stdin when it is a terminal, else the
1734
+ * controlling terminal, since the curl installer pipes its script into the
1735
+ * shell this runs under. Null with no terminal at all. */
1736
+ function terminal() {
1737
+ if (process.stdin.isTTY) return process.stdin;
1738
+ try {
1739
+ return new tty.ReadStream(fs.openSync("/dev/tty", "r"));
1740
+ } catch {
1741
+ return null;
1742
+ }
1743
+ }
1744
+
1745
+ /** A yes/no question, no unless answered yes. Line-buffered by the
1746
+ * terminal itself, so Ctrl-C stops setup as it would anywhere else. */
1747
+ async function confirm(input, question) {
1748
+ const rl = readline.createInterface({
1749
+ input,
1750
+ output: process.stdout,
1751
+ terminal: false,
1752
+ });
1753
+ try {
1754
+ return /^y(es)?$/i.test((await rl.question(question)).trim());
1755
+ } finally {
1756
+ rl.close();
1757
+ }
1319
1758
  }