@ghfs/cli 0.0.4 → 0.1.1

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 (335) hide show
  1. package/dist/cli.mjs +132 -3068
  2. package/dist/dir.d.mts +4 -0
  3. package/dist/dir.mjs +5 -0
  4. package/dist/{factory-DYHQBeCz.mjs → factory-DmX3S7tf.mjs} +350 -5
  5. package/dist/index.d.mts +2 -195
  6. package/dist/index.mjs +1 -1
  7. package/dist/provider-DCIsHVeA.d.mts +249 -0
  8. package/dist/server/index.d.mts +330 -0
  9. package/dist/server/index.mjs +2 -0
  10. package/dist/server-BwSU4kqr.mjs +3518 -0
  11. package/dist/ui/200.html +1 -1
  12. package/dist/ui/404.html +1 -1
  13. package/dist/ui/_nuxt/2UxHyX5q.js +1 -0
  14. package/dist/ui/_nuxt/32ctXXKs.js +1 -0
  15. package/dist/ui/_nuxt/3e1v2bzS.js +1 -0
  16. package/dist/ui/_nuxt/4A_iFExJ.js +1 -0
  17. package/dist/ui/_nuxt/5VeTt0Ye.js +1 -0
  18. package/dist/ui/_nuxt/5i3qLPDT.js +1 -0
  19. package/dist/ui/_nuxt/85-TOEBH.js +1 -0
  20. package/dist/ui/_nuxt/8syuri0t.js +1 -0
  21. package/dist/ui/_nuxt/B0m2ddpp.js +1 -0
  22. package/dist/ui/_nuxt/B1dDrJ26.js +1 -0
  23. package/dist/ui/_nuxt/B1yitclQ.js +1 -0
  24. package/dist/ui/_nuxt/B5q1GTce.js +1 -0
  25. package/dist/ui/_nuxt/B6aJPvgy.js +1 -0
  26. package/dist/ui/_nuxt/B6eN_cS4.js +1 -0
  27. package/dist/ui/_nuxt/B7c-h3xW.js +1 -0
  28. package/dist/ui/_nuxt/B7mTdjB0.js +1 -0
  29. package/dist/ui/_nuxt/BA47KaF1.js +1 -0
  30. package/dist/ui/_nuxt/BBf5iR-q.js +1 -0
  31. package/dist/ui/_nuxt/BEDo0Tqx.js +1 -0
  32. package/dist/ui/_nuxt/BERRCDM3.js +1 -0
  33. package/dist/ui/_nuxt/BETggiCN.js +1 -0
  34. package/dist/ui/_nuxt/BEwlwnbL.js +1 -0
  35. package/dist/ui/_nuxt/BFfxhgS-.js +1 -0
  36. package/dist/ui/_nuxt/BGJmEYvX.js +1 -0
  37. package/dist/ui/_nuxt/BH7IYjvW.js +1 -0
  38. package/dist/ui/_nuxt/BIGW1oBm.js +1 -0
  39. package/dist/ui/_nuxt/BIv1doCn.js +1 -0
  40. package/dist/ui/_nuxt/BJDFO7_C.js +1 -0
  41. package/dist/ui/_nuxt/BLtJtn59.js +1 -0
  42. package/dist/ui/_nuxt/BM1_JUlF.js +1 -0
  43. package/dist/ui/_nuxt/BMWR74SV.js +1 -0
  44. package/dist/ui/_nuxt/BPQ3VLAy.js +1 -0
  45. package/dist/ui/_nuxt/BQ8w6xss.js +1 -0
  46. package/dist/ui/_nuxt/BRHolxvo.js +1 -0
  47. package/dist/ui/_nuxt/BRZ36xJF.js +1 -0
  48. package/dist/ui/_nuxt/BTJTHyun.js +1 -0
  49. package/dist/ui/_nuxt/BTifaqeh.js +1 -0
  50. package/dist/ui/_nuxt/BUtzH8cE.js +1 -0
  51. package/dist/ui/_nuxt/BUw7H-hv.js +1 -0
  52. package/dist/ui/_nuxt/BV7otONQ.js +1 -0
  53. package/dist/ui/_nuxt/BVQ-GDCI.js +1 -0
  54. package/dist/ui/_nuxt/BVkGXMyj.js +1 -0
  55. package/dist/ui/_nuxt/BWvSN4gD.js +1 -0
  56. package/dist/ui/_nuxt/BXkSAIEj.js +1 -0
  57. package/dist/ui/_nuxt/BYunw83y.js +1 -0
  58. package/dist/ui/_nuxt/BZvkOJ9d.js +1 -0
  59. package/dist/ui/_nuxt/B_m7g4N7.js +1 -0
  60. package/dist/ui/_nuxt/BcOcwvcX.js +1 -0
  61. package/dist/ui/_nuxt/BcVCzyr7.js +1 -0
  62. package/dist/ui/_nuxt/BcllPdc-.js +1 -0
  63. package/dist/ui/_nuxt/BdImnpbu.js +1 -0
  64. package/dist/ui/_nuxt/BdnUsdx6.js +1 -0
  65. package/dist/ui/_nuxt/BeJSdlF9.js +1 -0
  66. package/dist/ui/_nuxt/BfHTSMKl.js +1 -0
  67. package/dist/ui/_nuxt/BfSCyJF4.js +1 -0
  68. package/dist/ui/_nuxt/BfjtVDDH.js +1 -0
  69. package/dist/ui/_nuxt/BgDCqdQA.js +1 -0
  70. package/dist/ui/_nuxt/BhOHFoWU.js +1 -0
  71. package/dist/ui/_nuxt/BhrAK1vL.js +1 -0
  72. package/dist/ui/_nuxt/BkioyH1T.js +1 -0
  73. package/dist/ui/_nuxt/Bkuqu6BP.js +1 -0
  74. package/dist/ui/_nuxt/BmXAJ9_W.js +1 -0
  75. package/dist/ui/_nuxt/Bmn6On1c.js +1 -0
  76. package/dist/ui/_nuxt/Bp3cYrEr.js +1 -0
  77. package/dist/ui/_nuxt/Bp6g37R7.js +1 -0
  78. package/dist/ui/_nuxt/BqNa2AkI.js +1 -0
  79. package/dist/ui/_nuxt/BqTXFGrv.js +1 -0
  80. package/dist/ui/_nuxt/BqYA7rlc.js +1 -0
  81. package/dist/ui/_nuxt/Br6cN0cg.js +1 -0
  82. package/dist/ui/_nuxt/BsS91CYL.js +1 -0
  83. package/dist/ui/_nuxt/BshV_Xbc.js +1 -0
  84. package/dist/ui/_nuxt/BspZqrRM.js +1 -0
  85. package/dist/ui/_nuxt/BtCnVYZw.js +1 -0
  86. package/dist/ui/_nuxt/BtOb2qkB.js +1 -0
  87. package/dist/ui/_nuxt/BthQWCQV.js +1 -0
  88. package/dist/ui/_nuxt/BtqSS_iP.js +1 -0
  89. package/dist/ui/_nuxt/Buea-lGh.js +1 -0
  90. package/dist/ui/_nuxt/Bv_4Rxtq.js +1 -0
  91. package/dist/ui/_nuxt/BvzEVeQv.js +1 -0
  92. package/dist/ui/_nuxt/Bw305WKR.js +1 -0
  93. package/dist/ui/_nuxt/BxgE0vQu.js +1 -0
  94. package/dist/ui/_nuxt/BzJJZx-M.js +1 -0
  95. package/dist/ui/_nuxt/C-HG3fhB.js +1 -0
  96. package/dist/ui/_nuxt/C-SQnVFl.js +1 -0
  97. package/dist/ui/_nuxt/C0HS_06l.js +1 -0
  98. package/dist/ui/_nuxt/C0hk2d4L.js +1 -0
  99. package/dist/ui/_nuxt/C151Ov-r.js +1 -0
  100. package/dist/ui/_nuxt/C27-OAKa.js +1 -0
  101. package/dist/ui/_nuxt/C2t-YnRu.js +1 -0
  102. package/dist/ui/_nuxt/C39BiMTA.js +1 -0
  103. package/dist/ui/_nuxt/C3B-1QV4.js +1 -0
  104. package/dist/ui/_nuxt/C3Wv6jpd.js +1 -0
  105. package/dist/ui/_nuxt/C3mMm8J8.js +1 -0
  106. package/dist/ui/_nuxt/C4EeE6gA.js +1 -0
  107. package/dist/ui/_nuxt/C4IJs8-o.js +1 -0
  108. package/dist/ui/_nuxt/C4gqWexZ.js +1 -0
  109. package/dist/ui/_nuxt/C4zNGwhW.js +1 -0
  110. package/dist/ui/_nuxt/C5BYcBs_.js +1 -0
  111. package/dist/ui/_nuxt/C5YyOfLZ.js +1 -0
  112. package/dist/ui/_nuxt/C7UmGho8.js +1 -0
  113. package/dist/ui/_nuxt/C7zT0LnQ.js +1 -0
  114. package/dist/ui/_nuxt/C8M2exoo.js +1 -0
  115. package/dist/ui/_nuxt/C8lEn-DE.js +1 -0
  116. package/dist/ui/_nuxt/C98Dy4si.js +1 -0
  117. package/dist/ui/_nuxt/C9dUb6Cb.js +1 -0
  118. package/dist/ui/_nuxt/C9oPPf7i.js +1 -0
  119. package/dist/ui/_nuxt/C9tS-k6U.js +1 -0
  120. package/dist/ui/_nuxt/CAFt9gP4.js +1 -0
  121. package/dist/ui/_nuxt/CANp3yme.js +1 -0
  122. package/dist/ui/_nuxt/CBaQQc7g.js +1 -0
  123. package/dist/ui/_nuxt/CDBrVQLm.js +1 -0
  124. package/dist/ui/_nuxt/CDVJQ6XC.js +1 -0
  125. package/dist/ui/_nuxt/CDx5xZoG.js +1 -0
  126. package/dist/ui/_nuxt/CEL-wOlO.js +1 -0
  127. package/dist/ui/_nuxt/CEu0bR-o.js +1 -0
  128. package/dist/ui/_nuxt/CFHQjOhq.js +1 -0
  129. package/dist/ui/_nuxt/CFbOZP3I.js +75 -0
  130. package/dist/ui/_nuxt/CG6Dc4jp.js +1 -0
  131. package/dist/ui/_nuxt/CG8Ifv2g.js +1 -0
  132. package/dist/ui/_nuxt/CH1njM8p.js +1 -0
  133. package/dist/ui/_nuxt/CHLpvVh8.js +1 -0
  134. package/dist/ui/_nuxt/CHM0blh-.js +1 -0
  135. package/dist/ui/_nuxt/CJc9bBzg.js +1 -0
  136. package/dist/ui/_nuxt/CKIfxQSi.js +1 -0
  137. package/dist/ui/_nuxt/CLj8gQPS.js +1 -0
  138. package/dist/ui/_nuxt/CLxacb5B.js +1 -0
  139. package/dist/ui/_nuxt/CMTm3GFP.js +1 -0
  140. package/dist/ui/_nuxt/CO1LY3CK.js +1 -0
  141. package/dist/ui/_nuxt/COcwbKMJ.js +1 -0
  142. package/dist/ui/_nuxt/COkxafJQ.js +1 -0
  143. package/dist/ui/_nuxt/COt5Ahok.js +1 -0
  144. package/dist/ui/_nuxt/CRBrlGZW.js +1 -0
  145. package/dist/ui/_nuxt/CS3Unz2-.js +1 -0
  146. package/dist/ui/_nuxt/CSXwinHm.js +1 -0
  147. package/dist/ui/_nuxt/CTRr51gU.js +1 -0
  148. package/dist/ui/_nuxt/CUuTKBJd.js +1 -0
  149. package/dist/ui/_nuxt/CVO1_9PV.js +1 -0
  150. package/dist/ui/_nuxt/CVdnzihN.js +1 -0
  151. package/dist/ui/_nuxt/CXtECtnM.js +1 -0
  152. package/dist/ui/_nuxt/CXvaQtF9.js +1 -0
  153. package/dist/ui/_nuxt/CYsAdtH9.js +1 -0
  154. package/dist/ui/_nuxt/CafNBF8u.js +1 -0
  155. package/dist/ui/_nuxt/CbFg5uaA.js +1 -0
  156. package/dist/ui/_nuxt/CbfX1IO0.js +1 -0
  157. package/dist/ui/_nuxt/CcsQSqEB.js +1 -0
  158. package/dist/ui/_nuxt/CeAyd5Ju.js +1 -0
  159. package/dist/ui/_nuxt/CeZK1NFH.js +1 -0
  160. package/dist/ui/_nuxt/CenWIFCC.js +1 -0
  161. package/dist/ui/_nuxt/CfQXZHmo.js +1 -0
  162. package/dist/ui/_nuxt/Cg-RD9OK.js +1 -0
  163. package/dist/ui/_nuxt/ChMvpjG-.js +1 -0
  164. package/dist/ui/_nuxt/Cj5Yp3dK.js +1 -0
  165. package/dist/ui/_nuxt/CkByrt1z.js +1 -0
  166. package/dist/ui/_nuxt/CkXjmgJE.js +1 -0
  167. package/dist/ui/_nuxt/CklMAg4u.js +1 -0
  168. package/dist/ui/_nuxt/Cmh6b_Ma.js +1 -0
  169. package/dist/ui/_nuxt/CnnmHF94.js +1 -0
  170. package/dist/ui/_nuxt/CnsnAmq5.js +1 -0
  171. package/dist/ui/_nuxt/Co6uUVPk.js +1 -0
  172. package/dist/ui/_nuxt/CoDkCxhg.js +1 -0
  173. package/dist/ui/_nuxt/Cp-IABpG.js +1 -0
  174. package/dist/ui/_nuxt/Cp8Y5tLI.js +1 -0
  175. package/dist/ui/_nuxt/Cq5zzVJU.js +1 -0
  176. package/dist/ui/_nuxt/CquLrc37.js +1 -0
  177. package/dist/ui/_nuxt/Cs0ovY-E.js +1 -0
  178. package/dist/ui/_nuxt/CsfeWuGM.js +1 -0
  179. package/dist/ui/_nuxt/Csfq5Kiy.js +1 -0
  180. package/dist/ui/_nuxt/CuPHTKiy.js +1 -0
  181. package/dist/ui/_nuxt/CufHLc7y.js +1 -0
  182. package/dist/ui/_nuxt/Cuk6v7N8.js +1 -0
  183. package/dist/ui/_nuxt/Cvjx9yec.js +1 -0
  184. package/dist/ui/_nuxt/CwoSXNpI.js +1 -0
  185. package/dist/ui/_nuxt/CxGSJlkm.js +1 -0
  186. package/dist/ui/_nuxt/CxbxFI8M.js +1 -0
  187. package/dist/ui/_nuxt/CyktbL80.js +1 -0
  188. package/dist/ui/_nuxt/CylS5w8V.js +1 -0
  189. package/dist/ui/_nuxt/Cz2AlsmD.js +1 -0
  190. package/dist/ui/_nuxt/CzTSHFRz.js +1 -0
  191. package/dist/ui/_nuxt/D-2ljcwZ.js +1 -0
  192. package/dist/ui/_nuxt/D0YGMca9.js +1 -0
  193. package/dist/ui/_nuxt/D0r3Knsf.js +1 -0
  194. package/dist/ui/_nuxt/D17OF-Vu.js +1 -0
  195. package/dist/ui/_nuxt/D1j8_8rp.js +1 -0
  196. package/dist/ui/_nuxt/D2j5LXpq.js +1 -0
  197. package/dist/ui/_nuxt/D3apom_0.js +1 -0
  198. package/dist/ui/_nuxt/D3lLCCz7.js +1 -0
  199. package/dist/ui/_nuxt/D4Qn4bBI.js +1 -0
  200. package/dist/ui/_nuxt/D4_iv3hh.js +1 -0
  201. package/dist/ui/_nuxt/D4h5O-jR.js +1 -0
  202. package/dist/ui/_nuxt/D5-asLiD.js +1 -0
  203. package/dist/ui/_nuxt/D53aC0YG.js +1 -0
  204. package/dist/ui/_nuxt/D5KoaKCx.js +1 -0
  205. package/dist/ui/_nuxt/D7o27uSR.js +1 -0
  206. package/dist/ui/_nuxt/D7oLnXFd.js +1 -0
  207. package/dist/ui/_nuxt/D82EKSYY.js +1 -0
  208. package/dist/ui/_nuxt/D82vCrfD.js +1 -0
  209. package/dist/ui/_nuxt/D87Tk5Gz.js +1 -0
  210. package/dist/ui/_nuxt/D8_7TLub.js +1 -0
  211. package/dist/ui/_nuxt/D8l8udqQ.js +1 -0
  212. package/dist/ui/_nuxt/D8rZpOte.js +1 -0
  213. package/dist/ui/_nuxt/D93ZcfNL.js +1 -0
  214. package/dist/ui/_nuxt/D97Zzqfu.js +1 -0
  215. package/dist/ui/_nuxt/DAi9KRSo.js +1 -0
  216. package/dist/ui/_nuxt/DElX3lK9.js +153 -0
  217. package/dist/ui/_nuxt/DFQXde-d.js +1 -0
  218. package/dist/ui/_nuxt/DFWUc33u.js +1 -0
  219. package/dist/ui/_nuxt/DFXneXwc.js +1 -0
  220. package/dist/ui/_nuxt/DGztddWO.js +1 -0
  221. package/dist/ui/_nuxt/DH5Ifo-i.js +1 -0
  222. package/dist/ui/_nuxt/DHCkPAjA.js +1 -0
  223. package/dist/ui/_nuxt/DHJKELXO.js +1 -0
  224. package/dist/ui/_nuxt/DHQR4-dF.js +1 -0
  225. package/dist/ui/_nuxt/DM8c43g1.js +1 -0
  226. package/dist/ui/_nuxt/DMk3OuwW.js +1 -0
  227. package/dist/ui/_nuxt/DMzUqQB5.js +1 -0
  228. package/dist/ui/_nuxt/DN-Z_aST.js +1 -0
  229. package/dist/ui/_nuxt/DQyhUUbL.js +1 -0
  230. package/dist/ui/_nuxt/DRg8JJMk.js +1 -0
  231. package/dist/ui/_nuxt/DRw_LuNl.js +1 -0
  232. package/dist/ui/_nuxt/DSSpBswy.js +1 -0
  233. package/dist/ui/_nuxt/DT3CNIhV.js +1 -0
  234. package/dist/ui/_nuxt/DU1UobuO.js +1 -0
  235. package/dist/ui/_nuxt/DUszq2jm.js +1 -0
  236. package/dist/ui/_nuxt/DV7GczEv.js +1 -0
  237. package/dist/ui/_nuxt/DVFEvuxE.js +1 -0
  238. package/dist/ui/_nuxt/DVMEJ2y_.js +1 -0
  239. package/dist/ui/_nuxt/DVxCFoDh.js +1 -0
  240. package/dist/ui/_nuxt/DWX1VroF.js +1 -0
  241. package/dist/ui/_nuxt/DWedfzmr.js +1 -0
  242. package/dist/ui/_nuxt/DWrx1Km3.js +1 -0
  243. package/dist/ui/_nuxt/DXbdFlpD.js +1 -0
  244. package/dist/ui/_nuxt/DXmwc3jG.js +1 -0
  245. package/dist/ui/_nuxt/DXvB9xmW.js +1 -0
  246. package/dist/ui/_nuxt/DYE7WIF3.js +1 -0
  247. package/dist/ui/_nuxt/DZ9PSxVF.js +1 -0
  248. package/dist/ui/_nuxt/DZxFcAj9.js +1 -0
  249. package/dist/ui/_nuxt/D_Q5rh1f.js +1 -0
  250. package/dist/ui/_nuxt/Da5cRb03.js +1 -0
  251. package/dist/ui/_nuxt/DbWOdLaN.js +1 -0
  252. package/dist/ui/_nuxt/Dc-5oQ3p.js +1 -0
  253. package/dist/ui/_nuxt/DcWK9jms.js +1 -0
  254. package/dist/ui/_nuxt/DcaNXYhu.js +1 -0
  255. package/dist/ui/_nuxt/Dd19v3D-.js +1 -0
  256. package/dist/ui/_nuxt/DdkO51Og.js +1 -0
  257. package/dist/ui/_nuxt/Ddv68eIx.js +1 -0
  258. package/dist/ui/_nuxt/Des-eS-w.js +1 -0
  259. package/dist/ui/_nuxt/Df6bDoY_.js +1 -0
  260. package/dist/ui/_nuxt/DfNjGIrv.js +1 -0
  261. package/dist/ui/_nuxt/DhaVEd23.js +1 -0
  262. package/dist/ui/_nuxt/DjAJT7YJ.js +1 -0
  263. package/dist/ui/_nuxt/DkFqJrB1.js +1 -0
  264. package/dist/ui/_nuxt/DkwncUOv.js +1 -0
  265. package/dist/ui/_nuxt/DlAUqK2U.js +1 -0
  266. package/dist/ui/_nuxt/DnF83RMp.js +1 -0
  267. package/dist/ui/_nuxt/DnULxvSX.js +1 -0
  268. package/dist/ui/_nuxt/Dpen1YoG.js +1 -0
  269. package/dist/ui/_nuxt/Dph4kLrZ.js +1 -0
  270. package/dist/ui/_nuxt/DqQDbK_p.js +1 -0
  271. package/dist/ui/_nuxt/DqwNpetd.js +1 -0
  272. package/dist/ui/_nuxt/DsOJ9woJ.js +1 -0
  273. package/dist/ui/_nuxt/Dspwwk_N.js +1 -0
  274. package/dist/ui/_nuxt/Dx-B1_4e.js +1 -0
  275. package/dist/ui/_nuxt/DyxjwDmM.js +1 -0
  276. package/dist/ui/_nuxt/E3gJ1_iC.js +1 -0
  277. package/dist/ui/_nuxt/GsRaNv29.js +1 -0
  278. package/dist/ui/_nuxt/IF9eRakj.js +1 -0
  279. package/dist/ui/_nuxt/IeuSbFQv.js +1 -0
  280. package/dist/ui/_nuxt/JZbLTBME.js +1 -0
  281. package/dist/ui/_nuxt/L9t79GZl.js +1 -0
  282. package/dist/ui/_nuxt/MzD3tlZU.js +1 -0
  283. package/dist/ui/_nuxt/P80f7IUj.js +1 -0
  284. package/dist/ui/_nuxt/Pmp26Uib.js +1 -0
  285. package/dist/ui/_nuxt/QIJgUcNo.js +1 -0
  286. package/dist/ui/_nuxt/VOosw3JB.js +1 -0
  287. package/dist/ui/_nuxt/Ve4PFQV2.js +1 -0
  288. package/dist/ui/_nuxt/W9tJ9s81.js +1 -0
  289. package/dist/ui/_nuxt/Yzrsuije.js +1 -0
  290. package/dist/ui/_nuxt/_H4v1dQx.js +1 -0
  291. package/dist/ui/_nuxt/_ykCGR6B.js +1 -0
  292. package/dist/ui/_nuxt/bCR0ucgS.js +1 -0
  293. package/dist/ui/_nuxt/bE4Kk8sk.js +1 -0
  294. package/dist/ui/_nuxt/bN70gL4F.js +1 -0
  295. package/dist/ui/_nuxt/builds/latest.json +1 -1
  296. package/dist/ui/_nuxt/builds/meta/12d68071-f4db-447f-b929-9033d66eadc3.json +1 -0
  297. package/dist/ui/_nuxt/dwOrl1Do.js +1 -0
  298. package/dist/ui/_nuxt/entry.DfrFFNxd.css +1 -0
  299. package/dist/ui/_nuxt/eo99z4R2.js +1 -0
  300. package/dist/ui/_nuxt/error-404.C93cll2q.css +1 -0
  301. package/dist/ui/_nuxt/error-500.CPQqaJsC.css +1 -0
  302. package/dist/ui/_nuxt/fuZLfV_i.js +1 -0
  303. package/dist/ui/_nuxt/g9-lgVsj.js +1 -0
  304. package/dist/ui/_nuxt/gcz8RCvz.js +1 -0
  305. package/dist/ui/_nuxt/hJgmCMqR.js +1 -0
  306. package/dist/ui/_nuxt/hegEt444.js +1 -0
  307. package/dist/ui/_nuxt/k_qm7-4y.js +1 -0
  308. package/dist/ui/_nuxt/lBpI9LM0.js +1 -0
  309. package/dist/ui/_nuxt/lXgVvXCa.js +1 -0
  310. package/dist/ui/_nuxt/mWjccvbQ.js +1 -0
  311. package/dist/ui/_nuxt/n2N0HUVH.js +1 -0
  312. package/dist/ui/_nuxt/oH_c3LbQ.js +1 -0
  313. package/dist/ui/_nuxt/qdsjHGoJ.js +1 -0
  314. package/dist/ui/_nuxt/rGO070M0.js +1 -0
  315. package/dist/ui/_nuxt/rZm6bMo-.js +1 -0
  316. package/dist/ui/_nuxt/rgL8RFrC.js +1 -0
  317. package/dist/ui/_nuxt/u5AG7uiY.js +1 -0
  318. package/dist/ui/_nuxt/uYugtg8r.js +1 -0
  319. package/dist/ui/_nuxt/vE_lwT2v.js +1 -0
  320. package/dist/ui/_nuxt/vGWfd6FD.js +1 -0
  321. package/dist/ui/_nuxt/wDzz0qaB.js +1 -0
  322. package/dist/ui/_nuxt/wEQ09or8.js +1 -0
  323. package/dist/ui/_nuxt/yv6CvBhz.js +1 -0
  324. package/dist/ui/index.html +1 -1
  325. package/package.json +21 -22
  326. package/dist/ui/_nuxt/BR_H8Y5I.js +0 -3
  327. package/dist/ui/_nuxt/Cr8ZhsI1.js +0 -3
  328. package/dist/ui/_nuxt/D7-M_06k.js +0 -1
  329. package/dist/ui/_nuxt/DFs1tY5L.js +0 -2
  330. package/dist/ui/_nuxt/DSg5v247.js +0 -1
  331. package/dist/ui/_nuxt/_eJq5waD.js +0 -1
  332. package/dist/ui/_nuxt/builds/meta/560d1a3b-6050-40f3-af12-eecaef34ee9e.json +0 -1
  333. package/dist/ui/_nuxt/entry.CpgMGkX_.css +0 -1
  334. package/dist/ui/_nuxt/error-404.Cl81wDDB.css +0 -1
  335. package/dist/ui/_nuxt/error-500.eY3F7Rvm.css +0 -1
