excalidrop 0.2.1 → 0.2.2

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 (153) hide show
  1. package/README.md +19 -1
  2. package/dist/cli.d.ts +7 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +538 -85
  5. package/dist/cli.js.map +1 -1
  6. package/dist/frontend/assets/{advancedFormat-CsxROTkH.js → advancedFormat-Be4KR5mp.js} +1 -1
  7. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-Bu8X6mOi.js → ar-SA-G6X2FPQ2-B6ekz3Ba.js} +1 -1
  8. package/dist/frontend/assets/arc-DlgXyGPj.js +1 -0
  9. package/dist/frontend/assets/{az-AZ-76LH7QW2-DpvzY31_.js → az-AZ-76LH7QW2-01MDUsF0.js} +1 -1
  10. package/dist/frontend/assets/{bg-BG-XCXSNQG7-DCtZnLyo.js → bg-BG-XCXSNQG7--fArB8Ao.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-38ab4fdb-4cfDkUsw.js → blockDiagram-38ab4fdb-CmsVhOwi.js} +1 -1
  12. package/dist/frontend/assets/{blockDiagram-68f4deed-CsgWL6uC.js → blockDiagram-68f4deed-DU_wNYlO.js} +6 -6
  13. package/dist/frontend/assets/{bn-BD-2XOGV67Q-B-IdLNKT.js → bn-BD-2XOGV67Q-h8juqzfQ.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-15b5d702-7BAqo4qT.js → c4Diagram-15b5d702-ClZalz0L.js} +1 -1
  15. package/dist/frontend/assets/{c4Diagram-3d4e48cf-CDW-O-vg.js → c4Diagram-3d4e48cf-BkFVM0tP.js} +1 -1
  16. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-DM8olClc.js → ca-ES-6MX7JW3Y-DMBR3qVX.js} +1 -1
  17. package/dist/frontend/assets/channel-BxFR7168.js +1 -0
  18. package/dist/frontend/assets/{classDiagram-70f12bd4-D2U4i4yC.js → classDiagram-70f12bd4-BXAJnakg.js} +1 -1
  19. package/dist/frontend/assets/{classDiagram-d40c83e7-DtBHRJc9.js → classDiagram-d40c83e7-DVhipa45.js} +1 -1
  20. package/dist/frontend/assets/{classDiagram-v2-d5a6b087-Drykcxns.js → classDiagram-v2-d5a6b087-CiHMsbhj.js} +1 -1
  21. package/dist/frontend/assets/classDiagram-v2-f2320105-1CqIxWOB.js +2 -0
  22. package/dist/frontend/assets/clone-CMl85WSi.js +1 -0
  23. package/dist/frontend/assets/{createText-2e5e7dd3-1KPUhDpM.js → createText-2e5e7dd3-D3kSz3YE.js} +1 -1
  24. package/dist/frontend/assets/{createText-d213de94-C6EhGbUN.js → createText-d213de94-DvJeLlQX.js} +1 -1
  25. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-DgjTgz6K.js → cs-CZ-2BRQDIVT-BUcdPrAr.js} +1 -1
  26. package/dist/frontend/assets/{cytoscape-cose-bilkent-BnIHl72R.js → cytoscape-cose-bilkent-CKoSm_2z.js} +1 -1
  27. package/dist/frontend/assets/{da-DK-5WZEPLOC-uEs3xGKF.js → da-DK-5WZEPLOC-C1nkchuC.js} +1 -1
  28. package/dist/frontend/assets/{de-DE-XR44H4JA-CBglLw42.js → de-DE-XR44H4JA-DO2_bO1X.js} +1 -1
  29. package/dist/frontend/assets/{edges-332bd1c7-cWCBwcrg.js → edges-332bd1c7-C5qM1sDU.js} +1 -1
  30. package/dist/frontend/assets/{edges-e0da2a9e-DKeDUJ_F.js → edges-e0da2a9e-BRt7qomS.js} +1 -1
  31. package/dist/frontend/assets/{el-GR-BZB4AONW-B-_eub00.js → el-GR-BZB4AONW-CrjSdaU2.js} +1 -1
  32. package/dist/frontend/assets/{elk.bundled-Ch9TeAu4.js → elk.bundled-Dmec6Rq6.js} +1 -1
  33. package/dist/frontend/assets/{erDiagram-880f2ed8-CtDf_58r.js → erDiagram-880f2ed8-BI7kP523.js} +1 -1
  34. package/dist/frontend/assets/{erDiagram-9861fffd-CAlUeljk.js → erDiagram-9861fffd-DMyCDrgI.js} +1 -1
  35. package/dist/frontend/assets/{es-ES-U4NZUMDT-BIN3bSVD.js → es-ES-U4NZUMDT-rrdjPi-M.js} +1 -1
  36. package/dist/frontend/assets/{eu-ES-A7QVB2H4-TyQet3-N.js → eu-ES-A7QVB2H4-uYn3S3D5.js} +1 -1
  37. package/dist/frontend/assets/{fa-IR-HGAKTJCU-DVmjjr57.js → fa-IR-HGAKTJCU-DWW_BVFR.js} +1 -1
  38. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-ZW9kXLa8.js → fi-FI-Z5N7JZ37-CMXxeZ4W.js} +1 -1
  39. package/dist/frontend/assets/{flowDb-7c981674-B5ppx8EN.js → flowDb-7c981674-Dsj6xMzr.js} +1 -1
  40. package/dist/frontend/assets/{flowDb-956e92f1-Dqi00Wq-.js → flowDb-956e92f1-26g7kJju.js} +1 -1
  41. package/dist/frontend/assets/{flowDiagram-66a62f08-v3Liu0m_.js → flowDiagram-66a62f08-C9eQZtdf.js} +1 -1
  42. package/dist/frontend/assets/{flowDiagram-cbd28bf7-DJ_YHRwa.js → flowDiagram-cbd28bf7-D051xFrI.js} +1 -1
  43. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-DIa4RdMh.js +1 -0
  44. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CnvFT8HV.js +1 -0
  45. package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-BleYul6b.js → flowchart-elk-definition-36e2d292-C9fsvhV-.js} +1 -1
  46. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-D6lLh4Ih.js → flowchart-elk-definition-4a651766-BkVNuJOw.js} +1 -1
  47. package/dist/frontend/assets/{fr-FR-RHASNOE6-CxLR66tH.js → fr-FR-RHASNOE6-DIfK_rjW.js} +1 -1
  48. package/dist/frontend/assets/{ganttDiagram-04f9e578-DJSOyr8B.js → ganttDiagram-04f9e578-CZeF2a4Y.js} +1 -1
  49. package/dist/frontend/assets/{ganttDiagram-c361ad54-CJmbCLvp.js → ganttDiagram-c361ad54-DbkIZ-wU.js} +1 -1
  50. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-DiRq_FJc.js → gitGraphDiagram-21fc4d3e-CLqlSEuE.js} +1 -1
  51. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-MfvDzoAb.js → gitGraphDiagram-72cf32ee-Cvf9uYWB.js} +1 -1
  52. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CxWj2itK.js → gl-ES-HMX3MZ6V-CCxCH2oK.js} +1 -1
  53. package/dist/frontend/assets/{graph-CT_r46y9.js → graph-MptL1QoF.js} +1 -1
  54. package/dist/frontend/assets/{graph-DJt_Vezo.js → graph-Mzuu2QFH.js} +1 -1
  55. package/dist/frontend/assets/{he-IL-6SHJWFNN-5v2WFGO3.js → he-IL-6SHJWFNN-CJuJutJZ.js} +1 -1
  56. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-BJGbvgRu.js → hi-IN-IWLTKZ5I-BORPH6vU.js} +1 -1
  57. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-REQT2EcE.js → hu-HU-A5ZG7DT2-Dr7UDbjo.js} +1 -1
  58. package/dist/frontend/assets/{id-ID-SAP4L64H-aZWQYWdg.js → id-ID-SAP4L64H-D0Widkq7.js} +1 -1
  59. package/dist/frontend/assets/{index-3862675e-BgrvgbsM.js → index-3862675e-DVT5-vN2.js} +1 -1
  60. package/dist/frontend/assets/{index-6079d271-Cp8jFHTC.js → index-6079d271-R4KRnW5Z.js} +1 -1
  61. package/dist/frontend/assets/{index-DBHHeSXy.js → index-B45XzZiR.js} +4 -4
  62. package/dist/frontend/assets/index-Bd-8869C.js +349 -0
  63. package/dist/frontend/assets/{infoDiagram-4a4f5b27-C1ZJJtWv.js → infoDiagram-4a4f5b27-BvY1qS17.js} +1 -1
  64. package/dist/frontend/assets/{infoDiagram-f8f76790-GeS0JKxo.js → infoDiagram-f8f76790-D60YvG33.js} +1 -1
  65. package/dist/frontend/assets/{it-IT-JPQ66NNP-EyeA2Ty1.js → it-IT-JPQ66NNP-C08Vu0BG.js} +1 -1
  66. package/dist/frontend/assets/{ja-JP-DBVTYXUO-BLcE45D9.js → ja-JP-DBVTYXUO-Ciu7LaRi.js} +1 -1
  67. package/dist/frontend/assets/{journeyDiagram-29694f62-4Z_lpK4g.js → journeyDiagram-29694f62-DvrnJXsd.js} +1 -1
  68. package/dist/frontend/assets/{journeyDiagram-49397b02-ChwU3Zo6.js → journeyDiagram-49397b02-Dy9LU86E.js} +1 -1
  69. package/dist/frontend/assets/{kaa-6HZHGXH3-CkpTcX9X.js → kaa-6HZHGXH3-GhGTIjpu.js} +1 -1
  70. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-CChq0y6o.js → kab-KAB-ZGHBKWFO-B3q4BqYw.js} +1 -1
  71. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-CwtoTY4z.js → kk-KZ-P5N5QNE5-GWoLZvco.js} +1 -1
  72. package/dist/frontend/assets/{km-KH-HSX4SM5Z-D26sdohj.js → km-KH-HSX4SM5Z-CnAQJCYr.js} +1 -1
  73. package/dist/frontend/assets/{ko-KR-MTYHY66A-DvnDcJQA.js → ko-KR-MTYHY66A-B2Rh9krp.js} +1 -1
  74. package/dist/frontend/assets/{ku-TR-6OUDTVRD-qdHbLgKg.js → ku-TR-6OUDTVRD-C27elmON.js} +1 -1
  75. package/dist/frontend/assets/{layout-YRLNN4au.js → layout-D1hl6oKi.js} +1 -1
  76. package/dist/frontend/assets/{layout-C-wUeNvb.js → layout-c-hOA8zE.js} +1 -1
  77. package/dist/frontend/assets/{line-BEgMClKG.js → line-DtMz1ssZ.js} +1 -1
  78. package/dist/frontend/assets/{linear-DDHpSdYw.js → linear-D-Y4WtO_.js} +1 -1
  79. package/dist/frontend/assets/{lt-LT-XHIRWOB4-DT4LYe_d.js → lt-LT-XHIRWOB4-B6jd693w.js} +1 -1
  80. package/dist/frontend/assets/{lv-LV-5QDEKY6T-Dbc3ivwT.js → lv-LV-5QDEKY6T-sXB-8i6R.js} +1 -1
  81. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-CY2RhWUN.js → mindmap-definition-ac74a2e8-CawO9I_f.js} +1 -1
  82. package/dist/frontend/assets/{mindmap-definition-fc14e90a-VT4JDi6N.js → mindmap-definition-fc14e90a-CXMNA68v.js} +4 -4
  83. package/dist/frontend/assets/{mr-IN-CRQNXWMA-D75p1fVc.js → mr-IN-CRQNXWMA-BJ6QSiPi.js} +1 -1
  84. package/dist/frontend/assets/{my-MM-5M5IBNSE-DnEoBXyE.js → my-MM-5M5IBNSE-BMdOIRP5.js} +1 -1
  85. package/dist/frontend/assets/{nb-NO-T6EIAALU-BL4ThJxl.js → nb-NO-T6EIAALU-BVG6JUMN.js} +1 -1
  86. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-u18nUblY.js → nl-NL-IS3SIHDZ-ZprCvTYR.js} +1 -1
  87. package/dist/frontend/assets/{nn-NO-6E72VCQL-B9LJ0oXc.js → nn-NO-6E72VCQL-CcljFAGy.js} +1 -1
  88. package/dist/frontend/assets/{oc-FR-POXYY2M6-BKG5kIcO.js → oc-FR-POXYY2M6-zCuu6Ns2.js} +1 -1
  89. package/dist/frontend/assets/{pa-IN-N4M65BXN-CmpnjbkL.js → pa-IN-N4M65BXN-D_iftL9v.js} +1 -1
  90. package/dist/frontend/assets/{pica-BcpcM0v1.js → pica-DsW4eP6T.js} +1 -1
  91. package/dist/frontend/assets/{pie-Swzl5NrI.js → pie-U_uHwWsL.js} +1 -1
  92. package/dist/frontend/assets/{pieDiagram-421022e6-ndz96BLe.js → pieDiagram-421022e6-naaI7HiE.js} +1 -1
  93. package/dist/frontend/assets/{pieDiagram-8a3498a8-D5rcV68O.js → pieDiagram-8a3498a8-m6nTf1O_.js} +1 -1
  94. package/dist/frontend/assets/{pl-PL-T2D74RX3-CrR66Rgj.js → pl-PL-T2D74RX3-CRQQfZUe.js} +1 -1
  95. package/dist/frontend/assets/{pt-BR-5N22H2LF-Crv7jZiH.js → pt-BR-5N22H2LF-DLvfG7RR.js} +1 -1
  96. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-Bkaa5ESR.js → pt-PT-UZXXM6DQ-Chkg6-JC.js} +1 -1
  97. package/dist/frontend/assets/{quadrantDiagram-0957ecba-DOdyxeTP.js → quadrantDiagram-0957ecba-BJR82kAx.js} +1 -1
  98. package/dist/frontend/assets/{quadrantDiagram-120e2f19-beQD7R1C.js → quadrantDiagram-120e2f19-CsWgLp5e.js} +1 -1
  99. package/dist/frontend/assets/relay-BSVGqyIy.js +2 -0
  100. package/dist/frontend/assets/{requirementDiagram-23d650b8-B2i-OB97.js → requirementDiagram-23d650b8-CHmg-ycX.js} +1 -1
  101. package/dist/frontend/assets/{requirementDiagram-deff3bca-beS-SvfY.js → requirementDiagram-deff3bca-LIsyxsZZ.js} +1 -1
  102. package/dist/frontend/assets/{ro-RO-JPDTUUEW-DsrnQm7K.js → ro-RO-JPDTUUEW-B_a5A5-x.js} +1 -1
  103. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-DrnGGdUs.js → ru-RU-B4JR7IUQ-CY5sI1Ru.js} +1 -1
  104. package/dist/frontend/assets/{sankeyDiagram-04a897e0-CVad45bN.js → sankeyDiagram-04a897e0-DN5e16d0.js} +1 -1
  105. package/dist/frontend/assets/{sankeyDiagram-23345273-ghWBXXF2.js → sankeyDiagram-23345273-BBjQCtHz.js} +1 -1
  106. package/dist/frontend/assets/{selectAll-XplaO988.js → selectAll-Dcq-dC_E.js} +1 -1
  107. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-DBI-EdWx.js → sequenceDiagram-17ac3bff-CIXyQiN6.js} +1 -1
  108. package/dist/frontend/assets/{sequenceDiagram-704730f1-BnJ__mBt.js → sequenceDiagram-704730f1-BmDwBhsF.js} +1 -1
  109. package/dist/frontend/assets/{si-LK-N5RQ5JYF-BxAwuRM5.js → si-LK-N5RQ5JYF-DEPKL_1i.js} +1 -1
  110. package/dist/frontend/assets/{sk-SK-C5VTKIMK-CnDyiLRf.js → sk-SK-C5VTKIMK-XqrGDM2K.js} +1 -1
  111. package/dist/frontend/assets/{sl-SI-NN7IZMDC-BXHXIdFA.js → sl-SI-NN7IZMDC-Dia3d1Yy.js} +1 -1
  112. package/dist/frontend/assets/{stateDiagram-587899a1-D3Ahxsgc.js → stateDiagram-587899a1-CY1BCaUa.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-9c5f0230-8XbLWvC0.js → stateDiagram-9c5f0230-gyjWQtSe.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-Cqf-MUGD.js → stateDiagram-v2-51a3dcff-CAIiXqRE.js} +1 -1
  115. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-DjGrud9D.js → stateDiagram-v2-d93cdb3a-DvUpzRiQ.js} +1 -1
  116. package/dist/frontend/assets/{styles-2ab5d517-B4qdHOgu.js → styles-2ab5d517-BE3yBf4Z.js} +1 -1
  117. package/dist/frontend/assets/{styles-5f03d8d2-DOD2v5V8.js → styles-5f03d8d2-CGBEZiW_.js} +1 -1
  118. package/dist/frontend/assets/{styles-6aaf32cf-bP3XXcmf.js → styles-6aaf32cf-JdIhb4_Y.js} +1 -1
  119. package/dist/frontend/assets/{styles-9a916d00-QLeuL_Dy.js → styles-9a916d00-DRmxoyEF.js} +1 -1
  120. package/dist/frontend/assets/{styles-c10674c1-Lpvh1u4f.js → styles-c10674c1-C3TEvIjw.js} +1 -1
  121. package/dist/frontend/assets/{styles-edf9a4b0-D4W9aPm7.js → styles-edf9a4b0-B_FeyXdP.js} +1 -1
  122. package/dist/frontend/assets/subset-shared.chunk.js +1 -1
  123. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  124. package/dist/frontend/assets/{sv-SE-XGPEYMSR-YrCQQBuo.js → sv-SE-XGPEYMSR-CBrvnOWh.js} +1 -1
  125. package/dist/frontend/assets/{svgDrawCommon-08f97a94-CwNv3PjS.js → svgDrawCommon-08f97a94-DI4ZG6xI.js} +1 -1
  126. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-DZbT0Bqu.js → svgDrawCommon-3ba9043b-Dl5HsA0X.js} +1 -1
  127. package/dist/frontend/assets/{ta-IN-2NMHFXQM-qR82aBSW.js → ta-IN-2NMHFXQM-DGYl9sGK.js} +1 -1
  128. package/dist/frontend/assets/{th-TH-HPSO5L25-C65V7hE5.js → th-TH-HPSO5L25-Bq3K3L0F.js} +1 -1
  129. package/dist/frontend/assets/{timeline-definition-7e6b55e7-GkPy3QeG.js → timeline-definition-7e6b55e7-DWL-fZtL.js} +3 -3
  130. package/dist/frontend/assets/{timeline-definition-85554ec2-CCdkBaC0.js → timeline-definition-85554ec2-C14kwDFd.js} +3 -3
  131. package/dist/frontend/assets/{tr-TR-DEFEU3FU-CFzbwAKV.js → tr-TR-DEFEU3FU-CSwI35xw.js} +1 -1
  132. package/dist/frontend/assets/{uk-UA-QMV73CPH-ePt5WVz3.js → uk-UA-QMV73CPH-Dxy7prmb.js} +1 -1
  133. package/dist/frontend/assets/{union-CVZobYBh.js → union-DQ7m6Pel.js} +1 -1
  134. package/dist/frontend/assets/{vi-VN-M7AON7JQ-CaPKykgJ.js → vi-VN-M7AON7JQ-BNyYTgjT.js} +1 -1
  135. package/dist/frontend/assets/{xychartDiagram-b6496bcd-CV5WRc81.js → xychartDiagram-b6496bcd-DWKYL0ZD.js} +1 -1
  136. package/dist/frontend/assets/{xychartDiagram-e933f94c-BH-ySdQf.js → xychartDiagram-e933f94c-Bw4brW9l.js} +1 -1
  137. package/dist/frontend/assets/youtubeResume-DPG81gOQ.js +1 -0
  138. package/dist/frontend/assets/{zh-CN-LNUGB5OW-CK8AEj8o.js → zh-CN-LNUGB5OW-BNhZCPEt.js} +1 -1
  139. package/dist/frontend/assets/{zh-HK-E62DVLB3-CUINiFgH.js → zh-HK-E62DVLB3-D_rDALSq.js} +1 -1
  140. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-BEBY7iJR.js → zh-TW-RAJ6MFWO-rUlbmCTA.js} +1 -1
  141. package/dist/frontend/assets/{zipObject-W8SLG3F1.js → zipObject-Db5-6vIz.js} +1 -1
  142. package/dist/frontend/index.html +6 -2
  143. package/dist/mcp.js +1 -1
  144. package/dist/mcp.js.map +1 -1
  145. package/package.json +1 -1
  146. package/dist/frontend/assets/arc-B9VNLSzj.js +0 -1
  147. package/dist/frontend/assets/channel-jdNM4v-F.js +0 -1
  148. package/dist/frontend/assets/classDiagram-v2-f2320105-D8ruMBA6.js +0 -2
  149. package/dist/frontend/assets/clone-DFO_zGRO.js +0 -1
  150. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-6vlgZ0Bg.js +0 -1
  151. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-FmnI78Vm.js +0 -1
  152. package/dist/frontend/assets/index-GTq6y6g6.js +0 -349
  153. package/dist/frontend/assets/relay-BIOL2uw2.js +0 -1
