excalidrop 0.2.1 → 0.2.3

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 +9 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +574 -89
  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
  }
@@ -154,15 +173,25 @@ async function deployViewerPages(repo) {
154
173
  function projectNameForRepo(repo) {
155
174
  return `excalidrop-${repo.replace('/', '-').toLowerCase().replace(/[^a-z0-9-]/g, '-')}`.slice(0, 60);
156
175
  }
157
- function viewerUrlFor(repo, target) {
176
+ function viewerUrlFor(repo, target, project) {
158
177
  if (target === 'cloudflare') {
159
178
  // Generic viewer build can't infer the repo from a pages.dev host —
160
179
  // detectRepo() reads it from ?repo= instead.
161
- return `https://${projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
180
+ return `https://${project || projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
162
181
  }
163
182
  const [owner, name] = repo.split('/');
164
183
  return `https://${owner}.github.io/${name}/`;
165
184
  }
185
+ /** Cloudflare project names: lowercase alphanumerics + hyphens, ≤63 chars. */
186
+ export function normalizeProjectName(input, fallback) {
187
+ const cleaned = (input || '')
188
+ .trim()
189
+ .toLowerCase()
190
+ .replace(/[^a-z0-9-]+/g, '-')
191
+ .replace(/^-+|-+$/g, '')
192
+ .slice(0, 63);
193
+ return cleaned || fallback;
194
+ }
166
195
  async function isPrivateRepo(repo) {
167
196
  try {
168
197
  const r = spawnSync('gh', ['api', `repos/${repo}`, '--jq', '.private'], { encoding: 'utf8' });
@@ -183,10 +212,22 @@ async function resolveTarget(repo, explicit) {
183
212
  return explicit;
184
213
  return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
185
214
  }
215
+ const WRANGLER = ['-y', 'wrangler@4'];
216
+ // Which Cloudflare identity would a Pages deploy use? whoami fails when
217
+ // logged out; a CLOUDFLARE_API_TOKEN works without a login. Deploys always
218
+ // run under the USER's own account/quota — never the author's.
219
+ function wranglerAccount() {
220
+ if (process.env.CLOUDFLARE_API_TOKEN)
221
+ return { ok: true, display: 'API token (CLOUDFLARE_API_TOKEN)' };
222
+ const r = runCapture('npx', [...WRANGLER, 'whoami']);
223
+ if (r.status !== 0)
224
+ return { ok: false, display: '' };
225
+ const m = r.out.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
226
+ return { ok: true, display: m?.[0] ?? 'logged in' };
227
+ }
186
228
  function deployToCloudflarePages(project) {
187
229
  // wrangler 4 doesn't auto-create Pages projects and its Workers delegation
188
230
  // misfires on explicit asset dirs — create once, then deploy classic with --force.
189
- const WRANGLER = ['-y', 'wrangler@4'];
190
231
  const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
191
232
  const createOut = (create.stdout || '') + (create.stderr || '');
192
233
  if (create.status !== 0 && !/already exists/i.test(createOut)) {
@@ -205,6 +246,19 @@ function detectSlug() {
205
246
  return '';
206
247
  }
207
248
  }
249
+ /** Accept owner/repo, a GitHub URL, or git@ SSH — always returns owner/repo or ''. */
250
+ export function normalizeRepoSlug(input) {
251
+ const s = (input || '').trim();
252
+ if (!s)
253
+ return '';
254
+ // Full URL / SSH / bare host path → extract the owner/repo tail.
255
+ const m = s.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\/.*)?$/i);
256
+ const candidate = m ? m[1] : s;
257
+ const cleaned = candidate.replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
258
+ const ok = cleaned.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
259
+ return ok ? `${ok[1].toLowerCase()}/${ok[2]}` : '';
260
+ }
261
+ const SLUG_HINT = 'owner/repo or a GitHub link';
208
262
  function ghAuthed() {
209
263
  return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
210
264
  }
@@ -213,59 +267,314 @@ function commandExists(cmd) {
213
267
  return probe.status === 0;
214
268
  }
215
269
  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;
270
+ // Inside a git checkout the repo top is the root. Never walk up past cwd
271
+ // looking for package.json: from a fresh non-checkout dir (e.g. a folder
272
+ // holding a brand-new canvas repo) the old walk-up climbed into an
273
+ // unrelated parent project and wrote .mcp.json / .excalidrop.json there.
274
+ try {
275
+ const top = execSync('git rev-parse --show-toplevel', { cwd: path.resolve(cwd), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
276
+ if (top)
277
+ return top;
225
278
  }
279
+ catch { /* not inside a git work tree — fall through to cwd */ }
280
+ return path.resolve(cwd);
226
281
  }
227
282
  function mcpArgs(repo) {
228
283
  return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
229
284
  }
230
- const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'desktop', 'opencode'];
285
+ const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'cursor-user', 'desktop', 'opencode', 'opencode-user'];
231
286
  function editorCommand(id, repo) {
232
287
  const a = mcpArgs(repo).join(' ');
233
288
  switch (id) {
234
289
  case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
235
290
  case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
236
291
  case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
237
- case 'cursor': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
292
+ case 'cursor':
293
+ case 'cursor-user':
238
294
  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);
295
+ case 'opencode':
296
+ case 'opencode-user': return JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: { excalidrop: { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true } } }, null, 2);
240
297
  }