package/dist/cli.mjs CHANGED
@@ -1,23 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as formatIssueNumber, i as formatDuration, n as normalizeReactions, o as formatTerminalLink$1, r as countNoun, s as formatValue, t as createRepositoryProvider } from "./factory-DYHQBeCz.mjs";
2
+ import { c as CodedError, i as formatDuration, o as formatTerminalLink$1, r as countNoun, s as formatValue, u as log } from "./factory-DmX3S7tf.mjs";
3
+ import { a as executePendingChanges, c as GHFS_NAME, d as ACTIONS_COLOR_HEX, f as pathExists, h as resolveConfig, i as appendExecutionResult, l as GHFS_VERSION, m as getStorageDirAbsolute, n as slugifyRepoName, o as isExecuteCancelledError, p as getExecuteFile, r as syncRepository, s as loadSyncState, t as createUiServer, u as ensureExecuteArtifacts } from "./server-BwSU4kqr.mjs";
3
4
  import process from "node:process";
4
5
  import { cac } from "cac";
5
- import { basename, dirname, extname, isAbsolute, join, normalize, resolve } from "pathe";
6
+ import { resolve } from "pathe";
6
7
  import { execFile } from "node:child_process";
7
8
  import { promisify } from "node:util";
8
- import { existsSync } from "node:fs";
9
- import { createJiti } from "jiti";
10
- import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
- import * as v from "valibot";
12
- import { parse, stringify } from "yaml";
13
- import { randomBytes } from "node:crypto";
9
+ import { readFile } from "node:fs/promises";
14
10
  import c from "ansis";
15
11
  import * as p from "@clack/prompts";
16
12
  import { cancel, confirm, isCancel, multiselect, password, select } from "@clack/prompts";
17
- import { createServer } from "node:http";
18
- import { createBirpcGroup } from "birpc";
19
- import { parse as parse$1, stringify as stringify$1 } from "structured-clone-es";
20
- import { WebSocketServer } from "ws";
21
13
  //#region src/config/auth.ts
22
14
  const execFileAsync$1 = promisify(execFile);
