@dylanpiercey/work 0.0.120 → 0.0.121

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 (262) hide show
  1. package/README.md +35 -8
  2. package/bin/work.mjs +380 -41
  3. package/dist/_B1lVb01g.js +2 -0
  4. package/dist/_Bjh5JEm7.js +2 -0
  5. package/dist/_CfwTkTbL.js +352 -0
  6. package/dist/{_DEBFGwto.js → _CivNK9fo.js} +22 -20
  7. package/dist/{_DE2RUdbF.js → _Cr71Xd0d.js} +8 -3
  8. package/dist/_DJyHc1w-.js +1739 -0
  9. package/dist/{_BZhGz-eY.js → _DnaZVHJ7.js} +97 -96
  10. package/dist/_Dpa8WFuv.js +2 -0
  11. package/dist/index.mjs +4183 -4191
  12. package/dist/public/assets/{_BrTLGsp82.js → _-zdUyUYM2.js} +1 -1
  13. package/dist/public/assets/_-zdUyUYM2.js.br +0 -0
  14. package/dist/public/assets/{_Cvly93xo2.js → _0L2K8sFq2.js} +1 -1
  15. package/dist/public/assets/_0L2K8sFq2.js.br +0 -0
  16. package/dist/public/assets/{_CQ-tqbUq.js → _42jdam49.js} +1 -1
  17. package/dist/public/assets/_42jdam49.js.br +0 -0
  18. package/dist/public/assets/{_BEOZghNJ2.js → _9dJ8BnyZ2.js} +1 -1
  19. package/dist/public/assets/_9dJ8BnyZ2.js.br +0 -0
  20. package/dist/public/assets/{_zJkCtRhb.js → _B03G1Nvs.js} +1 -1
  21. package/dist/public/assets/_B03G1Nvs.js.br +0 -0
  22. package/dist/public/assets/{_BLgtdDQm.js → _B0dZ7_qK.js} +1 -1
  23. package/dist/public/assets/_B0dZ7_qK.js.br +0 -0
  24. package/dist/public/assets/{_DdnCyOPs.js → _B1VuWiKK.js} +1 -1
  25. package/dist/public/assets/_B1VuWiKK.js.br +0 -0
  26. package/dist/public/assets/{_CwtAB8Ju2.js → _B5nrQZIq2.js} +1 -1
  27. package/dist/public/assets/_B5nrQZIq2.js.br +0 -0
  28. package/dist/public/assets/{_B_MP9UES.js → _B8ZQ8inF.js} +1 -1
  29. package/dist/public/assets/_B8ZQ8inF.js.br +0 -0
  30. package/dist/public/assets/{_DhLWIgCs.js → _B9e51G1K.js} +1 -1
  31. package/dist/public/assets/_B9e51G1K.js.br +0 -0
  32. package/dist/public/assets/{_h6HS-Zb_2.js → _BH015W2i2.js} +1 -1
  33. package/dist/public/assets/_BH015W2i2.js.br +0 -0
  34. package/dist/public/assets/{_Brws7C1N.js → _BHjcfVXB.js} +6 -6
  35. package/dist/public/assets/_BHjcfVXB.js.br +0 -0
  36. package/dist/public/assets/{_BfZMmKaW.js → _BNIilCsC.js} +1 -1
  37. package/dist/public/assets/_BNIilCsC.js.br +0 -0
  38. package/dist/public/assets/{_CihMY3_B2.js → _BPgL37mi2.js} +1 -1
  39. package/dist/public/assets/_BPgL37mi2.js.br +0 -0
  40. package/dist/public/assets/{_BDRc66NB2.js → _BR3D4nzj2.js} +1 -1
  41. package/dist/public/assets/_BR3D4nzj2.js.br +0 -0
  42. package/dist/public/assets/{_BoXfCUcF.js → _BRSJwfZ3.js} +1 -1
  43. package/dist/public/assets/{_BbE6iGBd.js → _BSFENb4l.js} +1 -1
  44. package/dist/public/assets/_BSFENb4l.js.br +0 -0
  45. package/dist/public/assets/{_DBXfsrRd.js → _BSWpz0dK.js} +1 -1
  46. package/dist/public/assets/_BSWpz0dK.js.br +0 -0
  47. package/dist/public/assets/{_DZgx_rAt.js → _BUOWar32.js} +1 -1
  48. package/dist/public/assets/_BUOWar32.js.br +0 -0
  49. package/dist/public/assets/{_Ces5jihM.js → _BZWlvMSJ.js} +1 -1
  50. package/dist/public/assets/_BZWlvMSJ.js.br +0 -0
  51. package/dist/public/assets/{_F8phZWNM2.js → _B_dMglA92.js} +1 -1
  52. package/dist/public/assets/_B_dMglA92.js.br +0 -0
  53. package/dist/public/assets/{_N0Ea8Q_R.js → _Beb6Lb1w.js} +1 -1
  54. package/dist/public/assets/_Beb6Lb1w.js.br +0 -0
  55. package/dist/public/assets/{_BUmLRB7c2.js → _BjIMoOV-2.js} +1 -1
  56. package/dist/public/assets/_BjIMoOV-2.js.br +0 -0
  57. package/dist/public/assets/{_Cv-4N4xQ.js → _BlLuOKRT.js} +1 -1
  58. package/dist/public/assets/_BlLuOKRT.js.br +0 -0
  59. package/dist/public/assets/{_BilqQTfw.js → _BlXwius8.js} +1 -1
  60. package/dist/public/assets/_BlXwius8.js.br +0 -0
  61. package/dist/public/assets/{_DdYfAoJz.js → _Bmt76eWT.js} +1 -1
  62. package/dist/public/assets/_Bmt76eWT.js.br +0 -0
  63. package/dist/public/assets/{_BaiP7ogH.js → _BoS5LU5l.js} +1 -1
  64. package/dist/public/assets/_BoS5LU5l.js.br +0 -0
  65. package/dist/public/assets/{_uJTU0UZd2.js → _BsNKabYu2.js} +1 -1
  66. package/dist/public/assets/_BsNKabYu2.js.br +0 -0
  67. package/dist/public/assets/{_D2be5yUI.js → _C0OXw3Vy.js} +1 -1
  68. package/dist/public/assets/_C0OXw3Vy.js.br +0 -0
  69. package/dist/public/assets/{_CrPK-ZOH2.js → _CBUJNdeq2.js} +1 -1
  70. package/dist/public/assets/_CBUJNdeq2.js.br +0 -0
  71. package/dist/public/assets/{_DfpMH3So2.js → _CCNhM32c2.js} +1 -1
  72. package/dist/public/assets/_CCNhM32c2.js.br +0 -0
  73. package/dist/public/assets/{_D11ZFURg.js → _CFBaZNUT.js} +1 -1
  74. package/dist/public/assets/_CFBaZNUT.js.br +0 -0
  75. package/dist/public/assets/{_BGHYY00K2.js → _CJHqzl--2.js} +1 -1
  76. package/dist/public/assets/_CJHqzl--2.js.br +0 -0
  77. package/dist/public/assets/{_BHgE7f-_2.js → _CMGgQ-4H2.js} +1 -1
  78. package/dist/public/assets/_CMGgQ-4H2.js.br +0 -0
  79. package/dist/public/assets/{_BzULaZGs.js → _CPX0D-my.js} +1 -1
  80. package/dist/public/assets/_CPX0D-my.js.br +0 -0
  81. package/dist/public/assets/{_CeECfdVo2.js → _CRuHDn_O2.js} +1 -1
  82. package/dist/public/assets/_CRuHDn_O2.js.br +0 -0
  83. package/dist/public/assets/{_p1VLQxPw.js → _CUOao_Hj.js} +1 -1
  84. package/dist/public/assets/_CUOao_Hj.js.br +0 -0
  85. package/dist/public/assets/{_CIio55Q2.js → _CUVw5V4z.js} +1 -1
  86. package/dist/public/assets/_CUVw5V4z.js.br +0 -0
  87. package/dist/public/assets/{_CYLlpjQc.js → _CYMGOhCb.js} +1 -1
  88. package/dist/public/assets/_CYMGOhCb.js.br +0 -0
  89. package/dist/public/assets/{_BtmFYBHk.js → _CZfap_Dj.js} +1 -1
  90. package/dist/public/assets/_CZfap_Dj.js.br +0 -0
  91. package/dist/public/assets/{_DRKLaQtq.js → _CbNmSe5k.js} +1 -1
  92. package/dist/public/assets/_CbNmSe5k.js.br +0 -0
  93. package/dist/public/assets/{_8hF31A2G.js → _CdtuhZSc.js} +1 -1
  94. package/dist/public/assets/_CdtuhZSc.js.br +0 -0
  95. package/dist/public/assets/{_BPcKgx9L.js → _ClhApln3.js} +1 -1
  96. package/dist/public/assets/_ClhApln3.js.br +0 -0
  97. package/dist/public/assets/{_QuFTKxMO2.js → _CvA5IOAW2.js} +1 -1
  98. package/dist/public/assets/_CvA5IOAW2.js.br +0 -0
  99. package/dist/public/assets/{_z3QzE4ht2.js → _CwZYmUdt2.js} +1 -1
  100. package/dist/public/assets/{_CG2uZvAG.js → _CxrcWZPE.js} +1 -1
  101. package/dist/public/assets/_CxrcWZPE.js.br +0 -0
  102. package/dist/public/assets/{_Du-i6H8r.js → _D-KjGTSW.js} +24 -21
  103. package/dist/public/assets/_D-KjGTSW.js.br +0 -0
  104. package/dist/public/assets/{_Ca811EeR.js → _D4u4SpiK.js} +1 -1
  105. package/dist/public/assets/_D4u4SpiK.js.br +0 -0
  106. package/dist/public/assets/{_DiaYv9e1.js → _D6CjH3JN.js} +1 -1
  107. package/dist/public/assets/_D6CjH3JN.js.br +0 -0
  108. package/dist/public/assets/{_BCKhXQct2.js → _D7cQLLGK2.js} +1 -1
  109. package/dist/public/assets/_D7cQLLGK2.js.br +0 -0
  110. package/dist/public/assets/{_DDag_nXJ.js → _DBoABI85.js} +1 -1
  111. package/dist/public/assets/_DBoABI85.js.br +0 -0
  112. package/dist/public/assets/{_CzMjLSTK.js → _DNcd1xtp.js} +1 -1
  113. package/dist/public/assets/_DNcd1xtp.js.br +0 -0
  114. package/dist/public/assets/{_BEPt7EWk.js → _DQGTProt.js} +1 -1
  115. package/dist/public/assets/_DQGTProt.js.br +0 -0
  116. package/dist/public/assets/{_D8lg2J6H.js → _DSIH6_Li.js} +1 -1
  117. package/dist/public/assets/_DSIH6_Li.js.br +0 -0
  118. package/dist/public/assets/{_cS3NYZ_K2.js → _DXHf4WHs2.js} +1 -1
  119. package/dist/public/assets/_DXHf4WHs2.js.br +0 -0
  120. package/dist/public/assets/{_0_ngOEbL.js → _D_qDhyn2.js} +1 -1
  121. package/dist/public/assets/_D_qDhyn2.js.br +0 -0
  122. package/dist/public/assets/{_BFgJoTOo.js → _DcMfCkmd.js} +1 -1
  123. package/dist/public/assets/_DcMfCkmd.js.br +0 -0
  124. package/dist/public/assets/{_CkBv_F0f.js → _DgDEdxEu.js} +1 -1
  125. package/dist/public/assets/_DgDEdxEu.js.br +0 -0
  126. package/dist/public/assets/{_dBVoJwVC.js → _DlhDim1D.js} +1 -1
  127. package/dist/public/assets/_DlhDim1D.js.br +0 -0
  128. package/dist/public/assets/{_-3OmHyNU.js → _DqLib98x.js} +1 -1
  129. package/dist/public/assets/_DqLib98x.js.br +0 -0
  130. package/dist/public/assets/{_fR2ilomn.js → _Ds0Qa0YX.js} +1 -1
  131. package/dist/public/assets/_Ds0Qa0YX.js.br +0 -0
  132. package/dist/public/assets/{_CwZCc-fE2.js → _DyUvL09k2.js} +1 -1
  133. package/dist/public/assets/_DyUvL09k2.js.br +0 -0
  134. package/dist/public/assets/{_D2xWjoK-2.js → _J6xlcydH2.js} +1 -1
  135. package/dist/public/assets/_J6xlcydH2.js.br +0 -0
  136. package/dist/public/assets/{_DkYHMy0q2.js → _MInulccf2.js} +1 -1
  137. package/dist/public/assets/{_GdcmEEuJ.js → _QMVFINxi.js} +1 -1
  138. package/dist/public/assets/_QMVFINxi.js.br +0 -0
  139. package/dist/public/assets/{_BqNoqWIY.js → _S9QQtt68.js} +1 -1
  140. package/dist/public/assets/_S9QQtt68.js.br +0 -0
  141. package/dist/public/assets/{_BuF6uiIm.js → _XCJLLGxV.js} +1 -1
  142. package/dist/public/assets/_XCJLLGxV.js.br +0 -0
  143. package/dist/public/assets/{_DWctbEWg.js → _Y-bTh0dW.js} +1 -1
  144. package/dist/public/assets/_Y-bTh0dW.js.br +0 -0
  145. package/dist/public/assets/{_CJ_9Hl5d.js → _Y8TOk1pR.js} +1 -1
  146. package/dist/public/assets/_Y8TOk1pR.js.br +0 -0
  147. package/dist/public/assets/{_cdbANa0O.js → _YetFhXkM.js} +1 -1
  148. package/dist/public/assets/_YetFhXkM.js.br +0 -0
  149. package/dist/public/assets/{_CeVHIZWy2.js → _YmJopjX92.js} +1 -1
  150. package/dist/public/assets/_YmJopjX92.js.br +0 -0
  151. package/dist/public/assets/{_wc-pkVQS.js → _mbzaOqik.js} +1 -1
  152. package/dist/public/assets/_mbzaOqik.js.br +0 -0
  153. package/dist/public/assets/{_FEbRnVo7.js → _rMcfapaa.js} +1 -1
  154. package/dist/public/assets/_rMcfapaa.js.br +0 -0
  155. package/dist/public/assets/{_BIsQjQ872.js → _rRtbbfKJ2.js} +1 -1
  156. package/dist/public/assets/_rRtbbfKJ2.js.br +0 -0
  157. package/dist/public/assets/{_D4V7vTBe.js → _sSWgnHeU.js} +1 -1
  158. package/dist/public/assets/_sSWgnHeU.js.br +0 -0
  159. package/dist/public/assets/{_B6bSumz1.js → _xOeYy_3v.js} +1 -1
  160. package/dist/public/assets/_xOeYy_3v.js.br +0 -0
  161. package/dist/public/assets/app-page-BWf0w6W9.css +1 -0
  162. package/dist/public/assets/app-page-BWf0w6W9.css.br +0 -0
  163. package/dist/public/assets/diff-view-BZaYYEDK.css +1 -0
  164. package/dist/public/assets/diff-view-BZaYYEDK.css.br +0 -0
  165. package/dist/public/assets/new-K-7z0qE8.js +1 -0
  166. package/dist/public/assets/settings-K-7z0qE8.js +1 -0
  167. package/dist/public/assets/w-K-7z0qE8.js +1 -0
  168. package/dist/public/assets/w._-D5d4klUv.js +1 -0
  169. package/dist/public/assets/w._-K-7z0qE8.js +1 -0
  170. package/lib/review/markdown.mjs +6 -2
  171. package/lib/setup/catalog.d.mts +3 -1
  172. package/lib/setup/catalog.mjs +11 -2
  173. package/lib/setup/doctor.mjs +96 -20
  174. package/lib/setup/install.d.mts +9 -3
  175. package/lib/setup/install.mjs +68 -39
  176. package/lib/share/manifest.d.mts +27 -0
  177. package/lib/share/manifest.mjs +144 -0
  178. package/package.json +6 -2
  179. package/scripts/migrate.mjs +101 -30
  180. package/dist/_BIDn_RW1.js +0 -2
  181. package/dist/public/assets/_-3OmHyNU.js.br +0 -0
  182. package/dist/public/assets/_0_ngOEbL.js.br +0 -0
  183. package/dist/public/assets/_8hF31A2G.js.br +0 -0
  184. package/dist/public/assets/_B6bSumz1.js.br +0 -0
  185. package/dist/public/assets/_BCKhXQct2.js.br +0 -0
  186. package/dist/public/assets/_BDRc66NB2.js.br +0 -0
  187. package/dist/public/assets/_BEOZghNJ2.js.br +0 -0
  188. package/dist/public/assets/_BEPt7EWk.js.br +0 -0
  189. package/dist/public/assets/_BFgJoTOo.js.br +0 -0
  190. package/dist/public/assets/_BGHYY00K2.js.br +0 -0
  191. package/dist/public/assets/_BHgE7f-_2.js.br +0 -0
  192. package/dist/public/assets/_BIsQjQ872.js.br +0 -0
  193. package/dist/public/assets/_BLgtdDQm.js.br +0 -0
  194. package/dist/public/assets/_BPcKgx9L.js.br +0 -0
  195. package/dist/public/assets/_BUmLRB7c2.js.br +0 -0
  196. package/dist/public/assets/_B_MP9UES.js.br +0 -0
  197. package/dist/public/assets/_BaiP7ogH.js.br +0 -0
  198. package/dist/public/assets/_BbE6iGBd.js.br +0 -0
  199. package/dist/public/assets/_BfZMmKaW.js.br +0 -0
  200. package/dist/public/assets/_BilqQTfw.js.br +0 -0
  201. package/dist/public/assets/_BqNoqWIY.js.br +0 -0
  202. package/dist/public/assets/_BrTLGsp82.js.br +0 -0
  203. package/dist/public/assets/_Brws7C1N.js.br +0 -0
  204. package/dist/public/assets/_BtmFYBHk.js.br +0 -0
  205. package/dist/public/assets/_BuF6uiIm.js.br +0 -0
  206. package/dist/public/assets/_BzULaZGs.js.br +0 -0
  207. package/dist/public/assets/_CG2uZvAG.js.br +0 -0
  208. package/dist/public/assets/_CIio55Q2.js.br +0 -0
  209. package/dist/public/assets/_CJ_9Hl5d.js.br +0 -0
  210. package/dist/public/assets/_CQ-tqbUq.js.br +0 -0
  211. package/dist/public/assets/_CYLlpjQc.js.br +0 -0
  212. package/dist/public/assets/_Ca811EeR.js.br +0 -0
  213. package/dist/public/assets/_CeECfdVo2.js.br +0 -0
  214. package/dist/public/assets/_CeVHIZWy2.js.br +0 -0
  215. package/dist/public/assets/_Ces5jihM.js.br +0 -7
  216. package/dist/public/assets/_CihMY3_B2.js.br +0 -0
  217. package/dist/public/assets/_CkBv_F0f.js.br +0 -0
  218. package/dist/public/assets/_CrPK-ZOH2.js.br +0 -0
  219. package/dist/public/assets/_Cv-4N4xQ.js.br +0 -0
  220. package/dist/public/assets/_Cvly93xo2.js.br +0 -0
  221. package/dist/public/assets/_CwZCc-fE2.js.br +0 -0
  222. package/dist/public/assets/_CwtAB8Ju2.js.br +0 -0
  223. package/dist/public/assets/_CzMjLSTK.js.br +0 -0
  224. package/dist/public/assets/_D11ZFURg.js.br +0 -0
  225. package/dist/public/assets/_D2be5yUI.js.br +0 -0
  226. package/dist/public/assets/_D2xWjoK-2.js.br +0 -0
  227. package/dist/public/assets/_D4V7vTBe.js.br +0 -0
  228. package/dist/public/assets/_D8lg2J6H.js.br +0 -0
  229. package/dist/public/assets/_DBXfsrRd.js.br +0 -0
  230. package/dist/public/assets/_DDag_nXJ.js.br +0 -0
  231. package/dist/public/assets/_DRKLaQtq.js.br +0 -0
  232. package/dist/public/assets/_DWctbEWg.js.br +0 -3
  233. package/dist/public/assets/_DZgx_rAt.js.br +0 -0
  234. package/dist/public/assets/_DdYfAoJz.js.br +0 -0
  235. package/dist/public/assets/_DdnCyOPs.js.br +0 -0
  236. package/dist/public/assets/_DfpMH3So2.js.br +0 -0
  237. package/dist/public/assets/_DhLWIgCs.js.br +0 -0
  238. package/dist/public/assets/_DiaYv9e1.js.br +0 -0
  239. package/dist/public/assets/_Du-i6H8r.js.br +0 -0
  240. package/dist/public/assets/_F8phZWNM2.js.br +0 -0
  241. package/dist/public/assets/_FEbRnVo7.js.br +0 -0
  242. package/dist/public/assets/_GdcmEEuJ.js.br +0 -0
  243. package/dist/public/assets/_N0Ea8Q_R.js.br +0 -0
  244. package/dist/public/assets/_QuFTKxMO2.js.br +0 -0
  245. package/dist/public/assets/_cS3NYZ_K2.js.br +0 -0
  246. package/dist/public/assets/_cdbANa0O.js.br +0 -0
  247. package/dist/public/assets/_dBVoJwVC.js.br +0 -0
  248. package/dist/public/assets/_fR2ilomn.js.br +0 -0
  249. package/dist/public/assets/_h6HS-Zb_2.js.br +0 -0
  250. package/dist/public/assets/_p1VLQxPw.js.br +0 -0
  251. package/dist/public/assets/_uJTU0UZd2.js.br +0 -0
  252. package/dist/public/assets/_wc-pkVQS.js.br +0 -0
  253. package/dist/public/assets/_zJkCtRhb.js.br +0 -0
  254. package/dist/public/assets/app-page-DgsAA8Om.css +0 -1
  255. package/dist/public/assets/app-page-DgsAA8Om.css.br +0 -0
  256. package/dist/public/assets/diff-view-BqwTpf5k.css +0 -1
  257. package/dist/public/assets/diff-view-BqwTpf5k.css.br +0 -0
  258. package/dist/public/assets/new-cBa61wMi.js +0 -1
  259. package/dist/public/assets/settings-cBa61wMi.js +0 -1
  260. package/dist/public/assets/w-cBa61wMi.js +0 -1
  261. package/dist/public/assets/w._-B9llVTHr.js +0 -1
  262. package/dist/public/assets/w._-cBa61wMi.js +0 -1