241
298
  }
242
- function installEditor(id, repo) {
299
+ function editorScope(id) {
300
+ return id === 'claude-project' || id === 'cursor' || id === 'opencode' ? 'project' : 'user';
301
+ }
302
+ // Where each editor id reads its config from. File-based harnesses support
303
+ // both a project file and a user-global file; CLI-driven ones (claude, codex)
304
+ // are detected through the same files their CLIs write.
305
+ function editorFile(id, root) {
306
+ const home = os.homedir();
307
+ switch (id) {
308
+ case 'claude-user': return { path: path.join(home, '.claude.json'), key: 'mcpServers' };
309
+ case 'claude-project': return { path: path.join(root, MCP_JSON), key: 'mcpServers' };
310
+ case 'cursor': return { path: path.join(root, '.cursor', 'mcp.json'), key: 'mcpServers' };
311
+ case 'cursor-user': return { path: path.join(home, '.cursor', 'mcp.json'), key: 'mcpServers' };
312
+ case 'desktop': return { path: desktopConfigPath(), key: 'mcpServers' };
313
+ case 'opencode': return { path: path.join(root, 'opencode.json'), key: 'mcp' };
314
+ case 'opencode-user': return { path: path.join(home, '.config', 'opencode', 'opencode.json'), key: 'mcp' };
315
+ case 'codex': return null; // TOML (~/.codex/config.toml) — parsed separately
316
+ }
317
+ }
318
+ function desktopConfigPath() {
319
+ const home = os.homedir();
320
+ if (process.platform === 'darwin')
321
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
322
+ if (process.platform === 'win32')
323
+ return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
324
+ return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
325
+ }
326
+ function editorTarget(id, root) {
327
+ const f = editorFile(id, root);
328
+ if (id === 'codex')
329
+ return 'user config (~/.codex/config.toml)';
330
+ const scope = editorScope(id);
331
+ const short = f ? shortenHome(f.path) : id;
332
+ return `${scope} scope (${short})`;
333
+ }
334
+ function shortenHome(p) {
335
+ const home = os.homedir();
336
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
337
+ }
338
+ function repoFromArgs(args) {
339
+ if (!Array.isArray(args))
340
+ return null;
341
+ const i = args.findIndex((a) => a === '--repo');
342
+ const v = i !== -1 ? args[i + 1] : null;
343
+ return typeof v === 'string' && v.includes('/') ? v.toLowerCase() : null;
344
+ }
345
+ // {present, repo} for a JSON MCP config (claude/cursor/desktop/opencode shapes).
346
+ function jsonMcpRepo(file, key) {
347
+ let j;
243
348
  try {
244
- if (id === 'claude-project' && commandExists('claude')) {
245
- return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
349
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
350
+ }
351
+ catch {
352
+ return { present: false, repo: null };
353
+ }
354
+ const e = key === 'mcpServers' ? j?.mcpServers?.excalidrop : j?.mcp?.excalidrop;
355
+ if (!e)
356
+ return { present: false, repo: null };
357
+ if (Array.isArray(e.command))
358
+ return { present: true, repo: repoFromArgs(e.command) };
359
+ if (e.command === 'npx' && Array.isArray(e.args))
360
+ return { present: true, repo: repoFromArgs(e.args) };
361
+ return { present: true, repo: null };
362
+ }
363
+ // {present, repo} for Codex's TOML config — crude section parse, no new deps.
364
+ function codexMcpRepo() {
365
+ let txt;
366
+ try {
367
+ txt = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
368
+ }
369
+ catch {
370
+ return { present: false, repo: null };
371
+ }
372
+ const sec = txt.match(/\[mcp_servers\.excalidrop\]([\s\S]*?)(?=^\[|\z)/m);
373
+ if (!sec)
374
+ return { present: false, repo: null };
375
+ const m = (sec[1] ?? '').match(/--repo["'\s=,]+([A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+)/);
376
+ return { present: true, repo: m?.[1]?.toLowerCase() ?? null };
377
+ }
378
+ function detectEditorState(id, repo, root) {
379
+ const want = repo.toLowerCase();
380
+ if (id === 'codex') {
381
+ const s = codexMcpRepo();
382
+ if (!s.present)
383
+ return { status: 'missing', repo: null, where: '~/.codex/config.toml' };
384
+ return s.repo === want
385
+ ? { status: 'current', repo: s.repo, where: '~/.codex/config.toml' }
386
+ : { status: 'stale', repo: s.repo, where: '~/.codex/config.toml' };
387
+ }
388
+ const f = editorFile(id, root);
389
+ const where = shortenHome(f.path);
390
+ const s = jsonMcpRepo(f.path, f.key);
391
+ if (!s.present)
392
+ return { status: 'missing', repo: null, where };
393
+ return s.repo === want
394
+ ? { status: 'current', repo: s.repo, where }
395
+ : { status: 'stale', repo: s.repo, where };
396
+ }
397
+ function updateCommand(id, repo) {
398
+ if (id === 'claude-project' || id === 'claude-user') {
399
+ const scope = id === 'claude-project' ? 'project' : 'user';
400
+ return `claude mcp remove excalidrop -s ${scope} && ${editorCommand(id, repo)}`;
401
+ }
402
+ return editorCommand(id, repo);
403
+ }
404
+ function runCapture(cmd, args) {
405
+ try {
406
+ const r = spawnSync(cmd, args, { encoding: 'utf8' });
407
+ return { status: r.status, out: `${r.stdout || ''}\n${r.stderr || ''}` };
408
+ }
409
+ catch (e) {
410
+ return { status: 1, out: e.message };
411
+ }
412
+ }
413
+ // Merge an MCP entry into a JSON config file (creates parent dirs). Returns
414
+ // false when the existing file isn't valid JSON — caller falls back to manual.
415
+ function upsertJsonMcp(file, key, entry) {
416
+ let j = {};
417
+ if (fs.existsSync(file)) {
418
+ try {
419
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
420
+ }
421
+ catch {
422
+ return false;
423
+ }
424
+ if (!j || typeof j !== 'object')
425
+ return false;
426
+ }
427
+ else {
428
+ try {
429
+ fs.mkdirSync(path.dirname(file), { recursive: true });
246
430
  }
247
- if (id === 'claude-user' && commandExists('claude')) {
248
- return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
431
+ catch {
432
+ return false;
249
433
  }
250
- if (id === 'codex' && commandExists('codex')) {
251
- return spawnSync('codex', ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
434
+ }
435
+ if (key === 'mcpServers') {
436
+ if (!j.mcpServers || typeof j.mcpServers !== 'object')
437
+ j.mcpServers = {};
438
+ j.mcpServers.excalidrop = entry;
439
+ }
440
+ else {
441
+ if (!j.mcp || typeof j.mcp !== 'object')
442
+ j.mcp = {};
443
+ j.mcp.excalidrop = entry;
444
+ if (!j.$schema)
445
+ j.$schema = 'https://opencode.ai/config.json';
446
+ }
447
+ try {
448
+ fs.writeFileSync(file, JSON.stringify(j, null, 2) + '\n');
449
+ return true;
450
+ }
451
+ catch {
452
+ return false;
453
+ }
454
+ }
455
+ function cursorEntry(repo) {
456
+ return { command: 'npx', args: mcpArgs(repo), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
457
+ }
458
+ function opencodeEntry(repo) {
459
+ return { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true, env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
460
+ }
461
+ function installEditor(id, repo, root) {
462
+ const fail = (detail) => ({ id, outcome: 'failed', detail });
463
+ // Idempotent first: never reinstall over an identical entry, never silently
464
+ // leave a stale one — report it with the exact update command.
465
+ const st = detectEditorState(id, repo, root);
466
+ if (st.status === 'current')
467
+ return { id, outcome: 'already-current', detail: `${st.where} already points at ${repo}` };
468
+ const staleNote = st.status === 'stale' ? ` (currently → ${st.repo ?? 'unknown'})` : '';
469
+ if (id === 'claude-project' || id === 'claude-user' || id === 'codex') {
470
+ const bin = id === 'codex' ? 'codex' : 'claude';
471
+ if (!commandExists(bin))
472
+ return fail(`${bin} CLI not found — run manually:\n${editorCommand(id, repo)}`);
473
+ const cargs = id === 'claude-project'
474
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)]
475
+ : id === 'claude-user'
476
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)]
477
+ : ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)];
478
+ const r = runCapture(bin, cargs);
479
+ if (r.status === 0 && !/already exists/i.test(r.out)) {
480
+ return { id, outcome: 'added', detail: editorCommand(id, repo) };
252
481
  }
482
+ if (/already exists/i.test(r.out)) {
483
+ // Exit code is 0 even when nothing changed — re-read the file to say
484
+ // honestly whether the existing entry is current or stale.
485
+ const now = detectEditorState(id, repo, root);
486
+ if (now.status === 'current')
487
+ return { id, outcome: 'already-current', detail: `${now.where} already points at ${repo}` };
488
+ return { id, outcome: 'stale', detail: `already exists${now.repo ? ` → ${now.repo}` : ''} — not updated. Run:\n${updateCommand(id, repo)}` };
489
+ }
490
+ return fail((r.out.trim() || `${bin} exited ${r.status}`).slice(0, 300));
253
491
  }
254
- catch { /* fall through to manual */ }
255
- return false;
492
+ // File-based harnesses: JSON merge is safe to write for both add and update.
493
+ const f = editorFile(id, root);
494
+ const entry = id === 'cursor' || id === 'cursor-user' || id === 'desktop' ? cursorEntry(repo) : opencodeEntry(repo);
495
+ if (upsertJsonMcp(f.path, f.key, entry)) {
496
+ return { id, outcome: 'added', detail: `wrote ${shortenHome(f.path)}${staleNote} → ${repo}` };
497
+ }
498
+ return fail(`${shortenHome(f.path)} is not valid JSON — leaving it untouched. Add manually:\n${editorCommand(id, repo)}`);
256
499
  }
257
500
  const EDITOR_LABELS = {
258
501
  'claude-project': 'Claude Code (project scope)',
259
502
  'claude-user': 'Claude Code (user scope)',
503
+ codex: 'Codex CLI (user config)',
504
+ cursor: 'Cursor (project file)',
505
+ 'cursor-user': 'Cursor (user file)',
506
+ desktop: 'Claude Desktop (user config)',
507
+ opencode: 'OpenCode (project file)',
508
+ 'opencode-user': 'OpenCode (user config)',
509
+ };
510
+ // Harnesses group the flat scope-specific ids for the two-step TUI picker:
511
+ // step 1 picks harnesses, step 2 picks the scope per multi-scope harness.
512
+ // Single-scope harnesses (codex, desktop) are user-global — no step 2.
513
+ const HARNESSES = ['claude', 'codex', 'cursor', 'desktop', 'opencode'];
514
+ const HARNESS_EDITORS = {
515
+ claude: ['claude-user', 'claude-project'],
516
+ codex: ['codex'],
517
+ cursor: ['cursor-user', 'cursor'],
518
+ desktop: ['desktop'],
519
+ opencode: ['opencode-user', 'opencode'],
520
+ };
521
+ const HARNESS_LABELS = {
522
+ claude: 'Claude Code',
260
523
  codex: 'Codex CLI',
261
- cursor: 'Cursor (.cursor/mcp.json)',
524
+ cursor: 'Cursor',
262
525
  desktop: 'Claude Desktop',
263
- opencode: 'OpenCode (opencode.json)',
526
+ opencode: 'OpenCode',
264
527
  };
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`);
528
+ // One-line install-state summary for the harness picker, e.g.
529
+ // "user: installed, project: —" or "installed → other/repo".
530
+ function harnessSummary(h, slug, root) {
531
+ const ids = HARNESS_EDITORS[h];
532
+ const parts = ids.map((id) => {
533
+ const st = detectEditorState(id, slug, root);
534
+ const cur = st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : '—';
535
+ return ids.length > 1 ? `${editorScope(id)}: ${cur}` : cur;
536
+ });
537
+ return parts.join(', ');
538
+ }
539
+ // Scope option for step 2 of the picker: short scope label + live state hint.
540
+ function scopeOption(id, slug, root) {
541
+ const st = detectEditorState(id, slug, root);
542
+ return {
543
+ value: id,
544
+ label: editorTarget(id, root),
545
+ hint: st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : undefined,
546
+ };
547
+ }
548
+ // One honest report for every editor install attempt — always shows the
549
+ // command/config that was (or should be) applied, so "installed" can never
550
+ // mean "already existed but points elsewhere".
551
+ function reportInstalls(results, repo, root) {
552
+ for (const r of results) {
553
+ const target = editorTarget(r.id, root);
554
+ if (r.outcome === 'added')
555
+ console.log(`ok ${EDITOR_LABELS[r.id]} → ${target}:\n ${editorCommand(r.id, repo).split('\n').join('\n ')}\n installed (${r.detail})`);
556
+ else if (r.outcome === 'already-current')
557
+ console.log(`skip ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
558
+ else if (r.outcome === 'stale')
559
+ console.log(`warn ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
560
+ else
561
+ console.log(`fail ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
562
+ }
563
+ }
564
+ /** Poll a viewer URL until it serves 200 (slow first builds take minutes). */
565
+ async function waitForLive(url, timeoutMs = 7 * 60_000) {
566
+ const deadline = Date.now() + timeoutMs;
567
+ while (Date.now() < deadline) {
568
+ try {
569
+ const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
570
+ if (r.ok)
571
+ return true;
572
+ // 404 during initial propagation — keep waiting; anything else too.
573
+ }
574
+ catch { /* offline / DNS — keep waiting */ }
575
+ await new Promise((r) => setTimeout(r, 10000));
576
+ }
577
+ return false;
269
578
  }
270
579
  function parseFlag(args, name) {
271
580
  const eq = args.find((a) => a.startsWith(`--${name}=`));
@@ -288,11 +597,12 @@ function parseEditorsFlag(args) {
288
597
  }
289
598
  return valid;
290
599
  }
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]');
600
+ async function cmdSetup(args, editorOverride, opts = {}) {
601
+ const positional = args.find((a) => !a.startsWith('-') && (a.includes('/') || a.includes('github.com')));
602
+ const rawSlug = parseFlag(args, 'repo') || positional || detectSlug();
603
+ const slug = normalizeRepoSlug(rawSlug);
604
+ if (!slug) {
605
+ console.error(`Usage: npx excalidrop setup ${SLUG_HINT} [--target pages|cloudflare] [--editor claude-project,codex] [--no-prompt]`);
296
606
  process.exit(1);
297
607
  }
298
608
  if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
@@ -311,7 +621,7 @@ async function cmdSetup(args, editorOverride) {
311
621
  }
312
622
  const explicitTarget = parseFlag(args, 'target');
313
623
  const target = await resolveTarget(slug, explicitTarget);
314
- console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? ' (explicit)' : ' (auto: private→cloudflare, public→pages)'}\n`);
624
+ console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? (opts.fromTui ? ' (selected)' : ' (explicit)') : ' (auto: private→cloudflare, public→pages)'}\n`);
315
625
  await ensureMainBranch(slug);
316
626
  const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
317
627
  if (!scene.sha) {
@@ -324,9 +634,39 @@ async function cmdSetup(args, editorOverride) {
324
634
  viewerUrl = viewerUrlFor(slug, 'pages');
325
635
  }
326
636
  else {
327
- deployToCloudflarePages(projectNameForRepo(slug));
328
- viewerUrl = viewerUrlFor(slug, 'cloudflare');
637
+ // Fail fast on missing auth instead of dying mid-deploy. The TUI checks
638
+ // interactively before the spinner; non-interactive runs land here.
639
+ const acct = wranglerAccount();
640
+ if (!acct.ok && !opts.fromTui) {
641
+ throw new Error('Not logged into Cloudflare (wrangler whoami failed). Run `npx -y wrangler@4 login` first, or set CLOUDFLARE_API_TOKEN.');
642
+ }
643
+ console.log(`Cloudflare account: ${acct.display || 'unknown'} — the viewer deploys to YOUR account and quota.`);
644
+ // TUI asks for this (prompt below); --project-name sets it headless.
645
+ // NOTE: renaming the project changes the viewer URL, so a previously
646
+ // registered OAuth callback URL stops matching — re-register login (see
647
+ // README "Browser login") after a rename.
648
+ const project = normalizeProjectName(parseFlag(args, 'project-name') || opts.project || '', projectNameForRepo(slug));
649
+ console.log(`Cloudflare project: ${project} (viewer host ${project}.pages.dev)`);
650
+ deployToCloudflarePages(project);
651
+ viewerUrl = viewerUrlFor(slug, 'cloudflare', project);
329
652
  }
653
+ // Don't declare victory until the viewer actually serves — first Pages /
654
+ // Cloudflare builds take minutes, and "Canvas live" on a 404 wastes a debug
655
+ // session. Poll the URL (up to 7 min for slow first builds), then report
656
+ // honestly with the Pages build state attached.
657
+ const live = await waitForLive(viewerUrl);
658
+ if (live) {
659
+ console.log('Viewer is serving.');
660
+ }
661
+ else {
662
+ console.log('Viewer not live yet (HTTP 404) — hosting may still be building; retry the URL in a minute.');
663
+ const info = await pagesBuildInfo(slug, target).catch(() => null);
664
+ if (info)
665
+ console.log(info);
666
+ }
667
+ await ensureRepoHomepage(slug, viewerUrl);
668
+ const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
669
+ 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
670
  const root = findProjectRoot(process.cwd());
331
671
  fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
332
672
  const mcpPath = path.join(root, MCP_JSON);
@@ -346,18 +686,28 @@ async function cmdSetup(args, editorOverride) {
346
686
  console.log(`MCP entry written to ${mcpPath}`);
347
687
  const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
348
688
  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 };
689
+ const installs = editors.map((id) => installEditor(id, slug, root));
690
+ // The TUI prints its own per-editor report (it owns the spinner); the plain
691
+ // CLI prints here so `setup --no-prompt` shows exactly what ran.
692
+ if (!opts.fromTui)
693
+ reportInstalls(installs, slug, root);
694
+ void noPrompt;
695
+ return { repo: slug, viewerUrl, live, installs };
696
+ }
697
+ // Latest GitHub Pages build state for a repo — printed when the viewer URL
698
+ // still 404s after the wait so the user knows whether it's "still building"
699
+ // vs "build errored / Pages misconfigured".
700
+ async function pagesBuildInfo(repo, target) {
701
+ if (target !== 'pages')
702
+ return `Cloudflare deploy is async — check the Pages project dashboard; if still 404, rerun: npx excalidrop publish ${repo}`;
703
+ try {
704
+ const latest = await ghGet(`/repos/${repo}/pages/builds/latest`);
705
+ const err = latest?.error?.message ? ` — ${latest.error.message}` : '';
706
+ 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}`;
707
+ }
708
+ catch {
709
+ return `Pages build status unknown — check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}).`;
710
+ }
361
711
  }
362
712
  function cmdMcp() {
363
713
  const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
@@ -419,18 +769,29 @@ async function runTui() {
419
769
  const detected = detectSlug();
420
770
  const repoAnswer = await clack.text({
421
771
  message: 'Which repo holds the canvas?',
422
- placeholder: detected || 'owner/repo',
772
+ placeholder: detected || SLUG_HINT,
423
773
  initialValue: detected || '',
424
- validate: (v) => (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(v.trim()) ? undefined : 'Use owner/repo'),
774
+ validate: (v) => (normalizeRepoSlug(v) ? undefined : `Use ${SLUG_HINT}`),
425
775
  });
426
776
  if (clack.isCancel(repoAnswer)) {
427
777
  clack.cancel('Aborted.');
428
778
  process.exit(0);
429
779
  }
430
- const slug = repoAnswer.trim().toLowerCase();
780
+ const slug = normalizeRepoSlug(repoAnswer);
431
781
  const priv = await isPrivateRepo(slug);
432
782
  if (priv !== null)
433
783
  clack.log.info(`Repo is ${priv ? 'private' : 'public'} — recommended host: ${priv ? 'Cloudflare Pages' : 'GitHub Pages'}.`);
784
+ // Visibility drift: repo flipped public<->private since last setup, so the
785
+ // stored target + viewer URL no longer match — nudge to re-pick below.
786
+ try {
787
+ const prevRaw = fs.readFileSync(path.join(findProjectRoot(process.cwd()), CONFIG_NAME), 'utf8');
788
+ const prevTarget = JSON.parse(prevRaw).target;
789
+ const want = priv === true ? 'cloudflare' : 'pages';
790
+ if (prevTarget && prevTarget !== want && priv !== null) {
791
+ 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".`);
792
+ }
793
+ }
794
+ catch { /* first setup — nothing to compare */ }
434
795
  if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
