excalidrop 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +58 -131
  2. package/dist/cli.d.ts +0 -21
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +488 -439
  5. package/dist/cli.js.map +1 -1
  6. package/dist/frontend/assets/{advancedFormat-bKUmsig-.js → advancedFormat-CsxROTkH.js} +1 -1
  7. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-C7K_UR3P.js → ar-SA-G6X2FPQ2-Bu8X6mOi.js} +1 -1
  8. package/dist/frontend/assets/arc-B9VNLSzj.js +1 -0
  9. package/dist/frontend/assets/{az-AZ-76LH7QW2-PkhuRuSr.js → az-AZ-76LH7QW2-DpvzY31_.js} +1 -1
  10. package/dist/frontend/assets/{bg-BG-XCXSNQG7-CgLRSYTl.js → bg-BG-XCXSNQG7-DCtZnLyo.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-38ab4fdb-BFdBRYSn.js → blockDiagram-38ab4fdb-4cfDkUsw.js} +1 -1
  12. package/dist/frontend/assets/{blockDiagram-68f4deed-Ynbqr7bQ.js → blockDiagram-68f4deed-CsgWL6uC.js} +1 -1
  13. package/dist/frontend/assets/{bn-BD-2XOGV67Q-CEvkU0mz.js → bn-BD-2XOGV67Q-B-IdLNKT.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-15b5d702-Dm7F1Pn_.js → c4Diagram-15b5d702-7BAqo4qT.js} +1 -1
  15. package/dist/frontend/assets/{c4Diagram-3d4e48cf-COkK-e67.js → c4Diagram-3d4e48cf-CDW-O-vg.js} +1 -1
  16. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-DzP6q9bE.js → ca-ES-6MX7JW3Y-DM8olClc.js} +1 -1
  17. package/dist/frontend/assets/channel-jdNM4v-F.js +1 -0
  18. package/dist/frontend/assets/{classDiagram-70f12bd4-CRzulwvH.js → classDiagram-70f12bd4-D2U4i4yC.js} +1 -1
  19. package/dist/frontend/assets/{classDiagram-d40c83e7-DNbA-n-a.js → classDiagram-d40c83e7-DtBHRJc9.js} +1 -1
  20. package/dist/frontend/assets/classDiagram-v2-d5a6b087-Drykcxns.js +2 -0
  21. package/dist/frontend/assets/classDiagram-v2-f2320105-D8ruMBA6.js +2 -0
  22. package/dist/frontend/assets/clone-DFO_zGRO.js +1 -0
  23. package/dist/frontend/assets/{createText-2e5e7dd3-DB_k52Pn.js → createText-2e5e7dd3-1KPUhDpM.js} +1 -1
  24. package/dist/frontend/assets/{createText-d213de94-D-G49giw.js → createText-d213de94-C6EhGbUN.js} +4 -4
  25. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-Bf4TOzpR.js → cs-CZ-2BRQDIVT-DgjTgz6K.js} +1 -1
  26. package/dist/frontend/assets/{cytoscape-cose-bilkent-DQYypj0P.js → cytoscape-cose-bilkent-BnIHl72R.js} +1 -1
  27. package/dist/frontend/assets/{da-DK-5WZEPLOC-jO-5kyXb.js → da-DK-5WZEPLOC-uEs3xGKF.js} +1 -1
  28. package/dist/frontend/assets/{de-DE-XR44H4JA-BkNXWOVh.js → de-DE-XR44H4JA-CBglLw42.js} +1 -1
  29. package/dist/frontend/assets/drop-BFhuMYSw.png +0 -0
  30. package/dist/frontend/assets/{edges-332bd1c7-CVvMtkTa.js → edges-332bd1c7-cWCBwcrg.js} +1 -1
  31. package/dist/frontend/assets/{edges-e0da2a9e-DQBAvC8-.js → edges-e0da2a9e-DKeDUJ_F.js} +1 -1
  32. package/dist/frontend/assets/{el-GR-BZB4AONW-Dv7Y9hMk.js → el-GR-BZB4AONW-B-_eub00.js} +1 -1
  33. package/dist/frontend/assets/{elk.bundled-YVS3PsMH.js → elk.bundled-Ch9TeAu4.js} +1 -1
  34. package/dist/frontend/assets/{erDiagram-880f2ed8-BjBeR38f.js → erDiagram-880f2ed8-CtDf_58r.js} +4 -4
  35. package/dist/frontend/assets/{erDiagram-9861fffd-B2Bi3R45.js → erDiagram-9861fffd-CAlUeljk.js} +1 -1
  36. package/dist/frontend/assets/{es-ES-U4NZUMDT-CWhs0SQY.js → es-ES-U4NZUMDT-BIN3bSVD.js} +1 -1
  37. package/dist/frontend/assets/{eu-ES-A7QVB2H4-BfkFwc3e.js → eu-ES-A7QVB2H4-TyQet3-N.js} +1 -1
  38. package/dist/frontend/assets/{fa-IR-HGAKTJCU-BgWzSj46.js → fa-IR-HGAKTJCU-DVmjjr57.js} +1 -1
  39. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-CUyvkum-.js → fi-FI-Z5N7JZ37-ZW9kXLa8.js} +1 -1
  40. package/dist/frontend/assets/{flowDb-7c981674-BSZyyVBf.js → flowDb-7c981674-B5ppx8EN.js} +1 -1
  41. package/dist/frontend/assets/{flowDb-956e92f1-BC3-CLkF.js → flowDb-956e92f1-Dqi00Wq-.js} +1 -1
  42. package/dist/frontend/assets/{flowDiagram-66a62f08-H57DAvmM.js → flowDiagram-66a62f08-v3Liu0m_.js} +1 -1
  43. package/dist/frontend/assets/{flowDiagram-cbd28bf7-Bw4Z2nBD.js → flowDiagram-cbd28bf7-DJ_YHRwa.js} +3 -3
  44. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-6vlgZ0Bg.js +1 -0
  45. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-FmnI78Vm.js +1 -0
  46. package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-1kZNdf3X.js → flowchart-elk-definition-36e2d292-BleYul6b.js} +2 -2
  47. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-DDhG2Esi.js → flowchart-elk-definition-4a651766-D6lLh4Ih.js} +1 -1
  48. package/dist/frontend/assets/{fr-FR-RHASNOE6-7xzoRwbd.js → fr-FR-RHASNOE6-CxLR66tH.js} +1 -1
  49. package/dist/frontend/assets/{ganttDiagram-04f9e578-CtUSsXCz.js → ganttDiagram-04f9e578-DJSOyr8B.js} +1 -1
  50. package/dist/frontend/assets/{ganttDiagram-c361ad54-K42AklFY.js → ganttDiagram-c361ad54-CJmbCLvp.js} +1 -1
  51. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-uW6XA6Y6.js → gitGraphDiagram-21fc4d3e-DiRq_FJc.js} +6 -6
  52. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-CCNbhnBg.js → gitGraphDiagram-72cf32ee-MfvDzoAb.js} +1 -1
  53. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CXuDDwNP.js → gl-ES-HMX3MZ6V-CxWj2itK.js} +1 -1
  54. package/dist/frontend/assets/{graph-CYOmcSYN.js → graph-CT_r46y9.js} +1 -1
  55. package/dist/frontend/assets/{graph-o42dOfI7.js → graph-DJt_Vezo.js} +1 -1
  56. package/dist/frontend/assets/{he-IL-6SHJWFNN-O9dzh9EM.js → he-IL-6SHJWFNN-5v2WFGO3.js} +1 -1
  57. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DlrOXhWs.js → hi-IN-IWLTKZ5I-BJGbvgRu.js} +1 -1
  58. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-C3eNg-uI.js → hu-HU-A5ZG7DT2-REQT2EcE.js} +1 -1
  59. package/dist/frontend/assets/{id-ID-SAP4L64H-6iM4pI5c.js → id-ID-SAP4L64H-aZWQYWdg.js} +1 -1
  60. package/dist/frontend/assets/{index-3862675e-DrvLhypC.js → index-3862675e-BgrvgbsM.js} +1 -1
  61. package/dist/frontend/assets/{index-6079d271-BgVTRqg2.js → index-6079d271-Cp8jFHTC.js} +1 -1
  62. package/dist/frontend/assets/{index-HkUXslJy.js → index-DBHHeSXy.js} +4 -4
  63. package/dist/frontend/assets/index-GTq6y6g6.js +349 -0
  64. package/dist/frontend/assets/{infoDiagram-4a4f5b27-ZxmAcaW1.js → infoDiagram-4a4f5b27-C1ZJJtWv.js} +3 -3
  65. package/dist/frontend/assets/{infoDiagram-f8f76790-Bn-PhiFG.js → infoDiagram-f8f76790-GeS0JKxo.js} +1 -1
  66. package/dist/frontend/assets/{it-IT-JPQ66NNP-B26NQzGp.js → it-IT-JPQ66NNP-EyeA2Ty1.js} +1 -1
  67. package/dist/frontend/assets/{ja-JP-DBVTYXUO-CB0fvnYJ.js → ja-JP-DBVTYXUO-BLcE45D9.js} +1 -1
  68. package/dist/frontend/assets/{journeyDiagram-29694f62-ggDKs06f.js → journeyDiagram-29694f62-4Z_lpK4g.js} +5 -5
  69. package/dist/frontend/assets/{journeyDiagram-49397b02-pdDS-n-z.js → journeyDiagram-49397b02-ChwU3Zo6.js} +1 -1
  70. package/dist/frontend/assets/{kaa-6HZHGXH3-YNdBdcwJ.js → kaa-6HZHGXH3-CkpTcX9X.js} +1 -1
  71. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-D86pwZO0.js → kab-KAB-ZGHBKWFO-CChq0y6o.js} +1 -1
  72. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-CpG5oheM.js → kk-KZ-P5N5QNE5-CwtoTY4z.js} +1 -1
  73. package/dist/frontend/assets/{km-KH-HSX4SM5Z-DYOZ3Azw.js → km-KH-HSX4SM5Z-D26sdohj.js} +1 -1
  74. package/dist/frontend/assets/{ko-KR-MTYHY66A-BHorm6lC.js → ko-KR-MTYHY66A-DvnDcJQA.js} +1 -1
  75. package/dist/frontend/assets/{ku-TR-6OUDTVRD-CM9RgEHv.js → ku-TR-6OUDTVRD-qdHbLgKg.js} +1 -1
  76. package/dist/frontend/assets/{layout-DW9Qf-At.js → layout-C-wUeNvb.js} +1 -1
  77. package/dist/frontend/assets/{layout-CCtkxrAf.js → layout-YRLNN4au.js} +1 -1
  78. package/dist/frontend/assets/{line-DvZfgg9P.js → line-BEgMClKG.js} +1 -1
  79. package/dist/frontend/assets/{linear-FwFrGRfa.js → linear-DDHpSdYw.js} +1 -1
  80. package/dist/frontend/assets/{lt-LT-XHIRWOB4-DvriKXBj.js → lt-LT-XHIRWOB4-DT4LYe_d.js} +1 -1
  81. package/dist/frontend/assets/{lv-LV-5QDEKY6T-DzQpwk87.js → lv-LV-5QDEKY6T-Dbc3ivwT.js} +1 -1
  82. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-DsRo8f2w.js → mindmap-definition-ac74a2e8-CY2RhWUN.js} +6 -6
  83. package/dist/frontend/assets/{mindmap-definition-fc14e90a-dQ9reluI.js → mindmap-definition-fc14e90a-VT4JDi6N.js} +1 -1
  84. package/dist/frontend/assets/{mr-IN-CRQNXWMA-C7RzxxP5.js → mr-IN-CRQNXWMA-D75p1fVc.js} +1 -1
  85. package/dist/frontend/assets/{my-MM-5M5IBNSE-B-RRuy29.js → my-MM-5M5IBNSE-DnEoBXyE.js} +1 -1
  86. package/dist/frontend/assets/{nb-NO-T6EIAALU-B47x16Ik.js → nb-NO-T6EIAALU-BL4ThJxl.js} +1 -1
  87. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-BxAKoQne.js → nl-NL-IS3SIHDZ-u18nUblY.js} +1 -1
  88. package/dist/frontend/assets/{nn-NO-6E72VCQL-dfpbEDSZ.js → nn-NO-6E72VCQL-B9LJ0oXc.js} +1 -1
  89. package/dist/frontend/assets/{oc-FR-POXYY2M6-gv6geBjV.js → oc-FR-POXYY2M6-BKG5kIcO.js} +1 -1
  90. package/dist/frontend/assets/{pa-IN-N4M65BXN-CY30-qH7.js → pa-IN-N4M65BXN-CmpnjbkL.js} +1 -1
  91. package/dist/frontend/assets/{pica-DWYGHLQV.js → pica-BcpcM0v1.js} +1 -1
  92. package/dist/frontend/assets/{pie-CDiWh1rj.js → pie-Swzl5NrI.js} +1 -1
  93. package/dist/frontend/assets/{pieDiagram-421022e6-Bgy-duyf.js → pieDiagram-421022e6-ndz96BLe.js} +5 -5
  94. package/dist/frontend/assets/{pieDiagram-8a3498a8-DH7FsbPK.js → pieDiagram-8a3498a8-D5rcV68O.js} +1 -1
  95. package/dist/frontend/assets/{pl-PL-T2D74RX3-TrHugsIA.js → pl-PL-T2D74RX3-CrR66Rgj.js} +1 -1
  96. package/dist/frontend/assets/{pt-BR-5N22H2LF-CJxdFp9c.js → pt-BR-5N22H2LF-Crv7jZiH.js} +1 -1
  97. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CB-1q6YJ.js → pt-PT-UZXXM6DQ-Bkaa5ESR.js} +1 -1
  98. package/dist/frontend/assets/{quadrantDiagram-0957ecba-DcwYI1oi.js → quadrantDiagram-0957ecba-DOdyxeTP.js} +3 -3
  99. package/dist/frontend/assets/{quadrantDiagram-120e2f19-CvHU5a9v.js → quadrantDiagram-120e2f19-beQD7R1C.js} +1 -1
  100. package/dist/frontend/assets/relay-BIOL2uw2.js +1 -0
  101. package/dist/frontend/assets/{requirementDiagram-23d650b8-tdagN8HP.js → requirementDiagram-23d650b8-B2i-OB97.js} +1 -1
  102. package/dist/frontend/assets/{requirementDiagram-deff3bca-BMGM-fSI.js → requirementDiagram-deff3bca-beS-SvfY.js} +1 -1
  103. package/dist/frontend/assets/{ro-RO-JPDTUUEW-DQq9DBOd.js → ro-RO-JPDTUUEW-DsrnQm7K.js} +1 -1
  104. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-CWX32rfh.js → ru-RU-B4JR7IUQ-DrnGGdUs.js} +1 -1
  105. package/dist/frontend/assets/{sankeyDiagram-04a897e0-3i6zWpY2.js → sankeyDiagram-04a897e0-CVad45bN.js} +1 -1
  106. package/dist/frontend/assets/{sankeyDiagram-23345273-ncj1Ol7S.js → sankeyDiagram-23345273-ghWBXXF2.js} +1 -1
  107. package/dist/frontend/assets/{selectAll-DJwesbwT.js → selectAll-XplaO988.js} +1 -1
  108. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-ChyKKRZA.js → sequenceDiagram-17ac3bff-DBI-EdWx.js} +1 -1
  109. package/dist/frontend/assets/{sequenceDiagram-704730f1-D57_wP9k.js → sequenceDiagram-704730f1-BnJ__mBt.js} +1 -1
  110. package/dist/frontend/assets/{si-LK-N5RQ5JYF-Lkc5ZxyG.js → si-LK-N5RQ5JYF-BxAwuRM5.js} +1 -1
  111. package/dist/frontend/assets/{sk-SK-C5VTKIMK-DyvYXjqZ.js → sk-SK-C5VTKIMK-CnDyiLRf.js} +1 -1
  112. package/dist/frontend/assets/{sl-SI-NN7IZMDC-D_MvP9Rh.js → sl-SI-NN7IZMDC-BXHXIdFA.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-587899a1-OjFTnFLS.js → stateDiagram-587899a1-D3Ahxsgc.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-9c5f0230-BzU36S5C.js → stateDiagram-9c5f0230-8XbLWvC0.js} +1 -1
  115. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-CfX1qo7U.js → stateDiagram-v2-51a3dcff-Cqf-MUGD.js} +1 -1
  116. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-BH7Ud4sU.js → stateDiagram-v2-d93cdb3a-DjGrud9D.js} +1 -1
  117. package/dist/frontend/assets/{styles-2ab5d517-CA8x9Nh5.js → styles-2ab5d517-B4qdHOgu.js} +3 -3
  118. package/dist/frontend/assets/{styles-5f03d8d2-BnO2pLhV.js → styles-5f03d8d2-DOD2v5V8.js} +3 -3
  119. package/dist/frontend/assets/{styles-6aaf32cf-COnot1ea.js → styles-6aaf32cf-bP3XXcmf.js} +1 -1
  120. package/dist/frontend/assets/{styles-9a916d00-C3QkBgTF.js → styles-9a916d00-QLeuL_Dy.js} +1 -1
  121. package/dist/frontend/assets/{styles-c10674c1-CPAG2_rr.js → styles-c10674c1-Lpvh1u4f.js} +1 -1
  122. package/dist/frontend/assets/{styles-edf9a4b0-B8Q9fyu0.js → styles-edf9a4b0-D4W9aPm7.js} +1 -1
  123. package/dist/frontend/assets/subset-shared.chunk.js +9 -9
  124. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  125. package/dist/frontend/assets/{sv-SE-XGPEYMSR-t6voGQSI.js → sv-SE-XGPEYMSR-YrCQQBuo.js} +1 -1
  126. package/dist/frontend/assets/{svgDrawCommon-08f97a94-CdT21LWf.js → svgDrawCommon-08f97a94-CwNv3PjS.js} +1 -1
  127. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-D1MU7jAE.js → svgDrawCommon-3ba9043b-DZbT0Bqu.js} +1 -1
  128. package/dist/frontend/assets/{ta-IN-2NMHFXQM-7VmfDDyV.js → ta-IN-2NMHFXQM-qR82aBSW.js} +1 -1
  129. package/dist/frontend/assets/{th-TH-HPSO5L25-B5rLEY9P.js → th-TH-HPSO5L25-C65V7hE5.js} +1 -1
  130. package/dist/frontend/assets/{timeline-definition-7e6b55e7-BIcL44O_.js → timeline-definition-7e6b55e7-GkPy3QeG.js} +3 -3
  131. package/dist/frontend/assets/{timeline-definition-85554ec2-TgrYYwpU.js → timeline-definition-85554ec2-CCdkBaC0.js} +3 -3
  132. package/dist/frontend/assets/{tr-TR-DEFEU3FU-0CgOHXTn.js → tr-TR-DEFEU3FU-CFzbwAKV.js} +1 -1
  133. package/dist/frontend/assets/{uk-UA-QMV73CPH-CzSFsIsx.js → uk-UA-QMV73CPH-ePt5WVz3.js} +1 -1
  134. package/dist/frontend/assets/{union-CnQAJu0l.js → union-CVZobYBh.js} +1 -1
  135. package/dist/frontend/assets/{vi-VN-M7AON7JQ-Bhx3rYU9.js → vi-VN-M7AON7JQ-CaPKykgJ.js} +1 -1
  136. package/dist/frontend/assets/{xychartDiagram-b6496bcd-BMKDop6v.js → xychartDiagram-b6496bcd-CV5WRc81.js} +4 -4
  137. package/dist/frontend/assets/{xychartDiagram-e933f94c-DAWithrN.js → xychartDiagram-e933f94c-BH-ySdQf.js} +1 -1
  138. package/dist/frontend/assets/{zh-CN-LNUGB5OW-Br52_wH4.js → zh-CN-LNUGB5OW-CK8AEj8o.js} +1 -1
  139. package/dist/frontend/assets/{zh-HK-E62DVLB3-Cj6mnIQ9.js → zh-HK-E62DVLB3-CUINiFgH.js} +1 -1
  140. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-yLEsVBbC.js → zh-TW-RAJ6MFWO-BEBY7iJR.js} +1 -1
  141. package/dist/frontend/assets/zipObject-W8SLG3F1.js +1 -0
  142. package/dist/frontend/index.html +168 -235
  143. package/dist/{index.d.ts → mcp.d.ts} +1 -1
  144. package/dist/mcp.d.ts.map +1 -0
  145. package/dist/mcp.js +1183 -0
  146. package/dist/mcp.js.map +1 -0
  147. package/package.json +19 -34
  148. package/skills/excalidraw-skill/SKILL.md +42 -55
  149. package/skills/excalidraw-skill/references/cheatsheet.md +23 -56
  150. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +9 -32
  151. package/skills/excalidraw-skill/scripts/create-element.cjs +18 -61
  152. package/skills/excalidraw-skill/scripts/delete-element.cjs +12 -42
  153. package/skills/excalidraw-skill/scripts/export-elements.cjs +17 -44
  154. package/skills/excalidraw-skill/scripts/gh-helper.cjs +58 -0
  155. package/skills/excalidraw-skill/scripts/healthcheck.cjs +8 -29
  156. package/skills/excalidraw-skill/scripts/import-elements.cjs +20 -70
  157. package/skills/excalidraw-skill/scripts/update-element.cjs +19 -63
  158. package/tsconfig.json +3 -2
  159. package/vite.config.js +3 -12
  160. package/dist/frontend/assets/arc-BI_FJWE7.js +0 -1
  161. package/dist/frontend/assets/channel-BqCiG5n3.js +0 -1
  162. package/dist/frontend/assets/classDiagram-v2-d5a6b087-l-GglywH.js +0 -2
  163. package/dist/frontend/assets/classDiagram-v2-f2320105-B_TY1zoc.js +0 -2
  164. package/dist/frontend/assets/clone-B6Jqcrku.js +0 -1
  165. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CBjbm_oB.js +0 -1
  166. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CoQufPp2.js +0 -1
  167. package/dist/frontend/assets/index-D-cr7jpJ.js +0 -349
  168. package/dist/frontend/assets/zipObject-BeXj5PVd.js +0 -1
  169. package/dist/index.d.ts.map +0 -1
  170. package/dist/index.js +0 -2305
  171. package/dist/index.js.map +0 -1
  172. package/dist/remote.d.ts +0 -5
  173. package/dist/remote.d.ts.map +0 -1
  174. package/dist/remote.js +0 -129
  175. package/dist/remote.js.map +0 -1
  176. package/dist/server.d.ts +0 -4
  177. package/dist/server.d.ts.map +0 -1
  178. package/dist/server.js +0 -1217
  179. package/dist/server.js.map +0 -1
  180. package/dist/target.d.ts +0 -41
  181. package/dist/target.d.ts.map +0 -1
  182. package/dist/target.js +0 -244
  183. package/dist/target.js.map +0 -1
  184. package/dist/types.d.ts +0 -228
  185. package/dist/types.d.ts.map +0 -1
  186. package/dist/types.js +0 -53
  187. package/dist/types.js.map +0 -1
  188. package/dist/utils/auth.d.ts +0 -15
  189. package/dist/utils/auth.d.ts.map +0 -1
  190. package/dist/utils/auth.js +0 -68
  191. package/dist/utils/auth.js.map +0 -1
  192. package/dist/utils/github.d.ts +0 -26
  193. package/dist/utils/github.d.ts.map +0 -1
  194. package/dist/utils/github.js +0 -111
  195. package/dist/utils/github.js.map +0 -1
  196. package/dist/utils/logger.d.ts +0 -4
  197. package/dist/utils/logger.d.ts.map +0 -1
  198. package/dist/utils/logger.js +0 -25
  199. package/dist/utils/logger.js.map +0 -1
  200. package/dist/utils/r2store.d.ts +0 -19
  201. package/dist/utils/r2store.d.ts.map +0 -1
  202. package/dist/utils/r2store.js +0 -75
  203. package/dist/utils/r2store.js.map +0 -1
  204. package/frontend/index.html +0 -338
  205. package/frontend/src/App.tsx +0 -1091
  206. package/frontend/src/main.tsx +0 -15
  207. package/frontend/src/utils/ghSync.ts +0 -171
  208. package/frontend/src/utils/mermaidConverter.ts +0 -49
  209. package/frontend/src/utils/webmcp.ts +0 -285
  210. package/scripts/check-local-bind.mjs +0 -164
  211. package/scripts/postinstall.mjs +0 -23
  212. package/scripts/publish-pages.sh +0 -39
  213. package/scripts/seed-r2.mjs +0 -9
  214. package/src/cli.ts +0 -559
  215. package/src/index.ts +0 -2557
  216. package/src/remote.ts +0 -92
  217. package/src/server.ts +0 -1349
  218. package/src/target.ts +0 -208
  219. package/src/types.ts +0 -338
  220. package/src/utils/auth.ts +0 -61
  221. package/src/utils/github.ts +0 -93
  222. package/src/utils/logger.ts +0 -35
  223. package/src/utils/r2store.ts +0 -76
