@dylanpiercey/work 0.0.119 → 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/{_BOIUqihs.js → _CivNK9fo.js} +20 -18
  7. package/dist/{_DE2RUdbF.js → _Cr71Xd0d.js} +8 -3
  8. package/dist/_DJyHc1w-.js +1739 -0
  9. package/dist/{_BIhoUpsW.js → _DnaZVHJ7.js} +95 -94
  10. package/dist/_Dpa8WFuv.js +2 -0
  11. package/dist/index.mjs +4301 -4236
  12. package/dist/public/assets/{_BSsp7Z4i2.js → _-zdUyUYM2.js} +1 -1
  13. package/dist/public/assets/_-zdUyUYM2.js.br +0 -0
  14. package/dist/public/assets/{_LUNsNRnJ2.js → _0L2K8sFq2.js} +1 -1
  15. package/dist/public/assets/_0L2K8sFq2.js.br +0 -0
  16. package/dist/public/assets/{_BE3wNPJo.js → _42jdam49.js} +1 -1
  17. package/dist/public/assets/_42jdam49.js.br +0 -0
  18. package/dist/public/assets/{_CIFN7c1t2.js → _9dJ8BnyZ2.js} +1 -1
  19. package/dist/public/assets/_9dJ8BnyZ2.js.br +0 -0
  20. package/dist/public/assets/{_CqDb111T.js → _B03G1Nvs.js} +1 -1
  21. package/dist/public/assets/_B03G1Nvs.js.br +0 -0
  22. package/dist/public/assets/{_Dzy9r1lm.js → _B0dZ7_qK.js} +1 -1
  23. package/dist/public/assets/_B0dZ7_qK.js.br +0 -0
  24. package/dist/public/assets/{_c1YZLlfE.js → _B1VuWiKK.js} +1 -1
  25. package/dist/public/assets/_B1VuWiKK.js.br +0 -0
  26. package/dist/public/assets/{_Bp1b0XWB2.js → _B5nrQZIq2.js} +1 -1
  27. package/dist/public/assets/_B5nrQZIq2.js.br +0 -0
  28. package/dist/public/assets/{_Bq2fn_8D.js → _B8ZQ8inF.js} +1 -1
  29. package/dist/public/assets/_B8ZQ8inF.js.br +0 -0
  30. package/dist/public/assets/{_CvnxxlPb.js → _B9e51G1K.js} +1 -1
  31. package/dist/public/assets/_B9e51G1K.js.br +0 -0
  32. package/dist/public/assets/{_CUjnrp_u2.js → _BH015W2i2.js} +1 -1
  33. package/dist/public/assets/_BH015W2i2.js.br +0 -0
  34. package/dist/public/assets/{_B62GzYdT.js → _BHjcfVXB.js} +6 -6
  35. package/dist/public/assets/_BHjcfVXB.js.br +0 -0
  36. package/dist/public/assets/{_DMHXo8uX.js → _BNIilCsC.js} +1 -1
  37. package/dist/public/assets/_BNIilCsC.js.br +0 -0
  38. package/dist/public/assets/{_szXVEFuj2.js → _BPgL37mi2.js} +1 -1
  39. package/dist/public/assets/_BPgL37mi2.js.br +0 -0
  40. package/dist/public/assets/{_Ctuh4IDL2.js → _BR3D4nzj2.js} +1 -1
  41. package/dist/public/assets/_BR3D4nzj2.js.br +0 -0
  42. package/dist/public/assets/{_BkTJUq-u.js → _BRSJwfZ3.js} +1 -1
  43. package/dist/public/assets/{_DS628Dnv.js → _BSFENb4l.js} +1 -1
  44. package/dist/public/assets/_BSFENb4l.js.br +0 -0
  45. package/dist/public/assets/{_Btt4k5w3.js → _BSWpz0dK.js} +1 -1
  46. package/dist/public/assets/_BSWpz0dK.js.br +0 -0
  47. package/dist/public/assets/{_N0vzdKCD.js → _BUOWar32.js} +1 -1
  48. package/dist/public/assets/_BUOWar32.js.br +0 -0
  49. package/dist/public/assets/{_1bmjwuWH.js → _BZWlvMSJ.js} +1 -1
  50. package/dist/public/assets/_BZWlvMSJ.js.br +0 -0
  51. package/dist/public/assets/{_BlVrxgss2.js → _B_dMglA92.js} +1 -1
  52. package/dist/public/assets/_B_dMglA92.js.br +0 -0
  53. package/dist/public/assets/{_XgOGaUKD.js → _Beb6Lb1w.js} +1 -1
  54. package/dist/public/assets/_Beb6Lb1w.js.br +0 -0
  55. package/dist/public/assets/{_CXQ_K2Vh2.js → _BjIMoOV-2.js} +1 -1
  56. package/dist/public/assets/_BjIMoOV-2.js.br +0 -0
  57. package/dist/public/assets/{_Dhu480me.js → _BlLuOKRT.js} +1 -1
  58. package/dist/public/assets/_BlLuOKRT.js.br +0 -0
  59. package/dist/public/assets/{_BijwdXvp.js → _BlXwius8.js} +1 -1
  60. package/dist/public/assets/_BlXwius8.js.br +0 -0
  61. package/dist/public/assets/{_DBJ43cPF.js → _Bmt76eWT.js} +1 -1
  62. package/dist/public/assets/_Bmt76eWT.js.br +0 -0
  63. package/dist/public/assets/{_CkuVE2zK.js → _BoS5LU5l.js} +1 -1
  64. package/dist/public/assets/_BoS5LU5l.js.br +0 -0
  65. package/dist/public/assets/{_DtLQVzKB2.js → _BsNKabYu2.js} +1 -1
  66. package/dist/public/assets/_BsNKabYu2.js.br +0 -0
  67. package/dist/public/assets/{_CEWjw4lq.js → _C0OXw3Vy.js} +1 -1
  68. package/dist/public/assets/_C0OXw3Vy.js.br +0 -0
  69. package/dist/public/assets/{_CpRKjPVO2.js → _CBUJNdeq2.js} +1 -1
  70. package/dist/public/assets/_CBUJNdeq2.js.br +0 -0
  71. package/dist/public/assets/{_CZzZOY4P2.js → _CCNhM32c2.js} +1 -1
  72. package/dist/public/assets/_CCNhM32c2.js.br +0 -0
  73. package/dist/public/assets/{_BgnvPND_.js → _CFBaZNUT.js} +1 -1
  74. package/dist/public/assets/_CFBaZNUT.js.br +0 -0
  75. package/dist/public/assets/{_CJT7ujcv2.js → _CJHqzl--2.js} +1 -1
  76. package/dist/public/assets/_CJHqzl--2.js.br +0 -0
  77. package/dist/public/assets/{_duTaTgEk2.js → _CMGgQ-4H2.js} +1 -1
  78. package/dist/public/assets/_CMGgQ-4H2.js.br +0 -0
  79. package/dist/public/assets/{_5YlmpA6X.js → _CPX0D-my.js} +1 -1
  80. package/dist/public/assets/_CPX0D-my.js.br +0 -0
  81. package/dist/public/assets/{_BYYDEuFR2.js → _CRuHDn_O2.js} +1 -1
  82. package/dist/public/assets/_CRuHDn_O2.js.br +0 -0
  83. package/dist/public/assets/{_CnKbN1QV.js → _CUOao_Hj.js} +1 -1
  84. package/dist/public/assets/_CUOao_Hj.js.br +0 -0
  85. package/dist/public/assets/{_CZejW_fq.js → _CUVw5V4z.js} +1 -1
  86. package/dist/public/assets/_CUVw5V4z.js.br +0 -0
  87. package/dist/public/assets/{_DO4n-NwM.js → _CYMGOhCb.js} +1 -1
  88. package/dist/public/assets/_CYMGOhCb.js.br +0 -0
  89. package/dist/public/assets/{_ER-_pvil.js → _CZfap_Dj.js} +1 -1
  90. package/dist/public/assets/_CZfap_Dj.js.br +0 -0
  91. package/dist/public/assets/{_DYngS949.js → _CbNmSe5k.js} +1 -1
  92. package/dist/public/assets/_CbNmSe5k.js.br +0 -0
  93. package/dist/public/assets/{_DKciAPQl.js → _CdtuhZSc.js} +1 -1
  94. package/dist/public/assets/_CdtuhZSc.js.br +0 -0
  95. package/dist/public/assets/{_B7w9Jp5h.js → _ClhApln3.js} +1 -1
  96. package/dist/public/assets/_ClhApln3.js.br +0 -0
  97. package/dist/public/assets/{_BD9TBsnM2.js → _CvA5IOAW2.js} +1 -1
  98. package/dist/public/assets/_CvA5IOAW2.js.br +0 -0
  99. package/dist/public/assets/{_Cp-yobQE2.js → _CwZYmUdt2.js} +1 -1
  100. package/dist/public/assets/{_MofKWwY9.js → _CxrcWZPE.js} +1 -1
  101. package/dist/public/assets/_CxrcWZPE.js.br +0 -0
  102. package/dist/public/assets/{_RhYe7Uqa.js → _D-KjGTSW.js} +24 -21
  103. package/dist/public/assets/_D-KjGTSW.js.br +0 -0
  104. package/dist/public/assets/{_BRiOyxeF.js → _D4u4SpiK.js} +1 -1
  105. package/dist/public/assets/_D4u4SpiK.js.br +0 -0
  106. package/dist/public/assets/{_D8raMSvy.js → _D6CjH3JN.js} +1 -1
  107. package/dist/public/assets/_D6CjH3JN.js.br +0 -0
  108. package/dist/public/assets/{_Cym2lUeD2.js → _D7cQLLGK2.js} +1 -1
  109. package/dist/public/assets/_D7cQLLGK2.js.br +0 -0
  110. package/dist/public/assets/{_C8DWM9rr.js → _DBoABI85.js} +1 -1
  111. package/dist/public/assets/_DBoABI85.js.br +0 -0
  112. package/dist/public/assets/{_Rf_7ZEPL.js → _DNcd1xtp.js} +1 -1
  113. package/dist/public/assets/_DNcd1xtp.js.br +0 -0
  114. package/dist/public/assets/{_Bhwci4Zy.js → _DQGTProt.js} +1 -1
  115. package/dist/public/assets/_DQGTProt.js.br +0 -0
  116. package/dist/public/assets/{_B2g0gJmZ.js → _DSIH6_Li.js} +1 -1
  117. package/dist/public/assets/_DSIH6_Li.js.br +0 -0
  118. package/dist/public/assets/{_T2999saD2.js → _DXHf4WHs2.js} +1 -1
  119. package/dist/public/assets/_DXHf4WHs2.js.br +0 -0
  120. package/dist/public/assets/{_CoStmjbL.js → _D_qDhyn2.js} +1 -1
  121. package/dist/public/assets/_D_qDhyn2.js.br +0 -0
  122. package/dist/public/assets/{_CGOYaUuV.js → _DcMfCkmd.js} +1 -1
  123. package/dist/public/assets/_DcMfCkmd.js.br +0 -0
  124. package/dist/public/assets/{_Cv9Hgy8P.js → _DgDEdxEu.js} +1 -1
  125. package/dist/public/assets/_DgDEdxEu.js.br +0 -0
  126. package/dist/public/assets/{_Bsk9ewWF.js → _DlhDim1D.js} +1 -1
  127. package/dist/public/assets/_DlhDim1D.js.br +0 -0
  128. package/dist/public/assets/{_BNQgBkwF.js → _DqLib98x.js} +1 -1
  129. package/dist/public/assets/_DqLib98x.js.br +0 -0
  130. package/dist/public/assets/{_l9vJoVeP.js → _Ds0Qa0YX.js} +1 -1
  131. package/dist/public/assets/_Ds0Qa0YX.js.br +0 -0
  132. package/dist/public/assets/{_iDn41XPe2.js → _DyUvL09k2.js} +1 -1
  133. package/dist/public/assets/_DyUvL09k2.js.br +0 -0
  134. package/dist/public/assets/{_AqmX0pBH2.js → _J6xlcydH2.js} +1 -1
  135. package/dist/public/assets/_J6xlcydH2.js.br +0 -0
  136. package/dist/public/assets/{_Dni1s_3Q2.js → _MInulccf2.js} +1 -1
  137. package/dist/public/assets/{_BsOtzbIC.js → _QMVFINxi.js} +1 -1
  138. package/dist/public/assets/_QMVFINxi.js.br +0 -0
  139. package/dist/public/assets/{_CWNv7bdi.js → _S9QQtt68.js} +1 -1
  140. package/dist/public/assets/_S9QQtt68.js.br +0 -0
  141. package/dist/public/assets/{_DlZyjzE_.js → _XCJLLGxV.js} +1 -1
  142. package/dist/public/assets/_XCJLLGxV.js.br +0 -0
  143. package/dist/public/assets/{_B2LXCPdE.js → _Y-bTh0dW.js} +1 -1
  144. package/dist/public/assets/_Y-bTh0dW.js.br +0 -0
  145. package/dist/public/assets/{_zPAwS_-H.js → _Y8TOk1pR.js} +1 -1
  146. package/dist/public/assets/_Y8TOk1pR.js.br +0 -0
  147. package/dist/public/assets/{_CzI2QhH4.js → _YetFhXkM.js} +1 -1
  148. package/dist/public/assets/_YetFhXkM.js.br +0 -0
  149. package/dist/public/assets/{_O8R4l7yw2.js → _YmJopjX92.js} +1 -1
  150. package/dist/public/assets/_YmJopjX92.js.br +0 -0
  151. package/dist/public/assets/{_Du5pjG7G.js → _mbzaOqik.js} +1 -1
  152. package/dist/public/assets/_mbzaOqik.js.br +0 -0
  153. package/dist/public/assets/{_qj_i_cqe.js → _rMcfapaa.js} +1 -1
  154. package/dist/public/assets/_rMcfapaa.js.br +0 -0
  155. package/dist/public/assets/{_yTOoKAfM2.js → _rRtbbfKJ2.js} +1 -1
  156. package/dist/public/assets/_rRtbbfKJ2.js.br +0 -0
  157. package/dist/public/assets/{_BD68UZIf.js → _sSWgnHeU.js} +1 -1
  158. package/dist/public/assets/_sSWgnHeU.js.br +0 -0
  159. package/dist/public/assets/{_CegJB2vl.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/_1bmjwuWH.js.br +0 -0
  182. package/dist/public/assets/_5YlmpA6X.js.br +0 -0
  183. package/dist/public/assets/_AqmX0pBH2.js.br +0 -0
  184. package/dist/public/assets/_B2LXCPdE.js.br +0 -0
  185. package/dist/public/assets/_B2g0gJmZ.js.br +0 -0
  186. package/dist/public/assets/_B62GzYdT.js.br +0 -0
  187. package/dist/public/assets/_B7w9Jp5h.js.br +0 -0
  188. package/dist/public/assets/_BD68UZIf.js.br +0 -0
  189. package/dist/public/assets/_BD9TBsnM2.js.br +0 -0
  190. package/dist/public/assets/_BE3wNPJo.js.br +0 -0
  191. package/dist/public/assets/_BNQgBkwF.js.br +0 -0
  192. package/dist/public/assets/_BRiOyxeF.js.br +0 -0
  193. package/dist/public/assets/_BSsp7Z4i2.js.br +0 -0
  194. package/dist/public/assets/_BYYDEuFR2.js.br +0 -0
  195. package/dist/public/assets/_BgnvPND_.js.br +0 -0
  196. package/dist/public/assets/_Bhwci4Zy.js.br +0 -0
  197. package/dist/public/assets/_BijwdXvp.js.br +0 -0
  198. package/dist/public/assets/_BlVrxgss2.js.br +0 -0
  199. package/dist/public/assets/_Bp1b0XWB2.js.br +0 -0
  200. package/dist/public/assets/_Bq2fn_8D.js.br +0 -0
  201. package/dist/public/assets/_BsOtzbIC.js.br +0 -0
  202. package/dist/public/assets/_Bsk9ewWF.js.br +0 -0
  203. package/dist/public/assets/_Btt4k5w3.js.br +0 -0
  204. package/dist/public/assets/_C8DWM9rr.js.br +0 -0
  205. package/dist/public/assets/_CEWjw4lq.js.br +0 -0
  206. package/dist/public/assets/_CGOYaUuV.js.br +0 -0
  207. package/dist/public/assets/_CIFN7c1t2.js.br +0 -0
  208. package/dist/public/assets/_CJT7ujcv2.js.br +0 -0
  209. package/dist/public/assets/_CUjnrp_u2.js.br +0 -0
  210. package/dist/public/assets/_CWNv7bdi.js.br +0 -0
  211. package/dist/public/assets/_CXQ_K2Vh2.js.br +0 -0
  212. package/dist/public/assets/_CZejW_fq.js.br +0 -0
  213. package/dist/public/assets/_CZzZOY4P2.js.br +0 -0
  214. package/dist/public/assets/_CegJB2vl.js.br +0 -0
  215. package/dist/public/assets/_CkuVE2zK.js.br +0 -0
  216. package/dist/public/assets/_CnKbN1QV.js.br +0 -0
  217. package/dist/public/assets/_CoStmjbL.js.br +0 -0
  218. package/dist/public/assets/_CpRKjPVO2.js.br +0 -0
  219. package/dist/public/assets/_CqDb111T.js.br +0 -0
  220. package/dist/public/assets/_Ctuh4IDL2.js.br +0 -0
  221. package/dist/public/assets/_Cv9Hgy8P.js.br +0 -0
  222. package/dist/public/assets/_CvnxxlPb.js.br +0 -0
  223. package/dist/public/assets/_Cym2lUeD2.js.br +0 -0
  224. package/dist/public/assets/_CzI2QhH4.js.br +0 -0
  225. package/dist/public/assets/_D8raMSvy.js.br +0 -0
  226. package/dist/public/assets/_DBJ43cPF.js.br +0 -0
  227. package/dist/public/assets/_DKciAPQl.js.br +0 -0
  228. package/dist/public/assets/_DMHXo8uX.js.br +0 -0
  229. package/dist/public/assets/_DO4n-NwM.js.br +0 -0
  230. package/dist/public/assets/_DS628Dnv.js.br +0 -0
  231. package/dist/public/assets/_DYngS949.js.br +0 -0
  232. package/dist/public/assets/_Dhu480me.js.br +0 -0
  233. package/dist/public/assets/_DlZyjzE_.js.br +0 -0
  234. package/dist/public/assets/_DtLQVzKB2.js.br +0 -0
  235. package/dist/public/assets/_Du5pjG7G.js.br +0 -0
  236. package/dist/public/assets/_Dzy9r1lm.js.br +0 -0
  237. package/dist/public/assets/_ER-_pvil.js.br +0 -0
  238. package/dist/public/assets/_LUNsNRnJ2.js.br +0 -0
  239. package/dist/public/assets/_MofKWwY9.js.br +0 -0
  240. package/dist/public/assets/_N0vzdKCD.js.br +0 -0
  241. package/dist/public/assets/_O8R4l7yw2.js.br +0 -0
  242. package/dist/public/assets/_Rf_7ZEPL.js.br +0 -0
  243. package/dist/public/assets/_RhYe7Uqa.js.br +0 -0
  244. package/dist/public/assets/_T2999saD2.js.br +0 -0
  245. package/dist/public/assets/_XgOGaUKD.js.br +0 -0
  246. package/dist/public/assets/_c1YZLlfE.js.br +0 -0
  247. package/dist/public/assets/_duTaTgEk2.js.br +0 -0
  248. package/dist/public/assets/_iDn41XPe2.js.br +0 -0
  249. package/dist/public/assets/_l9vJoVeP.js.br +0 -0
  250. package/dist/public/assets/_qj_i_cqe.js.br +0 -0
  251. package/dist/public/assets/_szXVEFuj2.js.br +0 -0
  252. package/dist/public/assets/_yTOoKAfM2.js.br +0 -0
  253. package/dist/public/assets/_zPAwS_-H.js.br +0 -0
  254. package/dist/public/assets/app-page-BXbewHHv.css +0 -1
  255. package/dist/public/assets/app-page-BXbewHHv.css.br +0 -0
  256. package/dist/public/assets/diff-view-BJ1RAVYB.css +0 -1
  257. package/dist/public/assets/diff-view-BJ1RAVYB.css.br +0 -0
  258. package/dist/public/assets/new-DUXTU7jB.js +0 -1
  259. package/dist/public/assets/settings-DUXTU7jB.js +0 -1
  260. package/dist/public/assets/w-DUXTU7jB.js +0 -1
  261. package/dist/public/assets/w._-DUXTU7jB.js +0 -1
  262. package/dist/public/assets/w._-bwrmfhu0.js +0 -1
@@ -46,7 +46,11 @@ export function setupPath(base = process.env.PATH || "") {
46
46
  * display (Settings rows, `work doctor`), never re-used to exec. */
47
47
  function tilde(p) {
48
48
  if (!p) return p;
49
- return p === home ? "~" : p.startsWith(home + path.sep) ? "~" + p.slice(home.length) : p;
49
+ return p === home
50
+ ? "~"
51
+ : p.startsWith(home + path.sep)
52
+ ? "~" + p.slice(home.length)
53
+ : p;
50
54
  }
51
55
 
52
56
  /** Absolute path of `bin` on setupPath(), or null. */
@@ -65,8 +69,17 @@ function run(cmd, args, { timeout = 15_000 } = {}) {
65
69
  execFile(
66
70
  cmd,
67
71
  args,
68
- { env: { ...process.env, PATH: setupPath() }, timeout, maxBuffer: 8 << 20 },
69
- (err, stdout, stderr) => resolve({ ok: !err, stdout: String(stdout || ""), stderr: String(stderr || "") }),
72
+ {
73
+ env: { ...process.env, PATH: setupPath() },
74
+ timeout,
75
+ maxBuffer: 8 << 20,
76
+ },
77
+ (err, stdout, stderr) =>
78
+ resolve({
79
+ ok: !err,
80
+ stdout: String(stdout || ""),
81
+ stderr: String(stderr || ""),
82
+ }),
70
83
  );
71
84
  });
72
85
  }