435
796
  const how = await clack.select({
436
797
  message: 'GitHub auth missing. How to log in?',
@@ -461,75 +822,189 @@ async function runTui() {
461
822
  process.exit(1);
462
823
  }
463
824
  }
464
- const auto = await resolveTarget(slug);
825
+ // No auto option: GitHub Pages can't serve private repos on free plans
826
+ // and public repos need zero config, so visibility pre-selects the right
827
+ // host — the user just confirms or flips. Non-interactive `--target` still
828
+ // goes through resolveTarget() in cmdSetup.
829
+ const recommended = priv === true ? 'cloudflare' : 'pages';
465
830
  const hostAnswer = await clack.select({
466
- message: `Where to host the viewer? (auto: ${auto})`,
831
+ message: 'Where to host the viewer?',
832
+ initialValue: recommended,
467
833
  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' },
834
+ { value: 'pages', label: 'GitHub Pages (public repo)', hint: 'zero config' },
835
+ { value: 'cloudflare', label: 'Cloudflare Pages (private repo)', hint: 'needs wrangler login' },
471
836
  ],
472
837
  });
473
838
  if (clack.isCancel(hostAnswer)) {
474
839
  clack.cancel('Aborted.');
475
840
  process.exit(0);
476
841
  }
477
- const target = (hostAnswer === 'auto' ? auto : hostAnswer);
478
- const editorAnswer = await clack.multiselect({
842
+ const target = hostAnswer;
843
+ // Cloudflare project name = the *.pages.dev subdomain. Offer the choice
844
+ // up front so the URL is predictable (and re-runnable without surprises).
845
+ let project = projectNameForRepo(slug);
846
+ if (target === 'cloudflare') {
847
+ const nameAnswer = await clack.text({
848
+ message: 'Cloudflare project name (viewer host)?',
849
+ placeholder: project,
850
+ initialValue: project,
851
+ validate: (v) => (normalizeProjectName(v, '') ? undefined : 'Lowercase letters, numbers, hyphens.'),
852
+ });
853
+ if (clack.isCancel(nameAnswer)) {
854
+ clack.cancel('Aborted.');
855
+ process.exit(0);
856
+ }
857
+ project = normalizeProjectName(nameAnswer, project);
858
+ }
859
+ // Cloudflare deploys run under the USER's own wrangler identity and quota.
860
+ // Verify login up front (with an offer to log in now) instead of failing
861
+ // minutes later mid-deploy.
862
+ if (target === 'cloudflare') {
863
+ let acct = wranglerAccount();
864
+ if (!acct.ok) {
865
+ const login = await clack.confirm({
866
+ message: 'Wrangler is not logged into Cloudflare. Log in now (opens a browser)?',
867
+ });
868
+ if (clack.isCancel(login)) {
869
+ clack.cancel('Aborted.');
870
+ process.exit(0);
871
+ }
872
+ if (login) {
873
+ spawnSync('npx', [...WRANGLER, 'login'], { stdio: 'inherit', env: process.env });
874
+ acct = wranglerAccount();
875
+ }
876
+ if (!acct.ok) {
877
+ clack.log.error('Cloudflare login is required for Cloudflare Pages hosting — pick GitHub Pages, or run `npx -y wrangler@4 login` first.');
878
+ process.exit(1);
879
+ }
880
+ }
881
+ clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
882
+ }
883
+ const projectRoot = findProjectRoot(process.cwd());
884
+ // Step 1: pick harnesses. Preselect ones already wired to this repo;
885
+ // fall back to the historical default when nothing is installed yet.
886
+ const detectedHarnesses = HARNESSES.filter((h) => HARNESS_EDITORS[h].some((id) => detectEditorState(id, slug, projectRoot).status === 'current'));
887
+ const harnessAnswer = await clack.multiselect({
479
888
  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'],
889
+ options: HARNESSES.map((h) => ({
890
+ value: h,
891
+ label: `${HARNESS_LABELS[h]} (${harnessSummary(h, slug, projectRoot)})`,
892
+ })),
893
+ initialValues: detectedHarnesses.length > 0 ? detectedHarnesses : ['claude'],
482
894
  required: false,
483
895
  });
484
- if (clack.isCancel(editorAnswer)) {
896
+ if (clack.isCancel(harnessAnswer)) {
485
897
  clack.cancel('Aborted.');
486
898
  process.exit(0);
487
899
  }
488
- const editors = editorAnswer;
900
+ const harnesses = harnessAnswer;
901
+ // Step 2: per multi-scope harness, pick where to install.
902
+ const editors = [];
903
+ for (const h of harnesses) {
904
+ const ids = HARNESS_EDITORS[h];
905
+ if (ids.length === 1) {
906
+ editors.push(ids[0]);
907
+ continue;
908
+ }
909
+ const current = ids.filter((id) => detectEditorState(id, slug, projectRoot).status === 'current');
910
+ const scopeAnswer = await clack.select({
911
+ message: `Where to install for ${HARNESS_LABELS[h]}?`,
912
+ initialValue: current.length === 1 ? current[0] : ids[0],
913
+ options: [
914
+ ...ids.map((id) => scopeOption(id, slug, projectRoot)),
915
+ { value: '__both__', label: 'Both scopes' },
916
+ ],
917
+ });
918
+ if (clack.isCancel(scopeAnswer)) {
919
+ clack.cancel('Aborted.');
920
+ process.exit(0);
921
+ }
922
+ if (scopeAnswer === '__both__')
923
+ editors.push(...ids);
924
+ else
925
+ editors.push(scopeAnswer);
926
+ }
927
+ // Stale Claude entries can't be overwritten by `add` (it exits 0 with
928
+ // "already exists" and changes nothing) — offer an explicit repoint before
929
+ // the deploy phase so the install below actually lands.
930
+ for (const id of editors) {
931
+ if (id !== 'claude-user' && id !== 'claude-project')
932
+ continue;
933
+ const st = detectEditorState(id, slug, projectRoot);
934
+ if (st.status !== 'stale')
935
+ continue;
936
+ const ok = await clack.confirm({
937
+ message: `${EDITOR_LABELS[id]} points at ${st.repo ?? 'unknown'} — repoint to ${slug}?`,
938
+ });
939
+ if (clack.isCancel(ok)) {
940
+ clack.cancel('Aborted.');
941
+ process.exit(0);
942
+ }
943
+ if (ok) {
944
+ const scope = id === 'claude-project' ? 'project' : 'user';
945
+ runCapture('claude', ['mcp', 'remove', 'excalidrop', '-s', scope]);
946
+ }
947
+ }
489
948
  const s = clack.spinner();
490
949
  s.start('Publishing viewer + wiring MCP…');
491
950
  let viewerUrl;
951
+ let live = false;
952
+ let installs = [];
492
953
  try {
493
- ({ viewerUrl } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors));
954
+ ({ viewerUrl, live, installs } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors, { fromTui: true, project }));
494
955
  s.stop('Setup complete.');
495
956
  }
496
957
  catch (e) {
497
958
  s.stop('Setup failed: ' + e.message);
498
959
  process.exit(1);
499
960
  }
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).`);
961
+ // Always show what ran where: installing to user scope prints the command
962
+ // itself; file writes print the path; stale entries print the update command.
963
+ for (const r of installs) {
964
+ const target = editorTarget(r.id, projectRoot);
965
+ const cmd = editorCommand(r.id, slug).split('\n').join('\n ');
966
+ if (r.outcome === 'added')
967
+ clack.log.success(`MCP → ${target}:\n ${cmd}\n ${r.detail}`);
968
+ else if (r.outcome === 'already-current')
969
+ clack.log.success(`MCP → ${target}: ${r.detail} — skipped.`);
970
+ else if (r.outcome === 'stale')
971
+ clack.log.warn(`MCP → ${target}: ${r.detail}`);
972
+ else
973
+ clack.log.warn(`MCP → ${target} failed: ${r.detail}`);
974
+ }
975
+ // Post-deploy readability probe: one retry — right after a deploy the
976
+ // first API read intermittently fails at TCP level ("fetch failed")
977
+ // while the retry succeeds, and a false "not readable" scares users.
978
+ let scene = null;
979
+ let sceneError = null;
980
+ for (let attempt = 0; attempt < 2 && !scene; attempt++) {
981
+ if (attempt > 0)
982
+ await new Promise((r) => setTimeout(r, 3000));
983
+ try {
984
+ scene = await getScene(slug);
503
985
  }
504
- else {
505
- clack.log.info(`${EDITOR_LABELS[id]} config:\n${editorCommand(id, slug)}`);
986
+ catch (e) {
987
+ sceneError = e;
506
988
  }
507
989
  }
508
- try {
509
- const scene = await getScene(slug);
990
+ if (scene) {
510
991
  clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
511
992
  }
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.');
993
+ else {
994
+ clack.log.warn(`Scene not readable yet: ${sceneError?.message || sceneError} — draw once to create it.`);
524
995
  }
525
- clack.outro(`Canvas live: ${viewerUrl}`);
996
+ if (live)
997
+ clack.log.success('Viewer is serving.');
998
+ else
999
+ clack.log.warn('Viewer was not live after 7 minutes — check the Pages/Cloudflare build, then retry the URL.');
1000
+ clack.outro(live ? `Canvas live: ${viewerUrl}` : `Canvas pending: ${viewerUrl}`);
526
1001
  }
527
1002
  function printHelp() {
528
1003
  console.log(`excalidrop — remote GitHub-backed canvas (no local server)