package/dist/cli.js CHANGED
@@ -1,209 +1,205 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * excalidrop — per-project Excalidraw canvas + MCP server setup.
4
- *
5
- * Modelled on the `chrome-devtools-mcp` install pattern (`npx -y <pkg>` +
6
- * `mcpServers` JSON), with one addition: per-project dynamic port selection
7
- * so multiple checkouts can run their own canvas concurrently.
8
- *
9
- * How the dynamic port works:
10
- * - `excalidrop init` picks a free port (default scan from 3030) and writes
11
- * it to `.excalidrop.json` in the project root.
12
- * - `excalidrop mcp` (what the AI agent runs via stdio) resolves that same
13
- * file by walking up from cwd, so each project transparently gets its own
14
- * canvas URL with nothing hardcoded in the agent config.
15
- *
16
- * Commands:
17
- * init pick a port, write .excalidrop.json + .mcp.json, offer agent install
18
- * up start the canvas server on this project's port
19
- * mcp run the MCP stdio server pointed at this project's canvas (for agents)
20
- * status show resolved port + canvas health
21
- * add print / run AI-agent install commands
22
- */
23
- import { spawn, spawnSync } from 'node:child_process';
24
- import { createConnection } from 'node:net';
2
+ import { spawn, spawnSync, execSync } from 'node:child_process';
25
3
  import fs from 'node:fs';