package/dist/cli.js CHANGED
@@ -38,6 +38,25 @@ async function ghPut(pathname, body) {
38
38
  throw new Error(`GitHub PUT ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
39
39
  return r.json();
40
40
  }
41
+ async function ghPatch(pathname, body) {
42
+ const r = await fetch(`${GH_API}${pathname}`, { method: 'PATCH', headers: ghHeaders(), body: JSON.stringify(body) });
43
+ if (!r.ok)
44
+ throw new Error(`GitHub PATCH ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
45
+ return r.json();
46
+ }
47
+ /** Set repo website (homepage) to the canvas viewer URL — only if empty. Best-effort. */
48
+ async function ensureRepoHomepage(repo, viewerUrl) {
49
+ try {
50
+ const info = await ghGet(`/repos/${repo}`);
51
+ if (info.homepage && String(info.homepage).trim())
52
+ return;
53
+ await ghPatch(`/repos/${repo}`, { homepage: viewerUrl });
54
+ console.log(`Repo website set to canvas: ${viewerUrl}`);
55
+ }
56
+ catch (e) {
57
+ console.warn(`Could not set repo website (non-fatal): ${e.message}`);
58
+ }
59
+ }
41
60
  async function ensureMainBranch(repo) {
42
61
  const main = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/main`, { headers: ghHeaders() });
43
62
  if (main.ok)
@@ -47,7 +66,7 @@ async function ensureMainBranch(repo) {
47
66
  return;
48
67
  await ghPut(`/repos/${repo}/contents/README.md`, {
49
68
  message: 'Initial commit',
50
- content: Buffer.from(`# ${repo.split('/')[1]}\n\nExcalidraw canvas powered by [excalidrop](https://github.com/wtf403/excalidrop).\n`).toString('base64'),
69
+ content: Buffer.from('').toString('base64'),
51
70
  branch: 'main',
52
71
  });