529
1004
 
530
1005
  Usage:
531
1006
  npx excalidrop interactive setup TUI
532
- npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex,cursor,desktop,opencode] [--no-prompt]
1007
+ npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,claude-user,codex,cursor,cursor-user,desktop,opencode,opencode-user] [--no-prompt]
533
1008
  npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
534
1009
  npx excalidrop login [--repo owner/repo] GitHub device-flow login
535
1010
  npx excalidrop publish owner/repo [--target ..] redeploy viewer only
@@ -564,9 +1039,19 @@ async function main() {
564
1039
  case 'publish':
565
1040
  await cmdSetup(['--no-prompt', ...rest]);
566
1041
  break;
567
- case 'add':
568
- printEditorCommands(parseEditorsFlag(rest) || [...EDITOR_IDS], rest.find((a) => a.includes('/')) || detectSlug() || 'owner/repo');
1042
+ case 'add': {
1043
+ const root = findProjectRoot(process.cwd());
1044
+ const ids = parseEditorsFlag(rest) || [...EDITOR_IDS];
1045
+ const raw = rest.find((a) => a.includes('/') || a.includes('github.com')) || detectSlug() || 'owner/repo';
1046
+ const repo = normalizeRepoSlug(raw) || raw;
1047
+ console.log('Add excalidrop to your AI agent (no install, always latest):\n');
1048
+ for (const id of ids) {
1049
+ const st = repo.includes('/') ? detectEditorState(id, repo, root) : null;
1050
+ const mark = st?.status === 'current' ? ' (installed)' : st?.status === 'stale' ? ` (installed → ${st.repo ?? 'unknown'})` : '';
1051
+ console.log(`## ${EDITOR_LABELS[id]} → ${editorTarget(id, root)}${mark}\n${editorCommand(id, repo)}\n`);
1052
+ }
569
1053
  break;
1054
+ }
570
1055
  case 'init':
571
1056
  case 'up':
572
1057
  case 'remote':