4
+ import os from 'node:os';
26
5
  import path from 'node:path';
27
- import readline from 'node:readline';
28
6
  import { fileURLToPath } from 'node:url';
29
7
  const __filename = fileURLToPath(import.meta.url);
30
8
  const __dirname = path.dirname(__filename);
31
9
  const CONFIG_NAME = '.excalidrop.json';
32
10
  const MCP_JSON = '.mcp.json';
33
- const SERVER_ENTRY = path.join(__dirname, 'server.js');
34
- const MCP_ENTRY = path.join(__dirname, 'index.js');
35
- const DEFAULT_START_PORT = 3030;
36
- const PORT_SCAN_SIZE = 200;
37
- // ─── port utils ──────────────────────────────────────────────────────────────
38
- function isPortFree(port, host = '127.0.0.1', timeoutMs = 300) {
39
- return new Promise((resolve) => {
40
- let settled = false;
41
- const socket = createConnection({ host, port });
42
- const done = (free) => {
43
- if (settled)
44
- return;
45
- settled = true;
46
- socket.destroy();
47
- resolve(free);
48
- };
49
- socket.setTimeout(timeoutMs);
50
- socket.once('connect', () => done(false)); // something listens → taken
51
- socket.once('timeout', () => done(true));
52
- socket.once('error', () => done(true)); // ECONNREFUSED → free
11
+ const RELAY_DEFAULT = 'https://excalidrop.wtf403.workers.dev';
12
+ const GH_API = 'https://api.github.com';
13
+ const SCENE_PATH = process.env.SCENE_PATH || 'canvas.excalidraw';
14
+ const CANVAS_BRANCH = process.env.CANVAS_BRANCH || 'excalidrop';
15
+ const PAGES_BRANCH = process.env.PAGES_BRANCH || 'excalidrop';
16
+ function ghToken() {
17
+ if (process.env.GITHUB_TOKEN)
18
+ return process.env.GITHUB_TOKEN;
19
+ try {
20
+ return execSync('gh auth token', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
21
+ }
22
+ catch {
23
+ return '';
24
+ }
25
+ }
26
+ function ghHeaders() {
27
+ return { Authorization: `Bearer ${ghToken()}`, Accept: 'application/vnd.github+json', 'User-Agent': 'excalidrop-setup', 'Content-Type': 'application/json' };
28
+ }
29
+ async function ghGet(pathname) {
30
+ const r = await fetch(`${GH_API}${pathname}`, { headers: ghHeaders() });
31
+ if (!r.ok)
32
+ throw new Error(`GitHub GET ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
33
+ return r.json();
34
+ }
35
+ async function ghPut(pathname, body) {
36
+ const r = await fetch(`${GH_API}${pathname}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
37
+ if (!r.ok)
38
+ throw new Error(`GitHub PUT ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
39
+ return r.json();
40
+ }
41
+ async function ensureMainBranch(repo) {
42
+ const main = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/main`, { headers: ghHeaders() });
43
+ if (main.ok)
44
+ return;
45
+ const branches = await (await fetch(`${GH_API}/repos/${repo}/branches`, { headers: ghHeaders() })).json();
46
+ if (branches.length !== 0)
47
+ return;
48
+ await ghPut(`/repos/${repo}/contents/README.md`, {
49
+ message: 'Initial commit',
50
+ content: Buffer.from(`# ${repo.split('/')[1]}\n\nExcalidraw canvas powered by [excalidrop](https://github.com/wtf403/excalidrop).\n`).toString('base64'),
51
+ branch: 'main',
53
52
  });
54
53
  }
55
- async function findFreePort(preferred, start = DEFAULT_START_PORT) {
56
- if (preferred && preferred > 0 && preferred < 65536 && (await isPortFree(preferred))) {
57
- return preferred;
54
+ async function getScene(repo) {
55
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}?ref=${CANVAS_BRANCH}`, { headers: ghHeaders() });
56
+ if (r.status === 404)
57
+ return { elements: [], sha: null };
58
+ if (!r.ok)
59
+ throw new Error(`scene read ${r.status}`);
60
+ const j = await r.json();
61
+ let b64 = typeof j.content === 'string' && j.content.length ? j.content : null;
62
+ if (!b64) {
63
+ const blob = await fetch(`${GH_API}/repos/${repo}/git/blobs/${j.sha}`, { headers: ghHeaders() });
64
+ if (blob.ok)
65
+ b64 = (await blob.json()).content || null;
58
66
  }
59
- for (let port = start; port < start + PORT_SCAN_SIZE; port++) {
60
- if (await isPortFree(port))
61
- return port;
67
+ if (!b64)
68
+ return { elements: [], sha: j.sha || null };
69
+ return { elements: (JSON.parse(Buffer.from(b64, 'base64').toString('utf8')).elements || []), sha: j.sha };
70
+ }
71
+ async function putScene(repo, elements, sha, message) {
72
+ const body = {
73
+ message,
74
+ content: Buffer.from(JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements, appState: { viewBackgroundColor: '#ffffff', gridSize: null }, files: {} }, null, 2)).toString('base64'),
75
+ branch: CANVAS_BRANCH,
76
+ };
77
+ if (sha)
78
+ body.sha = sha;
79
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
80
+ if (r.status === 404) {
81
+ // create the branch from default, then retry once
82
+ const info = await ghGet(`/repos/${repo}`);
83
+ const from = info.default_branch || 'main';
84
+ const ref = await ghGet(`/repos/${repo}/git/ref/heads/${from}`);
85
+ await fetch(`${GH_API}/repos/${repo}/git/refs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ref: `refs/heads/${CANVAS_BRANCH}`, sha: ref.object.sha }) });
86
+ const retry = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
87
+ if (!retry.ok)
88
+ throw new Error(`scene write ${retry.status}`);
89
+ return;
62
90
  }
63
- // Fallback: random high port
64
- for (let i = 0; i < 50; i++) {
65
- const port = 32000 + Math.floor(Math.random() * 2000);
66
- if (await isPortFree(port))
67
- return port;
91
+ if (!r.ok)
92
+ throw new Error(`scene write ${r.status}: ${(await r.text()).slice(0, 150)}`);
93
+ }
94
+ function viewerDir() {
95
+ // dist/cli.js → dist/frontend; fallback to checkout layout.
96
+ const cands = [path.join(__dirname, 'frontend'), path.join(process.cwd(), 'dist', 'frontend')];
97
+ for (const d of cands) {
98
+ if (fs.existsSync(path.join(d, 'index.html')) && fs.existsSync(path.join(d, 'assets')))
99
+ return d;
68
100
  }
69
- throw new Error('Could not find a free port for the excalidrop canvas server.');
101
+ throw new Error('Viewer build not found (dist/frontend). Reinstall excalidrop or run `npm run build:frontend` in the excalidrop checkout.');
70
102
  }
71
- // ─── config discovery ────────────────────────────────────────────────────────
72
- function findProjectRoot(cwd) {
73
- let dir = path.resolve(cwd);
74
- const home = process.env.HOME || process.env.USERPROFILE || '/';
75
- while (true) {
76
- if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))) {
77
- return dir;
78
- }
79
- const parent = path.dirname(dir);
80
- if (parent === dir || dir === home)
81
- return path.resolve(cwd);
82
- dir = parent;
103
+ function listFilesRecursive(dir, base = dir) {
104
+ const out = [];
105
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
106
+ const full = path.join(dir, e.name);
107
+ if (e.isDirectory())
108
+ out.push(...listFilesRecursive(full, base));
109
+ else if (e.isFile())
110
+ out.push(path.relative(base, full));
83
111
  }
112
+ return out;
84
113
  }