53
72
  }
@@ -183,10 +202,22 @@ async function resolveTarget(repo, explicit) {
183
202
  return explicit;
184
203
  return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
185
204
  }
205
+ const WRANGLER = ['-y', 'wrangler@4'];
206
+ // Which Cloudflare identity would a Pages deploy use? whoami fails when
207
+ // logged out; a CLOUDFLARE_API_TOKEN works without a login. Deploys always
208
+ // run under the USER's own account/quota — never the author's.
209
+ function wranglerAccount() {
210
+ if (process.env.CLOUDFLARE_API_TOKEN)
211
+ return { ok: true, display: 'API token (CLOUDFLARE_API_TOKEN)' };
212
+ const r = runCapture('npx', [...WRANGLER, 'whoami']);
213
+ if (r.status !== 0)
214
+ return { ok: false, display: '' };
215
+ const m = r.out.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
216
+ return { ok: true, display: m?.[0] ?? 'logged in' };
217
+ }
186
218
  function deployToCloudflarePages(project) {
187
219
  // wrangler 4 doesn't auto-create Pages projects and its Workers delegation
188
220
  // misfires on explicit asset dirs — create once, then deploy classic with --force.
189
- const WRANGLER = ['-y', 'wrangler@4'];
190
221
  const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
191
222
  const createOut = (create.stdout || '') + (create.stderr || '');
192
223
  if (create.status !== 0 && !/already exists/i.test(createOut)) {
@@ -205,6 +236,19 @@ function detectSlug() {
205
236
  return '';
206
237
  }
207
238
  }
239
+ /** Accept owner/repo, a GitHub URL, or git@ SSH — always returns owner/repo or ''. */
240
+ export function normalizeRepoSlug(input) {
241
+ const s = (input || '').trim();
242
+ if (!s)
243
+ return '';
244
+ // Full URL / SSH / bare host path → extract the owner/repo tail.
245
+ const m = s.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\/.*)?$/i);
246
+ const candidate = m ? m[1] : s;
247
+ const cleaned = candidate.replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
248
+ const ok = cleaned.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
249
+ return ok ? `${ok[1].toLowerCase()}/${ok[2]}` : '';
250
+ }
251
+ const SLUG_HINT = 'owner/repo or a GitHub link';
208
252
  function ghAuthed() {
209
253
  return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
210
254
  }