package/bin/work.mjs CHANGED
@@ -11,6 +11,10 @@
11
11
  // work repos [id|name] a workspace's repos with base branch and open PRs
12
12
  // work procs [id|name] a workspace's running processes and ports
13
13
  // work review [id|name] pending review comments as markdown (--json for rows)
14
+ // work export [file] write a backup manifest (workspaces, settings, skills, tool pins)
15
+ // work restore <file> bring a manifest's workspaces and machine setup back (--yes to run)
16
+ // work share [id|name] print a workspace's invite (paste into New → Join / work join)
17
+ // work join [file] join a workspace from an invite (file, or stdin)
14
18
  // work version print the installed version
15
19
  //
16
20
  // The read commands ask the running server (same data as the UI) and take
@@ -25,10 +29,13 @@ import { createRequire } from "node:module";
25
29
  import { doctor, formatDoctor, setupPath } from "../lib/setup/doctor.mjs";
26
30
  import { installTool } from "../lib/setup/install.mjs";
27
31
  import { commentMarkdown } from "../lib/review/markdown.mjs";
32
+ import { formatInvite, parseInvite } from "../lib/share/manifest.mjs";
28
33
  import { duration, pctText } from "../lib/usage/format.mjs";
29
34
 
30
35
  const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
31
- const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
36
+ const pkg = JSON.parse(
37
+ fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"),
38
+ );
32
39
  const distEntry = path.join(pkgRoot, "dist", "index.mjs");