85
- function findDropConfig(cwd) {
86
- let dir = path.resolve(cwd);
87
- const root = path.parse(dir).root;
88
- while (true) {
89
- const file = path.join(dir, CONFIG_NAME);
90
- if (fs.existsSync(file)) {
91
- try {
92
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
93
- if (typeof raw.port === 'number') {
94
- return {
95
- dir,
96
- config: {
97
- port: raw.port,
98
- canvasUrl: raw.canvasUrl || `http://127.0.0.1:${raw.port}`,
99
- },
100
- };
101
- }
102
- }
103
- catch {
104
- // corrupt config → ignore, treat as missing
105
- }
114
+ async function deployViewerPages(repo) {
115
+ const dir = viewerDir();
116
+ const files = listFilesRecursive(dir);
117
+ const blob = async (content) => (await (await fetch(`${GH_API}/repos/${repo}/git/blobs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ content, encoding: 'base64' }) })).json()).sha;
118
+ const tree = [];
119
+ const pool = 8;
120
+ let i = 0;
121
+ await Promise.all(Array.from({ length: Math.min(pool, files.length) }, async () => {
122
+ while (i < files.length) {
123
+ const rel = files[i++];
124
+ tree.push({ path: rel.split(path.sep).join('/'), mode: '100644', type: 'blob', sha: await blob(fs.readFileSync(path.join(dir, rel)).toString('base64')) });
106
125
  }
107
- if (dir === root)
108
- return null;
109
- dir = path.dirname(dir);
126
+ }));
127
+ tree.push({ path: '.nojekyll', mode: '100644', type: 'blob', sha: await blob('') });
128
+ let baseSha = null;
129
+ let baseTree = null;
130
+ try {
131
+ const ref = await ghGet(`/repos/${repo}/git/ref/heads/${PAGES_BRANCH}`);
132
+ baseSha = ref.object.sha;
133
+ baseTree = (await ghGet(`/repos/${repo}/git/commits/${baseSha}`)).tree.sha;
134
+ }
135
+ catch { /* first deploy — root commit */ }
136
+ const newTree = await (await fetch(`${GH_API}/repos/${repo}/git/trees`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ...(baseTree ? { base_tree: baseTree } : {}), tree }) })).json();
137
+ const commit = await (await fetch(`${GH_API}/repos/${repo}/git/commits`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ message: 'excalidrop: publish viewer', tree: newTree.sha, ...(baseSha ? { parents: [baseSha] } : { parents: [] }) }) })).json();
138
+ if (baseSha)
139
+ await fetch(`${GH_API}/repos/${repo}/git/refs/heads/${PAGES_BRANCH}`, { method: 'PATCH', headers: ghHeaders(), body: JSON.stringify({ sha: commit.sha, force: true }) });
140
+ else
141
+ await fetch(`${GH_API}/repos/${repo}/git/refs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ref: `refs/heads/${PAGES_BRANCH}`, sha: commit.sha }) });
142
+ // API commits don't reliably queue builds — request one explicitly.
143
+ try {
144
+ const cur = await fetch(`${GH_API}/repos/${repo}/pages`, { headers: ghHeaders() });
145
+ if (!cur.ok || (await cur.json())?.source?.branch !== PAGES_BRANCH) {
146
+ const up = await fetch(`${GH_API}/repos/${repo}/pages`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify({ source: { branch: PAGES_BRANCH, path: '/' } }) });
147
+ if (!up.ok)
148
+ await fetch(`${GH_API}/repos/${repo}/pages`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ source: { branch: PAGES_BRANCH, path: '/' } }) });
149
+ }
150
+ await fetch(`${GH_API}/repos/${repo}/pages/builds`, { method: 'POST', headers: ghHeaders() });
151
+ }
152
+ catch { /* Pages setup is best-effort */ }
153
+ }
154
+ function projectNameForRepo(repo) {
155
+ return `excalidrop-${repo.replace('/', '-').toLowerCase().replace(/[^a-z0-9-]/g, '-')}`.slice(0, 60);
156
+ }
157
+ function viewerUrlFor(repo, target) {
158
+ if (target === 'cloudflare') {
159
+ // Generic viewer build can't infer the repo from a pages.dev host —
160
+ // detectRepo() reads it from ?repo= instead.
161
+ return `https://${projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
110
162
  }
163
+ const [owner, name] = repo.split('/');
164
+ return `https://${owner}.github.io/${name}/`;
111
165
  }
112
- function portFromExpressUrl(url) {
113
- if (!url)
114
- return undefined;
166
+ async function isPrivateRepo(repo) {
115
167
  try {
116
- const port = Number(new URL(url).port);
117
- return Number.isFinite(port) && port > 0 ? port : undefined;
168
+ const r = spawnSync('gh', ['api', `repos/${repo}`, '--jq', '.private'], { encoding: 'utf8' });
169
+ if (r.status !== 0)
170
+ return null;
171
+ if (r.stdout.trim() === 'true')
172
+ return true;
173
+ if (r.stdout.trim() === 'false')
174
+ return false;
175
+ return null;
118
176
  }
119
177
  catch {
120
- return undefined;
121
- }
122
- }
123
- /** Resolve this project's canvas port without side effects (used by `mcp`). */
124
- function resolveSavedPort(cwd) {
125
- const envPort = Number(process.env.EXCALIDROP_PORT);
126
- if (Number.isFinite(envPort) && envPort > 0)
127
- return envPort;
128
- const envUrlPort = portFromExpressUrl(process.env.EXPRESS_SERVER_URL);
129
- if (envUrlPort)
130
- return envUrlPort;
131
- const found = findDropConfig(cwd);
132
- if (found)
133
- return found.config.port;
134
- return DEFAULT_START_PORT;
135
- }
136
- // ─── agent config snippets (chrome-devtools-mcp style) ───────────────────────
137
- function mcpServerJson() {
138
- // No hardcoded port: `excalidrop mcp` resolves `.excalidrop.json` at runtime,
139
- // so the same snippet works for every project, concurrently.
140
- return {
141
- mcpServers: {
142
- excalidrop: {
143
- command: 'npx',
144
- args: ['excalidrop', 'mcp'],
145
- },
146
- },
147
- };
178
+ return null;
179
+ }
148
180
  }
149
- function agentCommands() {
150
- return [
151
- {
152
- name: 'Claude Code (project scope)',
153
- command: 'claude mcp add excalidrop --scope project -- npx excalidrop mcp',
154
- },
155
- {
156
- name: 'Claude Code (user scope)',
157
- command: 'claude mcp add excalidraw --scope user -- npx excalidrop mcp',
158
- },
159
- {
160
- name: 'Codex CLI',
161
- command: 'codex mcp add excalidrop -- npx excalidrop mcp',
162
- },
163
- {
164
- name: 'Gemini CLI',
165
- command: 'gemini mcp add excalidrop npx excalidrop mcp',
166
- },
167
- {
168
- name: 'Cursor (.cursor/mcp.json) / Claude Desktop / Antigravity',
169
- command: JSON.stringify(mcpServerJson(), null, 2),
170
- },
171
- {
172
- name: 'OpenCode (opencode.json)',
173
- command: JSON.stringify({
174
- $schema: 'https://opencode.ai/config.json',
175
- mcp: {
176
- excalidrop: {
177
- type: 'local',
178
- command: ['npx', 'excalidrop', 'mcp'],
179
- enabled: true,
180
- },
181
- },
182
- }, null, 2),
183
- },
184
- ];
185
- }
186
- function prompt(question) {
187
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
188
- return new Promise((resolve) => {
189
- rl.question(question, (answer) => {
190
- rl.close();
191
- resolve(answer.trim());
192
- });
193
- });
181
+ async function resolveTarget(repo, explicit) {
182
+ if (explicit === 'pages' || explicit === 'cloudflare')
183
+ return explicit;
184
+ return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
194
185
  }
195
- function commandExists(cmd) {
196
- const probe = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', cmd], {
197
- stdio: 'ignore',
198
- shell: process.platform === 'win32',
199
- });
200
- return probe.status === 0;
186
+ function deployToCloudflarePages(project) {
187
+ // wrangler 4 doesn't auto-create Pages projects and its Workers delegation
188
+ // misfires on explicit asset dirs — create once, then deploy classic with --force.
189
+ const WRANGLER = ['-y', 'wrangler@4'];
190
+ const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
191
+ const createOut = (create.stdout || '') + (create.stderr || '');
192
+ if (create.status !== 0 && !/already exists/i.test(createOut)) {
193
+ throw new Error(`wrangler pages project create failed:\n${createOut.slice(-800)}\nRun \`wrangler login\` first (or set CLOUDFLARE_API_TOKEN).`);
194
+ }
195
+ const r = spawnSync('npx', [...WRANGLER, 'pages', 'deploy', viewerDir(), '--project-name', project, '--force'], { stdio: 'inherit', env: process.env });
196
+ if (r.status !== 0)
197
+ throw new Error('wrangler pages deploy failed. Run `wrangler login` first (or set CLOUDFLARE_API_TOKEN).');
201
198
  }
202
- // ─── commands ────────────────────────────────────────────────────────────────
203
199
  function detectSlug() {
204
200
  try {
205
201
  const url = spawnSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).stdout.trim().replace(/\.git$/, '');
206
- return url.match(/github\.com[:/](.+)/)?.[1] || '';
202
+ return url.match(/github\.com[:/](.+)/)?.[1]?.toLowerCase() || '';
207
203
  }
208
204
  catch {
209
205
  return '';
@@ -212,30 +208,127 @@ function detectSlug() {
212
208
  function ghAuthed() {
213
209
  return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
214
210
  }
215
- async function cmdInit(args) {
216
- const portFlag = args.find((a) => a.startsWith('--port='));
217
- const preferred = portFlag ? Number(portFlag.split('=')[1]) : undefined;
218
- const noPrompt = args.includes('--no-prompt') || args.includes('-y');
211
+ function commandExists(cmd) {
212
+ const probe = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', cmd], { stdio: 'ignore', shell: process.platform === 'win32' });
213
+ return probe.status === 0;
214
+ }
215
+ 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;
225
+ }
226
+ }
227
+ function mcpArgs(repo) {
228
+ return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
229
+ }
230
+ const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'desktop', 'opencode'];
231
+ function editorCommand(id, repo) {
232
+ const a = mcpArgs(repo).join(' ');
233
+ switch (id) {
234
+ case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
235
+ case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
236
+ case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
237
+ case 'cursor': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
238
+ 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);
240
+ }
241
+ }
242
+ function installEditor(id, repo) {
243
+ try {
244
+ if (id === 'claude-project' && commandExists('claude')) {
245
+ return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
246
+ }
247
+ if (id === 'claude-user' && commandExists('claude')) {
248
+ return spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
249
+ }
250
+ if (id === 'codex' && commandExists('codex')) {
251
+ return spawnSync('codex', ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)], { stdio: 'ignore' }).status === 0;
252
+ }
253
+ }
254
+ catch { /* fall through to manual */ }
255
+ return false;
256
+ }
257
+ const EDITOR_LABELS = {
258
+ 'claude-project': 'Claude Code (project scope)',
259
+ 'claude-user': 'Claude Code (user scope)',
260
+ codex: 'Codex CLI',
261
+ cursor: 'Cursor (.cursor/mcp.json)',
262
+ desktop: 'Claude Desktop',
263
+ opencode: 'OpenCode (opencode.json)',
264
+ };
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`);
269
+ }
270
+ function parseFlag(args, name) {
271
+ const eq = args.find((a) => a.startsWith(`--${name}=`));
272
+ if (eq)
273
+ return eq.split('=').slice(1).join('=');
274
+ const i = args.indexOf(`--${name}`);
275
+ if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('-'))
276
+ return args[i + 1];
277
+ return undefined;
278
+ }
279
+ function parseEditorsFlag(args) {
280
+ const raw = parseFlag(args, 'editor');
281
+ if (!raw)
282
+ return null;
283
+ const ids = raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
284
+ const valid = ids.filter((s) => EDITOR_IDS.includes(s));
285
+ if (!valid.length) {
286
+ console.error(`Unknown --editor value. Use comma-separated: ${EDITOR_IDS.join(',')}`);
287
+ process.exit(1);
288
+ }
289
+ return valid;
290
+ }
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]');
296
+ process.exit(1);
297
+ }
298
+ if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
299
+ console.error('Not logged in to GitHub. Run `gh auth login` or `npx excalidrop login` first.');
300
+ process.exit(1);
301
+ }
302
+ try {
303
+ await ghGet(`/repos/${slug}`);
304
+ }
305
+ catch (e) {
306
+ if (String(e.message).includes('→ 404')) {
307
+ console.error(`Repo "${slug}" not found (404). Check the slug for typos, or create it:\n\n gh repo create ${slug} --private # or --public\n`);
308
+ process.exit(1);
309
+ }
310
+ throw e;
311
+ }
312
+ const explicitTarget = parseFlag(args, 'target');
313
+ const target = await resolveTarget(slug, explicitTarget);
314
+ console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? ' (explicit)' : ' (auto: private→cloudflare, public→pages)'}\n`);
315
+ await ensureMainBranch(slug);
316
+ const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
317
+ if (!scene.sha) {
318
+ await putScene(slug, [], null, 'excalidrop: init canvas');
319
+ console.log('Empty canvas created on branch excalidrop.');
320
+ }
321
+ let viewerUrl;
322
+ if (target === 'pages') {
323
+ await deployViewerPages(slug);
324
+ viewerUrl = viewerUrlFor(slug, 'pages');
325
+ }
326
+ else {
327
+ deployToCloudflarePages(projectNameForRepo(slug));
328
+ viewerUrl = viewerUrlFor(slug, 'cloudflare');
329
+ }
219
330
  const root = findProjectRoot(process.cwd());
220
- const slug = detectSlug();
221
- const port = await findFreePort(preferred);
222
- // Preselect this repo as the remote canvas: the MCP auto-loads it, so the
223
- // agent never needs switch_remote for its home project (chrome-devtools-mcp
224
- // wrapper pattern: in-memory default + persisted startup target — ours
225
- // persists in .excalidrop.json so it survives restarts too).
226
- const config = { port, canvasUrl: `http://127.0.0.1:${port}`, ...(slug ? { remote: slug } : {}) };
227
- fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify(config, null, 2) + '\n');
228
- console.log(`\nexcalidrop: canvas port ${port} saved to ${path.join(root, CONFIG_NAME)}`);
229
- if (slug)
230
- console.log(`excalidrop: remote canvas preselected: ${slug} (agent uses it automatically)`);
231
- // Seed canvas.excalidraw so `npm i -D excalidrop` leaves a visible file
232
- // that export_scene/import_scene round-trip through.
233
- const scenePath = path.join(root, 'canvas.excalidraw');
234
- if (!fs.existsSync(scenePath)) {
235
- fs.writeFileSync(scenePath, JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements: [], appState: { viewBackgroundColor: '#ffffff', gridSize: null }, files: {} }, null, 2) + '\n');
236
- console.log(`excalidrop: empty scene written to ${scenePath}`);
237
- }
238
- // Merge into .mcp.json (generic MCP clients + Claude Code project scope)
331
+ fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
239
332
  const mcpPath = path.join(root, MCP_JSON);