@@ -81,7 +94,9 @@ export async function miseVersions() {
81
94
  const out = {};
82
95
  try {
83
96
  for (const [name, list] of Object.entries(JSON.parse(stdout))) {
84
- const active = list.find((v) => v.installed && v.active) || list.find((v) => v.installed);
97
+ const active =
98
+ list.find((v) => v.installed && v.active) ||
99
+ list.find((v) => v.installed);
85
100
  if (active) out[name] = active.version;
86
101
  }
87
102
  } catch {}
@@ -92,7 +107,10 @@ export async function miseVersions() {
92
107
  * source of truth for "which GitHub hosts has this machine signed in to",
93
108
  * merged with whatever the caller has configured. */
94
109
  export function ghKnownHosts() {
95
- const file = path.join(process.env.GH_CONFIG_DIR || path.join(home, ".config", "gh"), "hosts.yml");
110
+ const file = path.join(
111
+ process.env.GH_CONFIG_DIR || path.join(home, ".config", "gh"),
112
+ "hosts.yml",
113
+ );
96
114
  let text = "";
97
115
  try {
98
116
  text = fs.readFileSync(file, "utf8");
@@ -130,7 +148,13 @@ export function discoverAgents() {
130
148
  continue;
131
149
  }
132
150
  for (const name of names) {
133
- const hit = AGENTS.find((a) => a.bins.some((b) => name === b || (name.startsWith(b) && /^\d+$/.test(name.slice(b.length)))));
151
+ const hit = AGENTS.find((a) =>
152
+ a.bins.some(
153
+ (b) =>
154
+ name === b ||
155
+ (name.startsWith(b) && /^\d+$/.test(name.slice(b.length))),
156
+ ),
157
+ );
134
158
  if (!hit || seen.has(name)) continue;
135
159
  // Join against the PATH entry, not the realpath — tilde() matches on
136
160
  // $HOME, and on hosts where home is a symlink (/home → /var/home)
@@ -142,28 +166,65 @@ export function discoverAgents() {
142
166
  continue;
143
167
  }
144
168
  const bin = hit.bins.find((b) => name.startsWith(b));
145
- seen.set(name, { family: hit, id: `${hit.id}${name.slice(bin.length)}`, command: name, path: tilde(full) });
169
+ seen.set(name, {
170
+ family: hit,
171
+ id: `${hit.id}${name.slice(bin.length)}`,
172
+ command: name,
173
+ path: tilde(full),
174
+ });
146
175
  }
147
176
  }
148
177
  return AGENTS.map((a) => {
149
178
  const instances = [...seen.values()]
150
179
  .filter((i) => i.family === a)
151
- .map(({ id, command, path: p }) => ({ id, command, path: p, suffix: id.slice(a.id.length) }))
152
- .sort((x, y) => (x.suffix === "" ? -1 : y.suffix === "" ? 1 : Number(x.suffix) - Number(y.suffix)));
153
- return { id: a.id, label: a.label, installed: instances.length > 0, installable: true, instances };
180
+ .map(({ id, command, path: p }) => ({
181
+ id,
182
+ command,
183
+ path: p,
184
+ suffix: id.slice(a.id.length),
185
+ }))
186
+ .sort((x, y) =>
187
+ x.suffix === ""
188
+ ? -1
189
+ : y.suffix === ""
190
+ ? 1
191
+ : Number(x.suffix) - Number(y.suffix),
192
+ );
193
+ return {
194
+ id: a.id,
195
+ label: a.label,
196
+ installed: instances.length > 0,
197
+ installable: true,
198
+ instances,
199
+ };
154
200
  });
155
201
  }
156
202
 
157
203
  /** Auth status for one host via gh (which owns the tokens). */
158
204
  async function githubHostStatus(host) {
159
205
  const isEnterprise = host !== DEFAULT_GITHUB_HOST;
160
- if (!which("gh")) return { host, isEnterprise, authed: false, error: "gh not installed" };
161
- const { ok, stdout, stderr } = await run("gh", ["auth", "status", "--hostname", host]);
206
+ if (!which("gh"))
207
+ return { host, isEnterprise, authed: false, error: "gh not installed" };
208
+ const { ok, stdout, stderr } = await run("gh", [
209
+ "auth",
210
+ "status",
211
+ "--hostname",
212
+ host,
213
+ ]);
162
214
  const text = stdout + stderr;
163
- const user = text.match(/account (\S+)/)?.[1] || text.match(/Logged in to \S+ as (\S+)/)?.[1];
215
+ const user =
216
+ text.match(/account (\S+)/)?.[1] ||
217
+ text.match(/Logged in to \S+ as (\S+)/)?.[1];
164
218
  return ok
165
219
  ? { host, isEnterprise, authed: true, user }
166
- : { host, isEnterprise, authed: false, error: /not logged in/i.test(text) ? undefined : text.trim().split("\n").pop() };
220
+ : {
221
+ host,
222
+ isEnterprise,
223
+ authed: false,
224
+ error: /not logged in/i.test(text)
225
+ ? undefined
226
+ : text.trim().split("\n").pop(),
227
+ };
167
228
  }
168
229
 
169
230
  /**
@@ -173,7 +234,11 @@ async function githubHostStatus(host) {
173
234
  * github.com and any host gh is already signed in to are always included.
174
235
  */
175
236
  export async function doctor(opts = {}) {
176
- const hostSet = new Set([DEFAULT_GITHUB_HOST, ...(opts.hosts || []), ...ghKnownHosts()]);
237
+ const hostSet = new Set([
238
+ DEFAULT_GITHUB_HOST,
239
+ ...(opts.hosts || []),
240
+ ...ghKnownHosts(),
241
+ ]);
177
242
  const [versions, github] = await Promise.all([
178
243
  miseVersions(),
179
244
  Promise.all([...hostSet].map(githubHostStatus)),
@@ -205,7 +270,10 @@ export async function doctor(opts = {}) {
205
270
  return {
206
271
  ready: missing.length === 0,
207
272
  missing,
208
- mise: { installed: versions !== null, path: tilde(which("mise")) || undefined },
273
+ mise: {
274
+ installed: versions !== null,
275
+ path: tilde(which("mise")) || undefined,
276
+ },
209
277
  tools,
210
278
  agents,
211
279
  github,
@@ -216,16 +284,24 @@ export async function doctor(opts = {}) {
216
284
  export function formatDoctor(d) {
217
285
  const mark = (ok) => (ok ? "✓" : "✗");
218
286
  const lines = [];
219
- lines.push(`mise ${mark(d.mise.installed)} ${d.mise.path || "not installed"}`);
287
+ lines.push(
288
+ `mise ${mark(d.mise.installed)} ${d.mise.path || "not installed"}`,
289
+ );
220
290
  for (const t of d.tools) {
221
291
  const tag = t.required ? "" : " (optional)";
222
- lines.push(`${t.label.padEnd(11)}${mark(t.installed)} ${t.installed ? (t.version ? `v${t.version} ` : "") + (t.path || "") : "missing"}${tag}`);
292
+ lines.push(
293
+ `${t.label.padEnd(11)}${mark(t.installed)} ${t.installed ? (t.version ? `v${t.version} ` : "") + (t.path || "") : "missing"}${tag}`,
294
+ );
223
295
  }
224
296
  for (const h of d.github) {
225
- lines.push(`${h.host.padEnd(11)}${mark(h.authed)} ${h.authed ? `signed in${h.user ? ` as ${h.user}` : ""}` : h.error || "not signed in"}`);
297
+ lines.push(
298
+ `${h.host.padEnd(11)}${mark(h.authed)} ${h.authed ? `signed in${h.user ? ` as ${h.user}` : ""}` : h.error || "not signed in"}`,
299
+ );
226
300
  }
227
301
  for (const a of d.agents) {
228
- lines.push(`${a.label.padEnd(11)}${mark(a.installed)} ${a.installed ? a.instances.map((i) => `${i.command} (${i.path})`).join(", ") : "not installed"}`);
302
+ lines.push(
303
+ `${a.label.padEnd(11)}${mark(a.installed)} ${a.installed ? a.instances.map((i) => `${i.command} (${i.path})`).join(", ") : "not installed"}`,
304
+ );
229
305
  }
230
306
  lines.push("");
231
307
  lines.push(d.ready ? "ready" : `not ready, missing: ${d.missing.join(", ")}`);
@@ -1,14 +1,20 @@
1
1
  export interface RunOpts {
2
2
  log?: (line: string) => void;
3
- interactive?: boolean;
4
3
  input?: string;
5
4
  env?: Record<string, string>;
6
5
  cwd?: string;
7
6
  timeout?: number;
8
7
  }
9
- export function runStreaming(cmd: string, args: string[], opts?: RunOpts): Promise<string[]>;
8
+ export function runStreaming(
9
+ cmd: string,
10
+ args: string[],
11
+ opts?: RunOpts,
12
+ ): Promise<string[]>;
10
13
  export function installMise(opts?: RunOpts): Promise<void>;
11
- export function installTool(idOrName: string, opts?: RunOpts & { version?: string }): Promise<void>;
14
+ export function installTool(
15
+ idOrName: string,
16
+ opts?: RunOpts & { version?: string },
17
+ ): Promise<void>;
12
18
  export function updateTool(idOrName: string, opts?: RunOpts): Promise<void>;
13
19
  export function removeTool(idOrName: string, opts?: RunOpts): Promise<void>;
14
20
  export function installAgent(id: string, opts?: RunOpts): Promise<void>;
@@ -10,17 +10,20 @@ import { agentById, toolById } from "./catalog.mjs";
10
10
  import { setupPath, which } from "./doctor.mjs";
11
11
 
12
12
  /**
13
- * Run a command, streaming each output line to `log`.
14
- * `interactive` hands the terminal over (CLI prompts such as gh's device
15
- * flow); otherwise stdout/err are captured line by line and `input` (if
16
- * any) is written to stdin before it closes. `cwd` matters for tools whose
17
- * scope is the directory they run in, such as project-scope skill installs.
13
+ * Run a command, streaming each output line to `log`. stdout/err are
14
+ * captured line by line and `input` (if any) is written to stdin before it
15
+ * closes. `cwd` matters for tools whose scope is the directory they run in,
16
+ * such as project-scope skill installs.
18
17
  */
19
- export function runStreaming(cmd, args, { log = () => {}, interactive = false, input, env, cwd, timeout = 15 * 60_000 } = {}) {
18
+ export function runStreaming(
19
+ cmd,
20
+ args,
21
+ { log = () => {}, input, env, cwd, timeout = 15 * 60_000 } = {},
22
+ ) {
20
23
  return new Promise((resolve, reject) => {
21
24
  const child = spawn(cmd, args, {
22
25
  cwd,
23
- stdio: interactive ? "inherit" : ["pipe", "pipe", "pipe"],
26
+ stdio: ["pipe", "pipe", "pipe"],
24
27
  env: { ...process.env, ...env, PATH: setupPath() },
25
28
  });
26
29
  const lines = [];
@@ -29,7 +32,10 @@ export function runStreaming(cmd, args, { log = () => {}, interactive = false, i
29
32
  buf += String(chunk);
30
33
  let i;
31
34
  while ((i = buf.search(/\r?\n/)) >= 0) {
32
- const line = buf.slice(0, i).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").trimEnd();
35
+ const line = buf
36
+ .slice(0, i)
37
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, "")
38
+ .trimEnd();
33
39
  buf = buf.slice(i + (buf[i] === "\r" ? 2 : 1));
34
40
  if (line) {
35
41
  lines.push(line);
@@ -39,10 +45,8 @@ export function runStreaming(cmd, args, { log = () => {}, interactive = false, i
39
45
  };
40
46
  child.stdout?.on("data", onData);
41
47
  child.stderr?.on("data", onData);
42
- if (child.stdin) {
43
- if (input) child.stdin.write(input);
44
- child.stdin.end();
45
- }
48
+ if (input) child.stdin.write(input);
49
+ child.stdin.end();
46
50
  const timer = setTimeout(() => child.kill("SIGTERM"), timeout);
47
51
  child.on("error", (err) => {
48
52
  clearTimeout(timer);
@@ -52,7 +56,12 @@ export function runStreaming(cmd, args, { log = () => {}, interactive = false, i
52
56
  clearTimeout(timer);
53
57
  if (buf.trim()) onData("\n");
54
58
  if (code === 0) resolve(lines);
55
- else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}${lines.length ? `: ${lines[lines.length - 1]}` : ""}`));
59
+ else
60
+ reject(
61
+ new Error(
62
+ `${cmd} ${args.join(" ")} exited ${code}${lines.length ? `: ${lines[lines.length - 1]}` : ""}`,
63
+ ),
64
+ );
56
65
  });
57
66
  });
58
67
  }
@@ -62,7 +71,10 @@ export function runStreaming(cmd, args, { log = () => {}, interactive = false, i
62
71
  * stream into a login shell: the URL is pinned in the catalog and the
63
72
  * script runs in the setup PATH, not the user's rc. */
64
73
  async function runRemoteScript(url, shell, opts) {
65
- const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(60_000) });
74
+ const res = await fetch(url, {
75
+ redirect: "follow",
76
+ signal: AbortSignal.timeout(60_000),
77
+ });
66
78
  if (!res.ok) throw new Error(`${url} → ${res.status}`);
67
79
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "work-install-"));
68
80
  const file = path.join(dir, "install.sh");
@@ -78,18 +90,25 @@ export async function installMise(opts = {}) {
78
90
  if (which("mise")) return;
79
91
  opts.log?.("installing mise");
80
92
  await runRemoteScript("https://mise.run", "sh", opts);
81
- if (!which("mise")) throw new Error("mise install finished but binary not found");
93
+ if (!which("mise"))
94
+ throw new Error("mise install finished but binary not found");
82
95
  }
83
96
 
84
97
  function miseName(idOrName) {
85
98
  const t = toolById(idOrName);
86
- if (t && !t.mise) throw new Error(`${t.label} is a system package. Install it with the system package manager`);
99
+ if (t && !t.mise)
100
+ throw new Error(
101
+ `${t.label} is a system package. Install it with the system package manager`,
102
+ );
87
103
  return t ? t.mise : idOrName;
88
104
  }
89
105
 
90
106
  /** `mise use -g name@version` — installs (or re-pins) a global tool. Accepts
91
107
  * catalog ids or any mise registry short name (Settings search). */
92
- export async function installTool(idOrName, { version = "latest", ...opts } = {}) {
108
+ export async function installTool(
109
+ idOrName,
110
+ { version = "latest", ...opts } = {},
111
+ ) {
93
112
  await installMise(opts);
94
113
  const name = miseName(idOrName);
95
114
  opts.log?.(`mise use -g ${name}@${version}`);
@@ -119,39 +138,49 @@ export async function installAgent(id, opts = {}) {
119
138
  if (!a) throw new Error(`unknown agent: ${id}`);
120
139
  opts.log?.(`installing ${a.label} (${a.install.url})`);
121
140
  await runRemoteScript(a.install.url, a.install.shell, opts);
122
- if (!a.bins.some((b) => which(b))) throw new Error(`${a.label} installer finished but ${a.bins[0]} is not on PATH`);
141
+ if (!a.bins.some((b) => which(b)))
142
+ throw new Error(
143
+ `${a.label} installer finished but ${a.bins[0]} is not on PATH`,
144
+ );
123
145
  }
124
146
 
125
- /**
126
- * Sign in to a GitHub host with gh's browser device flow, never opening a
127
- * browser: `work` usually runs on a remote box, so gh's attempt to launch
128
- * one only produces "Failed opening a web browser" noise. Interactive (CLI)
129
- * lets gh own the terminal (it prints the code + URL and polls); otherwise
130
- * (server) we press Enter for it and the code and URL flow out through
131
- * `log` for the UI to show gh keeps polling until the user finishes in
132
- * their own browser.
133
- */
134
- export async function authGithub(host, { interactive = false, ...opts } = {}) {
147
+ /** Shared gh device flow: sign in / refresh without opening a browser.
148
+ * `work` usually runs on a remote box, so gh's attempt to launch one only
149
+ * produces "Failed opening a web browser" noise. Server-side we press Enter
150
+ * for gh and the code and URL flow out through `log` for the UI to show —
151
+ * gh keeps polling until the user finishes in their own browser
152
+ * (BROWSER=true suppresses the launch attempt; GH_PROMPT_DISABLED stops the
153
+ * prompt waiting on stdin we already fed). */
154
+ async function ghDeviceFlow(host, args, opts = {}) {
135
155
  if (!which("gh")) throw new Error("gh is not installed");
136
- const args = ["auth", "login", "--hostname", host, "--web", "--git-protocol", "https"];
137
- const env = { ...opts.env, BROWSER: "true" };
138
- if (interactive) return runStreaming("gh", args, { ...opts, env, interactive: true });
139
- return runStreaming("gh", args, { ...opts, input: "\n", env: { ...env, GH_PROMPT_DISABLED: "1" } });
156
+ return runStreaming("gh", args, {
157
+ ...opts,
158
+ input: "\n",
159
+ env: { ...opts.env, BROWSER: "true", GH_PROMPT_DISABLED: "1" },
160
+ });
161
+ }
162
+
163
+ /** Sign a GitHub host in with gh's device flow — see ghDeviceFlow. */
164
+ export async function authGithub(host, opts = {}) {
165
+ return ghDeviceFlow(
166
+ host,
167
+ ["auth", "login", "--hostname", host, "--web", "--git-protocol", "https"],
168
+ opts,
169
+ );
140
170
  }
141
171
 
142
172
  /** Re-authorize an existing gh login (device flow). Needed after an org
143
173
  * enables SAML: visiting the X-GitHub-SSO link only signs the browser into
144
174
  * the IdP, while `auth refresh` re-issues the OAuth token with org SSO. */
145
- export async function refreshGithub(host, { interactive = false, ...opts } = {}) {
146
- if (!which("gh")) throw new Error("gh is not installed");
147
- const args = ["auth", "refresh", "--hostname", host];
148
- const env = { ...opts.env, BROWSER: "true" };
149
- if (interactive) return runStreaming("gh", args, { ...opts, env, interactive: true });
150
- return runStreaming("gh", args, { ...opts, input: "\n", env: { ...env, GH_PROMPT_DISABLED: "1" } });
175
+ export async function refreshGithub(host, opts = {}) {
176
+ return ghDeviceFlow(host, ["auth", "refresh", "--hostname", host], opts);
151
177
  }
152
178
 
153
179
  /** Sign out of a GitHub host (drops gh's token; the host stays listed). */
154
180
  export async function logoutGithub(host, opts = {}) {
155
181
  if (!which("gh")) throw new Error("gh is not installed");
156
- return runStreaming("gh", ["auth", "logout", "--hostname", host], { ...opts, env: { GH_PROMPT_DISABLED: "1" } });
182
+ return runStreaming("gh", ["auth", "logout", "--hostname", host], {
183
+ ...opts,
184
+ env: { GH_PROMPT_DISABLED: "1" },
185
+ });
157
186
  }
@@ -0,0 +1,27 @@
1
+ export interface RepoRow {
2
+ full_name: string;
3
+ worktree: string;
4
+ base_branch: string;
5
+ default_branch: string;
6
+ }
7
+
8
+ /** `owner/name`, `owner/name@base`, or `{ repo, dir?, base? }`. */
9
+ export type RepoEntry = string | { repo: string; dir?: string; base?: string };
10
+
11
+ export interface Invite {
12
+ name: string;
13
+ branch: string;
14
+ /** `[host/]owner/name` per repo. */
15
+ repos: string[];
16
+ }
17
+
18
+ /** A repo row as the manifest writes it. */
19
+ export function encodeRepo(r: RepoRow): RepoEntry;
20
+ /** The row for a manifest entry (or an older object form); null if not one.
21
+ * `base_branch`/`default_branch` are empty when the file did not pin them. */
22
+ export function decodeRepo(entry: unknown): RepoRow | null;
23
+ /** The pastable invite for a workspace (Manage's copy button, `work share`). */
24
+ export function formatInvite(ws: Invite): string;
25
+ /** Parse an invite, or a one-workspace backup. Throws with a reason a
26
+ * person can act on. */
27
+ export function parseInvite(text: string): Invite;
@@ -0,0 +1,144 @@
1
+ // The backup manifest's workspace entry is the one shape a workspace has
2
+ // outside the database: what `work export` writes, what Manage's copy
3
+ // button puts on the clipboard, what New → Join and `work join` read. An
4
+ // invite is that entry without the machine-specific parts (id, created,
5
+ // archived). Plain ESM with no dependencies, shared by the browser, the
6
+ // server and the CLI so there is exactly one format:
7
+ //
8
+ // {
9
+ // "name": "persisted pages",
10
+ // "branch": "dpiercey-ws-persisted-pages-xh8764",
11
+ // "repos": ["marko-js/marko", "DylanPiercey/persisted-pages-scratch"]
12
+ // }
13
+ //
14
+ // A repo is `owner/name` (`host/owner/name` off github.com), `owner/name@base`
15
+ // when the workspace's base is not the remote's default branch, and an
16
+ // object `{ repo, dir?, base? }` only when its directory is not the repo's
17
+ // name. A join ignores the base — the shared branch is the base.
18
+
19
+ const BRANCH_RE = /^[\w./+@-]+$/;
20
+ // `owner/name`, or `host/owner/name` — a host has a dot in it.
21
+ const REPO_RE = /^(?:[\w-]+(?:\.[\w-]+)+\/)?[\w.-]+\/[\w.-]+$/;
22
+ // One path segment: a repo's directory in the workspace.
23
+ const DIR_RE = /^(?!\.{1,2}$)[\w.-]+$/;
24
+
25
+ /**
26
+ * @typedef {{ full_name: string, worktree: string, base_branch: string, default_branch: string }} RepoRow
27
+ * @typedef {string | { repo: string, dir?: string, base?: string }} RepoEntry
28
+ */
29
+
30
+ /** @param {RepoRow} r @returns {RepoEntry} */
31
+ export function encodeRepo(r) {
32
+ const base = r.base_branch !== r.default_branch ? r.base_branch : undefined;
33
+ // `@` in the base would split wrong on the way back: object form then.
34
+ if (r.worktree === r.full_name.split("/").at(-1) && !base?.includes("@")) {
35
+ return base ? `${r.full_name}@${base}` : r.full_name;
36
+ }
37
+ const dir = r.worktree !== r.full_name.split("/").at(-1);
38
+ return {
39
+ repo: r.full_name,
40
+ ...(dir && { dir: r.worktree }),
41
+ ...(base && { base }),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * The row for an entry, or null when it is not one. The remote's default
47
+ * branch is never in the file: `base_branch`/`default_branch` come back
48
+ * empty when not pinned and the server fills them from the clone.
49
+ * @param {unknown} entry @returns {RepoRow | null}
50
+ */
51
+ export function decodeRepo(entry) {
52
+ let full = "";
53
+ let dir;
54
+ let base;
55
+ if (typeof entry === "string") {
56
+ const at = entry.lastIndexOf("@");
57
+ full = at > 0 ? entry.slice(0, at) : entry;
58
+ base = at > 0 ? entry.slice(at + 1) : undefined;
59
+ } else if (entry && typeof entry === "object") {
60
+ const o = /** @type {Record<string, unknown>} */ (entry);
61
+ // Format-1 rows and API shapes spell the name differently.
62
+ full = String(o.repo ?? o.full_name ?? o.fullName ?? "");
63
+ dir = typeof o.dir === "string" ? o.dir : undefined;
64
+ if (typeof o.worktree === "string") dir = o.worktree;
65
+ base = typeof o.base === "string" ? o.base : undefined;
66
+ if (typeof o.base_branch === "string") base = o.base_branch;
67
+ } else return null;
68
+ full = full.trim();
69
+ if (!REPO_RE.test(full)) return null;
70
+ // A host is case-insensitive; the configured list is lowercase.
71
+ const parts = full.split("/");
72
+ if (parts.length === 3)
73
+ full = [parts[0].toLowerCase(), parts[1], parts[2]].join("/");
74
+ if (base !== undefined && !BRANCH_RE.test(base)) return null;
75
+ if (dir !== undefined && !DIR_RE.test(dir)) return null;
76
+ return {
77
+ full_name: full,
78
+ worktree: dir || full.split("/").at(-1),
79
+ base_branch: base || "",
80
+ default_branch:
81
+ entry &&
82
+ typeof entry === "object" &&
83
+ typeof entry.default_branch === "string"
84
+ ? entry.default_branch
85
+ : base || "",
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The invite for a workspace: its manifest entry, pared to what a join
91
+ * needs. Repos carry no base — the joiner's base is the branch.
92
+ * @param {{ name: string, branch: string, repos: string[] }} ws
93
+ * @returns {string}
94
+ */
95
+ export function formatInvite(ws) {
96
+ return (
97
+ JSON.stringify(
98
+ { name: ws.name, branch: ws.branch, repos: ws.repos },
99
+ null,
100
+ 1,
101
+ ) + "\n"
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Read an invite: a workspace entry, or a whole backup that holds one
107
+ * workspace. Throws with a reason a person can act on.
108
+ * @param {string} text
109
+ * @returns {{ name: string, branch: string, repos: string[] }}
110
+ */
111
+ export function parseInvite(text) {
112
+ const raw = String(text ?? "").trim();
113
+ if (!raw) throw new Error("paste a workspace invite");
114
+ let obj;
115
+ try {
116
+ obj = JSON.parse(raw);
117
+ } catch {
118
+ throw new Error("not an invite (expected JSON)");
119
+ }
120
+ if (!obj || typeof obj !== "object") throw new Error("not an invite");
121
+ if (Array.isArray(obj.workspaces)) {
122
+ if (obj.workspaces.length !== 1)
123
+ throw new Error(
124
+ obj.workspaces.length
125
+ ? "this is a whole backup — restore it from Settings, or paste one workspace entry"
126
+ : "this backup holds no workspaces",
127
+ );
128
+ obj = obj.workspaces[0];
129
+ if (!obj || typeof obj !== "object") throw new Error("not an invite");
130
+ }
131
+ const branch = String(obj.branch ?? "").trim();
132
+ if (!branch) throw new Error("the invite has no branch");
133
+ if (!BRANCH_RE.test(branch)) throw new Error(`not a branch name: ${branch}`);
134
+ const entries = Array.isArray(obj.repos) ? obj.repos : [];
135
+ const repos = [];
136
+ for (const e of entries) {
137
+ const r = decodeRepo(e);
138
+ if (!r) throw new Error(`not a repo: ${JSON.stringify(e)}`);
139
+ if (!repos.includes(r.full_name)) repos.push(r.full_name);
140
+ }
141
+ if (!repos.length) throw new Error("the invite lists no repos");
142
+ const name = String(obj.name ?? "").trim();
143
+ return { name: name || branch, branch, repos };
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dylanpiercey/work",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "type": "module",
5
5
  "description": "Web GUI for agentic dev workspaces",
6
6
  "license": "MIT",
@@ -29,7 +29,9 @@
29
29
  "postinstall": "chmod +x node_modules/node-pty/prebuilds/*/spawn-helper node_modules/node-pty/build/Release/spawn-helper 2>/dev/null || true",
30
30
  "build": "node scripts/agents-catalog.mjs && marko-run build src/entry.ts",
31
31
  "check": "node scripts/agents-catalog.mjs && mtc",
32
- "preview": "PORT=7171 node dist/index.mjs"
32
+ "preview": "PORT=7171 node dist/index.mjs",
33
+ "format": "prettier . -w",
34
+ "format:check": "prettier --check ."
33
35
  },
34
36
  "dependencies": {
35
37
  "node-pty": "^1.1.0"
@@ -56,6 +58,8 @@
56
58
  "iconoir": "^7.12.1",
57
59
  "marko": "^6.3.43",
58
60
  "playwright": "^1.62.1",
61
+ "prettier": "^3.9.6",
62
+ "prettier-plugin-marko": "^4.1.0",
59
63
  "serve-static": "^2.2.1",
60
64
  "typescript": "^6.0.3",
61
65
  "valibot": "^1.4.2",