@runcofactor/cli 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/{account-RHHS7HT4.js → account-CZYZD2JS.js} +5 -5
  2. package/dist/{alert-B5WEOKHP.js → alert-JQ4SDEB7.js} +8 -8
  3. package/dist/{api-catalog-UXONX3BR.js → api-catalog-IWQC35P2.js} +5 -5
  4. package/dist/{api-key-J3A4TJKN.js → api-key-GUDXIIEA.js} +5 -5
  5. package/dist/{attachment-ZQW7DR4G.js → attachment-CPZYOBUB.js} +5 -5
  6. package/dist/{auth-HQGKUZBS.js → auth-DS6MBVEI.js} +3 -3
  7. package/dist/{benchmark-TIKJA32J.js → benchmark-6HPCIQHD.js} +5 -5
  8. package/dist/{blackcat-Z5LNV2GS.js → blackcat-V3L63WFE.js} +3 -3
  9. package/dist/{chunk-JQG3V5E3.js → chunk-2E3STBHS.js} +2 -2
  10. package/dist/{chunk-KL2ARGH6.js → chunk-2OHYMT3A.js} +3 -3
  11. package/dist/{chunk-KL2ARGH6.js.map → chunk-2OHYMT3A.js.map} +1 -1
  12. package/dist/{chunk-W6DL4IP6.js → chunk-3LGCHNBC.js} +2 -2
  13. package/dist/{chunk-BJHAVM7Z.js → chunk-4MFMPC5P.js} +46 -2
  14. package/dist/chunk-4MFMPC5P.js.map +1 -0
  15. package/dist/{chunk-MB4FS35J.js → chunk-56HFZHQM.js} +8 -8
  16. package/dist/{chunk-CZDQVQ57.js → chunk-5OYWNS6D.js} +20 -9
  17. package/dist/chunk-5OYWNS6D.js.map +1 -0
  18. package/dist/{chunk-YTXZUWMN.js → chunk-62EXS76C.js} +6 -6
  19. package/dist/{chunk-QVMGXMP5.js → chunk-6ILWJFKY.js} +368 -6
  20. package/dist/chunk-6ILWJFKY.js.map +1 -0
  21. package/dist/{chunk-6X6ZTDXP.js → chunk-BDOL622T.js} +4 -4
  22. package/dist/{chunk-L4JQ236V.js → chunk-C3CXVX2B.js} +4 -4
  23. package/dist/{chunk-33U56YGD.js → chunk-CWICLBLU.js} +2 -1
  24. package/dist/{chunk-33U56YGD.js.map → chunk-CWICLBLU.js.map} +1 -1
  25. package/dist/{chunk-XM3WTJ7Y.js → chunk-DEVD2WD2.js} +2 -2
  26. package/dist/{chunk-WWUEKQLR.js → chunk-EWK44F6V.js} +3 -3
  27. package/dist/chunk-EWK44F6V.js.map +1 -0
  28. package/dist/{chunk-CQSQNDMV.js → chunk-FNHPTTRF.js} +7 -5
  29. package/dist/chunk-FNHPTTRF.js.map +1 -0
  30. package/dist/{chunk-Z72GWGH2.js → chunk-GCD4EHCD.js} +6 -6
  31. package/dist/{chunk-3KBYI6RR.js → chunk-H6ZMKNP7.js} +3 -3
  32. package/dist/{chunk-5OKCT6LF.js → chunk-IEWAH7W3.js} +5 -5
  33. package/dist/{chunk-TGI2AAOZ.js → chunk-IV4HRGKX.js} +77 -12
  34. package/dist/chunk-IV4HRGKX.js.map +1 -0
  35. package/dist/{chunk-635IQV6B.js → chunk-J2ZQULU3.js} +2 -2
  36. package/dist/{chunk-ZZZ33KTO.js → chunk-K2IDR6FR.js} +3 -3
  37. package/dist/{chunk-YEH7UIUY.js → chunk-KTZRI27M.js} +4 -4
  38. package/dist/{chunk-RV7FZ4ID.js → chunk-L644NMDI.js} +3 -3
  39. package/dist/{chunk-RV7FZ4ID.js.map → chunk-L644NMDI.js.map} +1 -1
  40. package/dist/{chunk-3TZABC7D.js → chunk-LFT7GUWS.js} +63 -17
  41. package/dist/chunk-LFT7GUWS.js.map +1 -0
  42. package/dist/{chunk-QHRWXCON.js → chunk-O7B2IYCZ.js} +11 -11
  43. package/dist/chunk-O7B2IYCZ.js.map +1 -0
  44. package/dist/{chunk-U3MRXQZS.js → chunk-RHWPZKRV.js} +24 -1
  45. package/dist/chunk-RHWPZKRV.js.map +1 -0
  46. package/dist/{chunk-OU4I3MEH.js → chunk-UJSW72LG.js} +3 -3
  47. package/dist/{chunk-R2CRH6JQ.js → chunk-VACACCD6.js} +3 -3
  48. package/dist/{chunk-EGWVL5XA.js → chunk-VMAR67FF.js} +2 -2
  49. package/dist/{chunk-ZH7HWVEE.js → chunk-VMJ7TBVM.js} +2 -2
  50. package/dist/chunk-WRRA4ATV.js +332 -0
  51. package/dist/chunk-WRRA4ATV.js.map +1 -0
  52. package/dist/{chunk-XWJJYZQC.js → chunk-XHEVDABB.js} +2 -2
  53. package/dist/{chunk-SCQVBLNR.js → chunk-XHGF6VC6.js} +4 -4
  54. package/dist/{credential-2IEZDTH3.js → credential-GJW7LGYP.js} +5 -5
  55. package/dist/{credential-template-S5WSOI3P.js → credential-template-UWBYKKWD.js} +5 -5
  56. package/dist/dev-PBW7BSOD.js +605 -0
  57. package/dist/dev-PBW7BSOD.js.map +1 -0
  58. package/dist/{digest-HQGL7H6E.js → digest-XEHGCJM4.js} +8 -8
  59. package/dist/{docs-CJQZVERZ.js → docs-KH4BCEQF.js} +5 -5
  60. package/dist/{doctor-EGK5AB77.js → doctor-FOXXE5SK.js} +3 -3
  61. package/dist/{environment-MWVXYEDU.js → environment-EBDPQTIS.js} +7 -7
  62. package/dist/{eval-F4NORBQI.js → eval-QZSSEICV.js} +67 -27
  63. package/dist/eval-QZSSEICV.js.map +1 -0
  64. package/dist/{exploration-JC7R7KTJ.js → exploration-LCGKRGOQ.js} +5 -5
  65. package/dist/{exploratory-test-BHK5FTMJ.js → exploratory-test-SGECQGIX.js} +6 -6
  66. package/dist/{external-run-UL5CKG54.js → external-run-2AJAI5O7.js} +9 -9
  67. package/dist/{feature-flag-NMO25TZN.js → feature-flag-P7N4WQIH.js} +5 -5
  68. package/dist/{feedback-3JNPSUXS.js → feedback-LAT7TQ2T.js} +6 -6
  69. package/dist/{flow-AYE4RTZN.js → flow-NH2DC6AP.js} +5 -5
  70. package/dist/{github-DM2DCXRY.js → github-FCEKBLXI.js} +6 -6
  71. package/dist/index.js +106 -408
  72. package/dist/index.js.map +1 -1
  73. package/dist/{init-CPDT5XAU.js → init-5U5EZOAW.js} +5 -5
  74. package/dist/{install-JZRNCHMO.js → install-X3UNVXKY.js} +5 -5
  75. package/dist/{issues-GRFJB52C.js → issues-4P6I3YQW.js} +8 -8
  76. package/dist/{knobs-3Y4QXWAD.js → knobs-JKTWJ6ZG.js} +5 -5
  77. package/dist/{landing-ABYL5WVG.js → landing-6AGLKI66.js} +7 -7
  78. package/dist/{list-WG5QB4NK.js → list-TWP2RP33.js} +5 -5
  79. package/dist/{local-KX24OR5Z.js → local-D75QE5TB.js} +7 -7
  80. package/dist/{login-MLJQAIFK.js → login-RBET4TIP.js} +5 -5
  81. package/dist/{mcp-DDTO54XD.js → mcp-562SUUMK.js} +11 -11
  82. package/dist/{metrics-BJLLOSPQ.js → metrics-4WF2GEPA.js} +5 -5
  83. package/dist/{monitor-E2CKGHPM.js → monitor-WJXH54H2.js} +5 -5
  84. package/dist/{perf-IX7JOMUE.js → perf-C6N6P7YG.js} +12 -361
  85. package/dist/perf-C6N6P7YG.js.map +1 -0
  86. package/dist/{product-2ECWM2JN.js → product-CYEQ7HOB.js} +5 -5
  87. package/dist/{product-map-O542LZQL.js → product-map-MSDLIFZY.js} +7 -7
  88. package/dist/{prompts-Y7O4XJ5P.js → prompts-ME53WPN4.js} +5 -5
  89. package/dist/{proofs-TNOBLSRP.js → proofs-D3G6ZUFP.js} +11 -9
  90. package/dist/{property-TZCGOZQG.js → property-5GLJNAVB.js} +29 -10
  91. package/dist/property-5GLJNAVB.js.map +1 -0
  92. package/dist/{psql-M66YSUXW.js → psql-EXAUDU3W.js} +5 -5
  93. package/dist/{record-3JINBRAR.js → record-EE7NGNKY.js} +6 -6
  94. package/dist/{redis-FOIBINYA.js → redis-M4EYWKOJ.js} +5 -5
  95. package/dist/{release-B4VWMVEI.js → release-4KUWVWWC.js} +5 -5
  96. package/dist/{runbook-EYMSE37E.js → runbook-AHU77ANQ.js} +5 -5
  97. package/dist/{sandbox-R6EMGOKR.js → sandbox-FBLJEYA2.js} +1910 -1629
  98. package/dist/sandbox-FBLJEYA2.js.map +1 -0
  99. package/dist/{scenarios-W7YNLBVO.js → scenarios-TU656T2B.js} +5 -5
  100. package/dist/{schedule-EDHQJUUZ.js → schedule-DXJI6SYO.js} +8 -8
  101. package/dist/{session-4N62APNQ.js → session-RUFTJHF4.js} +10 -10
  102. package/dist/{skill-XFGMNOVM.js → skill-ZK76GQ47.js} +7 -7
  103. package/dist/{skill-XFGMNOVM.js.map → skill-ZK76GQ47.js.map} +1 -1
  104. package/dist/skills/cofactor-debug-run/SKILL.md +2 -1
  105. package/dist/skills/cofactor-debug-run/history/2.0.10.md +320 -0
  106. package/dist/skills/cofactor-debug-run/version.json +1 -1
  107. package/dist/{slack-RUAXG6TB.js → slack-M3OKDVMY.js} +6 -6
  108. package/dist/{spec-ZZKMW7ER.js → spec-5MXEBLYR.js} +5 -5
  109. package/dist/{src-OKSAEHQS.js → src-2X5OQRPJ.js} +4 -4
  110. package/dist/{src-DGIVA22K.js → src-72EFBA2R.js} +3 -3
  111. package/dist/{src-ECZWYNSV.js → src-KH6WSBJV.js} +5 -5
  112. package/dist/{src-CQ2YAW7F.js → src-VLV36QYH.js} +127 -20
  113. package/dist/src-VLV36QYH.js.map +1 -0
  114. package/dist/{start-OCZGILRL.js → start-PDYBRB5T.js} +5 -5
  115. package/dist/{status-YTJ6YITG.js → status-VMVPVCDA.js} +5 -5
  116. package/dist/{tag-KBG4SWRL.js → tag-7KTDLPVU.js} +6 -6
  117. package/dist/{task-O6XRMHYE.js → task-QLFYZNKR.js} +6 -6
  118. package/dist/{teams-NFUHS7XI.js → teams-Y7BXCY6P.js} +7 -7
  119. package/dist/{test-suite-6BH3DESN.js → test-suite-6U22AYFG.js} +15 -14
  120. package/dist/test-suite-6U22AYFG.js.map +1 -0
  121. package/dist/{tickets-JU2STK4B.js → tickets-CQDJDNPY.js} +5 -5
  122. package/dist/{trace-NMSGKASJ.js → trace-QDORWXYX.js} +8 -8
  123. package/dist/{triage-JGECJJU5.js → triage-GY7M3UGG.js} +6 -6
  124. package/dist/{trigger-OSPEVCCQ.js → trigger-DLFAORX4.js} +9 -9
  125. package/dist/{update-check-FNP6RFZO.js → update-check-ES6XQ7OF.js} +4 -4
  126. package/dist/{upgrade-3PPBXFAT.js → upgrade-WKEOW5GS.js} +5 -5
  127. package/dist/{usage-QMCU2JIO.js → usage-E6FDTR3W.js} +7 -7
  128. package/dist/{utilities-7VHWTSJT.js → utilities-4Z6YX3L5.js} +6 -6
  129. package/dist/{visual-WIZSPTVK.js → visual-TA6LPVDJ.js} +5 -5
  130. package/dist/{workflow-23HKTJLS.js → workflow-B5FXS2CJ.js} +58 -31
  131. package/dist/workflow-B5FXS2CJ.js.map +1 -0
  132. package/dist/{workflow-cache-ACGM3BOI.js → workflow-cache-MNLEMRMM.js} +16 -16
  133. package/dist/{workflow-cache-convergence-resolve-KEQDQMKL.js → workflow-cache-convergence-resolve-G5HJM6MF.js} +5 -5
  134. package/dist/{workflow-debug-TVLBNRCB.js → workflow-debug-TNBUGSBK.js} +11 -11
  135. package/dist/{workflow-feedback-GB5VPGE2.js → workflow-feedback-G2ZXAUGI.js} +5 -5
  136. package/dist/{workflow-fork-2IPJNR76.js → workflow-fork-6HHOMC2D.js} +5 -5
  137. package/dist/{workflow-promotion-QEQUXVUT.js → workflow-promotion-RG7L6JZT.js} +5 -5
  138. package/dist/{workflow-proposals-KHGTPMD4.js → workflow-proposals-ZNF2F3UE.js} +5 -5
  139. package/dist/{workflow-revisions-F5KQEEXB.js → workflow-revisions-TP64Z6FO.js} +5 -5
  140. package/dist/{workflow-status-66HC5MEA.js → workflow-status-5RG3JX56.js} +5 -5
  141. package/package.json +3 -2
  142. package/dist/chunk-3TZABC7D.js.map +0 -1
  143. package/dist/chunk-BJHAVM7Z.js.map +0 -1
  144. package/dist/chunk-CQSQNDMV.js.map +0 -1
  145. package/dist/chunk-CZDQVQ57.js.map +0 -1
  146. package/dist/chunk-QHRWXCON.js.map +0 -1
  147. package/dist/chunk-QVMGXMP5.js.map +0 -1
  148. package/dist/chunk-TGI2AAOZ.js.map +0 -1
  149. package/dist/chunk-U3MRXQZS.js.map +0 -1
  150. package/dist/chunk-WWUEKQLR.js.map +0 -1
  151. package/dist/eval-F4NORBQI.js.map +0 -1
  152. package/dist/perf-IX7JOMUE.js.map +0 -1
  153. package/dist/property-TZCGOZQG.js.map +0 -1
  154. package/dist/sandbox-R6EMGOKR.js.map +0 -1
  155. package/dist/src-CQ2YAW7F.js.map +0 -1
  156. package/dist/test-suite-6BH3DESN.js.map +0 -1
  157. package/dist/workflow-23HKTJLS.js.map +0 -1
  158. /package/dist/{account-RHHS7HT4.js.map → account-CZYZD2JS.js.map} +0 -0
  159. /package/dist/{alert-B5WEOKHP.js.map → alert-JQ4SDEB7.js.map} +0 -0
  160. /package/dist/{api-catalog-UXONX3BR.js.map → api-catalog-IWQC35P2.js.map} +0 -0
  161. /package/dist/{api-key-J3A4TJKN.js.map → api-key-GUDXIIEA.js.map} +0 -0
  162. /package/dist/{attachment-ZQW7DR4G.js.map → attachment-CPZYOBUB.js.map} +0 -0
  163. /package/dist/{auth-HQGKUZBS.js.map → auth-DS6MBVEI.js.map} +0 -0
  164. /package/dist/{benchmark-TIKJA32J.js.map → benchmark-6HPCIQHD.js.map} +0 -0
  165. /package/dist/{blackcat-Z5LNV2GS.js.map → blackcat-V3L63WFE.js.map} +0 -0
  166. /package/dist/{chunk-JQG3V5E3.js.map → chunk-2E3STBHS.js.map} +0 -0
  167. /package/dist/{chunk-W6DL4IP6.js.map → chunk-3LGCHNBC.js.map} +0 -0
  168. /package/dist/{chunk-MB4FS35J.js.map → chunk-56HFZHQM.js.map} +0 -0
  169. /package/dist/{chunk-YTXZUWMN.js.map → chunk-62EXS76C.js.map} +0 -0
  170. /package/dist/{chunk-6X6ZTDXP.js.map → chunk-BDOL622T.js.map} +0 -0
  171. /package/dist/{chunk-L4JQ236V.js.map → chunk-C3CXVX2B.js.map} +0 -0
  172. /package/dist/{chunk-XM3WTJ7Y.js.map → chunk-DEVD2WD2.js.map} +0 -0
  173. /package/dist/{chunk-Z72GWGH2.js.map → chunk-GCD4EHCD.js.map} +0 -0
  174. /package/dist/{chunk-3KBYI6RR.js.map → chunk-H6ZMKNP7.js.map} +0 -0
  175. /package/dist/{chunk-5OKCT6LF.js.map → chunk-IEWAH7W3.js.map} +0 -0
  176. /package/dist/{chunk-635IQV6B.js.map → chunk-J2ZQULU3.js.map} +0 -0
  177. /package/dist/{chunk-ZZZ33KTO.js.map → chunk-K2IDR6FR.js.map} +0 -0
  178. /package/dist/{chunk-YEH7UIUY.js.map → chunk-KTZRI27M.js.map} +0 -0
  179. /package/dist/{chunk-OU4I3MEH.js.map → chunk-UJSW72LG.js.map} +0 -0
  180. /package/dist/{chunk-R2CRH6JQ.js.map → chunk-VACACCD6.js.map} +0 -0
  181. /package/dist/{chunk-EGWVL5XA.js.map → chunk-VMAR67FF.js.map} +0 -0
  182. /package/dist/{chunk-ZH7HWVEE.js.map → chunk-VMJ7TBVM.js.map} +0 -0
  183. /package/dist/{chunk-XWJJYZQC.js.map → chunk-XHEVDABB.js.map} +0 -0
  184. /package/dist/{chunk-SCQVBLNR.js.map → chunk-XHGF6VC6.js.map} +0 -0
  185. /package/dist/{credential-2IEZDTH3.js.map → credential-GJW7LGYP.js.map} +0 -0
  186. /package/dist/{credential-template-S5WSOI3P.js.map → credential-template-UWBYKKWD.js.map} +0 -0
  187. /package/dist/{digest-HQGL7H6E.js.map → digest-XEHGCJM4.js.map} +0 -0
  188. /package/dist/{docs-CJQZVERZ.js.map → docs-KH4BCEQF.js.map} +0 -0
  189. /package/dist/{doctor-EGK5AB77.js.map → doctor-FOXXE5SK.js.map} +0 -0
  190. /package/dist/{environment-MWVXYEDU.js.map → environment-EBDPQTIS.js.map} +0 -0
  191. /package/dist/{exploration-JC7R7KTJ.js.map → exploration-LCGKRGOQ.js.map} +0 -0
  192. /package/dist/{exploratory-test-BHK5FTMJ.js.map → exploratory-test-SGECQGIX.js.map} +0 -0
  193. /package/dist/{external-run-UL5CKG54.js.map → external-run-2AJAI5O7.js.map} +0 -0
  194. /package/dist/{feature-flag-NMO25TZN.js.map → feature-flag-P7N4WQIH.js.map} +0 -0
  195. /package/dist/{feedback-3JNPSUXS.js.map → feedback-LAT7TQ2T.js.map} +0 -0
  196. /package/dist/{flow-AYE4RTZN.js.map → flow-NH2DC6AP.js.map} +0 -0
  197. /package/dist/{github-DM2DCXRY.js.map → github-FCEKBLXI.js.map} +0 -0
  198. /package/dist/{init-CPDT5XAU.js.map → init-5U5EZOAW.js.map} +0 -0
  199. /package/dist/{install-JZRNCHMO.js.map → install-X3UNVXKY.js.map} +0 -0
  200. /package/dist/{issues-GRFJB52C.js.map → issues-4P6I3YQW.js.map} +0 -0
  201. /package/dist/{knobs-3Y4QXWAD.js.map → knobs-JKTWJ6ZG.js.map} +0 -0
  202. /package/dist/{landing-ABYL5WVG.js.map → landing-6AGLKI66.js.map} +0 -0
  203. /package/dist/{list-WG5QB4NK.js.map → list-TWP2RP33.js.map} +0 -0
  204. /package/dist/{local-KX24OR5Z.js.map → local-D75QE5TB.js.map} +0 -0
  205. /package/dist/{login-MLJQAIFK.js.map → login-RBET4TIP.js.map} +0 -0
  206. /package/dist/{mcp-DDTO54XD.js.map → mcp-562SUUMK.js.map} +0 -0
  207. /package/dist/{metrics-BJLLOSPQ.js.map → metrics-4WF2GEPA.js.map} +0 -0
  208. /package/dist/{monitor-E2CKGHPM.js.map → monitor-WJXH54H2.js.map} +0 -0
  209. /package/dist/{product-2ECWM2JN.js.map → product-CYEQ7HOB.js.map} +0 -0
  210. /package/dist/{product-map-O542LZQL.js.map → product-map-MSDLIFZY.js.map} +0 -0
  211. /package/dist/{prompts-Y7O4XJ5P.js.map → prompts-ME53WPN4.js.map} +0 -0
  212. /package/dist/{proofs-TNOBLSRP.js.map → proofs-D3G6ZUFP.js.map} +0 -0
  213. /package/dist/{psql-M66YSUXW.js.map → psql-EXAUDU3W.js.map} +0 -0
  214. /package/dist/{record-3JINBRAR.js.map → record-EE7NGNKY.js.map} +0 -0
  215. /package/dist/{redis-FOIBINYA.js.map → redis-M4EYWKOJ.js.map} +0 -0
  216. /package/dist/{release-B4VWMVEI.js.map → release-4KUWVWWC.js.map} +0 -0
  217. /package/dist/{runbook-EYMSE37E.js.map → runbook-AHU77ANQ.js.map} +0 -0
  218. /package/dist/{scenarios-W7YNLBVO.js.map → scenarios-TU656T2B.js.map} +0 -0
  219. /package/dist/{schedule-EDHQJUUZ.js.map → schedule-DXJI6SYO.js.map} +0 -0
  220. /package/dist/{session-4N62APNQ.js.map → session-RUFTJHF4.js.map} +0 -0
  221. /package/dist/{slack-RUAXG6TB.js.map → slack-M3OKDVMY.js.map} +0 -0
  222. /package/dist/{spec-ZZKMW7ER.js.map → spec-5MXEBLYR.js.map} +0 -0
  223. /package/dist/{src-DGIVA22K.js.map → src-2X5OQRPJ.js.map} +0 -0
  224. /package/dist/{src-ECZWYNSV.js.map → src-72EFBA2R.js.map} +0 -0
  225. /package/dist/{src-OKSAEHQS.js.map → src-KH6WSBJV.js.map} +0 -0
  226. /package/dist/{start-OCZGILRL.js.map → start-PDYBRB5T.js.map} +0 -0
  227. /package/dist/{status-YTJ6YITG.js.map → status-VMVPVCDA.js.map} +0 -0
  228. /package/dist/{tag-KBG4SWRL.js.map → tag-7KTDLPVU.js.map} +0 -0
  229. /package/dist/{task-O6XRMHYE.js.map → task-QLFYZNKR.js.map} +0 -0
  230. /package/dist/{teams-NFUHS7XI.js.map → teams-Y7BXCY6P.js.map} +0 -0
  231. /package/dist/{tickets-JU2STK4B.js.map → tickets-CQDJDNPY.js.map} +0 -0
  232. /package/dist/{trace-NMSGKASJ.js.map → trace-QDORWXYX.js.map} +0 -0
  233. /package/dist/{triage-JGECJJU5.js.map → triage-GY7M3UGG.js.map} +0 -0
  234. /package/dist/{trigger-OSPEVCCQ.js.map → trigger-DLFAORX4.js.map} +0 -0
  235. /package/dist/{update-check-FNP6RFZO.js.map → update-check-ES6XQ7OF.js.map} +0 -0
  236. /package/dist/{upgrade-3PPBXFAT.js.map → upgrade-WKEOW5GS.js.map} +0 -0
  237. /package/dist/{usage-QMCU2JIO.js.map → usage-E6FDTR3W.js.map} +0 -0
  238. /package/dist/{utilities-7VHWTSJT.js.map → utilities-4Z6YX3L5.js.map} +0 -0
  239. /package/dist/{visual-WIZSPTVK.js.map → visual-TA6LPVDJ.js.map} +0 -0
  240. /package/dist/{workflow-cache-ACGM3BOI.js.map → workflow-cache-MNLEMRMM.js.map} +0 -0
  241. /package/dist/{workflow-cache-convergence-resolve-KEQDQMKL.js.map → workflow-cache-convergence-resolve-G5HJM6MF.js.map} +0 -0
  242. /package/dist/{workflow-debug-TVLBNRCB.js.map → workflow-debug-TNBUGSBK.js.map} +0 -0
  243. /package/dist/{workflow-feedback-GB5VPGE2.js.map → workflow-feedback-G2ZXAUGI.js.map} +0 -0
  244. /package/dist/{workflow-fork-2IPJNR76.js.map → workflow-fork-6HHOMC2D.js.map} +0 -0
  245. /package/dist/{workflow-promotion-QEQUXVUT.js.map → workflow-promotion-RG7L6JZT.js.map} +0 -0
  246. /package/dist/{workflow-proposals-KHGTPMD4.js.map → workflow-proposals-ZNF2F3UE.js.map} +0 -0
  247. /package/dist/{workflow-revisions-F5KQEEXB.js.map → workflow-revisions-TP64Z6FO.js.map} +0 -0
  248. /package/dist/{workflow-status-66HC5MEA.js.map → workflow-status-5RG3JX56.js.map} +0 -0
@@ -1,23 +1,23 @@
1
1
  import { createRequire as __cr } from "module"; import { fileURLToPath as __fup } from "url"; import { dirname as __dn } from "path"; const require = __cr(import.meta.url); const __filename = __fup(import.meta.url); const __dirname = __dn(__filename);
2
2
  import {
3
3
  parseDurationSeconds
4
- } from "./chunk-TGI2AAOZ.js";
4
+ } from "./chunk-IV4HRGKX.js";
5
5
  import {
6
6
  resolveApplicationId,
7
7
  resolveOrCreateSandboxApplication
8
- } from "./chunk-QVMGXMP5.js";
9
- import "./chunk-ZH7HWVEE.js";
8
+ } from "./chunk-6ILWJFKY.js";
9
+ import "./chunk-VMJ7TBVM.js";
10
10
  import {
11
11
  SANDBOX_V2_GUIDE_SECTIONS,
12
12
  printServedGuide
13
- } from "./chunk-QHRWXCON.js";
13
+ } from "./chunk-O7B2IYCZ.js";
14
14
  import {
15
15
  findOperation,
16
16
  getAllPlugins,
17
17
  getPlugin,
18
18
  getPluginIds,
19
19
  pluginOperations
20
- } from "./chunk-33U56YGD.js";
20
+ } from "./chunk-CWICLBLU.js";
21
21
  import {
22
22
  UUID_RE,
23
23
  apiRequest,
@@ -27,9 +27,10 @@ import {
27
27
  getTargetOrg,
28
28
  selectProperty,
29
29
  stripOneTrailingNewline
30
- } from "./chunk-WWUEKQLR.js";
30
+ } from "./chunk-EWK44F6V.js";
31
31
  import "./chunk-S3Q7MDCS.js";
32
32
  import {
33
+ activeConfigDir,
33
34
  apiFetch,
34
35
  decodeJwtPayload,
35
36
  exitOnFailure,
@@ -39,33 +40,33 @@ import {
39
40
  resolveConfig,
40
41
  resolveOrgAppUrl,
41
42
  splitAtSeparator
42
- } from "./chunk-BJHAVM7Z.js";
43
+ } from "./chunk-4MFMPC5P.js";
43
44
  import {
44
45
  SANDBOX_GUIDE_SECTIONS,
45
46
  writeRawOutput
46
- } from "./chunk-KL2ARGH6.js";
47
+ } from "./chunk-2OHYMT3A.js";
47
48
  import {
48
49
  INSTANCE_SIZES,
49
50
  INSTANCE_SIZE_SPEC,
50
51
  SANDBOX_EGRESS_MODES,
51
52
  parseInstanceSize,
52
53
  sandboxPreviewPagePath
53
- } from "./chunk-U3MRXQZS.js";
54
+ } from "./chunk-RHWPZKRV.js";
54
55
  import "./chunk-TOWZAG3I.js";
55
56
 
56
57
  // src/sandbox.ts
57
- import process10 from "process";
58
- import { readFileSync as readFileSync6 } from "fs";
58
+ import process11 from "process";
59
+ import { readFileSync as readFileSync7 } from "fs";
59
60
 
60
61
  // src/sandbox-emulation.ts
61
- import process from "process";
62
+ import process2 from "process";
62
63
  import { readFileSync } from "fs";
63
64
  var pad = (value, width) => value.padEnd(width);
64
65
  async function fetchEmulation(apiUrl, token, templateId) {
65
66
  const res = await apiRequest(apiUrl, token, "GET", `/org/environments/templates/${templateId}/emulation`);
66
67
  if (!res.ok) {
67
68
  console.error(`Error: ${res.error}`);
68
- process.exit(1);
69
+ process2.exit(1);
69
70
  }
70
71
  return { config: res.config ?? { enabled: false, plugins: [] }, catalog: res.catalog ?? [] };
71
72
  }
@@ -80,14 +81,14 @@ async function patchEmulation(apiUrl, token, templateId, config) {
80
81
  );
81
82
  if (!res.ok) {
82
83
  console.error(`Error: ${res.error}`);
83
- process.exit(1);
84
+ process2.exit(1);
84
85
  }
85
86
  }
86
87
  function assertKnownPlugin(pluginId) {
87
88
  const ids = getPluginIds();
88
89
  if (!ids.includes(pluginId)) {
89
90
  console.error(`Error: unknown plugin '${pluginId}'. Available: ${ids.join(", ")}`);
90
- process.exit(1);
91
+ process2.exit(1);
91
92
  }
92
93
  }