240
333
  let mcp = {};
241
334
  if (fs.existsSync(mcpPath)) {
@@ -243,291 +336,247 @@ async function cmdInit(args) {
243
336
  mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
244
337
  }
245
338
  catch {
246
- console.warn(`excalidrop: existing ${MCP_JSON} is not valid JSON, leaving it untouched.`);
339
+ console.warn(`Existing ${MCP_JSON} is not valid JSON, leaving it untouched.`);
247
340
  }
248
341
  }
249
342
  if (!mcp.mcpServers || typeof mcp.mcpServers !== 'object')
250
343
  mcp.mcpServers = {};
251
- mcp.mcpServers.excalidrop = { command: 'npx', args: ['excalidrop', 'mcp'] };
252
- if (Object.keys(mcp).length > 0) {
253
- fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n');
254
- console.log(`excalidrop: MCP server entry written to ${mcpPath}`);
255
- }
256
- console.log('\nStart the canvas for this project with:');
257
- console.log(` npx excalidrop up # ${config.canvasUrl}\n`);
344
+ mcp.mcpServers.excalidrop = { command: 'npx', args: mcpArgs(slug), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
345
+ fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n');
346
+ console.log(`MCP entry written to ${mcpPath}`);
347
+ const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
348
+ const noPrompt = args.includes('--no-prompt') || args.includes('-y');
258
349
  if (noPrompt) {
259
- printAgentCommands();
260
- return;
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 };
261
359
  }