33
40
  const appPort = process.env.WORK_APP_PORT || "7171";
34
41
 
@@ -48,7 +55,19 @@ const sh = (cmd, args, opts = {}) =>
48
55
  const [, , command = "help", ...args] = process.argv;
49
56
  const yes = args.includes("--yes") || args.includes("-y");
50
57
  const jsonOut = args.includes("--json");
51
- const positional = args.filter((a) => !a.startsWith("-"));
58
+ /** `--name <value>` style flags take the next arg; keep it out of the
59
+ * positionals so `work join --name x` still reads stdin. */
60
+ const VALUED = new Set(["--name"]);
61
+ const positional = args.filter(
62
+ (a, i) => a === "-" || (!a.startsWith("-") && !VALUED.has(args[i - 1])),
63
+ );
64
+ const flagValue = (flag) => {
65
+ const i = args.indexOf(flag);
66
+ if (i < 0) return undefined;
67
+ if (i + 1 >= args.length || args[i + 1].startsWith("-"))
68
+ die(`${flag} needs a value`);
69
+ return args[i + 1];
70
+ };
52
71
 
53
72
  switch (command) {
54
73
  case "start":
@@ -57,6 +76,16 @@ switch (command) {
57
76
  case "setup":
58
77
  await setup();
59
78
  break;
79
+ case "install-service":
80
+ // Service installation only — no doctor/fix, no migrate. Used by
81
+ // scripts/setup-prod.sh so npm and source installs share one
82
+ // definition; WORK_SERVICE_EXEC points it at a checkout's run-prod.sh.
83
+ if (installService()) {
84
+ log(`done — http://localhost:${serviceEnv().PORT}`);
85
+ } else {
86
+ process.exitCode = 1;
87
+ }
88
+ break;
60
89
  case "doctor":
61
90
  await doctorCmd();
62
91
  break;
@@ -82,6 +111,18 @@ switch (command) {
82
111
  case "review":
83
112
  await reviewCmd();
84
113
  break;
114
+ case "export":
115
+ await exportCmd();
116
+ break;
117
+ case "restore":
118
+ await restoreCmd();
119
+ break;
120
+ case "share":
121
+ await shareCmd();
122
+ break;
123
+ case "join":
124
+ await joinCmd();
125
+ break;
85
126
  case "version":
86
127
  case "--version":
87
128
  case "-v":
@@ -89,7 +130,7 @@ switch (command) {
89
130
  break;
90
131
  default:
91
132
  console.log(
92
- `work ${pkg.version}\n\nusage: work <setup|doctor|start|update|usage|workspaces|workspace|repos|procs|review|version> [--json]`,
133
+ `work ${pkg.version}\n\nusage: work <setup|install-service|doctor|start|update|usage|workspaces|workspace|repos|procs|review|export|restore|share|join|version> [--json]`,
93
134
  );
94
135
  }
95
136
 
@@ -116,7 +157,11 @@ async function usage() {
116
157
  const agents = data.agents || [];
117
158
  if (!agents.length) return console.log("no agent accounts found");
118
159
  for (const agent of agents) {
119
- const head = [agent.agent, agent.plan?.replace(/^default_/, "").replaceAll("_", " "), agent.account]
160
+ const head = [
161
+ agent.agent,
162
+ agent.plan?.replace(/^default_/, "").replaceAll("_", " "),
163
+ agent.account,
164
+ ]
120
165
  .filter(Boolean)
121
166
  .join(" · ");
122
167
  console.log(`\n${head}`);
@@ -143,7 +188,9 @@ async function usage() {
143
188
  async function apiGet(path) {
144
189
  const port = serviceEnv().PORT;
145
190
  try {
146
- const res = await fetch(`http://127.0.0.1:${port}${path}`, { signal: AbortSignal.timeout(30_000) });
191
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
192
+ signal: AbortSignal.timeout(30_000),
193
+ });
147
194
  if (!res.ok) die(`server answered ${res.status} for ${path}`);
148
195
  return await res.json();
149
196
  } catch (err) {
@@ -159,14 +206,19 @@ async function apiGet(path) {
159
206
  * a workspace, so no argument is the common case.
160
207
  */
161
208
  async function resolveWorkspace(arg) {
162
- const [list, status] = await Promise.all([apiGet("/api/workspaces"), apiGet("/api/status")]);
209
+ const [list, status] = await Promise.all([
210
+ apiGet("/api/workspaces"),
211
+ apiGet("/api/status"),
212
+ ]);
163
213
  let ws;
164
214
  if (arg) {
165
215
  ws = list.find((w) => w.id === arg || w.name === arg);
166
216
  if (!ws) die(`no workspace named ${arg}`);
167
217
  } else if (process.env.WORK_SESSION_ID) {
168
218
  const sid = process.env.WORK_SESSION_ID;
169
- ws = list.find((w) => (status[w.id]?.sessions || []).some((s) => s.id === sid));
219
+ ws = list.find((w) =>
220
+ (status[w.id]?.sessions || []).some((s) => s.id === sid),
221
+ );
170
222
  }
171
223
  if (!ws) {
172
224
  // Compare real paths on both sides — /home may be a symlink to /var/home.
@@ -183,8 +235,12 @@ async function resolveWorkspace(arg) {
183
235
  return cwd === root || cwd.startsWith(root + path.sep);
184
236
  });
185
237
  }
186
- if (!ws) die("not inside a workspace. Pass its name or id (see: work workspaces)");
187
- return { ws, status: status[ws.id] || { ports: [], repos: [], sessions: [] } };
238
+ if (!ws)
239
+ die("not inside a workspace. Pass its name or id (see: work workspaces)");
240
+ return {
241
+ ws,
242
+ status: status[ws.id] || { ports: [], repos: [], sessions: [] },
243
+ };
188
244
  }
189
245
 
190
246
  function pad(s, n) {
@@ -192,12 +248,17 @@ function pad(s, n) {
192
248
  }
193
249
 
194
250
  async function workspacesCmd() {
195
- const [list, status] = await Promise.all([apiGet("/api/workspaces"), apiGet("/api/status")]);
251
+ const [list, status] = await Promise.all([
252
+ apiGet("/api/workspaces"),
253
+ apiGet("/api/status"),
254
+ ]);
196
255
  const rows = list.map((w) => ({ ...w, ...(status[w.id] || {}) }));
197
256
  if (jsonOut) return console.log(JSON.stringify(rows, null, 2));
198
257
  if (!rows.length) return console.log("no workspaces. Create one in the app");
199
258
  const wName = Math.max(4, ...rows.map((w) => w.name.length));
200
- console.log(`${pad("name", wName)} ${pad("status", 8)} ${pad("branch", 28)} repos agents ports`);
259
+ console.log(
260
+ `${pad("name", wName)} ${pad("status", 8)} ${pad("branch", 28)} repos agents ports`,
261
+ );
201
262
  for (const w of rows) {
202
263
  const ports = (w.ports || []).map((p) => `:${p.port}`).join(" ");
203
264
  console.log(
@@ -207,15 +268,30 @@ async function workspacesCmd() {
207
268
  }
208
269
 
209
270
  function prLine(pr) {
210
- const state = pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : pr.isDraft ? "draft" : "open";
271
+ const state =
272
+ pr.state === "MERGED"
273
+ ? "merged"
274
+ : pr.state === "CLOSED"
275
+ ? "closed"
276
+ : pr.isDraft
277
+ ? "draft"
278
+ : "open";
211
279
  return `#${pr.number} ${state} ${pr.title} ${pr.url}`;
212
280
  }
213
281
 
214
282
  async function workspaceCmd() {
215
283
  const { ws, status } = await resolveWorkspace(positional[0]);
216
284
  const procs = await apiGet(`/api/workspaces/${ws.id}/procs`).catch(() => []);
217
- const [sessions] = await Promise.all([apiGet(`/api/workspaces/${ws.id}/sessions`).catch(() => [])]);
218
- const out = { ...ws, ports: status.ports, repos: mergeRepos(ws, status), sessions, procs };
285
+ const [sessions] = await Promise.all([
286
+ apiGet(`/api/workspaces/${ws.id}/sessions`).catch(() => []),
287
+ ]);
288
+ const out = {
289
+ ...ws,
290
+ ports: status.ports,
291
+ repos: mergeRepos(ws, status),
292
+ sessions,
293
+ procs,
294
+ };
219
295
  if (jsonOut) return console.log(JSON.stringify(out, null, 2));
220
296
  console.log(`${ws.name} (${ws.status})`);
221
297
  console.log(` path: ${ws.display_path || ws.path}`);
@@ -234,14 +310,20 @@ async function workspaceCmd() {
234
310
  }
235
311
  if (procs.length) {
236
312
  console.log(` running:`);
237
- for (const p of procs) console.log(` ${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`);
313
+ for (const p of procs)
314
+ console.log(
315
+ ` ${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`,
316
+ );
238
317
  }
239
318
  }
240
319
 
241
320
  /** Workspace repos joined with the status snapshot's PRs per repo. */
242
321
  function mergeRepos(ws, status) {
243
322
  const byName = new Map((status.repos || []).map((r) => [r.full_name, r]));
244
- return ws.repos.map((r) => ({ ...r, prs: byName.get(r.full_name)?.prs || [] }));
323
+ return ws.repos.map((r) => ({
324
+ ...r,
325
+ prs: byName.get(r.full_name)?.prs || [],
326
+ }));
245
327
  }
246
328
 
247
329
  async function reposCmd() {
@@ -249,7 +331,9 @@ async function reposCmd() {
249
331
  const repos = mergeRepos(ws, status);
250
332
  if (jsonOut) return console.log(JSON.stringify(repos, null, 2));
251
333
  for (const r of repos) {
252
- console.log(`${r.worktree}/ ${r.full_name} base:${r.base_branch}${r.default_branch !== r.base_branch ? ` default:${r.default_branch}` : ""}`);
334
+ console.log(
335
+ `${r.worktree}/ ${r.full_name} base:${r.base_branch}${r.default_branch !== r.base_branch ? ` default:${r.default_branch}` : ""}`,
336
+ );
253
337
  for (const pr of r.prs) console.log(` ${prLine(pr)}`);
254
338
  }
255
339
  }
@@ -259,7 +343,10 @@ async function procsCmd() {
259
343
  const procs = await apiGet(`/api/workspaces/${ws.id}/procs`);
260
344
  if (jsonOut) return console.log(JSON.stringify(procs, null, 2));
261
345
  if (!procs.length) return console.log("nothing running");
262
- for (const p of procs) console.log(`${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`);
346
+ for (const p of procs)
347
+ console.log(
348
+ `${pad(p.pid, 7)} ${p.cmd}${p.ports?.length ? " " + p.ports.map((x) => `:${x}`).join(" ") : ""}`,
349
+ );
263
350
  }
264
351
 
265
352
  /** Comments left on the review page but not yet sent: an agent can pull
@@ -273,6 +360,192 @@ async function reviewCmd() {
273
360
  console.log(commentMarkdown(comments, null));
274
361
  }
275
362
 
363
+ async function apiPost(path, body) {
364
+ const port = serviceEnv().PORT;
365
+ let res;
366
+ try {
367
+ res = await fetch(`http://127.0.0.1:${port}${path}`, {
368
+ method: "POST",
369
+ headers: { "content-type": "application/json" },
370
+ body: body === undefined ? undefined : JSON.stringify(body),
371
+ signal: AbortSignal.timeout(15 * 60_000),
372
+ });
373
+ } catch {
374
+ die(`no work server on port ${port}. Start one with: work start`);
375
+ }
376
+ const data = await res.json().catch(() => ({}));
377
+ if (!res.ok || data.error)
378
+ die(data.error || `server answered ${res.status} for ${path}`);
379
+ return data;
380
+ }
381
+
382
+ /** One line per workspace/repo of what the file will not carry. */
383
+ function printPreflight(found) {
384
+ for (const w of found.workspaces) {
385
+ console.log(w.name);
386
+ for (const r of w.repos) {
387
+ const parts = [];
388
+ for (const b of r.unpushed)
389
+ parts.push(`${b.count} unpushed on ${b.branch}`);
390
+ if (r.modified.length) parts.push(`${r.modified.length} modified`);
391
+ if (r.untracked.length) parts.push(`${r.untracked.length} untracked`);
392
+ if (r.ignored.length) parts.push(`${r.ignored.length} ignored`);
393
+ if (r.more) parts.push(`+${r.more} more`);
394
+ console.log(` ${r.worktree}/ ${parts.join(", ")}`);
395
+ for (const f of r.modified) console.log(` M ${f}`);
396
+ for (const f of r.untracked) console.log(` ? ${f}`);
397
+ for (const f of r.ignored) console.log(` ! ${f}`);
398
+ }
399
+ }
400
+ }
401
+
402
+ /** Backup manifest to a file. Same pre-flight as Settings → Backup: work
403
+ * the file cannot carry is listed, and needs --yes (or --push, which
404
+ * pushes every workspace branch first) to go ahead. */
405
+ async function exportCmd() {
406
+ const found = await apiPost(
407
+ args.includes("--push") ? "/api/backup/push" : "/api/backup/preflight",
408
+ );
409
+ const c = found.contents;
410
+ if (c) {
411
+ log(
412
+ `in the backup: ${c.workspaces} workspaces` +
413
+ (c.archived ? ` (${c.archived} archived)` : "") +
414
+ `, ${c.tools} tool pins, ${c.skills} skills, ${c.agents} agents`,
415
+ );
416
+ }
417
+ if (found.workspaces.length) {
418
+ console.log("not in the backup:");
419
+ printPreflight(found);
420
+ // As in Settings: after --push, leftover uncommitted files are noted,
421
+ // not a stop; only unpushed commits still are.
422
+ if (!yes && (found.unpushed || !args.includes("--push"))) {
423
+ console.log("");
424
+ die(
425
+ found.unpushed
426
+ ? "unpushed commits: re-run with --push to push them first, or --yes to export without"
427
+ : "uncommitted files: re-run with --yes to export without them",
428
+ );
429
+ }
430
+ }
431
+ const port = serviceEnv().PORT;
432
+ const res = await fetch(`http://127.0.0.1:${port}/api/backup/export`);
433
+ if (!res.ok) die(`server answered ${res.status} for /api/backup/export`);
434
+ const name =
435
+ positional[0] ||
436
+ /filename="([^"]+)"/.exec(
437
+ res.headers.get("content-disposition") || "",
438
+ )?.[1] ||
439
+ "work-backup.json";
440
+ fs.writeFileSync(name, await res.text());
441
+ log(`wrote ${name}`);
442
+ }
443
+
444
+ function readManifest(file) {
445
+ if (!file) die("usage: work restore <file> [--yes]");
446
+ let manifest;
447
+ try {
448
+ manifest = JSON.parse(fs.readFileSync(file, "utf8"));
449
+ } catch {
450
+ die(`${file} is not a work backup file`);
451
+ }
452
+ return manifest;
453
+ }
454
+
455
+ /** Preview, then (with --yes) restore everything the file names that this
456
+ * machine does not have. Workspaces already here are skipped. */
457
+ async function restoreCmd() {
458
+ const manifest = readManifest(positional[0]);
459
+ const preview = await apiPost("/api/backup/preview", manifest);
460
+ if (jsonOut && !yes) return console.log(JSON.stringify(preview, null, 2));
461
+ const pick = (list) => list.filter((x) => !x.installed);
462
+ const say = jsonOut ? () => {} : console.log;
463
+ say(`from ${preview.from.host} on ${preview.from.exportedAt.slice(0, 10)}`);
464
+ const workspaces = preview.workspaces.filter(
465
+ (w) => !w.exists && !w.unknownHost,
466
+ );
467
+ for (const w of preview.workspaces) {
468
+ const note = w.exists
469
+ ? "already here"
470
+ : w.unknownHost
471
+ ? `unknown host ${w.unknownHost}`
472
+ : w.status;
473
+ say(` ${pad(w.name, 24)} ${pad(note, 14)} ${w.repos.join(", ")}`);
474
+ }
475
+ const tools = pick(preview.tools).map((t) => t.name);
476
+ const skills = pick(preview.skills).map((s) => s.id);
477
+ const agents = pick(preview.agents).map((a) => a.label);
478
+ const hosts = preview.hosts.filter((h) => !h.known).map((h) => h.host);
479
+ if (tools.length) say(` tools to install: ${tools.join(", ")}`);
480
+ if (skills.length) say(` skills to add: ${skills.join(", ")}`);
481
+ if (agents.length) say(` agents to install: ${agents.join(", ")}`);
482
+ if (hosts.length) say(` hosts to add: ${hosts.join(", ")}`);
483
+ if (preview.settings) say(` settings: ${preview.settings}`);
484
+ if (!yes) {
485
+ say("");
486
+ log("re-run with --yes to restore");
487
+ return;
488
+ }
489
+ const result = await apiPost("/api/backup/restore", {
490
+ manifest,
491
+ selection: {
492
+ workspaces: workspaces.map((w) => w.id),
493
+ tools: true,
494
+ skills: true,
495
+ agents: true,
496
+ settings: true,
497
+ },
498
+ });
499
+ if (jsonOut) return console.log(JSON.stringify(result, null, 2));
500
+ log(
501
+ `restored ${result.restored.length} workspace(s)${result.skipped.length ? `, skipped ${result.skipped.length}` : ""} — repos clone in the background`,
502
+ );
503
+ }
504
+
505
+ /** The invite for a workspace — what New → Join and `work join` take. A
506
+ * joined workspace shares the branch it is a lane of, not the lane. */
507
+ async function shareCmd() {
508
+ const { ws } = await resolveWorkspace(positional[0]);
509
+ const invite = {
510
+ name: ws.name,
511
+ branch: ws.origin_branch || ws.branch,
512
+ repos: ws.repos.map((r) => r.full_name),
513
+ };
514
+ if (jsonOut) return console.log(JSON.stringify(invite, null, 2));
515
+ process.stdout.write(formatInvite(invite));
516
+ }
517
+
518
+ /** Join from an invite: a file, or stdin (`work share | ssh box work join`). */
519
+ async function joinCmd() {
520
+ const file = positional[0];
521
+ let text;
522
+ try {
523
+ text = fs.readFileSync(file && file !== "-" ? file : 0, "utf8");
524
+ } catch {
525
+ die(
526
+ file
527
+ ? `cannot read ${file}`
528
+ : "usage: work join [file] (or pipe an invite in)",
529
+ );
530
+ }
531
+ let invite;
532
+ try {
533
+ invite = parseInvite(text);
534
+ } catch (err) {
535
+ die(`not an invite: ${err.message}`);
536
+ }
537
+ const name = flagValue("--name") ?? invite.name;
538
+ const ws = await apiPost("/api/workspaces", {
539
+ name,
540
+ repos: invite.repos.map((fullName) => ({ fullName })),
541
+ join: invite.branch,
542
+ });
543
+ if (jsonOut) return console.log(JSON.stringify(ws, null, 2));
544
+ log(
545
+ `joined ${invite.branch} as ${ws.branch} in ${ws.display_path || ws.path}`,
546
+ );
547
+ }
548
+
276
549
  /** "4h" / "6d" — how long until an ISO timestamp. A reset already in the past
277
550
  * means the window turned over and this row hasn't refreshed since, so there
278
551
  * is no honest countdown to print. */
@@ -302,9 +575,18 @@ async function setup() {
302
575
  if (running) log(`done — http://localhost:${port}`);
303
576
  else {
304
577
  // A fresh install's shell may not have mise on PATH yet — name the shim.
305
- const shim = path.join(os.homedir(), ".local", "share", "mise", "shims", "work");
578
+ const shim = path.join(
579
+ os.homedir(),
580
+ ".local",
581
+ "share",
582
+ "mise",
583
+ "shims",
584
+ "work",
585
+ );
306
586
  const cmd = fs.existsSync(shim) ? shim : "work";
307
- log(`done — nothing is serving yet: run \`${cmd} start\` (foreground) and open http://localhost:${port}`);
587
+ log(
588
+ `done — nothing is serving yet: run \`${cmd} start\` (foreground) and open http://localhost:${port}`,
589
+ );
308
590
  }
309
591
  }
310
592
 
@@ -336,7 +618,10 @@ async function doctorCmd() {
336
618
  async function fix(d) {
337
619
  for (const tool of d.tools.filter((t) => !t.installed)) {
338
620
  if (!tool.installable) {
339
- if (tool.required) die(`${tool.label} not found. Install it and re-run (the curl installer does this automatically)`);
621
+ if (tool.required)
622
+ die(
623
+ `${tool.label} not found. Install it and re-run (the curl installer does this automatically)`,
624
+ );
340
625
  continue;
341
626
  }
342
627
  log(`installing ${tool.label} via mise`);
@@ -350,13 +635,19 @@ async function fix(d) {
350
635
  // GitHub sign-in is a UI job (Settings → GitHub runs gh's device flow and
351
636
  // wires git credentials); here we only say so. GH_TOKEN still works headless.
352
637
  if (!d.github.some((h) => h.authed)) {
353
- if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) log("gh will use GH_TOKEN from the environment");
354
- else warn("GitHub is not signed in. Open the app and use Settings → GitHub → Sign in");
638
+ if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN)
639
+ log("gh will use GH_TOKEN from the environment");
640
+ else
641
+ warn(
642
+ "GitHub is not signed in. Open the app and use Settings → GitHub → Sign in",
643
+ );
355
644
  }
356
645
  // Agent CLIs are installed from the app (Settings → Agents), where each
357
646
  // one is a click and the picker updates immediately — no prompt here.
358
647
  if (!d.agents.some((a) => a.installed)) {
359
- warn("no coding agent CLI found. Install one from Settings → Agents in the app");
648
+ warn(
649
+ "no coding agent CLI found. Install one from Settings → Agents in the app",
650
+ );
360
651
  }
361
652
  }
362
653
 
@@ -420,12 +711,24 @@ function serviceEnv() {
420
711
  // before later top-level consts initialise — functions hoist, consts do
421
712
  // not.)
422
713
  const PROXY_ENV = [
423
- "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
424
- "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
714
+ "HTTP_PROXY",
715
+ "HTTPS_PROXY",
716
+ "NO_PROXY",
717
+ "http_proxy",
718
+ "https_proxy",
719
+ "no_proxy",
720
+ "NODE_EXTRA_CA_CERTS",
721
+ "SSL_CERT_FILE",
722
+ "SSL_CERT_DIR",
425
723
  ];
426
724
  const env = { PATH: servicePath() };
427
725
  const installed = installedServiceEnv();
428
- for (const key of ["WORK_APP_PORT", "WORK_ROOT", "WORK_DATA_DIR", "WORK_ALLOWED_HOSTS"]) {
726
+ for (const key of [
727
+ "WORK_APP_PORT",
728
+ "WORK_ROOT",
729
+ "WORK_DATA_DIR",
730
+ "WORK_ALLOWED_HOSTS",
731
+ ]) {
429
732
  // `in` rather than a truthy check, so `WORK_ALLOWED_HOSTS= work setup`
430
733
  // can deliberately clear a setting the installed service still carries.
431
734
  const value = key in process.env ? process.env[key] : installed[key];
@@ -442,7 +745,8 @@ function serviceEnv() {
442
745
  const value = key in process.env ? process.env[key] : installed[key];
443
746
  if (value) env[key] = value;
444
747
  }
445
- if (Object.keys(env).some((k) => /^https?_proxy$/i.test(k))) env.NODE_USE_ENV_PROXY = "1";
748
+ if (Object.keys(env).some((k) => /^https?_proxy$/i.test(k)))
749
+ env.NODE_USE_ENV_PROXY = "1";
446
750
  // launchd passes no locale, and under the C locale tmux mangles the \t
447
751
  // separators the server splits pane listings on — every session read dead
448
752
  // on macOS. The server defends itself (exec.ts utf8Locale), but the
@@ -451,7 +755,8 @@ function serviceEnv() {
451
755
  const locale = ["LC_ALL", "LC_CTYPE", "LANG"]
452
756
  .map((key) => (key in process.env ? process.env[key] : installed[key]))
453
757
  .find((value) => /utf-?8/i.test(value || ""));
454
- env.LC_CTYPE = locale || (process.platform === "darwin" ? "UTF-8" : "C.UTF-8");
758
+ env.LC_CTYPE =
759
+ locale || (process.platform === "darwin" ? "UTF-8" : "C.UTF-8");
455
760
  return env;
456
761
  }
457
762
 
@@ -492,7 +797,9 @@ function pidsListening(port) {
492
797
  ),
493
798
  ];
494
799
  }
495
- const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], { encoding: "utf8" });
800
+ const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], {
801
+ encoding: "utf8",
802
+ });
496
803
  const pids = [];
497
804
  for (const m of (ss.stdout || "").matchAll(/pid=(\d+)/g)) {
498
805
  const pid = Number(m[1]);
@@ -575,11 +882,23 @@ function installService() {
575
882
  const port = serviceEnv().PORT;
576
883
  stopOldCopies(port);
577
884
  const cliPath = fileURLToPath(import.meta.url);
885
+ // One service definition serves both installers. A source install
886
+ // (scripts/setup-prod.sh) points the unit at this checkout's run-prod.sh
887
+ // instead of the CLI — that script carries its own sleep inhibitor and
888
+ // PORT handling, so no awake prefix is layered on top.
889
+ const execOverride = process.env.WORK_SERVICE_EXEC;
890
+ const startArgs = execOverride
891
+ ? [execOverride]
892
+ : [...awakePrefix(), cliPath, "start"];
578
893
  if (process.platform === "darwin") {
579
894
  const plist = serviceFile();
580
895
  fs.mkdirSync(path.dirname(plist), { recursive: true });
581
896
  fs.mkdirSync(path.join(os.homedir(), "Library/Logs"), { recursive: true });
582
- const xml = (v) => String(v).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
897
+ const xml = (v) =>
898
+ String(v)
899
+ .replace(/&/g, "&amp;")
900
+ .replace(/</g, "&lt;")
901
+ .replace(/>/g, "&gt;");
583
902
  const envXml = Object.entries(serviceEnv())
584
903
  .map(([k, v]) => ` <key>${k}</key><string>${xml(v)}</string>`)
585
904
  .join("\n");
@@ -591,7 +910,7 @@ function installService() {
591
910
  <dict>
592
911
  <key>Label</key><string>dev.work.app</string>
593
912
  <key>ProgramArguments</key>
594
- <array>${[...awakePrefix(), process.execPath, cliPath, "start"].map((a) => `<string>${a}</string>`).join("")}</array>
913
+ <array>${startArgs.map((a) => `<string>${a}</string>`).join("")}</array>
595
914
  <key>RunAtLoad</key><true/>
596
915
  <key>KeepAlive</key><true/>
597
916
  <key>StandardOutPath</key><string>${os.homedir()}/Library/Logs/work.log</string>
@@ -606,15 +925,29 @@ ${envXml}
606
925
  );
607
926
  log("installing launchd agent");
608
927
  const uid = process.getuid();
609
- spawnSync("launchctl", ["bootout", `gui/${uid}/dev.work.app`], { stdio: "ignore" });
610
- const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], { stdio: "ignore" });
611
- if (boot.status !== 0 && spawnSync("launchctl", ["load", "-w", plist], { stdio: "ignore" }).status !== 0) {
612
- warn(`could not load the agent (no GUI launchd session?). Run from a local terminal: launchctl bootstrap gui/$UID ${plist}`);
928
+ spawnSync("launchctl", ["bootout", `gui/${uid}/dev.work.app`], {
929
+ stdio: "ignore",
930
+ });
931
+ const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], {
932
+ stdio: "ignore",
933
+ });
934
+ if (
935
+ boot.status !== 0 &&
936
+ spawnSync("launchctl", ["load", "-w", plist], { stdio: "ignore" })
937
+ .status !== 0
938
+ ) {
939
+ warn(
940
+ `could not load the agent (no GUI launchd session?). Run from a local terminal: launchctl bootstrap gui/$UID ${plist}`,
941
+ );
613
942
  warn("or start manually: work start");
614
943
  return false;
615
944
  }
616
- spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/dev.work.app`], { stdio: "ignore" });
617
- log("manage with: launchctl {kickstart -k,print} gui/$UID/dev.work.app (logs: ~/Library/Logs/work.log)");
945
+ spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/dev.work.app`], {
946
+ stdio: "ignore",
947
+ });
948
+ log(
949
+ "manage with: launchctl {kickstart -k,print} gui/$UID/dev.work.app (logs: ~/Library/Logs/work.log)",
950
+ );
618
951
  return true;
619
952
  }
620
953
 
@@ -626,7 +959,7 @@ ${envXml}
626
959
  .map(([k, v]) => `Environment=${k}=${v}`)
627
960
  .join("\n");
628
961
  // systemd splits ExecStart on whitespace, so quote the inhibitor's --why.
629
- const execStart = [...awakePrefix(), process.execPath, cliPath, "start"]
962
+ const execStart = startArgs
630
963
  .map((a) => (a.includes(" ") ? `"${a}"` : a))
631
964
  .join(" ");
632
965
  fs.writeFileSync(
@@ -655,12 +988,18 @@ WantedBy=default.target
655
988
  sh("systemctl", ["--user", "enable", "work.service"]);
656
989
  sh("systemctl", ["--user", "restart", "work.service"]);
657
990
  // Survive logout (best-effort; may need sudo on some systems).
658
- spawnSync("loginctl", ["enable-linger", os.userInfo().username], { stdio: "ignore" });
991
+ spawnSync("loginctl", ["enable-linger", os.userInfo().username], {
992
+ stdio: "ignore",
993
+ });
659
994
  log("manage with: systemctl --user {status,restart,stop} work.service");
660
995
  return true;
661
996
  }
662
997
 
663
- warn("no service manager found. Start manually with: work start");
998
+ warn(
999
+ execOverride
1000
+ ? "no service manager found. Start manually with: scripts/run-prod.sh"
1001
+ : "no service manager found. Start manually with: work start",
1002
+ );
664
1003
  return false;
665
1004
  }
666
1005
 
@@ -0,0 +1,2 @@
1
+ import { f as notifySessionsChanged } from "./_Cr71Xd0d.js";
2
+ export { notifySessionsChanged };
@@ -0,0 +1,2 @@
1
+ import { n as pokeStatusWatch } from "./_CfwTkTbL.js";
2
+ export { pokeStatusWatch };