93
94
  function readJsonFileOrExit(path, flag) {
@@ -98,7 +99,7 @@ function readJsonFileOrExit(path, flag) {
98
99
  console.error(
99
100
  `Error reading ${flag} file '${path}': ${err instanceof Error ? err.message : String(err)}`
100
101
  );
101
- process.exit(1);
102
+ process2.exit(1);
102
103
  }
103
104
  try {
104
105
  return JSON.parse(raw);
@@ -106,7 +107,7 @@ function readJsonFileOrExit(path, flag) {
106
107
  console.error(
107
108
  `Error parsing ${flag} file '${path}' as JSON: ${err instanceof Error ? err.message : String(err)}`
108
109
  );
109
- process.exit(1);
110
+ process2.exit(1);
110
111
  }
111
112
  }
112
113
  function readJsonObjectOrExit(path, flag) {
@@ -115,7 +116,7 @@ function readJsonObjectOrExit(path, flag) {
115
116
  console.error(
116
117
  `Error: ${flag} file '${path}' must contain a JSON object, not ${Array.isArray(parsed) ? "an array" : typeof parsed}.`
117
118
  );
118
- process.exit(1);
119
+ process2.exit(1);
119
120
  }
120
121
  return parsed;
121
122
  }
@@ -161,7 +162,7 @@ async function handleEmulationGet(argv, apiUrl, token) {
161
162
  const json = hasFlag(argv, "--json");
162
163
  if (!templateId) {
163
164
  console.error("Error: template ID required. Usage: canary sandbox emulation get <template-id>");
164
- process.exit(1);
165
+ process2.exit(1);
165
166
  }
166
167
  const { config, catalog } = await fetchEmulation(apiUrl, token, templateId);
167
168
  const available = getAllPlugins().map((plugin) => ({ id: plugin.id, name: plugin.name }));
@@ -210,7 +211,7 @@ async function setEmulationEnabled(argv, apiUrl, token, enable) {
210
211
  console.error(
211
212
  `Error: template ID required. Usage: canary sandbox emulation ${verb} <template-id> [pluginId]`
212
213
  );
213
- process.exit(1);
214
+ process2.exit(1);
214
215
  }
215
216
  const { config } = await fetchEmulation(apiUrl, token, templateId);
216
217
  let next;
@@ -221,7 +222,7 @@ async function setEmulationEnabled(argv, apiUrl, token, enable) {
221
222
  const exists = (config.plugins ?? []).some((plugin) => plugin.pluginId === pluginId);
222
223
  if (!enable && !exists) {
223
224
  console.error(`Error: plugin '${pluginId}' is not configured on this template.`);
224
- process.exit(1);
225
+ process2.exit(1);
225
226
  }
226
227
  next = withPluginEnabled(config, pluginId, enable);
227
228
  }
@@ -237,7 +238,7 @@ async function handleEmulationSet(argv, apiUrl, token) {
237
238
  console.error(
238
239
  "Usage: canary sandbox emulation set <template-id> <pluginId> [--config <file>] [--fixture <file>] [--env-mapping <file>]"
239
240
  );
240
- process.exit(1);
241
+ process2.exit(1);
241
242
  }
242
243
  assertKnownPlugin(pluginId);
243
244
  const configPath = getArgValue(argv, "--config");
@@ -245,7 +246,7 @@ async function handleEmulationSet(argv, apiUrl, token) {
245
246
  const envMappingPath = getArgValue(argv, "--env-mapping");
246
247
  if (!configPath && !fixturePath && !envMappingPath) {
247
248
  console.error("Error: provide at least one of --config, --fixture, --env-mapping");
248
- process.exit(1);
249
+ process2.exit(1);
249
250
  }
250
251
  const pluginConfig = configPath ? readJsonObjectOrExit(configPath, "--config") : void 0;
251
252
  const fixture = fixturePath ? readJsonFileOrExit(fixturePath, "--fixture") : void 0;
@@ -270,12 +271,12 @@ async function handleEmulationRemove(argv, apiUrl, token) {
270
271
  console.error(
271
272
  "Error: template ID and pluginId required. Usage: canary sandbox emulation remove <template-id> <pluginId>"
272
273
  );
273
- process.exit(1);
274
+ process2.exit(1);
274
275
  }
275
276
  const { config } = await fetchEmulation(apiUrl, token, templateId);
276
277
  if (!(config.plugins ?? []).some((plugin) => plugin.pluginId === pluginId)) {
277
278
  console.error(`Error: plugin '${pluginId}' is not configured on this template.`);
278
- process.exit(1);
279
+ process2.exit(1);
279
280
  }
280
281
  const next = withoutPlugin(config, pluginId);
281
282
  await patchEmulation(apiUrl, token, templateId, next);
@@ -304,12 +305,12 @@ async function handleEmulation(argv, apiUrl, token) {
304
305
  console.error(
305
306
  `Unknown emulation action: ${action2 ?? "(none)"}. Use get/enable/disable/set/remove.`
306
307
  );
307
- process.exit(1);
308
+ process2.exit(1);
308
309
  }
309
310
  }
310
311
 
311
312
  // src/sandbox-logs.ts
312
- import process2 from "process";
313
+ import process3 from "process";
313
314
  var requestFailureLogsUrl = (apiUrl, token, instanceId) => apiRequest(
314
315
  apiUrl,
315
316
  token,
@@ -344,7 +345,7 @@ async function handleLogs(argv, apiUrl, token) {
344
345
  const json = hasFlag(argv, "--json");
345
346
  if (!instanceId) {
346
347
  console.error("Error: instance ID required. Usage: canary sandbox logs <instance-id>");
347
- process2.exit(1);
348
+ process3.exit(1);
348
349
  }
349
350
  const service2 = getArgValue(argv, "--service");
350
351
  const tail = getArgValue(argv, "--tail") ?? "200";
@@ -354,13 +355,13 @@ async function handleLogs(argv, apiUrl, token) {
354
355
  const bundle = await fetchFailureLogsBundle(apiUrl, token, instanceId);
355
356
  if ("error" in bundle) {
356
357
  console.error(`Error: ${bundle.error}`);
357
- process2.exit(1);
358
+ process3.exit(1);
358
359
  }
359
360
  if (json) {
360
361
  writeRawOutput(JSON.stringify(bundle, null, 2));
361
362
  return;
362
363
  }
363
- process2.stdout.write(bundle.text || "(empty bundle)");
364
+ process3.stdout.write(bundle.text || "(empty bundle)");
364
365
  return;
365
366
  }
366
367
  const res = await apiRequest(
@@ -382,33 +383,33 @@ async function handleLogs(argv, apiUrl, token) {
382
383
  }
383
384
  console.error(`(live logs unavailable: ${res.error}; showing captured failure snapshot)
384
385
  `);
385
- process2.stdout.write(bundle.text || "(empty bundle)");
386
+ process3.stdout.write(bundle.text || "(empty bundle)");
386
387
  return;
387
388
  }
388
389
  console.error(`Error: ${res.error}`);
389
- process2.exit(1);
390
+ process3.exit(1);
390
391
  }
391
392
  if (json) {
392
393
  writeRawOutput(JSON.stringify(res.data, null, 2));
393
394
  return;
394
395
  }
395
- process2.stdout.write(res.data.logs ?? "(no logs)");
396
+ process3.stdout.write(res.data.logs ?? "(no logs)");
396
397
  }
397
398
 
398
399
  // src/sandbox-preview.ts
399
- import process3 from "process";
400
+ import process4 from "process";
400
401
  async function handlePreview(argv, apiUrl, token, appSubdomain) {
401
402
  const pr = getArgValue(argv, "--pr");
402
403
  if (!pr) {
403
404
  console.error("Usage: cofactor sandbox preview --application <name|id> --pr <number> [--json]");
404
- process3.exit(1);
405
+ process4.exit(1);
405
406
  }
406
407
  const application = await resolveApplicationId(argv, apiUrl, token);
407
408
  const query = new URLSearchParams({ application, pr });
408
409
  const res = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/previews?${query}`);
409
410
  if (!res.ok) {
410
411
  console.error(`Error: ${res.error}`);
411
- process3.exit(1);
412
+ process4.exit(1);
412
413
  }
413
414
  const preview = res.data;
414
415
  if (hasFlag(argv, "--json")) {
@@ -428,8 +429,8 @@ async function handlePreview(argv, apiUrl, token, appSubdomain) {
428
429
  }
429
430
 
430
431
  // src/sandbox-v2.ts
431
- import process4 from "process";
432
- import { readFileSync as readFileSync2 } from "fs";
432
+ import process6 from "process";
433
+ import { readFileSync as readFileSync4 } from "fs";
433
434
 
434
435
  // ../sandbox-helper/src/manifest.ts
435
436
  import { createHash as createHash2, randomBytes } from "crypto";
@@ -1270,1616 +1271,1889 @@ var prepareInputSchema = z2.strictObject({
1270
1271
  */
1271
1272
  sync: z2.strictObject({ key: syncKeySchema })
1272
1273
  });
1274
+ function definitionFile(dir) {
1275
+ if (dir === ".") return DEFINITION_FILE;
1276
+ if (dir === "" || dir.startsWith("/") || dir.endsWith("/") || dir.split("/").includes("..")) {
1277
+ throw new Error(`invalid definition directory "${dir}"`);
1278
+ }
1279
+ return `${dir}/${DEFINITION_FILE}`;
1280
+ }
1273
1281
 
1274
- // src/sandbox-v2.ts
1275
- function pad2(s, n) {
1276
- return s.padEnd(n);
1282
+ // src/sandbox-from-local.ts
1283
+ import { spawnSync as spawnSync2 } from "child_process";
1284
+ import { createHash as createHash3, randomUUID } from "crypto";
1285
+ import {
1286
+ existsSync as existsSync2,
1287
+ mkdirSync,
1288
+ readFileSync as readFileSync3,
1289
+ readdirSync,
1290
+ renameSync,
1291
+ rmSync as rmSync2,
1292
+ writeFileSync
1293
+ } from "fs";
1294
+ import { relative as relative2, resolve as resolve2, join as join2 } from "path";
1295
+ import { z as z4 } from "zod";
1296
+
1297
+ // src/sandbox-sync.ts
1298
+ import { spawnSync } from "child_process";
1299
+ import process5 from "process";
1300
+ import { copyFileSync, existsSync, lstatSync, mkdtempSync, readFileSync as readFileSync2, realpathSync, rmSync } from "fs";
1301
+ import { tmpdir } from "os";
1302
+ import { join, relative, resolve, sep } from "path";
1303
+
1304
+ // ../sandbox-helper/src/image-archive.ts
1305
+ import { z as z3 } from "zod";
1306
+ var MAX_BLOB_BYTES = 5 * 1024 ** 3;
1307
+ var ImageBlob = z3.object({
1308
+ digest: z3.string().regex(/^sha256:[0-9a-f]{64}$/),
1309
+ size: z3.number().int().nonnegative()
1310
+ });
1311
+ var ImageBlobs = z3.object({
1312
+ config: ImageBlob,
1313
+ // Docker allows at most 127 layers; each costs a storage lookup per push.
1314
+ layers: z3.array(ImageBlob).min(1).max(128)
1315
+ });
1316
+ function withSandboxLayers(team, added, created) {
1317
+ return {
1318
+ ...team,
1319
+ rootfs: { ...team.rootfs, diff_ids: [...team.rootfs.diff_ids, ...added.map((layer) => layer.digest)] },
1320
+ history: [...team.history ?? [], ...added.map(() => ({ created, created_by: "cofactor sandbox push" }))],
1321
+ config: { ...team.config, Entrypoint: ["/usr/local/bin/cofactor-sandbox", "boot"], Cmd: [] }
1322
+ };
1277
1323
  }
1278
- function sleep(ms) {
1279
- return new Promise((resolve3) => setTimeout(resolve3, ms));
1324
+ var BLOCK = 512;
1325
+ var encoder = new TextEncoder();
1326
+ var decoder = new TextDecoder();
1327
+ function tarHeader(name, size, mode = 420) {
1328
+ if (encoder.encode(name).length > 100) throw new Error(`tar entry name too long: ${name}`);
1329
+ const header = new Uint8Array(BLOCK);
1330
+ const put = (offset, length, value) => header.set(encoder.encode(value).slice(0, length), offset);
1331
+ const octal = (offset, length, value) => put(offset, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
1332
+ put(0, 100, name);
1333
+ octal(100, 8, mode);
1334
+ octal(108, 8, 0);
1335
+ octal(116, 8, 0);
1336
+ octal(124, 12, size);
1337
+ octal(136, 12, 0);
1338
+ put(148, 8, " ");
1339
+ put(156, 1, "0");
1340
+ put(257, 6, "ustar\0");
1341
+ put(263, 2, "00");
1342
+ const checksum = header.reduce((sum, byte) => sum + byte, 0);
1343
+ put(148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
1344
+ return header;
1280
1345
  }
1281
- function indentLines(text, indent) {
1282
- return text.split("\n").map((line) => `${indent}${line}`).join("\n");
1346
+ function tarPadding(size) {
1347
+ return new Uint8Array((BLOCK - size % BLOCK) % BLOCK);
1283
1348
  }
1284
- function formatActionSignature(a) {
1285
- const inputs = Object.keys(a.input).join(", ");
1286
- const outputs = Object.keys(a.output).join(", ");
1287
- return `${a.slug}(${inputs})${outputs ? ` \u2192 ${outputs}` : ""}`;
1349
+ var TAR_END = new Uint8Array(BLOCK * 2);
1350
+ function tarOf(files) {
1351
+ const parts = files.flatMap((file) => [tarHeader(file.name, file.data.length, file.mode), file.data, tarPadding(file.data.length)]);
1352
+ parts.push(TAR_END);
1353
+ const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
1354
+ let at = 0;
1355
+ for (const part of parts) {
1356
+ out.set(part, at);
1357
+ at += part.length;
1358
+ }
1359
+ return out;
1288
1360
  }
1289
- function jsonSchemaPropTypes(schema) {
1290
- const properties = schema.properties ?? {};
1291
- return Object.fromEntries(
1292
- Object.entries(properties).map(([key, prop]) => [key, typeof prop.type === "string" ? prop.type : "string"])
1293
- );
1361
+ var field = (block, offset, length) => decoder.decode(block.subarray(offset, offset + length)).replace(/\0.*$/s, "");
1362
+ async function tarEntries(read) {
1363
+ const entries = [];
1364
+ let longName = null;
1365
+ for (let offset = 0; ; ) {
1366
+ const block = await read(offset, BLOCK);
1367
+ if (block.length < BLOCK) throw new Error(`the tar ends at byte ${offset + block.length}, before its end-of-archive block; it is truncated`);
1368
+ if (block.every((byte) => byte === 0)) return entries;
1369
+ const size = Number.parseInt(field(block, 124, 12).trim() || "0", 8);
1370
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error(`tar entry at ${offset} has an invalid size`);
1371
+ const type = field(block, 156, 1) || "0";
1372
+ const prefix = field(block, 345, 155);
1373
+ const name = longName ?? (prefix ? `${prefix}/${field(block, 0, 100)}` : field(block, 0, 100));
1374
+ const data = offset + BLOCK;
1375
+ longName = null;
1376
+ if (type === "x" || type === "L") {
1377
+ const text = decoder.decode(await read(data, size)).replace(/\0+$/, "");
1378
+ longName = type === "L" ? text : /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text)?.[1] ?? null;
1379
+ } else if (type === "0") {
1380
+ entries.push({ name, offset: data, size });
1381
+ }
1382
+ offset = data + size + tarPadding(size).length;
1383
+ }
1294
1384
  }
1295
- function formatOperationSignature(op) {
1296
- const inputs = Object.keys(jsonSchemaPropTypes(op.inputSchema)).join(", ");
1297
- const outputs = Object.keys(jsonSchemaPropTypes(op.outputSchema)).join(", ");
1298
- return `${op.name}(${inputs})${outputs ? ` \u2192 ${outputs}` : ""}`;
1385
+
1386
+ // src/sandbox-sync.ts
1387
+ var SyncError = class extends Error {
1388
+ };
1389
+ function git(args, options) {
1390
+ const result = spawnSync("git", args, {
1391
+ cwd: options.cwd,
1392
+ env: options.env ?? process5.env,
1393
+ input: options.input,
1394
+ encoding: "utf8",
1395
+ maxBuffer: 1 << 30
1396
+ // Node defaults to 1 MiB; this repo's `ls-files -s -z` alone is 1.2 MB
1397
+ });
1398
+ if (result.error) throw new SyncError(`git ${args[0]} failed: ${result.error.message}`);
1399
+ if (!(options.okCodes ?? [0]).includes(result.status ?? -1)) {
1400
+ throw new SyncError(`git ${args[0]} failed (${result.signal ?? result.status}): ${result.stderr.trim()}`);
1401
+ }
1402
+ return result.stdout;
1299
1403
  }
1300
- function exitOnRefusal(res) {
1301
- if (res.ok) return;
1302
- console.error(`Error: ${res.error}`);
1303
- if (res.code === "SANDBOX_V2_DEFINITION_INVALID") printIssues(res.issues, " ");
1304
- process4.exit(1);
1404
+ var toPosix = (path) => path.split(sep).join("/");
1405
+ function repositoryOf(dir) {
1406
+ const top = git(["rev-parse", "--show-toplevel"], { cwd: dir }).trim();
1407
+ const gitDir = git(["rev-parse", "--absolute-git-dir"], { cwd: dir }).trim();
1408
+ return { top: realpathSync(top), gitDir };
1305
1409
  }
1306
- function printIssues(issues, indent) {
1307
- for (const issue of issues ?? []) {
1308
- console.log(`${indent}${issue.path || "(root)"}: ${issue.message}`);
1410
+ function indexEntries(output2, strip, top) {
1411
+ const files = [];
1412
+ const skipped = [];
1413
+ for (const entry of output2.split("\0")) {
1414
+ if (entry === "") continue;
1415
+ const match = /^(\d{6}) ([0-9a-f]+) \d\t(.*)$/s.exec(entry);
1416
+ if (!match) throw new SyncError(`unexpected git ls-files line: ${entry}`);
1417
+ const [, mode, blob, repoPath] = match;
1418
+ const path = strip === "" ? repoPath : repoPath.slice(strip.length + 1);
1419
+ const onDisk = mode === "100644" || mode === "100755" ? lstatOrNull(join(top, repoPath)) : null;
1420
+ if (onDisk?.isFile()) files.push({ path, blob, mode: mode === "100755" ? 493 : 420, size: onDisk.size });
1421
+ else skipped.push(path);
1309
1422
  }
1423
+ return { files, skipped };
1310
1424
  }
1311
- function shortVersion(version) {
1312
- return /^[0-9a-f]{40}$/.test(version) ? version.slice(0, 12) : version;
1425
+ function workingTree(root) {
1426
+ const { top, gitDir } = repositoryOf(root);
1427
+ const prefix = toPosix(relative(top, realpathSync(root)));
1428
+ const temp = mkdtempSync(join(tmpdir(), "cofactor-sync-"));
1429
+ const cleanup = () => rmSync(temp, { recursive: true, force: true });
1430
+ try {
1431
+ const index = join(temp, "index");
1432
+ const env = { ...process5.env, GIT_INDEX_FILE: index };
1433
+ const userIndex = join(gitDir, "index");
1434
+ if (existsSync(userIndex)) copyFileSync(userIndex, index);
1435
+ else if (git(["rev-parse", "--verify", "-q", "HEAD"], { cwd: top, okCodes: [0, 1] }).trim() !== "") git(["read-tree", "HEAD"], { cwd: top, env });
1436
+ const pathspec = prefix === "" ? "." : `:(literal)${prefix}`;
1437
+ git(["add", "-A", "--", pathspec], { cwd: top, env });
1438
+ const entries = indexEntries(git(["ls-files", "-s", "-z", "--", pathspec], { cwd: top, env }), prefix, top);
1439
+ const files = withRawBlobs(entries.files, top, prefix);
1440
+ const skipped = entries.skipped;
1441
+ const tree = git(prefix === "" ? ["write-tree"] : ["write-tree", `--prefix=${prefix}/`], { cwd: top, env }).trim();
1442
+ const untracked = new Set(
1443
+ git(["ls-files", "--others", "--exclude-standard", "-z", "--", pathspec], { cwd: top }).split("\0").filter(Boolean).map((path) => prefix === "" ? path : path.slice(prefix.length + 1))
1444
+ );
1445
+ return { files, skipped, untracked, tree, repo: top, prefix, env, cleanup };
1446
+ } catch (error) {
1447
+ cleanup();
1448
+ throw error;
1449
+ }
1313
1450
  }
1314
- function printApplicationHuman(app) {
1315
- console.log(
1316
- app.delivery === "github" ? `Delivered from GitHub: ${app.repo}${app.definitionDir && app.definitionDir !== "." ? `:${app.definitionDir}` : ""}` : "Delivered as pushed images"
1317
- );
1318
- console.log(` version ${app.version ? shortVersion(app.version) : "(none yet: run `cofactor sandbox push`)"}`);
1319
- if (app.pushes && app.pushes.length > 0) {
1320
- console.log(" pushes:");
1321
- for (const push2 of app.pushes) {
1322
- const tags = push2.tags.length > 0 ? ` [${push2.tags.join(", ")}]` : "";
1323
- console.log(` ${push2.id} ${pad2(push2.status, 9)} ${push2.createdAt}${tags}${push2.error ? `
1324
- ${push2.error}` : ""}`);
1325
- }
1451
+ function buildOutput(dir, to) {
1452
+ const { gitDir } = repositoryOf(dir);
1453
+ const temp = mkdtempSync(join(tmpdir(), "cofactor-sync-"));
1454
+ try {
1455
+ const env = { ...process5.env, GIT_INDEX_FILE: join(temp, "index"), GIT_DIR: gitDir, GIT_WORK_TREE: realpathSync(dir) };
1456
+ git(["add", "-A", "-f", "--", "."], { cwd: dir, env });
1457
+ const entries = indexEntries(git(["ls-files", "-s", "-z"], { cwd: dir, env }), "", realpathSync(dir));
1458
+ const files = withRawBlobs(entries.files, realpathSync(dir), "", env);
1459
+ const skipped = entries.skipped;
1460
+ const tree = git(["write-tree"], { cwd: dir, env }).trim();
1461
+ return { files: files.map((file) => ({ ...file, path: `${to}/${file.path}` })), skipped: skipped.map((path) => `${to}/${path}`), tree };
1462
+ } finally {
1463
+ rmSync(temp, { recursive: true, force: true });
1326
1464
  }
1327
- if (app.sandboxEnvironments.length > 0) {
1328
- console.log(" sandbox environments:");
1329
- for (const env of app.sandboxEnvironments) console.log(` ${pad2(env.environment, 16)} ${env.name}`);
1465
+ }
1466
+ var FILTER_ATTRIBUTES = ["filter", "text", "eol", "ident", "working-tree-encoding"];
1467
+ function withRawBlobs(files, top, prefix, env) {
1468
+ if (files.length === 0) return files;
1469
+ const options = { cwd: top, ...env ? { env } : {} };
1470
+ const repoPaths = files.map((file) => prefix === "" ? file.path : `${prefix}/${file.path}`);
1471
+ const autocrlf = git(["config", "--get", "core.autocrlf"], { ...options, okCodes: [0, 1] }).trim();
1472
+ const converts = autocrlf === "input" || autocrlf !== "" && git(["config", "--get", "--type=bool", "core.autocrlf"], options).trim() === "true";
1473
+ let filtered;
1474
+ if (converts) {
1475
+ filtered = new Set(repoPaths);
1476
+ } else {
1477
+ const out = git(["check-attr", "-z", "--stdin", ...FILTER_ATTRIBUTES], { ...options, input: repoPaths.map((path) => `${path}\0`).join("") }).split("\0");
1478
+ filtered = /* @__PURE__ */ new Set();
1479
+ for (let i = 0; i + 2 < out.length; i += 3) if (out[i + 2] !== "unspecified") filtered.add(out[i]);
1330
1480
  }
1331
- if (app.heads && app.heads.length > 0) {
1332
- console.log(" heads (what flow runs and unpinned claims use; a warm environment switches after its warm-up; update with `cofactor sandbox refresh`):");
1333
- for (const head of app.heads) {
1334
- const version = head.version ? shortVersion(head.version) : "(none)";
1335
- const built = head.builtAt ? `, built ${head.builtAt}` : "";
1336
- const behind = head.behindMain && head.mainHead ? ` main has moved: now ${shortVersion(head.mainHead)}` : "";
1337
- console.log(` ${pad2(head.environment, 16)} ${version} (${head.status}${built})${behind}`);
1338
- if (head.error) console.log(` ${head.error}`);
1339
- }
1481
+ return files.map(
1482
+ (file, i) => filtered.has(repoPaths[i]) ? { ...file, blob: gitBlobId(readFileSync2(join(top, repoPaths[i]))) } : file
1483
+ );
1484
+ }
1485
+ function lstatOrNull(path) {
1486
+ try {
1487
+ return lstatSync(path);
1488
+ } catch (error) {
1489
+ if (error.code === "ENOENT") return null;
1490
+ throw error;
1340
1491
  }
1341
- const def = app.definition;
1342
- if (!def) return;
1343
- console.log(" Environments:");
1344
- for (const env of def.environments) {
1345
- console.log(` ${env.name}${env.extends ? ` (extends ${env.extends})` : ""}`);
1346
- console.log(indentLines(env.description, " "));
1347
- if (env.seeds.length > 0) {
1348
- console.log(" seeds:");
1349
- for (const step of env.seeds) console.log(` ${step.id}: ${step.action}`);
1492
+ }
1493
+ function chunked(items, fn) {
1494
+ for (let i = 0; i < items.length; i += 200) fn(items.slice(i, i + 200));
1495
+ }
1496
+ function ignoredPaths(repo, env, paths) {
1497
+ if (paths.length === 0) return /* @__PURE__ */ new Set();
1498
+ const out = git(["check-ignore", "-z", "--stdin"], { cwd: repo, env, input: paths.map((p) => `${p}\0`).join(""), okCodes: [0, 1] });
1499
+ return new Set(out.split("\0").filter(Boolean));
1500
+ }
1501
+ function everTracked(repo, pathspecs) {
1502
+ const seen = /* @__PURE__ */ new Set();
1503
+ if (pathspecs.length === 0) return seen;
1504
+ if (git(["rev-parse", "--verify", "-q", "HEAD"], { cwd: repo, okCodes: [0, 1] }).trim() === "") return seen;
1505
+ chunked(pathspecs, (chunk) => {
1506
+ const out = git(["log", "--all", "--format=", "--name-only", "-z", "--", ...chunk.map((p) => `:(literal)${p}`)], { cwd: repo });
1507
+ for (const path of out.split("\0")) {
1508
+ const trimmed = path.replace(/^\n+/, "");
1509
+ if (trimmed) seen.add(trimmed);
1350
1510
  }
1351
- if (env.envKeys.length > 0) console.log(` env: ${env.envKeys.join(", ")}`);
1352
- }
1353
- if (def.actions.length > 0) {
1354
- console.log(" Actions:");
1355
- for (const a of def.actions) {
1356
- console.log(` ${formatActionSignature(a)}`);
1357
- console.log(` ${a.description}`);
1511
+ });
1512
+ return seen;
1513
+ }
1514
+ async function chooseDeletions(params) {
1515
+ const repoPath = (path) => params.prefix === "" ? path : `${params.prefix}/${path}`;
1516
+ const ignored = ignoredPaths(params.repo, params.env, params.copyOnly.map((entry) => repoPath(entry.path)));
1517
+ const candidates = params.copyOnly.filter((entry) => !ignored.has(repoPath(entry.path)));
1518
+ const history = everTracked(params.repo, candidates.map((entry) => repoPath(entry.path)));
1519
+ const kept = params.copyOnly.filter((entry) => ignored.has(repoPath(entry.path))).map((entry) => ({ ...entry, why: "ignored here" }));
1520
+ const remove = [];
1521
+ for (const entry of candidates) {
1522
+ if (!entry.path.endsWith("/")) {
1523
+ if (history.has(repoPath(entry.path))) remove.push(entry.path);
1524
+ else kept.push({ ...entry, why: "never tracked here" });
1525
+ continue;
1358
1526
  }
1527
+ const tracked = [...history].some((path) => path.startsWith(repoPath(entry.path)));
1528
+ const listed = tracked ? await params.listDir(entry.path.slice(0, -1)) : [];
1529
+ const ignoredInside = ignoredPaths(params.repo, params.env, listed.map(repoPath));
1530
+ const files = listed.filter((path) => !ignoredInside.has(repoPath(path)) && history.has(repoPath(path)));
1531
+ remove.push(...files);
1532
+ if (files.length < entry.files) kept.push({ path: entry.path, files: entry.files - files.length, why: "never tracked here" });
1359
1533
  }
1360
- if (def.operations.length > 0) {
1361
- console.log(" Operations:");
1362
- for (const op of def.operations) {
1363
- console.log(` ${formatOperationSignature(op)}`);
1364
- console.log(` ${op.description}`);
1534
+ return { delete: remove.sort(), kept: kept.sort((a, b) => a.path.localeCompare(b.path)) };
1535
+ }
1536
+ function applyBatches(files, remove) {
1537
+ const budget = SYNC_MAX_APPLY_BYTES - 1024 * 1024;
1538
+ const cost = (file) => 512 + Math.ceil(file.bytes.length / 512) * 512 + 100 + file.path.length;
1539
+ const batches = [[]];
1540
+ let size = 0;
1541
+ for (const file of files) {
1542
+ if (cost(file) > budget) throw new SyncError(`${file.path} is ${Math.ceil(file.bytes.length / 1024 / 1024)} MiB; a sync sends files of at most ${budget / 1024 / 1024} MiB`);
1543
+ if (size + cost(file) > budget && batches.at(-1).length > 0) {
1544
+ batches.push([]);
1545
+ size = 0;
1365
1546
  }
1547
+ batches.at(-1).push(file);
1548
+ size += cost(file);
1366
1549
  }
1367
- console.log(` Personas: ${def.personas.join(", ") || "(none)"}`);
1368
- console.log(` Ports: ${def.ports.map((p) => `${p.name} (${p.access})`).join(", ") || "(none)"}`);
1550
+ return batches.map((batch, index) => {
1551
+ const manifest = {
1552
+ files: batch.map(({ path, blob, mode }) => ({ path, blob, mode })),
1553
+ delete: index === 0 ? remove : []
1554
+ };
1555
+ return tarOf([
1556
+ { name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest)) },
1557
+ ...batch.map((file, i) => ({ name: syncApplyEntryName(i), data: file.bytes }))
1558
+ ]);
1559
+ });
1369
1560
  }
1370
- function printSandbox(data) {
1371
- console.log(`Sandbox ${data.id}: ${data.status}${data.expired ? " (expired)" : ""}`);
1372
- console.log(` environment ${data.environment}`);
1373
- console.log(` version ${shortVersion(data.version)}`);
1374
- if (data.error) console.log(` error ${data.error}`);
1375
- if (data.readyMs !== null) {
1376
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
1377
- const phases = data.phases ? ` (snapshot ${seconds(data.phases.snapshotMs)}, restore ${seconds(data.phases.restoreMs)}, prepare ${seconds(data.phases.prepareMs)})` : "";
1378
- console.log(` ready in ${seconds(data.readyMs)}${phases}`);
1561
+ async function copyRequest(session, path, init) {
1562
+ let response;
1563
+ try {
1564
+ response = await fetch(`${session.url}${path}`, {
1565
+ method: "POST",
1566
+ headers: { authorization: `Bearer ${session.token}`, "content-type": init.contentType },
1567
+ body: init.body
1568
+ });
1569
+ } catch (error) {
1570
+ throw new SyncError(`could not reach the sandbox for ${path}: ${error instanceof Error ? error.message : String(error)}`);
1379
1571
  }
1380
- if (data.snapshot?.buildPhases) {
1381
- const { buildMs, prepareMs, seeds } = data.snapshot.buildPhases;
1382
- const seedsMs = seeds.reduce((total, step) => total + step.ms, 0);
1383
- console.log(
1384
- ` snapshot built ${data.snapshot.builtAt} (build ${(buildMs / 1e3).toFixed(1)}s, prepare ${(prepareMs / 1e3).toFixed(1)}s, ${seeds.length} seeds ${(seedsMs / 1e3).toFixed(1)}s)`
1385
- );
1386
- for (const step of seeds) console.log(` ${step.id} ${step.action} ${(step.ms / 1e3).toFixed(1)}s`);
1387
- const ran = data.snapshot.buildPhases.build?.steps.filter((step) => step.ran) ?? [];
1388
- if (ran.length > 0) {
1389
- console.log(` build steps: ${ran.map((step) => `${step.name} ${(step.ms / 1e3).toFixed(1)}s`).join(", ")}`);
1572
+ if (!response.ok) {
1573
+ const text = await response.text();
1574
+ let message = text;
1575
+ try {
1576
+ message = JSON.parse(text).error ?? text;
1577
+ } catch {
1390
1578
  }
1579
+ throw new SyncError(`the sandbox refused ${path} (${response.status}): ${message.slice(0, 2e3)}`);
1391
1580
  }
1392
- if (data.leaseExpiresAt) console.log(` expires ${data.leaseExpiresAt}`);
1393
- if (data.synced) {
1394
- writeRawOutput(` synced ${data.synced.buildOutput ? "build output" : "tree"} ${data.synced.tree} at ${data.synced.at}`);
1581
+ return response;
1582
+ }
1583
+ async function plan(session, local, to) {
1584
+ const body = JSON.stringify(to === void 0 ? { local } : { local, to });
1585
+ if (Buffer.byteLength(body) > SYNC_MAX_APPLY_BYTES) {
1586
+ throw new SyncError(
1587
+ `the file list (${local.length} files) is over the ${SYNC_MAX_APPLY_BYTES / 1024 / 1024} MiB a sync can plan in one request; ignore what the sandbox doesn't need (.gitignore)`
1588
+ );
1395
1589
  }
1396
- if (Object.keys(data.portUrls).length > 0) {
1397
- console.log("");
1398
- console.log(" URLs:");
1399
- for (const [name, url] of Object.entries(data.portUrls)) writeRawOutput(` ${pad2(name, 10)} ${url}`);
1590
+ const response = await copyRequest(session, "/sync/plan", { body, contentType: "application/json" });
1591
+ return parseAnswer(syncPlanResponseSchema, await response.text(), "/sync/plan");
1592
+ }
1593
+ function parseAnswer(schema, text, path) {
1594
+ let value;
1595
+ try {
1596
+ value = JSON.parse(text);
1597
+ } catch {
1598
+ throw new SyncError(`the sandbox answered ${path} with something other than JSON: ${text.slice(0, 300)}`);
1400
1599
  }
1401
- if (data.personas.length > 0) {
1402
- console.log("");
1403
- console.log(" Logins:");
1404
- for (const p of data.personas) writeRawOutput(` ${pad2(p.name, 14)} ${p.email} ${p.password}`);
1405
- if (data.nativeLogin !== void 0) {
1406
- console.log(
1407
- data.nativeLogin ? " (native Login nodes sign these in via authentication.run)" : " (no authentication.run: native Login nodes fail with script_not_declared)"
1408
- );
1600
+ const parsed = schema.safeParse(value);
1601
+ if (!parsed.success) throw new SyncError(`the sandbox answered ${path} unexpectedly: ${text.slice(0, 300)}`);
1602
+ return parsed.data;
1603
+ }
1604
+ async function apply(session, batches, build, json) {
1605
+ let written = 0;
1606
+ let deleted = 0;
1607
+ let restarted = [];
1608
+ const steps = [];
1609
+ for (const [index, body] of batches.entries()) {
1610
+ const last = index === batches.length - 1;
1611
+ const response = await copyRequest(session, `/sync/apply${build && last ? "?build=1" : ""}`, { body, contentType: "application/x-tar" });
1612
+ if (!(build && last)) {
1613
+ const result = parseAnswer(syncApplyResultSchema, await response.text(), "/sync/apply");
1614
+ written += result.written;
1615
+ deleted += result.deleted;
1616
+ continue;
1409
1617
  }
1410
- }
1411
- if (data.signInUrl) {
1412
- console.log("");
1413
- writeRawOutput(` Open sign-in: ${data.signInUrl}`);
1414
- }
1415
- const seedIds = Object.keys(data.seedOutputs);
1416
- if (seedIds.length > 0) {
1417
- console.log("");
1418
- console.log(" Seed outputs:");
1419
- for (const id of seedIds) {
1420
- for (const [key, value] of Object.entries(data.seedOutputs[id])) {
1421
- writeRawOutput(` ${id}.${key}: ${JSON.stringify(value)}`);
1618
+ let buffer = "";
1619
+ const decoder2 = new TextDecoder();
1620
+ let finished = false;
1621
+ for await (const chunk of response.body) {
1622
+ buffer += decoder2.decode(chunk, { stream: true });
1623
+ let newline = buffer.indexOf("\n");
1624
+ while (newline >= 0) {
1625
+ const event = parseAnswer(syncBuildEventSchema, buffer.slice(0, newline), "/sync/apply?build=1");
1626
+ buffer = buffer.slice(newline + 1);
1627
+ newline = buffer.indexOf("\n");
1628
+ if (event.type === "applied") {
1629
+ written += event.result.written;
1630
+ deleted += event.result.deleted;
1631
+ } else if (event.type === "output") {
1632
+ if (!json) process5.stderr.write(event.text);
1633
+ } else if (event.type === "step") {
1634
+ steps.push({ name: event.name, ran: event.ran, ms: event.ms });
1635
+ if (!json) process5.stderr.write(` build step ${event.name}: ${event.ran ? `ran in ${(event.ms / 1e3).toFixed(1)} s` : "unchanged"}
1636
+ `);
1637
+ } else if (event.type === "done") {
1638
+ restarted = event.restarted;
1639
+ finished = true;
1640
+ } else if (event.type === "error") {
1641
+ throw new SyncError(`the build failed after the files were synced: ${event.message}`);
1642
+ }
1422
1643
  }
1423
1644
  }
1645
+ if (!finished) throw new SyncError("the build stream ended early; check the copy with `cofactor sandbox exec <id> -- cofactor-sandbox status`");
1424
1646
  }
1425
- if (data.actions.length > 0) {
1426
- console.log("");
1427
- console.log(" Actions:");
1428
- for (const a of data.actions) console.log(` ${formatActionSignature(a)}`);
1429
- }
1430
- if (data.operations.length > 0) {
1431
- console.log("");
1432
- console.log(" Operations:");
1433
- for (const op of data.operations) console.log(` ${formatOperationSignature(op)}`);
1434
- }
1647
+ return { written, deleted, restarted, steps };
1435
1648
  }
1436
- function handleSandboxValidate(argv) {
1437
- const file = argv.find((a) => !a.startsWith("-")) ?? "./cofactor.sandbox.yaml";
1649
+ var SYNC_USAGE = "Usage: cofactor sandbox sync <id> [--build] [--json]\n cofactor sandbox sync <id> <local-dir> --to <path-in-sandbox> [--json]";
1650
+ var LIST_LIMIT = 20;
1651
+ async function handleSandboxSync(positionals, argv, apiUrl, token) {
1652
+ const [id, localDir] = positionals;
1653
+ const to = getArgValue(argv, "--to");
1654
+ const build = hasFlag(argv, "--build");
1438
1655
  const json = hasFlag(argv, "--json");
1439
- const delivery = hasFlag(argv, "--upload") ? "upload" : "github";
1440
- let yamlText;
1441
- try {
1442
- yamlText = readFileSync2(file, "utf-8");
1443
- } catch (error) {
1444
- const message = error instanceof Error ? error.message : String(error);
1445
- if (json) {
1446
- writeRawOutput(JSON.stringify({ ok: false, issues: [{ path: "", message }] }, null, 2));
1447
- } else {
1448
- console.error(`Error reading "${file}": ${message}`);
1449
- }
1450
- process4.exit(1);
1451
- }
1452
- const { definition, issues } = validateDefinition(yamlText, delivery);
1453
- if (json) {
1454
- writeRawOutput(JSON.stringify({ ok: issues.length === 0, issues }, null, 2));
1455
- process4.exit(issues.length === 0 ? 0 : 1);
1656
+ const fail4 = (message) => {
1657
+ console.error(`Error: ${message}`);
1658
+ process5.exit(1);
1659
+ };
1660
+ if (!id) return fail4(`sandbox id required.
1661
+ ${SYNC_USAGE}`);
1662
+ if (localDir === void 0 !== (to === void 0)) return fail4(`a local directory and --to go together.
1663
+ ${SYNC_USAGE}`);
1664
+ if (to !== void 0 && build) return fail4("--build reruns the build of a working-tree sync; build output is already built (`sync <id> <dir> --to <path>`).");
1665
+ if (to !== void 0 && (to === "" || to.startsWith("/") || to.split("/").some((part) => part === "" || part === "." || part === ".."))) {
1666
+ return fail4("--to is a path under the sandbox's sync root, like `static` or `frontend/dist` (no leading `/`, no `..`).");
1456
1667
  }
1457
- if (issues.length > 0) {
1458
- for (const issue of issues) console.error(`${issue.path || "(root)"}: ${issue.message}`);
1459
- process4.exit(1);
1668
+ if (localDir !== void 0 && (!existsSync(localDir) || !lstatSync(localDir).isDirectory())) return fail4(`${localDir} is not a directory`);
1669
+ if (!isInsideRepo(to === void 0 ? process5.cwd() : resolve(localDir))) {
1670
+ return fail4("run this inside a git repository: the files to sync and the synced tree come from git.");
1460
1671
  }
1461
- const environmentCount = Object.keys(definition.environments).length;
1462
- const actionCount = Object.keys(definition.actions).length;
1463
- const operationCount = Object.keys(definition.emulation).reduce(
1464
- (sum, pluginId) => sum + pluginOperations(pluginId).length,
1465
- 0
1672
+ const started = Date.now();
1673
+ const sessionRes = await apiRequest(
1674
+ apiUrl,
1675
+ token,
1676
+ "POST",
1677
+ `/org/sandboxes-v2/sandboxes/${id}/sync-session`
1466
1678
  );
1467
- console.log(`\u2713 ${file} is valid`);
1468
- console.log(` ${environmentCount} environment(s), ${actionCount} action(s), ${operationCount} operation(s)`);
1469
- }
1470
- async function fetchApplicationSandbox(apiUrl, token, applicationId) {
1471
- const res = await apiRequest(apiUrl, token, "GET", `/org/applications/${applicationId}/sandbox`);
1472
- exitOnRefusal(res);
1473
- return res.data;
1474
- }
1475
- var V2_ARGS = {
1476
- envs: { values: ["--application", "--property"], flags: ["--json"], positionals: 0 },
1477
- refresh: { values: ["--application", "--property", "--environment"], flags: ["--wait", "--json"], positionals: 0 },
1478
- connect: { values: ["--application", "--property", "--repo", "--dir"], flags: ["--json"], positionals: 0 },
1479
- push: { values: ["--application", "--property", "--definition", "--tag"], flags: ["--wait", "--json"], positionals: 1 },
1480
- claim: { values: ["--application", "--property", "--environment", "--at", "--timeout"], flags: ["--wait", "--json"], positionals: 0 },
1481
- show: { values: [], flags: ["--json"], positionals: 1 },
1482
- claims: { values: ["--application", "--property"], flags: ["--json"], positionals: 0 },
1483
- run: { values: ["--input", "--json-input"], flags: ["--json"], positionals: 2 },
1484
- exec: { values: ["--timeout"], flags: ["--json"], positionals: 1 },
1485
- sync: { values: ["--to"], flags: ["--build", "--json"], positionals: 2 },
1486
- "verify-env": { values: ["--application", "--property", "--environment"], flags: ["--json"], positionals: 0 },
1487
- destroy: { values: [], flags: [], positionals: 1 },
1488
- secret: { values: ["--application", "--property", "--environment"], flags: ["--json"], positionals: 2 }
1489
- };
1490
- function rejectUnknownSandboxV2Args(command, argv) {
1491
- const spec = V2_ARGS[command];
1492
- if (!spec) return;
1493
- let positionals = 0;
1494
- for (let i = 0; i < argv.length; i++) {
1495
- const arg = argv[i];
1496
- if (spec.values.includes(arg)) {
1497
- i++;
1498
- continue;
1679
+ if (!sessionRes.ok) return fail4(sessionRes.error ?? "could not start a sync");
1680
+ const session = sessionRes.data;
1681
+ try {
1682
+ let local;
1683
+ let decide;
1684
+ let cleanup = () => {
1685
+ };
1686
+ if (to !== void 0) {
1687
+ const dir = resolve(localDir);
1688
+ const output2 = buildOutput(dir, to);
1689
+ local = { ...output2, untracked: /* @__PURE__ */ new Set(), root: dir };
1690
+ decide = async (planned) => ({ delete: planned.copyOnly.map((entry) => entry.path), kept: [] });
1691
+ } else {
1692
+ const root = session.delivery === "github" ? repositoryOf(process5.cwd()).top : process5.cwd();
1693
+ if (session.delivery === "upload" && !existsSync(join(root, "cofactor.sandbox.yaml"))) {
1694
+ return fail4("run this from the directory holding cofactor.sandbox.yaml: a pushed image's WORKDIR mirrors it.");
1695
+ }
1696
+ const tree = workingTree(root);
1697
+ cleanup = tree.cleanup;
1698
+ local = { ...tree, root };
1699
+ decide = (planned) => chooseDeletions({
1700
+ copyOnly: planned.copyOnly,
1701
+ repo: tree.repo,
1702
+ prefix: tree.prefix,
1703
+ env: tree.env,
1704
+ listDir: async (dir) => (await plan(session, [], dir)).copyOnly.map((entry) => entry.path)
1705
+ });
1499
1706
  }
1500
- if (["--input", "--tag"].some((value) => spec.values.includes(value) && arg.startsWith(`${value}=`))) continue;
1501
- if (spec.flags.includes(arg) || arg === "--debug") continue;
1502
- if (!arg.startsWith("-") && ++positionals <= spec.positionals) continue;
1503
- if (command === "secret" && !arg.startsWith("-")) {
1504
- console.error(
1505
- "Error: `cofactor sandbox secret` takes no value argument: pipe the value on stdin. If you typed a real key on the command line, rotate it."
1707
+ try {
1708
+ const planStarted = Date.now();
1709
+ const planned = await plan(session, local.files, to);
1710
+ const skipped = new Set(local.skipped.flatMap((path) => [path, `${path}/`]));
1711
+ const { delete: remove, kept } = await decide({ ...planned, copyOnly: planned.copyOnly.filter((entry) => !skipped.has(entry.path)) });
1712
+ const planMs = Date.now() - planStarted;
1713
+ const need = new Set(planned.need);
1714
+ const sending = local.files.filter((file) => need.has(file.path));
1715
+ if (!json) {
1716
+ writeRawOutput(`Syncing ${local.root} into sandbox ${id} (${session.delivery === "github" ? "GitHub" : "pushed image"}, version ${session.version.slice(0, 12)})`);
1717
+ writeRawOutput(` plan: ${sending.length} to write, ${remove.length} to delete, ${local.files.length - sending.length} unchanged (${(planMs / 1e3).toFixed(1)} s)`);
1718
+ const untracked = sending.filter((file) => local.untracked.has(file.path));
1719
+ const tracked = sending.filter((file) => !local.untracked.has(file.path));
1720
+ if (sending.length > 0) writeRawOutput(" write:");
1721
+ for (const file of tracked.slice(0, LIST_LIMIT)) writeRawOutput(` ${file.path}`);
1722
+ if (tracked.length > LIST_LIMIT) writeRawOutput(` ...and ${tracked.length - LIST_LIMIT} more`);
1723
+ for (const file of untracked) writeRawOutput(` ${file.path} (untracked)`);
1724
+ if (remove.length > 0) writeRawOutput(" delete:");
1725
+ for (const path of remove) writeRawOutput(` ${path}`);
1726
+ if (kept.length > 0) {
1727
+ writeRawOutput(" kept (only in the sandbox):");
1728
+ for (const entry of kept.slice(0, LIST_LIMIT)) {
1729
+ writeRawOutput(` ${entry.path}${entry.files > 1 ? ` (${entry.files} files)` : ""}, ${entry.why}`);
1730
+ }
1731
+ if (kept.length > LIST_LIMIT) writeRawOutput(` ...and ${kept.length - LIST_LIMIT} more`);
1732
+ }
1733
+ if (local.skipped.length > 0) writeRawOutput(` not synced (symlinks and submodules): ${local.skipped.join(", ")}`);
1734
+ }
1735
+ const applyStarted = Date.now();
1736
+ const batches = sending.length === 0 && remove.length === 0 && !build ? [] : applyBatches(
1737
+ sending.map((file) => ({ ...file, bytes: readFileSync2(join(local.root, to === void 0 ? file.path : file.path.slice(to.length + 1))) })),
1738
+ remove
1506
1739
  );
1507
- process4.exit(1);
1740
+ const applied = await apply(session, batches, build, json);
1741
+ const applyMs = Date.now() - applyStarted;
1742
+ const ms = Date.now() - started;
1743
+ const recorded = await apiRequest(apiUrl, token, "POST", `/org/sandboxes-v2/sandboxes/${id}/syncs`, {
1744
+ tree: local.tree,
1745
+ buildOutput: to !== void 0,
1746
+ written: applied.written,
1747
+ deleted: applied.deleted,
1748
+ ms
1749
+ });
1750
+ if (!recorded.ok) throw new SyncError(`the files were synced, but recording the sync failed: ${recorded.error}`);
1751
+ if (json) {
1752
+ writeRawOutput(
1753
+ JSON.stringify(
1754
+ {
1755
+ id,
1756
+ tree: local.tree,
1757
+ buildOutput: to !== void 0,
1758
+ written: sending.map((file) => file.path),
1759
+ deleted: remove,
1760
+ kept,
1761
+ build: build ? { steps: applied.steps, restarted: applied.restarted } : null,
1762
+ ms: { total: ms, plan: planMs, apply: applyMs }
1763
+ },
1764
+ null,
1765
+ 2
1766
+ )
1767
+ );
1768
+ return;
1769
+ }
1770
+ writeRawOutput(` applied: ${applied.written} written, ${applied.deleted} deleted (${(applyMs / 1e3).toFixed(1)} s)`);
1771
+ if (build) writeRawOutput(` restarted: ${applied.restarted.join(", ") || "nothing"}`);
1772
+ writeRawOutput(`Synced ${to === void 0 ? "tree" : "build output"} ${local.tree} in ${(ms / 1e3).toFixed(1)} s.`);
1773
+ if (!build) {
1774
+ writeRawOutput(` Restart what runs it: cofactor sandbox exec ${id} -- cofactor-sandbox restart <service>`);
1775
+ }
1776
+ const shown = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
1777
+ for (const [name, url] of Object.entries(shown.data?.portUrls ?? {})) writeRawOutput(` ${name.padEnd(10)} ${url}`);
1778
+ } finally {
1779
+ cleanup();
1508
1780
  }
1509
- console.error(`Error: \`cofactor sandbox ${command}\` does not take ${JSON.stringify(arg)}. See \`cofactor sandbox --help\`.`);
1510
- process4.exit(1);
1781
+ } catch (error) {
1782
+ if (error instanceof SyncError) return fail4(error.message);
1783
+ throw error;
1511
1784
  }
1512
1785
  }
1513
- function v2Positionals(command, argv) {
1514
- const values = V2_ARGS[command]?.values ?? [];
1515
- const result = [];
1516
- for (let i = 0; i < argv.length; i++) {
1517
- const arg = argv[i];
1518
- if (values.includes(arg)) i++;
1519
- else if (!arg.startsWith("-")) result.push(arg);
1520
- }
1521
- return result;
1786
+ function isInsideRepo(dir) {
1787
+ return git(["rev-parse", "--is-inside-work-tree"], { cwd: dir, okCodes: [0, 128] }).trim() === "true";
1522
1788
  }
1523
- async function handleSandboxEnvs(argv, apiUrl, token) {
1524
- const applicationId = await resolveApplicationId(argv, apiUrl, token);
1525
- const app = await fetchApplicationSandbox(apiUrl, token, applicationId);
1526
- if (hasFlag(argv, "--json")) {
1527
- writeRawOutput(JSON.stringify(app, null, 2));
1528
- return;
1529
- }
1530
- printApplicationHuman(app);
1789
+
1790
+ // src/sandbox-from-local.ts
1791
+ var TTL_MS = 7 * 24 * 3600 * 1e3;
1792
+ var RecordSchema = z4.strictObject({
1793
+ repo: z4.string(),
1794
+ repoName: z4.string(),
1795
+ remoteIdentity: z4.string(),
1796
+ application: z4.string(),
1797
+ applicationId: z4.string().nullable(),
1798
+ claims: z4.array(z4.string()),
1799
+ apiUrl: z4.string(),
1800
+ version: z4.string().regex(/^[0-9a-f]{40}$/),
1801
+ ref: z4.string().regex(/^refs\/cofactor\/wip\/[0-9a-f]{40}$/),
1802
+ updatedAt: z4.number()
1803
+ });
1804
+ function git2(repo, args, env = process.env) {
1805
+ const result = spawnSync2("git", args, {
1806
+ cwd: repo,
1807
+ env,
1808
+ encoding: "utf8",
1809
+ maxBuffer: 64 * 1024 * 1024
1810
+ });
1811
+ if (result.error || result.status !== 0) {
1812
+ const reason = (result.error?.message ?? result.stderr).replace(
1813
+ /https?:\/\/[^\s/@]+(?::[^\s/@]*)?@/g,
1814
+ "https://***@"
1815
+ );
1816
+ throw new Error(`git ${args[0]} failed: ${reason.trim()}`);
1817
+ }
1818
+ return result.stdout.trim();
1819
+ }
1820
+ function remoteIdentity(remote) {
1821
+ if (!/^https?:\/\//.test(remote)) return remote;
1822
+ const url = new URL(remote);
1823
+ url.username = "";
1824
+ url.password = "";
1825
+ return url.href;
1826
+ }
1827
+ var stateDir = () => join2(activeConfigDir(), "sandbox-wip");
1828
+ function save(file, record) {
1829
+ mkdirSync(stateDir(), { recursive: true, mode: 448 });
1830
+ const temp = `${file}.${randomUUID()}.tmp`;
1831
+ writeFileSync(temp, JSON.stringify(record), { mode: 384 });
1832
+ renameSync(temp, file);
1833
+ }
1834
+ function records() {
1835
+ if (!existsSync2(stateDir())) return [];
1836
+ return readdirSync(stateDir()).filter((name) => name.endsWith(".json")).map((name) => {
1837
+ const file = join2(stateDir(), name);
1838
+ return { file, record: RecordSchema.parse(JSON.parse(readFileSync3(file, "utf8"))) };
1839
+ });
1531
1840
  }
1532
- async function handleSandboxConnect(argv, apiUrl, token) {
1533
- const repo = getArgValue(argv, "--repo");
1534
- if (!repo) {
1535
- console.error("Usage: cofactor sandbox connect --application <name|id> --repo <owner/name> [--dir <path>] [--json]");
1536
- process4.exit(1);
1537
- }
1538
- const body = { repo, definitionDir: getArgValue(argv, "--dir") ?? "." };
1539
- const checkCanConnect = async () => {
1540
- exitOnRefusal(await apiRequest(apiUrl, token, "POST", "/org/sandboxes-v2/connect-check", body));
1541
- };
1542
- const applicationId = await resolveOrCreateSandboxApplication(argv, apiUrl, token, checkCanConnect);
1543
- const res = await apiRequest(apiUrl, token, "PUT", `/org/applications/${applicationId}/sandbox`, body);
1544
- exitOnRefusal(res);
1545
- if (hasFlag(argv, "--json")) {
1546
- writeRawOutput(JSON.stringify(res.data, null, 2));
1547
- return;
1841
+ function publishLocalSource(argv, apiUrl) {
1842
+ const application = getArgValue(argv, "--application") ?? getArgValue(argv, "--property");
1843
+ if (!application) throw new Error("--from-local requires --application <name|id>.");
1844
+ const { top } = repositoryOf(process.cwd());
1845
+ const remote = git2(top, ["remote", "get-url", "--push", "origin"]);
1846
+ const inferred = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/.exec(
1847
+ remote
1848
+ )?.[1];
1849
+ const repoName = getArgValue(argv, "--repo") ?? inferred;
1850
+ if (!repoName)
1851
+ throw new Error("Cannot infer a GitHub repository from origin; pass --repo <owner/name>.");
1852
+ if (inferred && inferred.toLowerCase() !== repoName.toLowerCase())
1853
+ throw new Error("--repo must match the origin push repository.");
1854
+ const directory = getArgValue(argv, "--dir") ?? relative2(top, process.cwd());
1855
+ const definitionDir = relative2(top, resolve2(top, directory)).split("\\").join("/") || ".";
1856
+ if (definitionDir === ".." || definitionDir.startsWith("../"))
1857
+ throw new Error("--dir must be inside the repository.");
1858
+ const tree = workingTree(top);
1859
+ try {
1860
+ const yaml = git2(top, ["show", `${tree.tree}:${definitionFile(definitionDir)}`], tree.env);
1861
+ const { definition, issues } = validateDefinition(yaml, "github");
1862
+ if (!definition || issues.length)
1863
+ throw new Error(
1864
+ `Invalid sandbox definition:
1865
+ ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n")}`
1866
+ );
1867
+ const env = {
1868
+ ...tree.env,
1869
+ GIT_AUTHOR_NAME: "Cofactor",
1870
+ GIT_AUTHOR_EMAIL: "sandbox@example.com",
1871
+ GIT_COMMITTER_NAME: "Cofactor",
1872
+ GIT_COMMITTER_EMAIL: "sandbox@example.com",
1873
+ GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
1874
+ GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
1875
+ };
1876
+ const version = git2(
1877
+ top,
1878
+ ["commit-tree", "--no-gpg-sign", tree.tree, "-m", "Cofactor working tree"],
1879
+ env
1880
+ );
1881
+ const key = createHash3("sha256").update(JSON.stringify([top, repoName, application, apiUrl, version])).digest("hex");
1882
+ const file = join2(stateDir(), `${key}.json`);
1883
+ const previous = existsSync2(file) ? RecordSchema.parse(JSON.parse(readFileSync3(file, "utf8"))) : null;
1884
+ const record = {
1885
+ repo: top,
1886
+ repoName,
1887
+ remoteIdentity: remoteIdentity(remote),
1888
+ application,
1889
+ applicationId: previous?.applicationId ?? null,
1890
+ claims: previous?.claims ?? [],
1891
+ apiUrl,
1892
+ version,
1893
+ ref: `refs/cofactor/wip/${tree.tree}`,
1894
+ updatedAt: Date.now()
1895
+ };
1896
+ save(file, record);
1897
+ git2(top, ["push", "--porcelain", "origin", `${version}:${record.ref}`]);
1898
+ return { record, file, definitionDir };
1899
+ } finally {
1900
+ tree.cleanup();
1548
1901
  }
1549
- printApplicationHuman(res.data);
1550
1902
  }
1551
- var CLAIM_POLL_INTERVAL_MS = 3e3;
1552
- var CLAIM_TIMEOUT_SECONDS = 2 * 3600;
1553
- var CLAIM_POLL_GRACE_MS = 2 * 60 * 1e3;
1554
- async function pollSandboxUntilReady(apiUrl, token, id, timeoutSeconds) {
1555
- const deadline = Date.now() + timeoutSeconds * 1e3 + CLAIM_POLL_GRACE_MS;
1556
- let toldUpgrading = false;
1557
- while (Date.now() < deadline) {
1558
- const res = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
1559
- if (!res.ok) return { error: res.error ?? "unknown error" };
1560
- if (res.data.status === "ready" || res.data.status === "failed") return { data: res.data };
1561
- if (res.data.upgrading && !toldUpgrading) {
1562
- toldUpgrading = true;
1563
- console.error(" updating this push to the current sandbox platform (once; the claim continues after)...");
1564
- }
1565
- await sleep(CLAIM_POLL_INTERVAL_MS);
1566
- }
1567
- return { error: `timed out waiting for sandbox ${id} to become ready (${Math.round(timeoutSeconds / 60)} min)` };
1903
+ function rememberLocalApplication(source, applicationId) {
1904
+ source.record.applicationId = applicationId;
1905
+ save(source.file, source.record);
1568
1906
  }
1569
- async function handleSandboxClaim(argv, apiUrl, token) {
1570
- const applicationId = await resolveApplicationId(argv, apiUrl, token);
1571
- const environment2 = getArgValue(argv, "--environment") ?? "default";
1572
- const version = getArgValue(argv, "--at");
1573
- const timeout = parseDurationSeconds(getArgValue(argv, "--timeout"), "--timeout");
1574
- const timeoutSeconds = timeout ?? CLAIM_TIMEOUT_SECONDS;
1575
- const wait = hasFlag(argv, "--wait");
1576
- const json = hasFlag(argv, "--json");
1577
- const body = { applicationId, environment: environment2 };
1578
- if (version) body.version = version;
1579
- if (timeout !== void 0) body.timeoutSeconds = timeout;
1580
- if (wait) body.wait = true;
1581
- const res = await apiRequest(apiUrl, token, "POST", "/org/sandboxes-v2/sandboxes", body);
1582
- if (!res.ok) {
1583
- console.error(`Error: ${res.error}`);
1584
- if (res.code === "SANDBOX_V2_DEFINITION_INVALID") printIssues(res.issues, " ");
1585
- process4.exit(1);
1586
- }
1587
- const id = res.data.id;
1588
- if (!wait) {
1589
- if (json) {
1590
- writeRawOutput(JSON.stringify({ id }, null, 2));
1591
- } else {
1592
- console.log(`Sandbox claiming: ${id}`);
1593
- console.log(` Poll with: cofactor sandbox show ${id}`);
1907
+ function rememberLocalClaim(source, id) {
1908
+ const latest = RecordSchema.parse(JSON.parse(readFileSync3(source.file, "utf8")));
1909
+ source.record.claims = [.../* @__PURE__ */ new Set([...latest.claims, id])];
1910
+ save(source.file, source.record);
1911
+ }
1912
+ function trackedLocalSandbox(id, apiUrl) {
1913
+ const entry = records().find(
1914
+ ({ record }) => record.apiUrl === apiUrl && record.claims.includes(id)
1915
+ );
1916
+ if (!entry || entry.record.applicationId === null) return null;
1917
+ return { applicationId: entry.record.applicationId, version: entry.record.version };
1918
+ }
1919
+ async function cleanupLocalRefs(apiUrl, token, applicationId, application, destroyed) {
1920
+ const candidates = records().filter(
1921
+ ({ record }) => record.apiUrl === apiUrl && (record.applicationId === applicationId || record.applicationId === null && record.application === application) && (destroyed ? record.version === destroyed.version : Date.now() - record.updatedAt >= TTL_MS)
1922
+ );
1923
+ if (!candidates.length) return;
1924
+ const live = /* @__PURE__ */ new Set();
1925
+ let cursor = null;
1926
+ do {
1927
+ const query = new URLSearchParams({ status: "claiming,ready", limit: "100" });
1928
+ if (cursor) query.set("cursor", cursor);
1929
+ const page = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes?${query}`);
1930
+ if (!page.ok)
1931
+ throw new Error(page.error ?? "Could not check live claims; wip refs were retained.");
1932
+ for (const row of page.data) if (row.id !== destroyed?.id) live.add(row.version);
1933
+ cursor = page.nextCursor ?? null;
1934
+ } while (cursor);
1935
+ for (const { file, record } of candidates) {
1936
+ if (live.has(record.version)) continue;
1937
+ if (remoteIdentity(git2(record.repo, ["remote", "get-url", "--push", "origin"])) !== record.remoteIdentity) {
1938
+ throw new Error("The origin push repository changed; wip cleanup left it intact.");
1594
1939
  }
1595
- return;
1596
- }
1597
- const settled = "status" in res.data && (res.data.status === "ready" || res.data.status === "failed") ? res.data : null;
1598
- if (!json && !settled) console.log(`Claiming sandbox ${id}...`);
1599
- const waited = settled ? { data: settled } : await pollSandboxUntilReady(apiUrl, token, id, timeoutSeconds);
1600
- if ("error" in waited) {
1601
- console.error(`Error: ${waited.error}`);
1602
- process4.exit(1);
1603
- }
1604
- const { data } = waited;
1605
- if (json) {
1606
- writeRawOutput(JSON.stringify(data, null, 2));
1607
- } else {
1608
- printSandbox(data);
1940
+ const remoteSha = git2(record.repo, ["ls-remote", "--refs", "origin", record.ref]).split(
1941
+ /\s/
1942
+ )[0];
1943
+ if (remoteSha) {
1944
+ if (remoteSha !== record.version)
1945
+ throw new Error("Wip ref changed remotely; cleanup left it intact.");
1946
+ git2(record.repo, [
1947
+ "push",
1948
+ `--force-with-lease=${record.ref}:${record.version}`,
1949
+ "origin",
1950
+ `:${record.ref}`
1951
+ ]);
1952
+ }
1953
+ rmSync2(file);
1609
1954
  }
1610
- if (data.status === "failed") process4.exit(1);
1611
1955
  }
1612
- async function handleSandboxShow(argv, apiUrl, token) {
1613
- const [id] = argv.filter((a) => !a.startsWith("-"));
1614
- if (!id) {
1615
- console.error("Usage: cofactor sandbox show <id> [--json]");
1616
- process4.exit(1);
1617
- }
1618
- const json = hasFlag(argv, "--json");
1619
- const res = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
1620
- if (!res.ok) {
1621
- console.error(`Error: ${res.error}`);
1622
- process4.exit(1);
1623
- }
1624
- if (json) {
1625
- writeRawOutput(JSON.stringify(res.data, null, 2));
1626
- return;
1956
+
1957
+ // src/sandbox-v2.ts
1958
+ function pad2(s, n) {
1959
+ return s.padEnd(n);
1960
+ }
1961
+ function sleep(ms) {
1962
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1963
+ }
1964
+ function indentLines(text, indent) {
1965
+ return text.split("\n").map((line) => `${indent}${line}`).join("\n");
1966
+ }
1967
+ function formatActionSignature(a) {
1968
+ const inputs = Object.keys(a.input).join(", ");
1969
+ const outputs = Object.keys(a.output).join(", ");
1970
+ return `${a.slug}(${inputs})${outputs ? ` \u2192 ${outputs}` : ""}`;
1971
+ }
1972
+ function jsonSchemaPropTypes(schema) {
1973
+ const properties = schema.properties ?? {};
1974
+ return Object.fromEntries(
1975
+ Object.entries(properties).map(([key, prop]) => [key, typeof prop.type === "string" ? prop.type : "string"])
1976
+ );
1977
+ }
1978
+ function formatOperationSignature(op) {
1979
+ const inputs = Object.keys(jsonSchemaPropTypes(op.inputSchema)).join(", ");
1980
+ const outputs = Object.keys(jsonSchemaPropTypes(op.outputSchema)).join(", ");
1981
+ return `${op.name}(${inputs})${outputs ? ` \u2192 ${outputs}` : ""}`;
1982
+ }
1983
+ function exitOnRefusal(res) {
1984
+ if (res.ok) return;
1985
+ console.error(`Error: ${res.error}`);
1986
+ if (res.code === "SANDBOX_V2_DEFINITION_INVALID") printIssues(res.issues, " ");
1987
+ process6.exit(1);
1988
+ }
1989
+ function printIssues(issues, indent) {
1990
+ for (const issue of issues ?? []) {
1991
+ console.log(`${indent}${issue.path || "(root)"}: ${issue.message}`);
1627
1992
  }
1628
- printSandbox(res.data);
1629
1993
  }
1630
- async function handleSandboxClaims(argv, apiUrl, token) {
1631
- const json = hasFlag(argv, "--json");
1632
- const applicationArg = getArgValue(argv, "--application") ?? getArgValue(argv, "--property");
1633
- const applicationId = applicationArg ? await resolveApplicationId(argv, apiUrl, token) : void 0;
1634
- const claims = [];
1635
- let cursor = null;
1636
- do {
1637
- const query = new URLSearchParams({ mine: "true", status: "claiming,ready", limit: "100" });
1638
- if (applicationId) query.set("applicationId", applicationId);
1639
- if (cursor) query.set("cursor", cursor);
1640
- const res = await apiRequest(
1641
- apiUrl,
1642
- token,
1643
- "GET",
1644
- `/org/sandboxes-v2/sandboxes?${query}`
1645
- );
1646
- if (!res.ok) {
1647
- console.error(`Error: ${res.error ?? "could not list sandboxes"}`);
1648
- process4.exit(1);
1994
+ function shortVersion(version) {
1995
+ return /^[0-9a-f]{40}$/.test(version) ? version.slice(0, 12) : version;
1996
+ }
1997
+ function printApplicationHuman(app) {
1998
+ console.log(
1999
+ app.delivery === "github" ? `Delivered from GitHub: ${app.repo}${app.definitionDir && app.definitionDir !== "." ? `:${app.definitionDir}` : ""}` : "Delivered as pushed images"
2000
+ );
2001
+ console.log(
2002
+ ` version ${app.version ? shortVersion(app.version) : app.delivery === "github" ? "(no definition on the default branch yet; use --at <sha>)" : "(none yet: run `cofactor sandbox push`)"}`
2003
+ );
2004
+ if (app.pushes && app.pushes.length > 0) {
2005
+ console.log(" pushes:");
2006
+ for (const push2 of app.pushes) {
2007
+ const tags = push2.tags.length > 0 ? ` [${push2.tags.join(", ")}]` : "";
2008
+ console.log(` ${push2.id} ${pad2(push2.status, 9)} ${push2.createdAt}${tags}${push2.error ? `
2009
+ ${push2.error}` : ""}`);
1649
2010
  }
1650
- claims.push(...res.data);
1651
- cursor = res.nextCursor ?? null;
1652
- } while (cursor);
1653
- if (json) {
1654
- writeRawOutput(JSON.stringify(claims, null, 2));
1655
- return;
1656
- }
1657
- if (claims.length === 0) {
1658
- console.log("No live claims.");
1659
- return;
1660
2011
  }
1661
- const names = new Map((await fetchProperties(apiUrl, token)).map((p) => [p.id, p.name]));
1662
- console.log(`${pad2("ID", 38)} ${pad2("Application", 24)} ${pad2("Environment", 14)} ${pad2("Status", 10)} ${pad2("Claimed", 26)} Expires`);
1663
- for (const c of claims) {
1664
- console.log(
1665
- `${pad2(c.id, 38)} ${pad2(names.get(c.applicationId) ?? c.applicationId, 24)} ${pad2(c.environment, 14)} ${pad2(c.status, 10)} ${pad2(c.createdAt, 26)} ${c.leaseExpiresAt ?? "-"}`
1666
- );
2012
+ if (app.sandboxEnvironments.length > 0) {
2013
+ console.log(" sandbox environments:");
2014
+ for (const env of app.sandboxEnvironments) console.log(` ${pad2(env.environment, 16)} ${env.name}`);
1667
2015
  }
1668
- console.log("\nTear one down with: cofactor sandbox destroy <id>");
1669
- }
1670
- function coerceInputValue(raw, type) {
1671
- switch (type) {
1672
- case "integer": {
1673
- if (!/^-?\d+$/.test(raw)) throw new Error(`must be an integer, got "${raw}"`);
1674
- return Number(raw);
1675
- }
1676
- case "number": {
1677
- const value = Number(raw);
1678
- if (Number.isNaN(value)) throw new Error(`must be a number, got "${raw}"`);
1679
- return value;
1680
- }
1681
- case "boolean": {
1682
- if (raw === "true") return true;
1683
- if (raw === "false") return false;
1684
- throw new Error(`must be "true" or "false", got "${raw}"`);
2016
+ if (app.heads && app.heads.length > 0) {
2017
+ console.log(" heads (what flow runs and unpinned claims use; a warm environment switches after its warm-up; update with `cofactor sandbox refresh`):");
2018
+ for (const head of app.heads) {
2019
+ const version = head.version ? shortVersion(head.version) : "(none)";
2020
+ const built = head.builtAt ? `, built ${head.builtAt}` : "";
2021
+ const behind = head.behindMain && head.mainHead ? ` main has moved: now ${shortVersion(head.mainHead)}` : "";
2022
+ console.log(` ${pad2(head.environment, 16)} ${version} (${head.status}${built})${behind}`);
2023
+ if (head.error) console.log(` ${head.error}`);
1685
2024
  }
1686
- default:
1687
- return raw;
1688
2025
  }
1689
- }
1690
- function buildRunInputFromFlags(argv, propTypes) {
1691
- const pairs = getArgValues(argv, "--input");
1692
- const input = {};
1693
- for (const pair of pairs) {
1694
- const eq = pair.indexOf("=");
1695
- if (eq <= 0) {
1696
- console.error(`Error: invalid --input "${pair}". Use key=value.`);
1697
- process4.exit(1);
2026
+ const def = app.definition;
2027
+ if (!def) return;
2028
+ console.log(" Environments:");
2029
+ for (const env of def.environments) {
2030
+ console.log(` ${env.name}${env.extends ? ` (extends ${env.extends})` : ""}`);
2031
+ console.log(indentLines(env.description, " "));
2032
+ if (env.seeds.length > 0) {
2033
+ console.log(" seeds:");
2034
+ for (const step of env.seeds) console.log(` ${step.id}: ${step.action}`);
1698
2035
  }
1699
- const key = pair.slice(0, eq);
1700
- const raw = pair.slice(eq + 1);
1701
- const type = propTypes[key];
1702
- if (!type) {
1703
- console.error(`Error: unknown input "${key}". Known: ${Object.keys(propTypes).join(", ") || "(none)"}`);
1704
- process4.exit(1);
2036
+ if (env.envKeys.length > 0) console.log(` env: ${env.envKeys.join(", ")}`);
2037
+ }
2038
+ if (def.actions.length > 0) {
2039
+ console.log(" Actions:");
2040
+ for (const a of def.actions) {
2041
+ console.log(` ${formatActionSignature(a)}`);
2042
+ console.log(` ${a.description}`);
1705
2043
  }
1706
- try {
1707
- input[key] = coerceInputValue(raw, type);
1708
- } catch (error) {
1709
- console.error(`Error: --input ${key}: ${error instanceof Error ? error.message : String(error)}`);
1710
- process4.exit(1);
2044
+ }
2045
+ if (def.operations.length > 0) {
2046
+ console.log(" Operations:");
2047
+ for (const op of def.operations) {
2048
+ console.log(` ${formatOperationSignature(op)}`);
2049
+ console.log(` ${op.description}`);
1711
2050
  }
1712
2051
  }
1713
- return input;
2052
+ console.log(` Personas: ${def.personas.join(", ") || "(none)"}`);
2053
+ console.log(` Ports: ${def.ports.map((p) => `${p.name} (${p.access})`).join(", ") || "(none)"}`);
1714
2054
  }
1715
- async function pollRunUntilDone(apiUrl, token, runId, timeoutMs, timeoutNote = "") {
1716
- const deadline = Date.now() + timeoutMs;
1717
- while (Date.now() < deadline) {
1718
- const pollRes = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/runs/${runId}`);
1719
- if (!pollRes.ok) {
1720
- console.error(`Error: ${pollRes.error}`);
1721
- process4.exit(1);
2055
+ function printSandbox(data) {
2056
+ console.log(`Sandbox ${data.id}: ${data.status}${data.expired ? " (expired)" : ""}`);
2057
+ console.log(` environment ${data.environment}`);
2058
+ console.log(` version ${shortVersion(data.version)}`);
2059
+ if (data.error) console.log(` error ${data.error}`);
2060
+ if (data.readyMs !== null) {
2061
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
2062
+ const phases = data.phases ? ` (snapshot ${seconds(data.phases.snapshotMs)}, restore ${seconds(data.phases.restoreMs)}, prepare ${seconds(data.phases.prepareMs)})` : "";
2063
+ console.log(` ready in ${seconds(data.readyMs)}${phases}`);
2064
+ }
2065
+ if (data.snapshot?.buildPhases) {
2066
+ const { buildMs, prepareMs, seeds } = data.snapshot.buildPhases;
2067
+ const seedsMs = seeds.reduce((total, step) => total + step.ms, 0);
2068
+ console.log(
2069
+ ` snapshot built ${data.snapshot.builtAt} (build ${(buildMs / 1e3).toFixed(1)}s, prepare ${(prepareMs / 1e3).toFixed(1)}s, ${seeds.length} seeds ${(seedsMs / 1e3).toFixed(1)}s)`
2070
+ );
2071
+ for (const step of seeds) console.log(` ${step.id} ${step.action} ${(step.ms / 1e3).toFixed(1)}s`);
2072
+ const ran = data.snapshot.buildPhases.build?.steps.filter((step) => step.ran) ?? [];
2073
+ if (ran.length > 0) {
2074
+ console.log(` build steps: ${ran.map((step) => `${step.name} ${(step.ms / 1e3).toFixed(1)}s`).join(", ")}`);
1722
2075
  }
1723
- if (pollRes.data.status !== "running") return pollRes.data;
1724
- await sleep(2e3);
1725
2076
  }
1726
- console.error(`Timed out waiting for run ${runId}${timeoutNote}.`);
1727
- process4.exit(1);
1728
- }
1729
- async function handleSandboxRun(argv, apiUrl, token) {
1730
- const positional = argv.filter((a) => !a.startsWith("-"));
1731
- const [id, actionRef] = positional;
1732
- if (!id || !actionRef) {
1733
- console.error("Usage: cofactor sandbox run <id> <action|plugin.op> [--input k=v ...] [--json-input '{...}'] [--json]");
1734
- process4.exit(1);
2077
+ if (data.leaseExpiresAt) console.log(` expires ${data.leaseExpiresAt}`);
2078
+ if (data.synced) {
2079
+ writeRawOutput(` synced ${data.synced.buildOutput ? "build output" : "tree"} ${data.synced.tree} at ${data.synced.at}`);
1735
2080
  }
1736
- const json = hasFlag(argv, "--json");
1737
- const jsonInput = getArgValue(argv, "--json-input");
1738
- const showRes = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
1739
- if (!showRes.ok) {
1740
- console.error(`Error: ${showRes.error}`);
1741
- process4.exit(1);
2081
+ if (Object.keys(data.portUrls).length > 0) {
2082
+ console.log("");
2083
+ console.log(" URLs:");
2084
+ for (const [name, url] of Object.entries(data.portUrls)) writeRawOutput(` ${pad2(name, 10)} ${url}`);
1742
2085
  }
1743
- const sandbox = showRes.data;
1744
- const repoAction = sandbox.actions.find((a) => a.slug === actionRef);
1745
- const operation = sandbox.operations.find((op) => op.name === actionRef);
1746
- if (!repoAction && !operation) {
1747
- console.error(`Error: unknown action or operation "${actionRef}".`);
1748
- console.error(` Actions: ${sandbox.actions.map((a) => a.slug).join(", ") || "(none)"}`);
1749
- console.error(` Operations: ${sandbox.operations.map((op) => op.name).join(", ") || "(none)"}`);
1750
- process4.exit(1);
2086
+ if (data.personas.length > 0) {
2087
+ console.log("");
2088
+ console.log(" Logins:");
2089
+ for (const p of data.personas) writeRawOutput(` ${pad2(p.name, 14)} ${p.email} ${p.password}`);
2090
+ if (data.nativeLogin !== void 0) {
2091
+ console.log(
2092
+ data.nativeLogin ? " (native Login nodes sign these in via authentication.run)" : " (no authentication.run: native Login nodes fail with script_not_declared)"
2093
+ );
2094
+ }
1751
2095
  }
1752
- let input;
1753
- if (jsonInput !== void 0) {
1754
- try {
1755
- input = JSON.parse(jsonInput);
1756
- } catch (error) {
1757
- console.error(`Error: invalid --json-input: ${error instanceof Error ? error.message : String(error)}`);
1758
- process4.exit(1);
2096
+ if (data.signInUrl) {
2097
+ console.log("");
2098
+ writeRawOutput(` Open sign-in: ${data.signInUrl}`);
2099
+ }
2100
+ const seedIds = Object.keys(data.seedOutputs);
2101
+ if (seedIds.length > 0) {
2102
+ console.log("");
2103
+ console.log(" Seed outputs:");
2104
+ for (const id of seedIds) {
2105
+ for (const [key, value] of Object.entries(data.seedOutputs[id])) {
2106
+ writeRawOutput(` ${id}.${key}: ${JSON.stringify(value)}`);
2107
+ }
1759
2108
  }
1760
- } else if (repoAction) {
1761
- const propTypes = Object.fromEntries(Object.entries(repoAction.input).map(([key, prop]) => [key, prop.type]));
1762
- input = buildRunInputFromFlags(argv, propTypes);
1763
- } else {
1764
- input = buildRunInputFromFlags(argv, jsonSchemaPropTypes(operation.inputSchema));
1765
2109
  }
1766
- const enqueueRes = await apiRequest(
2110
+ if (data.actions.length > 0) {
2111
+ console.log("");
2112
+ console.log(" Actions:");
2113
+ for (const a of data.actions) console.log(` ${formatActionSignature(a)}`);
2114
+ }
2115
+ if (data.operations.length > 0) {
2116
+ console.log("");
2117
+ console.log(" Operations:");
2118
+ for (const op of data.operations) console.log(` ${formatOperationSignature(op)}`);
2119
+ }
2120
+ }
2121
+ function handleSandboxValidate(argv) {
2122
+ const file = argv.find((a) => !a.startsWith("-")) ?? "./cofactor.sandbox.yaml";
2123
+ const json = hasFlag(argv, "--json");
2124
+ const delivery = hasFlag(argv, "--upload") ? "upload" : "github";
2125
+ let yamlText;
2126
+ try {
2127
+ yamlText = readFileSync4(file, "utf-8");
2128
+ } catch (error) {
2129
+ const message = error instanceof Error ? error.message : String(error);
2130
+ if (json) {
2131
+ writeRawOutput(JSON.stringify({ ok: false, issues: [{ path: "", message }] }, null, 2));
2132
+ } else {
2133
+ console.error(`Error reading "${file}": ${message}`);
2134
+ }
2135
+ process6.exit(1);
2136
+ }
2137
+ const { definition, issues } = validateDefinition(yamlText, delivery);
2138
+ if (json) {
2139
+ writeRawOutput(JSON.stringify({ ok: issues.length === 0, issues }, null, 2));
2140
+ process6.exit(issues.length === 0 ? 0 : 1);
2141
+ }
2142
+ if (issues.length > 0) {
2143
+ for (const issue of issues) console.error(`${issue.path || "(root)"}: ${issue.message}`);
2144
+ process6.exit(1);
2145
+ }
2146
+ const environmentCount = Object.keys(definition.environments).length;
2147
+ const actionCount = Object.keys(definition.actions).length;
2148
+ const operationCount = Object.keys(definition.emulation).reduce(
2149
+ (sum, pluginId) => sum + pluginOperations(pluginId).length,
2150
+ 0
2151
+ );
2152
+ console.log(`\u2713 ${file} is valid`);
2153
+ console.log(` ${environmentCount} environment(s), ${actionCount} action(s), ${operationCount} operation(s)`);
2154
+ }
2155
+ async function fetchApplicationSandbox(apiUrl, token, applicationId, at) {
2156
+ const res = await apiRequest(
1767
2157
  apiUrl,
1768
2158
  token,
1769
- "POST",
1770
- `/org/sandboxes-v2/sandboxes/${id}/runs`,
1771
- { action: actionRef, input }
2159
+ "GET",
2160
+ `/org/applications/${applicationId}/sandbox${at ? `?at=${encodeURIComponent(at)}` : ""}`
1772
2161
  );
1773
- if (!enqueueRes.ok) {
1774
- if (enqueueRes.code === "SANDBOX_V2_ACTION_INVALID_INPUT") {
1775
- console.error(`Error: invalid input for "${actionRef}":`);
1776
- for (const issue of enqueueRes.issues ?? []) {
1777
- if (typeof issue === "string") console.error(` ${issue}`);
1778
- else console.error(` ${(issue.path ?? []).join(".")}: ${issue.message ?? ""}`);
2162
+ exitOnRefusal(res);
2163
+ return res.data;
2164
+ }
2165
+ var V2_ARGS = {
2166
+ envs: { values: ["--application", "--property"], flags: ["--json"], positionals: 0 },
2167
+ refresh: {
2168
+ values: ["--application", "--property", "--environment"],
2169
+ flags: ["--wait", "--json"],
2170
+ positionals: 0
2171
+ },
2172
+ connect: {
2173
+ values: ["--application", "--property", "--repo", "--dir", "--at"],
2174
+ flags: ["--json"],
2175
+ positionals: 0
2176
+ },
2177
+ push: {
2178
+ values: ["--application", "--property", "--definition", "--tag"],
2179
+ flags: ["--wait", "--json"],
2180
+ positionals: 1
2181
+ },
2182
+ claim: {
2183
+ values: ["--application", "--property", "--environment", "--at", "--timeout", "--repo", "--dir"],
2184
+ flags: ["--wait", "--json", "--from-local"],
2185
+ positionals: 0
2186
+ },
2187
+ show: { values: [], flags: ["--json"], positionals: 1 },
2188
+ claims: { values: ["--application", "--property"], flags: ["--json"], positionals: 0 },
2189
+ run: { values: ["--input", "--json-input"], flags: ["--json"], positionals: 2 },
2190
+ exec: { values: ["--timeout"], flags: ["--json"], positionals: 1 },
2191
+ sync: { values: ["--to"], flags: ["--build", "--json"], positionals: 2 },
2192
+ "auth-check": { values: ["--persona"], flags: ["--json"], positionals: 1 },
2193
+ "verify-env": {
2194
+ values: ["--application", "--property", "--environment", "--at"],
2195
+ flags: ["--json"],
2196
+ positionals: 0
2197
+ },
2198
+ destroy: { values: [], flags: [], positionals: 1 },
2199
+ secret: { values: ["--application", "--property", "--environment"], flags: ["--json"], positionals: 2 }
2200
+ };
2201
+ function rejectUnknownSandboxV2Args(command, argv) {
2202
+ const spec = V2_ARGS[command];
2203
+ if (!spec) return;
2204
+ let positionals = 0;
2205
+ for (let i = 0; i < argv.length; i++) {
2206
+ const arg = argv[i];
2207
+ if (spec.values.includes(arg)) {
2208
+ const value = argv[i + 1];
2209
+ if (!value || value.startsWith("--")) {
2210
+ console.error(`Error: ${arg} requires a value. See \`cofactor sandbox --help\`.`);
2211
+ process6.exit(1);
1779
2212
  }
1780
- console.error("");
1781
- console.error("Input schema:");
1782
- writeRawOutput(JSON.stringify(repoAction ? repoAction.input : operation.inputSchema, null, 2));
1783
- } else {
1784
- console.error(`Error: ${enqueueRes.error}`);
2213
+ i++;
2214
+ continue;
1785
2215
  }
1786
- process4.exit(1);
2216
+ if (["--input", "--tag"].some((value) => spec.values.includes(value) && arg.startsWith(`${value}=`))) continue;
2217
+ if (spec.flags.includes(arg) || arg === "--debug") continue;
2218
+ if (!arg.startsWith("-") && ++positionals <= spec.positionals) continue;
2219
+ if (command === "secret" && !arg.startsWith("-")) {
2220
+ console.error(
2221
+ "Error: `cofactor sandbox secret` takes no value argument: pipe the value on stdin. If you typed a real key on the command line, rotate it."
2222
+ );
2223
+ process6.exit(1);
2224
+ }
2225
+ console.error(`Error: \`cofactor sandbox ${command}\` does not take ${JSON.stringify(arg)}. See \`cofactor sandbox --help\`.`);
2226
+ process6.exit(1);
1787
2227
  }
1788
- const runId = enqueueRes.data.id;
1789
- const run = await pollRunUntilDone(apiUrl, token, runId, ((repoAction?.timeout ?? 60) + 60) * 1e3);
1790
- if (json) {
1791
- writeRawOutput(JSON.stringify(run, null, 2));
1792
- if (run.status === "failed") process4.exit(1);
2228
+ }
2229
+ function v2Positionals(command, argv) {
2230
+ const values = V2_ARGS[command]?.values ?? [];
2231
+ const result = [];
2232
+ for (let i = 0; i < argv.length; i++) {
2233
+ const arg = argv[i];
2234
+ if (values.includes(arg)) i++;
2235
+ else if (!arg.startsWith("-")) result.push(arg);
2236
+ }
2237
+ return result;
2238
+ }
2239
+ async function handleSandboxEnvs(argv, apiUrl, token) {
2240
+ const applicationId = await resolveApplicationId(argv, apiUrl, token);
2241
+ const app = await fetchApplicationSandbox(apiUrl, token, applicationId);
2242
+ if (hasFlag(argv, "--json")) {
2243
+ writeRawOutput(JSON.stringify(app, null, 2));
1793
2244
  return;
1794
2245
  }
1795
- if (run.status === "failed") {
1796
- console.error(`Run failed: ${run.error ?? "unknown error"}`);
1797
- if (run.stderrTail) {
1798
- console.error("");
1799
- console.error("stderr:");
1800
- console.error(run.stderrTail);
1801
- }
1802
- process4.exit(1);
2246
+ printApplicationHuman(app);
2247
+ }
2248
+ async function connectSandboxApplication(argv, apiUrl, token) {
2249
+ const repo = getArgValue(argv, "--repo");
2250
+ if (!repo) {
2251
+ console.error("Usage: cofactor sandbox connect --application <name|id> --repo <owner/name> [--dir <path>] [--json]");
2252
+ process6.exit(1);
1803
2253
  }
1804
- writeRawOutput(JSON.stringify(run.output ?? {}, null, 2));
2254
+ const body = {
2255
+ repo,
2256
+ definitionDir: getArgValue(argv, "--dir") ?? ".",
2257
+ at: getArgValue(argv, "--at")
2258
+ };
2259
+ const checkCanConnect = async () => {
2260
+ exitOnRefusal(await apiRequest(apiUrl, token, "POST", "/org/sandboxes-v2/connect-check", body));
2261
+ };
2262
+ const applicationId = await resolveOrCreateSandboxApplication(argv, apiUrl, token, checkCanConnect);
2263
+ const res = await apiRequest(apiUrl, token, "PUT", `/org/applications/${applicationId}/sandbox`, body);
2264
+ exitOnRefusal(res);
2265
+ return res.data;
1805
2266
  }
1806
- var EXEC_USAGE = "Usage: cofactor sandbox exec <id> [--timeout <seconds>] [--json] -- <command...>";
1807
- function parseExecArgs(options, commandWords) {
1808
- const command = commandWords.join(" ");
1809
- if (command.trim() === "") return { ok: false, error: `a command is required after \`--\`. ${EXEC_USAGE}` };
1810
- const id = v2Positionals("exec", options)[0];
1811
- if (!id) return { ok: false, error: `sandbox id required. ${EXEC_USAGE}` };
1812
- const timeout = getArgValue(options, "--timeout");
1813
- let timeoutSeconds;
1814
- if (timeout !== void 0) {
1815
- timeoutSeconds = /^\d+$/.test(timeout) ? Number(timeout) : NaN;
1816
- if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > EXEC_MAX_TIMEOUT_SECONDS) {
1817
- return { ok: false, error: `--timeout must be a whole number of seconds from 1 to ${EXEC_MAX_TIMEOUT_SECONDS}.` };
2267
+ async function handleSandboxConnect(argv, apiUrl, token) {
2268
+ const app = await connectSandboxApplication(argv, apiUrl, token);
2269
+ if (hasFlag(argv, "--json")) writeRawOutput(JSON.stringify(app, null, 2));
2270
+ else printApplicationHuman(app);
2271
+ }
2272
+ var CLAIM_POLL_INTERVAL_MS = 3e3;
2273
+ var CLAIM_TIMEOUT_SECONDS = 2 * 3600;
2274
+ var CLAIM_POLL_GRACE_MS = 2 * 60 * 1e3;
2275
+ async function pollSandboxUntilReady(apiUrl, token, id, timeoutSeconds) {
2276
+ const deadline = Date.now() + timeoutSeconds * 1e3 + CLAIM_POLL_GRACE_MS;
2277
+ let toldUpgrading = false;
2278
+ while (Date.now() < deadline) {
2279
+ const res = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
2280
+ if (!res.ok) return { error: res.error ?? "unknown error" };
2281
+ if (res.data.status === "ready" || res.data.status === "failed") return { data: res.data };
2282
+ if (res.data.upgrading && !toldUpgrading) {
2283
+ toldUpgrading = true;
2284
+ console.error(" updating this push to the current sandbox platform (once; the claim continues after)...");
1818
2285
  }
2286
+ await sleep(CLAIM_POLL_INTERVAL_MS);
1819
2287
  }
1820
- return { ok: true, args: { id, timeoutSeconds, json: hasFlag(options, "--json"), command } };
1821
- }
1822
- function execExitCode(run) {
1823
- if (run.status === "succeeded") return 0;
1824
- return run.exitCode ?? 1;
2288
+ return { error: `timed out waiting for sandbox ${id} to become ready (${Math.round(timeoutSeconds / 60)} min)` };
1825
2289
  }
1826
- async function handleSandboxExec(options, commandWords, apiUrl, token) {
1827
- const parsed = parseExecArgs(options, commandWords);
1828
- if (!parsed.ok) {
1829
- console.error(`Error: ${parsed.error}`);
1830
- process4.exit(1);
2290
+ async function handleSandboxClaim(argv, apiUrl, token) {
2291
+ const timeout = parseDurationSeconds(getArgValue(argv, "--timeout"), "--timeout");
2292
+ const fromLocal = hasFlag(argv, "--from-local");
2293
+ if (fromLocal && hasFlag(argv, "--at")) throw new Error("--from-local and --at are mutually exclusive.");
2294
+ if (!fromLocal && (hasFlag(argv, "--repo") || hasFlag(argv, "--dir"))) throw new Error("--repo and --dir on claim require --from-local.");
2295
+ const source = fromLocal ? publishLocalSource(argv, apiUrl) : null;
2296
+ const applicationId = source ? (await connectSandboxApplication(["--application", source.record.application, "--repo", source.record.repoName, "--dir", source.definitionDir, "--at", source.record.version], apiUrl, token)).applicationId : await resolveApplicationId(argv, apiUrl, token);
2297
+ if (source) {
2298
+ rememberLocalApplication(source, applicationId);
2299
+ await cleanupLocalRefs(apiUrl, token, applicationId, source.record.application);
1831
2300
  }
1832
- const { id, timeoutSeconds, json, command } = parsed.args;
1833
- const enqueueRes = await apiRequest(
1834
- apiUrl,
1835
- token,
1836
- "POST",
1837
- `/org/sandboxes-v2/sandboxes/${id}/exec`,
1838
- timeoutSeconds === void 0 ? { command } : { command, timeoutSeconds }
1839
- );
1840
- if (!enqueueRes.ok) {
1841
- console.error(`Error: ${enqueueRes.error}`);
1842
- process4.exit(1);
2301
+ const environment2 = getArgValue(argv, "--environment") ?? "default";
2302
+ const version = source?.record.version ?? getArgValue(argv, "--at");
2303
+ const timeoutSeconds = timeout ?? CLAIM_TIMEOUT_SECONDS;
2304
+ const wait = hasFlag(argv, "--wait");
2305
+ const json = hasFlag(argv, "--json");
2306
+ const body = { applicationId, environment: environment2 };
2307
+ if (version) body.version = version;
2308
+ if (timeout !== void 0) body.timeoutSeconds = timeout;
2309
+ if (wait) body.wait = true;
2310
+ const res = await apiRequest(apiUrl, token, "POST", "/org/sandboxes-v2/sandboxes", body);
2311
+ if (!res.ok) {
2312
+ console.error(`Error: ${res.error}`);
2313
+ if (res.code === "SANDBOX_V2_DEFINITION_INVALID") printIssues(res.issues, " ");
2314
+ process6.exit(1);
1843
2315
  }
1844
- const runId = enqueueRes.data.id;
1845
- const run = await pollRunUntilDone(
1846
- apiUrl,
1847
- token,
1848
- runId,
1849
- ((timeoutSeconds ?? EXEC_DEFAULT_TIMEOUT_SECONDS) + 60) * 1e3,
1850
- "; the command may still be running in the sandbox"
1851
- );
1852
- const code = execExitCode(run);
1853
- if (json) {
1854
- writeRawOutput(JSON.stringify(run, null, 2));
1855
- process4.exit(code);
2316
+ const id = res.data.id;
2317
+ if (source) rememberLocalClaim(source, id);
2318
+ if (source && wait && !json) console.log(`Source commit: ${version}`);
2319
+ if (!wait) {
2320
+ if (json) {
2321
+ writeRawOutput(JSON.stringify(source ? { id, version } : { id }, null, 2));
2322
+ } else {
2323
+ console.log(`Sandbox claiming: ${id}`);
2324
+ if (source) console.log(` Source commit: ${version}`);
2325
+ console.log(` Poll with: cofactor sandbox show ${id}`);
2326
+ }
2327
+ return;
1856
2328
  }
1857
- if (run.status === "failed" && run.exitCode === null) {
1858
- process4.stderr.write(`Error: ${run.error ?? "unknown error"}
1859
- `);
1860
- if (run.stdoutTail) process4.stderr.write(run.stdoutTail);
1861
- if (run.stdoutTail && !run.stdoutTail.endsWith("\n") && run.stderrTail) process4.stderr.write("\n");
1862
- if (run.stderrTail) process4.stderr.write(run.stderrTail);
1863
- process4.exit(1);
2329
+ const settled = "status" in res.data && (res.data.status === "ready" || res.data.status === "failed") ? res.data : null;
2330
+ if (!json && !settled) console.log(`Claiming sandbox ${id}...`);
2331
+ const waited = settled ? { data: settled } : await pollSandboxUntilReady(apiUrl, token, id, timeoutSeconds);
2332
+ if ("error" in waited) {
2333
+ console.error(`Error: ${waited.error}`);
2334
+ process6.exit(1);
1864
2335
  }
1865
- if (run.stdoutTail) process4.stdout.write(run.stdoutTail);
1866
- if (run.stderrTail) process4.stderr.write(run.stderrTail);
1867
- if (code === 124 && run.error) process4.stderr.write(`
1868
- ${run.error}
1869
- `);
1870
- process4.exit(code);
2336
+ const { data } = waited;
2337
+ if (json) {
2338
+ writeRawOutput(JSON.stringify(data, null, 2));
2339
+ } else {
2340
+ printSandbox(data);
2341
+ }
2342
+ if (data.status === "failed") process6.exit(1);
1871
2343
  }
1872
- async function handleSandboxDestroy(argv, apiUrl, token) {
2344
+ async function handleSandboxShow(argv, apiUrl, token) {
1873
2345
  const [id] = argv.filter((a) => !a.startsWith("-"));
1874
2346
  if (!id) {
1875
- console.error("Usage: cofactor sandbox destroy <id>");
1876
- process4.exit(1);
2347
+ console.error("Usage: cofactor sandbox show <id> [--json]");
2348
+ process6.exit(1);
1877
2349
  }
1878
- const res = await apiRequest(
1879
- apiUrl,
1880
- token,
1881
- "DELETE",
1882
- `/org/sandboxes-v2/sandboxes/${id}`
1883
- );
2350
+ const json = hasFlag(argv, "--json");
2351
+ const res = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
1884
2352
  if (!res.ok) {
1885
2353
  console.error(`Error: ${res.error}`);
1886
- process4.exit(1);
2354
+ process6.exit(1);
1887
2355
  }
1888
- console.log(`Sandbox ${id} destroying.`);
1889
- }
1890
- async function handleSandboxVerifyEnv(argv, apiUrl, token) {
1891
- const applicationId = await resolveApplicationId(argv, apiUrl, token);
1892
- const onlyEnvironment = getArgValue(argv, "--environment");
1893
- const json = hasFlag(argv, "--json");
1894
- const log = (line) => json ? console.error(line) : console.log(line);
1895
- const app = await fetchApplicationSandbox(apiUrl, token, applicationId);
1896
- if (!app.version || !app.definition) {
1897
- console.error("Error: this application has no version to verify yet: run `cofactor sandbox push` first.");
1898
- process4.exit(1);
1899
- }
1900
- const version = app.version;
1901
- const allEnvironments = app.definition.environments.map((e) => e.name);
1902
- if (onlyEnvironment && !allEnvironments.includes(onlyEnvironment)) {
1903
- console.error(`Error: environment "${onlyEnvironment}" not found. Known: ${allEnvironments.join(", ")}`);
1904
- process4.exit(1);
2356
+ if (json) {
2357
+ writeRawOutput(JSON.stringify(res.data, null, 2));
2358
+ return;
1905
2359
  }
1906
- const environments = onlyEnvironment ? [onlyEnvironment] : allEnvironments;
1907
- const results = [];
1908
- for (const environment2 of environments) {
1909
- log(`
1910
- === ${environment2} ===`);
1911
- const started = Date.now();
1912
- const claimRes = await apiRequest(
2360
+ printSandbox(res.data);
2361
+ }
2362
+ async function handleSandboxClaims(argv, apiUrl, token) {
2363
+ const json = hasFlag(argv, "--json");
2364
+ const applicationArg = getArgValue(argv, "--application") ?? getArgValue(argv, "--property");
2365
+ const applicationId = applicationArg ? await resolveApplicationId(argv, apiUrl, token) : void 0;
2366
+ const claims = [];
2367
+ let cursor = null;
2368
+ do {
2369
+ const query = new URLSearchParams({ mine: "true", status: "claiming,ready", limit: "100" });
2370
+ if (applicationId) query.set("applicationId", applicationId);
2371
+ if (cursor) query.set("cursor", cursor);
2372
+ const res = await apiRequest(
1913
2373
  apiUrl,
1914
2374
  token,
1915
- "POST",
1916
- "/org/sandboxes-v2/sandboxes",
1917
- { applicationId, environment: environment2, version }
2375
+ "GET",
2376
+ `/org/sandboxes-v2/sandboxes?${query}`
1918
2377
  );
1919
- if (!claimRes.ok) {
1920
- const seconds2 = (Date.now() - started) / 1e3;
1921
- console.error(` claim failed: ${claimRes.error}`);
1922
- results.push({ environment: environment2, ok: false, seconds: seconds2, error: claimRes.error });
1923
- continue;
1924
- }
1925
- const id = claimRes.data.id;
1926
- log(` claimed ${id}, waiting...`);
1927
- const waited = await pollSandboxUntilReady(apiUrl, token, id, CLAIM_TIMEOUT_SECONDS);
1928
- const seconds = (Date.now() - started) / 1e3;
1929
- if ("error" in waited || waited.data.status !== "ready") {
1930
- const error = "error" in waited ? waited.error : waited.data.error ?? "failed";
1931
- console.error(` failed: ${error}`);
1932
- results.push({ environment: environment2, ok: false, seconds, error });
1933
- } else {
1934
- const { data } = waited;
1935
- log(` ready in ${data.readyMs !== null ? (data.readyMs / 1e3).toFixed(1) : "?"}s`);
1936
- for (const [stepId, outputs] of Object.entries(data.seedOutputs)) {
1937
- for (const [key, value] of Object.entries(outputs)) {
1938
- log(` ${stepId}.${key}: ${JSON.stringify(value)}`);
1939
- }
1940
- }
1941
- const personas = [];
1942
- if (data.nativeLogin) {
1943
- for (const { name } of data.personas) {
1944
- const checked = await apiRequest(
1945
- apiUrl,
1946
- token,
1947
- "POST",
1948
- `/org/sandboxes-v2/sandboxes/${id}/auth-check`,
1949
- { persona: name },
1950
- // The API waits on the script (up to 60 s) and the sandbox connection.
1951
- { timeoutMs: 9e4 }
1952
- );
1953
- const check = checked.ok ? checked.data : { persona: name, ok: false, error: checked.error ?? "unknown error" };
1954
- personas.push(check);
1955
- if (check.ok) log(` persona ${name}: signed in`);
1956
- else console.error(` persona ${name}: FAILED
1957
- ${indentLines(check.error ?? "", " ")}`);
1958
- }
1959
- }
1960
- const failedPersonas = personas.filter((p) => !p.ok).map((p) => p.persona);
1961
- results.push({
1962
- environment: environment2,
1963
- ok: failedPersonas.length === 0,
1964
- seconds,
1965
- ...failedPersonas.length > 0 ? { error: `persona sign-in failed: ${failedPersonas.join(", ")}` } : {},
1966
- ...data.nativeLogin ? { personas } : {}
1967
- });
2378
+ if (!res.ok) {
2379
+ console.error(`Error: ${res.error ?? "could not list sandboxes"}`);
2380
+ process6.exit(1);
1968
2381
  }
1969
- log(` destroying ${id}...`);
1970
- await apiRequest(apiUrl, token, "DELETE", `/org/sandboxes-v2/sandboxes/${id}`);
2382
+ claims.push(...res.data);
2383
+ cursor = res.nextCursor ?? null;
2384
+ } while (cursor);
2385
+ if (json) {
2386
+ writeRawOutput(JSON.stringify(claims, null, 2));
2387
+ return;
1971
2388
  }
1972
- log("");
1973
- log("Summary:");
1974
- log(` ${pad2("Environment", 20)} ${pad2("Result", 6)} Seconds`);
1975
- for (const r of results) {
1976
- const errSuffix = r.error ? ` (${r.error})` : "";
1977
- log(` ${pad2(r.environment, 20)} ${pad2(r.ok ? "OK" : "FAIL", 6)} ${r.seconds.toFixed(1)}${errSuffix}`);
2389
+ if (claims.length === 0) {
2390
+ console.log("No live claims.");
2391
+ return;
1978
2392
  }
1979
- if (json) writeRawOutput(JSON.stringify(results, null, 2));
1980
- if (results.some((r) => !r.ok)) process4.exit(1);
1981
- }
1982
-
1983
- // src/sandbox-push.ts
1984
- import process5 from "process";
1985
- import { spawnSync as spawnSync2 } from "child_process";
1986
- import { createHash as createHash3 } from "crypto";
1987
- import { createReadStream, createWriteStream, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
1988
- import { open } from "fs/promises";
1989
- import { tmpdir as tmpdir2 } from "os";
1990
- import { dirname as dirname2, join as join2 } from "path";
1991
- import { pipeline } from "stream/promises";
1992
- import { createGunzip } from "zlib";
1993
-
1994
- // src/sandbox-push-syncable.ts
1995
- import { spawnSync } from "child_process";
1996
- import { existsSync, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync } from "fs";
1997
- import { tmpdir } from "os";
1998
- import { dirname, join, resolve } from "path";
1999
- var realDocker = (args) => {
2000
- const result = spawnSync("docker", args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
2001
- if (result.error) return { status: null, stdout: "", stderr: result.error.message };
2002
- return { status: result.status, stdout: result.stdout, stderr: result.stderr };
2003
- };
2004
- var SAMPLE = 20;
2005
- function sampleOf(paths) {
2006
- const sorted = [...paths].sort();
2007
- if (sorted.length <= SAMPLE) return sorted;
2008
- return Array.from({ length: SAMPLE }, (_, i) => sorted[Math.floor(i * sorted.length / SAMPLE)]);
2009
- }
2010
- var SYNCABLE_RULE = "a syncable image has your definition file at its WORKDIR, as `COPY . .` gives you";
2011
- function syncabilityLine(params) {
2012
- const docker2 = params.docker ?? realDocker;
2013
- const git2 = spawnSync("git", ["ls-files", "-s", "-z"], { cwd: params.definitionDir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
2014
- if (git2.status !== 0) return "Sync: not checked (the definition is not in a git repository).";
2015
- const tracked = git2.stdout.split("\0").flatMap((entry) => /^(100644|100755) [0-9a-f]+ \d\t(.*)$/s.exec(entry)?.[2] ?? []).filter((path) => path !== "cofactor.sandbox.yaml").filter((path) => existsSync(join(params.definitionDir, path)));
2016
- const temp = mkdtempSync(join(tmpdir(), "cofactor-syncable-"));
2017
- const created = docker2(["create", params.image]);
2018
- if (created.status !== 0) {
2019
- rmSync(temp, { recursive: true, force: true });
2020
- return `Sync: not checked (docker create failed: ${created.stderr.trim()}).`;
2393
+ const names = new Map((await fetchProperties(apiUrl, token)).map((p) => [p.id, p.name]));
2394
+ console.log(`${pad2("ID", 38)} ${pad2("Application", 24)} ${pad2("Environment", 14)} ${pad2("Status", 10)} ${pad2("Claimed", 26)} Expires`);
2395
+ for (const c of claims) {
2396
+ console.log(
2397
+ `${pad2(c.id, 38)} ${pad2(names.get(c.applicationId) ?? c.applicationId, 24)} ${pad2(c.environment, 14)} ${pad2(c.status, 10)} ${pad2(c.createdAt, 26)} ${c.leaseExpiresAt ?? "-"}`
2398
+ );
2021
2399
  }
2022
- const container = created.stdout.trim();
2023
- let copies = 0;
2024
- try {
2025
- const copyOut = (path) => {
2026
- const target = join(temp, String(copies++));
2027
- if (docker2(["cp", "-L", `${container}:${params.workdir}/${path}`, target]).status !== 0) return null;
2028
- return statSync(target).isFile() ? readFileSync3(target) : null;
2029
- };
2030
- const anchor = copyOut("cofactor.sandbox.yaml");
2031
- if (!anchor) {
2032
- return `Sync: not syncable: cofactor.sandbox.yaml isn't at ${params.workdir}/cofactor.sandbox.yaml (${SYNCABLE_RULE}).`;
2400
+ console.log("\nTear one down with: cofactor sandbox destroy <id>");
2401
+ }
2402
+ function coerceInputValue(raw, type) {
2403
+ switch (type) {
2404
+ case "integer": {
2405
+ if (!/^-?\d+$/.test(raw)) throw new Error(`must be an integer, got "${raw}"`);
2406
+ return Number(raw);
2033
2407
  }
2034
- const sample = sampleOf(tracked);
2035
- let differ = 0;
2036
- let missing = 0;
2037
- for (const path of sample) {
2038
- const bytes = copyOut(path);
2039
- if (!bytes) missing += 1;
2040
- else if (gitBlobId(bytes) !== gitBlobId(readFileSync3(join(params.definitionDir, path)))) differ += 1;
2408
+ case "number": {
2409
+ const value = Number(raw);
2410
+ if (Number.isNaN(value)) throw new Error(`must be a number, got "${raw}"`);
2411
+ return value;
2041
2412
  }
2042
- const anchorOlder = gitBlobId(anchor) !== gitBlobId(readFileSync3(params.definitionFile));
2043
- const notes = [
2044
- ...anchorOlder || differ > 0 ? ["the image is older than your working tree, which is fine"] : [],
2045
- ...missing > 0 ? [`${missing} of ${sample.length} sampled files aren't in the image (fine for sources it doesn't ship)`] : []
2046
- ];
2047
- return `Sync: syncable${notes.length > 0 ? ` (${notes.join("; ")})` : ""}.`;
2048
- } finally {
2049
- docker2(["rm", container]);
2050
- rmSync(temp, { recursive: true, force: true });
2413
+ case "boolean": {
2414
+ if (raw === "true") return true;
2415
+ if (raw === "false") return false;
2416
+ throw new Error(`must be "true" or "false", got "${raw}"`);
2417
+ }
2418
+ default:
2419
+ return raw;
2051
2420
  }
2052
2421
  }
2053
- function syncabilityLineOrSkipped(params) {
2054
- try {
2055
- const definitionFile = resolve(params.definitionFile);
2056
- return syncabilityLine({ ...params, definitionFile, definitionDir: dirname(definitionFile) });
2057
- } catch (error) {
2058
- return `Sync: not checked (${error instanceof Error ? error.message : String(error)}).`;
2422
+ function buildRunInputFromFlags(argv, propTypes) {
2423
+ const pairs = getArgValues(argv, "--input");
2424
+ const input = {};
2425
+ for (const pair of pairs) {
2426
+ const eq = pair.indexOf("=");
2427
+ if (eq <= 0) {
2428
+ console.error(`Error: invalid --input "${pair}". Use key=value.`);
2429
+ process6.exit(1);
2430
+ }
2431
+ const key = pair.slice(0, eq);
2432
+ const raw = pair.slice(eq + 1);
2433
+ const type = propTypes[key];
2434
+ if (!type) {
2435
+ console.error(`Error: unknown input "${key}". Known: ${Object.keys(propTypes).join(", ") || "(none)"}`);
2436
+ process6.exit(1);
2437
+ }
2438
+ try {
2439
+ input[key] = coerceInputValue(raw, type);
2440
+ } catch (error) {
2441
+ console.error(`Error: --input ${key}: ${error instanceof Error ? error.message : String(error)}`);
2442
+ process6.exit(1);
2443
+ }
2059
2444
  }
2445
+ return input;
2060
2446
  }
2061
-
2062
- // ../sandbox-helper/src/image-archive.ts
2063
- import { z as z3 } from "zod";
2064
- var MAX_BLOB_BYTES = 5 * 1024 ** 3;
2065
- var ImageBlob = z3.object({
2066
- digest: z3.string().regex(/^sha256:[0-9a-f]{64}$/),
2067
- size: z3.number().int().nonnegative()
2068
- });
2069
- var ImageBlobs = z3.object({
2070
- config: ImageBlob,
2071
- // Docker allows at most 127 layers; each costs a storage lookup per push.
2072
- layers: z3.array(ImageBlob).min(1).max(128)
2073
- });
2074
- function withSandboxLayers(team, added, created) {
2075
- return {
2076
- ...team,
2077
- rootfs: { ...team.rootfs, diff_ids: [...team.rootfs.diff_ids, ...added.map((layer) => layer.digest)] },
2078
- history: [...team.history ?? [], ...added.map(() => ({ created, created_by: "cofactor sandbox push" }))],
2079
- config: { ...team.config, Entrypoint: ["/usr/local/bin/cofactor-sandbox", "boot"], Cmd: [] }
2080
- };
2447
+ async function pollRunUntilDone(apiUrl, token, runId, timeoutMs, timeoutNote = "") {
2448
+ const deadline = Date.now() + timeoutMs;
2449
+ while (Date.now() < deadline) {
2450
+ const pollRes = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/runs/${runId}`);
2451
+ if (!pollRes.ok) {
2452
+ console.error(`Error: ${pollRes.error}`);
2453
+ process6.exit(1);
2454
+ }
2455
+ if (pollRes.data.status !== "running") return pollRes.data;
2456
+ await sleep(2e3);
2457
+ }
2458
+ console.error(`Timed out waiting for run ${runId}${timeoutNote}.`);
2459
+ process6.exit(1);
2081
2460
  }
2082
- var BLOCK = 512;
2083
- var encoder = new TextEncoder();
2084
- var decoder = new TextDecoder();
2085
- function tarHeader(name, size, mode = 420) {
2086
- if (encoder.encode(name).length > 100) throw new Error(`tar entry name too long: ${name}`);
2087
- const header = new Uint8Array(BLOCK);
2088
- const put = (offset, length, value) => header.set(encoder.encode(value).slice(0, length), offset);
2089
- const octal = (offset, length, value) => put(offset, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
2090
- put(0, 100, name);
2091
- octal(100, 8, mode);
2092
- octal(108, 8, 0);
2093
- octal(116, 8, 0);
2094
- octal(124, 12, size);
2095
- octal(136, 12, 0);
2096
- put(148, 8, " ");
2097
- put(156, 1, "0");
2098
- put(257, 6, "ustar\0");
2099
- put(263, 2, "00");
2100
- const checksum = header.reduce((sum, byte) => sum + byte, 0);
2101
- put(148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
2102
- return header;
2103
- }
2104
- function tarPadding(size) {
2105
- return new Uint8Array((BLOCK - size % BLOCK) % BLOCK);
2106
- }
2107
- var TAR_END = new Uint8Array(BLOCK * 2);
2108
- function tarOf(files) {
2109
- const parts = files.flatMap((file) => [tarHeader(file.name, file.data.length, file.mode), file.data, tarPadding(file.data.length)]);
2110
- parts.push(TAR_END);
2111
- const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
2112
- let at = 0;
2113
- for (const part of parts) {
2114
- out.set(part, at);
2115
- at += part.length;
2461
+ async function handleSandboxRun(argv, apiUrl, token) {
2462
+ const positional = argv.filter((a) => !a.startsWith("-"));
2463
+ const [id, actionRef] = positional;
2464
+ if (!id || !actionRef) {
2465
+ console.error(
2466
+ "Usage: cofactor sandbox run <id> <action|plugin.op> [--input k=v ...] [--json-input '{...}'] [--json]"
2467
+ );
2468
+ process6.exit(1);
2116
2469
  }
2117
- return out;
2118
- }
2119
- var field = (block, offset, length) => decoder.decode(block.subarray(offset, offset + length)).replace(/\0.*$/s, "");
2120
- async function tarEntries(read) {
2121
- const entries = [];
2122
- let longName = null;
2123
- for (let offset = 0; ; ) {
2124
- const block = await read(offset, BLOCK);
2125
- if (block.length < BLOCK) throw new Error(`the tar ends at byte ${offset + block.length}, before its end-of-archive block; it is truncated`);
2126
- if (block.every((byte) => byte === 0)) return entries;
2127
- const size = Number.parseInt(field(block, 124, 12).trim() || "0", 8);
2128
- if (!Number.isSafeInteger(size) || size < 0) throw new Error(`tar entry at ${offset} has an invalid size`);
2129
- const type = field(block, 156, 1) || "0";
2130
- const prefix = field(block, 345, 155);
2131
- const name = longName ?? (prefix ? `${prefix}/${field(block, 0, 100)}` : field(block, 0, 100));
2132
- const data = offset + BLOCK;
2133
- longName = null;
2134
- if (type === "x" || type === "L") {
2135
- const text = decoder.decode(await read(data, size)).replace(/\0+$/, "");
2136
- longName = type === "L" ? text : /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text)?.[1] ?? null;
2137
- } else if (type === "0") {
2138
- entries.push({ name, offset: data, size });
2470
+ const json = hasFlag(argv, "--json");
2471
+ const jsonInput = getArgValue(argv, "--json-input");
2472
+ const showRes = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
2473
+ if (!showRes.ok) {
2474
+ console.error(`Error: ${showRes.error}`);
2475
+ process6.exit(1);
2476
+ }
2477
+ const sandbox = showRes.data;
2478
+ const repoAction = sandbox.actions.find((a) => a.slug === actionRef);
2479
+ const operation = sandbox.operations.find((op) => op.name === actionRef);
2480
+ if (!repoAction && !operation) {
2481
+ console.error(`Error: unknown action or operation "${actionRef}".`);
2482
+ console.error(` Actions: ${sandbox.actions.map((a) => a.slug).join(", ") || "(none)"}`);
2483
+ console.error(` Operations: ${sandbox.operations.map((op) => op.name).join(", ") || "(none)"}`);
2484
+ process6.exit(1);
2485
+ }
2486
+ let input;
2487
+ if (jsonInput !== void 0) {
2488
+ try {
2489
+ input = JSON.parse(jsonInput);
2490
+ } catch (error) {
2491
+ console.error(`Error: invalid --json-input: ${error instanceof Error ? error.message : String(error)}`);
2492
+ process6.exit(1);
2139
2493
  }
2140
- offset = data + size + tarPadding(size).length;
2494
+ } else if (repoAction) {
2495
+ const propTypes = Object.fromEntries(Object.entries(repoAction.input).map(([key, prop]) => [key, prop.type]));
2496
+ input = buildRunInputFromFlags(argv, propTypes);
2497
+ } else {
2498
+ input = buildRunInputFromFlags(argv, jsonSchemaPropTypes(operation.inputSchema));
2499
+ }
2500
+ const enqueueRes = await apiRequest(
2501
+ apiUrl,
2502
+ token,
2503
+ "POST",
2504
+ `/org/sandboxes-v2/sandboxes/${id}/runs`,
2505
+ { action: actionRef, input }
2506
+ );
2507
+ if (!enqueueRes.ok) {
2508
+ if (enqueueRes.code === "SANDBOX_V2_ACTION_INVALID_INPUT") {
2509
+ console.error(`Error: invalid input for "${actionRef}":`);
2510
+ for (const issue of enqueueRes.issues ?? []) {
2511
+ if (typeof issue === "string") console.error(` ${issue}`);
2512
+ else console.error(` ${(issue.path ?? []).join(".")}: ${issue.message ?? ""}`);
2513
+ }
2514
+ console.error("");
2515
+ console.error("Input schema:");
2516
+ writeRawOutput(JSON.stringify(repoAction ? repoAction.input : operation.inputSchema, null, 2));
2517
+ } else {
2518
+ console.error(`Error: ${enqueueRes.error}`);
2519
+ }
2520
+ process6.exit(1);
2521
+ }
2522
+ const runId = enqueueRes.data.id;
2523
+ const run = await pollRunUntilDone(apiUrl, token, runId, ((repoAction?.timeout ?? 60) + 60) * 1e3);
2524
+ if (json) {
2525
+ writeRawOutput(JSON.stringify(run, null, 2));
2526
+ if (run.status === "failed") process6.exit(1);
2527
+ return;
2528
+ }
2529
+ if (run.status === "failed") {
2530
+ console.error(`Run failed: ${run.error ?? "unknown error"}`);
2531
+ if (run.stderrTail) {
2532
+ console.error("");
2533
+ console.error("stderr:");
2534
+ console.error(run.stderrTail);
2535
+ }
2536
+ process6.exit(1);
2141
2537
  }
2538
+ writeRawOutput(JSON.stringify(run.output ?? {}, null, 2));
2142
2539
  }
2143
-
2144
- // src/sandbox-push.ts
2145
- var USAGE = [
2146
- "Usage: cofactor sandbox push --application <name|id> <image> [--definition <file>] [--tag <tag>]... [--wait] [--json]",
2147
- "",
2148
- "Tag main builds `main` (or `nightly`): once an application has a push with either tag, its head is the",
2149
- "newest such push, and only those runs move the visual-review baseline. Other pushes are claimed with --at."
2150
- ].join("\n");
2151
- var UPLOAD_CONCURRENCY = 4;
2152
- var UPLOAD_ATTEMPTS = 3;
2153
- var HASH_CHUNK_BYTES = 8 * 1024 * 1024;
2154
- var IMPORT_TIMEOUT_MS = 50 * 6e4;
2155
- var IMPORT_POLL_MS = 5e3;
2156
- var PushError = class extends Error {
2157
- constructor(message, issues = []) {
2158
- super(message);
2159
- this.issues = issues;
2540
+ var EXEC_USAGE = "Usage: cofactor sandbox exec <id> [--timeout <seconds>] [--json] -- <command...>";
2541
+ function parseExecArgs(options, commandWords) {
2542
+ const command = commandWords.join(" ");
2543
+ if (command.trim() === "") return { ok: false, error: `a command is required after \`--\`. ${EXEC_USAGE}` };
2544
+ const id = v2Positionals("exec", options)[0];
2545
+ if (!id) return { ok: false, error: `sandbox id required. ${EXEC_USAGE}` };
2546
+ const timeout = getArgValue(options, "--timeout");
2547
+ let timeoutSeconds;
2548
+ if (timeout !== void 0) {
2549
+ timeoutSeconds = /^\d+$/.test(timeout) ? Number(timeout) : NaN;
2550
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > EXEC_MAX_TIMEOUT_SECONDS) {
2551
+ return { ok: false, error: `--timeout must be a whole number of seconds from 1 to ${EXEC_MAX_TIMEOUT_SECONDS}.` };
2552
+ }
2160
2553
  }
2161
- issues;
2162
- };
2163
- function fail(message, issues) {
2164
- throw new PushError(message, issues);
2554
+ return { ok: true, args: { id, timeoutSeconds, json: hasFlag(options, "--json"), command } };
2165
2555
  }
2166
- function docker(args, options) {
2167
- const result = spawnSync2("docker", args, {
2168
- encoding: "utf8",
2169
- stdio: ["ignore", "pipe", options.quiet ? "pipe" : "inherit"],
2170
- maxBuffer: 64 * 1024 * 1024
2171
- });
2172
- if (result.error) fail(`could not run docker (${result.error.message}); is Docker installed and running?`);
2173
- if (result.status !== 0) fail(`docker ${args.slice(0, 2).join(" ")} failed:
2174
- ${result.stderr ?? ""}`.trimEnd());
2175
- return result.stdout;
2556
+ function execExitCode(run) {
2557
+ if (run.status === "succeeded") return 0;
2558
+ return run.exitCode ?? 1;
2176
2559
  }
2177
- function imageConfigOf(image) {
2178
- const result = spawnSync2("docker", ["image", "inspect", image], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
2179
- if (result.error) fail(`could not run docker (${result.error.message}); is Docker installed and running?`);
2180
- const [inspected] = result.status === 0 ? JSON.parse(result.stdout) : [];
2181
- if (!inspected) fail(`Docker has no local image ${image}: build or pull it first`);
2182
- if (inspected.Os !== "linux" || inspected.Architecture !== "amd64") {
2183
- fail(
2184
- `${image} is ${inspected.Os}/${inspected.Architecture}; sandboxes run linux/amd64. Rebuild it with
2185
- docker buildx build --platform linux/amd64 -t ${image} --load <context>`
2186
- );
2560
+ async function handleSandboxExec(options, commandWords, apiUrl, token) {
2561
+ const parsed = parseExecArgs(options, commandWords);
2562
+ if (!parsed.ok) {
2563
+ console.error(`Error: ${parsed.error}`);
2564
+ process6.exit(1);
2187
2565
  }
2188
- const user = inspected.Config.User ?? "";
2189
- if (!["", "0", "root"].includes(user.split(":")[0])) {
2190
- fail(
2191
- `${image} runs as USER ${user}; a sandbox image must leave USER as root (the sandbox drops to uid 1000 itself). Remove the final \`USER\` line from the Dockerfile, or end it with \`USER root\`.`
2192
- );
2566
+ const { id, timeoutSeconds, json, command } = parsed.args;
2567
+ const enqueueRes = await apiRequest(
2568
+ apiUrl,
2569
+ token,
2570
+ "POST",
2571
+ `/org/sandboxes-v2/sandboxes/${id}/exec`,
2572
+ timeoutSeconds === void 0 ? { command } : { command, timeoutSeconds }
2573
+ );
2574
+ if (!enqueueRes.ok) {
2575
+ console.error(`Error: ${enqueueRes.error}`);
2576
+ process6.exit(1);
2193
2577
  }
2194
- const env = Object.fromEntries(
2195
- (inspected.Config.Env ?? []).map((entry) => {
2196
- const separator = entry.indexOf("=");
2197
- return [entry.slice(0, separator), entry.slice(separator + 1)];
2198
- })
2578
+ const runId = enqueueRes.data.id;
2579
+ const run = await pollRunUntilDone(
2580
+ apiUrl,
2581
+ token,
2582
+ runId,
2583
+ ((timeoutSeconds ?? EXEC_DEFAULT_TIMEOUT_SECONDS) + 60) * 1e3,
2584
+ "; the command may still be running in the sandbox"
2199
2585
  );
2200
- const parsed = imageConfigSchema.safeParse({ workdir: inspected.Config.WorkingDir ?? "", env });
2201
- if (!parsed.success) {
2202
- const workdir = inspected.Config.WorkingDir || "(none)";
2203
- fail(
2204
- `${image}'s WORKDIR ${workdir} can't be used: ${parsed.error.issues[0]?.message ?? "invalid"}. Set one for your app in its Dockerfile (e.g. \`WORKDIR /app\`): services, prepare commands and actions run from it, and the sandbox makes it writable by the app.`
2205
- );
2586
+ const code = execExitCode(run);
2587
+ if (json) {
2588
+ writeRawOutput(JSON.stringify(run, null, 2));
2589
+ process6.exit(code);
2206
2590
  }
2207
- return parsed.data;
2208
- }
2209
- function shellWord(value) {
2210
- return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
2211
- }
2212
- async function apiOrFail(result) {
2213
- const res = await result;
2214
- if (!res.ok) fail(res.error ?? "unknown error", res.issues);
2215
- return res.data;
2216
- }
2217
- var mib = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
2218
- async function readAt(file, offset, size) {
2219
- const bytes = new Uint8Array(size);
2220
- for (let at = 0; at < size; ) {
2221
- const { bytesRead } = await file.read(bytes, at, size - at, offset + at);
2222
- if (bytesRead === 0) return bytes.subarray(0, at);
2223
- at += bytesRead;
2591
+ if (run.status === "failed" && run.exitCode === null) {
2592
+ process6.stderr.write(`Error: ${run.error ?? "unknown error"}
2593
+ `);
2594
+ if (run.stdoutTail) process6.stderr.write(run.stdoutTail);
2595
+ if (run.stdoutTail && !run.stdoutTail.endsWith("\n") && run.stderrTail) process6.stderr.write("\n");
2596
+ if (run.stderrTail) process6.stderr.write(run.stderrTail);
2597
+ process6.exit(1);
2224
2598
  }
2225
- return bytes;
2599
+ if (run.stdoutTail) process6.stdout.write(run.stdoutTail);
2600
+ if (run.stderrTail) process6.stderr.write(run.stderrTail);
2601
+ if (code === 124 && run.error) process6.stderr.write(`
2602
+ ${run.error}
2603
+ `);
2604
+ process6.exit(code);
2226
2605
  }
2227
- async function digestOf(file, offset, size) {
2228
- const hash = createHash3("sha256");
2229
- for (let at = 0; at < size; at += HASH_CHUNK_BYTES) {
2230
- hash.update(await readAt(file, offset + at, Math.min(HASH_CHUNK_BYTES, size - at)));
2606
+ async function handleSandboxDestroy(argv, apiUrl, token) {
2607
+ const [id] = argv.filter((a) => !a.startsWith("-"));
2608
+ if (!id) {
2609
+ console.error("Usage: cofactor sandbox destroy <id>");
2610
+ process6.exit(1);
2231
2611
  }
2232
- return `sha256:${hash.digest("hex")}`;
2612
+ const local = trackedLocalSandbox(id, apiUrl);
2613
+ const res = await apiRequest(
2614
+ apiUrl,
2615
+ token,
2616
+ "DELETE",
2617
+ `/org/sandboxes-v2/sandboxes/${id}`
2618
+ );
2619
+ if (!res.ok) {
2620
+ console.error(`Error: ${res.error}`);
2621
+ process6.exit(1);
2622
+ }
2623
+ console.log(`Sandbox ${id} destroying.`);
2624
+ if (local) await cleanupLocalRefs(apiUrl, token, local.applicationId, local.applicationId, { id, version: local.version });
2233
2625
  }
2234
- function partOf(bytes) {
2235
- return { digest: `sha256:${createHash3("sha256").update(bytes).digest("hex")}`, size: bytes.length, body: () => bytes };
2236
- }
2237
- async function putBlob(url, headers, part) {
2238
- const digest = part.digest;
2239
- for (let attempt = 1; ; attempt++) {
2240
- let response;
2241
- try {
2242
- const init = {
2243
- method: "PUT",
2244
- headers: { ...headers, "content-length": String(part.size) },
2245
- body: part.body(),
2246
- duplex: "half"
2247
- };
2248
- response = await fetch(url, init);
2249
- } catch (error) {
2250
- if (attempt >= UPLOAD_ATTEMPTS) throw error;
2251
- await new Promise((resolve3) => setTimeout(resolve3, 2e3 * attempt));
2252
- continue;
2253
- }
2254
- if (response.ok) return;
2255
- const detail = `${response.status}: ${(await response.text()).slice(0, 300)}`;
2256
- if (response.status < 500 || attempt >= UPLOAD_ATTEMPTS) {
2257
- fail(`uploading ${digest} was refused (HTTP ${detail}); run the push again to upload what is still missing`);
2258
- }
2259
- await new Promise((resolve3) => setTimeout(resolve3, 2e3 * attempt));
2260
- }
2261
- }
2262
- async function savedImage(archive) {
2263
- const file = await open(archive);
2264
- try {
2265
- const entries = await tarEntries((offset, length) => readAt(file, offset, length)).catch(
2266
- (error) => fail(`reading the saved image: ${error instanceof Error ? error.message : String(error)}`)
2267
- );
2268
- const byName = new Map(entries.map((entry) => [entry.name.replace(/^\.\//, ""), entry]));
2269
- const entryOf = (name) => {
2270
- const entry = byName.get(name);
2271
- if (!entry) fail(`the saved image has no ${name}; is this a \`docker save\` archive?`);
2272
- return entry;
2273
- };
2274
- const json = async (name) => {
2275
- const entry = entryOf(name);
2276
- return JSON.parse(new TextDecoder().decode(await readAt(file, entry.offset, entry.size)));
2277
- };
2278
- const layerOf = async (name) => {
2279
- const entry = entryOf(name);
2280
- const tooLarge = (bytes) => fail(`a layer of the image is ${(bytes / 1024 ** 3).toFixed(1)} GB; a push takes at most 5 GB per layer. Slim the image.`);
2281
- if (entry.size > MAX_BLOB_BYTES) tooLarge(entry.size);
2282
- const range = { start: entry.offset, end: entry.offset + entry.size - 1 };
2283
- const [first, second] = await readAt(file, entry.offset, 2);
2284
- if (first === 31 && second === 139) {
2285
- const unpacked = join2(dirname2(archive), `layer-${entry.offset}.tar`);
2286
- await pipeline(createReadStream(archive, range), createGunzip(), createWriteStream(unpacked)).catch(
2287
- (error) => fail(`unpacking a compressed layer of the image: ${error instanceof Error ? error.message : String(error)}`)
2288
- );
2289
- const plain = await open(unpacked);
2290
- try {
2291
- const { size } = await plain.stat();
2292
- if (size > MAX_BLOB_BYTES) tooLarge(size);
2293
- return { digest: await digestOf(plain, 0, size), size, body: () => createReadStream(unpacked) };
2294
- } finally {
2295
- await plain.close();
2296
- }
2297
- }
2298
- return { digest: await digestOf(file, entry.offset, entry.size), size: entry.size, body: () => createReadStream(archive, range) };
2299
- };
2300
- const [manifest] = await json("manifest.json");
2301
- if (!manifest) fail("the saved image lists no image");
2302
- const layers = [];
2303
- for (const name of manifest.Layers) layers.push(await layerOf(name));
2304
- return { config: await json(manifest.Config), layers };
2305
- } finally {
2306
- await file.close();
2307
- }
2308
- }
2309
- async function pushedImage(archive, cofactorLayers, definitionYaml, config) {
2310
- const saved = await savedImage(archive);
2311
- const own = partOf(
2312
- tarOf([
2313
- { name: IMAGE_DEFINITION_FILE.slice(1), data: new TextEncoder().encode(definitionYaml) },
2314
- { name: IMAGE_CONFIG_FILE.slice(1), data: new TextEncoder().encode(JSON.stringify(config)) }
2315
- ])
2316
- );
2317
- const added = [...cofactorLayers, own];
2318
- const pushed = withSandboxLayers(saved.config, added, (/* @__PURE__ */ new Date()).toISOString());
2319
- const imageConfig = partOf(new TextEncoder().encode(JSON.stringify(pushed)));
2320
- const blob = ({ digest, size }) => ({ digest, size });
2321
- return {
2322
- config: blob(imageConfig),
2323
- layers: [...saved.layers, ...added].map(blob),
2324
- blobs: new Map([imageConfig, ...saved.layers, own].map((part) => [part.digest, part]))
2325
- };
2326
- }
2327
- async function handleSandboxPush(argv, apiUrl, token) {
2328
- try {
2329
- await push(argv, apiUrl, token);
2330
- } catch (error) {
2331
- if (!(error instanceof PushError)) throw error;
2332
- console.error(`Error: ${error.message}`);
2333
- for (const issue of error.issues) console.error(` ${issue.path || "(root)"}: ${issue.message}`);
2334
- process5.exit(1);
2335
- }
2336
- }
2337
- async function push(argv, apiUrl, token) {
2338
- const valueFlags = /* @__PURE__ */ new Set(["--application", "--property", "--definition", "--tag", "--env", "--org", "--token", "--api-url", "--app-url"]);
2339
- const positional = argv.filter((arg, i) => !arg.startsWith("-") && !valueFlags.has(argv[i - 1] ?? ""));
2340
- const image = positional[0];
2341
- if (!image) fail(`an image is required
2342
- ${USAGE}`);
2343
- const definitionFile = getArgValue(argv, "--definition") ?? "./cofactor.sandbox.yaml";
2344
- const tags = getArgValues(argv, "--tag");
2345
- const badTag = tags.find((tag) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(tag));
2346
- if (badTag !== void 0) fail(`--tag ${badTag}: use letters, digits, '.', '_' and '-' (at most 64, starting with a letter or digit)`);
2347
- if (tags.length > 10) fail("a push takes at most 10 tags");
2348
- const wait = hasFlag(argv, "--wait");
2626
+ async function handleSandboxVerifyEnv(argv, apiUrl, token) {
2627
+ const applicationId = await resolveApplicationId(argv, apiUrl, token);
2628
+ const onlyEnvironment = getArgValue(argv, "--environment");
2349
2629
  const json = hasFlag(argv, "--json");
2350
2630
  const log = (line) => json ? console.error(line) : console.log(line);
2351
- let definitionYaml;
2352
- try {
2353
- definitionYaml = readFileSync4(definitionFile, "utf8");
2354
- } catch (error) {
2355
- fail(`reading ${definitionFile}: ${error instanceof Error ? error.message : String(error)}`);
2356
- }
2357
- const { definition, issues } = validateDefinition(definitionYaml, "upload");
2358
- if (!definition || issues.length > 0) fail(`${definitionFile} is not a valid definition for pushed images:`, issues);
2359
- const storageProvider = definition.object_storage?.provider;
2360
- const applicationId = await resolveOrCreateSandboxApplication(argv, apiUrl, token);
2361
- const config = imageConfigOf(image);
2362
- log(syncabilityLineOrSkipped({ image, workdir: config.workdir, definitionFile }));
2363
- const work = mkdtempSync2(join2(tmpdir2(), "cofactor-push-"));
2364
- const cleanUp = () => rmSync2(work, { recursive: true, force: true });
2365
- const interrupted = (signal) => {
2366
- cleanUp();
2367
- process5.exit(signal === "SIGINT" ? 130 : 143);
2368
- };
2369
- process5.once("SIGINT", interrupted);
2370
- process5.once("SIGTERM", interrupted);
2371
- process5.exitCode = 1;
2372
- try {
2373
- const ours = await apiOrFail(
2374
- apiRequest(
2375
- apiUrl,
2376
- token,
2377
- "GET",
2378
- "/org/sandboxes-v2/helper-layer"
2379
- )
2380
- );
2381
- if (!ours.helper) fail("this Cofactor takes pushes from an older cofactor CLI; use the version it ships with");
2382
- const cofactorLayers = [ours.helper, ...storageProvider ? [ours.objectStorage[storageProvider]] : []];
2383
- const archive = join2(work, "image.tar");
2384
- log("Saving the image...");
2385
- docker(["save", "--output", archive, image], { quiet: json });
2386
- const pushing = await pushedImage(archive, cofactorLayers, definitionYaml, config);
2387
- const { layers } = pushing;
2388
- const created = await apiOrFail(
2389
- apiRequest(
2390
- apiUrl,
2391
- token,
2392
- "POST",
2393
- `/org/applications/${applicationId}/sandbox/uploads`,
2394
- { definitionYaml, platformVersion: ours.platformVersion, tags, image: { config: pushing.config, layers } }
2395
- )
2396
- );
2397
- const partsByDigest = pushing.blobs;
2398
- const uploadBytes = created.uploads.reduce((total, upload) => total + (partsByDigest.get(upload.digest)?.size ?? 0), 0);
2399
- const imageBytes = layers.reduce((total, layer) => total + layer.size, 0);
2400
- log(`Pushing ${created.id}: uploading ${created.uploads.length} of ${layers.length + 1} blobs (${mib(uploadBytes)}); ${mib(imageBytes - uploadBytes)} already in Cofactor`);
2401
- const queue = [...created.uploads];
2402
- let sent = 0;
2403
- await Promise.all(
2404
- Array.from({ length: UPLOAD_CONCURRENCY }, async () => {
2405
- for (let next = queue.shift(); next; next = queue.shift()) {
2406
- const part = partsByDigest.get(next.digest);
2407
- if (!part) fail(`Cofactor asked for ${next.digest}, which this image doesn't have`);
2408
- await putBlob(next.url, next.headers, part);
2409
- sent += part.size;
2410
- if (part.size > 10 * 1024 * 1024) log(` ${mib(sent)} of ${mib(uploadBytes)}`);
2411
- }
2412
- })
2631
+ const app = await fetchApplicationSandbox(
2632
+ apiUrl,
2633
+ token,
2634
+ applicationId,
2635
+ getArgValue(argv, "--at")
2636
+ );
2637
+ if (!app.version || !app.definition) {
2638
+ console.error(
2639
+ "Error: no sandbox definition at the default version. For GitHub, push it and use --at <sha>; for images, run `cofactor sandbox push`."
2413
2640
  );
2414
- rmSync2(archive);
2415
- let pushed = await apiOrFail(
2416
- apiRequest(
2417
- apiUrl,
2418
- token,
2419
- "POST",
2420
- `/org/applications/${applicationId}/sandbox/uploads/${created.id}/complete`
2421
- )
2641
+ process6.exit(1);
2642
+ }
2643
+ const version = app.version;
2644
+ const allEnvironments = app.definition.environments.map((e) => e.name);
2645
+ if (onlyEnvironment && !allEnvironments.includes(onlyEnvironment)) {
2646
+ console.error(`Error: environment "${onlyEnvironment}" not found. Known: ${allEnvironments.join(", ")}`);
2647
+ process6.exit(1);
2648
+ }
2649
+ const environments = onlyEnvironment ? [onlyEnvironment] : allEnvironments;
2650
+ const results = [];
2651
+ for (const environment2 of environments) {
2652
+ log(`
2653
+ === ${environment2} ===`);
2654
+ const started = Date.now();
2655
+ const claimRes = await apiRequest(
2656
+ apiUrl,
2657
+ token,
2658
+ "POST",
2659
+ "/org/sandboxes-v2/sandboxes",
2660
+ { applicationId, environment: environment2, version }
2422
2661
  );
2423
- if (wait) {
2424
- log("Importing (Cofactor boots the image until its services are healthy)...");
2425
- const deadline = Date.now() + IMPORT_TIMEOUT_MS;
2426
- const importStarted = Date.now();
2427
- let reported = importStarted;
2428
- while ((pushed.status === "importing" || pushed.status === "uploading") && Date.now() < deadline) {
2429
- await new Promise((resolve3) => setTimeout(resolve3, IMPORT_POLL_MS));
2430
- if (Date.now() - reported >= 6e4) {
2431
- reported = Date.now();
2432
- log(` still importing (${Math.round((reported - importStarted) / 6e4)} min)`);
2433
- }
2434
- pushed = await apiOrFail(
2435
- apiRequest(apiUrl, token, "GET", `/org/applications/${applicationId}/sandbox/uploads/${created.id}`)
2436
- );
2437
- }
2662
+ if (!claimRes.ok) {
2663
+ const seconds2 = (Date.now() - started) / 1e3;
2664
+ console.error(` claim failed: ${claimRes.error}`);
2665
+ results.push({ environment: environment2, ok: false, seconds: seconds2, error: claimRes.error });
2666
+ continue;
2438
2667
  }
2439
- const uploaded = { blobs: created.uploads.length, of: layers.length + 1, bytes: uploadBytes, reusedBytes: imageBytes - uploadBytes };
2440
- const application = shellWord(getArgValue(argv, "--application") ?? getArgValue(argv, "--property") ?? applicationId);
2441
- const target = ["--env", "--org"].map((flag) => {
2442
- const value = getArgValue(process5.argv, flag);
2443
- return value ? ` ${flag} ${shellWord(value)}` : "";
2444
- }).join("");
2445
- if (json) {
2446
- writeRawOutput(JSON.stringify({ ...pushed, uploaded }, null, 2));
2668
+ const id = claimRes.data.id;
2669
+ log(` claimed ${id}, waiting...`);
2670
+ const waited = await pollSandboxUntilReady(apiUrl, token, id, CLAIM_TIMEOUT_SECONDS);
2671
+ const seconds = (Date.now() - started) / 1e3;
2672
+ if ("error" in waited || waited.data.status !== "ready") {
2673
+ const error = "error" in waited ? waited.error : waited.data.error ?? "failed";
2674
+ console.error(` failed: ${error}`);
2675
+ results.push({ environment: environment2, ok: false, seconds, error });
2447
2676
  } else {
2448
- console.log(`Push ${pushed.id}: ${pushed.status}${pushed.tags.length > 0 ? ` [${pushed.tags.join(", ")}]` : ""}`);
2449
- if (pushed.status === "ready") {
2450
- console.log(` Claim it: cofactor sandbox claim --application ${application} --environment default --wait${target}`);
2451
- } else if (pushed.status !== "failed") {
2452
- console.log(` Importing; follow it with: cofactor sandbox envs --application ${application}${target}`);
2677
+ const { data } = waited;
2678
+ log(` ready in ${data.readyMs !== null ? (data.readyMs / 1e3).toFixed(1) : "?"}s`);
2679
+ for (const [stepId, outputs] of Object.entries(data.seedOutputs)) {
2680
+ for (const [key, value] of Object.entries(outputs)) {
2681
+ log(` ${stepId}.${key}: ${JSON.stringify(value)}`);
2682
+ }
2453
2683
  }
2454
- }
2455
- if (pushed.status === "failed") fail(`push ${pushed.id} failed:
2456
- ${pushed.error ?? "no reason given"}`);
2457
- process5.exitCode = 0;
2458
- } finally {
2459
- process5.off("SIGINT", interrupted);
2460
- process5.off("SIGTERM", interrupted);
2461
- cleanUp();
2462
- }
2463
- }
2464
-
2465
- // src/sandbox-sync.ts
2466
- import { spawnSync as spawnSync3 } from "child_process";
2467
- import process6 from "process";
2468
- import { copyFileSync, existsSync as existsSync2, lstatSync, mkdtempSync as mkdtempSync3, readFileSync as readFileSync5, realpathSync, rmSync as rmSync3 } from "fs";
2469
- import { tmpdir as tmpdir3 } from "os";
2470
- import { join as join3, relative, resolve as resolve2, sep } from "path";
2471
- var SyncError = class extends Error {
2472
- };
2473
- function git(args, options) {
2474
- const result = spawnSync3("git", args, {
2475
- cwd: options.cwd,
2476
- env: options.env ?? process6.env,
2477
- input: options.input,
2478
- encoding: "utf8",
2479
- maxBuffer: 1 << 30
2480
- // Node defaults to 1 MiB; this repo's `ls-files -s -z` alone is 1.2 MB
2481
- });
2482
- if (result.error) throw new SyncError(`git ${args[0]} failed: ${result.error.message}`);
2483
- if (!(options.okCodes ?? [0]).includes(result.status ?? -1)) {
2484
- throw new SyncError(`git ${args[0]} failed (${result.signal ?? result.status}): ${result.stderr.trim()}`);
2485
- }
2486
- return result.stdout;
2487
- }
2488
- var toPosix = (path) => path.split(sep).join("/");
2489
- function repositoryOf(dir) {
2490
- const top = git(["rev-parse", "--show-toplevel"], { cwd: dir }).trim();
2491
- const gitDir = git(["rev-parse", "--absolute-git-dir"], { cwd: dir }).trim();
2492
- return { top: realpathSync(top), gitDir };
2493
- }
2494
- function indexEntries(output2, strip, top) {
2495
- const files = [];
2496
- const skipped = [];
2497
- for (const entry of output2.split("\0")) {
2498
- if (entry === "") continue;
2499
- const match = /^(\d{6}) ([0-9a-f]+) \d\t(.*)$/s.exec(entry);
2500
- if (!match) throw new SyncError(`unexpected git ls-files line: ${entry}`);
2501
- const [, mode, blob, repoPath] = match;
2502
- const path = strip === "" ? repoPath : repoPath.slice(strip.length + 1);
2503
- const onDisk = mode === "100644" || mode === "100755" ? lstatOrNull(join3(top, repoPath)) : null;
2504
- if (onDisk?.isFile()) files.push({ path, blob, mode: mode === "100755" ? 493 : 420, size: onDisk.size });
2505
- else skipped.push(path);
2684
+ const personas = [];
2685
+ if (data.nativeLogin) {
2686
+ for (const { name } of data.personas) {
2687
+ const checked = await apiRequest(
2688
+ apiUrl,
2689
+ token,
2690
+ "POST",
2691
+ `/org/sandboxes-v2/sandboxes/${id}/auth-check`,
2692
+ { persona: name },
2693
+ // The API waits on the script (up to 60 s) and the sandbox connection.
2694
+ { timeoutMs: 9e4 }
2695
+ );
2696
+ const check = checked.ok ? checked.data : { persona: name, ok: false, error: checked.error ?? "unknown error" };
2697
+ personas.push(check);
2698
+ if (check.ok) log(` persona ${name}: signed in`);
2699
+ else console.error(` persona ${name}: FAILED
2700
+ ${indentLines(check.error ?? "", " ")}`);
2701
+ }
2702
+ }
2703
+ const failedPersonas = personas.filter((p) => !p.ok).map((p) => p.persona);
2704
+ results.push({
2705
+ environment: environment2,
2706
+ ok: failedPersonas.length === 0,
2707
+ seconds,
2708
+ ...failedPersonas.length > 0 ? { error: `persona sign-in failed: ${failedPersonas.join(", ")}` } : {},
2709
+ ...data.nativeLogin ? { personas } : {}
2710
+ });
2711
+ }
2712
+ log(` destroying ${id}...`);
2713
+ await apiRequest(apiUrl, token, "DELETE", `/org/sandboxes-v2/sandboxes/${id}`);
2506
2714
  }
2507
- return { files, skipped };
2715
+ log("");
2716
+ log("Summary:");
2717
+ log(` ${pad2("Environment", 20)} ${pad2("Result", 6)} Seconds`);
2718
+ for (const r of results) {
2719
+ const errSuffix = r.error ? ` (${r.error})` : "";
2720
+ log(` ${pad2(r.environment, 20)} ${pad2(r.ok ? "OK" : "FAIL", 6)} ${r.seconds.toFixed(1)}${errSuffix}`);
2721
+ }
2722
+ if (json) writeRawOutput(JSON.stringify(results, null, 2));
2723
+ if (results.some((r) => !r.ok)) process6.exit(1);
2508
2724
  }
2509
- function workingTree(root) {
2510
- const { top, gitDir } = repositoryOf(root);
2511
- const prefix = toPosix(relative(top, realpathSync(root)));
2512
- const temp = mkdtempSync3(join3(tmpdir3(), "cofactor-sync-"));
2513
- const cleanup = () => rmSync3(temp, { recursive: true, force: true });
2514
- try {
2515
- const index = join3(temp, "index");
2516
- const env = { ...process6.env, GIT_INDEX_FILE: index };
2517
- const userIndex = join3(gitDir, "index");
2518
- if (existsSync2(userIndex)) copyFileSync(userIndex, index);
2519
- else if (git(["rev-parse", "--verify", "-q", "HEAD"], { cwd: top, okCodes: [0, 1] }).trim() !== "") git(["read-tree", "HEAD"], { cwd: top, env });
2520
- const pathspec = prefix === "" ? "." : `:(literal)${prefix}`;
2521
- git(["add", "-A", "--", pathspec], { cwd: top, env });
2522
- const entries = indexEntries(git(["ls-files", "-s", "-z", "--", pathspec], { cwd: top, env }), prefix, top);
2523
- const files = withRawBlobs(entries.files, top, prefix);
2524
- const skipped = entries.skipped;
2525
- const tree = git(prefix === "" ? ["write-tree"] : ["write-tree", `--prefix=${prefix}/`], { cwd: top, env }).trim();
2526
- const untracked = new Set(
2527
- git(["ls-files", "--others", "--exclude-standard", "-z", "--", pathspec], { cwd: top }).split("\0").filter(Boolean).map((path) => prefix === "" ? path : path.slice(prefix.length + 1))
2528
- );
2529
- return { files, skipped, untracked, tree, repo: top, prefix, env, cleanup };
2530
- } catch (error) {
2531
- cleanup();
2532
- throw error;
2725
+ async function handleSandboxAuthCheck(argv, apiUrl, token) {
2726
+ const id = v2Positionals("auth-check", argv)[0];
2727
+ const persona = getArgValue(argv, "--persona");
2728
+ if (!id || !persona) {
2729
+ console.error("Usage: cofactor sandbox auth-check <id> --persona <name> [--json]");
2730
+ process6.exit(1);
2533
2731
  }
2732
+ const checked = await apiRequest(
2733
+ apiUrl,
2734
+ token,
2735
+ "POST",
2736
+ `/org/sandboxes-v2/sandboxes/${encodeURIComponent(id)}/auth-check`,
2737
+ { persona },
2738
+ { timeoutMs: 9e4 }
2739
+ );
2740
+ exitOnRefusal(checked);
2741
+ if (hasFlag(argv, "--json")) writeRawOutput(JSON.stringify(checked.data, null, 2));
2742
+ else if (checked.data.ok) console.log(`Persona ${persona}: signed in`);
2743
+ else console.error(`Persona ${persona}: FAILED
2744
+ ${checked.data.error ?? "unknown error"}`);
2745
+ if (!checked.data.ok) process6.exit(1);
2534
2746
  }
2535
- function buildOutput(dir, to) {
2536
- const { gitDir } = repositoryOf(dir);
2537
- const temp = mkdtempSync3(join3(tmpdir3(), "cofactor-sync-"));
2747
+
2748
+ // src/sandbox-push.ts
2749
+ import process7 from "process";
2750
+ import { spawnSync as spawnSync4 } from "child_process";
2751
+ import { createHash as createHash4 } from "crypto";
2752
+ import { createReadStream, createWriteStream, mkdtempSync as mkdtempSync3, readFileSync as readFileSync6, rmSync as rmSync4 } from "fs";
2753
+ import { open } from "fs/promises";
2754
+ import { tmpdir as tmpdir3 } from "os";
2755
+ import { dirname as dirname2, join as join4 } from "path";
2756
+ import { pipeline } from "stream/promises";
2757
+ import { createGunzip } from "zlib";
2758
+
2759
+ // src/sandbox-push-syncable.ts
2760
+ import { spawnSync as spawnSync3 } from "child_process";
2761
+ import { existsSync as existsSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync as rmSync3, statSync } from "fs";
2762
+ import { tmpdir as tmpdir2 } from "os";
2763
+ import { dirname, join as join3, resolve as resolve3 } from "path";
2764
+ var realDocker = (args) => {
2765
+ const result = spawnSync3("docker", args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
2766
+ if (result.error) return { status: null, stdout: "", stderr: result.error.message };
2767
+ return { status: result.status, stdout: result.stdout, stderr: result.stderr };
2768
+ };
2769
+ var SAMPLE = 20;
2770
+ function sampleOf(paths) {
2771
+ const sorted = [...paths].sort();
2772
+ if (sorted.length <= SAMPLE) return sorted;
2773
+ return Array.from({ length: SAMPLE }, (_, i) => sorted[Math.floor(i * sorted.length / SAMPLE)]);
2774
+ }
2775
+ var SYNCABLE_RULE = "a syncable image has your definition file at its WORKDIR, as `COPY . .` gives you";
2776
+ function syncabilityLine(params) {
2777
+ const docker2 = params.docker ?? realDocker;
2778
+ const git3 = spawnSync3("git", ["ls-files", "-s", "-z"], { cwd: params.definitionDir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
2779
+ if (git3.status !== 0) return "Sync: not checked (the definition is not in a git repository).";
2780
+ const tracked = git3.stdout.split("\0").flatMap((entry) => /^(100644|100755) [0-9a-f]+ \d\t(.*)$/s.exec(entry)?.[2] ?? []).filter((path) => path !== "cofactor.sandbox.yaml").filter((path) => existsSync3(join3(params.definitionDir, path)));
2781
+ const temp = mkdtempSync2(join3(tmpdir2(), "cofactor-syncable-"));
2782
+ const created = docker2(["create", params.image]);
2783
+ if (created.status !== 0) {
2784
+ rmSync3(temp, { recursive: true, force: true });
2785
+ return `Sync: not checked (docker create failed: ${created.stderr.trim()}).`;
2786
+ }
2787
+ const container = created.stdout.trim();
2788
+ let copies = 0;
2538
2789
  try {
2539
- const env = { ...process6.env, GIT_INDEX_FILE: join3(temp, "index"), GIT_DIR: gitDir, GIT_WORK_TREE: realpathSync(dir) };
2540
- git(["add", "-A", "-f", "--", "."], { cwd: dir, env });
2541
- const entries = indexEntries(git(["ls-files", "-s", "-z"], { cwd: dir, env }), "", realpathSync(dir));
2542
- const files = withRawBlobs(entries.files, realpathSync(dir), "", env);
2543
- const skipped = entries.skipped;
2544
- const tree = git(["write-tree"], { cwd: dir, env }).trim();
2545
- return { files: files.map((file) => ({ ...file, path: `${to}/${file.path}` })), skipped: skipped.map((path) => `${to}/${path}`), tree };
2790
+ const copyOut = (path) => {
2791
+ const target = join3(temp, String(copies++));
2792
+ if (docker2(["cp", "-L", `${container}:${params.workdir}/${path}`, target]).status !== 0) return null;
2793
+ return statSync(target).isFile() ? readFileSync5(target) : null;
2794
+ };
2795
+ const anchor = copyOut("cofactor.sandbox.yaml");
2796
+ if (!anchor) {
2797
+ return `Sync: not syncable: cofactor.sandbox.yaml isn't at ${params.workdir}/cofactor.sandbox.yaml (${SYNCABLE_RULE}).`;
2798
+ }
2799
+ const sample = sampleOf(tracked);
2800
+ let differ = 0;
2801
+ let missing = 0;
2802
+ for (const path of sample) {
2803
+ const bytes = copyOut(path);
2804
+ if (!bytes) missing += 1;
2805
+ else if (gitBlobId(bytes) !== gitBlobId(readFileSync5(join3(params.definitionDir, path)))) differ += 1;
2806
+ }
2807
+ const anchorOlder = gitBlobId(anchor) !== gitBlobId(readFileSync5(params.definitionFile));
2808
+ const notes = [
2809
+ ...anchorOlder || differ > 0 ? ["the image is older than your working tree, which is fine"] : [],
2810
+ ...missing > 0 ? [`${missing} of ${sample.length} sampled files aren't in the image (fine for sources it doesn't ship)`] : []
2811
+ ];
2812
+ return `Sync: syncable${notes.length > 0 ? ` (${notes.join("; ")})` : ""}.`;
2546
2813
  } finally {
2814
+ docker2(["rm", container]);
2547
2815
  rmSync3(temp, { recursive: true, force: true });
2548
2816
  }
2549
2817
  }
2550
- var FILTER_ATTRIBUTES = ["filter", "text", "eol", "ident", "working-tree-encoding"];
2551
- function withRawBlobs(files, top, prefix, env) {
2552
- if (files.length === 0) return files;
2553
- const options = { cwd: top, ...env ? { env } : {} };
2554
- const repoPaths = files.map((file) => prefix === "" ? file.path : `${prefix}/${file.path}`);
2555
- const autocrlf = git(["config", "--get", "core.autocrlf"], { ...options, okCodes: [0, 1] }).trim();
2556
- const converts = autocrlf === "input" || autocrlf !== "" && git(["config", "--get", "--type=bool", "core.autocrlf"], options).trim() === "true";
2557
- let filtered;
2558
- if (converts) {
2559
- filtered = new Set(repoPaths);
2560
- } else {
2561
- const out = git(["check-attr", "-z", "--stdin", ...FILTER_ATTRIBUTES], { ...options, input: repoPaths.map((path) => `${path}\0`).join("") }).split("\0");
2562
- filtered = /* @__PURE__ */ new Set();
2563
- for (let i = 0; i + 2 < out.length; i += 3) if (out[i + 2] !== "unspecified") filtered.add(out[i]);
2564
- }
2565
- return files.map(
2566
- (file, i) => filtered.has(repoPaths[i]) ? { ...file, blob: gitBlobId(readFileSync5(join3(top, repoPaths[i]))) } : file
2567
- );
2568
- }
2569
- function lstatOrNull(path) {
2818
+ function syncabilityLineOrSkipped(params) {
2570
2819
  try {
2571
- return lstatSync(path);
2820
+ const definitionFile2 = resolve3(params.definitionFile);
2821
+ return syncabilityLine({ ...params, definitionFile: definitionFile2, definitionDir: dirname(definitionFile2) });
2572
2822
  } catch (error) {
2573
- if (error.code === "ENOENT") return null;
2574
- throw error;
2823
+ return `Sync: not checked (${error instanceof Error ? error.message : String(error)}).`;
2575
2824
  }
2576
2825
  }
2577
- function chunked(items, fn) {
2578
- for (let i = 0; i < items.length; i += 200) fn(items.slice(i, i + 200));
2579
- }
2580
- function ignoredPaths(repo, env, paths) {
2581
- if (paths.length === 0) return /* @__PURE__ */ new Set();
2582
- const out = git(["check-ignore", "-z", "--stdin"], { cwd: repo, env, input: paths.map((p) => `${p}\0`).join(""), okCodes: [0, 1] });
2583
- return new Set(out.split("\0").filter(Boolean));
2584
- }
2585
- function everTracked(repo, pathspecs) {
2586
- const seen = /* @__PURE__ */ new Set();
2587
- if (pathspecs.length === 0) return seen;
2588
- if (git(["rev-parse", "--verify", "-q", "HEAD"], { cwd: repo, okCodes: [0, 1] }).trim() === "") return seen;
2589
- chunked(pathspecs, (chunk) => {
2590
- const out = git(["log", "--all", "--format=", "--name-only", "-z", "--", ...chunk.map((p) => `:(literal)${p}`)], { cwd: repo });
2591
- for (const path of out.split("\0")) {
2592
- const trimmed = path.replace(/^\n+/, "");
2593
- if (trimmed) seen.add(trimmed);
2594
- }
2595
- });
2596
- return seen;
2597
- }
2598
- async function chooseDeletions(params) {
2599
- const repoPath = (path) => params.prefix === "" ? path : `${params.prefix}/${path}`;
2600
- const ignored = ignoredPaths(params.repo, params.env, params.copyOnly.map((entry) => repoPath(entry.path)));
2601
- const candidates = params.copyOnly.filter((entry) => !ignored.has(repoPath(entry.path)));
2602
- const history = everTracked(params.repo, candidates.map((entry) => repoPath(entry.path)));
2603
- const kept = params.copyOnly.filter((entry) => ignored.has(repoPath(entry.path))).map((entry) => ({ ...entry, why: "ignored here" }));
2604
- const remove = [];
2605
- for (const entry of candidates) {
2606
- if (!entry.path.endsWith("/")) {
2607
- if (history.has(repoPath(entry.path))) remove.push(entry.path);
2608
- else kept.push({ ...entry, why: "never tracked here" });
2609
- continue;
2610
- }
2611
- const tracked = [...history].some((path) => path.startsWith(repoPath(entry.path)));
2612
- const listed = tracked ? await params.listDir(entry.path.slice(0, -1)) : [];
2613
- const ignoredInside = ignoredPaths(params.repo, params.env, listed.map(repoPath));
2614
- const files = listed.filter((path) => !ignoredInside.has(repoPath(path)) && history.has(repoPath(path)));
2615
- remove.push(...files);
2616
- if (files.length < entry.files) kept.push({ path: entry.path, files: entry.files - files.length, why: "never tracked here" });
2826
+
2827
+ // src/sandbox-push.ts
2828
+ var USAGE = [
2829
+ "Usage: cofactor sandbox push --application <name|id> <image> [--definition <file>] [--tag <tag>]... [--wait] [--json]",
2830
+ "",
2831
+ "Tag main builds `main` (or `nightly`): once an application has a push with either tag, its head is the",
2832
+ "newest such push, and only those runs move the visual-review baseline. Other pushes are claimed with --at."
2833
+ ].join("\n");
2834
+ var UPLOAD_CONCURRENCY = 4;
2835
+ var UPLOAD_ATTEMPTS = 3;
2836
+ var HASH_CHUNK_BYTES = 8 * 1024 * 1024;
2837
+ var IMPORT_TIMEOUT_MS = 50 * 6e4;
2838
+ var IMPORT_POLL_MS = 5e3;
2839
+ var PushError = class extends Error {
2840
+ constructor(message, issues = []) {
2841
+ super(message);
2842
+ this.issues = issues;
2617
2843
  }
2618
- return { delete: remove.sort(), kept: kept.sort((a, b) => a.path.localeCompare(b.path)) };
2844
+ issues;
2845
+ };
2846
+ function fail(message, issues) {
2847
+ throw new PushError(message, issues);
2619
2848
  }
2620
- function applyBatches(files, remove) {
2621
- const budget = SYNC_MAX_APPLY_BYTES - 1024 * 1024;
2622
- const cost = (file) => 512 + Math.ceil(file.bytes.length / 512) * 512 + 100 + file.path.length;
2623
- const batches = [[]];
2624
- let size = 0;
2625
- for (const file of files) {
2626
- if (cost(file) > budget) throw new SyncError(`${file.path} is ${Math.ceil(file.bytes.length / 1024 / 1024)} MiB; a sync sends files of at most ${budget / 1024 / 1024} MiB`);
2627
- if (size + cost(file) > budget && batches.at(-1).length > 0) {
2628
- batches.push([]);
2629
- size = 0;
2630
- }
2631
- batches.at(-1).push(file);
2632
- size += cost(file);
2633
- }
2634
- return batches.map((batch, index) => {
2635
- const manifest = {
2636
- files: batch.map(({ path, blob, mode }) => ({ path, blob, mode })),
2637
- delete: index === 0 ? remove : []
2638
- };
2639
- return tarOf([
2640
- { name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest)) },
2641
- ...batch.map((file, i) => ({ name: syncApplyEntryName(i), data: file.bytes }))
2642
- ]);
2849
+ function docker(args, options) {
2850
+ const result = spawnSync4("docker", args, {
2851
+ encoding: "utf8",
2852
+ stdio: ["ignore", "pipe", options.quiet ? "pipe" : "inherit"],
2853
+ maxBuffer: 64 * 1024 * 1024
2643
2854
  });
2855
+ if (result.error) fail(`could not run docker (${result.error.message}); is Docker installed and running?`);
2856
+ if (result.status !== 0) fail(`docker ${args.slice(0, 2).join(" ")} failed:
2857
+ ${result.stderr ?? ""}`.trimEnd());
2858
+ return result.stdout;
2644
2859
  }
2645
- async function copyRequest(session, path, init) {
2646
- let response;
2647
- try {
2648
- response = await fetch(`${session.url}${path}`, {
2649
- method: "POST",
2650
- headers: { authorization: `Bearer ${session.token}`, "content-type": init.contentType },
2651
- body: init.body
2652
- });
2653
- } catch (error) {
2654
- throw new SyncError(`could not reach the sandbox for ${path}: ${error instanceof Error ? error.message : String(error)}`);
2860
+ function imageConfigOf(image) {
2861
+ const result = spawnSync4("docker", ["image", "inspect", image], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
2862
+ if (result.error) fail(`could not run docker (${result.error.message}); is Docker installed and running?`);
2863
+ const [inspected] = result.status === 0 ? JSON.parse(result.stdout) : [];
2864
+ if (!inspected) fail(`Docker has no local image ${image}: build or pull it first`);
2865
+ if (inspected.Os !== "linux" || inspected.Architecture !== "amd64") {
2866
+ fail(
2867
+ `${image} is ${inspected.Os}/${inspected.Architecture}; sandboxes run linux/amd64. Rebuild it with
2868
+ docker buildx build --platform linux/amd64 -t ${image} --load <context>`
2869
+ );
2655
2870
  }
2656
- if (!response.ok) {
2657
- const text = await response.text();
2658
- let message = text;
2659
- try {
2660
- message = JSON.parse(text).error ?? text;
2661
- } catch {
2662
- }
2663
- throw new SyncError(`the sandbox refused ${path} (${response.status}): ${message.slice(0, 2e3)}`);
2871
+ const user = inspected.Config.User ?? "";
2872
+ if (!["", "0", "root"].includes(user.split(":")[0])) {
2873
+ fail(
2874
+ `${image} runs as USER ${user}; a sandbox image must leave USER as root (the sandbox drops to uid 1000 itself). Remove the final \`USER\` line from the Dockerfile, or end it with \`USER root\`.`
2875
+ );
2876
+ }
2877
+ const env = Object.fromEntries(
2878
+ (inspected.Config.Env ?? []).map((entry) => {
2879
+ const separator = entry.indexOf("=");
2880
+ return [entry.slice(0, separator), entry.slice(separator + 1)];
2881
+ })
2882
+ );
2883
+ const parsed = imageConfigSchema.safeParse({ workdir: inspected.Config.WorkingDir ?? "", env });
2884
+ if (!parsed.success) {
2885
+ const workdir = inspected.Config.WorkingDir || "(none)";
2886
+ fail(
2887
+ `${image}'s WORKDIR ${workdir} can't be used: ${parsed.error.issues[0]?.message ?? "invalid"}. Set one for your app in its Dockerfile (e.g. \`WORKDIR /app\`): services, prepare commands and actions run from it, and the sandbox makes it writable by the app.`
2888
+ );
2664
2889
  }
2665
- return response;
2890
+ return parsed.data;
2666
2891
  }
2667
- async function plan(session, local, to) {
2668
- const body = JSON.stringify(to === void 0 ? { local } : { local, to });
2669
- if (Buffer.byteLength(body) > SYNC_MAX_APPLY_BYTES) {
2670
- throw new SyncError(
2671
- `the file list (${local.length} files) is over the ${SYNC_MAX_APPLY_BYTES / 1024 / 1024} MiB a sync can plan in one request; ignore what the sandbox doesn't need (.gitignore)`
2672
- );
2892
+ function shellWord(value) {
2893
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
2894
+ }
2895
+ async function apiOrFail(result) {
2896
+ const res = await result;
2897
+ if (!res.ok) fail(res.error ?? "unknown error", res.issues);
2898
+ return res.data;
2899
+ }
2900
+ var mib = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
2901
+ async function readAt(file, offset, size) {
2902
+ const bytes = new Uint8Array(size);
2903
+ for (let at = 0; at < size; ) {
2904
+ const { bytesRead } = await file.read(bytes, at, size - at, offset + at);
2905
+ if (bytesRead === 0) return bytes.subarray(0, at);
2906
+ at += bytesRead;
2673
2907
  }
2674
- const response = await copyRequest(session, "/sync/plan", { body, contentType: "application/json" });
2675
- return parseAnswer(syncPlanResponseSchema, await response.text(), "/sync/plan");
2908
+ return bytes;
2676
2909
  }
2677
- function parseAnswer(schema, text, path) {
2678
- let value;
2679
- try {
2680
- value = JSON.parse(text);
2681
- } catch {
2682
- throw new SyncError(`the sandbox answered ${path} with something other than JSON: ${text.slice(0, 300)}`);
2910
+ async function digestOf(file, offset, size) {
2911
+ const hash = createHash4("sha256");
2912
+ for (let at = 0; at < size; at += HASH_CHUNK_BYTES) {
2913
+ hash.update(await readAt(file, offset + at, Math.min(HASH_CHUNK_BYTES, size - at)));
2683
2914
  }
2684
- const parsed = schema.safeParse(value);
2685
- if (!parsed.success) throw new SyncError(`the sandbox answered ${path} unexpectedly: ${text.slice(0, 300)}`);
2686
- return parsed.data;
2915
+ return `sha256:${hash.digest("hex")}`;
2687
2916
  }
2688
- async function apply(session, batches, build, json) {
2689
- let written = 0;
2690
- let deleted = 0;
2691
- let restarted = [];
2692
- const steps = [];
2693
- for (const [index, body] of batches.entries()) {
2694
- const last = index === batches.length - 1;
2695
- const response = await copyRequest(session, `/sync/apply${build && last ? "?build=1" : ""}`, { body, contentType: "application/x-tar" });
2696
- if (!(build && last)) {
2697
- const result = parseAnswer(syncApplyResultSchema, await response.text(), "/sync/apply");
2698
- written += result.written;
2699
- deleted += result.deleted;
2917
+ function partOf(bytes) {
2918
+ return { digest: `sha256:${createHash4("sha256").update(bytes).digest("hex")}`, size: bytes.length, body: () => bytes };
2919
+ }
2920
+ async function putBlob(url, headers, part) {
2921
+ const digest = part.digest;
2922
+ for (let attempt = 1; ; attempt++) {
2923
+ let response;
2924
+ try {
2925
+ const init = {
2926
+ method: "PUT",
2927
+ headers: { ...headers, "content-length": String(part.size) },
2928
+ body: part.body(),
2929
+ duplex: "half"
2930
+ };
2931
+ response = await fetch(url, init);
2932
+ } catch (error) {
2933
+ if (attempt >= UPLOAD_ATTEMPTS) throw error;
2934
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3 * attempt));
2700
2935
  continue;
2701
2936
  }
2702
- let buffer = "";
2703
- const decoder2 = new TextDecoder();
2704
- let finished = false;
2705
- for await (const chunk of response.body) {
2706
- buffer += decoder2.decode(chunk, { stream: true });
2707
- let newline = buffer.indexOf("\n");
2708
- while (newline >= 0) {
2709
- const event = parseAnswer(syncBuildEventSchema, buffer.slice(0, newline), "/sync/apply?build=1");
2710
- buffer = buffer.slice(newline + 1);
2711
- newline = buffer.indexOf("\n");
2712
- if (event.type === "applied") {
2713
- written += event.result.written;
2714
- deleted += event.result.deleted;
2715
- } else if (event.type === "output") {
2716
- if (!json) process6.stderr.write(event.text);
2717
- } else if (event.type === "step") {
2718
- steps.push({ name: event.name, ran: event.ran, ms: event.ms });
2719
- if (!json) process6.stderr.write(` build step ${event.name}: ${event.ran ? `ran in ${(event.ms / 1e3).toFixed(1)} s` : "unchanged"}
2720
- `);
2721
- } else if (event.type === "done") {
2722
- restarted = event.restarted;
2723
- finished = true;
2724
- } else if (event.type === "error") {
2725
- throw new SyncError(`the build failed after the files were synced: ${event.message}`);
2726
- }
2727
- }
2937
+ if (response.ok) return;
2938
+ const detail = `${response.status}: ${(await response.text()).slice(0, 300)}`;
2939
+ if (response.status < 500 || attempt >= UPLOAD_ATTEMPTS) {
2940
+ fail(`uploading ${digest} was refused (HTTP ${detail}); run the push again to upload what is still missing`);
2728
2941
  }
2729
- if (!finished) throw new SyncError("the build stream ended early; check the copy with `cofactor sandbox exec <id> -- cofactor-sandbox status`");
2942
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3 * attempt));
2730
2943
  }
2731
- return { written, deleted, restarted, steps };
2732
2944
  }
2733
- var SYNC_USAGE = "Usage: cofactor sandbox sync <id> [--build] [--json]\n cofactor sandbox sync <id> <local-dir> --to <path-in-sandbox> [--json]";
2734
- var LIST_LIMIT = 20;
2735
- async function handleSandboxSync(positionals, argv, apiUrl, token) {
2736
- const [id, localDir] = positionals;
2737
- const to = getArgValue(argv, "--to");
2738
- const build = hasFlag(argv, "--build");
2739
- const json = hasFlag(argv, "--json");
2740
- const fail4 = (message) => {
2741
- console.error(`Error: ${message}`);
2742
- process6.exit(1);
2743
- };
2744
- if (!id) return fail4(`sandbox id required.
2745
- ${SYNC_USAGE}`);
2746
- if (localDir === void 0 !== (to === void 0)) return fail4(`a local directory and --to go together.
2747
- ${SYNC_USAGE}`);
2748
- if (to !== void 0 && build) return fail4("--build reruns the build of a working-tree sync; build output is already built (`sync <id> <dir> --to <path>`).");
2749
- if (to !== void 0 && (to === "" || to.startsWith("/") || to.split("/").some((part) => part === "" || part === "." || part === ".."))) {
2750
- return fail4("--to is a path under the sandbox's sync root, like `static` or `frontend/dist` (no leading `/`, no `..`).");
2751
- }
2752
- if (localDir !== void 0 && (!existsSync2(localDir) || !lstatSync(localDir).isDirectory())) return fail4(`${localDir} is not a directory`);
2753
- if (!isInsideRepo(to === void 0 ? process6.cwd() : resolve2(localDir))) {
2754
- return fail4("run this inside a git repository: the files to sync and the synced tree come from git.");
2755
- }
2756
- const started = Date.now();
2757
- const sessionRes = await apiRequest(
2758
- apiUrl,
2759
- token,
2760
- "POST",
2761
- `/org/sandboxes-v2/sandboxes/${id}/sync-session`
2762
- );
2763
- if (!sessionRes.ok) return fail4(sessionRes.error ?? "could not start a sync");
2764
- const session = sessionRes.data;
2945
+ async function savedImage(archive) {
2946
+ const file = await open(archive);
2765
2947
  try {
2766
- let local;
2767
- let decide;
2768
- let cleanup = () => {
2948
+ const entries = await tarEntries((offset, length) => readAt(file, offset, length)).catch(
2949
+ (error) => fail(`reading the saved image: ${error instanceof Error ? error.message : String(error)}`)
2950
+ );
2951
+ const byName = new Map(entries.map((entry) => [entry.name.replace(/^\.\//, ""), entry]));
2952
+ const entryOf = (name) => {
2953
+ const entry = byName.get(name);
2954
+ if (!entry) fail(`the saved image has no ${name}; is this a \`docker save\` archive?`);
2955
+ return entry;
2769
2956
  };
2770
- if (to !== void 0) {
2771
- const dir = resolve2(localDir);
2772
- const output2 = buildOutput(dir, to);
2773
- local = { ...output2, untracked: /* @__PURE__ */ new Set(), root: dir };
2774
- decide = async (planned) => ({ delete: planned.copyOnly.map((entry) => entry.path), kept: [] });
2775
- } else {
2776
- const root = session.delivery === "github" ? repositoryOf(process6.cwd()).top : process6.cwd();
2777
- if (session.delivery === "upload" && !existsSync2(join3(root, "cofactor.sandbox.yaml"))) {
2778
- return fail4("run this from the directory holding cofactor.sandbox.yaml: a pushed image's WORKDIR mirrors it.");
2957
+ const json = async (name) => {
2958
+ const entry = entryOf(name);
2959
+ return JSON.parse(new TextDecoder().decode(await readAt(file, entry.offset, entry.size)));
2960
+ };
2961
+ const layerOf = async (name) => {
2962
+ const entry = entryOf(name);
2963
+ const tooLarge = (bytes) => fail(`a layer of the image is ${(bytes / 1024 ** 3).toFixed(1)} GB; a push takes at most 5 GB per layer. Slim the image.`);
2964
+ if (entry.size > MAX_BLOB_BYTES) tooLarge(entry.size);
2965
+ const range = { start: entry.offset, end: entry.offset + entry.size - 1 };
2966
+ const [first, second] = await readAt(file, entry.offset, 2);
2967
+ if (first === 31 && second === 139) {
2968
+ const unpacked = join4(dirname2(archive), `layer-${entry.offset}.tar`);
2969
+ await pipeline(createReadStream(archive, range), createGunzip(), createWriteStream(unpacked)).catch(
2970
+ (error) => fail(`unpacking a compressed layer of the image: ${error instanceof Error ? error.message : String(error)}`)
2971
+ );
2972
+ const plain = await open(unpacked);
2973
+ try {
2974
+ const { size } = await plain.stat();
2975
+ if (size > MAX_BLOB_BYTES) tooLarge(size);
2976
+ return { digest: await digestOf(plain, 0, size), size, body: () => createReadStream(unpacked) };
2977
+ } finally {
2978
+ await plain.close();
2979
+ }
2779
2980
  }
2780
- const tree = workingTree(root);
2781
- cleanup = tree.cleanup;
2782
- local = { ...tree, root };
2783
- decide = (planned) => chooseDeletions({
2784
- copyOnly: planned.copyOnly,
2785
- repo: tree.repo,
2786
- prefix: tree.prefix,
2787
- env: tree.env,
2788
- listDir: async (dir) => (await plan(session, [], dir)).copyOnly.map((entry) => entry.path)
2789
- });
2790
- }
2791
- try {
2792
- const planStarted = Date.now();
2793
- const planned = await plan(session, local.files, to);
2794
- const skipped = new Set(local.skipped.flatMap((path) => [path, `${path}/`]));
2795
- const { delete: remove, kept } = await decide({ ...planned, copyOnly: planned.copyOnly.filter((entry) => !skipped.has(entry.path)) });
2796
- const planMs = Date.now() - planStarted;
2797
- const need = new Set(planned.need);
2798
- const sending = local.files.filter((file) => need.has(file.path));
2799
- if (!json) {
2800
- writeRawOutput(`Syncing ${local.root} into sandbox ${id} (${session.delivery === "github" ? "GitHub" : "pushed image"}, version ${session.version.slice(0, 12)})`);
2801
- writeRawOutput(` plan: ${sending.length} to write, ${remove.length} to delete, ${local.files.length - sending.length} unchanged (${(planMs / 1e3).toFixed(1)} s)`);
2802
- const untracked = sending.filter((file) => local.untracked.has(file.path));
2803
- const tracked = sending.filter((file) => !local.untracked.has(file.path));
2804
- if (sending.length > 0) writeRawOutput(" write:");
2805
- for (const file of tracked.slice(0, LIST_LIMIT)) writeRawOutput(` ${file.path}`);
2806
- if (tracked.length > LIST_LIMIT) writeRawOutput(` ...and ${tracked.length - LIST_LIMIT} more`);
2807
- for (const file of untracked) writeRawOutput(` ${file.path} (untracked)`);
2808
- if (remove.length > 0) writeRawOutput(" delete:");
2809
- for (const path of remove) writeRawOutput(` ${path}`);
2810
- if (kept.length > 0) {
2811
- writeRawOutput(" kept (only in the sandbox):");
2812
- for (const entry of kept.slice(0, LIST_LIMIT)) {
2813
- writeRawOutput(` ${entry.path}${entry.files > 1 ? ` (${entry.files} files)` : ""}, ${entry.why}`);
2814
- }
2815
- if (kept.length > LIST_LIMIT) writeRawOutput(` ...and ${kept.length - LIST_LIMIT} more`);
2981
+ return { digest: await digestOf(file, entry.offset, entry.size), size: entry.size, body: () => createReadStream(archive, range) };
2982
+ };
2983
+ const [manifest] = await json("manifest.json");
2984
+ if (!manifest) fail("the saved image lists no image");
2985
+ const layers = [];
2986
+ for (const name of manifest.Layers) layers.push(await layerOf(name));
2987
+ return { config: await json(manifest.Config), layers };
2988
+ } finally {
2989
+ await file.close();
2990
+ }
2991
+ }
2992
+ async function pushedImage(archive, cofactorLayers, definitionYaml, config) {
2993
+ const saved = await savedImage(archive);
2994
+ const own = partOf(
2995
+ tarOf([
2996
+ { name: IMAGE_DEFINITION_FILE.slice(1), data: new TextEncoder().encode(definitionYaml) },
2997
+ { name: IMAGE_CONFIG_FILE.slice(1), data: new TextEncoder().encode(JSON.stringify(config)) }
2998
+ ])
2999
+ );
3000
+ const added = [...cofactorLayers, own];
3001
+ const pushed = withSandboxLayers(saved.config, added, (/* @__PURE__ */ new Date()).toISOString());
3002
+ const imageConfig = partOf(new TextEncoder().encode(JSON.stringify(pushed)));
3003
+ const blob = ({ digest, size }) => ({ digest, size });
3004
+ return {
3005
+ config: blob(imageConfig),
3006
+ layers: [...saved.layers, ...added].map(blob),
3007
+ blobs: new Map([imageConfig, ...saved.layers, own].map((part) => [part.digest, part]))
3008
+ };
3009
+ }
3010
+ async function handleSandboxPush(argv, apiUrl, token) {
3011
+ try {
3012
+ await push(argv, apiUrl, token);
3013
+ } catch (error) {
3014
+ if (!(error instanceof PushError)) throw error;
3015
+ console.error(`Error: ${error.message}`);
3016
+ for (const issue of error.issues) console.error(` ${issue.path || "(root)"}: ${issue.message}`);
3017
+ process7.exit(1);
3018
+ }
3019
+ }
3020
+ async function push(argv, apiUrl, token) {
3021
+ const valueFlags = /* @__PURE__ */ new Set(["--application", "--property", "--definition", "--tag", "--env", "--org", "--token", "--api-url", "--app-url"]);
3022
+ const positional = argv.filter((arg, i) => !arg.startsWith("-") && !valueFlags.has(argv[i - 1] ?? ""));
3023
+ const image = positional[0];
3024
+ if (!image) fail(`an image is required
3025
+ ${USAGE}`);
3026
+ const definitionFile2 = getArgValue(argv, "--definition") ?? "./cofactor.sandbox.yaml";
3027
+ const tags = getArgValues(argv, "--tag");
3028
+ const badTag = tags.find((tag) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(tag));
3029
+ if (badTag !== void 0) fail(`--tag ${badTag}: use letters, digits, '.', '_' and '-' (at most 64, starting with a letter or digit)`);
3030
+ if (tags.length > 10) fail("a push takes at most 10 tags");
3031
+ const wait = hasFlag(argv, "--wait");
3032
+ const json = hasFlag(argv, "--json");
3033
+ const log = (line) => json ? console.error(line) : console.log(line);
3034
+ let definitionYaml;
3035
+ try {
3036
+ definitionYaml = readFileSync6(definitionFile2, "utf8");
3037
+ } catch (error) {
3038
+ fail(`reading ${definitionFile2}: ${error instanceof Error ? error.message : String(error)}`);
3039
+ }
3040
+ const { definition, issues } = validateDefinition(definitionYaml, "upload");
3041
+ if (!definition || issues.length > 0) fail(`${definitionFile2} is not a valid definition for pushed images:`, issues);
3042
+ const storageProvider = definition.object_storage?.provider;
3043
+ const applicationId = await resolveOrCreateSandboxApplication(argv, apiUrl, token);
3044
+ const config = imageConfigOf(image);
3045
+ log(syncabilityLineOrSkipped({ image, workdir: config.workdir, definitionFile: definitionFile2 }));
3046
+ const work = mkdtempSync3(join4(tmpdir3(), "cofactor-push-"));
3047
+ const cleanUp = () => rmSync4(work, { recursive: true, force: true });
3048
+ const interrupted = (signal) => {
3049
+ cleanUp();
3050
+ process7.exit(signal === "SIGINT" ? 130 : 143);
3051
+ };
3052
+ process7.once("SIGINT", interrupted);
3053
+ process7.once("SIGTERM", interrupted);
3054
+ process7.exitCode = 1;
3055
+ try {
3056
+ const ours = await apiOrFail(
3057
+ apiRequest(
3058
+ apiUrl,
3059
+ token,
3060
+ "GET",
3061
+ "/org/sandboxes-v2/helper-layer"
3062
+ )
3063
+ );
3064
+ if (!ours.helper) fail("this Cofactor takes pushes from an older cofactor CLI; use the version it ships with");
3065
+ const cofactorLayers = [ours.helper, ...storageProvider ? [ours.objectStorage[storageProvider]] : []];
3066
+ const archive = join4(work, "image.tar");
3067
+ log("Saving the image...");
3068
+ docker(["save", "--output", archive, image], { quiet: json });
3069
+ const pushing = await pushedImage(archive, cofactorLayers, definitionYaml, config);
3070
+ const { layers } = pushing;
3071
+ const created = await apiOrFail(
3072
+ apiRequest(
3073
+ apiUrl,
3074
+ token,
3075
+ "POST",
3076
+ `/org/applications/${applicationId}/sandbox/uploads`,
3077
+ { definitionYaml, platformVersion: ours.platformVersion, tags, image: { config: pushing.config, layers } }
3078
+ )
3079
+ );
3080
+ const partsByDigest = pushing.blobs;
3081
+ const uploadBytes = created.uploads.reduce((total, upload) => total + (partsByDigest.get(upload.digest)?.size ?? 0), 0);
3082
+ const imageBytes = layers.reduce((total, layer) => total + layer.size, 0);
3083
+ log(`Pushing ${created.id}: uploading ${created.uploads.length} of ${layers.length + 1} blobs (${mib(uploadBytes)}); ${mib(imageBytes - uploadBytes)} already in Cofactor`);
3084
+ const queue = [...created.uploads];
3085
+ let sent = 0;
3086
+ await Promise.all(
3087
+ Array.from({ length: UPLOAD_CONCURRENCY }, async () => {
3088
+ for (let next = queue.shift(); next; next = queue.shift()) {
3089
+ const part = partsByDigest.get(next.digest);
3090
+ if (!part) fail(`Cofactor asked for ${next.digest}, which this image doesn't have`);
3091
+ await putBlob(next.url, next.headers, part);
3092
+ sent += part.size;
3093
+ if (part.size > 10 * 1024 * 1024) log(` ${mib(sent)} of ${mib(uploadBytes)}`);
2816
3094
  }
2817
- if (local.skipped.length > 0) writeRawOutput(` not synced (symlinks and submodules): ${local.skipped.join(", ")}`);
2818
- }
2819
- const applyStarted = Date.now();
2820
- const batches = sending.length === 0 && remove.length === 0 && !build ? [] : applyBatches(
2821
- sending.map((file) => ({ ...file, bytes: readFileSync5(join3(local.root, to === void 0 ? file.path : file.path.slice(to.length + 1))) })),
2822
- remove
2823
- );
2824
- const applied = await apply(session, batches, build, json);
2825
- const applyMs = Date.now() - applyStarted;
2826
- const ms = Date.now() - started;
2827
- const recorded = await apiRequest(apiUrl, token, "POST", `/org/sandboxes-v2/sandboxes/${id}/syncs`, {
2828
- tree: local.tree,
2829
- buildOutput: to !== void 0,
2830
- written: applied.written,
2831
- deleted: applied.deleted,
2832
- ms
2833
- });
2834
- if (!recorded.ok) throw new SyncError(`the files were synced, but recording the sync failed: ${recorded.error}`);
2835
- if (json) {
2836
- writeRawOutput(
2837
- JSON.stringify(
2838
- {
2839
- id,
2840
- tree: local.tree,
2841
- buildOutput: to !== void 0,
2842
- written: sending.map((file) => file.path),
2843
- deleted: remove,
2844
- kept,
2845
- build: build ? { steps: applied.steps, restarted: applied.restarted } : null,
2846
- ms: { total: ms, plan: planMs, apply: applyMs }
2847
- },
2848
- null,
2849
- 2
2850
- )
3095
+ })
3096
+ );
3097
+ rmSync4(archive);
3098
+ let pushed = await apiOrFail(
3099
+ apiRequest(
3100
+ apiUrl,
3101
+ token,
3102
+ "POST",
3103
+ `/org/applications/${applicationId}/sandbox/uploads/${created.id}/complete`
3104
+ )
3105
+ );
3106
+ if (wait) {
3107
+ log("Importing (Cofactor boots the image until its services are healthy)...");
3108
+ const deadline = Date.now() + IMPORT_TIMEOUT_MS;
3109
+ const importStarted = Date.now();
3110
+ let reported = importStarted;
3111
+ while ((pushed.status === "importing" || pushed.status === "uploading") && Date.now() < deadline) {
3112
+ await new Promise((resolve4) => setTimeout(resolve4, IMPORT_POLL_MS));
3113
+ if (Date.now() - reported >= 6e4) {
3114
+ reported = Date.now();
3115
+ log(` still importing (${Math.round((reported - importStarted) / 6e4)} min)`);
3116
+ }
3117
+ pushed = await apiOrFail(
3118
+ apiRequest(apiUrl, token, "GET", `/org/applications/${applicationId}/sandbox/uploads/${created.id}`)
2851
3119
  );
2852
- return;
2853
3120
  }
2854
- writeRawOutput(` applied: ${applied.written} written, ${applied.deleted} deleted (${(applyMs / 1e3).toFixed(1)} s)`);
2855
- if (build) writeRawOutput(` restarted: ${applied.restarted.join(", ") || "nothing"}`);
2856
- writeRawOutput(`Synced ${to === void 0 ? "tree" : "build output"} ${local.tree} in ${(ms / 1e3).toFixed(1)} s.`);
2857
- if (!build) {
2858
- writeRawOutput(` Restart what runs it: cofactor sandbox exec ${id} -- cofactor-sandbox restart <service>`);
3121
+ }
3122
+ const uploaded = { blobs: created.uploads.length, of: layers.length + 1, bytes: uploadBytes, reusedBytes: imageBytes - uploadBytes };
3123
+ const application = shellWord(getArgValue(argv, "--application") ?? getArgValue(argv, "--property") ?? applicationId);
3124
+ const target = ["--env", "--org"].map((flag) => {
3125
+ const value = getArgValue(process7.argv, flag);
3126
+ return value ? ` ${flag} ${shellWord(value)}` : "";
3127
+ }).join("");
3128
+ if (json) {
3129
+ writeRawOutput(JSON.stringify({ ...pushed, uploaded }, null, 2));
3130
+ } else {
3131
+ console.log(`Push ${pushed.id}: ${pushed.status}${pushed.tags.length > 0 ? ` [${pushed.tags.join(", ")}]` : ""}`);
3132
+ if (pushed.status === "ready") {
3133
+ console.log(` Claim it: cofactor sandbox claim --application ${application} --environment default --wait${target}`);
3134
+ } else if (pushed.status !== "failed") {
3135
+ console.log(` Importing; follow it with: cofactor sandbox envs --application ${application}${target}`);
2859
3136
  }
2860
- const shown = await apiRequest(apiUrl, token, "GET", `/org/sandboxes-v2/sandboxes/${id}`);
2861
- for (const [name, url] of Object.entries(shown.data?.portUrls ?? {})) writeRawOutput(` ${name.padEnd(10)} ${url}`);
2862
- } finally {
2863
- cleanup();
2864
3137
  }
2865
- } catch (error) {
2866
- if (error instanceof SyncError) return fail4(error.message);
2867
- throw error;
3138
+ if (pushed.status === "failed") fail(`push ${pushed.id} failed:
3139
+ ${pushed.error ?? "no reason given"}`);
3140
+ process7.exitCode = 0;
3141
+ } finally {
3142
+ process7.off("SIGINT", interrupted);
3143
+ process7.off("SIGTERM", interrupted);
3144
+ cleanUp();
2868
3145
  }
2869
3146
  }
2870
- function isInsideRepo(dir) {
2871
- return git(["rev-parse", "--is-inside-work-tree"], { cwd: dir, okCodes: [0, 128] }).trim() === "true";
2872
- }
2873
3147
 
2874
3148
  // src/sandbox-refresh.ts
2875
- import process7 from "process";
3149
+ import process8 from "process";
2876
3150
  var POLL_INTERVAL_MS = 5e3;
2877
3151
  var WAIT_TIMEOUT_MS = 50 * 6e4;
2878
3152
  function shortVersion2(version) {
2879
3153
  return /^[0-9a-f]{40}$/.test(version) ? version.slice(0, 12) : version;
2880
3154
  }
2881
3155
  function sleep2(ms) {
2882
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3156
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2883
3157
  }
2884
3158
  async function handleSandboxRefresh(argv, apiUrl, token) {
2885
3159
  const applicationId = await resolveApplicationId(argv, apiUrl, token);
@@ -2892,7 +3166,7 @@ async function handleSandboxRefresh(argv, apiUrl, token) {
2892
3166
  });
2893
3167
  if (!res.ok) {
2894
3168
  console.error(`Error: ${res.error}`);
2895
- process7.exit(1);
3169
+ process8.exit(1);
2896
3170
  }
2897
3171
  return res.data;
2898
3172
  };
@@ -2912,7 +3186,7 @@ async function handleSandboxRefresh(argv, apiUrl, token) {
2912
3186
  );
2913
3187
  if (!res.ok) {
2914
3188
  console.error(`Error: ${res.error}`);
2915
- process7.exit(1);
3189
+ process8.exit(1);
2916
3190
  }
2917
3191
  if (res.data.status === "ready" && !res.data.isHead && !askedAgain) {
2918
3192
  askedAgain = true;
@@ -2942,11 +3216,11 @@ async function handleSandboxRefresh(argv, apiUrl, token) {
2942
3216
  }
2943
3217
  }
2944
3218
  }
2945
- if (latest.head.status === "failed" || wait && latest.head.status === "building") process7.exit(1);
3219
+ if (latest.head.status === "failed" || wait && latest.head.status === "building") process8.exit(1);
2946
3220
  }
2947
3221
 
2948
3222
  // src/sandbox-warm.ts
2949
- import process8 from "process";
3223
+ import process9 from "process";
2950
3224
  var STATE_LABELS = {
2951
3225
  warming: "Filling",
2952
3226
  warm: "On",
@@ -2973,7 +3247,7 @@ function isSandboxV2WarmCommand(first) {
2973
3247
  }
2974
3248
  function fail2(message) {
2975
3249
  console.error(`Error: ${message}`);
2976
- process8.exit(1);
3250
+ process9.exit(1);
2977
3251
  }
2978
3252
  function rejectUnknownWarmArgs(command, argv) {
2979
3253
  const spec = COMMANDS[command];
@@ -3047,7 +3321,7 @@ async function handleSandboxWarm(argv, apiUrl, token) {
3047
3321
  if (!command || !isSandboxV2WarmCommand(command)) {
3048
3322
  console.error(command ? `Unknown warm command: ${command}
3049
3323
  ${USAGE2}` : USAGE2);
3050
- process8.exit(1);
3324
+ process9.exit(1);
3051
3325
  }
3052
3326
  rejectUnknownWarmArgs(command, rest);
3053
3327
  if (command === "list") {
@@ -3078,7 +3352,7 @@ ${USAGE2}` : USAGE2);
3078
3352
  }
3079
3353
 
3080
3354
  // src/sandbox-secrets.ts
3081
- import process9 from "process";
3355
+ import process10 from "process";
3082
3356
  function scopeText(environment2) {
3083
3357
  return environment2 === null ? "all environments" : `environment ${environment2}`;
3084
3358
  }
@@ -3102,7 +3376,7 @@ function stdinRefusal(isTTY) {
3102
3376
  }
3103
3377
  function fail3(message) {
3104
3378
  console.error(`Error: ${message}`);
3105
- process9.exit(1);
3379
+ process10.exit(1);
3106
3380
  }
3107
3381
  function decodeSecretValue(bytes) {
3108
3382
  let text;
@@ -3116,7 +3390,7 @@ function decodeSecretValue(bytes) {
3116
3390
  }
3117
3391
  async function readStdin() {
3118
3392
  const chunks = [];
3119
- for await (const chunk of process9.stdin) chunks.push(chunk);
3393
+ for await (const chunk of process10.stdin) chunks.push(chunk);
3120
3394
  return Buffer.concat(chunks);
3121
3395
  }
3122
3396
  function environmentArg(argv) {
@@ -3132,7 +3406,7 @@ function printUsage() {
3132
3406
  console.error("Usage: cofactor sandbox secret set <NAME> --application <name|id> [--environment <e>] [--json] (value on stdin)");
3133
3407
  console.error(" cofactor sandbox secret list --application <name|id> [--json]");
3134
3408
  console.error(" cofactor sandbox secret rm <NAME> --application <name|id> [--environment <e>] [--json]");
3135
- process9.exit(1);
3409
+ process10.exit(1);
3136
3410
  }
3137
3411
  async function handleSandboxSecret(argv, apiUrl, token) {
3138
3412
  const [action2, name, ...extra] = v2Positionals("secret", argv);
@@ -3148,10 +3422,10 @@ async function handleSandboxSecret(argv, apiUrl, token) {
3148
3422
  const json = hasFlag(argv, "--json");
3149
3423
  const application = getArgValue(argv, "--application") ?? getArgValue(argv, "--property") ?? "";
3150
3424
  if (action2 === "set") {
3151
- const refusal = stdinRefusal(process9.stdin.isTTY);
3425
+ const refusal = stdinRefusal(process10.stdin.isTTY);
3152
3426
  if (refusal) {
3153
3427
  console.error(`Error: ${refusal}`);
3154
- process9.exit(1);
3428
+ process10.exit(1);
3155
3429
  }
3156
3430
  }
3157
3431
  let value;
@@ -3170,7 +3444,7 @@ async function handleSandboxSecret(argv, apiUrl, token) {
3170
3444
  );
3171
3445
  if (!res2.ok) {
3172
3446
  console.error(`Error: ${res2.error}`);
3173
- process9.exit(1);
3447
+ process10.exit(1);
3174
3448
  }
3175
3449
  if (json) {
3176
3450
  writeRawOutput(JSON.stringify(res2.data, null, 2));
@@ -3199,7 +3473,7 @@ async function handleSandboxSecret(argv, apiUrl, token) {
3199
3473
  );
3200
3474
  if (!res.ok) {
3201
3475
  console.error(`Error: ${res.error}`);
3202
- process9.exit(1);
3476
+ process10.exit(1);
3203
3477
  }
3204
3478
  if (json) {
3205
3479
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3217,7 +3491,7 @@ var parseEgressMode = (value) => {
3217
3491
  console.error(
3218
3492
  `Error: invalid --egress-mode '${value}'. Must be one of: ${SANDBOX_EGRESS_MODES.join(", ")}`
3219
3493
  );
3220
- process10.exit(1);
3494
+ process11.exit(1);
3221
3495
  };
3222
3496
  var parseEgressAllowlist = (argv) => {
3223
3497
  const rawEntries = getArgValues(argv, "--egress-allow").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
@@ -3227,13 +3501,13 @@ var parseEgressAllowlist = (argv) => {
3227
3501
  console.error(
3228
3502
  `Error: invalid --egress-allow '${entry}'. Use domain:example.com or cidr:203.0.113.0/24`
3229
3503
  );
3230
- process10.exit(1);
3504
+ process11.exit(1);
3231
3505
  }
3232
3506
  const kind = entry.slice(0, separator);
3233
3507
  const value = entry.slice(separator + 1);
3234
3508
  if (kind !== "domain" && kind !== "cidr") {
3235
3509
  console.error(`Error: invalid --egress-allow kind '${kind}'. Must be domain or cidr.`);
3236
- process10.exit(1);
3510
+ process11.exit(1);
3237
3511
  }
3238
3512
  return { kind, value };
3239
3513
  });
@@ -3290,7 +3564,8 @@ function printHelp() {
3290
3564
  ' sync <id> <local-dir> --to <path> Mirror local build output into <path> in the sandbox (see "Sync options" below)',
3291
3565
  " destroy <id> Tear a claimed sandbox down",
3292
3566
  ' warm list|set|show|clear [options] Keep an environment warm so claims start in about a second (staff only; see "Warm options" below)',
3293
- " verify-env --application <a> [options] Claim each environment of the current version, sign its personas in, destroy",
3567
+ " auth-check <id> --persona <p> Prove sign-in on an existing sandbox",
3568
+ " verify-env --application <a> [options] Claim each environment (optionally --at <sha>), sign its personas in, destroy",
3294
3569
  " preview --application <a> --pr <n> A pull request's preview and its logins",
3295
3570
  " guide [topic] Show the Sandboxes v2 guide (`guide v2 [topic]` is the same)",
3296
3571
  ` topics: ${SANDBOX_V2_GUIDE_SECTIONS.map((section) => section.slug).join(", ")}`,
@@ -3309,6 +3584,9 @@ function printHelp() {
3309
3584
  " --wait Block until Cofactor has imported (booted) the image",
3310
3585
  "",
3311
3586
  "Claim options (Sandboxes v2):",
3587
+ " --from-local Claim local tracked and nonignored untracked files without committing or merging",
3588
+ " --repo <owner/name> With --from-local: GitHub repository (default: origin)",
3589
+ " --dir <path> With --from-local: definition directory relative to the repository root (default: cwd)",
3312
3590
  ' --environment <name> Environment to claim (default: "default")',
3313
3591
  " --at <version> A commit SHA, or a push id or tag (default: the default branch head / push head)",
3314
3592
  " --timeout <duration> The claim's time limit: not ready by then, it fails (e.g. 45m, 3h; default 2h,",
@@ -3498,11 +3776,11 @@ async function handleList(argv, apiUrl, token) {
3498
3776
  ]);
3499
3777
  if (!templatesRes.ok) {
3500
3778
  console.error(`Error: ${templatesRes.error}`);
3501
- process10.exit(1);
3779
+ process11.exit(1);
3502
3780
  }
3503
3781
  if (!instancesRes.ok) {
3504
3782
  console.error(`Error loading instances: ${instancesRes.error}`);
3505
- process10.exit(1);
3783
+ process11.exit(1);
3506
3784
  }
3507
3785
  if (json) {
3508
3786
  writeRawOutput(JSON.stringify({ templates: templatesRes.data, instances: instancesRes.data }, null, 2));
@@ -3550,7 +3828,7 @@ async function handleTemplate(argv, apiUrl, token) {
3550
3828
  );
3551
3829
  if (!res.ok) {
3552
3830
  console.error(`Error: ${res.error}`);
3553
- process10.exit(1);
3831
+ process11.exit(1);
3554
3832
  }
3555
3833
  if (json) {
3556
3834
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3579,7 +3857,7 @@ async function handleArtifacts(argv, apiUrl, token) {
3579
3857
  const json = hasFlag(argv, "--json");
3580
3858
  if (!templateId) {
3581
3859
  console.error("Error: template ID required. Usage: canary sandbox artifacts <template-id>");
3582
- process10.exit(1);
3860
+ process11.exit(1);
3583
3861
  }
3584
3862
  const res = await apiRequest(
3585
3863
  apiUrl,
@@ -3589,7 +3867,7 @@ async function handleArtifacts(argv, apiUrl, token) {
3589
3867
  );
3590
3868
  if (!res.ok) {
3591
3869
  console.error(`Error: ${res.error}`);
3592
- process10.exit(1);
3870
+ process11.exit(1);
3593
3871
  }
3594
3872
  if (json) {
3595
3873
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3619,7 +3897,7 @@ async function handleChecklist(argv, apiUrl, token) {
3619
3897
  const templateId = positional[0];
3620
3898
  if (!templateId) {
3621
3899
  console.error("Error: template ID required. Usage: canary sandbox checklist <template-id>");
3622
- process10.exit(1);
3900
+ process11.exit(1);
3623
3901
  }
3624
3902
  const res = await apiRequest(
3625
3903
  apiUrl,
@@ -3629,7 +3907,7 @@ async function handleChecklist(argv, apiUrl, token) {
3629
3907
  );
3630
3908
  if (!res.ok) {
3631
3909
  console.error(`Error: ${res.error}`);
3632
- process10.exit(1);
3910
+ process11.exit(1);
3633
3911
  }
3634
3912
  if (json) {
3635
3913
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3664,7 +3942,7 @@ async function handleChecklistSkip(argv, apiUrl, token) {
3664
3942
  const itemId = positional[1];
3665
3943
  if (!templateId || !itemId) {
3666
3944
  console.error("Error: both template ID and item ID required. Usage: canary sandbox checklist skip <template-id> <item-id>");
3667
- process10.exit(1);
3945
+ process11.exit(1);
3668
3946
  }
3669
3947
  const res = await apiRequest(
3670
3948
  apiUrl,
@@ -3674,7 +3952,7 @@ async function handleChecklistSkip(argv, apiUrl, token) {
3674
3952
  );
3675
3953
  if (!res.ok) {
3676
3954
  console.error(`Error: ${res.error}`);
3677
- process10.exit(1);
3955
+ process11.exit(1);
3678
3956
  }
3679
3957
  console.log(`Skipped checklist item: ${itemId}`);
3680
3958
  }
@@ -3686,7 +3964,7 @@ async function handleChecklistComplete(argv, apiUrl, token) {
3686
3964
  if (!templateId || !itemId) {
3687
3965
  console.error("Error: both template ID and item ID required.");
3688
3966
  console.error('Usage: canary sandbox checklist complete <template-id> <item-id> [--status success|failed] [--summary "..."]');
3689
- process10.exit(1);
3967
+ process11.exit(1);
3690
3968
  }
3691
3969
  const status = getArgValue(argv, "--status") ?? "success";
3692
3970
  const summary = getArgValue(argv, "--summary");
@@ -3698,11 +3976,11 @@ async function handleChecklistComplete(argv, apiUrl, token) {
3698
3976
  configSchema = JSON.parse(configSchemaArg);
3699
3977
  } catch {
3700
3978
  try {
3701
- configSchema = JSON.parse(readFileSync6(configSchemaArg, "utf-8"));
3979
+ configSchema = JSON.parse(readFileSync7(configSchemaArg, "utf-8"));
3702
3980
  } catch (e) {
3703
3981
  const msg = e instanceof Error ? e.message : String(e);
3704
3982
  console.error(`Error reading config schema from "${configSchemaArg}": ${msg}`);
3705
- process10.exit(1);
3983
+ process11.exit(1);
3706
3984
  }
3707
3985
  }
3708
3986
  }
@@ -3721,7 +3999,7 @@ async function handleChecklistComplete(argv, apiUrl, token) {
3721
3999
  );
3722
4000
  if (!res.ok) {
3723
4001
  console.error(`Error: ${res.error}`);
3724
- process10.exit(1);
4002
+ process11.exit(1);
3725
4003
  }
3726
4004
  if (json) {
3727
4005
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3735,7 +4013,7 @@ async function handleConfigList(argv, apiUrl, token) {
3735
4013
  const json = hasFlag(argv, "--json");
3736
4014
  if (!templateId) {
3737
4015
  console.error("Error: template ID required. Usage: canary sandbox config list <template-id>");
3738
- process10.exit(1);
4016
+ process11.exit(1);
3739
4017
  }
3740
4018
  const res = await apiRequest(
3741
4019
  apiUrl,
@@ -3745,7 +4023,7 @@ async function handleConfigList(argv, apiUrl, token) {
3745
4023
  );
3746
4024
  if (!res.ok) {
3747
4025
  console.error(`Error: ${res.error}`);
3748
- process10.exit(1);
4026
+ process11.exit(1);
3749
4027
  }
3750
4028
  const t = res.data;
3751
4029
  const schema = t.configSchema ?? [];
@@ -3795,16 +4073,16 @@ async function handleConfigSet(argv, apiUrl, token) {
3795
4073
  console.error("Error: template ID and key required.");
3796
4074
  console.error("Usage: canary sandbox config set <template-id> <key> <value>");
3797
4075
  console.error(' For sensitive values: echo "sk-ant-..." | canary sandbox config set <template-id> <key> --value-stdin');
3798
- process10.exit(1);
4076
+ process11.exit(1);
3799
4077
  }
3800
4078
  if (fromStdin) {
3801
4079
  const chunks = [];
3802
- for await (const chunk of process10.stdin) chunks.push(chunk);
4080
+ for await (const chunk of process11.stdin) chunks.push(chunk);
3803
4081
  value = Buffer.concat(chunks).toString("utf-8").trim();
3804
4082
  }
3805
4083
  if (value === void 0) {
3806
4084
  console.error("Error: value required. Pass as argument or use --value-stdin");
3807
- process10.exit(1);
4085
+ process11.exit(1);
3808
4086
  }
3809
4087
  const res = await apiRequest(
3810
4088
  apiUrl,
@@ -3818,7 +4096,7 @@ async function handleConfigSet(argv, apiUrl, token) {
3818
4096
  );
3819
4097
  if (!res.ok) {
3820
4098
  console.error(`Error: ${res.error}`);
3821
- process10.exit(1);
4099
+ process11.exit(1);
3822
4100
  }
3823
4101
  console.log(`Config set: ${key}`);
3824
4102
  }
@@ -3826,7 +4104,7 @@ async function handleConfigUnset(argv, apiUrl, token) {
3826
4104
  const [templateId, key] = argv.filter((arg) => !arg.startsWith("-"));
3827
4105
  if (!templateId || !key) {
3828
4106
  console.error("Usage: canary sandbox config unset <template-id> <key>");
3829
- process10.exit(1);
4107
+ process11.exit(1);
3830
4108
  }
3831
4109
  const res = await apiRequest(
3832
4110
  apiUrl,
@@ -3838,7 +4116,7 @@ async function handleConfigUnset(argv, apiUrl, token) {
3838
4116
  );
3839
4117
  if (!res.ok) {
3840
4118
  console.error(`Error: ${res.error}`);
3841
- process10.exit(1);
4119
+ process11.exit(1);
3842
4120
  }
3843
4121
  console.log(`Config unset: ${key}`);
3844
4122
  }
@@ -3847,12 +4125,12 @@ async function handleSetSize(argv, apiUrl, token) {
3847
4125
  if (!templateId || !sizeArg) {
3848
4126
  console.error("Error: template ID and size required.");
3849
4127
  console.error(`Usage: canary sandbox set-size <template-id> <${INSTANCE_SIZES.join("|")}>`);
3850
- process10.exit(1);
4128
+ process11.exit(1);
3851
4129
  }
3852
4130
  const size = parseInstanceSize(sizeArg);
3853
4131
  if (!size) {
3854
4132
  console.error(`Error: invalid size '${sizeArg}'. Must be one of: ${INSTANCE_SIZES.join(", ")}`);
3855
- process10.exit(1);
4133
+ process11.exit(1);
3856
4134
  }
3857
4135
  const res = await apiRequest(
3858
4136
  apiUrl,
@@ -3863,7 +4141,7 @@ async function handleSetSize(argv, apiUrl, token) {
3863
4141
  );
3864
4142
  if (!res.ok) {
3865
4143
  console.error(`Error: ${res.error}`);
3866
- process10.exit(1);
4144
+ process11.exit(1);
3867
4145
  }
3868
4146
  console.log(`Instance size set to ${size}`);
3869
4147
  }
@@ -3872,12 +4150,12 @@ async function handlePoolSetSize(argv, apiUrl, token) {
3872
4150
  if (!templateId || countArg === void 0) {
3873
4151
  console.error("Error: template ID and count required.");
3874
4152
  console.error("Usage: canary sandbox pool set-size <template-id> <count>");
3875
- process10.exit(1);
4153
+ process11.exit(1);
3876
4154
  }
3877
4155
  const poolMinReady = /^\d+$/.test(countArg) ? Number(countArg) : Number.NaN;
3878
4156
  if (!Number.isInteger(poolMinReady)) {
3879
4157
  console.error(`Error: invalid count '${countArg}'. Must be a non-negative integer.`);
3880
- process10.exit(1);
4158
+ process11.exit(1);
3881
4159
  }
3882
4160
  const res = await apiRequest(
3883
4161
  apiUrl,
@@ -3888,7 +4166,7 @@ async function handlePoolSetSize(argv, apiUrl, token) {
3888
4166
  );
3889
4167
  if (!res.ok) {
3890
4168
  console.error(`Error: ${res.error}`);
3891
- process10.exit(1);
4169
+ process11.exit(1);
3892
4170
  }
3893
4171
  console.log(`Warm stopped-pool size set to ${poolMinReady}`);
3894
4172
  }
@@ -3897,7 +4175,7 @@ async function handleCreate(argv, apiUrl, token) {
3897
4175
  const json = hasFlag(argv, "--json");
3898
4176
  const propertyHint = positional[0] ?? getArgValue(argv, "--property");
3899
4177
  const property = await selectProperty(apiUrl, token, propertyHint ?? void 0);
3900
- if (!property) process10.exit(1);
4178
+ if (!property) process11.exit(1);
3901
4179
  const reposRes = await apiRequest(
3902
4180
  apiUrl,
3903
4181
  token,
@@ -3906,7 +4184,7 @@ async function handleCreate(argv, apiUrl, token) {
3906
4184
  );
3907
4185
  if (!reposRes.ok || !reposRes.items?.length) {
3908
4186
  console.error(`No GitHub repos linked to property "${property.name}". Link one first in the web app.`);
3909
- process10.exit(1);
4187
+ process11.exit(1);
3910
4188
  }
3911
4189
  const linkedRepo = reposRes.items[0];
3912
4190
  const branch = getArgValue(argv, "--branch") ?? linkedRepo.defaultBranch ?? "main";
@@ -3916,7 +4194,7 @@ async function handleCreate(argv, apiUrl, token) {
3916
4194
  const instanceSize = sizeArg ? parseInstanceSize(sizeArg) : void 0;
3917
4195
  if (sizeArg && !instanceSize) {
3918
4196
  console.error(`Error: invalid --size '${sizeArg}'. Must be one of: ${INSTANCE_SIZES.join(", ")}`);
3919
- process10.exit(1);
4197
+ process11.exit(1);
3920
4198
  }
3921
4199
  const body = {
3922
4200
  name,
@@ -3936,7 +4214,7 @@ async function handleCreate(argv, apiUrl, token) {
3936
4214
  );
3937
4215
  if (!res.ok) {
3938
4216
  console.error(`Error: ${res.error}`);
3939
- process10.exit(1);
4217
+ process11.exit(1);
3940
4218
  }
3941
4219
  if (json) {
3942
4220
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -3956,7 +4234,7 @@ async function handleProvision(argv, apiUrl, token) {
3956
4234
  const json = hasFlag(argv, "--json");
3957
4235
  if (!templateId) {
3958
4236
  console.error("Error: template ID required. Usage: canary sandbox provision <template-id> [--ttl 8]");
3959
- process10.exit(1);
4237
+ process11.exit(1);
3960
4238
  }
3961
4239
  const name = getArgValue(argv, "--name") ?? `instance-${Date.now()}`;
3962
4240
  const ttl = parseInt(getArgValue(argv, "--ttl") ?? "8", 10);
@@ -3983,7 +4261,7 @@ async function handleProvision(argv, apiUrl, token) {
3983
4261
  );
3984
4262
  if (!res.ok) {
3985
4263
  console.error(`Error: ${res.error}`);
3986
- process10.exit(1);
4264
+ process11.exit(1);
3987
4265
  }
3988
4266
  if (json) {
3989
4267
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4008,7 +4286,7 @@ async function handleWarm(argv, apiUrl, token) {
4008
4286
  "Error: template ID required. Usage: canary sandbox warm <template-id> [--wait] [--timeout <sec>]"
4009
4287
  );
4010
4288
  console.error("Find the template id with: canary sandbox list");
4011
- process10.exit(1);
4289
+ process11.exit(1);
4012
4290
  }
4013
4291
  const ensure = () => apiRequest(
4014
4292
  apiUrl,
@@ -4023,7 +4301,7 @@ async function handleWarm(argv, apiUrl, token) {
4023
4301
  } else {
4024
4302
  console.error(`Error: ${first.error ?? "Failed to warm sandbox"}`);
4025
4303
  }
4026
- process10.exit(1);
4304
+ process11.exit(1);
4027
4305
  }
4028
4306
  const terminalOk = /* @__PURE__ */ new Set(["running"]);
4029
4307
  const terminalFail = /* @__PURE__ */ new Set(["failed", "terminated", "stopping"]);
@@ -4036,7 +4314,7 @@ async function handleWarm(argv, apiUrl, token) {
4036
4314
  const poll = await ensure();
4037
4315
  if (!poll.ok || !poll.data) {
4038
4316
  console.error(`Error polling sandbox: ${poll.error ?? "Unknown error"}`);
4039
- process10.exit(1);
4317
+ process11.exit(1);
4040
4318
  }
4041
4319
  current = poll.data;
4042
4320
  if (terminalOk.has(current.status) || terminalFail.has(current.status)) break;
@@ -4060,8 +4338,8 @@ async function handleWarm(argv, apiUrl, token) {
4060
4338
  console.log("\nWarming in the background. Poll with: canary sandbox warm " + templateId + " --wait");
4061
4339
  }
4062
4340
  }
4063
- if (terminalFail.has(current.status)) process10.exit(1);
4064
- if (wait && current.status !== "running") process10.exit(1);
4341
+ if (terminalFail.has(current.status)) process11.exit(1);
4342
+ if (wait && current.status !== "running") process11.exit(1);
4065
4343
  }
4066
4344
  function parseDockerPs(stdout) {
4067
4345
  const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -4106,7 +4384,7 @@ async function handleVerify(argv, apiUrl, token) {
4106
4384
  const autoTerminateOnFailure = hasFlag(argv, "--auto-terminate-on-failure");
4107
4385
  if (!templateId) {
4108
4386
  console.error("Error: template ID required. Usage: canary sandbox verify <template-id>");
4109
- process10.exit(1);
4387
+ process11.exit(1);
4110
4388
  }
4111
4389
  console.log(`Verifying template ${templateId}...`);
4112
4390
  const verifyRes = await apiRequest(
@@ -4118,7 +4396,7 @@ async function handleVerify(argv, apiUrl, token) {
4118
4396
  );
4119
4397
  if (!verifyRes.ok) {
4120
4398
  console.error(`Error: ${verifyRes.error}`);
4121
- process10.exit(1);
4399
+ process11.exit(1);
4122
4400
  }
4123
4401
  const instanceId = verifyRes.data.instanceId;
4124
4402
  console.log(` Verification instance: ${instanceId}`);
@@ -4133,7 +4411,7 @@ async function handleVerify(argv, apiUrl, token) {
4133
4411
  let routes = [];
4134
4412
  let routesFetched = false;
4135
4413
  while (Date.now() - startTime < TIMEOUT_MS) {
4136
- await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL_MS2));
4414
+ await new Promise((resolve4) => setTimeout(resolve4, POLL_INTERVAL_MS2));
4137
4415
  const diag = await apiRequest(
4138
4416
  apiUrl,
4139
4417
  token,
@@ -4154,23 +4432,23 @@ async function handleVerify(argv, apiUrl, token) {
4154
4432
  const containers = parseDockerPs(dockerOutput);
4155
4433
  const broken = containers.filter(isBrokenContainer);
4156
4434
  const ttl = remaining > 60 ? `${elapsed}s elapsed` : `${remaining}s remaining`;
4157
- process10.stdout.write(
4435
+ process11.stdout.write(
4158
4436
  `\r [${ttl}] status: ${status}, app: ${appUp ? "up" : "waiting"}, containers: ${containers.length}, unhealthy: ${broken.length} `
4159
4437
  );
4160
4438
  if (status === "running" && appUp) {
4161
- process10.stdout.write("\n");
4439
+ process11.stdout.write("\n");
4162
4440
  finalStatus = "running";
4163
4441
  break;
4164
4442
  }
4165
4443
  if (status === "failed" || status === "terminated") {
4166
- process10.stdout.write("\n");
4444
+ process11.stdout.write("\n");
4167
4445
  finalStatus = status;
4168
4446
  break;
4169
4447
  }
4170
4448
  const shouldDump = !appUp && broken.length > 0 && now - lastDetailDumpAt >= DETAIL_DUMP_INTERVAL_MS;
4171
4449
  if (shouldDump) {
4172
4450
  lastDetailDumpAt = now;
4173
- process10.stdout.write("\n");
4451
+ process11.stdout.write("\n");
4174
4452
  console.log("");
4175
4453
  console.log(` App unreachable after ${elapsed}s. Container state:`);
4176
4454
  for (const c of containers) {
@@ -4235,7 +4513,7 @@ async function handleVerify(argv, apiUrl, token) {
4235
4513
  console.error(` Terminate manually with:`);
4236
4514
  console.error(` canary sandbox terminate ${instanceId}`);
4237
4515
  }
4238
- process10.exit(1);
4516
+ process11.exit(1);
4239
4517
  }
4240
4518
  const evidenceSummary = `Verified via canary sandbox verify \u2014 fresh instance ${instanceId} came up healthy.`;
4241
4519
  for (const itemId of ["server-running", "server-accessible"]) {
@@ -4261,7 +4539,7 @@ async function handleVerify(argv, apiUrl, token) {
4261
4539
  if (!completeRes.ok) {
4262
4540
  console.error(`Failed to record verification: ${completeRes.error}`);
4263
4541
  console.error(`Instance ${instanceId} left running for inspection.`);
4264
- process10.exit(1);
4542
+ process11.exit(1);
4265
4543
  }
4266
4544
  if (!keepInstance) {
4267
4545
  await apiRequest(apiUrl, token, "POST", `/org/environments/instances/${instanceId}/terminate`);
@@ -4306,7 +4584,7 @@ async function handleVerify(argv, apiUrl, token) {
4306
4584
  console.log('Complete: canary sandbox checklist complete <template-id> <item-id> --status success --summary "..."');
4307
4585
  console.log("Skip: canary sandbox checklist skip <template-id> <item-id>");
4308
4586
  }
4309
- process10.exit(1);
4587
+ process11.exit(1);
4310
4588
  }
4311
4589
  async function handleArtifactGet(argv, apiUrl, token) {
4312
4590
  const positional = argv.filter((a) => !a.startsWith("-"));
@@ -4315,7 +4593,7 @@ async function handleArtifactGet(argv, apiUrl, token) {
4315
4593
  const outputFile = getArgValue(argv, "--output");
4316
4594
  if (!templateId || !type) {
4317
4595
  console.error("Error: template ID and artifact type required. Usage: canary sandbox artifact get <template-id> <type>");
4318
- process10.exit(1);
4596
+ process11.exit(1);
4319
4597
  }
4320
4598
  const res = await apiRequest(
4321
4599
  apiUrl,
@@ -4325,14 +4603,14 @@ async function handleArtifactGet(argv, apiUrl, token) {
4325
4603
  );
4326
4604
  if (!res.ok) {
4327
4605
  console.error(`Error: ${res.error}`);
4328
- process10.exit(1);
4606
+ process11.exit(1);
4329
4607
  }
4330
4608
  if (outputFile) {
4331
- const { writeFileSync } = await import("fs");
4332
- writeFileSync(outputFile, res.data.content ?? "", "utf-8");
4609
+ const { writeFileSync: writeFileSync2 } = await import("fs");
4610
+ writeFileSync2(outputFile, res.data.content ?? "", "utf-8");
4333
4611
  console.log(`Written to ${outputFile}`);
4334
4612
  } else {
4335
- process10.stdout.write(res.data.content ?? "");
4613
+ process11.stdout.write(res.data.content ?? "");
4336
4614
  }
4337
4615
  }
4338
4616
  async function handleArtifactCreate(argv, apiUrl, token) {
@@ -4343,17 +4621,17 @@ async function handleArtifactCreate(argv, apiUrl, token) {
4343
4621
  const json = hasFlag(argv, "--json");
4344
4622
  if (!templateId || !type || !filePath) {
4345
4623
  console.error("Error: all args required. Usage: canary sandbox artifact create <template-id> <type> <file>");
4346
- process10.exit(1);
4624
+ process11.exit(1);
4347
4625
  }
4348
4626
  const WELL_KNOWN_TYPES = ["docker-compose", "dockerfile", "startup-script", "setup-script", "seed-script", "environment-config", "file"];
4349
4627
  if (!WELL_KNOWN_TYPES.includes(type)) {
4350
4628
  console.error(`Error: invalid artifact type "${type}". Valid types: ${WELL_KNOWN_TYPES.join(", ")}`);
4351
- process10.exit(1);
4629
+ process11.exit(1);
4352
4630
  }
4353
4631
  const destinationPath = getArgValue(argv, "--path");
4354
4632
  if (type === "file" && !destinationPath) {
4355
4633
  console.error("Error: file artifacts require --path <destination>. Example: --path /workspace/.canary/vite.sandbox.config.ts");
4356
- process10.exit(1);
4634
+ process11.exit(1);
4357
4635
  }
4358
4636
  if (destinationPath && !destinationPath.startsWith("/workspace/.canary/")) {
4359
4637
  const suggestedName = destinationPath.split("/").filter(Boolean).pop() ?? "yourfile";
@@ -4361,14 +4639,14 @@ async function handleArtifactCreate(argv, apiUrl, token) {
4361
4639
  `Error: --path must be under /workspace/.canary/ (the repo tree is user-owned \u2014 sandbox files live in .canary/).
4362
4640
  try: --path /workspace/.canary/${suggestedName}`
4363
4641
  );
4364
- process10.exit(1);
4642
+ process11.exit(1);
4365
4643
  }
4366
4644
  let content;
4367
4645
  try {
4368
- content = readFileSync6(filePath, "utf-8");
4646
+ content = readFileSync7(filePath, "utf-8");
4369
4647
  } catch (err) {
4370
4648
  console.error(`Error reading file: ${err}`);
4371
- process10.exit(1);
4649
+ process11.exit(1);
4372
4650
  }
4373
4651
  const filename = filePath.split("/").pop() ?? filePath;
4374
4652
  const res = await apiRequest(
@@ -4386,7 +4664,7 @@ async function handleArtifactCreate(argv, apiUrl, token) {
4386
4664
  );
4387
4665
  if (!res.ok) {
4388
4666
  console.error(`Error: ${res.error}`);
4389
- process10.exit(1);
4667
+ process11.exit(1);
4390
4668
  }
4391
4669
  if (json) {
4392
4670
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4406,7 +4684,7 @@ async function handleBuild(argv, apiUrl, token) {
4406
4684
  const instanceId = positional[0];
4407
4685
  if (!instanceId) {
4408
4686
  console.error("Error: instance ID required. Usage: canary sandbox build <instance-id>");
4409
- process10.exit(1);
4687
+ process11.exit(1);
4410
4688
  }
4411
4689
  console.log("Building sandbox (this takes a few minutes)...");
4412
4690
  const KICKOFF_TIMEOUT_MS = 8e3;
@@ -4428,7 +4706,7 @@ async function handleBuild(argv, apiUrl, token) {
4428
4706
  buildResult = data.data;
4429
4707
  } else if (data && !data.ok) {
4430
4708
  console.error(`Error: ${data.error ?? "build failed"}`);
4431
- process10.exit(1);
4709
+ process11.exit(1);
4432
4710
  }
4433
4711
  } else {
4434
4712
  const { failure } = result;
@@ -4436,7 +4714,7 @@ async function handleBuild(argv, apiUrl, token) {
4436
4714
  if (!isExpectedAsync) {
4437
4715
  if (isTerminalFailure(failure)) exitOnFailure(failure);
4438
4716
  console.error(`Error: ${failure.message}`);
4439
- process10.exit(1);
4717
+ process11.exit(1);
4440
4718
  }
4441
4719
  }
4442
4720
  if (buildCompleted && buildResult) {
@@ -4445,18 +4723,18 @@ async function handleBuild(argv, apiUrl, token) {
4445
4723
  return;
4446
4724
  }
4447
4725
  if (buildResult.stdout) {
4448
- process10.stdout.write(buildResult.stdout);
4449
- if (!buildResult.stdout.endsWith("\n")) process10.stdout.write("\n");
4726
+ process11.stdout.write(buildResult.stdout);
4727
+ if (!buildResult.stdout.endsWith("\n")) process11.stdout.write("\n");
4450
4728
  }
4451
4729
  if (buildResult.stderr) {
4452
- process10.stderr.write(`STDERR: ${buildResult.stderr}`);
4453
- if (!buildResult.stderr.endsWith("\n")) process10.stderr.write("\n");
4730
+ process11.stderr.write(`STDERR: ${buildResult.stderr}`);
4731
+ if (!buildResult.stderr.endsWith("\n")) process11.stderr.write("\n");
4454
4732
  }
4455
4733
  if (buildResult.exitCode === 0) {
4456
4734
  console.log("Build complete.");
4457
4735
  } else {
4458
4736
  console.error(`Build failed with exit code ${buildResult.exitCode}`);
4459
- process10.exit(1);
4737
+ process11.exit(1);
4460
4738
  }
4461
4739
  return;
4462
4740
  }
@@ -4470,7 +4748,7 @@ async function handleBuild(argv, apiUrl, token) {
4470
4748
  const POLL_INTERVAL_MS2 = 1e4;
4471
4749
  const startTime = Date.now();
4472
4750
  while (Date.now() - startTime < BUILD_POLL_TIMEOUT_MS) {
4473
- await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL_MS2));
4751
+ await new Promise((resolve4) => setTimeout(resolve4, POLL_INTERVAL_MS2));
4474
4752
  const diag = await apiRequest(
4475
4753
  apiUrl,
4476
4754
  token,
@@ -4486,18 +4764,18 @@ async function handleBuild(argv, apiUrl, token) {
4486
4764
  const appUp = appOutput.length > 0 && !appOutput.includes("UNREACHABLE");
4487
4765
  const containerCount = (dockerOutput.match(/\n/g) || []).length + (dockerOutput ? 1 : 0);
4488
4766
  const healthyCount = (dockerOutput.match(/\(healthy\)/g) || []).length;
4489
- process10.stdout.write(`\r [${elapsed}s] containers: ${containerCount}, healthy: ${healthyCount}, app: ${appUp ? "up" : "waiting"} `);
4767
+ process11.stdout.write(`\r [${elapsed}s] containers: ${containerCount}, healthy: ${healthyCount}, app: ${appUp ? "up" : "waiting"} `);
4490
4768
  if (appUp) {
4491
- process10.stdout.write("\n");
4769
+ process11.stdout.write("\n");
4492
4770
  console.log("Build complete.");
4493
4771
  return;
4494
4772
  }
4495
4773
  }
4496
- process10.stdout.write("\n");
4774
+ process11.stdout.write("\n");
4497
4775
  console.error("Build timed out waiting for the app to respond. Check diagnostics and logs:");
4498
4776
  console.error(` canary sandbox diagnostics ${instanceId}`);
4499
4777
  console.error(` canary sandbox logs ${instanceId} --service <name>`);
4500
- process10.exit(1);
4778
+ process11.exit(1);
4501
4779
  }
4502
4780
  async function handleRunCommand(argv, apiUrl, token) {
4503
4781
  const json = hasFlag(argv, "--json");
@@ -4514,7 +4792,7 @@ async function handleRunCommand(argv, apiUrl, token) {
4514
4792
  const command = firstNonFlag >= 0 ? filtered.slice(firstNonFlag + 1).join(" ") : "";
4515
4793
  if (!instanceId || !command) {
4516
4794
  console.error("Error: instance ID and command required. Usage: canary sandbox run-command <instance-id> [--service <name>] [--timeout <ms>] <command...>");
4517
- process10.exit(1);
4795
+ process11.exit(1);
4518
4796
  }
4519
4797
  const timeoutMs = timeoutFlag ? Math.max(parseInt(timeoutFlag, 10), 1e3) : 6e4;
4520
4798
  const res = await apiRequest(
@@ -4527,19 +4805,19 @@ async function handleRunCommand(argv, apiUrl, token) {
4527
4805
  );
4528
4806
  if (!res.ok) {
4529
4807
  console.error(`Error: ${res.error}`);
4530
- process10.exit(1);
4808
+ process11.exit(1);
4531
4809
  }
4532
4810
  if (json) {
4533
4811
  writeRawOutput(JSON.stringify(res.data, null, 2));
4534
4812
  return;
4535
4813
  }
4536
4814
  if (res.data.stdout) {
4537
- process10.stdout.write(res.data.stdout);
4538
- if (!res.data.stdout.endsWith("\n")) process10.stdout.write("\n");
4815
+ process11.stdout.write(res.data.stdout);
4816
+ if (!res.data.stdout.endsWith("\n")) process11.stdout.write("\n");
4539
4817
  }
4540
4818
  if (res.data.stderr) {
4541
- process10.stderr.write(`STDERR: ${res.data.stderr}`);
4542
- if (!res.data.stderr.endsWith("\n")) process10.stderr.write("\n");
4819
+ process11.stderr.write(`STDERR: ${res.data.stderr}`);
4820
+ if (!res.data.stderr.endsWith("\n")) process11.stderr.write("\n");
4543
4821
  }
4544
4822
  if (res.data.exitCode !== 0) {
4545
4823
  console.log(`Exit code: ${res.data.exitCode}`);
@@ -4551,7 +4829,7 @@ async function handleDiagnostics(argv, apiUrl, token) {
4551
4829
  const json = hasFlag(argv, "--json");
4552
4830
  if (!instanceId) {
4553
4831
  console.error("Error: instance ID required. Usage: canary sandbox diagnostics <instance-id>");
4554
- process10.exit(1);
4832
+ process11.exit(1);
4555
4833
  }
4556
4834
  const res = await apiRequest(
4557
4835
  apiUrl,
@@ -4563,7 +4841,7 @@ async function handleDiagnostics(argv, apiUrl, token) {
4563
4841
  );
4564
4842
  if (!res.ok) {
4565
4843
  console.error(`Error: ${res.error}`);
4566
- process10.exit(1);
4844
+ process11.exit(1);
4567
4845
  }
4568
4846
  const routes = await fetchInstanceRoutes(apiUrl, token, instanceId);
4569
4847
  const data = { ...res.data, routes };
@@ -4618,7 +4896,7 @@ async function handleTerminate(argv, apiUrl, token) {
4618
4896
  const instanceId = positional[0];
4619
4897
  if (!instanceId) {
4620
4898
  console.error("Error: instance ID required. Usage: canary sandbox terminate <instance-id>");
4621
- process10.exit(1);
4899
+ process11.exit(1);
4622
4900
  }
4623
4901
  const res = await apiRequest(
4624
4902
  apiUrl,
@@ -4628,7 +4906,7 @@ async function handleTerminate(argv, apiUrl, token) {
4628
4906
  );
4629
4907
  if (!res.ok) {
4630
4908
  console.error(`Error: ${res.error}`);
4631
- process10.exit(1);
4909
+ process11.exit(1);
4632
4910
  }
4633
4911
  console.log(`Instance ${instanceId} termination initiated.`);
4634
4912
  }
@@ -4637,7 +4915,7 @@ async function handleDelete(argv, apiUrl, token) {
4637
4915
  const templateId = positional[0];
4638
4916
  if (!templateId) {
4639
4917
  console.error("Error: template ID required. Usage: canary sandbox delete <template-id>");
4640
- process10.exit(1);
4918
+ process11.exit(1);
4641
4919
  }
4642
4920
  const res = await apiRequest(
4643
4921
  apiUrl,
@@ -4647,7 +4925,7 @@ async function handleDelete(argv, apiUrl, token) {
4647
4925
  );
4648
4926
  if (!res.ok) {
4649
4927
  console.error(`Error: ${res.error}`);
4650
- process10.exit(1);
4928
+ process11.exit(1);
4651
4929
  }
4652
4930
  console.log(`Template ${templateId} deleted.`);
4653
4931
  }
@@ -4657,7 +4935,7 @@ async function handleActionList(argv, apiUrl, token) {
4657
4935
  const json = hasFlag(argv, "--json");
4658
4936
  if (!templateId) {
4659
4937
  console.error("Error: template ID required. Usage: canary sandbox action list <template-id>");
4660
- process10.exit(1);
4938
+ process11.exit(1);
4661
4939
  }
4662
4940
  const res = await apiRequest(
4663
4941
  apiUrl,
@@ -4667,7 +4945,7 @@ async function handleActionList(argv, apiUrl, token) {
4667
4945
  );
4668
4946
  if (!res.ok) {
4669
4947
  console.error(`Error: ${res.error}`);
4670
- process10.exit(1);
4948
+ process11.exit(1);
4671
4949
  }
4672
4950
  if (json) {
4673
4951
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4691,7 +4969,7 @@ async function handleActionGet(argv, apiUrl, token) {
4691
4969
  const json = hasFlag(argv, "--json");
4692
4970
  if (!templateId || !slug) {
4693
4971
  console.error("Error: template ID and slug required. Usage: canary sandbox action get <template-id> <slug>");
4694
- process10.exit(1);
4972
+ process11.exit(1);
4695
4973
  }
4696
4974
  const res = await apiRequest(
4697
4975
  apiUrl,
@@ -4701,7 +4979,7 @@ async function handleActionGet(argv, apiUrl, token) {
4701
4979
  );
4702
4980
  if (!res.ok) {
4703
4981
  console.error(`Error: ${res.error}`);
4704
- process10.exit(1);
4982
+ process11.exit(1);
4705
4983
  }
4706
4984
  if (json) {
4707
4985
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4734,7 +5012,7 @@ async function handleActionCreate(argv, apiUrl, token) {
4734
5012
  const json = hasFlag(argv, "--json");
4735
5013
  if (!templateId || !slug) {
4736
5014
  console.error("Error: template ID and slug required. Usage: canary sandbox action create <template-id> <slug> [options]");
4737
- process10.exit(1);
5015
+ process11.exit(1);
4738
5016
  }
4739
5017
  const scriptPath = getArgValue(argv, "--script");
4740
5018
  const inputSchemaPath = getArgValue(argv, "--input-schema");
@@ -4752,63 +5030,63 @@ async function handleActionCreate(argv, apiUrl, token) {
4752
5030
  const provisionOrderStr = getArgValue(argv, "--provision-order");
4753
5031
  if (provisioningMode && provisioningMode !== "shared" && provisioningMode !== "unique") {
4754
5032
  console.error("Error: --provisioning-mode must be 'shared' or 'unique'");
4755
- process10.exit(1);
5033
+ process11.exit(1);
4756
5034
  }
4757
5035
  if (provisionOrderStr && !/^-?\d+$/.test(provisionOrderStr)) {
4758
5036
  console.error("Error: --provision-order must be an integer");
4759
- process10.exit(1);
5037
+ process11.exit(1);
4760
5038
  }
4761
5039
  if (!scriptPath) {
4762
5040
  console.error("Error: --script <file> is required");
4763
- process10.exit(1);
5041
+ process11.exit(1);
4764
5042
  }
4765
5043
  if (!inputSchemaPath) {
4766
5044
  console.error("Error: --input-schema <file> is required");
4767
- process10.exit(1);
5045
+ process11.exit(1);
4768
5046
  }
4769
5047
  let scriptContent;
4770
5048
  try {
4771
- scriptContent = readFileSync6(scriptPath, "utf-8");
5049
+ scriptContent = readFileSync7(scriptPath, "utf-8");
4772
5050
  } catch (err) {
4773
5051
  console.error(`Error reading script file: ${err}`);
4774
- process10.exit(1);
5052
+ process11.exit(1);
4775
5053
  }
4776
5054
  let inputSchema;
4777
5055
  try {
4778
- inputSchema = JSON.parse(readFileSync6(inputSchemaPath, "utf-8"));
5056
+ inputSchema = JSON.parse(readFileSync7(inputSchemaPath, "utf-8"));
4779
5057
  } catch (err) {
4780
5058
  console.error(`Error reading input schema file: ${err}`);
4781
- process10.exit(1);
5059
+ process11.exit(1);
4782
5060
  }
4783
5061
  let outputSchema;
4784
5062
  if (outputSchemaPath) {
4785
5063
  try {
4786
- outputSchema = JSON.parse(readFileSync6(outputSchemaPath, "utf-8"));
5064
+ outputSchema = JSON.parse(readFileSync7(outputSchemaPath, "utf-8"));
4787
5065
  } catch (err) {
4788
5066
  console.error(`Error reading output schema file: ${err}`);
4789
- process10.exit(1);
5067
+ process11.exit(1);
4790
5068
  }
4791
5069
  }
4792
5070
  let credentialOutputConfig;
4793
5071
  if (credentialOutputConfigPath) {
4794
5072
  try {
4795
- credentialOutputConfig = JSON.parse(readFileSync6(credentialOutputConfigPath, "utf-8"));
5073
+ credentialOutputConfig = JSON.parse(readFileSync7(credentialOutputConfigPath, "utf-8"));
4796
5074
  } catch (err) {
4797
5075
  console.error(`Error reading credential output config file: ${err}`);
4798
- process10.exit(1);
5076
+ process11.exit(1);
4799
5077
  }
4800
5078
  }
4801
5079
  let outputConfig;
4802
5080
  if (outputConfigPath) {
4803
5081
  try {
4804
- outputConfig = JSON.parse(readFileSync6(outputConfigPath, "utf-8"));
5082
+ outputConfig = JSON.parse(readFileSync7(outputConfigPath, "utf-8"));
4805
5083
  } catch (err) {
4806
5084
  console.error(`Error reading output config file: ${err}`);
4807
- process10.exit(1);
5085
+ process11.exit(1);
4808
5086
  }
4809
5087
  if (!Array.isArray(outputConfig)) {
4810
5088
  console.error("Error: --output-config must contain a JSON array of output slots");
4811
- process10.exit(1);
5089
+ process11.exit(1);
4812
5090
  }
4813
5091
  }
4814
5092
  const body = {
@@ -4836,7 +5114,7 @@ async function handleActionCreate(argv, apiUrl, token) {
4836
5114
  );
4837
5115
  if (!res.ok) {
4838
5116
  console.error(`Error: ${res.error}`);
4839
- process10.exit(1);
5117
+ process11.exit(1);
4840
5118
  }
4841
5119
  if (json) {
4842
5120
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4850,7 +5128,7 @@ async function handleActionDelete(argv, apiUrl, token) {
4850
5128
  const slug = positional[1];
4851
5129
  if (!templateId || !slug) {
4852
5130
  console.error("Error: template ID and slug required. Usage: canary sandbox action delete <template-id> <slug>");
4853
- process10.exit(1);
5131
+ process11.exit(1);
4854
5132
  }
4855
5133
  const res = await apiRequest(
4856
5134
  apiUrl,
@@ -4860,7 +5138,7 @@ async function handleActionDelete(argv, apiUrl, token) {
4860
5138
  );
4861
5139
  if (!res.ok) {
4862
5140
  console.error(`Error: ${res.error}`);
4863
- process10.exit(1);
5141
+ process11.exit(1);
4864
5142
  }
4865
5143
  console.log(`Action '${slug}' deleted.`);
4866
5144
  }
@@ -4871,16 +5149,16 @@ async function handleActionExecute(argv, apiUrl, token) {
4871
5149
  const json = hasFlag(argv, "--json");
4872
5150
  if (!instanceId || !slug) {
4873
5151
  console.error("Error: instance ID and slug required. Usage: canary sandbox action execute <instance-id> <slug> [options]");
4874
- process10.exit(1);
5152
+ process11.exit(1);
4875
5153
  }
4876
5154
  let params = {};
4877
5155
  const paramsFile = getArgValue(argv, "--params-file");
4878
5156
  if (paramsFile) {
4879
5157
  try {
4880
- params = JSON.parse(readFileSync6(paramsFile, "utf-8"));
5158
+ params = JSON.parse(readFileSync7(paramsFile, "utf-8"));
4881
5159
  } catch (err) {
4882
5160
  console.error(`Error reading params file: ${err}`);
4883
- process10.exit(1);
5161
+ process11.exit(1);
4884
5162
  }
4885
5163
  }
4886
5164
  for (let i = 0; i < argv.length; i++) {
@@ -4899,11 +5177,11 @@ async function handleActionExecute(argv, apiUrl, token) {
4899
5177
  const parsed = Number(timeoutArg);
4900
5178
  if (!Number.isFinite(parsed) || parsed <= 0) {
4901
5179
  console.error(`Error: --timeout-ms must be a positive number, got '${timeoutArg}'`);
4902
- process10.exit(1);
5180
+ process11.exit(1);
4903
5181
  }
4904
5182
  if (!runAsync) {
4905
5183
  console.error("Error: --timeout-ms only applies with --async");
4906
- process10.exit(1);
5184
+ process11.exit(1);
4907
5185
  }
4908
5186
  executionTimeoutMs = parsed;
4909
5187
  }
@@ -4921,7 +5199,7 @@ async function handleActionExecute(argv, apiUrl, token) {
4921
5199
  );
4922
5200
  if (!res.ok) {
4923
5201
  console.error(`Error: ${res.error}`);
4924
- process10.exit(1);
5202
+ process11.exit(1);
4925
5203
  }
4926
5204
  if (json) {
4927
5205
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -4962,7 +5240,7 @@ async function handleActionExecutions(argv, apiUrl, token) {
4962
5240
  const json = hasFlag(argv, "--json");
4963
5241
  if (!instanceId) {
4964
5242
  console.error("Error: instance ID required. Usage: canary sandbox action executions <instance-id>");
4965
- process10.exit(1);
5243
+ process11.exit(1);
4966
5244
  }
4967
5245
  const res = await apiRequest(
4968
5246
  apiUrl,
@@ -4972,7 +5250,7 @@ async function handleActionExecutions(argv, apiUrl, token) {
4972
5250
  );
4973
5251
  if (!res.ok) {
4974
5252
  console.error(`Error: ${res.error}`);
4975
- process10.exit(1);
5253
+ process11.exit(1);
4976
5254
  }
4977
5255
  if (json) {
4978
5256
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -5016,7 +5294,7 @@ async function handleActionExecutionGet(argv, apiUrl, token) {
5016
5294
  console.error(
5017
5295
  "Error: instance ID and execution ID required. Usage: cofactor sandbox action execution get <instance-id> <execution-id>"
5018
5296
  );
5019
- process10.exit(1);
5297
+ process11.exit(1);
5020
5298
  }
5021
5299
  const res = await apiRequest(
5022
5300
  apiUrl,
@@ -5026,7 +5304,7 @@ async function handleActionExecutionGet(argv, apiUrl, token) {
5026
5304
  );
5027
5305
  if (!res.ok) {
5028
5306
  console.error(`Error: ${res.error}`);
5029
- process10.exit(1);
5307
+ process11.exit(1);
5030
5308
  }
5031
5309
  if (json) {
5032
5310
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -5043,7 +5321,7 @@ async function handleActionExecutionCancel(argv, apiUrl, token) {
5043
5321
  console.error(
5044
5322
  "Error: instance ID and execution ID required. Usage: cofactor sandbox action execution cancel <instance-id> <execution-id>"
5045
5323
  );
5046
- process10.exit(1);
5324
+ process11.exit(1);
5047
5325
  }
5048
5326
  const res = await apiRequest(
5049
5327
  apiUrl,
@@ -5053,7 +5331,7 @@ async function handleActionExecutionCancel(argv, apiUrl, token) {
5053
5331
  );
5054
5332
  if (!res.ok) {
5055
5333
  console.error(`Error: ${res.error}`);
5056
- process10.exit(1);
5334
+ process11.exit(1);
5057
5335
  }
5058
5336
  if (json) {
5059
5337
  writeRawOutput(JSON.stringify(res.data, null, 2));
@@ -5088,7 +5366,7 @@ async function handleRepoList(argv, apiUrl, token) {
5088
5366
  const templateId = getArgValue(argv, "--template");
5089
5367
  if (!templateId) {
5090
5368
  console.error("Error: --template is required.");
5091
- process10.exit(1);
5369
+ process11.exit(1);
5092
5370
  }
5093
5371
  const result = await apiRequest(
5094
5372
  apiUrl,
@@ -5098,7 +5376,7 @@ async function handleRepoList(argv, apiUrl, token) {
5098
5376
  );
5099
5377
  if (!result.ok) {
5100
5378
  console.error(`Error: ${result.error ?? "Unknown error"}`);
5101
- process10.exit(1);
5379
+ process11.exit(1);
5102
5380
  }
5103
5381
  if (json) {
5104
5382
  writeRawOutput(JSON.stringify(result.items ?? [], null, 2));
@@ -5127,7 +5405,7 @@ async function handleRepoAdd(argv, apiUrl, token) {
5127
5405
  if (!templateId || !propertyGithubRepoId) {
5128
5406
  console.error("Error: --template and --property-github-repo-id are required.");
5129
5407
  console.error("Tip: link the repo to the property first via `canary property repo link`, then pass its id here.");
5130
- process10.exit(1);
5408
+ process11.exit(1);
5131
5409
  }
5132
5410
  const parsed = spec ? parseRepoSpec(spec) : {};
5133
5411
  const body = { propertyGithubRepoId };
@@ -5148,7 +5426,7 @@ async function handleRepoAdd(argv, apiUrl, token) {
5148
5426
  if (!result.ok) {
5149
5427
  if (json) writeRawOutput(JSON.stringify(result, null, 2));
5150
5428
  else console.error(`Error: ${result.error ?? "Unknown error"}`);
5151
- process10.exit(1);
5429
+ process11.exit(1);
5152
5430
  }
5153
5431
  if (json) writeRawOutput(JSON.stringify(result.data, null, 2));
5154
5432
  else console.log(`Linked repo to template ${templateId} (link=${result.data?.id})`);
@@ -5163,7 +5441,7 @@ async function handleRepoUpdate(argv, apiUrl, token) {
5163
5441
  const serviceName = getArgValue(argv, "--service-name");
5164
5442
  if (!templateId || !linkId) {
5165
5443
  console.error("Error: --template and --link-id are required.");
5166
- process10.exit(1);
5444
+ process11.exit(1);
5167
5445
  }
5168
5446
  const body = {};
5169
5447
  if (role !== void 0) body.role = role;
@@ -5180,7 +5458,7 @@ async function handleRepoUpdate(argv, apiUrl, token) {
5180
5458
  if (!result.ok) {
5181
5459
  if (json) writeRawOutput(JSON.stringify(result, null, 2));
5182
5460
  else console.error(`Error: ${result.error ?? "Unknown error"}`);
5183
- process10.exit(1);
5461
+ process11.exit(1);
5184
5462
  }
5185
5463
  if (json) writeRawOutput(JSON.stringify(result.data, null, 2));
5186
5464
  else console.log(`Updated repo link ${linkId}`);
@@ -5191,7 +5469,7 @@ async function handleRepoRemove(argv, apiUrl, token) {
5191
5469
  const linkId = getArgValue(argv, "--link-id");
5192
5470
  if (!templateId || !linkId) {
5193
5471
  console.error("Error: --template and --link-id are required.");
5194
- process10.exit(1);
5472
+ process11.exit(1);
5195
5473
  }
5196
5474
  const result = await apiRequest(
5197
5475
  apiUrl,
@@ -5202,7 +5480,7 @@ async function handleRepoRemove(argv, apiUrl, token) {
5202
5480
  if (!result.ok) {
5203
5481
  if (json) writeRawOutput(JSON.stringify(result, null, 2));
5204
5482
  else console.error(`Error: ${result.error ?? "Unknown error"}`);
5205
- process10.exit(1);
5483
+ process11.exit(1);
5206
5484
  }
5207
5485
  if (json) writeRawOutput(JSON.stringify(result, null, 2));
5208
5486
  else console.log(`Removed repo link ${linkId} from template ${templateId}`);
@@ -5231,7 +5509,7 @@ Examples:
5231
5509
  canary sandbox repo add --template <template-id> --property-github-repo-id <uuid> --role frontend
5232
5510
  canary sandbox repo add --template <template-id> --property-github-repo-id <uuid> --spec frontend=owner/web@main:/apps/web`
5233
5511
  );
5234
- process10.exit(1);
5512
+ process11.exit(1);
5235
5513
  }
5236
5514
  }
5237
5515
  async function runSandbox(argv) {
@@ -5250,7 +5528,7 @@ async function runSandbox(argv) {
5250
5528
  const sections = legacy ? SANDBOX_GUIDE_SECTIONS : SANDBOX_V2_GUIDE_SECTIONS;
5251
5529
  if (section && !sections.some((s) => s.slug === section) && SANDBOX_GUIDE_SECTIONS.some((s) => s.slug === section)) {
5252
5530
  console.error(`"${section}" is a section of the v1 sandbox guide: run "cofactor sandbox guide v1 ${section}".`);
5253
- process10.exit(1);
5531
+ process11.exit(1);
5254
5532
  }
5255
5533
  await printServedGuide(legacy ? "sandbox" : "sandbox-v2", guideRest);
5256
5534
  return;
@@ -5305,7 +5583,7 @@ async function runSandbox(argv) {
5305
5583
  await handlePoolSetSize(poolRest, apiUrl, token);
5306
5584
  } else {
5307
5585
  console.error(`Unknown pool action: ${poolAction}. Use "set-size".`);
5308
- process10.exit(1);
5586
+ process11.exit(1);
5309
5587
  }
5310
5588
  break;
5311
5589
  }
@@ -5317,7 +5595,7 @@ async function runSandbox(argv) {
5317
5595
  await handleArtifactCreate(actionRest, apiUrl, token);
5318
5596
  } else {
5319
5597
  console.error(`Unknown artifact action: ${action2}. Use "get" or "create".`);
5320
- process10.exit(1);
5598
+ process11.exit(1);
5321
5599
  }
5322
5600
  break;
5323
5601
  }
@@ -5331,7 +5609,7 @@ async function runSandbox(argv) {
5331
5609
  await handleConfigUnset(configRest, apiUrl, token);
5332
5610
  } else {
5333
5611
  console.error(`Unknown config action: ${configAction}. Use "list", "set", or "unset".`);
5334
- process10.exit(1);
5612
+ process11.exit(1);
5335
5613
  }
5336
5614
  break;
5337
5615
  }
@@ -5367,13 +5645,13 @@ async function runSandbox(argv) {
5367
5645
  await handleActionExecutionCancel(execRest, apiUrl, token);
5368
5646
  } else {
5369
5647
  console.error(`Unknown execution sub-command: ${execSub}. Use "get" or "cancel".`);
5370
- process10.exit(1);
5648
+ process11.exit(1);
5371
5649
  }
5372
5650
  break;
5373
5651
  }
5374
5652
  default:
5375
5653
  console.error(`Unknown action sub-command: ${actionSub}. Use "list", "get", "create", "delete", "execute", "executions", or "execution".`);
5376
- process10.exit(1);
5654
+ process11.exit(1);
5377
5655
  }
5378
5656
  break;
5379
5657
  }
@@ -5425,6 +5703,9 @@ async function runSandbox(argv) {
5425
5703
  case "sync":
5426
5704
  await handleSandboxSync(v2Positionals("sync", args), args, apiUrl, token);
5427
5705
  break;
5706
+ case "auth-check":
5707
+ await handleSandboxAuthCheck(args, apiUrl, token);
5708
+ break;
5428
5709
  case "verify-env":
5429
5710
  await handleSandboxVerifyEnv(args, apiUrl, token);
5430
5711
  break;
@@ -5440,7 +5721,7 @@ async function runSandbox(argv) {
5440
5721
  default:
5441
5722
  console.error(`Unknown sub-command: ${subCommand}`);
5442
5723
  printHelp();
5443
- process10.exit(1);
5724
+ process11.exit(1);
5444
5725
  }
5445
5726
  }
5446
5727
  export {
@@ -5457,4 +5738,4 @@ export {
5457
5738
  withPluginEnabled,
5458
5739
  withoutPlugin
5459
5740
  };
5460
- //# sourceMappingURL=sandbox-R6EMGOKR.js.map
5741
+ //# sourceMappingURL=sandbox-FBLJEYA2.js.map