262
- // Guided remote setup: gh auth → publish empty canvas → agent install → login
263
- if (slug) {
264
- if (!ghAuthed()) {
265
- console.log('GitHub: not logged in. Run `gh auth login` first (2FA via GitHub), then re-run init.\n');
266
- }
267
- else {
268
- const pub = await prompt(`Publish an empty canvas to ${slug} (creates gh-pages + enables Pages)? [Y/n] `);
269
- if (!pub || /^(y|yes)$/i.test(pub)) {
270
- const r = spawnSync('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], {
271
- stdio: 'inherit', env: { ...process.env, REPO_SLUG: slug },
272
- });
273
- if (r.status === 0) {
274
- const [owner, repo] = slug.split('/');
275
- console.log(`\nCanvas live at https://${owner}.github.io/${repo}/`);
276
- const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
277
- console.log(`Install the app once per repo: https://github.com/apps/${appSlug}/installations/new`);
278
- }
279
- }
360
+ return { repo: slug, viewerUrl };
361
+ }
362
+ function cmdMcp() {
363
+ const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
364
+ child.on('exit', (code) => process.exit(code ?? 0));
365
+ }
366
+ async function cmdLogin() {
367
+ const clientId = process.env.GITHUB_OAUTH_CLIENT_ID || 'Iv23liuS2fx3QOEIoDmx';
368
+ console.log('Starting GitHub device login…');
369
+ const dev = await (await fetch('https://github.com/login/device/code', {
370
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
371
+ body: JSON.stringify({ client_id: clientId }),
372
+ })).json();
373
+ console.log(`\nOpen ${dev.verification_uri} and enter code: ${dev.user_code}\n`);
374
+ const deadline = Date.now() + 300_000;
375
+ while (Date.now() < deadline) {
376
+ await new Promise((r) => setTimeout(r, (dev.interval || 5) * 1000));
377
+ const j = await (await fetch('https://github.com/login/oauth/access_token', {
378
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ client_id: clientId, device_code: dev.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
380
+ })).json();
381
+ if (j.access_token) {
382
+ const dir = path.join(os.homedir(), '.config', 'excalidrop');
383
+ fs.mkdirSync(dir, { recursive: true });
384
+ fs.writeFileSync(path.join(dir, 'gh_token'), j.access_token + '\n', { mode: 0o600 });
385
+ console.log('Logged in — token stored in ~/.config/excalidrop/gh_token');
386
+ return;
280
387
  }
388
+ if (j.error && j.error !== 'authorization_pending' && j.error !== 'slow_down')
389
+ throw new Error(j.error_description || j.error);
281
390
  }
282
- const answer = await prompt('Install the excalidrop MCP server into your AI agent now? [Y/n] ');
283
- if (answer && !/^(y|yes)$/i.test(answer)) {
284
- printAgentCommands();
391
+ throw new Error('device login timed out');
392
+ }
393
+ async function cmdStatus() {
394
+ const flag = process.argv.find((a) => a.startsWith('--repo='));
395
+ const repo = flag ? flag.split('=').slice(1).join('=') : detectSlug();
396
+ console.log(`repo: ${repo || '(none — run setup or pass --repo=owner/repo)'}`);
397
+ console.log(`auth: ${ghToken() ? 'ok (env/gh CLI/stored)' : 'missing — run `gh auth login` or `npx excalidrop login`'}`);
398
+ console.log(`relay: ${process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT}`);
399
+ if (!repo || !ghToken())
285
400
  return;
401
+ try {
402
+ const scene = await getScene(repo);
403
+ console.log(`scene: ${scene.elements.length} elements @${CANVAS_BRANCH}`);
286
404
  }
287
- // Try the CLIs that exist; fall back to printing snippets.
288
- let installed = false;
289
- if (commandExists('claude')) {
290
- const scopeAnswer = await prompt('Claude Code scope? [project/user] (default: project) ');
291
- const scope = /user/i.test(scopeAnswer) ? 'user' : 'project';
292
- const r = spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', scope, '--', 'npx', 'excalidrop', 'mcp'], {
293
- stdio: 'inherit',
294
- });
295
- installed = r.status === 0;
296
- }
297
- if (!installed && commandExists('codex')) {
298
- const useIt = await prompt('Run `codex mcp add excalidrop`? [Y/n] ');
299
- if (!useIt || /^(y|yes)$/i.test(useIt)) {
300
- const r = spawnSync('codex', ['mcp', 'add', 'excalidrop', '--', 'npx', 'excalidrop', 'mcp'], {
301
- stdio: 'inherit',
302
- });
303
- installed = r.status === 0;
304
- }
405
+ catch (e) {
406
+ console.log(`scene: unreachable (${e.message})`);
305
407
  }
306
- if (!installed)
307
- printAgentCommands();
308
408
  }