@@ -213,59 +257,314 @@ function commandExists(cmd) {
213
257
  return probe.status === 0;
214
258
  }
215
259
  function findProjectRoot(cwd) {
216
- let dir = path.resolve(cwd);
217
- const home = process.env.HOME || process.env.USERPROFILE || '/';
218
- while (true) {
219
- if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git')))
220
- return dir;
221
- const parent = path.dirname(dir);
222
- if (parent === dir || dir === home)
223
- return path.resolve(cwd);
224
- dir = parent;
260
+ // Inside a git checkout the repo top is the root. Never walk up past cwd
261
+ // looking for package.json: from a fresh non-checkout dir (e.g. a folder
262
+ // holding a brand-new canvas repo) the old walk-up climbed into an
263
+ // unrelated parent project and wrote .mcp.json / .excalidrop.json there.
264
+ try {
265
+ const top = execSync('git rev-parse --show-toplevel', { cwd: path.resolve(cwd), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
266
+ if (top)
267
+ return top;
225
268
  }
269
+ catch { /* not inside a git work tree — fall through to cwd */ }
270
+ return path.resolve(cwd);
226
271
  }
227
272
  function mcpArgs(repo) {
228
273
  return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
229
274
  }
230
- const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'desktop', 'opencode'];
275
+ const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'cursor-user', 'desktop', 'opencode', 'opencode-user'];
231
276
  function editorCommand(id, repo) {
232
277
  const a = mcpArgs(repo).join(' ');
233
278
  switch (id) {
234
279
  case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
235
280
  case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
236
281
  case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
237
- case 'cursor': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
282
+ case 'cursor':
283
+ case 'cursor-user':
238
284
  case 'desktop': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
239
- case 'opencode': return JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: { excalidrop: { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true } } }, null, 2);
285
+ case 'opencode':
286
+ case 'opencode-user': return JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: { excalidrop: { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true } } }, null, 2);
287
+ }
288
+ }
289
+ function editorScope(id) {
290
+ return id === 'claude-project' || id === 'cursor' || id === 'opencode' ? 'project' : 'user';
291
+ }
292
+ // Where each editor id reads its config from. File-based harnesses support
293
+ // both a project file and a user-global file; CLI-driven ones (claude, codex)
294
+ // are detected through the same files their CLIs write.
295
+ function editorFile(id, root) {
296
+ const home = os.homedir();
297
+ switch (id) {
298
+ case 'claude-user': return { path: path.join(home, '.claude.json'), key: 'mcpServers' };
299
+ case 'claude-project': return { path: path.join(root, MCP_JSON), key: 'mcpServers' };
300
+ case 'cursor': return { path: path.join(root, '.cursor', 'mcp.json'), key: 'mcpServers' };
301
+ case 'cursor-user': return { path: path.join(home, '.cursor', 'mcp.json'), key: 'mcpServers' };
302
+ case 'desktop': return { path: desktopConfigPath(), key: 'mcpServers' };
303
+ case 'opencode': return { path: path.join(root, 'opencode.json'), key: 'mcp' };
304
+ case 'opencode-user': return { path: path.join(home, '.config', 'opencode', 'opencode.json'), key: 'mcp' };
305
+ case 'codex': return null; // TOML (~/.codex/config.toml) — parsed separately
306
+ }
307
+ }
308
+ function desktopConfigPath() {
309
+ const home = os.homedir();
310
+ if (process.platform === 'darwin')
311
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
312
+ if (process.platform === 'win32')
313
+ return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
314
+ return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
315
+ }
316
+ function editorTarget(id, root) {
317
+ const f = editorFile(id, root);
318
+ if (id === 'codex')
319
+ return 'user config (~/.codex/config.toml)';
320
+ const scope = editorScope(id);
321
+ const short = f ? shortenHome(f.path) : id;
322
+ return `${scope} scope (${short})`;
323
+ }
324
+ function shortenHome(p) {
325
+ const home = os.homedir();
326
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
327
+ }
328
+ function repoFromArgs(args) {
329
+ if (!Array.isArray(args))
330
+ return null;
331
+ const i = args.findIndex((a) => a === '--repo');
332
+ const v = i !== -1 ? args[i + 1] : null;
333
+ return typeof v === 'string' && v.includes('/') ? v.toLowerCase() : null;
334
+ }
335
+ // {present, repo} for a JSON MCP config (claude/cursor/desktop/opencode shapes).
336
+ function jsonMcpRepo(file, key) {
337
+ let j;
338
+ try {
339
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
340
+ }
341
+ catch {
342
+ return { present: false, repo: null };
343
+ }
344
+ const e = key === 'mcpServers' ? j?.mcpServers?.excalidrop : j?.mcp?.excalidrop;
345
+ if (!e)
346
+ return { present: false, repo: null };
347
+ if (Array.isArray(e.command))
348
+ return { present: true, repo: repoFromArgs(e.command) };
349
+ if (e.command === 'npx' && Array.isArray(e.args))
350
+ return { present: true, repo: repoFromArgs(e.args) };
351
+ return { present: true, repo: null };
352
+ }
353
+ // {present, repo} for Codex's TOML config — crude section parse, no new deps.
354
+ function codexMcpRepo() {
355
+ let txt;
356
+ try {
357
+ txt = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
358
+ }
359
+ catch {
360
+ return { present: false, repo: null };
361
+ }
362
+ const sec = txt.match(/\[mcp_servers\.excalidrop\]([\s\S]*?)(?=^\[|\z)/m);
363
+ if (!sec)
364
+ return { present: false, repo: null };
365
+ const m = (sec[1] ?? '').match(/--repo["'\s=,]+([A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+)/);
366
+ return { present: true, repo: m?.[1]?.toLowerCase() ?? null };
367
+ }
368
+ function detectEditorState(id, repo, root) {
369
+ const want = repo.toLowerCase();
370
+ if (id === 'codex') {
371
+ const s = codexMcpRepo();
372
+ if (!s.present)
373
+ return { status: 'missing', repo: null, where: '~/.codex/config.toml' };
374
+ return s.repo === want
375
+ ? { status: 'current', repo: s.repo, where: '~/.codex/config.toml' }
376
+ : { status: 'stale', repo: s.repo, where: '~/.codex/config.toml' };
377
+ }
378
+ const f = editorFile(id, root);
379
+ const where = shortenHome(f.path);
380
+ const s = jsonMcpRepo(f.path, f.key);
381
+ if (!s.present)
382
+ return { status: 'missing', repo: null, where };
383
+ return s.repo === want
384
+ ? { status: 'current', repo: s.repo, where }
385
+ : { status: 'stale', repo: s.repo, where };
386
+ }
387
+ function updateCommand(id, repo) {
388
+ if (id === 'claude-project' || id === 'claude-user') {
389
+ const scope = id === 'claude-project' ? 'project' : 'user';
390
+ return `claude mcp remove excalidrop -s ${scope} && ${editorCommand(id, repo)}`;
240
391
  }
392
+ return editorCommand(id, repo);
241
393
  }
242
- function installEditor(id, repo) {
394
+ function runCapture(cmd, args) {
243
395
  try {
244
- if (id === 'claude-project' && commandExists('claude')) {
245
- return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
396
+ const r = spawnSync(cmd, args, { encoding: 'utf8' });
397
+ return { status: r.status, out: `${r.stdout || ''}\n${r.stderr || ''}` };
398
+ }
399
+ catch (e) {
400
+ return { status: 1, out: e.message };
401
+ }
402
+ }
403
+ // Merge an MCP entry into a JSON config file (creates parent dirs). Returns
404
+ // false when the existing file isn't valid JSON — caller falls back to manual.
405
+ function upsertJsonMcp(file, key, entry) {
406
+ let j = {};
407
+ if (fs.existsSync(file)) {
408
+ try {
409
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
246
410
  }
247
- if (id === 'claude-user' && commandExists('claude')) {
248
- return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
411
+ catch {
412
+ return false;
249
413
  }
250
- if (id === 'codex' && commandExists('codex')) {
251
- return spawnSync('codex', ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
414
+ if (!j || typeof j !== 'object')
415
+ return false;
416
+ }
417
+ else {
418
+ try {
419
+ fs.mkdirSync(path.dirname(file), { recursive: true });
420
+ }
421
+ catch {
422
+ return false;
252
423
  }
253
424
  }
254
- catch { /* fall through to manual */ }
255
- return false;
425
+ if (key === 'mcpServers') {
426
+ if (!j.mcpServers || typeof j.mcpServers !== 'object')
427
+ j.mcpServers = {};
428
+ j.mcpServers.excalidrop = entry;
429
+ }
430
+ else {
431
+ if (!j.mcp || typeof j.mcp !== 'object')
432
+ j.mcp = {};
433
+ j.mcp.excalidrop = entry;
434
+ if (!j.$schema)
435
+ j.$schema = 'https://opencode.ai/config.json';
436
+ }
437
+ try {
438
+ fs.writeFileSync(file, JSON.stringify(j, null, 2) + '\n');
439
+ return true;
440
+ }
441
+ catch {
442
+ return false;
443
+ }
444
+ }
445
+ function cursorEntry(repo) {
446
+ return { command: 'npx', args: mcpArgs(repo), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
447
+ }
448
+ function opencodeEntry(repo) {
449
+ return { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true, env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
450
+ }
451
+ function installEditor(id, repo, root) {
452
+ const fail = (detail) => ({ id, outcome: 'failed', detail });
453
+ // Idempotent first: never reinstall over an identical entry, never silently
454
+ // leave a stale one — report it with the exact update command.
455
+ const st = detectEditorState(id, repo, root);
456
+ if (st.status === 'current')
457
+ return { id, outcome: 'already-current', detail: `${st.where} already points at ${repo}` };
458
+ const staleNote = st.status === 'stale' ? ` (currently → ${st.repo ?? 'unknown'})` : '';
459
+ if (id === 'claude-project' || id === 'claude-user' || id === 'codex') {
460
+ const bin = id === 'codex' ? 'codex' : 'claude';
461
+ if (!commandExists(bin))
462
+ return fail(`${bin} CLI not found — run manually:\n${editorCommand(id, repo)}`);
463
+ const cargs = id === 'claude-project'
464
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)]
465
+ : id === 'claude-user'
466
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)]
467
+ : ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)];
468
+ const r = runCapture(bin, cargs);
469
+ if (r.status === 0 && !/already exists/i.test(r.out)) {
470
+ return { id, outcome: 'added', detail: editorCommand(id, repo) };
471
+ }
472
+ if (/already exists/i.test(r.out)) {
473
+ // Exit code is 0 even when nothing changed — re-read the file to say
474
+ // honestly whether the existing entry is current or stale.
475
+ const now = detectEditorState(id, repo, root);
476
+ if (now.status === 'current')
477
+ return { id, outcome: 'already-current', detail: `${now.where} already points at ${repo}` };
478
+ return { id, outcome: 'stale', detail: `already exists${now.repo ? ` → ${now.repo}` : ''} — not updated. Run:\n${updateCommand(id, repo)}` };
479
+ }
480
+ return fail((r.out.trim() || `${bin} exited ${r.status}`).slice(0, 300));
481
+ }
482
+ // File-based harnesses: JSON merge is safe to write for both add and update.
483
+ const f = editorFile(id, root);
484
+ const entry = id === 'cursor' || id === 'cursor-user' || id === 'desktop' ? cursorEntry(repo) : opencodeEntry(repo);
485
+ if (upsertJsonMcp(f.path, f.key, entry)) {
486
+ return { id, outcome: 'added', detail: `wrote ${shortenHome(f.path)}${staleNote} → ${repo}` };
487
+ }
488
+ return fail(`${shortenHome(f.path)} is not valid JSON — leaving it untouched. Add manually:\n${editorCommand(id, repo)}`);
256
489
  }
257
490
  const EDITOR_LABELS = {
258
491
  'claude-project': 'Claude Code (project scope)',
259
492
  'claude-user': 'Claude Code (user scope)',
493
+ codex: 'Codex CLI (user config)',
494
+ cursor: 'Cursor (project file)',
495
+ 'cursor-user': 'Cursor (user file)',
496
+ desktop: 'Claude Desktop (user config)',
497
+ opencode: 'OpenCode (project file)',
498
+ 'opencode-user': 'OpenCode (user config)',
499
+ };
500
+ // Harnesses group the flat scope-specific ids for the two-step TUI picker:
501
+ // step 1 picks harnesses, step 2 picks the scope per multi-scope harness.
502
+ // Single-scope harnesses (codex, desktop) are user-global — no step 2.
503
+ const HARNESSES = ['claude', 'codex', 'cursor', 'desktop', 'opencode'];
504
+ const HARNESS_EDITORS = {
505
+ claude: ['claude-user', 'claude-project'],
506
+ codex: ['codex'],
507
+ cursor: ['cursor-user', 'cursor'],
508
+ desktop: ['desktop'],
509
+ opencode: ['opencode-user', 'opencode'],
510
+ };
511
+ const HARNESS_LABELS = {
512
+ claude: 'Claude Code',
260
513
  codex: 'Codex CLI',
261
- cursor: 'Cursor (.cursor/mcp.json)',
514
+ cursor: 'Cursor',
262
515
  desktop: 'Claude Desktop',
263
- opencode: 'OpenCode (opencode.json)',
516
+ opencode: 'OpenCode',
264
517
  };
265
- function printEditorCommands(ids, repo) {
266
- console.log('Add excalidrop to your AI agent (no install, always latest):\n');
267
- for (const id of ids)
268
- console.log(`## ${EDITOR_LABELS[id]}\n${editorCommand(id, repo)}\n`);
518
+ // One-line install-state summary for the harness picker, e.g.
519
+ // "user: installed, project: —" or "installed → other/repo".
520
+ function harnessSummary(h, slug, root) {
521
+ const ids = HARNESS_EDITORS[h];
522
+ const parts = ids.map((id) => {
523
+ const st = detectEditorState(id, slug, root);
524
+ const cur = st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : '—';
525
+ return ids.length > 1 ? `${editorScope(id)}: ${cur}` : cur;
526
+ });
527
+ return parts.join(', ');
528
+ }
529
+ // Scope option for step 2 of the picker: short scope label + live state hint.
530
+ function scopeOption(id, slug, root) {
531
+ const st = detectEditorState(id, slug, root);
532
+ return {
533
+ value: id,
534
+ label: editorTarget(id, root),
535
+ hint: st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : undefined,
536
+ };
537
+ }
538
+ // One honest report for every editor install attempt — always shows the
539
+ // command/config that was (or should be) applied, so "installed" can never
540
+ // mean "already existed but points elsewhere".
541
+ function reportInstalls(results, repo, root) {
542
+ for (const r of results) {
543
+ const target = editorTarget(r.id, root);
544
+ if (r.outcome === 'added')
545
+ console.log(`ok ${EDITOR_LABELS[r.id]} → ${target}:\n ${editorCommand(r.id, repo).split('\n').join('\n ')}\n installed (${r.detail})`);
546
+ else if (r.outcome === 'already-current')
547
+ console.log(`skip ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
548
+ else if (r.outcome === 'stale')
549
+ console.log(`warn ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
550
+ else
551
+ console.log(`fail ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
552
+ }
553
+ }
554
+ /** Poll a viewer URL until it serves 200 (slow first builds take minutes). */
555
+ async function waitForLive(url, timeoutMs = 7 * 60_000) {
556
+ const deadline = Date.now() + timeoutMs;
557
+ while (Date.now() < deadline) {
558
+ try {
559
+ const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
560
+ if (r.ok)
561
+ return true;
562
+ // 404 during initial propagation — keep waiting; anything else too.
563
+ }
564
+ catch { /* offline / DNS — keep waiting */ }
565
+ await new Promise((r) => setTimeout(r, 10000));
566
+ }
567
+ return false;
269
568
  }
270
569
  function parseFlag(args, name) {
271
570
  const eq = args.find((a) => a.startsWith(`--${name}=`));
@@ -288,11 +587,12 @@ function parseEditorsFlag(args) {
288
587
  }
289
588
  return valid;
290
589
  }
291
- async function cmdSetup(args, editorOverride) {
292
- const positional = args.find((a) => !a.startsWith('-') && a.includes('/'));
293
- const slug = parseFlag(args, 'repo') || positional || detectSlug();
294
- if (!slug || !/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(slug)) {
295
- console.error('Usage: npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex] [--no-prompt]');
590
+ async function cmdSetup(args, editorOverride, opts = {}) {
591
+ const positional = args.find((a) => !a.startsWith('-') && (a.includes('/') || a.includes('github.com')));
592
+ const rawSlug = parseFlag(args, 'repo') || positional || detectSlug();
593
+ const slug = normalizeRepoSlug(rawSlug);
594
+ if (!slug) {
595
+ console.error(`Usage: npx excalidrop setup ${SLUG_HINT} [--target pages|cloudflare] [--editor claude-project,codex] [--no-prompt]`);
296
596
  process.exit(1);
297
597
  }
298
598
  if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
@@ -311,7 +611,7 @@ async function cmdSetup(args, editorOverride) {
311
611
  }
312
612
  const explicitTarget = parseFlag(args, 'target');
313
613
  const target = await resolveTarget(slug, explicitTarget);
314
- console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? ' (explicit)' : ' (auto: private→cloudflare, public→pages)'}\n`);
614
+ console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? (opts.fromTui ? ' (selected)' : ' (explicit)') : ' (auto: private→cloudflare, public→pages)'}\n`);
315
615
  await ensureMainBranch(slug);
316
616
  const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
317
617
  if (!scene.sha) {
@@ -324,9 +624,33 @@ async function cmdSetup(args, editorOverride) {
324
624
  viewerUrl = viewerUrlFor(slug, 'pages');
325
625
  }
326
626
  else {
627
+ // Fail fast on missing auth instead of dying mid-deploy. The TUI checks
628
+ // interactively before the spinner; non-interactive runs land here.
629
+ const acct = wranglerAccount();
630
+ if (!acct.ok && !opts.fromTui) {
631
+ throw new Error('Not logged into Cloudflare (wrangler whoami failed). Run `npx -y wrangler@4 login` first, or set CLOUDFLARE_API_TOKEN.');
632
+ }
633
+ console.log(`Cloudflare account: ${acct.display || 'unknown'} — the viewer deploys to YOUR account and quota.`);
327
634
  deployToCloudflarePages(projectNameForRepo(slug));
328
635
  viewerUrl = viewerUrlFor(slug, 'cloudflare');
329
636
  }
637
+ // Don't declare victory until the viewer actually serves — first Pages /
638
+ // Cloudflare builds take minutes, and "Canvas live" on a 404 wastes a debug
639
+ // session. Poll the URL (up to 7 min for slow first builds), then report
640
+ // honestly with the Pages build state attached.
641
+ const live = await waitForLive(viewerUrl);
642
+ if (live) {
643
+ console.log('Viewer is serving.');
644
+ }
645
+ else {
646
+ console.log('Viewer not live yet (HTTP 404) — hosting may still be building; retry the URL in a minute.');
647
+ const info = await pagesBuildInfo(slug, target).catch(() => null);
648
+ if (info)
649
+ console.log(info);
650
+ }
651
+ await ensureRepoHomepage(slug, viewerUrl);
652
+ const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
653
+ console.log(`Note: browser login stays read-only until the Excalidrop app is installed on ${slug}:\n https://github.com/apps/${appSlug}/installations/new`);
330
654
  const root = findProjectRoot(process.cwd());
331
655
  fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
332
656
  const mcpPath = path.join(root, MCP_JSON);
@@ -346,18 +670,28 @@ async function cmdSetup(args, editorOverride) {
346
670
  console.log(`MCP entry written to ${mcpPath}`);
347
671
  const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
348
672
  const noPrompt = args.includes('--no-prompt') || args.includes('-y');
349
- if (noPrompt) {
350
- const installed = [];
351
- const manual = [];
352
- for (const id of editors)
353
- (installEditor(id, slug) ? installed : manual).push(id);
354
- if (installed.length)
355
- console.log(`Installed into: ${installed.map((i) => EDITOR_LABELS[i]).join(', ')}`);
356
- if (manual.length)
357
- printEditorCommands(manual, slug);
358
- return { repo: slug, viewerUrl };
359
- }
360
- return { repo: slug, viewerUrl };
673
+ const installs = editors.map((id) => installEditor(id, slug, root));
674
+ // The TUI prints its own per-editor report (it owns the spinner); the plain
675
+ // CLI prints here so `setup --no-prompt` shows exactly what ran.
676
+ if (!opts.fromTui)
677
+ reportInstalls(installs, slug, root);
678
+ void noPrompt;
679
+ return { repo: slug, viewerUrl, live, installs };
680
+ }
681
+ // Latest GitHub Pages build state for a repo — printed when the viewer URL
682
+ // still 404s after the wait so the user knows whether it's "still building"
683
+ // vs "build errored / Pages misconfigured".
684
+ async function pagesBuildInfo(repo, target) {
685
+ if (target !== 'pages')
686
+ return `Cloudflare deploy is async — check the Pages project dashboard; if still 404, rerun: npx excalidrop publish ${repo}`;
687
+ try {
688
+ const latest = await ghGet(`/repos/${repo}/pages/builds/latest`);
689
+ const err = latest?.error?.message ? ` — ${latest.error.message}` : '';
690
+ return `Pages latest build: ${latest.status}${err} (updated ${latest.updated_at || 'unknown'}). Check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}) — first builds can take several minutes. If stuck, rerun: npx excalidrop publish ${repo}`;
691
+ }
692
+ catch {
693
+ return `Pages build status unknown — check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}).`;
694
+ }
361
695
  }
362
696
  function cmdMcp() {
363
697
  const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
@@ -419,18 +753,29 @@ async function runTui() {
419
753
  const detected = detectSlug();
420
754
  const repoAnswer = await clack.text({
421
755
  message: 'Which repo holds the canvas?',
422
- placeholder: detected || 'owner/repo',
756
+ placeholder: detected || SLUG_HINT,
423
757
  initialValue: detected || '',
424
- validate: (v) => (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(v.trim()) ? undefined : 'Use owner/repo'),
758
+ validate: (v) => (normalizeRepoSlug(v) ? undefined : `Use ${SLUG_HINT}`),
425
759
  });
426
760
  if (clack.isCancel(repoAnswer)) {
427
761
  clack.cancel('Aborted.');
428
762
  process.exit(0);
429
763
  }
430
- const slug = repoAnswer.trim().toLowerCase();
764
+ const slug = normalizeRepoSlug(repoAnswer);
431
765
  const priv = await isPrivateRepo(slug);
432
766
  if (priv !== null)
433
767
  clack.log.info(`Repo is ${priv ? 'private' : 'public'} — recommended host: ${priv ? 'Cloudflare Pages' : 'GitHub Pages'}.`);
768
+ // Visibility drift: repo flipped public<->private since last setup, so the
769
+ // stored target + viewer URL no longer match — nudge to re-pick below.
770
+ try {
771
+ const prevRaw = fs.readFileSync(path.join(findProjectRoot(process.cwd()), CONFIG_NAME), 'utf8');
772
+ const prevTarget = JSON.parse(prevRaw).target;
773
+ const want = priv === true ? 'cloudflare' : 'pages';
774
+ if (prevTarget && prevTarget !== want && priv !== null) {
775
+ clack.log.warn(`Visibility changed since last setup (host: ${prevTarget}, repo now ${priv ? 'private' : 'public'}). Pick ${want} below to migrate — see README "Migrating public<->private".`);
776
+ }
777
+ }
778
+ catch { /* first setup — nothing to compare */ }
434
779
  if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
435
780
  const how = await clack.select({
436
781
  message: 'GitHub auth missing. How to log in?',
@@ -461,75 +806,173 @@ async function runTui() {
461
806
  process.exit(1);
462
807
  }
463
808
  }
464
- const auto = await resolveTarget(slug);
809
+ // No auto option: GitHub Pages can't serve private repos on free plans
810
+ // and public repos need zero config, so visibility pre-selects the right
811
+ // host — the user just confirms or flips. Non-interactive `--target` still
812
+ // goes through resolveTarget() in cmdSetup.
813
+ const recommended = priv === true ? 'cloudflare' : 'pages';
465
814
  const hostAnswer = await clack.select({
466
- message: `Where to host the viewer? (auto: ${auto})`,
815
+ message: 'Where to host the viewer?',
816
+ initialValue: recommended,
467
817
  options: [
468
- { value: 'auto', label: `Auto — ${auto === 'pages' ? 'GitHub Pages (public repo)' : 'Cloudflare Pages (private repo)'}`, hint: 'recommended' },
469
- { value: 'pages', label: 'GitHub Pages', hint: 'public repos, zero config' },
470
- { value: 'cloudflare', label: 'Cloudflare Pages', hint: 'private repos' },
818
+ { value: 'pages', label: 'GitHub Pages (public repo)', hint: 'zero config' },
819
+ { value: 'cloudflare', label: 'Cloudflare Pages (private repo)', hint: 'needs wrangler login' },
471
820
  ],
472
821
  });
473
822
  if (clack.isCancel(hostAnswer)) {
474
823
  clack.cancel('Aborted.');
475
824
  process.exit(0);
476
825
  }
477
- const target = (hostAnswer === 'auto' ? auto : hostAnswer);
478
- const editorAnswer = await clack.multiselect({
826
+ const target = hostAnswer;
827
+ // Cloudflare deploys run under the USER's own wrangler identity and quota.
828
+ // Verify login up front (with an offer to log in now) instead of failing
829
+ // minutes later mid-deploy.
830
+ if (target === 'cloudflare') {
831
+ let acct = wranglerAccount();
832
+ if (!acct.ok) {
833
+ const login = await clack.confirm({
834
+ message: 'Wrangler is not logged into Cloudflare. Log in now (opens a browser)?',
835
+ });
836
+ if (clack.isCancel(login)) {
837
+ clack.cancel('Aborted.');
838
+ process.exit(0);
839
+ }
840
+ if (login) {
841
+ spawnSync('npx', [...WRANGLER, 'login'], { stdio: 'inherit', env: process.env });
842
+ acct = wranglerAccount();
843
+ }
844
+ if (!acct.ok) {
845
+ clack.log.error('Cloudflare login is required for Cloudflare Pages hosting — pick GitHub Pages, or run `npx -y wrangler@4 login` first.');
846
+ process.exit(1);
847
+ }
848
+ }
849
+ clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
850
+ }
851
+ const projectRoot = findProjectRoot(process.cwd());
852
+ // Step 1: pick harnesses. Preselect ones already wired to this repo;
853
+ // fall back to the historical default when nothing is installed yet.
854
+ const detectedHarnesses = HARNESSES.filter((h) => HARNESS_EDITORS[h].some((id) => detectEditorState(id, slug, projectRoot).status === 'current'));
855
+ const harnessAnswer = await clack.multiselect({
479
856
  message: 'Install the MCP server into which editors?',
480
- options: Object.keys(EDITOR_LABELS).map((id) => ({ value: id, label: EDITOR_LABELS[id] })),
481
- initialValues: ['claude-project', 'codex'],
857
+ options: HARNESSES.map((h) => ({
858
+ value: h,
859
+ label: `${HARNESS_LABELS[h]} (${harnessSummary(h, slug, projectRoot)})`,
860
+ })),
861
+ initialValues: detectedHarnesses.length > 0 ? detectedHarnesses : ['claude', 'codex'],
482
862
  required: false,
483
863
  });
484
- if (clack.isCancel(editorAnswer)) {
864
+ if (clack.isCancel(harnessAnswer)) {
485
865
  clack.cancel('Aborted.');
486
866
  process.exit(0);
487
867
  }
488
- const editors = editorAnswer;
868
+ const harnesses = harnessAnswer;
869
+ // Step 2: per multi-scope harness, pick where to install.
870
+ const editors = [];
871
+ for (const h of harnesses) {
872
+ const ids = HARNESS_EDITORS[h];
873
+ if (ids.length === 1) {
874
+ editors.push(ids[0]);
875
+ continue;
876
+ }
877
+ const current = ids.filter((id) => detectEditorState(id, slug, projectRoot).status === 'current');
878
+ const scopeAnswer = await clack.select({
879
+ message: `Where to install for ${HARNESS_LABELS[h]}?`,
880
+ initialValue: current.length === 1 ? current[0] : ids[0],
881
+ options: [
882
+ ...ids.map((id) => scopeOption(id, slug, projectRoot)),
883
+ { value: '__both__', label: 'Both scopes' },
884
+ ],
885
+ });
886
+ if (clack.isCancel(scopeAnswer)) {
887
+ clack.cancel('Aborted.');
888
+ process.exit(0);
889
+ }
890
+ if (scopeAnswer === '__both__')
891
+ editors.push(...ids);
892
+ else
893
+ editors.push(scopeAnswer);
894
+ }
895
+ // Stale Claude entries can't be overwritten by `add` (it exits 0 with
896
+ // "already exists" and changes nothing) — offer an explicit repoint before
897
+ // the deploy phase so the install below actually lands.
898
+ for (const id of editors) {
899
+ if (id !== 'claude-user' && id !== 'claude-project')
900
+ continue;
901
+ const st = detectEditorState(id, slug, projectRoot);
902
+ if (st.status !== 'stale')
903
+ continue;
904
+ const ok = await clack.confirm({
905
+ message: `${EDITOR_LABELS[id]} points at ${st.repo ?? 'unknown'} — repoint to ${slug}?`,
906
+ });
907
+ if (clack.isCancel(ok)) {
908
+ clack.cancel('Aborted.');
909
+ process.exit(0);
910
+ }
911
+ if (ok) {
912
+ const scope = id === 'claude-project' ? 'project' : 'user';
913
+ runCapture('claude', ['mcp', 'remove', 'excalidrop', '-s', scope]);
914
+ }
915
+ }
489
916
  const s = clack.spinner();
490
917
  s.start('Publishing viewer + wiring MCP…');
491
918
  let viewerUrl;
919
+ let live = false;
920
+ let installs = [];
492
921
  try {
493
- ({ viewerUrl } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors));
922
+ ({ viewerUrl, live, installs } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors, { fromTui: true }));
494
923
  s.stop('Setup complete.');
495
924
  }
496
925
  catch (e) {
497
926
  s.stop('Setup failed: ' + e.message);
498
927
  process.exit(1);
499
928
  }
500
- for (const id of editors) {
501
- if (id === 'claude-project' || id === 'claude-user' || id === 'codex') {
502
- clack.log.success(`${EDITOR_LABELS[id]}: installed (or already present).`);
929
+ // Always show what ran where: installing to user scope prints the command
930
+ // itself; file writes print the path; stale entries print the update command.
931
+ for (const r of installs) {
932
+ const target = editorTarget(r.id, projectRoot);
933
+ const cmd = editorCommand(r.id, slug).split('\n').join('\n ');
934
+ if (r.outcome === 'added')
935
+ clack.log.success(`MCP → ${target}:\n ${cmd}\n ${r.detail}`);
936
+ else if (r.outcome === 'already-current')
937
+ clack.log.success(`MCP → ${target}: ${r.detail} — skipped.`);
938
+ else if (r.outcome === 'stale')
939
+ clack.log.warn(`MCP → ${target}: ${r.detail}`);
940
+ else
941
+ clack.log.warn(`MCP → ${target} failed: ${r.detail}`);
942
+ }
943
+ // Post-deploy readability probe: one retry — right after a deploy the
944
+ // first API read intermittently fails at TCP level ("fetch failed")
945
+ // while the retry succeeds, and a false "not readable" scares users.
946
+ let scene = null;
947
+ let sceneError = null;
948
+ for (let attempt = 0; attempt < 2 && !scene; attempt++) {
949
+ if (attempt > 0)
950
+ await new Promise((r) => setTimeout(r, 3000));
951
+ try {
952
+ scene = await getScene(slug);
503
953
  }
504
- else {
505
- clack.log.info(`${EDITOR_LABELS[id]} config:\n${editorCommand(id, slug)}`);
954
+ catch (e) {
955
+ sceneError = e;
506
956
  }
507
957
  }
508
- try {
509
- const scene = await getScene(slug);
958
+ if (scene) {
510
959
  clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
511
960
  }
512
- catch (e) {
513
- clack.log.warn(`Scene not readable yet: ${e.message} — draw once to create it.`);
514
- }
515
- try {
516
- const r = await fetch(viewerUrl, { signal: AbortSignal.timeout(15000) });
517
- if (r.ok)
518
- clack.log.success('Viewer is serving.');
519
- else
520
- clack.log.warn(`Viewer returned HTTP ${r.status} — Pages/Cloudflare may still be building; retry in a minute.`);
521
- }
522
- catch {
523
- clack.log.warn('Viewer not reachable yet — hosting may still be building; retry in a minute.');
961
+ else {
962
+ clack.log.warn(`Scene not readable yet: ${sceneError?.message || sceneError} — draw once to create it.`);
524
963
  }
525
- clack.outro(`Canvas live: ${viewerUrl}`);
964
+ if (live)
965
+ clack.log.success('Viewer is serving.');
966
+ else
967
+ clack.log.warn('Viewer was not live after 7 minutes — check the Pages/Cloudflare build, then retry the URL.');
968
+ clack.outro(live ? `Canvas live: ${viewerUrl}` : `Canvas pending: ${viewerUrl}`);
526
969
  }
527
970
  function printHelp() {
528
971
  console.log(`excalidrop — remote GitHub-backed canvas (no local server)
529
972
 
530
973
  Usage:
531
974
  npx excalidrop interactive setup TUI
532
- npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex,cursor,desktop,opencode] [--no-prompt]
975
+ npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,claude-user,codex,cursor,cursor-user,desktop,opencode,opencode-user] [--no-prompt]
533
976
  npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
534
977
  npx excalidrop login [--repo owner/repo] GitHub device-flow login
535
978
  npx excalidrop publish owner/repo [--target ..] redeploy viewer only
@@ -564,9 +1007,19 @@ async function main() {
564
1007
  case 'publish':
565
1008
  await cmdSetup(['--no-prompt', ...rest]);
566
1009
  break;
567
- case 'add':
568
- printEditorCommands(parseEditorsFlag(rest) || [...EDITOR_IDS], rest.find((a) => a.includes('/')) || detectSlug() || 'owner/repo');
1010
+ case 'add': {
1011
+ const root = findProjectRoot(process.cwd());
1012
+ const ids = parseEditorsFlag(rest) || [...EDITOR_IDS];
1013
+ const raw = rest.find((a) => a.includes('/') || a.includes('github.com')) || detectSlug() || 'owner/repo';
1014
+ const repo = normalizeRepoSlug(raw) || raw;
1015
+ console.log('Add excalidrop to your AI agent (no install, always latest):\n');
1016
+ for (const id of ids) {
1017
+ const st = repo.includes('/') ? detectEditorState(id, repo, root) : null;
1018
+ const mark = st?.status === 'current' ? ' (installed)' : st?.status === 'stale' ? ` (installed → ${st.repo ?? 'unknown'})` : '';
1019
+ console.log(`## ${EDITOR_LABELS[id]} → ${editorTarget(id, root)}${mark}\n${editorCommand(id, repo)}\n`);
1020
+ }
569
1021
  break;
1022
+ }
570
1023
  case 'init':
571
1024
  case 'up':
572
1025
  case 'remote':