23
15
  async function resolveAuthToken(options) {
@@ -27,11 +19,11 @@ async function resolveAuthToken(options) {
27
19
  if (token) return token;
28
20
  const envToken = await readTokenFromEnv();
29
21
  if (envToken) return envToken;
30
- if (!options.interactive || !process.stdin.isTTY) throw new Error("Missing GitHub token. Set GH_TOKEN/GITHUB_TOKEN or run gh auth login.");
31
- if (!options.promptForToken) throw new Error("Missing GitHub token. Set GH_TOKEN/GITHUB_TOKEN or run gh auth login.");
22
+ if (!options.interactive || !process.stdin.isTTY) throw new CodedError(log.GHFS0001());
23
+ if (!options.promptForToken) throw new CodedError(log.GHFS0001());
32
24
  const promptedToken = await options.promptForToken();
33
25
  if (promptedToken?.trim()) return promptedToken.trim();
34
- throw new Error("Token prompt cancelled");
26
+ throw new CodedError(log.GHFS0002());
35
27
  }
36
28
  async function readTokenFromGhCli() {
37
29
  try {
@@ -48,143 +40,12 @@ async function readTokenFromEnv() {
48
40
  }
49
41
  }
50
42
  //#endregion
51
- //#region src/constants.ts
52
- const CONFIG_FILE_CANDIDATES = [
53
- "ghfs.config.ts",
54
- "ghfs.config.mts",
55
- "ghfs.config.mjs",
56
- "ghfs.config.js",
57
- "ghfs.config.cjs"
58
- ];
59
- const ISSUE_DIR_NAME = "issues";
60
- const PULL_DIR_NAME = "pulls";
61
- const CLOSED_DIR_NAME = "closed";
62
- const SYNC_STATE_FILE_NAME = ".sync.json";
63
- const ISSUES_INDEX_FILE_NAME = "issues.md";
64
- const PULLS_INDEX_FILE_NAME = "pulls.md";
65
- const REPO_SNAPSHOT_FILE_NAME = "repo.json";
66
- const EXECUTE_FILE_NAME = "execute.yml";
67
- const EXECUTE_MD_FILE_NAME = "execute.md";
68
- const EXECUTE_SCHEMA_RELATIVE_PATH = "schema/execute.schema.json";
69
- //#endregion
70
- //#region src/config/load.ts
71
- async function loadUserConfig(cwd) {
72
- const configPath = findConfigFile(cwd);
73
- if (!configPath) return { config: {} };
74
- return {
75
- path: configPath,
76
- config: extractUserConfig(await createJiti(resolve(cwd, "ghfs.config.ts"), { interopDefault: true }).import(configPath))
77
- };
78
- }
79
- function extractUserConfig(loaded) {
80
- if (!loaded || typeof loaded !== "object") return {};
81
- if ("default" in loaded) {
82
- const config = loaded.default;
83
- if (config && typeof config === "object") return config;
84
- return {};
85
- }
86
- return loaded;
87
- }
88
- async function resolveConfig(options = {}) {
89
- const cwd = options.cwd ?? process.cwd();
90
- const overrides = options.overrides ?? {};
91
- const { config: userConfig } = await loadUserConfig(cwd);
92
- const merged = mergeUserConfig(userConfig, overrides);
93
- const directory = merged.directory ?? ".ghfs";
94
- const configuredToken = merged.auth?.token?.trim() || "";
95
- const repo = merged.repo?.trim() || "";
96
- const issuesEnabled = merged.sync?.issues ?? true;
97
- const pullsEnabled = merged.sync?.pulls ?? true;
98
- const closedMode = merged.sync?.closed ?? false;
99
- const patchesMode = merged.sync?.patches ?? "open";
100
- return {
101
- cwd,
102
- repo,
103
- directory,
104
- auth: { token: configuredToken },
105
- sync: {
106
- issues: issuesEnabled,
107
- pulls: pullsEnabled,
108
- closed: closedMode,
109
- patches: patchesMode
110
- }
111
- };
112
- }
113
- function getStorageDirAbsolute(config) {
114
- return resolve(config.cwd, config.directory);
115
- }
116
- function getExecuteFile(config) {
117
- return join(config.directory, EXECUTE_FILE_NAME);
118
- }
119
- function findConfigFile(cwd) {
120
- for (const candidate of CONFIG_FILE_CANDIDATES) {
121
- const fullPath = resolve(cwd, candidate);
122
- if (existsSync(fullPath)) return fullPath;
123
- }
124
- }
125
- function mergeUserConfig(base, overrides) {
126
- return {
127
- ...base,
128
- ...overrides,
129
- auth: {
130
- ...base.auth,
131
- ...overrides.auth
132
- },
133
- sync: {
134
- ...base.sync,
135
- ...overrides.sync
136
- }
137
- };
138
- }
139
- //#endregion
140
- //#region src/utils/fs.ts
141
- async function pathExists(path) {
142
- try {
143
- await access(path);
144
- return true;
145
- } catch {
146
- return false;
147
- }
148
- }
149
- async function writeFileEnsured(path, content) {
150
- await mkdir(dirname(path), { recursive: true });
151
- await writeFile(path, content, "utf8");
152
- }
153
- async function removePath(path) {
154
- await rm(path, { force: true });
155
- }
156
- async function movePath(from, to) {
157
- await mkdir(dirname(to), { recursive: true });
158
- await rename(from, to);
159
- }
160
- async function removePatchIfExists(storageDirAbsolute, number) {
161
- const pullsDir = join(storageDirAbsolute, PULL_DIR_NAME);
162
- let entries;
163
- try {
164
- entries = await readdir(pullsDir, { withFileTypes: true });
165
- } catch {
166
- return 0;
167
- }
168
- const padded = String(number).padStart(5, "0");
169
- let removed = 0;
170
- for (const entry of entries) {
171
- if (!entry.isFile()) continue;
172
- const fileName = entry.name;
173
- const isLegacyPatch = fileName === `${number}.patch`;
174
- const isCurrentPatch = fileName.startsWith(`${padded}-`) && fileName.endsWith(".patch");
175
- if (!isLegacyPatch && !isCurrentPatch) continue;
176
- await removePath(join(pullsDir, fileName));
177
- removed += 1;
178
- }
179
- return removed;
180
- }
181
- //#endregion
182
43
  //#region src/config/repo.ts
183
44
  const execFileAsync = promisify(execFile);
184
45
  async function resolveRepo(options) {
185
46
  if (options.cliRepo) {
186
47
  const repo = normalizeRepo(options.cliRepo);
187
- if (!repo) throw new Error(`Invalid --repo value: ${options.cliRepo}`);
48
+ if (!repo) throw new CodedError(log.GHFS0010({ value: options.cliRepo }));
188
49
  return {
189
50
  repo,
190
51
  source: "cli",
@@ -193,7 +54,7 @@ async function resolveRepo(options) {
193
54
  }
194
55
  if (options.configRepo) {
195
56
  const repo = normalizeRepo(options.configRepo);
196
- if (!repo) throw new Error(`Invalid repo in ghfs.config.ts: ${options.configRepo}`);
57
+ if (!repo) throw new CodedError(log.GHFS0011({ value: options.configRepo }));
197
58
  return {
198
59
  repo,
199
60
  source: "config",
@@ -208,16 +69,19 @@ async function resolveRepo(options) {
208
69
  if (gitCandidate && pkgCandidate && gitCandidate.repo !== pkgCandidate.repo) {
209
70
  if (options.interactive && process.stdin.isTTY && options.selectRepoChoice) {
210
71
  const repo = await options.selectRepoChoice(gitCandidate, pkgCandidate);
211
- if (!repo) throw new Error("Repository selection cancelled");
72
+ if (!repo) throw new CodedError(log.GHFS0012());
212
73
  const normalizedRepo = normalizeRepo(repo);
213
- if (!normalizedRepo || normalizedRepo !== gitCandidate.repo && normalizedRepo !== pkgCandidate.repo) throw new Error(`Invalid repository selection: ${repo}`);
74
+ if (!normalizedRepo || normalizedRepo !== gitCandidate.repo && normalizedRepo !== pkgCandidate.repo) throw new CodedError(log.GHFS0013({ value: repo }));
214
75
  return {
215
76
  repo: normalizedRepo,
216
77
  source: normalizedRepo === gitCandidate.repo ? "git" : "package-json",
217
78
  candidates
218
79
  };
219
80
  }
220
- throw new Error(`Repo mismatch detected. git=${gitCandidate.repo} package.json=${pkgCandidate.repo}. Use --repo to disambiguate.`);
81
+ throw new CodedError(log.GHFS0014({
82
+ gitRepo: gitCandidate.repo,
83
+ pkgRepo: pkgCandidate.repo
84
+ }));
221
85
  }
222
86
  if (gitCandidate) return {
223
87
  repo: gitCandidate.repo,
@@ -229,7 +93,7 @@ async function resolveRepo(options) {
229
93
  source: "package-json",
230
94
  candidates
231
95
  };
232
- throw new Error("Could not resolve repository. Provide --repo or set repo in ghfs.config.ts.");
96
+ throw new CodedError(log.GHFS0015());
233
97
  }
234
98
  function normalizeRepo(input) {
235
99
  const trimmed = input.trim();
@@ -258,2290 +122,63 @@ async function detectRepoFromGit(cwd) {
258
122
  return;
259
123
  }
260
124
  const remotes = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
261
- if (!remotes.length) return void 0;
262
- const orderedRemotes = prioritizeRemotes(remotes);
263
- for (const remote of orderedRemotes) try {
264
- const repo = normalizeRepo((await execFileAsync("git", [
265
- "remote",
266
- "get-url",
267
- remote
268
- ], { cwd })).stdout.trim());
269
- if (repo) return {
270
- source: "git",
271
- repo,
272
- detail: `remote:${remote}`
273
- };
274
- } catch {
275
- continue;
276
- }
277
- }
278
- async function detectRepoFromPackageJson(cwd) {
279
- const path = `${cwd}/package.json`;
280
- if (!await pathExists(path)) return void 0;
281
- let parsed;
282
- try {
283
- parsed = JSON.parse(await readFile(path, "utf8"));
284
- } catch {
285
- return;
286
- }
287
- if (!parsed || typeof parsed !== "object") return void 0;
288
- const repository = parsed.repository;
289
- if (typeof repository === "string") {
290
- const repo = normalizeRepo(repository);
291
- if (repo) return {
292
- source: "package-json",
293
- repo,
294
- detail: "package.json#repository"
295
- };
296
- }
297
- if (repository && typeof repository === "object") {
298
- const url = repository.url;
299
- if (typeof url === "string") {
300
- const repo = normalizeRepo(url);
301
- if (repo) return {
302
- source: "package-json",
303
- repo,
304
- detail: "package.json#repository.url"
305
- };
306
- }
307
- }
308
- }
309
- function prioritizeRemotes(remotes) {
310
- const unique = [...new Set(remotes)];
311
- const priority = ["origin", "upstream"];
312
- const prioritized = priority.filter((name) => unique.includes(name));
313
- const rest = unique.filter((name) => !priority.includes(name));
314
- return [...prioritized, ...rest];
315
- }
316
- function stripGitSuffix(name) {
317
- return name.replace(/\.git$/, "");
318
- }
319
- //#endregion
320
- //#region src/execute/actions.ts
321
- const ACTIONS_SUPPORTED = [
322
- "close",
323
- "close-with-comment",
324
- "reopen",
325
- "set-title",
326
- "set-body",
327
- "add-comment",
328
- "add-labels",
329
- "remove-labels",
330
- "set-labels",
331
- "add-assignees",
332
- "remove-assignees",
333
- "set-assignees",
334
- "set-milestone",
335
- "clear-milestone",
336
- "lock",
337
- "unlock",
338
- "request-reviewers",
339
- "remove-reviewers",
340
- "mark-ready-for-review",
341
- "convert-to-draft"
342
- ];
343
- const ACTIONS_ALIAS_MAP = {
344
- "open": "reopen",
345
- "closes": "close",
346
- "close-comment": "close-with-comment",
347
- "comment-close": "close-with-comment",
348
- "close-and-comment": "close-with-comment",
349
- "comment-and-close": "close-with-comment",
350
- "label": "add-labels",
351
- "labels": "add-labels",
352
- "tag": "add-labels",
353
- "tags": "add-labels",
354
- "add-tag": "add-labels",
355
- "assign": "add-assignees",
356
- "assignee": "add-assignees",
357
- "assignees": "add-assignees",
358
- "body": "set-body",
359
- "title": "set-title",
360
- "retitle": "set-title",
361
- "ready": "mark-ready-for-review",
362
- "undraft": "mark-ready-for-review",
363
- "draft": "convert-to-draft",
364
- "comment": "add-comment"
365
- };
366
- const ACTIONS_COLOR_HEX = {
367
- "close": "#ef4444",
368
- "close-with-comment": "#fb7185",
369
- "reopen": "#22c55e",
370
- "set-title": "#3b82f6",
371
- "set-body": "#06b6d4",
372
- "add-comment": "#f97316",
373
- "add-labels": "#84cc16",
374
- "remove-labels": "#f43f5e",
375
- "set-labels": "#eab308",
376
- "add-assignees": "#10b981",
377
- "remove-assignees": "#fb7185",
378
- "set-assignees": "#0ea5e9",
379
- "set-milestone": "#6366f1",
380
- "clear-milestone": "#f59e0b",
381
- "lock": "#a855f7",
382
- "unlock": "#14b8a6",
383
- "request-reviewers": "#38bdf8",
384
- "remove-reviewers": "#e879f9",
385
- "mark-ready-for-review": "#34d399",
386
- "convert-to-draft": "#f472b6"
387
- };
388
- const ACTION_NAMES_SET = new Set(ACTIONS_SUPPORTED);
389
- const ACTION_ALIASES = Object.keys(ACTIONS_ALIAS_MAP);
390
- const ACTION_INPUTS = [...ACTIONS_SUPPORTED, ...ACTION_ALIASES];
391
- function resolveActionName(action) {
392
- const normalized = normalizeActionInput(action);
393
- if (!normalized) return void 0;
394
- if (ACTION_NAMES_SET.has(normalized)) return normalized;
395
- return ACTIONS_ALIAS_MAP[normalized];
396
- }
397
- function normalizeActionInput(action) {
398
- return action.trim().toLowerCase();
399
- }
400
- //#endregion
401
- //#region src/execute/schema.ts
402
- const executeSchema = {
403
- $id: "https://ghfs.dev/schema/execute.json",
404
- type: "array",
405
- items: {
406
- type: "object",
407
- additionalProperties: true,
408
- required: ["number", "action"],
409
- properties: {
410
- number: { type: "number" },
411
- action: {
412
- type: "string",
413
- enum: [...ACTION_INPUTS]
414
- },
415
- ifUnchangedSince: {
416
- type: "string",
417
- format: "date-time"
418
- },
419
- title: { type: "string" },
420
- body: { type: "string" },
421
- labels: {
422
- type: "array",
423
- items: { type: "string" }
424
- },
425
- assignees: {
426
- type: "array",
427
- items: { type: "string" }
428
- },
429
- milestone: { anyOf: [{ type: "string" }, { type: "number" }] },
430
- reviewers: {
431
- type: "array",
432
- items: { type: "string" }
433
- },
434
- reason: {
435
- type: "string",
436
- enum: [
437
- "resolved",
438
- "off-topic",
439
- "too heated",
440
- "too-heated",
441
- "spam"
442
- ]
443
- }
444
- }
445
- }
446
- };
447
- const EXECUTE_FILE_PLACEHOLDER = [
448
- `# yaml-language-server: $schema=./${EXECUTE_SCHEMA_RELATIVE_PATH}`,
449
- "# Add operations as YAML list items, then run: `ghfs execute`, examples:",
450
- "#",
451
- "# - action: close",
452
- "# number: 123",
453
- ""
454
- ].join("\n");
455
- const EXECUTE_MD_FILE_PLACEHOLDER = [
456
- "<!-- Add one action per line, then run: `ghfs execute`, examples: -->",
457
- "",
458
- "<!-- close #123 #124 -->",
459
- "<!-- label #123 bug, triage -->",
460
- "<!-- assign #123 antfu -->",
461
- "<!-- comment #123 \"Need more context\" -->",
462
- "<!-- close-comment #123 \"Closing as completed\" -->",
463
- "<!-- set-title #123 \"new title\" -->",
464
- "<!-- add-tag #123 foo, bar -->",
465
- ""
466
- ].join("\n");
467
- async function writeExecuteSchema(storageDirAbsolute) {
468
- const schemaPath = getExecuteSchemaPath(storageDirAbsolute);
469
- await mkdir(dirname(schemaPath), { recursive: true });
470
- await writeFile(schemaPath, `${JSON.stringify(executeSchema, null, 2)}\n`, "utf8");
471
- return schemaPath;
472
- }
473
- async function ensureExecuteArtifacts(executeFilePath) {
474
- const storageDirAbsolute = dirname(executeFilePath);
475
- const [schemaPath] = await Promise.all([
476
- ensureExecuteSchema(storageDirAbsolute),
477
- ensureExecuteFile(executeFilePath),
478
- ensureExecuteMdFile(storageDirAbsolute)
479
- ]);
480
- return {
481
- executeFilePath,
482
- schemaPath
483
- };
484
- }
485
- async function ensureExecuteSchema(storageDirAbsolute) {
486
- const schemaPath = getExecuteSchemaPath(storageDirAbsolute);
487
- if (await pathExists(schemaPath)) return schemaPath;
488
- return writeExecuteSchema(storageDirAbsolute);
489
- }
490
- async function ensureExecuteFile(executeFilePath) {
491
- if (await pathExists(executeFilePath)) return;
492
- await mkdir(dirname(executeFilePath), { recursive: true });
493
- await writeFile(executeFilePath, EXECUTE_FILE_PLACEHOLDER, "utf8");
494
- }
495
- async function ensureExecuteMdFile(storageDirAbsolute) {
496
- const executeMdPath = join(storageDirAbsolute, EXECUTE_MD_FILE_NAME);
497
- if (await pathExists(executeMdPath)) return;
498
- await mkdir(dirname(executeMdPath), { recursive: true });
499
- await writeFile(executeMdPath, EXECUTE_MD_FILE_PLACEHOLDER, "utf8");
500
- }
501
- function getExecuteSchemaPath(storageDirAbsolute) {
502
- return join(storageDirAbsolute, EXECUTE_SCHEMA_RELATIVE_PATH);
503
- }
504
- //#endregion
505
- //#region src/execute/validate.ts
506
- const executeOpSchema = v.looseObject({
507
- number: v.number(),
508
- action: v.string(),
509
- ifUnchangedSince: v.optional(v.string()),
510
- title: v.optional(v.string()),
511
- body: v.optional(v.string()),
512
- labels: v.optional(v.array(v.string())),
513
- assignees: v.optional(v.array(v.string())),
514
- milestone: v.optional(v.union([v.string(), v.number()])),
515
- reviewers: v.optional(v.array(v.string())),
516
- reason: v.optional(v.picklist([
517
- "resolved",
518
- "off-topic",
519
- "too heated",
520
- "too-heated",
521
- "spam"
522
- ]))
523
- });
524
- const executeFileSchema = v.array(executeOpSchema);
525
- async function readAndValidateExecuteFileWithSource(path) {
526
- const raw = await readFile(path, "utf8");
527
- let parsed;
528
- try {
529
- parsed = parse(raw || "[]") || [];
530
- } catch (error) {
531
- throw new Error(`Failed to parse execute YAML: ${error.message}`);
532
- }
533
- const parsedResult = v.safeParse(executeFileSchema, parsed);
534
- if (!parsedResult.success) {
535
- const message = parsedResult.issues.map((issue) => {
536
- const path = issue.path?.map((segment) => String(segment.key)).join(".");
537
- return `${path ? `${path}: ` : ""}${issue.message}`;
538
- }).join("; ");
539
- throw new Error(`Invalid execute file: ${message}`);
540
- }
541
- const { pending, sourceActions, actionErrors } = normalizeActionInputs(parsedResult.output);
542
- if (actionErrors.length) throw new Error(`Invalid execute file: ${actionErrors.join("; ")}`);
543
- const customErrors = validateExecuteRules(pending);
544
- if (customErrors.length) throw new Error(`Invalid execute file: ${customErrors.join("; ")}`);
545
- return {
546
- ops: pending,
547
- sourceActions
548
- };
549
- }
550
- async function writeExecuteFile(path, pending) {
551
- const content = stringify(pending);
552
- await mkdir(dirname(path), { recursive: true });
553
- await writeFile(path, content.endsWith("\n") ? content : `${content}\n`, "utf8");
554
- }
555
- function validateExecuteRules(pending) {
556
- const errors = [];
557
- for (const [index, op] of pending.entries()) {
558
- const key = `[${index}]`;
559
- errors.push(...validateOperationRules(key, op));
560
- }
561
- return errors;
562
- }
563
- function validateOperationRules(key, op) {
564
- const errors = [];
565
- if (!Number.isInteger(op.number) || op.number <= 0) errors.push(`${key}: number must be a positive integer`);
566
- switch (op.action) {
567
- case "set-title":
568
- if (!isNonEmptyString(op.title)) errors.push(`${key}: set-title requires title`);
569
- break;
570
- case "set-body":
571
- case "add-comment":
572
- case "close-with-comment":
573
- if (!isNonEmptyString(op.body)) errors.push(`${key}: ${op.action} requires body`);
574
- break;
575
- case "add-labels":
576
- case "remove-labels":
577
- case "set-labels":
578
- if (!isStringArray(op.labels)) errors.push(`${key}: ${op.action} requires labels[]`);
579
- break;
580
- case "add-assignees":
581
- case "remove-assignees":
582
- case "set-assignees":
583
- if (!isStringArray(op.assignees)) errors.push(`${key}: ${op.action} requires assignees[]`);
584
- break;
585
- case "set-milestone":
586
- if (!(typeof op.milestone === "string" || typeof op.milestone === "number")) errors.push(`${key}: set-milestone requires milestone`);
587
- break;
588
- case "request-reviewers":
589
- case "remove-reviewers":
590
- if (!isStringArray(op.reviewers)) errors.push(`${key}: ${op.action} requires reviewers[]`);
591
- break;
592
- default: break;
593
- }
594
- if (op.ifUnchangedSince && Number.isNaN(Date.parse(op.ifUnchangedSince))) errors.push(`${key}: ifUnchangedSince must be a valid datetime`);
595
- return errors;
596
- }
597
- function isNonEmptyString(value) {
598
- return typeof value === "string" && value.trim().length > 0;
599
- }
600
- function isStringArray(value) {
601
- return Array.isArray(value) && value.every((entry) => typeof entry === "string") && value.length > 0;
602
- }
603
- function normalizeActionInputs(pending) {
604
- const normalized = [];
605
- const sourceActions = [];
606
- const actionErrors = [];
607
- for (const [index, op] of pending.entries()) {
608
- const sourceAction = op.action;
609
- sourceActions.push(sourceAction);
610
- const action = resolveActionName(sourceAction);
611
- if (!action) {
612
- actionErrors.push(`[${index}]: unknown action: ${sourceAction}`);
613
- continue;
614
- }
615
- normalized.push({
616
- ...op,
617
- action
618
- });
619
- }
620
- return {
621
- pending: normalized,
622
- sourceActions,
623
- actionErrors
624
- };
625
- }
626
- //#endregion
627
- //#region src/execute/sources/execute-md.ts
628
- const MULTI_SIMPLE_ACTIONS = new Set([
629
- "close",
630
- "reopen",
631
- "clear-milestone",
632
- "unlock",
633
- "mark-ready-for-review",
634
- "convert-to-draft"
635
- ]);
636
- function parseExecuteMdLine(line) {
637
- const trimmed = line.trim();
638
- if (!trimmed || isCommentLine(trimmed)) return void 0;
639
- const tokens = tokenizeCommand(trimmed);
640
- if (!tokens) return {
641
- kind: "warning",
642
- message: "invalid quoted string syntax"
643
- };
644
- if (tokens.length === 0) return void 0;
645
- const [commandInput, ...args] = tokens;
646
- const command = resolveActionName(commandInput);
647
- if (!command) return {
648
- kind: "warning",
649
- message: `unrecognized action pattern: ${commandInput}`
650
- };
651
- if (command === "set-title") return parseSetTitle(args);
652
- if (command === "add-labels") return parseAddLabels(args, commandInput);
653
- if (command === "add-assignees") return parseAddAssignees(args, commandInput);
654
- if (command === "add-comment") return parseAddComment(args, commandInput);
655
- if (command === "close-with-comment") return parseCloseWithComment(args, commandInput);
656
- if (MULTI_SIMPLE_ACTIONS.has(command)) return parseMultiSimpleAction(command, args, commandInput);
657
- return {
658
- kind: "warning",
659
- message: `unrecognized action pattern: ${commandInput}`
660
- };
661
- }
662
- async function readExecuteMdFile(path) {
663
- if (!await pathExists(path)) return parseExecuteMd("");
664
- return parseExecuteMd(await readFile(path, "utf8"));
665
- }
666
- function parseExecuteMd(raw) {
667
- const lines = raw.split(/\r?\n/);
668
- const ops = [];
669
- const parsedLines = [];
670
- const warnings = [];
671
- let inHtmlCommentBlock = false;
672
- for (const [lineIndex, rawLine] of lines.entries()) {
673
- const trimmed = rawLine.trim();
674
- if (inHtmlCommentBlock) {
675
- parsedLines.push({
676
- kind: "raw",
677
- raw: rawLine
678
- });
679
- if (trimmed.includes("-->")) inHtmlCommentBlock = false;
680
- continue;
681
- }
682
- if (trimmed.startsWith("<!--")) {
683
- parsedLines.push({
684
- kind: "raw",
685
- raw: rawLine
686
- });
687
- if (!trimmed.includes("-->")) inHtmlCommentBlock = true;
688
- continue;
689
- }
690
- const parsed = parseExecuteMdLine(rawLine);
691
- if (!parsed) {
692
- parsedLines.push({
693
- kind: "raw",
694
- raw: rawLine
695
- });
696
- continue;
697
- }
698
- if (parsed.kind === "warning") {
699
- warnings.push(`execute-md line ${lineIndex + 1}: ${parsed.message}`);
700
- parsedLines.push({
701
- kind: "raw",
702
- raw: rawLine
703
- });
704
- continue;
705
- }
706
- if (parsed.kind === "single") {
707
- const opIndex = ops.length;
708
- ops.push(parsed.op);
709
- parsedLines.push({
710
- kind: "single",
711
- raw: rawLine,
712
- opIndex
713
- });
714
- continue;
715
- }
716
- const opIndexes = [];
717
- for (const number of parsed.numbers) {
718
- opIndexes.push(ops.length);
719
- ops.push({
720
- action: parsed.action,
721
- number
722
- });
723
- }
724
- parsedLines.push({
725
- kind: "multi",
726
- action: parsed.action,
727
- command: parsed.command,
728
- opIndexes
729
- });
730
- }
731
- return {
732
- ops,
733
- warnings,
734
- lines: parsedLines
735
- };
736
- }
737
- function stringifyExecuteMd(parsed, remainingOpIndexes) {
738
- const lines = [];
739
- for (const line of parsed.lines) {
740
- if (line.kind === "raw") {
741
- lines.push(line.raw);
742
- continue;
743
- }
744
- if (line.kind === "single") {
745
- if (remainingOpIndexes.has(line.opIndex)) lines.push(line.raw);
746
- continue;
747
- }
748
- const numbers = line.opIndexes.filter((index) => remainingOpIndexes.has(index)).map((index) => parsed.ops[index]?.number).filter((value) => typeof value === "number");
749
- if (numbers.length > 0) lines.push(`${line.command} ${numbers.map((number) => `#${number}`).join(" ")}`);
750
- }
751
- return `${lines.join("\n")}\n`;
752
- }
753
- function parseSetTitle(args) {
754
- if (args.length !== 2) return {
755
- kind: "warning",
756
- message: "set-title expects: set-title #<number> \"<title>\""
757
- };
758
- const number = parseIssueRef(args[0]);
759
- if (!number) return {
760
- kind: "warning",
761
- message: "set-title expects a single issue reference (#123)"
762
- };
763
- return {
764
- kind: "single",
765
- op: {
766
- action: "set-title",
767
- number,
768
- title: args[1]
769
- }
770
- };
771
- }
772
- function parseAddLabels(args, command) {
773
- if (args.length < 2) return {
774
- kind: "warning",
775
- message: `${command} expects: ${command} #<number> <label1, label2>`
776
- };
777
- const number = parseIssueRef(args[0]);
778
- if (!number) return {
779
- kind: "warning",
780
- message: `${command} expects a single issue reference (#123)`
781
- };
782
- const labels = args.slice(1).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
783
- if (labels.length === 0) return {
784
- kind: "warning",
785
- message: `${command} requires at least one label`
786
- };
787
- return {
788
- kind: "single",
789
- op: {
790
- action: "add-labels",
791
- number,
792
- labels
793
- }
794
- };
795
- }
796
- function parseAddAssignees(args, command) {
797
- if (args.length < 2) return {
798
- kind: "warning",
799
- message: `${command} expects: ${command} #<number> <assignee1, assignee2>`
800
- };
801
- const number = parseIssueRef(args[0]);
802
- if (!number) return {
803
- kind: "warning",
804
- message: `${command} expects a single issue reference (#123)`
805
- };
806
- const assignees = args.slice(1).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
807
- if (assignees.length === 0) return {
808
- kind: "warning",
809
- message: `${command} requires at least one assignee`
810
- };
811
- return {
812
- kind: "single",
813
- op: {
814
- action: "add-assignees",
815
- number,
816
- assignees
817
- }
818
- };
819
- }
820
- function parseAddComment(args, command) {
821
- if (args.length < 2) return {
822
- kind: "warning",
823
- message: `${command} expects: ${command} #<number> "<comment>"`
824
- };
825
- const number = parseIssueRef(args[0]);
826
- if (!number) return {
827
- kind: "warning",
828
- message: `${command} expects a single issue reference (#123)`
829
- };
830
- const body = args.slice(1).join(" ").trim();
831
- if (!body) return {
832
- kind: "warning",
833
- message: `${command} requires a non-empty comment`
834
- };
835
- return {
836
- kind: "single",
837
- op: {
838
- action: "add-comment",
839
- number,
840
- body
841
- }
842
- };
843
- }
844
- function parseCloseWithComment(args, command) {
845
- if (args.length < 2) return {
846
- kind: "warning",
847
- message: `${command} expects: ${command} #<number> "<comment>"`
848
- };
849
- const number = parseIssueRef(args[0]);
850
- if (!number) return {
851
- kind: "warning",
852
- message: `${command} expects a single issue reference (#123)`
853
- };
854
- const body = args.slice(1).join(" ").trim();
855
- if (!body) return {
856
- kind: "warning",
857
- message: `${command} requires a non-empty comment`
858
- };
859
- return {
860
- kind: "single",
861
- op: {
862
- action: "close-with-comment",
863
- number,
864
- body
865
- }
866
- };
867
- }
868
- function parseMultiSimpleAction(action, args, command) {
869
- const numbers = args.map(parseIssueRef);
870
- if (numbers.length === 0 || numbers.some((number) => !number)) return {
871
- kind: "warning",
872
- message: `${command} expects one or more issue references (#123 #456)`
873
- };
874
- return {
875
- kind: "multi",
876
- action,
877
- command,
878
- numbers
879
- };
880
- }
881
- function parseIssueRef(value) {
882
- const match = value.match(/^#(\d+)$/);
883
- if (!match) return void 0;
884
- const number = Number.parseInt(match[1], 10);
885
- if (!Number.isInteger(number) || number <= 0) return void 0;
886
- return number;
887
- }
888
- function tokenizeCommand(value) {
889
- const tokens = [];
890
- let index = 0;
891
- while (index < value.length) {
892
- while (index < value.length && /\s/.test(value[index])) index += 1;
893
- if (index >= value.length) break;
894
- if (value[index] === "\"") {
895
- index += 1;
896
- let token = "";
897
- let closed = false;
898
- while (index < value.length) {
899
- const char = value[index];
900
- if (char === "\\") {
901
- const next = value[index + 1];
902
- if (next === "\"" || next === "\\") {
903
- token += next;
904
- index += 2;
905
- continue;
906
- }
907
- }
908
- if (char === "\"") {
909
- closed = true;
910
- index += 1;
911
- break;
912
- }
913
- token += char;
914
- index += 1;
915
- }
916
- if (!closed) return void 0;
917
- tokens.push(token);
918
- continue;
919
- }
920
- const start = index;
921
- while (index < value.length && !/\s/.test(value[index])) index += 1;
922
- tokens.push(value.slice(start, index));
923
- }
924
- return tokens;
925
- }
926
- function isCommentLine(trimmed) {
927
- return trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("<!--");
928
- }
929
- //#endregion
930
- //#region package.json
931
- var version = "0.0.4";
932
- //#endregion
933
- //#region src/meta.ts
934
- const GHFS_NAME = "ghfs";
935
- const GHFS_VERSION = version;
936
- //#endregion
937
- //#region src/sync/state.ts
938
- function getSyncStatePath(storageDirAbsolute) {
939
- return join(storageDirAbsolute, SYNC_STATE_FILE_NAME);
940
- }
941
- async function loadSyncState(storageDirAbsolute) {
942
- const path = getSyncStatePath(storageDirAbsolute);
943
- try {
944
- const raw = await readFile(path, "utf8");
945
- const parsed = JSON.parse(raw);
946
- if (parsed.version !== 2) return createEmptySyncState();
947
- return {
948
- version: 2,
949
- items: normalizeItems(parsed.items),
950
- executions: normalizeExecutions(parsed.executions),
951
- ghfsVersion: typeof parsed.ghfsVersion === "string" ? parsed.ghfsVersion : void 0,
952
- repo: parsed.repo,
953
- lastSyncedAt: parsed.lastSyncedAt,
954
- lastSince: parsed.lastSince,
955
- lastRepoUpdatedAt: parsed.lastRepoUpdatedAt,
956
- lastSyncRun: parsed.lastSyncRun
957
- };
958
- } catch {
959
- return createEmptySyncState();
960
- }
961
- }
962
- function normalizeItems(items) {
963
- if (!items) return {};
964
- if (typeof items !== "object" || Array.isArray(items)) return {};
965
- const normalizedItems = {};
966
- for (const [key, item] of Object.entries(items)) {
967
- const normalizedItem = normalizeItem(item);
968
- if (!normalizedItem) continue;
969
- normalizedItems[key] = normalizedItem;
970
- }
971
- return normalizedItems;
972
- }
973
- function normalizeExecutions(executions) {
974
- if (!Array.isArray(executions)) return [];
975
- return executions.map((execution) => {
976
- if (!execution || typeof execution !== "object") return execution;
977
- const typedExecution = execution;
978
- if (typedExecution.mode === "dry-run") return {
979
- ...typedExecution,
980
- mode: "report"
981
- };
982
- return typedExecution;
983
- });
984
- }
985
- async function saveSyncState(storageDirAbsolute, state) {
986
- await mkdir(storageDirAbsolute, { recursive: true });
987
- const normalizedState = {
988
- ...state,
989
- ghfsVersion: state.ghfsVersion ?? GHFS_VERSION
990
- };
991
- await writeFile(getSyncStatePath(storageDirAbsolute), `${JSON.stringify(normalizedState, null, 2)}\n`, "utf8");
992
- }
993
- function createEmptySyncState() {
994
- return {
995
- version: 2,
996
- items: {},
997
- executions: []
998
- };
999
- }
1000
- function appendExecution(state, result, limit = 20) {
1001
- const nextExecutions = [result, ...state.executions].slice(0, limit);
1002
- return {
1003
- ...state,
1004
- executions: nextExecutions
1005
- };
1006
- }
1007
- function normalizeItem(item) {
1008
- if (!item || typeof item !== "object") return void 0;
1009
- if (!item.lastUpdatedAt || !item.lastSyncedAt || !item.filePath) return void 0;
1010
- if (!item.data || !item.data.item) return void 0;
1011
- const comments = Array.isArray(item.data.comments) ? item.data.comments : [];
1012
- return {
1013
- ...item,
1014
- data: {
1015
- ...item.data,
1016
- item: {
1017
- ...item.data.item,
1018
- reactions: normalizeReactions(item.data.item.reactions)
1019
- },
1020
- comments: comments.filter((comment) => comment && typeof comment === "object").map((comment) => ({
1021
- ...comment,
1022
- reactions: normalizeReactions(comment.reactions)
1023
- }))
1024
- }
1025
- };
1026
- }
1027
- //#endregion
1028
- //#region src/execute/diff.ts
1029
- function computeExecuteDiffOps(options) {
1030
- const ops = [];
1031
- const ifUnchangedSince = options.ifUnchangedSince;
1032
- const current = normalizeDiffFields(options.current);
1033
- const desired = normalizeDiffFields(options.desired);
1034
- if (current.title !== desired.title) ops.push({
1035
- action: "set-title",
1036
- number: options.number,
1037
- title: desired.title,
1038
- ifUnchangedSince
1039
- });
1040
- if (options.includeBody && current.body !== desired.body && desired.body) ops.push({
1041
- action: "set-body",
1042
- number: options.number,
1043
- body: desired.body,
1044
- ifUnchangedSince
1045
- });
1046
- if (current.state !== desired.state) ops.push({
1047
- action: desired.state === "closed" ? "close" : "reopen",
1048
- number: options.number,
1049
- ifUnchangedSince
1050
- });
1051
- if (!sameStringSet(current.labels, desired.labels)) {
1052
- const additions = diffStrings(desired.labels, current.labels);
1053
- const deletions = diffStrings(current.labels, desired.labels);
1054
- if (additions.length > 0 && deletions.length > 0) ops.push({
1055
- action: "set-labels",
1056
- number: options.number,
1057
- labels: desired.labels,
1058
- ifUnchangedSince
1059
- });
1060
- else if (additions.length > 0) ops.push({
1061
- action: "add-labels",
1062
- number: options.number,
1063
- labels: additions,
1064
- ifUnchangedSince
1065
- });
1066
- else if (deletions.length > 0) ops.push({
1067
- action: "remove-labels",
1068
- number: options.number,
1069
- labels: deletions,
1070
- ifUnchangedSince
1071
- });
1072
- }
1073
- if (!sameStringSet(current.assignees, desired.assignees)) {
1074
- if (desired.assignees.length > 0) ops.push({
1075
- action: "set-assignees",
1076
- number: options.number,
1077
- assignees: desired.assignees,
1078
- ifUnchangedSince
1079
- });
1080
- else if (current.assignees.length > 0) ops.push({
1081
- action: "remove-assignees",
1082
- number: options.number,
1083
- assignees: current.assignees,
1084
- ifUnchangedSince
1085
- });
1086
- }
1087
- if (current.milestone !== desired.milestone) if (desired.milestone) ops.push({
1088
- action: "set-milestone",
1089
- number: options.number,
1090
- milestone: desired.milestone,
1091
- ifUnchangedSince
1092
- });
1093
- else ops.push({
1094
- action: "clear-milestone",
1095
- number: options.number,
1096
- ifUnchangedSince
1097
- });
1098
- if (!sameStringSet(current.reviewers, desired.reviewers)) {
1099
- const additions = diffStrings(desired.reviewers, current.reviewers);
1100
- const deletions = diffStrings(current.reviewers, desired.reviewers);
1101
- if (additions.length > 0) ops.push({
1102
- action: "request-reviewers",
1103
- number: options.number,
1104
- reviewers: additions,
1105
- ifUnchangedSince
1106
- });
1107
- if (deletions.length > 0) ops.push({
1108
- action: "remove-reviewers",
1109
- number: options.number,
1110
- reviewers: deletions,
1111
- ifUnchangedSince
1112
- });
1113
- }
1114
- if (typeof current.isDraft === "boolean" && typeof desired.isDraft === "boolean" && current.isDraft !== desired.isDraft) ops.push({
1115
- action: desired.isDraft ? "convert-to-draft" : "mark-ready-for-review",
1116
- number: options.number,
1117
- ifUnchangedSince
1118
- });
1119
- return ops;
1120
- }
1121
- function normalizeStringArray(value) {
1122
- if (!Array.isArray(value)) return [];
1123
- const unique = /* @__PURE__ */ new Set();
1124
- for (const entry of value) {
1125
- if (typeof entry !== "string") continue;
1126
- const normalized = entry.trim();
1127
- if (!normalized) continue;
1128
- unique.add(normalized);
1129
- }
1130
- return [...unique];
1131
- }
1132
- function normalizeMilestone(value) {
1133
- if (typeof value !== "string") return null;
1134
- const normalized = value.trim();
1135
- return normalized.length > 0 ? normalized : null;
1136
- }
1137
- function normalizeBody(value) {
1138
- if (value == null) return null;
1139
- const trimmed = value.trim();
1140
- return trimmed.length > 0 ? trimmed : null;
1141
- }
1142
- function normalizeDiffFields(fields) {
1143
- return {
1144
- ...fields,
1145
- title: fields.title.trim(),
1146
- body: normalizeBody(fields.body),
1147
- labels: normalizeStringArray(fields.labels),
1148
- assignees: normalizeStringArray(fields.assignees),
1149
- milestone: normalizeMilestone(fields.milestone),
1150
- reviewers: normalizeStringArray(fields.reviewers),
1151
- isDraft: Boolean(fields.isDraft)
1152
- };
1153
- }
1154
- function sameStringSet(left, right) {
1155
- if (left.length !== right.length) return false;
1156
- const sortedLeft = [...left].sort();
1157
- const sortedRight = [...right].sort();
1158
- return sortedLeft.every((value, index) => value === sortedRight[index]);
1159
- }
1160
- function diffStrings(source, target) {
1161
- const targetSet = new Set(target);
1162
- return source.filter((value) => !targetSet.has(value));
1163
- }
1164
- //#endregion
1165
- //#region src/execute/sources/per-item.ts
1166
- async function loadPerItemSource(storageDir) {
1167
- const syncState = await loadSyncState(storageDir);
1168
- const ops = [];
1169
- const warnings = [];
1170
- const repo = syncState.repo;
1171
- for (const tracked of Object.values(syncState.items)) {
1172
- const markdownPath = join(storageDir, tracked.filePath);
1173
- if (!await pathExists(markdownPath)) {
1174
- warnings.push(`per-item: missing markdown for ${formatIssueNumber(tracked.number, { repo })} (${tracked.filePath})`);
1175
- continue;
1176
- }
1177
- const frontmatter = parseFrontmatter(await readFile(markdownPath, "utf8"));
1178
- if (!frontmatter) {
1179
- warnings.push(`per-item: invalid or missing frontmatter for ${formatIssueNumber(tracked.number, { repo })}`);
1180
- continue;
1181
- }
1182
- const trackedItem = tracked.data.item;
1183
- const itemOps = computePerItemOps({
1184
- number: tracked.number,
1185
- current: {
1186
- title: trackedItem.title,
1187
- state: trackedItem.state,
1188
- labels: trackedItem.labels,
1189
- assignees: trackedItem.assignees,
1190
- milestone: trackedItem.milestone
1191
- },
1192
- desired: frontmatter,
1193
- updatedAt: trackedItem.updatedAt
1194
- });
1195
- ops.push(...itemOps);
1196
- }
1197
- return {
1198
- ops,
1199
- warnings
1200
- };
1201
- }
1202
- function computePerItemOps(input) {
1203
- return computeExecuteDiffOps({
1204
- number: input.number,
1205
- current: {
1206
- ...input.current,
1207
- body: null,
1208
- reviewers: []
1209
- },
1210
- desired: {
1211
- ...input.desired,
1212
- body: null,
1213
- reviewers: []
1214
- },
1215
- ifUnchangedSince: input.updatedAt
1216
- });
1217
- }
1218
- function parseFrontmatter(raw) {
1219
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
1220
- if (!match) return void 0;
1221
- let parsed;
1222
- try {
1223
- parsed = parse(match[1]);
1224
- } catch {
1225
- return;
1226
- }
1227
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1228
- const data = parsed;
1229
- const title = typeof data.title === "string" && data.title.trim().length > 0 ? data.title.trim() : void 0;
1230
- const state = data.state === "open" || data.state === "closed" ? data.state : void 0;
1231
- if (!title || !state) return void 0;
1232
- return {
1233
- title,
1234
- state,
1235
- labels: normalizeStringArray(data.labels ?? data.tags),
1236
- assignees: normalizeStringArray(data.assignees),
1237
- milestone: normalizeMilestone(data.milestone)
1238
- };
1239
- }
1240
- //#endregion
1241
- //#region src/execute/sources/index.ts
1242
- async function loadExecuteSources(executeFilePath) {
1243
- const storageDir = dirname(executeFilePath);
1244
- const executeMdPath = join(storageDir, EXECUTE_MD_FILE_NAME);
1245
- const yml = await readAndValidateExecuteFileWithSource(executeFilePath);
1246
- const ymlOps = yml.ops;
1247
- const executeMd = await readExecuteMdFile(executeMdPath);
1248
- const perItem = await loadPerItemSource(storageDir);
1249
- const mergedOps = [
1250
- ...ymlOps,
1251
- ...executeMd.ops,
1252
- ...perItem.ops
1253
- ];
1254
- const entries = mergedOps.map((op, mergedIndex) => {
1255
- if (mergedIndex < ymlOps.length) return {
1256
- op,
1257
- source: "execute.yml",
1258
- sourceIndex: mergedIndex,
1259
- mergedIndex
1260
- };
1261
- const mdOffset = mergedIndex - ymlOps.length;
1262
- if (mdOffset < executeMd.ops.length) return {
1263
- op,
1264
- source: "execute.md",
1265
- sourceIndex: mdOffset,
1266
- mergedIndex
1267
- };
1268
- return {
1269
- op,
1270
- source: "per-item",
1271
- sourceIndex: mdOffset - executeMd.ops.length,
1272
- mergedIndex
1273
- };
1274
- });
1275
- const customErrors = validateExecuteRules(mergedOps);
1276
- if (customErrors.length) throw new Error(`Invalid execute file: ${customErrors.join("; ")}`);
1277
- return {
1278
- ops: mergedOps,
1279
- entries,
1280
- warnings: [...executeMd.warnings, ...perItem.warnings],
1281
- async writeRemaining(remainingIndexes) {
1282
- await writeExecuteFile(executeFilePath, ymlOps.map((op, index) => ({
1283
- op,
1284
- index
1285
- })).filter((item) => remainingIndexes.has(item.index)).map(({ op, index }) => ({
1286
- ...op,
1287
- action: yml.sourceActions[index] ?? op.action
1288
- })));
1289
- if (!await pathExists(executeMdPath)) return;
1290
- const mdOffset = ymlOps.length;
1291
- const mdRemaining = /* @__PURE__ */ new Set();
1292
- for (const index of remainingIndexes) if (index >= mdOffset && index < mdOffset + executeMd.ops.length) mdRemaining.add(index - mdOffset);
1293
- await writeFile(executeMdPath, stringifyExecuteMd(executeMd, mdRemaining), "utf-8");
1294
- }
1295
- };
1296
- }
1297
- //#endregion
1298
- //#region src/execute/index.ts
1299
- var ExecuteCancelledError = class extends Error {
1300
- constructor() {
1301
- super("Execution cancelled");
1302
- this.name = "ExecuteCancelledError";
1303
- }
1304
- };
1305
- function isExecuteCancelledError(error) {
1306
- return error instanceof ExecuteCancelledError;
1307
- }
1308
- async function executePendingChanges(options) {
1309
- try {
1310
- await ensureExecuteArtifacts(options.executeFilePath);
1311
- const sources = await loadExecuteSources(options.executeFilePath);
1312
- const allOps = sources.ops;
1313
- for (const warning of sources.warnings) options.onWarning?.(warning);
1314
- if (allOps.length === 0) return {
1315
- runId: createRunId(),
1316
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1317
- mode: "report",
1318
- repo: options.repo,
1319
- planned: 0,
1320
- applied: 0,
1321
- failed: 0,
1322
- details: []
1323
- };
1324
- const interactive = process.stdin.isTTY && !options.nonInteractive;
1325
- if (interactive && !options.prompts) throw new Error("Interactive execute prompts are unavailable. Use --non-interactive or provide prompts.");
1326
- const selected = Array.isArray(options.selectedIndexes) ? selectOperationsByIndexes(allOps, options.selectedIndexes) : interactive ? await selectOperations(allOps, options.prompts) : allOps.map((op, index) => ({
1327
- op,
1328
- index
1329
- }));
1330
- const runId = createRunId();
1331
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1332
- const mode = options.apply ? "apply" : "report";
1333
- options.reporter?.onStart?.({
1334
- repo: options.repo,
1335
- mode,
1336
- planned: selected.length
1337
- });
1338
- if (selected.length === 0) {
1339
- const result = {
1340
- runId,
1341
- createdAt,
1342
- mode,
1343
- repo: options.repo,
1344
- planned: 0,
1345
- applied: 0,
1346
- failed: 0,
1347
- details: []
1348
- };
1349
- options.reporter?.onComplete?.({ result });
1350
- return result;
1351
- }
1352
- options.onPlan?.(selected.map((item) => item.op));
1353
- if (!options.apply) {
1354
- const result = {
1355
- runId,
1356
- createdAt,
1357
- mode: "report",
1358
- repo: options.repo,
1359
- planned: selected.length,
1360
- applied: 0,
1361
- failed: 0,
1362
- details: selected.map(({ op, index }) => ({
1363
- op: index + 1,
1364
- action: op.action,
1365
- number: op.number,
1366
- status: "planned",
1367
- message: describeExecutionAction(op.action, op.number)
1368
- }))
1369
- };
1370
- options.reporter?.onComplete?.({ result });
1371
- return result;
1372
- }
1373
- if (interactive) {
1374
- if (!await confirmApply(selected.length, options.prompts)) throw new ExecuteCancelledError();
1375
- }
1376
- const provider = options.provider ?? createRepositoryProvider({
1377
- token: options.token,
1378
- repo: options.repo
1379
- });
1380
- const details = [];
1381
- const appliedIndexes = /* @__PURE__ */ new Set();
1382
- let applied = 0;
1383
- let failed = 0;
1384
- for (const { op, index } of selected) try {
1385
- const target = await applyOperation(provider, op);
1386
- appliedIndexes.add(index);
1387
- await persistRemainingOps(sources.writeRemaining, allOps, appliedIndexes);
1388
- const detail = {
1389
- op: index + 1,
1390
- action: op.action,
1391
- number: op.number,
1392
- target,
1393
- status: "applied",
1394
- message: describeExecutionAction(op.action, op.number)
1395
- };
1396
- details.push(detail);
1397
- applied += 1;
1398
- options.reporter?.onProgress?.({
1399
- repo: options.repo,
1400
- mode: "apply",
1401
- planned: selected.length,
1402
- completed: details.length,
1403
- applied,
1404
- failed,
1405
- detail
1406
- });
1407
- } catch (error) {
1408
- failed += 1;
1409
- const detail = {
1410
- op: index + 1,
1411
- action: op.action,
1412
- number: op.number,
1413
- status: "failed",
1414
- message: error.message
1415
- };
1416
- details.push(detail);
1417
- options.reporter?.onProgress?.({
1418
- repo: options.repo,
1419
- mode: "apply",
1420
- planned: selected.length,
1421
- completed: details.length,
1422
- applied,
1423
- failed,
1424
- detail
1425
- });
1426
- if (!options.continueOnError) break;
1427
- }
1428
- const result = {
1429
- runId,
1430
- createdAt,
1431
- mode: "apply",
1432
- repo: options.repo,
1433
- planned: selected.length,
1434
- applied,
1435
- failed,
1436
- details
1437
- };
1438
- options.reporter?.onComplete?.({ result });
1439
- return result;
1440
- } catch (error) {
1441
- options.reporter?.onError?.({ error });
1442
- throw error;
1443
- }
1444
- }
1445
- async function persistRemainingOps(writeRemaining, allOps, appliedIndexes) {
1446
- const remainingIndexes = /* @__PURE__ */ new Set();
1447
- for (const [index] of allOps.entries()) if (!appliedIndexes.has(index)) remainingIndexes.add(index);
1448
- await writeRemaining(remainingIndexes);
1449
- }
1450
- async function applyOperation(provider, op) {
1451
- const item = await provider.fetchItemSnapshot(op.number);
1452
- const isPull = item.kind === "pull";
1453
- if (op.ifUnchangedSince) {
1454
- const remoteUpdatedAt = item.updatedAt;
1455
- if (remoteUpdatedAt && new Date(remoteUpdatedAt).getTime() > new Date(op.ifUnchangedSince).getTime()) throw new Error(`Operation conflict: remote updated_at=${remoteUpdatedAt}`);
1456
- }
1457
- switch (op.action) {
1458
- case "close":
1459
- await provider.actionClose(op.number);
1460
- break;
1461
- case "reopen":
1462
- await provider.actionReopen(op.number);
1463
- break;
1464
- case "set-title":
1465
- await provider.actionSetTitle(op.number, op.title);
1466
- break;
1467
- case "set-body":
1468
- await provider.actionSetBody(op.number, op.body);
1469
- break;
1470
- case "add-comment":
1471
- await provider.actionAddComment(op.number, op.body);
1472
- break;
1473
- case "close-with-comment":
1474
- await provider.actionAddComment(op.number, op.body);
1475
- await provider.actionClose(op.number);
1476
- break;
1477
- case "add-labels":
1478
- await provider.actionAddLabels(op.number, op.labels);
1479
- break;
1480
- case "remove-labels":
1481
- await provider.actionRemoveLabels(op.number, op.labels);
1482
- break;
1483
- case "set-labels":
1484
- await provider.actionSetLabels(op.number, op.labels);
1485
- break;
1486
- case "add-assignees":
1487
- await provider.actionAddAssignees(op.number, op.assignees);
1488
- break;
1489
- case "remove-assignees":
1490
- await provider.actionRemoveAssignees(op.number, op.assignees);
1491
- break;
1492
- case "set-assignees":
1493
- await provider.actionSetAssignees(op.number, op.assignees);
1494
- break;
1495
- case "set-milestone":
1496
- await provider.actionSetMilestone(op.number, op.milestone);
1497
- break;
1498
- case "clear-milestone":
1499
- await provider.actionClearMilestone(op.number);
1500
- break;
1501
- case "lock":
1502
- await provider.actionLock(op.number, op.reason);
1503
- break;
1504
- case "unlock":
1505
- await provider.actionUnlock(op.number);
1506
- break;
1507
- case "request-reviewers":
1508
- ensurePullAction(op.action, op.number, isPull);
1509
- await provider.actionRequestReviewers(op.number, op.reviewers);
1510
- break;
1511
- case "remove-reviewers":
1512
- ensurePullAction(op.action, op.number, isPull);
1513
- await provider.actionRemoveReviewers(op.number, op.reviewers);
1514
- break;
1515
- case "mark-ready-for-review":
1516
- ensurePullAction(op.action, op.number, isPull);
1517
- await provider.actionMarkReadyForReview(op.number);
1518
- break;
1519
- case "convert-to-draft":
1520
- ensurePullAction(op.action, op.number, isPull);
1521
- await provider.actionConvertToDraft(op.number);
1522
- break;
1523
- default: throw new Error(`Unsupported action: ${String(op.action)}`);
1524
- }
1525
- return item.kind;
1526
- }
1527
- function createRunId() {
1528
- return `run_${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "")}_${Math.random().toString(36).slice(2, 7)}`;
1529
- }
1530
- async function selectOperations(ops, prompts) {
1531
- const selectedIndexes = await prompts.selectOperations(ops);
1532
- if (!selectedIndexes) throw new ExecuteCancelledError();
1533
- const selectedIndexesSet = new Set(selectedIndexes);
1534
- return ops.map((op, index) => ({
1535
- op,
1536
- index
1537
- })).filter((item) => selectedIndexesSet.has(item.index));
1538
- }
1539
- async function confirmApply(count, prompts) {
1540
- const result = await prompts.confirmApply(count);
1541
- if (result == null) return false;
1542
- return result;
1543
- }
1544
- function selectOperationsByIndexes(ops, selectedIndexes) {
1545
- const selectedSet = /* @__PURE__ */ new Set();
1546
- for (const index of selectedIndexes) if (Number.isInteger(index) && index >= 0 && index < ops.length) selectedSet.add(index);
1547
- return ops.map((op, index) => ({
1548
- op,
1549
- index
1550
- })).filter((item) => selectedSet.has(item.index));
1551
- }
1552
- function ensurePullAction(action, number, isPull) {
1553
- if (!isPull) throw new Error(`Action ${action} requires #${number} to be a pull request`);
1554
- }
1555
- function describeExecutionAction(action, number) {
1556
- return `${action} #${number}`;
1557
- }
1558
- //#endregion
1559
- //#region src/sync/execution-log.ts
1560
- async function appendExecutionResult(storageDirAbsolute, result) {
1561
- await saveSyncState(storageDirAbsolute, appendExecution(await loadSyncState(storageDirAbsolute), result));
1562
- }
1563
- //#endregion
1564
- //#region src/utils/sync.ts
1565
- function resolveSince(options, syncState) {
1566
- if (options.full) return void 0;
1567
- if (options.since) return options.since;
1568
- return syncState.lastSyncedAt;
1569
- }
1570
- function normalizeIssueNumbers(numbers) {
1571
- if (!numbers) return void 0;
1572
- return [...new Set(numbers.filter((number) => Number.isInteger(number) && number > 0))];
1573
- }
1574
- //#endregion
1575
- //#region src/sync/markdown.ts
1576
- const FIELDS_ALWAYS_KEEP = new Set(["labels", "assignees"]);
1577
- const FIELDS_ALWAYS_EXCLUDE = new Set(["repo", "kind"]);
1578
- const REACTION_FIELDS = [
1579
- {
1580
- key: "plusOne",
1581
- emoji: "👍"
1582
- },
1583
- {
1584
- key: "minusOne",
1585
- emoji: "👎"
1586
- },
1587
- {
1588
- key: "laugh",
1589
- emoji: "😄"
1590
- },
1591
- {
1592
- key: "hooray",
1593
- emoji: "🎉"
1594
- },
1595
- {
1596
- key: "confused",
1597
- emoji: "😕"
1598
- },
1599
- {
1600
- key: "heart",
1601
- emoji: "❤️"
1602
- },
1603
- {
1604
- key: "rocket",
1605
- emoji: "🚀"
1606
- },
1607
- {
1608
- key: "eyes",
1609
- emoji: "👀"
1610
- }
1611
- ];
1612
- function renderIssueMarkdown(input) {
1613
- const url = input.url || `https://github.com/${input.repo}/${input.kind === "pull" ? "pull" : "issues"}/${input.number}`;
1614
- const frontmatter = {
1615
- repo: input.repo,
1616
- number: input.number,
1617
- kind: input.kind,
1618
- url,
1619
- state: input.state,
1620
- title: input.title,
1621
- author: input.author,
1622
- labels: input.labels,
1623
- assignees: input.assignees,
1624
- milestone: input.milestone,
1625
- created_at: input.createdAt,
1626
- updated_at: input.updatedAt,
1627
- closed_at: input.closedAt,
1628
- last_synced_at: input.lastSyncedAt,
1629
- reactions: formatReactionsFrontmatter(input.reactions),
1630
- is_draft: input.pr?.isDraft,
1631
- merged: input.pr?.merged,
1632
- merged_at: input.pr?.mergedAt,
1633
- base_ref: input.pr?.baseRef,
1634
- head_ref: input.pr?.headRef,
1635
- reviewers_requested: input.pr?.requestedReviewers
1636
- };
1637
- const compactFrontmatter = Object.fromEntries(Object.entries(frontmatter).filter(([key, value]) => {
1638
- if (FIELDS_ALWAYS_EXCLUDE.has(key)) return false;
1639
- if (FIELDS_ALWAYS_KEEP.has(key)) return true;
1640
- if (value === void 0 || value === null || value === false) return false;
1641
- if (Array.isArray(value)) return value.length > 0;
1642
- return true;
1643
- }));
1644
- const sections = [
1645
- `# ${input.title}`,
1646
- "",
1647
- "## Description",
1648
- "",
1649
- input.body?.trim() || "_No description._"
1650
- ];
1651
- const bodyReactionsLine = formatReactionsLine(input.reactions);
1652
- if (bodyReactionsLine) {
1653
- sections.push("");
1654
- sections.push(bodyReactionsLine);
1655
- }
1656
- sections.push("");
1657
- sections.push("---");
1658
- sections.push("");
1659
- sections.push("## Comments");
1660
- sections.push("");
1661
- if (input.comments.length === 0) sections.push("_No comments._");
1662
- else for (const [index, comment] of input.comments.entries()) {
1663
- if (index > 0) {
1664
- sections.push("---");
1665
- sections.push("");
1666
- }
1667
- sections.push(`### @${comment.author} on ${comment.createdAt}`);
1668
- sections.push(`<!-- comment-id:${comment.id} updated:${comment.updatedAt} -->`);
1669
- sections.push("");
1670
- sections.push(comment.body?.trim() || "_No content._");
1671
- const reactionsLine = formatReactionsLine(comment.reactions);
1672
- if (reactionsLine) {
1673
- sections.push("");
1674
- sections.push(reactionsLine);
1675
- }
1676
- sections.push("");
1677
- }
1678
- return [
1679
- "---",
1680
- stringify(compactFrontmatter).trimEnd(),
1681
- "---",
1682
- "",
1683
- ...sections,
1684
- ""
1685
- ].join("\n");
1686
- }
1687
- function formatReactionsLine(reactions) {
1688
- const entries = getReactionEntries(reactions);
1689
- if (entries.length === 0) return void 0;
1690
- return `> ${entries.map((entry) => `\`${entry.emoji} ${entry.count}\``).join(" | ")}`;
1691
- }
1692
- function formatReactionsFrontmatter(reactions) {
1693
- const entries = getReactionEntries(reactions);
1694
- if (entries.length === 0) return void 0;
1695
- return Object.fromEntries(entries.map((entry) => [entry.emoji, entry.count]));
1696
- }
1697
- function getReactionEntries(reactions) {
1698
- const normalized = normalizeReactions(reactions);
1699
- return REACTION_FIELDS.map(({ key, emoji }) => {
1700
- const count = normalized[key];
1701
- if (!count) return void 0;
1702
- return {
1703
- emoji,
1704
- count
1705
- };
1706
- }).filter((entry) => Boolean(entry));
1707
- }
1708
- //#endregion
1709
- //#region src/utils/string.ts
1710
- function slugifyTitle(title, maxLength = 48) {
1711
- const normalized = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1712
- if (!normalized) return "item";
1713
- return normalized.slice(0, maxLength).replace(/-+$/g, "") || "item";
1714
- }
1715
- //#endregion
1716
- //#region src/sync/paths.ts
1717
- const FILE_NUMBER_PAD_LENGTH = 5;
1718
- const MAX_SLUG_LENGTH = 48;
1719
- function getIssueMarkdownPath(storageDirAbsolute, number, state, title) {
1720
- const fileName = getItemFileName(number, title);
1721
- if (state === "closed") return join(storageDirAbsolute, ISSUE_DIR_NAME, CLOSED_DIR_NAME, fileName);
1722
- return join(storageDirAbsolute, ISSUE_DIR_NAME, fileName);
1723
- }
1724
- function getPullMarkdownPath(storageDirAbsolute, number, state, title) {
1725
- const fileName = getItemFileName(number, title);
1726
- if (state === "closed") return join(storageDirAbsolute, PULL_DIR_NAME, CLOSED_DIR_NAME, fileName);
1727
- return join(storageDirAbsolute, PULL_DIR_NAME, fileName);
1728
- }
1729
- function getItemMarkdownPath(storageDirAbsolute, kind, number, state, title) {
1730
- if (kind === "pull") return getPullMarkdownPath(storageDirAbsolute, number, state, title);
1731
- return getIssueMarkdownPath(storageDirAbsolute, number, state, title);
1732
- }
1733
- function getItemFileName(number, title) {
1734
- return `${String(number).padStart(FILE_NUMBER_PAD_LENGTH, "0")}-${slugifyTitle(title, MAX_SLUG_LENGTH)}.md`;
1735
- }
1736
- function getPrPatchPath(storageDirAbsolute, number, title) {
1737
- return join(storageDirAbsolute, PULL_DIR_NAME, getItemFileName(number, title).replace(/\.md$/, ".patch"));
1738
- }
1739
- //#endregion
1740
- //#region src/sync/sync-repository-utils.ts
1741
- function createCounters(scanned = 0, selected = 0) {
1742
- return {
1743
- scanned,
1744
- selected,
1745
- processed: 0,
1746
- skipped: 0,
1747
- written: 0,
1748
- moved: 0,
1749
- patchesWritten: 0,
1750
- patchesDeleted: 0
1751
- };
1752
- }
1753
- function addItemStats(counters, stats) {
1754
- counters.skipped += stats.skipped;
1755
- counters.written += stats.written;
1756
- counters.moved += stats.moved;
1757
- counters.patchesWritten += stats.patchesWritten;
1758
- counters.patchesDeleted += stats.patchesDeleted;
1759
- }
1760
- function shouldSyncKind(sync, kind) {
1761
- return kind === "issue" ? sync.issues : sync.pulls;
1762
- }
1763
- function shouldSyncIssue(sync, issue) {
1764
- return shouldSyncKind(sync, issue.kind);
1765
- }
1766
- function resolvePatchPlan(patchesMode, kind, state) {
1767
- if (kind !== "pull") return {
1768
- shouldWritePatch: false,
1769
- shouldDeletePatch: false
1770
- };
1771
- const shouldWritePatch = patchesMode === "all" || patchesMode === "open" && state === "open";
1772
- return {
1773
- shouldWritePatch,
1774
- shouldDeletePatch: !shouldWritePatch
1775
- };
1776
- }
1777
- function relativeToStorage(storageDirAbsolute, absolutePath) {
1778
- if (absolutePath.startsWith(storageDirAbsolute)) return absolutePath.slice(storageDirAbsolute.length + 1);
1779
- return basename(absolutePath);
1780
- }
1781
- //#endregion
1782
- //#region src/sync/sync-repository-storage.ts
1783
- async function resolveIssuePaths(storageDirAbsolute, kind, number, title, state, trackedFilePath) {
1784
- const closedPath = getItemMarkdownPath(storageDirAbsolute, kind, number, "closed", title);
1785
- const openPath = getItemMarkdownPath(storageDirAbsolute, kind, number, "open", title);
1786
- const hasClosedFile = await pathExists(closedPath);
1787
- const hasOpenFile = await pathExists(openPath);
1788
- const trackedPath = resolveTrackedPath(storageDirAbsolute, trackedFilePath);
1789
- const hasTrackedFile = trackedPath ? await pathExists(trackedPath) : false;
1790
- const targetPath = getItemMarkdownPath(storageDirAbsolute, kind, number, state, title);
1791
- const hasTargetFile = state === "open" ? hasOpenFile : hasClosedFile;
1792
- const matchedPaths = await findMatchedMarkdownPaths(storageDirAbsolute, kind, number, [
1793
- openPath,
1794
- closedPath,
1795
- trackedPath
1796
- ]);
1797
- return {
1798
- openPath,
1799
- closedPath,
1800
- targetPath,
1801
- patchPath: getPrPatchPath(storageDirAbsolute, number, title),
1802
- trackedPath,
1803
- hasOpenFile,
1804
- hasClosedFile,
1805
- hasTrackedFile,
1806
- matchedPaths,
1807
- hasLocalFile: hasOpenFile || hasClosedFile || hasTrackedFile || matchedPaths.length > 0,
1808
- hasTargetFile
1809
- };
1810
- }
1811
- async function moveMarkdownByState(paths, state) {
1812
- const sourcePath = resolveMoveSourcePath(paths, state);
1813
- if (!sourcePath) return 0;
1814
- if (sourcePath === paths.targetPath) return 0;
1815
- if (await pathExists(paths.targetPath)) return 0;
1816
- await movePath(sourcePath, paths.targetPath);
1817
- return 1;
1818
- }
1819
- function resolveMoveSourcePath(paths, state) {
1820
- if (paths.hasTrackedFile && paths.trackedPath && paths.trackedPath !== paths.targetPath) return paths.trackedPath;
1821
- if (state === "open" && paths.hasClosedFile && paths.closedPath !== paths.targetPath) return paths.closedPath;
1822
- if (state === "closed" && paths.hasOpenFile && paths.openPath !== paths.targetPath) return paths.openPath;
1823
- if (paths.hasOpenFile && paths.openPath !== paths.targetPath) return paths.openPath;
1824
- if (paths.hasClosedFile && paths.closedPath !== paths.targetPath) return paths.closedPath;
1825
- return paths.matchedPaths.find((path) => path !== paths.targetPath);
1826
- }
1827
- function getExistingMarkdownPaths(paths) {
1828
- const markdownPaths = /* @__PURE__ */ new Set();
1829
- if (paths.hasOpenFile) markdownPaths.add(paths.openPath);
1830
- if (paths.hasClosedFile) markdownPaths.add(paths.closedPath);
1831
- if (paths.hasTrackedFile && paths.trackedPath) markdownPaths.add(paths.trackedPath);
1832
- for (const matchedPath of paths.matchedPaths) markdownPaths.add(matchedPath);
1833
- return [...markdownPaths];
1834
- }
1835
- function resolveTrackedPath(storageDirAbsolute, trackedFilePath) {
1836
- if (!trackedFilePath) return void 0;
1837
- if (isAbsolute(trackedFilePath)) return trackedFilePath;
1838
- return join(storageDirAbsolute, trackedFilePath);
1839
- }
1840
- function resolveTrackedPathOrJoin(storageDirAbsolute, trackedFilePath) {
1841
- return resolveTrackedPath(storageDirAbsolute, trackedFilePath) ?? join(storageDirAbsolute, trackedFilePath);
1842
- }
1843
- async function findMatchedMarkdownPaths(storageDirAbsolute, kind, number, knownPaths) {
1844
- const matchedPaths = /* @__PURE__ */ new Set();
1845
- const knownPathSet = new Set(knownPaths.filter(Boolean));
1846
- const padded = String(number).padStart(5, "0");
1847
- const kindDir = kind === "issue" ? ISSUE_DIR_NAME : PULL_DIR_NAME;
1848
- for (const stateDir of ["", CLOSED_DIR_NAME]) {
1849
- const dir = stateDir ? join(storageDirAbsolute, kindDir, stateDir) : join(storageDirAbsolute, kindDir);
1850
- let files;
1851
- try {
1852
- files = await readdir(dir);
1853
- } catch {
1854
- continue;
1855
- }
1856
- for (const fileName of files) {
1857
- if (!fileName.startsWith(`${padded}-`) || !fileName.endsWith(".md")) continue;
1858
- const fullPath = join(dir, fileName);
1859
- if (!knownPathSet.has(fullPath)) matchedPaths.add(fullPath);
1860
- }
1861
- }
1862
- return [...matchedPaths];
1863
- }
1864
- function updateTrackedItem(context, number, kind, state, issueUpdatedAt, markdownPath, patchPath, data) {
1865
- context.syncState.items[String(number)] = {
1866
- number,
1867
- kind,
1868
- state,
1869
- lastUpdatedAt: issueUpdatedAt,
1870
- lastSyncedAt: context.syncedAt,
1871
- filePath: relativeToStorage(context.storageDirAbsolute, markdownPath),
1872
- patchPath: patchPath ? relativeToStorage(context.storageDirAbsolute, patchPath) : void 0,
1873
- data
1874
- };
1875
- }
1876
- async function pruneTrackedClosedItems(storageDirAbsolute, syncState, sync) {
1877
- if (sync.issues) await rm(join(storageDirAbsolute, ISSUE_DIR_NAME, CLOSED_DIR_NAME), {
1878
- recursive: true,
1879
- force: true
1880
- });
1881
- if (sync.pulls) await rm(join(storageDirAbsolute, PULL_DIR_NAME, CLOSED_DIR_NAME), {
1882
- recursive: true,
1883
- force: true
1884
- });
1885
- let patchesDeleted = 0;
1886
- for (const item of Object.values(syncState.items)) {
1887
- if (item.state !== "closed") continue;
1888
- if (!shouldSyncKind(sync, item.kind)) continue;
1889
- await removePath(resolveTrackedPathOrJoin(storageDirAbsolute, item.filePath));
1890
- if (item.kind === "pull") patchesDeleted += await removePatchIfExists(storageDirAbsolute, item.number);
1891
- delete syncState.items[String(item.number)];
1892
- }
1893
- return patchesDeleted;
1894
- }
1895
- async function pruneMissingOpenTrackedItems(storageDirAbsolute, syncState, openNumbers, sync) {
1896
- let patchesDeleted = 0;
1897
- for (const item of Object.values(syncState.items)) {
1898
- if (item.state !== "open") continue;
1899
- if (!shouldSyncKind(sync, item.kind)) continue;
1900
- if (openNumbers.has(item.number)) continue;
1901
- await removePath(resolveTrackedPathOrJoin(storageDirAbsolute, item.filePath));
1902
- if (item.kind === "pull") patchesDeleted += await removePatchIfExists(storageDirAbsolute, item.number);
1903
- delete syncState.items[String(item.number)];
1904
- }
1905
- return patchesDeleted;
1906
- }
1907
- //#endregion
1908
- //#region src/sync/sync-repository-item.ts
1909
- async function prepareIssueCandidateSync(context, issue) {
1910
- const number = issue.number;
1911
- const kind = issue.kind;
1912
- const state = issue.state;
1913
- const tracked = context.syncState.items[String(number)];
1914
- const paths = await resolveIssuePaths(context.storageDirAbsolute, kind, number, issue.title, state, tracked?.filePath);
1915
- const patchPlan = resolvePatchPlan(context.config.sync.patches, kind, state);
1916
- if (state === "closed" && context.config.sync.closed === false) {
1917
- delete context.syncState.items[String(number)];
1918
- return {
1919
- number,
1920
- kind,
1921
- state,
1922
- action: "remove",
1923
- paths,
1924
- patchPlan
1925
- };
1926
- }
1927
- if (state === "closed" && context.config.sync.closed === true && !paths.hasLocalFile) {
1928
- delete context.syncState.items[String(number)];
1929
- return {
1930
- number,
1931
- kind,
1932
- state,
1933
- action: "remove",
1934
- paths,
1935
- patchPlan
1936
- };
1937
- }
1938
- const hasCanonicalData = Boolean(tracked?.data && (kind !== "pull" || tracked.data.pull));
1939
- const shouldRefetch = !tracked || tracked.lastUpdatedAt !== issue.updatedAt || !hasCanonicalData;
1940
- const data = shouldRefetch ? await fetchCanonicalData(context, issue) : tracked.data;
1941
- updateTrackedItem(context, number, kind, state, issue.updatedAt, paths.targetPath, patchPlan.shouldWritePatch ? paths.patchPath : void 0, data);
1942
- return {
1943
- number,
1944
- kind,
1945
- state,
1946
- action: resolveSyncAction(shouldRefetch, paths, state),
1947
- paths,
1948
- patchPlan
1949
- };
1950
- }
1951
- async function materializePreparedIssue(context, candidate) {
1952
- const { number, kind, state, action, patchPlan, paths } = candidate;
1953
- if (action === "remove") {
1954
- for (const markdownPath of getExistingMarkdownPaths(paths)) await removePath(markdownPath);
1955
- let patchesDeleted = 0;
1956
- if (kind === "pull") patchesDeleted += await removePatchIfExists(context.storageDirAbsolute, number);
1957
- return {
1958
- kind,
1959
- action,
1960
- skipped: 0,
1961
- written: 0,
1962
- moved: 0,
1963
- patchesWritten: 0,
1964
- patchesDeleted
1965
- };
1966
- }
1967
- if (action === "skip") {
1968
- let patchesDeleted = 0;
1969
- if (patchPlan.shouldDeletePatch) patchesDeleted += await removePatchIfExists(context.storageDirAbsolute, number);
1970
- return {
1971
- kind,
1972
- action,
1973
- skipped: 1,
1974
- written: 0,
1975
- moved: 0,
1976
- patchesWritten: 0,
1977
- patchesDeleted
1978
- };
1979
- }
1980
- const tracked = context.syncState.items[String(number)];
1981
- if (!tracked) throw new Error(`Missing tracked canonical data for ${formatIssueNumber(number, {
1982
- repo: context.repoSlug,
1983
- kind
1984
- })}`);
1985
- const markdown = buildTrackedMarkdown(context, tracked);
1986
- const moved = await moveMarkdownByState(paths, state);
1987
- await writeFileEnsured(paths.targetPath, markdown);
1988
- const patchStats = await syncPatchByPlan(context, number, paths.patchPath, patchPlan);
1989
- return {
1990
- kind,
1991
- action,
1992
- skipped: 0,
1993
- written: 1,
1994
- moved,
1995
- patchesWritten: patchStats.patchesWritten,
1996
- patchesDeleted: patchStats.patchesDeleted
1997
- };
1998
- }
1999
- async function rematerializeTrackedMarkdown(context) {
2000
- let processed = 0;
2001
- let written = 0;
2002
- let moved = 0;
2003
- for (const tracked of Object.values(context.syncState.items)) {
2004
- const paths = await resolveIssuePaths(context.storageDirAbsolute, tracked.kind, tracked.number, tracked.data.item.title, tracked.state, tracked.filePath);
2005
- moved += await moveMarkdownByState(paths, tracked.state);
2006
- await writeFileEnsured(paths.targetPath, buildTrackedMarkdown(context, tracked));
2007
- tracked.filePath = relativeToStorage(context.storageDirAbsolute, paths.targetPath);
2008
- tracked.lastSyncedAt = context.syncedAt;
2009
- processed += 1;
2010
- written += 1;
2011
- }
2012
- return {
2013
- processed,
2014
- written,
2015
- moved
2016
- };
2017
- }
2018
- async function reconcileMarkdownFilesByScan(context) {
2019
- let written = 0;
2020
- let moved = 0;
2021
- const expectedPaths = /* @__PURE__ */ new Set();
2022
- for (const tracked of Object.values(context.syncState.items)) {
2023
- const paths = await resolveIssuePaths(context.storageDirAbsolute, tracked.kind, tracked.number, tracked.data.item.title, tracked.state, tracked.filePath);
2024
- expectedPaths.add(paths.targetPath);
2025
- const movedByState = !paths.hasTargetFile ? await moveMarkdownByState(paths, tracked.state) : 0;
2026
- let changed = movedByState > 0;
2027
- if (!await pathExists(paths.targetPath)) {
2028
- await writeFileEnsured(paths.targetPath, buildTrackedMarkdown(context, tracked));
2029
- written += 1;
2030
- changed = true;
2031
- }
2032
- tracked.filePath = relativeToStorage(context.storageDirAbsolute, paths.targetPath);
2033
- if (changed) tracked.lastSyncedAt = context.syncedAt;
2034
- moved += movedByState;
2035
- }
2036
- moved += await moveExtraMarkdownFilesToClosed(context.storageDirAbsolute, expectedPaths);
2037
- return {
2038
- written,
2039
- moved
2040
- };
2041
- }
2042
- function resolveSyncAction(shouldRefetch, paths, state) {
2043
- if (shouldRefetch) return "refetch";
2044
- if (paths.hasTargetFile) return "skip";
2045
- if (resolveMoveSourcePath(paths, state)) return "move";
2046
- return "create";
2047
- }
2048
- async function fetchCanonicalData(context, issue) {
2049
- return {
2050
- item: issue,
2051
- comments: await context.provider.fetchComments(issue.number),
2052
- pull: issue.kind === "pull" ? await context.provider.fetchPullMetadata(issue.number) : void 0
2053
- };
2054
- }
2055
- async function syncPatchByPlan(context, number, patchPath, patchPlan) {
2056
- let patchesWritten = 0;
2057
- let patchesDeleted = 0;
2058
- if (patchPlan.shouldWritePatch) {
2059
- const patch = await context.provider.fetchPullPatch(number);
2060
- await removePatchIfExists(context.storageDirAbsolute, number);
2061
- await writeFileEnsured(patchPath, patch);
2062
- patchesWritten += 1;
2063
- }
2064
- if (patchPlan.shouldDeletePatch) patchesDeleted += await removePatchIfExists(context.storageDirAbsolute, number);
2065
- return {
2066
- patchesWritten,
2067
- patchesDeleted
2068
- };
2069
- }
2070
- function buildTrackedMarkdown(context, tracked) {
2071
- return renderIssueMarkdown({
2072
- repo: context.repoSlug,
2073
- number: tracked.data.item.number,
2074
- kind: tracked.data.item.kind,
2075
- url: tracked.data.item.url,
2076
- state: tracked.data.item.state,
2077
- title: tracked.data.item.title,
2078
- body: tracked.data.item.body ?? "",
2079
- author: tracked.data.item.author ?? "unknown",
2080
- labels: tracked.data.item.labels,
2081
- assignees: tracked.data.item.assignees,
2082
- milestone: tracked.data.item.milestone,
2083
- createdAt: tracked.data.item.createdAt,
2084
- updatedAt: tracked.data.item.updatedAt,
2085
- closedAt: tracked.data.item.closedAt,
2086
- lastSyncedAt: context.syncedAt,
2087
- reactions: normalizeReactions(tracked.data.item.reactions),
2088
- comments: tracked.data.comments.map((comment) => ({
2089
- id: comment.id,
2090
- author: comment.author ?? "unknown",
2091
- body: comment.body ?? "",
2092
- createdAt: comment.createdAt,
2093
- updatedAt: comment.updatedAt,
2094
- reactions: normalizeReactions(comment.reactions)
2095
- })),
2096
- pr: tracked.data.pull
2097
- });
2098
- }
2099
- async function moveExtraMarkdownFilesToClosed(storageDirAbsolute, expectedPaths) {
2100
- let moved = 0;
2101
- moved += await moveOpenMarkdownFilesToClosed(join(storageDirAbsolute, ISSUE_DIR_NAME), expectedPaths);
2102
- moved += await moveOpenMarkdownFilesToClosed(join(storageDirAbsolute, PULL_DIR_NAME), expectedPaths);
2103
- return moved;
2104
- }
2105
- async function moveOpenMarkdownFilesToClosed(kindDirAbsolute, expectedPaths) {
2106
- let moved = 0;
2107
- const openFiles = await listOpenMarkdownFiles(kindDirAbsolute);
2108
- const closedDirAbsolute = join(kindDirAbsolute, CLOSED_DIR_NAME);
2109
- for (const markdownPath of openFiles) {
2110
- if (expectedPaths.has(markdownPath)) continue;
2111
- await movePath(markdownPath, await resolveUniqueClosedTarget(closedDirAbsolute, basename(markdownPath)));
2112
- moved += 1;
125
+ if (!remotes.length) return void 0;
126
+ const orderedRemotes = prioritizeRemotes(remotes);
127
+ for (const remote of orderedRemotes) try {
128
+ const repo = normalizeRepo((await execFileAsync("git", [
129
+ "remote",
130
+ "get-url",
131
+ remote
132
+ ], { cwd })).stdout.trim());
133
+ if (repo) return {
134
+ source: "git",
135
+ repo,
136
+ detail: `remote:${remote}`
137
+ };
138
+ } catch {
139
+ continue;
2113
140
  }
2114
- return moved;
2115
141
  }
2116
- async function listOpenMarkdownFiles(kindDirAbsolute) {
142
+ async function detectRepoFromPackageJson(cwd) {
143
+ const path = `${cwd}/package.json`;
144
+ if (!await pathExists(path)) return void 0;
145
+ let parsed;
2117
146
  try {
2118
- return (await readdir(kindDirAbsolute, {
2119
- withFileTypes: true,
2120
- encoding: "utf8"
2121
- })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => join(kindDirAbsolute, entry.name));
147
+ parsed = JSON.parse(await readFile(path, "utf8"));
2122
148
  } catch {
2123
- return [];
2124
- }
2125
- }
2126
- async function resolveUniqueClosedTarget(closedDirAbsolute, fileName) {
2127
- const baseName = fileName.replace(/\.md$/i, "");
2128
- let candidate = join(closedDirAbsolute, fileName);
2129
- let index = 1;
2130
- while (await pathExists(candidate)) {
2131
- candidate = join(closedDirAbsolute, `${baseName}-extra-${index}.md`);
2132
- index += 1;
2133
- }
2134
- return candidate;
2135
- }
2136
- //#endregion
2137
- //#region src/sync/sync-repository-provider.ts
2138
- async function fetchIssueCandidatesByPagination(context, since) {
2139
- const issues = [];
2140
- let scanned = 0;
2141
- const allOpenNumbers = context.config.sync.closed === false && !since ? /* @__PURE__ */ new Set() : void 0;
2142
- const state = context.config.sync.closed === false ? "open" : "all";
2143
- for await (const page of context.provider.paginateItems({
2144
- state,
2145
- since
2146
- })) for (const issue of page) {
2147
- if (!shouldSyncIssue(context.config.sync, issue)) continue;
2148
- scanned += 1;
2149
- issues.push(issue);
2150
- if (state === "open" && allOpenNumbers) allOpenNumbers.add(issue.number);
2151
- }
2152
- return {
2153
- issues,
2154
- scanned,
2155
- allOpenNumbers
2156
- };
2157
- }
2158
- async function fetchIssueCandidatesByNumbers(context, numbers) {
2159
- const issues = (await context.provider.fetchItemsByNumbers(numbers)).filter((issue) => shouldSyncIssue(context.config.sync, issue));
2160
- return {
2161
- issues,
2162
- scanned: issues.length
2163
- };
2164
- }
2165
- //#endregion
2166
- //#region src/utils/markdown.ts
2167
- function getTimestamp(value) {
2168
- const timestamp = Date.parse(value);
2169
- if (Number.isFinite(timestamp)) return timestamp;
2170
- return Number.NEGATIVE_INFINITY;
2171
- }
2172
- function renderRowsTable(rows) {
2173
- const lines = ["| Number | Title | Labels | Updated | File |", "| --- | --- | --- | --- | --- |"];
2174
- if (rows.length === 0) {
2175
- lines.push("| - | - | - | - | - |");
2176
- return lines;
149
+ return;
2177
150
  }
2178
- for (const row of rows) {
2179
- const labels = row.labels.length ? row.labels.map((label) => `\`${escapeInlineCode(label)}\``).join(", ") : "-";
2180
- lines.push(`| #${row.number} | ${escapeTableCell(row.title)} | ${labels} | ${escapeTableCell(row.updatedAt)} | [${row.filePath}](${row.filePath}) |`);
151
+ if (!parsed || typeof parsed !== "object") return void 0;
152
+ const repository = parsed.repository;
153
+ if (typeof repository === "string") {
154
+ const repo = normalizeRepo(repository);
155
+ if (repo) return {
156
+ source: "package-json",
157
+ repo,
158
+ detail: "package.json#repository"
159
+ };
2181
160
  }
2182
- return lines;
2183
- }
2184
- function escapeTableCell(value) {
2185
- return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim() || "-";
2186
- }
2187
- function escapeInlineCode(value) {
2188
- return value.replace(/`/g, "\\`");
2189
- }
2190
- //#endregion
2191
- //#region src/sync/sync-repository-snapshot.ts
2192
- async function writeRepoSnapshot(context) {
2193
- const repoSnapshot = await buildRepoSnapshot(context);
2194
- await mkdir(context.storageDirAbsolute, { recursive: true });
2195
- await writeFile(join(context.storageDirAbsolute, REPO_SNAPSHOT_FILE_NAME), `${JSON.stringify(repoSnapshot, null, 2)}\n`, "utf8");
2196
- }
2197
- async function writeRepositoryIndexes(context) {
2198
- const [issuesMarkdown, pullsMarkdown] = await Promise.all([renderIndexMarkdown(context, "issue"), renderIndexMarkdown(context, "pull")]);
2199
- await mkdir(context.storageDirAbsolute, { recursive: true });
2200
- await Promise.all([writeFile(join(context.storageDirAbsolute, ISSUES_INDEX_FILE_NAME), issuesMarkdown, "utf8"), writeFile(join(context.storageDirAbsolute, PULLS_INDEX_FILE_NAME), pullsMarkdown, "utf8")]);
2201
- }
2202
- async function renderIndexMarkdown(context, kind) {
2203
- const rows = Object.values(context.syncState.items).filter((item) => item.kind === kind).map((item) => readIndexRow(item));
2204
- const openRows = sortRows(rows.filter((row) => row.state === "open"));
2205
- const closedRows = sortRows(rows.filter((row) => row.state === "closed"));
2206
- return [
2207
- `# ${kind === "issue" ? "Issues" : "Pull Requests"}`,
2208
- "",
2209
- `- repo: ${context.repoSlug}`,
2210
- `- synced_at: ${context.syncedAt}`,
2211
- `- total: ${rows.length}`,
2212
- `- open: ${openRows.length}`,
2213
- `- closed: ${closedRows.length}`,
2214
- "",
2215
- `## Open (${openRows.length})`,
2216
- "",
2217
- ...renderRowsTable(openRows),
2218
- "",
2219
- `## Closed (${closedRows.length})`,
2220
- "",
2221
- ...renderRowsTable(closedRows),
2222
- ""
2223
- ].join("\n");
2224
- }
2225
- function readIndexRow(item) {
2226
- return {
2227
- number: item.number,
2228
- state: item.state,
2229
- title: item.data.item.title,
2230
- labels: item.data.item.labels,
2231
- updatedAt: item.data.item.updatedAt,
2232
- filePath: item.filePath
2233
- };
2234
- }
2235
- function sortRows(rows) {
2236
- return [...rows].sort((left, right) => {
2237
- const updatedDiff = getTimestamp(right.updatedAt) - getTimestamp(left.updatedAt);
2238
- if (updatedDiff !== 0) return updatedDiff;
2239
- return right.number - left.number;
2240
- });
2241
- }
2242
- async function buildRepoSnapshot(context) {
2243
- const [repoResult, labelsResult, milestonesResult] = await Promise.all([
2244
- context.provider.fetchRepository(),
2245
- context.provider.fetchRepositoryLabels(),
2246
- context.provider.fetchRepositoryMilestones()
2247
- ]);
2248
- const repository = repoResult;
2249
- const labels = labelsResult.map((label) => ({
2250
- name: label.name,
2251
- color: label.color,
2252
- description: label.description ?? null,
2253
- default: Boolean(label.default)
2254
- })).sort((left, right) => left.name.localeCompare(right.name));
2255
- const milestones = milestonesResult.map((milestone) => ({
2256
- number: milestone.number,
2257
- title: milestone.title,
2258
- state: milestone.state,
2259
- description: milestone.description ?? null,
2260
- due_on: milestone.due_on,
2261
- open_issues: milestone.open_issues,
2262
- closed_issues: milestone.closed_issues,
2263
- created_at: milestone.created_at,
2264
- updated_at: milestone.updated_at,
2265
- closed_at: milestone.closed_at
2266
- })).sort((left, right) => left.number - right.number);
2267
- return {
2268
- repo: context.repoSlug,
2269
- synced_at: context.syncedAt,
2270
- repository: {
2271
- owner: repository.owner.login,
2272
- name: repository.name,
2273
- full_name: repository.full_name,
2274
- description: repository.description ?? null,
2275
- private: repository.private,
2276
- archived: repository.archived,
2277
- default_branch: repository.default_branch,
2278
- html_url: repository.html_url,
2279
- fork: repository.fork,
2280
- open_issues_count: repository.open_issues_count,
2281
- has_issues: repository.has_issues,
2282
- has_projects: repository.has_projects,
2283
- has_wiki: repository.has_wiki,
2284
- created_at: repository.created_at,
2285
- updated_at: repository.updated_at,
2286
- pushed_at: repository.pushed_at
2287
- },
2288
- labels,
2289
- milestones
2290
- };
2291
- }
2292
- //#endregion
2293
- //#region src/sync/sync-repository.ts
2294
- async function syncRepository(options) {
2295
- const reporter = options.reporter;
2296
- const startedAt = /* @__PURE__ */ new Date();
2297
- const startedAtIso = startedAt.toISOString();
2298
- const runId = createSyncRunId();
2299
- const provider = options.provider ?? createRepositoryProvider({
2300
- token: options.token,
2301
- repo: options.repo
2302
- });
2303
- const storageDirAbsolute = resolve(options.config.cwd, options.config.directory);
2304
- const targetNumbers = normalizeIssueNumbers(options.numbers);
2305
- const counters = createCounters();
2306
- const stageDurations = createStageDurations();
2307
- let errorReported = false;
2308
- reporter?.onStart?.({
2309
- repo: options.repo,
2310
- startedAt: startedAtIso,
2311
- numbersCount: targetNumbers?.length,
2312
- snapshot: cloneSnapshot(counters)
2313
- });
2314
- let context;
2315
- let since;
2316
- let repoUpdatedAt;
2317
- let candidates = {
2318
- issues: [],
2319
- scanned: 0
2320
- };
2321
- const preparedCandidates = [];
2322
- let updatedIssues = 0;
2323
- let updatedPulls = 0;
2324
- let ghfsVersionMismatch = false;
2325
- let previousGhfsVersion;
2326
- const runStage = async (stage, message, fn) => {
2327
- reporter?.onStageStart?.({
2328
- stage,
2329
- message,
2330
- snapshot: cloneSnapshot(counters)
2331
- });
2332
- const stageStartedAt = Date.now();
2333
- try {
2334
- const result = await fn();
2335
- const durationMs = Date.now() - stageStartedAt;
2336
- stageDurations[stage] = durationMs;
2337
- reporter?.onStageEnd?.({
2338
- stage,
2339
- message,
2340
- durationMs,
2341
- snapshot: cloneSnapshot(counters)
2342
- });
2343
- return result;
2344
- } catch (error) {
2345
- errorReported = true;
2346
- stageDurations[stage] = Date.now() - stageStartedAt;
2347
- reporter?.onError?.({
2348
- stage,
2349
- error,
2350
- snapshot: cloneSnapshot(counters)
2351
- });
2352
- throw error;
2353
- }
2354
- };
2355
- try {
2356
- let shouldEarlyReturn = false;
2357
- await runStage("metadata", "Fetch repository metadata", async () => {
2358
- const syncState = await loadSyncState(storageDirAbsolute);
2359
- since = targetNumbers ? void 0 : resolveSince(options, syncState);
2360
- const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
2361
- context = {
2362
- provider,
2363
- repoSlug: options.repo,
2364
- storageDirAbsolute,
2365
- config: options.config,
2366
- syncState,
2367
- syncedAt,
2368
- totalIssues: 0,
2369
- totalPulls: 0
161
+ if (repository && typeof repository === "object") {
162
+ const url = repository.url;
163
+ if (typeof url === "string") {
164
+ const repo = normalizeRepo(url);
165
+ if (repo) return {
166
+ source: "package-json",
167
+ repo,
168
+ detail: "package.json#repository.url"
2370
169
  };
2371
- previousGhfsVersion = syncState.ghfsVersion;
2372
- ghfsVersionMismatch = syncState.ghfsVersion !== GHFS_VERSION;
2373
- if (targetNumbers) return;
2374
- repoUpdatedAt = (await provider.fetchRepository()).updated_at;
2375
- if (!options.full && syncState.lastRepoUpdatedAt && syncState.lastRepoUpdatedAt === repoUpdatedAt) shouldEarlyReturn = true;
2376
- reporter?.onStageUpdate?.({
2377
- stage: "metadata",
2378
- snapshot: cloneSnapshot(counters),
2379
- message: `since=${since ?? "(full)"} repoUpdatedAt=${repoUpdatedAt}`
2380
- });
2381
- });
2382
- assertContext(context);
2383
- const syncContext = context;
2384
- if (!shouldEarlyReturn) {
2385
- await runStage("pagination", "Pagination", async () => {
2386
- const paginatedSince = options.full ? void 0 : since;
2387
- candidates = targetNumbers ? await fetchIssueCandidatesByNumbers(syncContext, targetNumbers) : await fetchIssueCandidatesByPagination(syncContext, paginatedSince);
2388
- counters.scanned = candidates.scanned;
2389
- counters.selected = candidates.issues.length;
2390
- reporter?.onStageUpdate?.({
2391
- stage: "pagination",
2392
- snapshot: cloneSnapshot(counters),
2393
- message: `scanned=${counters.scanned} selected=${counters.selected}`
2394
- });
2395
- });
2396
- await runStage("fetch", "Fetch updated issues/PRs", async () => {
2397
- for (const issue of candidates.issues) {
2398
- const prepared = await prepareIssueCandidateSync(syncContext, issue);
2399
- preparedCandidates.push(prepared);
2400
- counters.processed += 1;
2401
- if (prepared.action === "refetch" || prepared.action === "create") if (prepared.kind === "issue") updatedIssues += 1;
2402
- else updatedPulls += 1;
2403
- reporter?.onStageUpdate?.({
2404
- stage: "fetch",
2405
- snapshot: cloneSnapshot(counters),
2406
- message: `${formatIssueNumber(issue.number, {
2407
- repo: options.repo,
2408
- kind: issue.kind
2409
- })} ${prepared.kind} ${prepared.action}`
2410
- });
2411
- }
2412
- });
2413
- syncContext.syncState.repo = options.repo;
2414
- if (!targetNumbers) {
2415
- syncContext.syncState.lastSyncedAt = syncContext.syncedAt;
2416
- syncContext.syncState.lastSince = since;
2417
- syncContext.syncState.lastRepoUpdatedAt = repoUpdatedAt;
2418
- }
2419
- await saveSyncState(syncContext.storageDirAbsolute, syncContext.syncState);
2420
- await runStage("materialize", "Materialize local files", async () => {
2421
- for (const prepared of preparedCandidates) addItemStats(counters, await materializePreparedIssue(syncContext, prepared));
2422
- });
2423
- await runStage("prune", "Prune stale local artifacts", async () => {
2424
- if (options.config.sync.closed === false) counters.patchesDeleted += await pruneTrackedClosedItems(syncContext.storageDirAbsolute, syncContext.syncState, options.config.sync);
2425
- if (!targetNumbers && options.config.sync.closed === false && candidates.allOpenNumbers) counters.patchesDeleted += await pruneMissingOpenTrackedItems(syncContext.storageDirAbsolute, syncContext.syncState, candidates.allOpenNumbers, options.config.sync);
2426
- reporter?.onStageUpdate?.({
2427
- stage: "prune",
2428
- snapshot: cloneSnapshot(counters),
2429
- message: `patchesDeleted=${counters.patchesDeleted}`
2430
- });
2431
- });
2432
170
  }
2433
- await runStage("save", "Save sync state", async () => {
2434
- if (ghfsVersionMismatch) {
2435
- const rematerialized = await rematerializeTrackedMarkdown(syncContext);
2436
- reporter?.onStageUpdate?.({
2437
- stage: "save",
2438
- snapshot: cloneSnapshot(counters),
2439
- message: `regenerated=${rematerialized.written} version=${previousGhfsVersion ?? "(none)"}->${GHFS_VERSION}`
2440
- });
2441
- }
2442
- const scanStats = await reconcileMarkdownFilesByScan(syncContext);
2443
- counters.written += scanStats.written;
2444
- counters.moved += scanStats.moved;
2445
- reporter?.onStageUpdate?.({
2446
- stage: "save",
2447
- snapshot: cloneSnapshot(counters),
2448
- message: `scan-fixed written=${scanStats.written} moved=${scanStats.moved}`
2449
- });
2450
- if (!shouldEarlyReturn) await writeRepoSnapshot(syncContext);
2451
- if (!shouldEarlyReturn || ghfsVersionMismatch) await writeRepositoryIndexes(syncContext);
2452
- syncContext.syncState.ghfsVersion = GHFS_VERSION;
2453
- await saveSyncState(syncContext.storageDirAbsolute, syncContext.syncState);
2454
- });
2455
- const totals = computeTotals(syncContext.syncState.items);
2456
- syncContext.totalIssues = totals.totalIssues;
2457
- syncContext.totalPulls = totals.totalPulls;
2458
- const finishedAt = /* @__PURE__ */ new Date();
2459
- const durationMs = Math.max(0, finishedAt.getTime() - startedAt.getTime());
2460
- const requestCount = provider.getRequestCount();
2461
- const summary = {
2462
- repo: options.repo,
2463
- since,
2464
- syncedAt: syncContext.syncedAt,
2465
- totalIssues: totals.totalIssues,
2466
- totalPulls: totals.totalPulls,
2467
- updatedIssues,
2468
- updatedPulls,
2469
- trackedItems: totals.trackedItems,
2470
- requestCount,
2471
- scanned: counters.scanned,
2472
- selected: counters.selected,
2473
- processed: counters.processed,
2474
- skipped: counters.skipped,
2475
- written: counters.written,
2476
- moved: counters.moved,
2477
- patchesWritten: counters.patchesWritten,
2478
- patchesDeleted: counters.patchesDeleted,
2479
- durationMs
2480
- };
2481
- syncContext.syncState.lastSyncRun = {
2482
- runId,
2483
- repo: options.repo,
2484
- startedAt: startedAtIso,
2485
- finishedAt: finishedAt.toISOString(),
2486
- durationMs,
2487
- requestCount,
2488
- since,
2489
- numbersCount: targetNumbers?.length,
2490
- counters: cloneSnapshot(counters),
2491
- stages: { ...stageDurations }
2492
- };
2493
- await saveSyncState(syncContext.storageDirAbsolute, syncContext.syncState);
2494
- reporter?.onComplete?.({
2495
- summary,
2496
- stages: { ...stageDurations }
2497
- });
2498
- return summary;
2499
- } catch (error) {
2500
- if (!errorReported) reporter?.onError?.({
2501
- error,
2502
- snapshot: cloneSnapshot(counters)
2503
- });
2504
- throw error;
2505
171
  }
2506
172
  }
2507
- function assertContext(context) {
2508
- if (!context) throw new Error("Sync context was not initialized");
2509
- }
2510
- function createSyncRunId() {
2511
- return `sync_${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "")}_${randomBytes(3).toString("hex")}`;
2512
- }
2513
- function createStageDurations() {
2514
- return {
2515
- metadata: 0,
2516
- pagination: 0,
2517
- fetch: 0,
2518
- materialize: 0,
2519
- prune: 0,
2520
- save: 0
2521
- };
2522
- }
2523
- function cloneSnapshot(counters) {
2524
- return {
2525
- scanned: counters.scanned,
2526
- selected: counters.selected,
2527
- processed: counters.processed,
2528
- skipped: counters.skipped,
2529
- written: counters.written,
2530
- moved: counters.moved,
2531
- patchesWritten: counters.patchesWritten,
2532
- patchesDeleted: counters.patchesDeleted
2533
- };
173
+ function prioritizeRemotes(remotes) {
174
+ const unique = [...new Set(remotes)];
175
+ const priority = ["origin", "upstream"];
176
+ const prioritized = priority.filter((name) => unique.includes(name));
177
+ const rest = unique.filter((name) => !priority.includes(name));
178
+ return [...prioritized, ...rest];
2534
179
  }
2535
- function computeTotals(items) {
2536
- let totalIssues = 0;
2537
- let totalPulls = 0;
2538
- for (const item of Object.values(items)) if (item.kind === "issue") totalIssues += 1;
2539
- else totalPulls += 1;
2540
- return {
2541
- totalIssues,
2542
- totalPulls,
2543
- trackedItems: totalIssues + totalPulls
2544
- };
180
+ function stripGitSuffix(name) {
181
+ return name.replace(/\.git$/, "");
2545
182
  }
2546
183
  //#endregion
2547
184
  //#region src/cli/action-color.ts
@@ -2601,7 +238,12 @@ function wrapTextValue(value) {
2601
238
  function withErrorHandling(fn) {
2602
239
  return (...args) => {
2603
240
  fn(...args).catch((error) => {
2604
- console.error(error);
241
+ if (error instanceof CodedError) {
242
+ const d = error.diagnostic;
243
+ console.error(c.red(`[${d.code}] ${d.message}`));
244
+ if (d.fix) console.error(c.dim(` fix: ${d.fix}`));
245
+ if (d.docs) console.error(c.dim(` see: ${d.docs}`));
246
+ } else console.error(error);
2605
247
  process.exit(1);
2606
248
  });
2607
249
  };
@@ -2743,7 +385,7 @@ function createRichSyncReporter(printer) {
2743
385
  printer.success(`Sync finished. ${event.summary.updatedIssues} issues and ${event.summary.updatedPulls} PRs updated${c.dim(` (${formatDuration(event.summary.durationMs)})`)}.`);
2744
386
  },
2745
387
  onError(event) {
2746
- const message = `Sync failed${event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : ""}: ${toErrorMessage$2(event.error)}`;
388
+ const message = `Sync failed${event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : ""}: ${toErrorMessage(event.error)}`;
2747
389
  if (hasSyncProgress) {
2748
390
  syncProgress.error(message);
2749
391
  hasSyncProgress = false;
@@ -2783,7 +425,7 @@ function createPlainSyncReporter(printer, progressEvery) {
2783
425
  },
2784
426
  onError(event) {
2785
427
  const stage = event.stage && !isHiddenSyncStage(event.stage) ? ` while ${describeStage(event.stage)}` : "";
2786
- printer.error(c.red(`Sync failed${stage}: ${toErrorMessage$2(event.error)}`));
428
+ printer.error(c.red(`Sync failed${stage}: ${toErrorMessage(event.error)}`));
2787
429
  }
2788
430
  };
2789
431
  }
@@ -2821,7 +463,7 @@ function createRichExecuteReporter(printer) {
2821
463
  printer.success(`${runMode} finished. Planned ${event.result.planned}, applied ${event.result.applied}, failed ${event.result.failed}.`);
2822
464
  },
2823
465
  onError(event) {
2824
- const message = `Execution failed: ${toErrorMessage$2(event.error)}`;
466
+ const message = `Execution failed: ${toErrorMessage(event.error)}`;
2825
467
  if (hasApplyProgress) {
2826
468
  applyProgress.error(message);
2827
469
  hasApplyProgress = false;
@@ -2845,7 +487,7 @@ function createPlainExecuteReporter(printer) {
2845
487
  printer.success(`${runMode} finished. Planned ${event.result.planned}, applied ${event.result.applied}, failed ${event.result.failed}.`);
2846
488
  },
2847
489
  onError(event) {
2848
- printer.error(c.red(`Execution failed: ${toErrorMessage$2(event.error)}`));
490
+ printer.error(c.red(`Execution failed: ${toErrorMessage(event.error)}`));
2849
491
  }
2850
492
  };
2851
493
  }
@@ -2865,7 +507,7 @@ function formatStageCompletionLine(stage, snapshot, durationMs) {
2865
507
  if (stage === "pagination") return `Pagination scanned ${countNoun(snapshot.scanned, "candidate item")}${duration}.`;
2866
508
  if (stage === "fetch") return `Fetched updated issues/PRs (${snapshot.processed}/${snapshot.selected})${duration}.`;
2867
509
  }
2868
- function toErrorMessage$2(error) {
510
+ function toErrorMessage(error) {
2869
511
  return error.message || String(error);
2870
512
  }
2871
513
  function formatKeyValueLines(entries, options = {}) {
@@ -2972,7 +614,7 @@ async function confirmExecuteApply(count) {
2972
614
  //#endregion
2973
615
  //#region src/cli/commands/execute.ts
2974
616
  const PLAN_PREVIEW_LIMIT = 20;
2975
- const defaultDependencies$1 = {
617
+ const defaultDependencies = {
2976
618
  createCliPrinter,
2977
619
  resolveConfig,
2978
620
  isTTY: () => Boolean(process.stdin.isTTY),
@@ -2988,7 +630,7 @@ const defaultDependencies$1 = {
2988
630
  function registerExecuteCommand(cli) {
2989
631
  cli.command("execute", "Execute operations from .ghfs/execute.yml").option("--repo <repo>", "GitHub repository in owner/name format").option("--file <file>", "Path to execute yaml file").option("--run", "Run mutations on GitHub").option("--non-interactive", "Disable interactive prompts").option("--continue-on-error", "Continue applying ops after a failure").action(withErrorHandling(async (options) => runExecuteCommand(options)));
2990
632
  }
2991
- async function runExecuteCommand(options, dependencies = defaultDependencies$1) {
633
+ async function runExecuteCommand(options, dependencies = defaultDependencies) {
2992
634
  const printer = dependencies.createCliPrinter("execute");
2993
635
  const config = await dependencies.resolveConfig();
2994
636
  const storageDirAbsolute = getStorageDirAbsolute(config);
@@ -3265,645 +907,67 @@ function setupSyncCommand(command) {
3265
907
  }));
3266
908
  }
3267
909
  //#endregion
3268
- //#region src/cli/ui/server.ts
3269
- const MIME_TYPES = {
3270
- ".css": "text/css; charset=utf-8",
3271
- ".html": "text/html; charset=utf-8",
3272
- ".ico": "image/x-icon",
3273
- ".js": "text/javascript; charset=utf-8",
3274
- ".json": "application/json; charset=utf-8",
3275
- ".map": "application/json; charset=utf-8",
3276
- ".png": "image/png",
3277
- ".svg": "image/svg+xml",
3278
- ".txt": "text/plain; charset=utf-8",
3279
- ".woff": "font/woff",
3280
- ".woff2": "font/woff2"
3281
- };
3282
- async function createUiServer(options) {
3283
- const uiRoot = resolve(options.uiDir);
3284
- const uiRootPrefix = `${uiRoot}/`;
3285
- const indexPath = resolve(uiRoot, "index.html");
3286
- await assertFile(indexPath);
3287
- const server = createServer(async (req, res) => {
3288
- try {
3289
- const pathname = normalizeRequestPath(req.url || "/");
3290
- if (pathname === "/api/metadata.json") {
3291
- const body = `${JSON.stringify(options.getMetadata())}\n`;
3292
- res.statusCode = 200;
3293
- res.setHeader("Content-Type", "application/json; charset=utf-8");
3294
- res.end(body);
3295
- return;
3296
- }
3297
- const filePath = resolve(uiRoot, `.${pathname === "/" ? "/index.html" : pathname}`);
3298
- const resolvedFilePath = (filePath === uiRoot || filePath.startsWith(uiRootPrefix)) && await isFile(filePath) ? filePath : indexPath;
3299
- const body = await readFile(resolvedFilePath);
3300
- res.statusCode = 200;
3301
- res.setHeader("Content-Type", mimeTypeFor(resolvedFilePath));
3302
- res.end(body);
3303
- } catch (error) {
3304
- res.statusCode = 500;
3305
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
3306
- res.end(`Internal Server Error: ${toErrorMessage$1(error)}`);
3307
- }
3308
- });
3309
- await new Promise((resolvePromise, reject) => {
3310
- server.once("error", reject);
3311
- server.listen(options.port, options.host, () => {
3312
- resolvePromise();
3313
- });
3314
- });
3315
- const address = server.address();
3316
- if (!address || typeof address === "string") throw new Error("Failed to start UI HTTP server");
3317
- return {
3318
- server,
3319
- close: () => {
3320
- return new Promise((resolvePromise, reject) => {
3321
- server.close((error) => {
3322
- if (error) {
3323
- reject(error);
3324
- return;
3325
- }
3326
- resolvePromise();
3327
- });
3328
- });
3329
- },
3330
- url: `http://${address.address === "127.0.0.1" ? "localhost" : address.address}:${address.port}`
3331
- };
3332
- }
3333
- function normalizeRequestPath(raw) {
3334
- const pathOnly = raw.split("?")[0].split("#")[0] || "/";
3335
- let decodedPath = pathOnly;
3336
- try {
3337
- decodedPath = decodeURIComponent(pathOnly);
3338
- } catch {
3339
- decodedPath = pathOnly;
3340
- }
3341
- const normalized = normalize(decodedPath);
3342
- return normalized.startsWith("/") ? normalized : `/${normalized}`;
3343
- }
3344
- async function assertFile(path) {
3345
- if (!await isFile(path)) throw new Error(`UI assets not found at ${path}`);
3346
- }
3347
- async function isFile(path) {
3348
- try {
3349
- return (await stat(path)).isFile();
3350
- } catch {
3351
- return false;
3352
- }
3353
- }
3354
- function mimeTypeFor(path) {
3355
- return MIME_TYPES[extname(path)] ?? "application/octet-stream";
3356
- }
3357
- function toErrorMessage$1(error) {
3358
- if (error instanceof Error) return error.message;
3359
- return String(error);
3360
- }
3361
- //#endregion
3362
- //#region src/cli/ui/rpc.ts
3363
- const REPLACEABLE_FAMILIES = new Set([
3364
- "title",
3365
- "body",
3366
- "state",
3367
- "labels",
3368
- "assignees",
3369
- "milestone",
3370
- "reviewers",
3371
- "draft"
3372
- ]);
3373
- function createServerFunctions(options) {
3374
- const resolveRepoFn = options.resolveRepo ?? resolveRepo;
3375
- const resolveAuthTokenFn = options.resolveAuthToken ?? resolveAuthToken;
3376
- const executePendingChangesFn = options.executePendingChanges ?? executePendingChanges;
3377
- const appendExecutionResultFn = options.appendExecutionResult ?? appendExecutionResult;
3378
- const syncRepositoryFn = options.syncRepository ?? syncRepository;
3379
- const loadExecuteSourcesFn = options.loadExecuteSources ?? loadExecuteSources;
3380
- let executing = false;
3381
- async function getBootstrap() {
3382
- const syncState = await loadSyncState(options.storageDirAbsolute);
3383
- const loaded = await loadExecuteSourcesFn(options.executeFilePath);
3384
- const items = Object.values(syncState.items).map((entry) => {
3385
- const item = entry.data.item;
3386
- return {
3387
- number: entry.number,
3388
- kind: entry.kind,
3389
- state: entry.state,
3390
- title: item.title,
3391
- updatedAt: item.updatedAt,
3392
- createdAt: item.createdAt,
3393
- closedAt: item.closedAt,
3394
- author: item.author,
3395
- url: item.url,
3396
- labels: item.labels,
3397
- assignees: item.assignees,
3398
- milestone: item.milestone,
3399
- commentsCount: entry.data.comments.length,
3400
- isDraft: entry.data.pull?.isDraft,
3401
- merged: entry.data.pull?.merged,
3402
- requestedReviewers: entry.data.pull?.requestedReviewers ?? []
3403
- };
3404
- }).sort((left, right) => right.number - left.number);
3405
- const queue = toQueueEntries(loaded.entries, syncState.repo);
3406
- const openCount = items.filter((item) => item.state === "open").length;
3407
- return {
3408
- repo: syncState.repo,
3409
- syncedAt: syncState.lastSyncedAt,
3410
- lastSyncRunAt: syncState.lastSyncRun?.finishedAt,
3411
- totalTracked: items.length,
3412
- openCount,
3413
- closedCount: items.length - openCount,
3414
- warnings: loaded.warnings,
3415
- items,
3416
- queue,
3417
- queueSummary: {
3418
- total: queue.length,
3419
- executeYml: queue.filter((entry) => entry.source === "execute.yml").length,
3420
- executeMd: queue.filter((entry) => entry.source === "execute.md").length,
3421
- perItem: queue.filter((entry) => entry.source === "per-item").length
3422
- }
3423
- };
3424
- }
3425
- async function getItemDetail(number) {
3426
- const [syncState, repoSnapshot, loaded, yml] = await Promise.all([
3427
- loadSyncState(options.storageDirAbsolute),
3428
- readRepoSnapshot(options.storageDirAbsolute),
3429
- loadExecuteSourcesFn(options.executeFilePath),
3430
- readAndValidateExecuteFileWithSource(options.executeFilePath)
3431
- ]);
3432
- const tracked = syncState.items[String(number)];
3433
- if (!tracked) throw new Error(`Item #${number} is not available in local mirror`);
3434
- const queue = toQueueEntries(loaded.entries, syncState.repo).filter((entry) => entry.op.number === number);
3435
- const effective = applyQueuedYmlOpsToItem(tracked, yml.ops.filter((op) => op.number === number));
3436
- return {
3437
- number: tracked.number,
3438
- kind: tracked.kind,
3439
- state: effective.state,
3440
- title: effective.title,
3441
- body: effective.body,
3442
- updatedAt: tracked.data.item.updatedAt,
3443
- createdAt: tracked.data.item.createdAt,
3444
- closedAt: tracked.data.item.closedAt,
3445
- author: tracked.data.item.author,
3446
- url: tracked.data.item.url,
3447
- labels: effective.labels,
3448
- assignees: effective.assignees,
3449
- milestone: effective.milestone,
3450
- commentsCount: tracked.data.comments.length,
3451
- comments: tracked.data.comments.map((comment) => ({
3452
- id: comment.id,
3453
- author: comment.author,
3454
- body: comment.body || "",
3455
- createdAt: comment.createdAt,
3456
- updatedAt: comment.updatedAt
3457
- })),
3458
- isDraft: effective.isDraft,
3459
- merged: tracked.data.pull?.merged,
3460
- requestedReviewers: effective.reviewers,
3461
- labelsCatalog: repoSnapshot?.labels ?? [],
3462
- milestonesCatalog: (repoSnapshot?.milestones ?? []).map((m) => ({
3463
- number: m.number,
3464
- title: m.title,
3465
- state: m.state
3466
- })),
3467
- queue
3468
- };
3469
- }
3470
- async function queueItemEdits(payload) {
3471
- const tracked = (await loadSyncState(options.storageDirAbsolute)).items[String(payload.number)];
3472
- if (!tracked) throw new Error(`Item #${payload.number} is not available in local mirror`);
3473
- const nextOps = toQueuedOps(tracked, payload);
3474
- const yml = await readAndValidateExecuteFileWithSource(options.executeFilePath);
3475
- const filteredOps = [];
3476
- for (const [index, op] of yml.ops.entries()) {
3477
- const family = getActionFamily(op.action);
3478
- if (op.number === payload.number && REPLACEABLE_FAMILIES.has(family)) continue;
3479
- filteredOps.push({
3480
- ...op,
3481
- _actionInput: yml.sourceActions[index] ?? op.action
3482
- });
3483
- }
3484
- const writable = [...filteredOps.map(({ _actionInput, ...op }) => ({
3485
- ...op,
3486
- action: _actionInput
3487
- })), ...nextOps.map((op) => ({
3488
- ...op,
3489
- action: op.action
3490
- }))];
3491
- await writeExecuteFile(options.executeFilePath, writable);
3492
- return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3493
- }
3494
- async function removeQueueYmlEntry(index) {
3495
- const yml = await readAndValidateExecuteFileWithSource(options.executeFilePath);
3496
- if (!Number.isInteger(index) || index < 0 || index >= yml.ops.length) throw new Error(`Invalid execute.yml index: ${index}`);
3497
- const writable = yml.ops.map((op, opIndex) => ({
3498
- ...op,
3499
- action: yml.sourceActions[opIndex] ?? op.action
3500
- })).filter((_, opIndex) => opIndex !== index);
3501
- await writeExecuteFile(options.executeFilePath, writable);
3502
- return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3503
- }
3504
- async function refresh() {
3505
- return await notifyAndGetBootstrap(options.onStateChanged, getBootstrap);
3506
- }
3507
- async function executeNow() {
3508
- if (executing) throw new Error("Execution is already in progress");
3509
- executing = true;
3510
- try {
3511
- const resolvedRepo = await resolveRepoFn({
3512
- cwd: options.config.cwd,
3513
- configRepo: options.config.repo,
3514
- interactive: false
3515
- });
3516
- const token = await resolveAuthTokenFn({
3517
- token: options.config.auth.token,
3518
- interactive: false
3519
- });
3520
- const result = await executePendingChangesFn({
3521
- config: options.config,
3522
- repo: resolvedRepo.repo,
3523
- token,
3524
- executeFilePath: options.executeFilePath,
3525
- apply: true,
3526
- nonInteractive: true,
3527
- continueOnError: false,
3528
- reporter: {
3529
- onStart: (event) => {
3530
- fireAndForget(options.onExecuteProgress, {
3531
- type: "start",
3532
- planned: event.planned,
3533
- repo: event.repo
3534
- });
3535
- },
3536
- onProgress: (event) => {
3537
- fireAndForget(options.onExecuteProgress, {
3538
- type: "progress",
3539
- repo: event.repo,
3540
- planned: event.planned,
3541
- completed: event.completed,
3542
- applied: event.applied,
3543
- failed: event.failed,
3544
- detail: event.detail
3545
- });
3546
- },
3547
- onError: (event) => {
3548
- fireAndForget(options.onExecuteProgress, {
3549
- type: "error",
3550
- message: toErrorMessage(event.error)
3551
- });
3552
- }
3553
- }
3554
- });
3555
- await appendExecutionResultFn(options.storageDirAbsolute, result);
3556
- const affectedNumbers = [...new Set(result.details.filter((detail) => detail.status === "applied").map((detail) => detail.number))];
3557
- if (affectedNumbers.length > 0) await syncRepositoryFn({
3558
- config: options.config,
3559
- repo: resolvedRepo.repo,
3560
- token,
3561
- numbers: affectedNumbers
3562
- });
3563
- await options.onExecuteComplete?.(result);
3564
- return {
3565
- result,
3566
- bootstrap: await notifyAndGetBootstrap(options.onStateChanged, getBootstrap)
3567
- };
3568
- } finally {
3569
- executing = false;
3570
- }
3571
- }
3572
- return {
3573
- getBootstrap,
3574
- getItemDetail,
3575
- queueItemEdits,
3576
- removeQueueYmlEntry,
3577
- refresh,
3578
- executeNow
3579
- };
3580
- }
3581
- function toQueueEntries(entries, repo) {
3582
- return entries.map((entry) => ({
3583
- id: `${entry.source}:${entry.sourceIndex}:${entry.mergedIndex}`,
3584
- mergedIndex: entry.mergedIndex,
3585
- source: entry.source,
3586
- sourceIndex: entry.sourceIndex,
3587
- editable: entry.source === "execute.yml",
3588
- op: entry.op,
3589
- description: describeCliOperation(entry.op, {
3590
- tty: false,
3591
- repo
3592
- })
3593
- }));
3594
- }
3595
- function toQueuedOps(tracked, payload) {
3596
- const current = tracked.data.item;
3597
- const reviewersCurrent = tracked.data.pull?.requestedReviewers ?? [];
3598
- const desiredLabels = normalizeStringArray(payload.labels);
3599
- const desiredAssignees = normalizeStringArray(payload.assignees);
3600
- const desiredReviewers = normalizeStringArray(payload.reviewers);
3601
- const desiredMilestone = normalizeMilestone(payload.milestone);
3602
- const desiredTitle = payload.title.trim() || current.title;
3603
- const desiredBody = payload.body.trim().length > 0 ? payload.body : current.body || "";
3604
- const desiredState = payload.state;
3605
- const desiredDraft = tracked.kind === "pull" ? Boolean(payload.isDraft) : tracked.data.pull?.isDraft;
3606
- const ops = computeExecuteDiffOps({
3607
- number: tracked.number,
3608
- current: {
3609
- title: current.title,
3610
- body: current.body,
3611
- state: current.state,
3612
- labels: current.labels,
3613
- assignees: current.assignees,
3614
- milestone: current.milestone,
3615
- reviewers: reviewersCurrent,
3616
- isDraft: tracked.data.pull?.isDraft
3617
- },
3618
- desired: {
3619
- title: desiredTitle,
3620
- body: desiredBody,
3621
- state: desiredState,
3622
- labels: desiredLabels,
3623
- assignees: desiredAssignees,
3624
- milestone: desiredMilestone,
3625
- reviewers: tracked.kind === "pull" ? desiredReviewers : reviewersCurrent,
3626
- isDraft: desiredDraft
3627
- },
3628
- ifUnchangedSince: current.updatedAt,
3629
- includeBody: true
3630
- });
3631
- const comment = payload.comment.trim();
3632
- if (comment) ops.push({
3633
- action: "add-comment",
3634
- number: tracked.number,
3635
- body: comment
3636
- });
3637
- return ops;
3638
- }
3639
- function applyQueuedYmlOpsToItem(tracked, ops) {
3640
- let title = tracked.data.item.title;
3641
- let body = tracked.data.item.body || "";
3642
- let state = tracked.data.item.state;
3643
- let labels = [...tracked.data.item.labels];
3644
- let assignees = [...tracked.data.item.assignees];
3645
- let milestone = tracked.data.item.milestone;
3646
- let reviewers = [...tracked.data.pull?.requestedReviewers ?? []];
3647
- let isDraft = tracked.data.pull?.isDraft;
3648
- for (const op of ops) switch (op.action) {
3649
- case "set-title":
3650
- title = op.title;
3651
- break;
3652
- case "set-body":
3653
- body = op.body;
3654
- break;
3655
- case "close":
3656
- case "close-with-comment":
3657
- state = "closed";
3658
- break;
3659
- case "reopen":
3660
- state = "open";
3661
- break;
3662
- case "add-labels":
3663
- labels = mergeStrings(labels, op.labels);
3664
- break;
3665
- case "remove-labels":
3666
- labels = removeStrings(labels, op.labels);
3667
- break;
3668
- case "set-labels":
3669
- labels = normalizeStringArray(op.labels);
3670
- break;
3671
- case "add-assignees":
3672
- assignees = mergeStrings(assignees, op.assignees);
3673
- break;
3674
- case "remove-assignees":
3675
- assignees = removeStrings(assignees, op.assignees);
3676
- break;
3677
- case "set-assignees":
3678
- assignees = normalizeStringArray(op.assignees);
3679
- break;
3680
- case "set-milestone":
3681
- milestone = String(op.milestone);
3682
- break;
3683
- case "clear-milestone":
3684
- milestone = null;
3685
- break;
3686
- case "request-reviewers":
3687
- reviewers = mergeStrings(reviewers, op.reviewers);
3688
- break;
3689
- case "remove-reviewers":
3690
- reviewers = removeStrings(reviewers, op.reviewers);
3691
- break;
3692
- case "convert-to-draft":
3693
- if (tracked.kind === "pull") isDraft = true;
3694
- break;
3695
- case "mark-ready-for-review":
3696
- if (tracked.kind === "pull") isDraft = false;
3697
- break;
3698
- default: break;
3699
- }
3700
- return {
3701
- title,
3702
- body,
3703
- state,
3704
- labels,
3705
- assignees,
3706
- milestone,
3707
- reviewers,
3708
- isDraft
3709
- };
3710
- }
3711
- async function notifyAndGetBootstrap(onStateChanged, getBootstrap) {
3712
- const bootstrap = await getBootstrap();
3713
- await onStateChanged?.(bootstrap);
3714
- return bootstrap;
3715
- }
3716
- function getActionFamily(action) {
3717
- switch (action) {
3718
- case "set-title": return "title";
3719
- case "set-body": return "body";
3720
- case "close":
3721
- case "reopen": return "state";
3722
- case "add-labels":
3723
- case "remove-labels":
3724
- case "set-labels": return "labels";
3725
- case "add-assignees":
3726
- case "remove-assignees":
3727
- case "set-assignees": return "assignees";
3728
- case "set-milestone":
3729
- case "clear-milestone": return "milestone";
3730
- case "request-reviewers":
3731
- case "remove-reviewers": return "reviewers";
3732
- case "mark-ready-for-review":
3733
- case "convert-to-draft": return "draft";
3734
- case "add-comment":
3735
- case "close-with-comment": return "comment";
3736
- default: return "other";
3737
- }
3738
- }
3739
- function mergeStrings(base, incoming) {
3740
- const known = new Set(base);
3741
- const merged = [...base];
3742
- for (const value of normalizeStringArray(incoming)) {
3743
- if (known.has(value)) continue;
3744
- known.add(value);
3745
- merged.push(value);
3746
- }
3747
- return merged;
3748
- }
3749
- function removeStrings(base, removing) {
3750
- const removingSet = new Set(normalizeStringArray(removing));
3751
- return base.filter((value) => !removingSet.has(value));
3752
- }
3753
- function fireAndForget(callback, payload) {
3754
- if (!callback) return;
3755
- Promise.resolve(callback(payload)).catch(() => {});
3756
- }
3757
- function toErrorMessage(error) {
3758
- if (error instanceof Error) return error.message;
3759
- return String(error);
3760
- }
3761
- async function readRepoSnapshot(storageDirAbsolute) {
3762
- const path = resolve(storageDirAbsolute, REPO_SNAPSHOT_FILE_NAME);
3763
- if (!await pathExists(path)) return void 0;
3764
- try {
3765
- const raw = await readFile(path, "utf8");
3766
- return JSON.parse(raw);
3767
- } catch {
3768
- return;
3769
- }
3770
- }
3771
- //#endregion
3772
- //#region src/cli/ui/ws.ts
3773
- async function createWsServer(options) {
3774
- const wss = new WebSocketServer({
3775
- host: options.host,
3776
- port: options.port ?? 0
3777
- });
3778
- await waitForListen(wss);
3779
- const wsClients = /* @__PURE__ */ new Set();
3780
- let rpc;
3781
- const serverFunctions = createServerFunctions({
3782
- ...options,
3783
- onStateChanged: (bootstrap) => rpc.broadcast.onStateChanged.asEvent(bootstrap),
3784
- onExecuteProgress: (event) => rpc.broadcast.onExecuteProgress.asEvent(event),
3785
- onExecuteComplete: (result) => rpc.broadcast.onExecuteComplete.asEvent(result)
3786
- });
3787
- rpc = createBirpcGroup(serverFunctions, [], {
3788
- timeout: 12e4,
3789
- onFunctionError(error, name) {
3790
- console.error(c.red(`RPC error on "${name}":`));
3791
- console.error(error);
3792
- }
3793
- });
3794
- wss.on("connection", (ws) => {
3795
- wsClients.add(ws);
3796
- const channel = {
3797
- post: (d) => ws.send(d),
3798
- on: (fn) => {
3799
- ws.on("message", (data) => {
3800
- fn(data);
3801
- });
3802
- },
3803
- serialize: stringify$1,
3804
- deserialize: parse$1
3805
- };
3806
- rpc.updateChannels((channels) => {
3807
- channels.push(channel);
3808
- });
3809
- ws.on("close", () => {
3810
- wsClients.delete(ws);
3811
- rpc.updateChannels((channels) => {
3812
- const index = channels.indexOf(channel);
3813
- if (index >= 0) channels.splice(index, 1);
3814
- });
3815
- });
3816
- });
3817
- return {
3818
- wss,
3819
- rpc,
3820
- serverFunctions,
3821
- close: async () => {
3822
- for (const client of wsClients) client.terminate();
3823
- await new Promise((resolve, reject) => {
3824
- wss.close((error) => {
3825
- if (error) {
3826
- reject(error);
3827
- return;
3828
- }
3829
- resolve();
3830
- });
3831
- });
3832
- },
3833
- getMetadata() {
3834
- const address = wss.address();
3835
- if (!address) throw new Error("WebSocket server is not listening");
3836
- return {
3837
- backend: "websocket",
3838
- websocket: address.port
3839
- };
3840
- }
3841
- };
3842
- }
3843
- async function waitForListen(server) {
3844
- await new Promise((resolve, reject) => {
3845
- server.once("listening", () => resolve());
3846
- server.once("error", (error) => reject(error));
3847
- });
3848
- }
3849
- //#endregion
3850
910
  //#region src/cli/commands/ui.ts
3851
- const defaultDependencies = {
3852
- createCliPrinter,
3853
- resolveConfig,
3854
- ensureExecuteArtifacts,
3855
- createUiServer,
3856
- createWsServer
3857
- };
3858
911
  function registerUiCommand(cli) {
3859
- cli.command("ui", "Serve local Web UI for synced mirror and execute queue").option("--host <host>", "Host for local UI server", { default: "127.0.0.1" }).option("--port <port>", "Port for local UI server", { default: 3589 }).action(withErrorHandling(async (options) => {
3860
- await runUiCommand(options);
3861
- }));
3862
- }
3863
- async function runUiCommand(options, dependencies = defaultDependencies) {
3864
- const printer = dependencies.createCliPrinter("ui");
3865
- const host = options.host || "127.0.0.1";
3866
- const parsedPort = Number(options.port);
3867
- const port = Number.isFinite(parsedPort) && parsedPort >= 0 ? parsedPort : 3589;
3868
- const config = await dependencies.resolveConfig();
3869
- const storageDirAbsolute = getStorageDirAbsolute(config);
3870
- const executeFilePath = resolve(config.cwd, getExecuteFile(config));
3871
- const uiDir = resolve(config.cwd, "dist/ui");
3872
- await dependencies.ensureExecuteArtifacts(executeFilePath);
3873
- const ws = await dependencies.createWsServer({
3874
- host,
3875
- config,
3876
- executeFilePath,
3877
- storageDirAbsolute
3878
- });
3879
- let http;
3880
- try {
3881
- http = await dependencies.createUiServer({
3882
- host,
912
+ cli.command("ui", "Launch a local web UI for the mirror").option("--repo <repo>", "GitHub repository in owner/name format").option("--port <port>", "Port to listen on", { default: 7710 }).option("--host <host>", "Host to bind", { default: "127.0.0.1" }).option("--no-open", "Do not open the browser automatically").option("--no-portless", "Do not expose the UI through the portless reverse proxy").option("--subdomain <slug>", "Override the portless subdomain (defaults to the repository name)").option("--cwd <cwd>", "Project root directory (defaults to current directory)").action(withErrorHandling(async (options) => {
913
+ const printer = createCliPrinter("ui");
914
+ const config = await resolveConfig({ cwd: options.cwd ? resolve(options.cwd) : void 0 });
915
+ await ensureExecuteArtifacts(resolve(config.cwd, getExecuteFile(config)));
916
+ const repo = await resolveRepo({
917
+ cwd: config.cwd,
918
+ cliRepo: options.repo,
919
+ configRepo: config.repo,
920
+ interactive: Boolean(process.stdin.isTTY),
921
+ selectRepoChoice: promptRepoChoice
922
+ });
923
+ printer.header(repo.repo);
924
+ const initialToken = await resolveAuthToken({
925
+ token: config.auth.token,
926
+ interactive: false,
927
+ promptForToken
928
+ }).catch(() => "");
929
+ const port = typeof options.port === "number" ? options.port : Number(options.port ?? 7710);
930
+ const host = options.host ?? "127.0.0.1";
931
+ const portlessEnabled = options.portless !== false;
932
+ const subdomain = options.subdomain?.trim() || slugifyRepoName(repo.repo);
933
+ const server = await createUiServer({
934
+ config,
935
+ repo: repo.repo,
936
+ initialToken,
3883
937
  port,
3884
- uiDir,
3885
- getMetadata: ws.getMetadata
938
+ host,
939
+ devMode: process.env.GHFS_UI_DEV === "1",
940
+ portless: portlessEnabled ? {
941
+ enabled: true,
942
+ subdomain
943
+ } : void 0,
944
+ logger: {
945
+ info: (message) => printer.info(message),
946
+ warn: (message) => printer.warn(message)
947
+ },
948
+ onRequestToken: async () => resolveAuthToken({
949
+ token: config.auth.token,
950
+ interactive: Boolean(process.stdin.isTTY),
951
+ promptForToken
952
+ })
3886
953
  });
3887
- } catch (error) {
3888
- await ws.close().catch(() => {});
3889
- throw error;
3890
- }
3891
- printer.success(`Web UI ready at ${http.url}`);
3892
- printer.info(`RPC websocket listening on ${ws.getMetadata().websocket}`);
3893
- printer.info("Press Ctrl+C to stop.");
3894
- let shuttingDown = false;
3895
- const shutdown = async () => {
3896
- if (shuttingDown) return;
3897
- shuttingDown = true;
3898
- await Promise.allSettled([http.close(), ws.close()]);
3899
- process.exit(0);
3900
- };
3901
- process.once("SIGINT", () => {
3902
- shutdown();
3903
- });
3904
- process.once("SIGTERM", () => {
3905
- shutdown();
3906
- });
954
+ if (server.portlessUrl) {
955
+ printer.info(`ghfs UI running at ${server.portlessUrl}`);
956
+ printer.info(` direct: ${server.directUrl}`);
957
+ } else printer.info(`ghfs UI running at ${server.directUrl}`);
958
+ if (!initialToken) printer.info("No GitHub token yet; sync/execute will prompt or fail until one is available.");
959
+ if (options.open !== false) {
960
+ const { default: open } = await import("open");
961
+ await open(server.url);
962
+ }
963
+ const shutdown = async () => {
964
+ await server.close().catch(() => {});
965
+ process.exit(0);
966
+ };
967
+ process.once("SIGINT", shutdown);
968
+ process.once("SIGTERM", shutdown);
969
+ await new Promise(() => {});
970
+ }));
3907
971
  }
3908
972
  //#endregion
3909
973
  //#region src/cli/index.ts