309
- function printAgentCommands() {
310
- console.log('Add excalidrop to your AI agent (same pattern as chrome-devtools-mcp):\n');
311
- for (const c of agentCommands()) {
312
- console.log(`## ${c.name}\n${c.command}\n`);
409
+ async function runTui() {
410
+ let clack;
411
+ try {
412
+ clack = await import('@clack/prompts');
313
413
  }
314
- }
315
- async function cmdUp(args) {
316
- const portFlag = args.find((a) => a.startsWith('--port='));
317
- const preferred = portFlag ? Number(portFlag.split('=')[1]) : undefined;
318
- const root = findProjectRoot(process.cwd());
319
- const existing = findDropConfig(process.cwd());
320
- // Reuse the saved port if it's still free (stable URL per project),
321
- // otherwise claim a new one so concurrent projects never collide.
322
- let port;
323
- if (preferred) {
324
- port = await findFreePort(preferred);
414
+ catch {
415
+ console.error('TUI needs @clack/prompts. Run instead: npx excalidrop setup owner/repo');
416
+ process.exit(1);
325
417
  }
326
- else if (existing && (await isPortFree(existing.config.port))) {
327
- port = existing.config.port;
418
+ clack.intro('excalidrop — remote canvas setup');
419
+ const detected = detectSlug();
420
+ const repoAnswer = await clack.text({
421
+ message: 'Which repo holds the canvas?',
422
+ placeholder: detected || 'owner/repo',
423
+ initialValue: detected || '',
424
+ validate: (v) => (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(v.trim()) ? undefined : 'Use owner/repo'),
425
+ });
426
+ if (clack.isCancel(repoAnswer)) {
427
+ clack.cancel('Aborted.');
428
+ process.exit(0);
328
429
  }
329
- else {
330
- port = await findFreePort(existing?.config.port ?? DEFAULT_START_PORT);
331
- }
332
- fs.writeFileSync(path.join(existing?.dir ?? root, CONFIG_NAME), JSON.stringify({ port, canvasUrl: `http://127.0.0.1:${port}` }, null, 2) + '\n');
333
- const host = process.env.HOST || '127.0.0.1';
334
- console.log(`excalidrop: starting canvas on http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`);
335
- const child = spawn(process.execPath, [SERVER_ENTRY], {
336
- stdio: 'inherit',
337
- env: { ...process.env, PORT: String(port), HOST: host },
430
+ const slug = repoAnswer.trim().toLowerCase();
431
+ const priv = await isPrivateRepo(slug);
432
+ if (priv !== null)
433
+ clack.log.info(`Repo is ${priv ? 'private' : 'public'} — recommended host: ${priv ? 'Cloudflare Pages' : 'GitHub Pages'}.`);
434
+ if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
435
+ const how = await clack.select({
436
+ message: 'GitHub auth missing. How to log in?',
437
+ options: [
438
+ { value: 'retry', label: 'I ran `gh auth login` (re-check)' },
439
+ { value: 'device', label: 'Device flow (code + browser, 2FA ok)' },
440
+ { value: 'skip', label: 'Skip (I set GITHUB_TOKEN later)' },
441
+ ],
442
+ });
443
+ if (clack.isCancel(how)) {
444
+ clack.cancel('Aborted.');
445
+ process.exit(0);
446
+ }
447
+ if (how === 'device') {
448
+ const s = clack.spinner();
449
+ s.start('Waiting for browser approval…');
450
+ try {
451
+ await cmdLogin();
452
+ s.stop('Logged in.');
453
+ }
454
+ catch (e) {
455
+ s.stop('Login failed: ' + e.message);
456
+ process.exit(1);
457
+ }
458
+ }
459
+ else if (how === 'retry' && !ghAuthed() && !process.env.GITHUB_TOKEN) {
460
+ clack.log.error('Still not logged in. Run `gh auth login` first.');
461
+ process.exit(1);
462
+ }
463
+ }
464
+ const auto = await resolveTarget(slug);
465
+ const hostAnswer = await clack.select({
466
+ message: `Where to host the viewer? (auto: ${auto})`,
467
+ 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' },
471
+ ],
338
472
  });
339
- child.on('exit', (code) => process.exit(code ?? 0));
340
- }
341
- function cmdMcp() {
342
- // What the agent runs over stdio. Port resolves per-project at runtime.
343
- const port = resolveSavedPort(process.cwd());
344
- const canvasUrl = process.env.EXPRESS_SERVER_URL || `http://127.0.0.1:${port}`;
345
- const child = spawn(process.execPath, [MCP_ENTRY], {
346
- stdio: 'inherit',
347
- env: { ...process.env, EXPRESS_SERVER_URL: canvasUrl },
473
+ if (clack.isCancel(hostAnswer)) {
474
+ clack.cancel('Aborted.');
475
+ process.exit(0);
476
+ }
477
+ const target = (hostAnswer === 'auto' ? auto : hostAnswer);
478
+ const editorAnswer = await clack.multiselect({
479
+ 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'],
482
+ required: false,
348
483
  });
349
- child.on('exit', (code) => process.exit(code ?? 0));
350
- }
351
- async function cmdStatus() {
352
- const found = findDropConfig(process.cwd());
353
- const port = found?.config.port ?? resolveSavedPort(process.cwd());
354
- const url = process.env.EXPRESS_SERVER_URL || `http://127.0.0.1:${port}`;
355
- console.log(`config: ${found ? path.join(found.dir, CONFIG_NAME) : '(none — using defaults)'}`);
356
- console.log(`canvas: ${url}`);
484
+ if (clack.isCancel(editorAnswer)) {
485
+ clack.cancel('Aborted.');
486
+ process.exit(0);
487
+ }
488
+ const editors = editorAnswer;
489
+ const s = clack.spinner();
490
+ s.start('Publishing viewer + wiring MCP…');
491
+ let viewerUrl;
357
492
  try {
358
- const res = await fetch(`${url}/health`);
359
- console.log(`health: ${res.ok ? 'ok' : `HTTP ${res.status}`} ${res.ok ? '' : await res.text()}`);
360
- if (res.ok)
361
- console.log(await res.text());
362
- }
363
- catch (err) {
364
- console.log(`health: unreachable (${err.message}) — start it with \`npx excalidrop up\``);
365
- }
366
- }
367
- async function cmdPages(_args) {
368
- console.log(`excalidrop pages (GitHub as source of truth, no Actions, no setup)\n`);
369
- console.log('1. Publish viewer: npx excalidrop publish');
370
- console.log(' (uses `gh auth` — repo auto-detected, Pages auto-enabled.)\n');
371
- console.log('2. In your agent: switch_remote { target: "<owner>.github.io/<repo>" }');
372
- console.log(' Draw as usual — commits land on GitHub, viewer updates on gh-pages.\n');
373
- console.log('3. Login: ask the agent to run github_login (device flow, 2FA via GitHub),');
374
- console.log(' or paste a token (`gh auth token`) on the viewer login pill.\n');
375
- console.log('Rule: never edit the canvas locally — the Pages site + GitHub repo are the canvas.');
376
- }
377
- function cmdRemote() {
378
- const child = spawn(process.execPath, [path.join(__dirname, 'remote.js')], { stdio: 'inherit', env: process.env });
379
- child.on('exit', (code) => process.exit(code ?? 0));
380
- }
381
- /** One-command per-user setup on ANY repo: gh auth → publish viewer → app install link. */
382
- async function cmdSetup(args = []) {
383
- if (args.includes('--help') || args.includes('-h')) {
384
- console.log('Usage: npx excalidrop setup\n\nChecks `gh auth`, publishes the viewer to this repo\'s gh-pages\n(enabling Pages if needed), then prints the app-install link and next steps.');
385
- return;
493
+ ({ viewerUrl } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors));
494
+ s.stop('Setup complete.');
386
495
  }
387
- console.log('excalidrop setup — works on any repo you own or can access\n');
388
- // 1. gh auth (the only prerequisite; 2FA enforced by GitHub itself)
389
- const auth = spawnSync('gh', ['auth', 'status'], { stdio: 'pipe', encoding: 'utf8' });
390
- if (auth.status !== 0) {
391
- console.error('Not logged in to GitHub. Run this first:\n\n gh auth login\n');
496
+ catch (e) {
497
+ s.stop('Setup failed: ' + e.message);
392
498
  process.exit(1);
393
499
  }
394
- const slug = (() => {
395
- try {
396
- const url = spawnSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).stdout.trim().replace(/\.git$/, '');
397
- const m = url.match(/github\.com[:/](.+)/);
398
- return m?.[1] || '';
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).`);
399
503
  }
400
- catch {
401
- return '';
504
+ else {
505
+ clack.log.info(`${EDITOR_LABELS[id]} config:\n${editorCommand(id, slug)}`);
402
506
  }
403
- })();
404
- if (!slug) {
405
- console.error('No GitHub remote detected. Run setup inside a cloned repo.\n');
406
- process.exit(1);
407
507
  }
408
- console.log(`Repo: ${slug} (detected from git remote)\n`);
409
- // 2. publish viewer (auto-enables Pages, auto-syncs scene)
410
- await new Promise((resolve, reject) => {
411
- const child = spawn('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], { stdio: 'inherit', env: { ...process.env, REPO_SLUG: slug } });
412
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`publish failed (exit ${code})`))));
413
- });
414
- // 3. remember the target: agent auto-loads it, no switch_remote needed
415
508
  try {
416
- const cfgFile = path.join(process.cwd(), CONFIG_NAME);
417
- const cfg = fs.existsSync(cfgFile) ? JSON.parse(fs.readFileSync(cfgFile, 'utf8')) : {};
418
- fs.writeFileSync(cfgFile, JSON.stringify({ port: cfg.port || 3030, canvasUrl: cfg.canvasUrl || 'http://127.0.0.1:3030', ...cfg, remote: slug }, null, 2) + '\n');
419
- console.log(`Remote target remembered in ${CONFIG_NAME} (agent uses it automatically).`);
420
- }
421
- catch { /* non-fatal */ }
422
- // 4. verify: Pages serves the viewer AND the scene file
423
- const [sOwner, sRepo] = slug.split('/');
424
- let verified = false;
425
- for (let i = 0; i < 12; i++) {
426
- await new Promise((r) => setTimeout(r, 10000));
427
- try {
428
- const res = await fetch(`https://${sOwner}.github.io/${sRepo}/canvas.excalidraw`, { signal: AbortSignal.timeout(15000) });
429
- if (res.ok) {
430
- verified = true;
431
- break;
432
- }
433
- }
434
- catch { /* Pages build still running */ }
435
- }
436
- console.log(verified ? 'Verified: viewer + scene live.' : 'Note: Pages still building — check back in a minute.');
437
- // 4. next steps: install the shared app + draw
438
- const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
439
- console.log(`\nDone. Your canvas: https://${sOwner}.github.io/${sRepo}/\n`);
440
- console.log('Two remaining clicks (one time per repo):');
441
- console.log(` 1. Install the Excalidrop app on this repo:\n https://github.com/apps/${appSlug}/installations/new\n`);
442
- console.log(' 2. In your agent: switch_remote { target: "<that canvas URL>" }');
443
- console.log(' then draw — commits land on GitHub, the viewer updates itself.');
444
- console.log('\nLogin on the page: pill → device code → github.com/login/device (2FA via GitHub).');
445
- }
446
- /** Terminal GitHub login via device flow (same flow the agent's github_login uses). */
447
- async function cmdLogin() {
448
- const { APP_CLIENT_ID } = await import('./target.js');
449
- const clientId = process.env.GITHUB_OAUTH_CLIENT_ID || APP_CLIENT_ID;
450
- if (!clientId) {
451
- console.error('No GitHub App Client ID configured yet.');
452
- console.error('Set GITHUB_OAUTH_CLIENT_ID (or wait for the shared excalidrop app),');
453
- console.error('or just run `gh auth login` — the MCP picks that token up automatically.');
454
- process.exit(1);
509
+ const scene = await getScene(slug);
510
+ clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
455
511
  }
456
- // Dynamic import to avoid loading MCP deps in CLI path
457
- const mod = await import('./target.js');
458
- console.log('Starting GitHub device login…');
459
- const dev = await mod.deviceStart(clientId);
460
- console.log(`\nOpen ${dev.verification_uri} and enter code: ${dev.user_code}\n`);
461
- const repoId = undefined; // unrestricted; pass --repo owner/repo? (future: parse argv)
462
- const token = await mod.devicePoll(clientId, dev.device_code, dev.interval || 5, 300, repoId);
463
- console.log('Logged in — token stored in ~/.config/excalidrop/gh_token');
464
- void token;
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.');
524
+ }
525
+ clack.outro(`Canvas live: ${viewerUrl}`);
465
526
  }
466
527
  function printHelp() {
467
- console.log(`excalidrop — drop an Excalidraw canvas + MCP server into any project
528
+ console.log(`excalidrop — remote GitHub-backed canvas (no local server)
468
529
 
469
530
  Usage:
470
- npx excalidrop init [--port=N] [--no-prompt] pick a free port, write .excalidrop.json + .mcp.json
471
- npx excalidrop up [--port=N] start this project's canvas server
472
- npx excalidrop setup one-command setup on any repo (gh auth + publish + guide)
473
- npx excalidrop login GitHub device-flow login from the terminal
474
- npx excalidrop pages GitHub-truth + gh-pages publish guide
475
- npx excalidrop remote run remote MCP-v2 (GitHub-backed, multi-project)
476
- npx excalidrop publish publish viewer straight to gh-pages (no Actions)
477
- npx excalidrop mcp run MCP stdio server (used by AI agents)
478
- npx excalidrop status show port + canvas health
479
- npx excalidrop add print AI-agent install commands
531
+ npx excalidrop interactive setup TUI
532
+ npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex,cursor,desktop,opencode] [--no-prompt]
533
+ npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
534
+ npx excalidrop login [--repo owner/repo] GitHub device-flow login
535
+ npx excalidrop publish owner/repo [--target ..] redeploy viewer only
536
+ npx excalidrop status show repo + auth + scene
537
+ npx excalidrop add [owner/repo] print editor install commands
480
538
 
481
- Install:
482
- npm i -D excalidrop && npx excalidrop init
483
-
484
- Each project gets its own port (scanned free from ${DEFAULT_START_PORT}), so several
485
- checkouts can run concurrently. Agent config needs no hardcoded port.`);
539
+ No devDependency needed. Token: GITHUB_TOKEN → gh auth → ~/.config/excalidrop/gh_token.`);
486
540
  }
487
- // ─── entry ───────────────────────────────────────────────────────────────────
488
541
  async function main() {
489
542
  const [cmd, ...rest] = process.argv.slice(2);
543
+ if (!cmd) {
544
+ if (!process.stdout.isTTY) {
545
+ printHelp();
546
+ return;
547
+ }
548
+ await runTui();
549
+ return;
550
+ }
490
551
  switch (cmd) {
491
- case 'init':
492
- await cmdInit(rest);
493
- break;
494
- case 'up':
495
- await cmdUp(rest);
552
+ case 'setup':
553
+ await cmdSetup(rest);
496
554
  break;
497
555
  case 'mcp':
498
556
  cmdMcp();
499
557
  break;
500
- case 'status':
501
- await cmdStatus();
502
- break;
503
- case 'pages':
504
- await cmdPages(rest);
505
- break;
506
- case 'remote':
507
- cmdRemote();
508
- break;
509
- case 'setup':
510
- await cmdSetup(rest);
511
- break;
512
558
  case 'login':
513
559
  await cmdLogin();
514
560
  break;
515
- case 'publish': {
516
- const child = spawn('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], { stdio: 'inherit', env: process.env });
517
- child.on('exit', (code) => process.exit(code ?? 0));
561
+ case 'status':
562
+ await cmdStatus();
563
+ break;
564
+ case 'publish':
565
+ await cmdSetup(['--no-prompt', ...rest]);
518
566
  break;
519
- }
520
567
  case 'add':
521
- printAgentCommands();
568
+ printEditorCommands(parseEditorsFlag(rest) || [...EDITOR_IDS], rest.find((a) => a.includes('/')) || detectSlug() || 'owner/repo');
569
+ break;
570
+ case 'init':
571
+ case 'up':
572
+ case 'remote':
573
+ console.error(`\`excalidrop ${cmd}\` was removed (no local canvas). Run: npx excalidrop setup owner/repo`);
574
+ process.exit(1);
522
575
  break;
523
576
  default:
524
577
  printHelp();
525
- if (cmd && cmd !== '--help' && cmd !== '-h')
526
- process.exitCode = 1;
578
+ process.exitCode = 1;
527
579
  }
528
580
  }
529
- main().catch((err) => {
530
- console.error(`excalidrop: ${err.message}`);
531
- process.exit(1);
532
- });
581
+ main().catch((err) => { console.error(`excalidrop: ${err.message}`); process.exit(1); });
533
582
  //# sourceMappingURL=cli.js.map