excalidrop 0.1.4 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +70 -125
  2. package/dist/cli.d.ts +7 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +936 -389
  5. package/dist/cli.js.map +1 -1
  6. package/dist/frontend/assets/{advancedFormat-tiBAHNGo.js → advancedFormat-Be4KR5mp.js} +1 -1
  7. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-DVxiWV5Z.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-Z_2BqDo-.js → az-AZ-76LH7QW2-01MDUsF0.js} +1 -1
  10. package/dist/frontend/assets/{bg-BG-XCXSNQG7-DrxGJZOG.js → bg-BG-XCXSNQG7--fArB8Ao.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-38ab4fdb-CDnkTFfP.js → blockDiagram-38ab4fdb-CmsVhOwi.js} +1 -1
  12. package/dist/frontend/assets/{blockDiagram-68f4deed-Bb0W6mxN.js → blockDiagram-68f4deed-DU_wNYlO.js} +6 -6
  13. package/dist/frontend/assets/{bn-BD-2XOGV67Q-BkGJ-w8k.js → bn-BD-2XOGV67Q-h8juqzfQ.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-15b5d702-ByRV0tka.js → c4Diagram-15b5d702-ClZalz0L.js} +1 -1
  15. package/dist/frontend/assets/{c4Diagram-3d4e48cf-QSVnzYYK.js → c4Diagram-3d4e48cf-BkFVM0tP.js} +1 -1
  16. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-BBJvOPE1.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-DDFBNGUt.js → classDiagram-70f12bd4-BXAJnakg.js} +1 -1
  19. package/dist/frontend/assets/{classDiagram-d40c83e7-cgC3s7Nz.js → classDiagram-d40c83e7-DVhipa45.js} +1 -1
  20. package/dist/frontend/assets/classDiagram-v2-d5a6b087-CiHMsbhj.js +2 -0
  21. package/dist/frontend/assets/{classDiagram-v2-f2320105-Dtywkpeg.js → classDiagram-v2-f2320105-1CqIxWOB.js} +2 -2
  22. package/dist/frontend/assets/clone-CMl85WSi.js +1 -0
  23. package/dist/frontend/assets/{createText-2e5e7dd3-CqY00v-4.js → createText-2e5e7dd3-D3kSz3YE.js} +1 -1
  24. package/dist/frontend/assets/{createText-d213de94-D59sBfqq.js → createText-d213de94-DvJeLlQX.js} +4 -4
  25. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-CK09MAsg.js → cs-CZ-2BRQDIVT-BUcdPrAr.js} +1 -1
  26. package/dist/frontend/assets/{cytoscape-cose-bilkent-CKy_tuGZ.js → cytoscape-cose-bilkent-CKoSm_2z.js} +1 -1
  27. package/dist/frontend/assets/{da-DK-5WZEPLOC-V4hdLKuT.js → da-DK-5WZEPLOC-C1nkchuC.js} +1 -1
  28. package/dist/frontend/assets/{de-DE-XR44H4JA-Dw8qkP9g.js → de-DE-XR44H4JA-DO2_bO1X.js} +1 -1
  29. package/dist/frontend/assets/{edges-332bd1c7-D-u0KMZB.js → edges-332bd1c7-C5qM1sDU.js} +1 -1
  30. package/dist/frontend/assets/{edges-e0da2a9e-B3UFqLqY.js → edges-e0da2a9e-BRt7qomS.js} +1 -1
  31. package/dist/frontend/assets/{el-GR-BZB4AONW-BUdBQbYg.js → el-GR-BZB4AONW-CrjSdaU2.js} +1 -1
  32. package/dist/frontend/assets/{elk.bundled-BZA3Xcoo.js → elk.bundled-Dmec6Rq6.js} +1 -1
  33. package/dist/frontend/assets/{erDiagram-880f2ed8-CNxm7e1x.js → erDiagram-880f2ed8-BI7kP523.js} +4 -4
  34. package/dist/frontend/assets/{erDiagram-9861fffd-C2u_ziAM.js → erDiagram-9861fffd-DMyCDrgI.js} +1 -1
  35. package/dist/frontend/assets/{es-ES-U4NZUMDT-DS0FAZqX.js → es-ES-U4NZUMDT-rrdjPi-M.js} +1 -1
  36. package/dist/frontend/assets/{eu-ES-A7QVB2H4-D-D5uidc.js → eu-ES-A7QVB2H4-uYn3S3D5.js} +1 -1
  37. package/dist/frontend/assets/{fa-IR-HGAKTJCU-mAW_2-jg.js → fa-IR-HGAKTJCU-DWW_BVFR.js} +1 -1
  38. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-B9mW6nxS.js → fi-FI-Z5N7JZ37-CMXxeZ4W.js} +1 -1
  39. package/dist/frontend/assets/{flowDb-7c981674-6JB2gd0I.js → flowDb-7c981674-Dsj6xMzr.js} +1 -1
  40. package/dist/frontend/assets/{flowDb-956e92f1-BbpkWRi7.js → flowDb-956e92f1-26g7kJju.js} +1 -1
  41. package/dist/frontend/assets/{flowDiagram-66a62f08-DrhrQuXc.js → flowDiagram-66a62f08-C9eQZtdf.js} +1 -1
  42. package/dist/frontend/assets/{flowDiagram-cbd28bf7-BmNY7bvJ.js → flowDiagram-cbd28bf7-D051xFrI.js} +3 -3
  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-4G1YiIlk.js → flowchart-elk-definition-36e2d292-C9fsvhV-.js} +2 -2
  46. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-Cm94SrrQ.js → flowchart-elk-definition-4a651766-BkVNuJOw.js} +1 -1
  47. package/dist/frontend/assets/{fr-FR-RHASNOE6-BXnmUxTA.js → fr-FR-RHASNOE6-DIfK_rjW.js} +1 -1
  48. package/dist/frontend/assets/{ganttDiagram-04f9e578-X0BweAnG.js → ganttDiagram-04f9e578-CZeF2a4Y.js} +1 -1
  49. package/dist/frontend/assets/{ganttDiagram-c361ad54-D3vxZ5MQ.js → ganttDiagram-c361ad54-DbkIZ-wU.js} +1 -1
  50. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-wznrpRWn.js → gitGraphDiagram-21fc4d3e-CLqlSEuE.js} +6 -6
  51. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-B9zCS3Pw.js → gitGraphDiagram-72cf32ee-Cvf9uYWB.js} +1 -1
  52. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CrUx-ab6.js → gl-ES-HMX3MZ6V-CCxCH2oK.js} +1 -1
  53. package/dist/frontend/assets/{graph-BQZ6SIVr.js → graph-MptL1QoF.js} +1 -1
  54. package/dist/frontend/assets/{graph-Cda8xSTx.js → graph-Mzuu2QFH.js} +1 -1
  55. package/dist/frontend/assets/{he-IL-6SHJWFNN-D33eaw6-.js → he-IL-6SHJWFNN-CJuJutJZ.js} +1 -1
  56. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DEnStF-g.js → hi-IN-IWLTKZ5I-BORPH6vU.js} +1 -1
  57. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-BjH2w8Sn.js → hu-HU-A5ZG7DT2-Dr7UDbjo.js} +1 -1
  58. package/dist/frontend/assets/{id-ID-SAP4L64H-Bc6R-JEz.js → id-ID-SAP4L64H-D0Widkq7.js} +1 -1
  59. package/dist/frontend/assets/{index-3862675e-CpYfS3Db.js → index-3862675e-DVT5-vN2.js} +1 -1
  60. package/dist/frontend/assets/{index-6079d271-Y2bgyRpa.js → index-6079d271-R4KRnW5Z.js} +1 -1
  61. package/dist/frontend/assets/{index-D7eYnAQp.js → index-B45XzZiR.js} +4 -4
  62. package/dist/frontend/assets/index-Bd-8869C.js +349 -0
  63. package/dist/frontend/assets/{infoDiagram-4a4f5b27-Bm3Oq-Rq.js → infoDiagram-4a4f5b27-BvY1qS17.js} +3 -3
  64. package/dist/frontend/assets/{infoDiagram-f8f76790-oNVitBKM.js → infoDiagram-f8f76790-D60YvG33.js} +1 -1
  65. package/dist/frontend/assets/{it-IT-JPQ66NNP-BcxQABF2.js → it-IT-JPQ66NNP-C08Vu0BG.js} +1 -1
  66. package/dist/frontend/assets/{ja-JP-DBVTYXUO-D1zIM-nt.js → ja-JP-DBVTYXUO-Ciu7LaRi.js} +1 -1
  67. package/dist/frontend/assets/{journeyDiagram-29694f62-CamPk_3P.js → journeyDiagram-29694f62-DvrnJXsd.js} +5 -5
  68. package/dist/frontend/assets/{journeyDiagram-49397b02-D6w3tLtf.js → journeyDiagram-49397b02-Dy9LU86E.js} +1 -1
  69. package/dist/frontend/assets/{kaa-6HZHGXH3-CoILysVg.js → kaa-6HZHGXH3-GhGTIjpu.js} +1 -1
  70. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-B9ZbybBt.js → kab-KAB-ZGHBKWFO-B3q4BqYw.js} +1 -1
  71. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-Dxi4GIQ7.js → kk-KZ-P5N5QNE5-GWoLZvco.js} +1 -1
  72. package/dist/frontend/assets/{km-KH-HSX4SM5Z-DaJ7EtO2.js → km-KH-HSX4SM5Z-CnAQJCYr.js} +1 -1
  73. package/dist/frontend/assets/{ko-KR-MTYHY66A-DG57Ah12.js → ko-KR-MTYHY66A-B2Rh9krp.js} +1 -1
  74. package/dist/frontend/assets/{ku-TR-6OUDTVRD-DnDf9E8a.js → ku-TR-6OUDTVRD-C27elmON.js} +1 -1
  75. package/dist/frontend/assets/{layout-Drr-66M9.js → layout-D1hl6oKi.js} +1 -1
  76. package/dist/frontend/assets/{layout-BsWfhUAF.js → layout-c-hOA8zE.js} +1 -1
  77. package/dist/frontend/assets/{line-DwQObOhP.js → line-DtMz1ssZ.js} +1 -1
  78. package/dist/frontend/assets/{linear-C36MjJps.js → linear-D-Y4WtO_.js} +1 -1
  79. package/dist/frontend/assets/{lt-LT-XHIRWOB4-D_zqS0yg.js → lt-LT-XHIRWOB4-B6jd693w.js} +1 -1
  80. package/dist/frontend/assets/{lv-LV-5QDEKY6T-owIsIh1b.js → lv-LV-5QDEKY6T-sXB-8i6R.js} +1 -1
  81. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-Cf9JbrBJ.js → mindmap-definition-ac74a2e8-CawO9I_f.js} +6 -6
  82. package/dist/frontend/assets/{mindmap-definition-fc14e90a-BpsNCNCp.js → mindmap-definition-fc14e90a-CXMNA68v.js} +4 -4
  83. package/dist/frontend/assets/{mr-IN-CRQNXWMA-CS9XUj74.js → mr-IN-CRQNXWMA-BJ6QSiPi.js} +1 -1
  84. package/dist/frontend/assets/{my-MM-5M5IBNSE-BKB060k9.js → my-MM-5M5IBNSE-BMdOIRP5.js} +1 -1
  85. package/dist/frontend/assets/{nb-NO-T6EIAALU-CkmLUrvm.js → nb-NO-T6EIAALU-BVG6JUMN.js} +1 -1
  86. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-DdiLyJoz.js → nl-NL-IS3SIHDZ-ZprCvTYR.js} +1 -1
  87. package/dist/frontend/assets/{nn-NO-6E72VCQL-Yevp6f_Y.js → nn-NO-6E72VCQL-CcljFAGy.js} +1 -1
  88. package/dist/frontend/assets/{oc-FR-POXYY2M6-D_M6GHup.js → oc-FR-POXYY2M6-zCuu6Ns2.js} +1 -1
  89. package/dist/frontend/assets/{pa-IN-N4M65BXN-1vuACN9n.js → pa-IN-N4M65BXN-D_iftL9v.js} +1 -1
  90. package/dist/frontend/assets/{pica-DDDx0iqx.js → pica-DsW4eP6T.js} +1 -1
  91. package/dist/frontend/assets/{pie-CFIpuQMO.js → pie-U_uHwWsL.js} +1 -1
  92. package/dist/frontend/assets/{pieDiagram-421022e6-DO7e_Qgw.js → pieDiagram-421022e6-naaI7HiE.js} +5 -5
  93. package/dist/frontend/assets/{pieDiagram-8a3498a8-BEoqChXo.js → pieDiagram-8a3498a8-m6nTf1O_.js} +1 -1
  94. package/dist/frontend/assets/{pl-PL-T2D74RX3-Cc6Q5nSh.js → pl-PL-T2D74RX3-CRQQfZUe.js} +1 -1
  95. package/dist/frontend/assets/{pt-BR-5N22H2LF-3eEiAax3.js → pt-BR-5N22H2LF-DLvfG7RR.js} +1 -1
  96. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CgnF0Le7.js → pt-PT-UZXXM6DQ-Chkg6-JC.js} +1 -1
  97. package/dist/frontend/assets/{quadrantDiagram-0957ecba-C1Eo-ZUY.js → quadrantDiagram-0957ecba-BJR82kAx.js} +3 -3
  98. package/dist/frontend/assets/{quadrantDiagram-120e2f19-Bdfsl_O7.js → quadrantDiagram-120e2f19-CsWgLp5e.js} +1 -1
  99. package/dist/frontend/assets/relay-BSVGqyIy.js +2 -0
  100. package/dist/frontend/assets/{requirementDiagram-23d650b8-DiOK6QW3.js → requirementDiagram-23d650b8-CHmg-ycX.js} +1 -1
  101. package/dist/frontend/assets/{requirementDiagram-deff3bca-BDJ-7T4h.js → requirementDiagram-deff3bca-LIsyxsZZ.js} +1 -1
  102. package/dist/frontend/assets/{ro-RO-JPDTUUEW-CbJ3IB7a.js → ro-RO-JPDTUUEW-B_a5A5-x.js} +1 -1
  103. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-PQGmeGG4.js → ru-RU-B4JR7IUQ-CY5sI1Ru.js} +1 -1
  104. package/dist/frontend/assets/{sankeyDiagram-04a897e0-DV25bgN2.js → sankeyDiagram-04a897e0-DN5e16d0.js} +1 -1
  105. package/dist/frontend/assets/{sankeyDiagram-23345273-szSb_Sei.js → sankeyDiagram-23345273-BBjQCtHz.js} +1 -1
  106. package/dist/frontend/assets/{selectAll-DsrPvhqV.js → selectAll-Dcq-dC_E.js} +1 -1
  107. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-Dg88bSlc.js → sequenceDiagram-17ac3bff-CIXyQiN6.js} +1 -1
  108. package/dist/frontend/assets/{sequenceDiagram-704730f1-DRIjLCxx.js → sequenceDiagram-704730f1-BmDwBhsF.js} +1 -1
  109. package/dist/frontend/assets/{si-LK-N5RQ5JYF-BtIIudm3.js → si-LK-N5RQ5JYF-DEPKL_1i.js} +1 -1
  110. package/dist/frontend/assets/{sk-SK-C5VTKIMK-CIwXticU.js → sk-SK-C5VTKIMK-XqrGDM2K.js} +1 -1
  111. package/dist/frontend/assets/{sl-SI-NN7IZMDC-CXEdg57y.js → sl-SI-NN7IZMDC-Dia3d1Yy.js} +1 -1
  112. package/dist/frontend/assets/{stateDiagram-587899a1-BwRwgRo6.js → stateDiagram-587899a1-CY1BCaUa.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-9c5f0230-C8n96rER.js → stateDiagram-9c5f0230-gyjWQtSe.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-B7e5E4J7.js → stateDiagram-v2-51a3dcff-CAIiXqRE.js} +1 -1
  115. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-B18iZQLw.js → stateDiagram-v2-d93cdb3a-DvUpzRiQ.js} +1 -1
  116. package/dist/frontend/assets/{styles-2ab5d517-CfWubwio.js → styles-2ab5d517-BE3yBf4Z.js} +3 -3
  117. package/dist/frontend/assets/{styles-5f03d8d2-C-_n19AB.js → styles-5f03d8d2-CGBEZiW_.js} +3 -3
  118. package/dist/frontend/assets/{styles-6aaf32cf-Dh-DHkVP.js → styles-6aaf32cf-JdIhb4_Y.js} +1 -1
  119. package/dist/frontend/assets/{styles-9a916d00-Boy0hUEG.js → styles-9a916d00-DRmxoyEF.js} +1 -1
  120. package/dist/frontend/assets/{styles-c10674c1-BfxiK3Ns.js → styles-c10674c1-C3TEvIjw.js} +1 -1
  121. package/dist/frontend/assets/{styles-edf9a4b0-T7mKViRF.js → styles-edf9a4b0-B_FeyXdP.js} +1 -1
  122. package/dist/frontend/assets/subset-shared.chunk.js +9 -9
  123. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  124. package/dist/frontend/assets/{sv-SE-XGPEYMSR-dVrUAqld.js → sv-SE-XGPEYMSR-CBrvnOWh.js} +1 -1
  125. package/dist/frontend/assets/{svgDrawCommon-08f97a94-fsWk6SQc.js → svgDrawCommon-08f97a94-DI4ZG6xI.js} +1 -1
  126. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-BJ8X3qS-.js → svgDrawCommon-3ba9043b-Dl5HsA0X.js} +1 -1
  127. package/dist/frontend/assets/{ta-IN-2NMHFXQM-CtVVZfIT.js → ta-IN-2NMHFXQM-DGYl9sGK.js} +1 -1
  128. package/dist/frontend/assets/{th-TH-HPSO5L25-Do90CyDN.js → th-TH-HPSO5L25-Bq3K3L0F.js} +1 -1
  129. package/dist/frontend/assets/{timeline-definition-7e6b55e7-D_iHA485.js → timeline-definition-7e6b55e7-DWL-fZtL.js} +3 -3
  130. package/dist/frontend/assets/{timeline-definition-85554ec2-nhT8QMsO.js → timeline-definition-85554ec2-C14kwDFd.js} +1 -1
  131. package/dist/frontend/assets/{tr-TR-DEFEU3FU-MfR-IQke.js → tr-TR-DEFEU3FU-CSwI35xw.js} +1 -1
  132. package/dist/frontend/assets/{uk-UA-QMV73CPH-CPet8B1O.js → uk-UA-QMV73CPH-Dxy7prmb.js} +1 -1
  133. package/dist/frontend/assets/{union-BgYwhB7T.js → union-DQ7m6Pel.js} +1 -1
  134. package/dist/frontend/assets/{vi-VN-M7AON7JQ-3HD06T_8.js → vi-VN-M7AON7JQ-BNyYTgjT.js} +1 -1
  135. package/dist/frontend/assets/{xychartDiagram-b6496bcd-D5vSaFT2.js → xychartDiagram-b6496bcd-DWKYL0ZD.js} +4 -4
  136. package/dist/frontend/assets/{xychartDiagram-e933f94c-DLIHXsUi.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-t2RyWPw2.js → zh-CN-LNUGB5OW-BNhZCPEt.js} +1 -1
  139. package/dist/frontend/assets/{zh-HK-E62DVLB3-Bj22hc7e.js → zh-HK-E62DVLB3-D_rDALSq.js} +1 -1
  140. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-CBRhqhU1.js → zh-TW-RAJ6MFWO-rUlbmCTA.js} +1 -1
  141. package/dist/frontend/assets/zipObject-Db5-6vIz.js +1 -0
  142. package/dist/frontend/index.html +172 -236
  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 -10
  160. package/dist/frontend/assets/arc-Cc3ncJ1n.js +0 -1
  161. package/dist/frontend/assets/channel-BQSIb3i3.js +0 -1
  162. package/dist/frontend/assets/classDiagram-v2-d5a6b087-BgB8AG2e.js +0 -2
  163. package/dist/frontend/assets/clone-BoZziNL9.js +0 -1
  164. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-Z3H2d3Wj.js +0 -1
  165. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CIQOMghC.js +0 -1
  166. package/dist/frontend/assets/index-DIz4WrSB.js +0 -349
  167. package/dist/frontend/assets/zipObject-DMfHW2o8.js +0 -1
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -2279
  170. package/dist/index.js.map +0 -1
  171. package/dist/remote.d.ts +0 -5
  172. package/dist/remote.d.ts.map +0 -1
  173. package/dist/remote.js +0 -129
  174. package/dist/remote.js.map +0 -1
  175. package/dist/server.d.ts +0 -4
  176. package/dist/server.d.ts.map +0 -1
  177. package/dist/server.js +0 -1208
  178. package/dist/server.js.map +0 -1
  179. package/dist/target.d.ts +0 -38
  180. package/dist/target.d.ts.map +0 -1
  181. package/dist/target.js +0 -225
  182. package/dist/target.js.map +0 -1
  183. package/dist/types.d.ts +0 -228
  184. package/dist/types.d.ts.map +0 -1
  185. package/dist/types.js +0 -45
  186. package/dist/types.js.map +0 -1
  187. package/dist/utils/auth.d.ts +0 -14
  188. package/dist/utils/auth.d.ts.map +0 -1
  189. package/dist/utils/auth.js +0 -67
  190. package/dist/utils/auth.js.map +0 -1
  191. package/dist/utils/github.d.ts +0 -24
  192. package/dist/utils/github.d.ts.map +0 -1
  193. package/dist/utils/github.js +0 -132
  194. package/dist/utils/github.js.map +0 -1
  195. package/dist/utils/logger.d.ts +0 -4
  196. package/dist/utils/logger.d.ts.map +0 -1
  197. package/dist/utils/logger.js +0 -25
  198. package/dist/utils/logger.js.map +0 -1
  199. package/dist/utils/r2store.d.ts +0 -12
  200. package/dist/utils/r2store.d.ts.map +0 -1
  201. package/dist/utils/r2store.js +0 -75
  202. package/dist/utils/r2store.js.map +0 -1
  203. package/frontend/drop.png +0 -0
  204. package/frontend/index.html +0 -339
  205. package/frontend/src/App.tsx +0 -1091
  206. package/frontend/src/main.tsx +0 -15
  207. package/frontend/src/utils/ghSync.ts +0 -234
  208. package/frontend/src/utils/mermaidConverter.ts +0 -43
  209. package/frontend/src/utils/webmcp.ts +0 -265
  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 -528
  215. package/src/index.ts +0 -2538
  216. package/src/remote.ts +0 -92
  217. package/src/server.ts +0 -1349
  218. package/src/target.ts +0 -198
  219. package/src/types.ts +0 -336
  220. package/src/utils/auth.ts +0 -61
  221. package/src/utils/github.ts +0 -121
  222. package/src/utils/logger.ts +0 -35
  223. package/src/utils/r2store.ts +0 -70
package/dist/cli.js CHANGED
@@ -1,488 +1,1035 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from 'node:child_process';
3
- import { createConnection } from 'node:net';
2
+ import { spawn, spawnSync, execSync } from 'node:child_process';
4
3
  import fs from 'node:fs';
4
+ import os from 'node:os';
5
5
  import path from 'node:path';
6
- import readline from 'node:readline';
7
6
  import { fileURLToPath } from 'node:url';
8
7
  const __filename = fileURLToPath(import.meta.url);
9
8
  const __dirname = path.dirname(__filename);
10
9
  const CONFIG_NAME = '.excalidrop.json';
11
10
  const MCP_JSON = '.mcp.json';
12
- const SERVER_ENTRY = path.join(__dirname, 'server.js');
13
- const MCP_ENTRY = path.join(__dirname, 'index.js');
14
- const DEFAULT_START_PORT = 3030;
15
- const PORT_SCAN_SIZE = 200;
16
- function isPortFree(port, host = '127.0.0.1', timeoutMs = 300) {
17
- return new Promise((resolve) => {
18
- let settled = false;
19
- const socket = createConnection({ host, port });
20
- const done = (free) => {
21
- if (settled)
22
- return;
23
- settled = true;
24
- socket.destroy();
25
- resolve(free);
26
- };
27
- socket.setTimeout(timeoutMs);
28
- socket.once('connect', () => done(false)); // something listens → taken
29
- socket.once('timeout', () => done(true));
30
- 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 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
+ }
60
+ async function ensureMainBranch(repo) {
61
+ const main = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/main`, { headers: ghHeaders() });
62
+ if (main.ok)
63
+ return;
64
+ const branches = await (await fetch(`${GH_API}/repos/${repo}/branches`, { headers: ghHeaders() })).json();
65
+ if (branches.length !== 0)
66
+ return;
67
+ await ghPut(`/repos/${repo}/contents/README.md`, {
68
+ message: 'Initial commit',
69
+ content: Buffer.from('').toString('base64'),
70
+ branch: 'main',
31
71
  });
32
72
  }
33
- async function findFreePort(preferred, start = DEFAULT_START_PORT) {
34
- if (preferred && preferred > 0 && preferred < 65536 && (await isPortFree(preferred))) {
35
- return preferred;
73
+ async function getScene(repo) {
74
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}?ref=${CANVAS_BRANCH}`, { headers: ghHeaders() });
75
+ if (r.status === 404)
76
+ return { elements: [], sha: null };
77
+ if (!r.ok)
78
+ throw new Error(`scene read ${r.status}`);
79
+ const j = await r.json();
80
+ let b64 = typeof j.content === 'string' && j.content.length ? j.content : null;
81
+ if (!b64) {
82
+ const blob = await fetch(`${GH_API}/repos/${repo}/git/blobs/${j.sha}`, { headers: ghHeaders() });
83
+ if (blob.ok)
84
+ b64 = (await blob.json()).content || null;
85
+ }
86
+ if (!b64)
87
+ return { elements: [], sha: j.sha || null };
88
+ return { elements: (JSON.parse(Buffer.from(b64, 'base64').toString('utf8')).elements || []), sha: j.sha };
89
+ }
90
+ async function putScene(repo, elements, sha, message) {
91
+ const body = {
92
+ message,
93
+ content: Buffer.from(JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements, appState: { viewBackgroundColor: '#ffffff', gridSize: null }, files: {} }, null, 2)).toString('base64'),
94
+ branch: CANVAS_BRANCH,
95
+ };
96
+ if (sha)
97
+ body.sha = sha;
98
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
99
+ if (r.status === 404) {
100
+ // create the branch from default, then retry once
101
+ const info = await ghGet(`/repos/${repo}`);
102
+ const from = info.default_branch || 'main';
103
+ const ref = await ghGet(`/repos/${repo}/git/ref/heads/${from}`);
104
+ await fetch(`${GH_API}/repos/${repo}/git/refs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ref: `refs/heads/${CANVAS_BRANCH}`, sha: ref.object.sha }) });
105
+ const retry = await fetch(`${GH_API}/repos/${repo}/contents/${SCENE_PATH}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
106
+ if (!retry.ok)
107
+ throw new Error(`scene write ${retry.status}`);
108
+ return;
36
109
  }
37
- for (let port = start; port < start + PORT_SCAN_SIZE; port++) {
38
- if (await isPortFree(port))
39
- return port;
110
+ if (!r.ok)
111
+ throw new Error(`scene write ${r.status}: ${(await r.text()).slice(0, 150)}`);
112
+ }
113
+ function viewerDir() {
114
+ // dist/cli.js → dist/frontend; fallback to checkout layout.
115
+ const cands = [path.join(__dirname, 'frontend'), path.join(process.cwd(), 'dist', 'frontend')];
116
+ for (const d of cands) {
117
+ if (fs.existsSync(path.join(d, 'index.html')) && fs.existsSync(path.join(d, 'assets')))
118
+ return d;
40
119
  }
41
- for (let i = 0; i < 50; i++) {
42
- const port = 32000 + Math.floor(Math.random() * 2000);
43
- if (await isPortFree(port))
44
- return port;
120
+ throw new Error('Viewer build not found (dist/frontend). Reinstall excalidrop or run `npm run build:frontend` in the excalidrop checkout.');
121
+ }
122
+ function listFilesRecursive(dir, base = dir) {
123
+ const out = [];
124
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
125
+ const full = path.join(dir, e.name);
126
+ if (e.isDirectory())
127
+ out.push(...listFilesRecursive(full, base));
128
+ else if (e.isFile())
129
+ out.push(path.relative(base, full));
45
130
  }
46
- throw new Error('Could not find a free port for the excalidrop canvas server.');
131
+ return out;
47
132
  }
48
- function findProjectRoot(cwd) {
49
- let dir = path.resolve(cwd);
50
- const home = process.env.HOME || process.env.USERPROFILE || '/';
51
- while (true) {
52
- if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))) {
53
- return dir;
133
+ async function deployViewerPages(repo) {
134
+ const dir = viewerDir();
135
+ const files = listFilesRecursive(dir);
136
+ 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;
137
+ const tree = [];
138
+ const pool = 8;
139
+ let i = 0;
140
+ await Promise.all(Array.from({ length: Math.min(pool, files.length) }, async () => {
141
+ while (i < files.length) {
142
+ const rel = files[i++];
143
+ tree.push({ path: rel.split(path.sep).join('/'), mode: '100644', type: 'blob', sha: await blob(fs.readFileSync(path.join(dir, rel)).toString('base64')) });
54
144
  }
55
- const parent = path.dirname(dir);
56
- if (parent === dir || dir === home)
57
- return path.resolve(cwd);
58
- dir = parent;
59
- }
60
- }
61
- function findDropConfig(cwd) {
62
- let dir = path.resolve(cwd);
63
- const root = path.parse(dir).root;
64
- while (true) {
65
- const file = path.join(dir, CONFIG_NAME);
66
- if (fs.existsSync(file)) {
67
- try {
68
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
69
- if (typeof raw.port === 'number') {
70
- return {
71
- dir,
72
- config: {
73
- port: raw.port,
74
- canvasUrl: raw.canvasUrl || `http://127.0.0.1:${raw.port}`,
75
- },
76
- };
77
- }
78
- }
79
- catch {
80
- }
145
+ }));
146
+ tree.push({ path: '.nojekyll', mode: '100644', type: 'blob', sha: await blob('') });
147
+ let baseSha = null;
148
+ let baseTree = null;
149
+ try {
150
+ const ref = await ghGet(`/repos/${repo}/git/ref/heads/${PAGES_BRANCH}`);
151
+ baseSha = ref.object.sha;
152
+ baseTree = (await ghGet(`/repos/${repo}/git/commits/${baseSha}`)).tree.sha;
153
+ }
154
+ catch { /* first deploy — root commit */ }
155
+ const newTree = await (await fetch(`${GH_API}/repos/${repo}/git/trees`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ...(baseTree ? { base_tree: baseTree } : {}), tree }) })).json();
156
+ 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();
157
+ if (baseSha)
158
+ await fetch(`${GH_API}/repos/${repo}/git/refs/heads/${PAGES_BRANCH}`, { method: 'PATCH', headers: ghHeaders(), body: JSON.stringify({ sha: commit.sha, force: true }) });
159
+ else
160
+ await fetch(`${GH_API}/repos/${repo}/git/refs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ref: `refs/heads/${PAGES_BRANCH}`, sha: commit.sha }) });
161
+ // API commits don't reliably queue builds — request one explicitly.
162
+ try {
163
+ const cur = await fetch(`${GH_API}/repos/${repo}/pages`, { headers: ghHeaders() });
164
+ if (!cur.ok || (await cur.json())?.source?.branch !== PAGES_BRANCH) {
165
+ const up = await fetch(`${GH_API}/repos/${repo}/pages`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify({ source: { branch: PAGES_BRANCH, path: '/' } }) });
166
+ if (!up.ok)
167
+ await fetch(`${GH_API}/repos/${repo}/pages`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ source: { branch: PAGES_BRANCH, path: '/' } }) });
81
168
  }
82
- if (dir === root)
83
- return null;
84
- dir = path.dirname(dir);
169
+ await fetch(`${GH_API}/repos/${repo}/pages/builds`, { method: 'POST', headers: ghHeaders() });
85
170
  }
171
+ catch { /* Pages setup is best-effort */ }
86
172
  }
87
- function portFromExpressUrl(url) {
88
- if (!url)
89
- return undefined;
173
+ function projectNameForRepo(repo) {
174
+ return `excalidrop-${repo.replace('/', '-').toLowerCase().replace(/[^a-z0-9-]/g, '-')}`.slice(0, 60);
175
+ }
176
+ function viewerUrlFor(repo, target) {
177
+ if (target === 'cloudflare') {
178
+ // Generic viewer build can't infer the repo from a pages.dev host —
179
+ // detectRepo() reads it from ?repo= instead.
180
+ return `https://${projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
181
+ }
182
+ const [owner, name] = repo.split('/');
183
+ return `https://${owner}.github.io/${name}/`;
184
+ }
185
+ async function isPrivateRepo(repo) {
90
186
  try {
91
- const port = Number(new URL(url).port);
92
- return Number.isFinite(port) && port > 0 ? port : undefined;
187
+ const r = spawnSync('gh', ['api', `repos/${repo}`, '--jq', '.private'], { encoding: 'utf8' });
188
+ if (r.status !== 0)
189
+ return null;
190
+ if (r.stdout.trim() === 'true')
191
+ return true;
192
+ if (r.stdout.trim() === 'false')
193
+ return false;
194
+ return null;
93
195
  }
94
196
  catch {
95
- return undefined;
96
- }
97
- }
98
- function resolveSavedPort(cwd) {
99
- const envPort = Number(process.env.EXCALIDROP_PORT);
100
- if (Number.isFinite(envPort) && envPort > 0)
101
- return envPort;
102
- const envUrlPort = portFromExpressUrl(process.env.EXPRESS_SERVER_URL);
103
- if (envUrlPort)
104
- return envUrlPort;
105
- const found = findDropConfig(cwd);
106
- if (found)
107
- return found.config.port;
108
- return DEFAULT_START_PORT;
109
- }
110
- function mcpServerJson() {
111
- return {
112
- mcpServers: {
113
- excalidrop: {
114
- command: 'npx',
115
- args: ['excalidrop', 'mcp'],
116
- },
117
- },
118
- };
197
+ return null;
198
+ }
119
199
  }
120
- function agentCommands() {
121
- return [
122
- {
123
- name: 'Claude Code (project scope)',
124
- command: 'claude mcp add excalidrop --scope project -- npx excalidrop mcp',
125
- },
126
- {
127
- name: 'Claude Code (user scope)',
128
- command: 'claude mcp add excalidraw --scope user -- npx excalidrop mcp',
129
- },
130
- {
131
- name: 'Codex CLI',
132
- command: 'codex mcp add excalidrop -- npx excalidrop mcp',
133
- },
134
- {
135
- name: 'Gemini CLI',
136
- command: 'gemini mcp add excalidrop npx excalidrop mcp',
137
- },
138
- {
139
- name: 'Cursor (.cursor/mcp.json) / Claude Desktop / Antigravity',
140
- command: JSON.stringify(mcpServerJson(), null, 2),
141
- },
142
- {
143
- name: 'OpenCode (opencode.json)',
144
- command: JSON.stringify({
145
- $schema: 'https://opencode.ai/config.json',
146
- mcp: {
147
- excalidrop: {
148
- type: 'local',
149
- command: ['npx', 'excalidrop', 'mcp'],
150
- enabled: true,
151
- },
152
- },
153
- }, null, 2),
154
- },
155
- ];
156
- }
157
- function prompt(question) {
158
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
- return new Promise((resolve) => {
160
- rl.question(question, (answer) => {
161
- rl.close();
162
- resolve(answer.trim());
163
- });
164
- });
200
+ async function resolveTarget(repo, explicit) {
201
+ if (explicit === 'pages' || explicit === 'cloudflare')
202
+ return explicit;
203
+ return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
165
204
  }
166
- function commandExists(cmd) {
167
- const probe = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', cmd], {
168
- stdio: 'ignore',
169
- shell: process.platform === 'win32',
170
- });
171
- return probe.status === 0;
205
+ const WRANGLER = ['-y', 'wrangler@4'];
206
+ // Which Cloudflare identity would a Pages deploy use? whoami fails when
207
+ // logged out; a CLOUDFLARE_API_TOKEN works without a login. Deploys always
208
+ // run under the USER's own account/quota — never the author's.
209
+ function wranglerAccount() {
210
+ if (process.env.CLOUDFLARE_API_TOKEN)
211
+ return { ok: true, display: 'API token (CLOUDFLARE_API_TOKEN)' };
212
+ const r = runCapture('npx', [...WRANGLER, 'whoami']);
213
+ if (r.status !== 0)
214
+ return { ok: false, display: '' };
215
+ const m = r.out.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
216
+ return { ok: true, display: m?.[0] ?? 'logged in' };
217
+ }
218
+ function deployToCloudflarePages(project) {
219
+ // wrangler 4 doesn't auto-create Pages projects and its Workers delegation
220
+ // misfires on explicit asset dirs — create once, then deploy classic with --force.
221
+ const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
222
+ const createOut = (create.stdout || '') + (create.stderr || '');
223
+ if (create.status !== 0 && !/already exists/i.test(createOut)) {
224
+ throw new Error(`wrangler pages project create failed:\n${createOut.slice(-800)}\nRun \`wrangler login\` first (or set CLOUDFLARE_API_TOKEN).`);
225
+ }
226
+ const r = spawnSync('npx', [...WRANGLER, 'pages', 'deploy', viewerDir(), '--project-name', project, '--force'], { stdio: 'inherit', env: process.env });
227
+ if (r.status !== 0)
228
+ throw new Error('wrangler pages deploy failed. Run `wrangler login` first (or set CLOUDFLARE_API_TOKEN).');
172
229
  }
173
230
  function detectSlug() {
174
231
  try {
175
232
  const url = spawnSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).stdout.trim().replace(/\.git$/, '');
176
- return url.match(/github\.com[:/](.+)/)?.[1] || '';
233
+ return url.match(/github\.com[:/](.+)/)?.[1]?.toLowerCase() || '';
177
234
  }
178
235
  catch {
179
236
  return '';
180
237
  }
181
238
  }
239
+ /** Accept owner/repo, a GitHub URL, or git@ SSH — always returns owner/repo or ''. */
240
+ export function normalizeRepoSlug(input) {
241
+ const s = (input || '').trim();
242
+ if (!s)
243
+ return '';
244
+ // Full URL / SSH / bare host path → extract the owner/repo tail.
245
+ const m = s.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\/.*)?$/i);
246
+ const candidate = m ? m[1] : s;
247
+ const cleaned = candidate.replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
248
+ const ok = cleaned.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
249
+ return ok ? `${ok[1].toLowerCase()}/${ok[2]}` : '';
250
+ }
251
+ const SLUG_HINT = 'owner/repo or a GitHub link';
182
252
  function ghAuthed() {
183
253
  return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
184
254
  }
185
- async function cmdInit(args) {
186
- const portFlag = args.find((a) => a.startsWith('--port='));
187
- const preferred = portFlag ? Number(portFlag.split('=')[1]) : undefined;
188
- const noPrompt = args.includes('--no-prompt') || args.includes('-y');
189
- const root = findProjectRoot(process.cwd());
190
- const slug = detectSlug();
191
- const port = await findFreePort(preferred);
192
- const config = { port, canvasUrl: `http://127.0.0.1:${port}`, ...(slug ? { remote: slug } : {}) };
193
- fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify(config, null, 2) + '\n');
194
- console.log(`\nexcalidrop: canvas port ${port} saved to ${path.join(root, CONFIG_NAME)}`);
195
- if (slug)
196
- console.log(`excalidrop: remote canvas preselected: ${slug} (agent uses it automatically)`);
197
- const scenePath = path.join(root, 'canvas.excalidraw');
198
- if (!fs.existsSync(scenePath)) {
199
- fs.writeFileSync(scenePath, JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements: [], appState: { viewBackgroundColor: '#ffffff', gridSize: null }, files: {} }, null, 2) + '\n');
200
- console.log(`excalidrop: empty scene written to ${scenePath}`);
255
+ function commandExists(cmd) {
256
+ const probe = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', cmd], { stdio: 'ignore', shell: process.platform === 'win32' });
257
+ return probe.status === 0;
258
+ }
259
+ function findProjectRoot(cwd) {
260
+ // Inside a git checkout the repo top is the root. Never walk up past cwd
261
+ // looking for package.json: from a fresh non-checkout dir (e.g. a folder
262
+ // holding a brand-new canvas repo) the old walk-up climbed into an
263
+ // unrelated parent project and wrote .mcp.json / .excalidrop.json there.
264
+ try {
265
+ const top = execSync('git rev-parse --show-toplevel', { cwd: path.resolve(cwd), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
266
+ if (top)
267
+ return top;
201
268
  }
202
- const mcpPath = path.join(root, MCP_JSON);
203
- let mcp = {};
204
- if (fs.existsSync(mcpPath)) {
269
+ catch { /* not inside a git work tree — fall through to cwd */ }
270
+ return path.resolve(cwd);
271
+ }
272
+ function mcpArgs(repo) {
273
+ return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
274
+ }
275
+ const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'cursor-user', 'desktop', 'opencode', 'opencode-user'];
276
+ function editorCommand(id, repo) {
277
+ const a = mcpArgs(repo).join(' ');
278
+ switch (id) {
279
+ case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
280
+ case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
281
+ case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
282
+ case 'cursor':
283
+ case 'cursor-user':
284
+ case 'desktop': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
285
+ case 'opencode':
286
+ case 'opencode-user': return JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: { excalidrop: { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true } } }, null, 2);
287
+ }
288
+ }
289
+ function editorScope(id) {
290
+ return id === 'claude-project' || id === 'cursor' || id === 'opencode' ? 'project' : 'user';
291
+ }
292
+ // Where each editor id reads its config from. File-based harnesses support
293
+ // both a project file and a user-global file; CLI-driven ones (claude, codex)
294
+ // are detected through the same files their CLIs write.
295
+ function editorFile(id, root) {
296
+ const home = os.homedir();
297
+ switch (id) {
298
+ case 'claude-user': return { path: path.join(home, '.claude.json'), key: 'mcpServers' };
299
+ case 'claude-project': return { path: path.join(root, MCP_JSON), key: 'mcpServers' };
300
+ case 'cursor': return { path: path.join(root, '.cursor', 'mcp.json'), key: 'mcpServers' };
301
+ case 'cursor-user': return { path: path.join(home, '.cursor', 'mcp.json'), key: 'mcpServers' };
302
+ case 'desktop': return { path: desktopConfigPath(), key: 'mcpServers' };
303
+ case 'opencode': return { path: path.join(root, 'opencode.json'), key: 'mcp' };
304
+ case 'opencode-user': return { path: path.join(home, '.config', 'opencode', 'opencode.json'), key: 'mcp' };
305
+ case 'codex': return null; // TOML (~/.codex/config.toml) — parsed separately
306
+ }
307
+ }
308
+ function desktopConfigPath() {
309
+ const home = os.homedir();
310
+ if (process.platform === 'darwin')
311
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
312
+ if (process.platform === 'win32')
313
+ return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
314
+ return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
315
+ }
316
+ function editorTarget(id, root) {
317
+ const f = editorFile(id, root);
318
+ if (id === 'codex')
319
+ return 'user config (~/.codex/config.toml)';
320
+ const scope = editorScope(id);
321
+ const short = f ? shortenHome(f.path) : id;
322
+ return `${scope} scope (${short})`;
323
+ }
324
+ function shortenHome(p) {
325
+ const home = os.homedir();
326
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
327
+ }
328
+ function repoFromArgs(args) {
329
+ if (!Array.isArray(args))
330
+ return null;
331
+ const i = args.findIndex((a) => a === '--repo');
332
+ const v = i !== -1 ? args[i + 1] : null;
333
+ return typeof v === 'string' && v.includes('/') ? v.toLowerCase() : null;
334
+ }
335
+ // {present, repo} for a JSON MCP config (claude/cursor/desktop/opencode shapes).
336
+ function jsonMcpRepo(file, key) {
337
+ let j;
338
+ try {
339
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
340
+ }
341
+ catch {
342
+ return { present: false, repo: null };
343
+ }
344
+ const e = key === 'mcpServers' ? j?.mcpServers?.excalidrop : j?.mcp?.excalidrop;
345
+ if (!e)
346
+ return { present: false, repo: null };
347
+ if (Array.isArray(e.command))
348
+ return { present: true, repo: repoFromArgs(e.command) };
349
+ if (e.command === 'npx' && Array.isArray(e.args))
350
+ return { present: true, repo: repoFromArgs(e.args) };
351
+ return { present: true, repo: null };
352
+ }
353
+ // {present, repo} for Codex's TOML config — crude section parse, no new deps.
354
+ function codexMcpRepo() {
355
+ let txt;
356
+ try {
357
+ txt = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
358
+ }
359
+ catch {
360
+ return { present: false, repo: null };
361
+ }
362
+ const sec = txt.match(/\[mcp_servers\.excalidrop\]([\s\S]*?)(?=^\[|\z)/m);
363
+ if (!sec)
364
+ return { present: false, repo: null };
365
+ const m = (sec[1] ?? '').match(/--repo["'\s=,]+([A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+)/);
366
+ return { present: true, repo: m?.[1]?.toLowerCase() ?? null };
367
+ }
368
+ function detectEditorState(id, repo, root) {
369
+ const want = repo.toLowerCase();
370
+ if (id === 'codex') {
371
+ const s = codexMcpRepo();
372
+ if (!s.present)
373
+ return { status: 'missing', repo: null, where: '~/.codex/config.toml' };
374
+ return s.repo === want
375
+ ? { status: 'current', repo: s.repo, where: '~/.codex/config.toml' }
376
+ : { status: 'stale', repo: s.repo, where: '~/.codex/config.toml' };
377
+ }
378
+ const f = editorFile(id, root);
379
+ const where = shortenHome(f.path);
380
+ const s = jsonMcpRepo(f.path, f.key);
381
+ if (!s.present)
382
+ return { status: 'missing', repo: null, where };
383
+ return s.repo === want
384
+ ? { status: 'current', repo: s.repo, where }
385
+ : { status: 'stale', repo: s.repo, where };
386
+ }
387
+ function updateCommand(id, repo) {
388
+ if (id === 'claude-project' || id === 'claude-user') {
389
+ const scope = id === 'claude-project' ? 'project' : 'user';
390
+ return `claude mcp remove excalidrop -s ${scope} && ${editorCommand(id, repo)}`;
391
+ }
392
+ return editorCommand(id, repo);
393
+ }
394
+ function runCapture(cmd, args) {
395
+ try {
396
+ const r = spawnSync(cmd, args, { encoding: 'utf8' });
397
+ return { status: r.status, out: `${r.stdout || ''}\n${r.stderr || ''}` };
398
+ }
399
+ catch (e) {
400
+ return { status: 1, out: e.message };
401
+ }
402
+ }
403
+ // Merge an MCP entry into a JSON config file (creates parent dirs). Returns
404
+ // false when the existing file isn't valid JSON — caller falls back to manual.
405
+ function upsertJsonMcp(file, key, entry) {
406
+ let j = {};
407
+ if (fs.existsSync(file)) {
205
408
  try {
206
- mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
409
+ j = JSON.parse(fs.readFileSync(file, 'utf8'));
207
410
  }
208
411
  catch {
209
- console.warn(`excalidrop: existing ${MCP_JSON} is not valid JSON, leaving it untouched.`);
412
+ return false;
210
413
  }
414
+ if (!j || typeof j !== 'object')
415
+ return false;
211
416
  }
212
- if (!mcp.mcpServers || typeof mcp.mcpServers !== 'object')
213
- mcp.mcpServers = {};
214
- mcp.mcpServers.excalidrop = { command: 'npx', args: ['excalidrop', 'mcp'] };
215
- if (Object.keys(mcp).length > 0) {
216
- fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n');
217
- console.log(`excalidrop: MCP server entry written to ${mcpPath}`);
218
- }
219
- console.log('\nStart the canvas for this project with:');
220
- console.log(` npx excalidrop up # ${config.canvasUrl}\n`);
221
- if (noPrompt) {
222
- printAgentCommands();
223
- return;
417
+ else {
418
+ try {
419
+ fs.mkdirSync(path.dirname(file), { recursive: true });
420
+ }
421
+ catch {
422
+ return false;
423
+ }
224
424
  }
225
- if (slug) {
226
- if (!ghAuthed()) {
227
- console.log('GitHub: not logged in. Run `gh auth login` first (2FA via GitHub), then re-run init.\n');
425
+ if (key === 'mcpServers') {
426
+ if (!j.mcpServers || typeof j.mcpServers !== 'object')
427
+ j.mcpServers = {};
428
+ j.mcpServers.excalidrop = entry;
429
+ }
430
+ else {
431
+ if (!j.mcp || typeof j.mcp !== 'object')
432
+ j.mcp = {};
433
+ j.mcp.excalidrop = entry;
434
+ if (!j.$schema)
435
+ j.$schema = 'https://opencode.ai/config.json';
436
+ }
437
+ try {
438
+ fs.writeFileSync(file, JSON.stringify(j, null, 2) + '\n');
439
+ return true;
440
+ }
441
+ catch {
442
+ return false;
443
+ }
444
+ }
445
+ function cursorEntry(repo) {
446
+ return { command: 'npx', args: mcpArgs(repo), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
447
+ }
448
+ function opencodeEntry(repo) {
449
+ return { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true, env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
450
+ }
451
+ function installEditor(id, repo, root) {
452
+ const fail = (detail) => ({ id, outcome: 'failed', detail });
453
+ // Idempotent first: never reinstall over an identical entry, never silently
454
+ // leave a stale one — report it with the exact update command.
455
+ const st = detectEditorState(id, repo, root);
456
+ if (st.status === 'current')
457
+ return { id, outcome: 'already-current', detail: `${st.where} already points at ${repo}` };
458
+ const staleNote = st.status === 'stale' ? ` (currently → ${st.repo ?? 'unknown'})` : '';
459
+ if (id === 'claude-project' || id === 'claude-user' || id === 'codex') {
460
+ const bin = id === 'codex' ? 'codex' : 'claude';
461
+ if (!commandExists(bin))
462
+ return fail(`${bin} CLI not found — run manually:\n${editorCommand(id, repo)}`);
463
+ const cargs = id === 'claude-project'
464
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)]
465
+ : id === 'claude-user'
466
+ ? ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)]
467
+ : ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)];
468
+ const r = runCapture(bin, cargs);
469
+ if (r.status === 0 && !/already exists/i.test(r.out)) {
470
+ return { id, outcome: 'added', detail: editorCommand(id, repo) };
228
471
  }
229
- else {
230
- const pub = await prompt(`Publish an empty canvas to ${slug} (creates gh-pages + enables Pages)? [Y/n] `);
231
- if (!pub || /^(y|yes)$/i.test(pub)) {
232
- const r = spawnSync('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], {
233
- stdio: 'inherit', env: { ...process.env, REPO_SLUG: slug },
234
- });
235
- if (r.status === 0) {
236
- const [owner, repo] = slug.split('/');
237
- console.log(`\nCanvas live at https://${owner}.github.io/${repo}/`);
238
- const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
239
- console.log(`Install the app once per repo: https://github.com/apps/${appSlug}/installations/new`);
240
- }
241
- }
472
+ if (/already exists/i.test(r.out)) {
473
+ // Exit code is 0 even when nothing changed — re-read the file to say
474
+ // honestly whether the existing entry is current or stale.
475
+ const now = detectEditorState(id, repo, root);
476
+ if (now.status === 'current')
477
+ return { id, outcome: 'already-current', detail: `${now.where} already points at ${repo}` };
478
+ return { id, outcome: 'stale', detail: `already exists${now.repo ? ` → ${now.repo}` : ''} — not updated. Run:\n${updateCommand(id, repo)}` };
242
479
  }
480
+ return fail((r.out.trim() || `${bin} exited ${r.status}`).slice(0, 300));
243
481
  }
244
- const answer = await prompt('Install the excalidrop MCP server into your AI agent now? [Y/n] ');
245
- if (answer && !/^(y|yes)$/i.test(answer)) {
246
- printAgentCommands();
247
- return;
482
+ // File-based harnesses: JSON merge is safe to write for both add and update.
483
+ const f = editorFile(id, root);
484
+ const entry = id === 'cursor' || id === 'cursor-user' || id === 'desktop' ? cursorEntry(repo) : opencodeEntry(repo);
485
+ if (upsertJsonMcp(f.path, f.key, entry)) {
486
+ return { id, outcome: 'added', detail: `wrote ${shortenHome(f.path)}${staleNote} → ${repo}` };
248
487
  }
249
- let installed = false;
250
- if (commandExists('claude')) {
251
- const scopeAnswer = await prompt('Claude Code scope? [project/user] (default: project) ');
252
- const scope = /user/i.test(scopeAnswer) ? 'user' : 'project';
253
- const r = spawnSync('claude', ['mcp', 'add', 'excalidrop', '--scope', scope, '--', 'npx', 'excalidrop', 'mcp'], {
254
- stdio: 'inherit',
255
- });
256
- installed = r.status === 0;
488
+ return fail(`${shortenHome(f.path)} is not valid JSON — leaving it untouched. Add manually:\n${editorCommand(id, repo)}`);
489
+ }
490
+ const EDITOR_LABELS = {
491
+ 'claude-project': 'Claude Code (project scope)',
492
+ 'claude-user': 'Claude Code (user scope)',
493
+ codex: 'Codex CLI (user config)',
494
+ cursor: 'Cursor (project file)',
495
+ 'cursor-user': 'Cursor (user file)',
496
+ desktop: 'Claude Desktop (user config)',
497
+ opencode: 'OpenCode (project file)',
498
+ 'opencode-user': 'OpenCode (user config)',
499
+ };
500
+ // Harnesses group the flat scope-specific ids for the two-step TUI picker:
501
+ // step 1 picks harnesses, step 2 picks the scope per multi-scope harness.
502
+ // Single-scope harnesses (codex, desktop) are user-global — no step 2.
503
+ const HARNESSES = ['claude', 'codex', 'cursor', 'desktop', 'opencode'];
504
+ const HARNESS_EDITORS = {
505
+ claude: ['claude-user', 'claude-project'],
506
+ codex: ['codex'],
507
+ cursor: ['cursor-user', 'cursor'],
508
+ desktop: ['desktop'],
509
+ opencode: ['opencode-user', 'opencode'],
510
+ };
511
+ const HARNESS_LABELS = {
512
+ claude: 'Claude Code',
513
+ codex: 'Codex CLI',
514
+ cursor: 'Cursor',
515
+ desktop: 'Claude Desktop',
516
+ opencode: 'OpenCode',
517
+ };
518
+ // One-line install-state summary for the harness picker, e.g.
519
+ // "user: installed, project: —" or "installed → other/repo".
520
+ function harnessSummary(h, slug, root) {
521
+ const ids = HARNESS_EDITORS[h];
522
+ const parts = ids.map((id) => {
523
+ const st = detectEditorState(id, slug, root);
524
+ const cur = st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : '—';
525
+ return ids.length > 1 ? `${editorScope(id)}: ${cur}` : cur;
526
+ });
527
+ return parts.join(', ');
528
+ }
529
+ // Scope option for step 2 of the picker: short scope label + live state hint.
530
+ function scopeOption(id, slug, root) {
531
+ const st = detectEditorState(id, slug, root);
532
+ return {
533
+ value: id,
534
+ label: editorTarget(id, root),
535
+ hint: st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : undefined,
536
+ };
537
+ }
538
+ // One honest report for every editor install attempt — always shows the
539
+ // command/config that was (or should be) applied, so "installed" can never
540
+ // mean "already existed but points elsewhere".
541
+ function reportInstalls(results, repo, root) {
542
+ for (const r of results) {
543
+ const target = editorTarget(r.id, root);
544
+ if (r.outcome === 'added')
545
+ console.log(`ok ${EDITOR_LABELS[r.id]} → ${target}:\n ${editorCommand(r.id, repo).split('\n').join('\n ')}\n installed (${r.detail})`);
546
+ else if (r.outcome === 'already-current')
547
+ console.log(`skip ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
548
+ else if (r.outcome === 'stale')
549
+ console.log(`warn ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
550
+ else
551
+ console.log(`fail ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
257
552
  }
258
- if (!installed && commandExists('codex')) {
259
- const useIt = await prompt('Run `codex mcp add excalidrop`? [Y/n] ');
260
- if (!useIt || /^(y|yes)$/i.test(useIt)) {
261
- const r = spawnSync('codex', ['mcp', 'add', 'excalidrop', '--', 'npx', 'excalidrop', 'mcp'], {
262
- stdio: 'inherit',
263
- });
264
- installed = r.status === 0;
553
+ }
554
+ /** Poll a viewer URL until it serves 200 (slow first builds take minutes). */
555
+ async function waitForLive(url, timeoutMs = 7 * 60_000) {
556
+ const deadline = Date.now() + timeoutMs;
557
+ while (Date.now() < deadline) {
558
+ try {
559
+ const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
560
+ if (r.ok)
561
+ return true;
562
+ // 404 during initial propagation — keep waiting; anything else too.
265
563
  }
564
+ catch { /* offline / DNS — keep waiting */ }
565
+ await new Promise((r) => setTimeout(r, 10000));
266
566
  }
267
- if (!installed)
268
- printAgentCommands();
567
+ return false;
269
568
  }
270
- function printAgentCommands() {
271
- console.log('Add excalidrop to your AI agent (same pattern as chrome-devtools-mcp):\n');
272
- for (const c of agentCommands()) {
273
- console.log(`## ${c.name}\n${c.command}\n`);
569
+ function parseFlag(args, name) {
570
+ const eq = args.find((a) => a.startsWith(`--${name}=`));
571
+ if (eq)
572
+ return eq.split('=').slice(1).join('=');
573
+ const i = args.indexOf(`--${name}`);
574
+ if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('-'))
575
+ return args[i + 1];
576
+ return undefined;
577
+ }
578
+ function parseEditorsFlag(args) {
579
+ const raw = parseFlag(args, 'editor');
580
+ if (!raw)
581
+ return null;
582
+ const ids = raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
583
+ const valid = ids.filter((s) => EDITOR_IDS.includes(s));
584
+ if (!valid.length) {
585
+ console.error(`Unknown --editor value. Use comma-separated: ${EDITOR_IDS.join(',')}`);
586
+ process.exit(1);
274
587
  }
588
+ return valid;
275
589
  }
276
- async function cmdUp(args) {
277
- const portFlag = args.find((a) => a.startsWith('--port='));
278
- const preferred = portFlag ? Number(portFlag.split('=')[1]) : undefined;
279
- const root = findProjectRoot(process.cwd());
280
- const existing = findDropConfig(process.cwd());
281
- let port;
282
- if (preferred) {
283
- port = await findFreePort(preferred);
590
+ async function cmdSetup(args, editorOverride, opts = {}) {
591
+ const positional = args.find((a) => !a.startsWith('-') && (a.includes('/') || a.includes('github.com')));
592
+ const rawSlug = parseFlag(args, 'repo') || positional || detectSlug();
593
+ const slug = normalizeRepoSlug(rawSlug);
594
+ if (!slug) {
595
+ console.error(`Usage: npx excalidrop setup ${SLUG_HINT} [--target pages|cloudflare] [--editor claude-project,codex] [--no-prompt]`);
596
+ process.exit(1);
597
+ }
598
+ if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
599
+ console.error('Not logged in to GitHub. Run `gh auth login` or `npx excalidrop login` first.');
600
+ process.exit(1);
601
+ }
602
+ try {
603
+ await ghGet(`/repos/${slug}`);
604
+ }
605
+ catch (e) {
606
+ if (String(e.message).includes('→ 404')) {
607
+ 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`);
608
+ process.exit(1);
609
+ }
610
+ throw e;
284
611
  }
285
- else if (existing && (await isPortFree(existing.config.port))) {
286
- port = existing.config.port;
612
+ const explicitTarget = parseFlag(args, 'target');
613
+ const target = await resolveTarget(slug, explicitTarget);
614
+ console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? (opts.fromTui ? ' (selected)' : ' (explicit)') : ' (auto: private→cloudflare, public→pages)'}\n`);
615
+ await ensureMainBranch(slug);
616
+ const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
617
+ if (!scene.sha) {
618
+ await putScene(slug, [], null, 'excalidrop: init canvas');
619
+ console.log('Empty canvas created on branch excalidrop.');
620
+ }
621
+ let viewerUrl;
622
+ if (target === 'pages') {
623
+ await deployViewerPages(slug);
624
+ viewerUrl = viewerUrlFor(slug, 'pages');
287
625
  }
288
626
  else {
289
- port = await findFreePort(existing?.config.port ?? DEFAULT_START_PORT);
290
- }
291
- fs.writeFileSync(path.join(existing?.dir ?? root, CONFIG_NAME), JSON.stringify({ port, canvasUrl: `http://127.0.0.1:${port}` }, null, 2) + '\n');
292
- const host = process.env.HOST || '127.0.0.1';
293
- console.log(`excalidrop: starting canvas on http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`);
294
- const child = spawn(process.execPath, [SERVER_ENTRY], {
295
- stdio: 'inherit',
296
- env: { ...process.env, PORT: String(port), HOST: host },
297
- });
298
- child.on('exit', (code) => process.exit(code ?? 0));
627
+ // Fail fast on missing auth instead of dying mid-deploy. The TUI checks
628
+ // interactively before the spinner; non-interactive runs land here.
629
+ const acct = wranglerAccount();
630
+ if (!acct.ok && !opts.fromTui) {
631
+ throw new Error('Not logged into Cloudflare (wrangler whoami failed). Run `npx -y wrangler@4 login` first, or set CLOUDFLARE_API_TOKEN.');
632
+ }
633
+ console.log(`Cloudflare account: ${acct.display || 'unknown'} — the viewer deploys to YOUR account and quota.`);
634
+ deployToCloudflarePages(projectNameForRepo(slug));
635
+ viewerUrl = viewerUrlFor(slug, 'cloudflare');
636
+ }
637
+ // Don't declare victory until the viewer actually serves — first Pages /
638
+ // Cloudflare builds take minutes, and "Canvas live" on a 404 wastes a debug
639
+ // session. Poll the URL (up to 7 min for slow first builds), then report
640
+ // honestly with the Pages build state attached.
641
+ const live = await waitForLive(viewerUrl);
642
+ if (live) {
643
+ console.log('Viewer is serving.');
644
+ }
645
+ else {
646
+ console.log('Viewer not live yet (HTTP 404) — hosting may still be building; retry the URL in a minute.');
647
+ const info = await pagesBuildInfo(slug, target).catch(() => null);
648
+ if (info)
649
+ console.log(info);
650
+ }
651
+ await ensureRepoHomepage(slug, viewerUrl);
652
+ const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
653
+ console.log(`Note: browser login stays read-only until the Excalidrop app is installed on ${slug}:\n https://github.com/apps/${appSlug}/installations/new`);
654
+ const root = findProjectRoot(process.cwd());
655
+ fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
656
+ const mcpPath = path.join(root, MCP_JSON);
657
+ let mcp = {};
658
+ if (fs.existsSync(mcpPath)) {
659
+ try {
660
+ mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
661
+ }
662
+ catch {
663
+ console.warn(`Existing ${MCP_JSON} is not valid JSON, leaving it untouched.`);
664
+ }
665
+ }
666
+ if (!mcp.mcpServers || typeof mcp.mcpServers !== 'object')
667
+ mcp.mcpServers = {};
668
+ mcp.mcpServers.excalidrop = { command: 'npx', args: mcpArgs(slug), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
669
+ fs.writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + '\n');
670
+ console.log(`MCP entry written to ${mcpPath}`);
671
+ const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
672
+ const noPrompt = args.includes('--no-prompt') || args.includes('-y');
673
+ const installs = editors.map((id) => installEditor(id, slug, root));
674
+ // The TUI prints its own per-editor report (it owns the spinner); the plain
675
+ // CLI prints here so `setup --no-prompt` shows exactly what ran.
676
+ if (!opts.fromTui)
677
+ reportInstalls(installs, slug, root);
678
+ void noPrompt;
679
+ return { repo: slug, viewerUrl, live, installs };
680
+ }
681
+ // Latest GitHub Pages build state for a repo — printed when the viewer URL
682
+ // still 404s after the wait so the user knows whether it's "still building"
683
+ // vs "build errored / Pages misconfigured".
684
+ async function pagesBuildInfo(repo, target) {
685
+ if (target !== 'pages')
686
+ return `Cloudflare deploy is async — check the Pages project dashboard; if still 404, rerun: npx excalidrop publish ${repo}`;
687
+ try {
688
+ const latest = await ghGet(`/repos/${repo}/pages/builds/latest`);
689
+ const err = latest?.error?.message ? ` — ${latest.error.message}` : '';
690
+ return `Pages latest build: ${latest.status}${err} (updated ${latest.updated_at || 'unknown'}). Check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}) — first builds can take several minutes. If stuck, rerun: npx excalidrop publish ${repo}`;
691
+ }
692
+ catch {
693
+ return `Pages build status unknown — check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}).`;
694
+ }
299
695
  }
300
696
  function cmdMcp() {
301
- const port = resolveSavedPort(process.cwd());
302
- const canvasUrl = process.env.EXPRESS_SERVER_URL || `http://127.0.0.1:${port}`;
303
- const child = spawn(process.execPath, [MCP_ENTRY], {
304
- stdio: 'inherit',
305
- env: { ...process.env, EXPRESS_SERVER_URL: canvasUrl },
306
- });
697
+ const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
307
698
  child.on('exit', (code) => process.exit(code ?? 0));
308
699
  }
700
+ async function cmdLogin() {
701
+ const clientId = process.env.GITHUB_OAUTH_CLIENT_ID || 'Iv23liuS2fx3QOEIoDmx';
702
+ console.log('Starting GitHub device login…');
703
+ const dev = await (await fetch('https://github.com/login/device/code', {
704
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
705
+ body: JSON.stringify({ client_id: clientId }),
706
+ })).json();
707
+ console.log(`\nOpen ${dev.verification_uri} and enter code: ${dev.user_code}\n`);
708
+ const deadline = Date.now() + 300_000;
709
+ while (Date.now() < deadline) {
710
+ await new Promise((r) => setTimeout(r, (dev.interval || 5) * 1000));
711
+ const j = await (await fetch('https://github.com/login/oauth/access_token', {
712
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
713
+ body: JSON.stringify({ client_id: clientId, device_code: dev.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
714
+ })).json();
715
+ if (j.access_token) {
716
+ const dir = path.join(os.homedir(), '.config', 'excalidrop');
717
+ fs.mkdirSync(dir, { recursive: true });
718
+ fs.writeFileSync(path.join(dir, 'gh_token'), j.access_token + '\n', { mode: 0o600 });
719
+ console.log('Logged in — token stored in ~/.config/excalidrop/gh_token');
720
+ return;
721
+ }
722
+ if (j.error && j.error !== 'authorization_pending' && j.error !== 'slow_down')
723
+ throw new Error(j.error_description || j.error);
724
+ }
725
+ throw new Error('device login timed out');
726
+ }
309
727
  async function cmdStatus() {
310
- const found = findDropConfig(process.cwd());
311
- const port = found?.config.port ?? resolveSavedPort(process.cwd());
312
- const url = process.env.EXPRESS_SERVER_URL || `http://127.0.0.1:${port}`;
313
- console.log(`config: ${found ? path.join(found.dir, CONFIG_NAME) : '(none — using defaults)'}`);
314
- console.log(`canvas: ${url}`);
728
+ const flag = process.argv.find((a) => a.startsWith('--repo='));
729
+ const repo = flag ? flag.split('=').slice(1).join('=') : detectSlug();
730
+ console.log(`repo: ${repo || '(none — run setup or pass --repo=owner/repo)'}`);
731
+ console.log(`auth: ${ghToken() ? 'ok (env/gh CLI/stored)' : 'missing — run `gh auth login` or `npx excalidrop login`'}`);
732
+ console.log(`relay: ${process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT}`);
733
+ if (!repo || !ghToken())
734
+ return;
315
735
  try {
316
- const res = await fetch(`${url}/health`);
317
- console.log(`health: ${res.ok ? 'ok' : `HTTP ${res.status}`} ${res.ok ? '' : await res.text()}`);
318
- if (res.ok)
319
- console.log(await res.text());
320
- }
321
- catch (err) {
322
- console.log(`health: unreachable (${err.message}) — start it with \`npx excalidrop up\``);
323
- }
324
- }
325
- async function cmdPages(_args) {
326
- console.log(`excalidrop pages (GitHub as source of truth, no Actions, no setup)\n`);
327
- console.log('1. Publish viewer: npx excalidrop publish');
328
- console.log(' (uses `gh auth` — repo auto-detected, Pages auto-enabled.)\n');
329
- console.log('2. In your agent: switch_remote { target: "<owner>.github.io/<repo>" }');
330
- console.log(' Draw as usual — commits land on GitHub, viewer updates on gh-pages.\n');
331
- console.log('3. Login: ask the agent to run github_login (device flow, 2FA via GitHub),');
332
- console.log(' or paste a token (`gh auth token`) on the viewer login pill.\n');
333
- console.log('Rule: never edit the canvas locally — the Pages site + GitHub repo are the canvas.');
334
- }
335
- function cmdRemote() {
336
- const child = spawn(process.execPath, [path.join(__dirname, 'remote.js')], { stdio: 'inherit', env: process.env });
337
- child.on('exit', (code) => process.exit(code ?? 0));
736
+ const scene = await getScene(repo);
737
+ console.log(`scene: ${scene.elements.length} elements @${CANVAS_BRANCH}`);
738
+ }
739
+ catch (e) {
740
+ console.log(`scene: unreachable (${e.message})`);
741
+ }
338
742
  }
339
- async function cmdSetup(args) {
340
- if (args.includes('--help') || args.includes('-h')) {
341
- 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.');
342
- return;
743
+ async function runTui() {
744
+ let clack;
745
+ try {
746
+ clack = await import('@clack/prompts');
343
747
  }
344
- console.log('excalidrop setup — works on any repo you own or can access\n');
345
- // 1. gh auth (the only prerequisite; 2FA enforced by GitHub itself)
346
- const auth = spawnSync('gh', ['auth', 'status'], { stdio: 'pipe', encoding: 'utf8' });
347
- if (auth.status !== 0) {
348
- console.error('Not logged in to GitHub. Run this first:\n\n gh auth login\n');
748
+ catch {
749
+ console.error('TUI needs @clack/prompts. Run instead: npx excalidrop setup owner/repo');
349
750
  process.exit(1);
350
751
  }
351
- const slug = (() => {
352
- try {
353
- const url = spawnSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).stdout.trim().replace(/\.git$/, '');
354
- const m = url.match(/github\.com[:/](.+)/);
355
- return m?.[1] || '';
752
+ clack.intro('excalidrop — remote canvas setup');
753
+ const detected = detectSlug();
754
+ const repoAnswer = await clack.text({
755
+ message: 'Which repo holds the canvas?',
756
+ placeholder: detected || SLUG_HINT,
757
+ initialValue: detected || '',
758
+ validate: (v) => (normalizeRepoSlug(v) ? undefined : `Use ${SLUG_HINT}`),
759
+ });
760
+ if (clack.isCancel(repoAnswer)) {
761
+ clack.cancel('Aborted.');
762
+ process.exit(0);
763
+ }
764
+ const slug = normalizeRepoSlug(repoAnswer);
765
+ const priv = await isPrivateRepo(slug);
766
+ if (priv !== null)
767
+ clack.log.info(`Repo is ${priv ? 'private' : 'public'} — recommended host: ${priv ? 'Cloudflare Pages' : 'GitHub Pages'}.`);
768
+ // Visibility drift: repo flipped public<->private since last setup, so the
769
+ // stored target + viewer URL no longer match — nudge to re-pick below.
770
+ try {
771
+ const prevRaw = fs.readFileSync(path.join(findProjectRoot(process.cwd()), CONFIG_NAME), 'utf8');
772
+ const prevTarget = JSON.parse(prevRaw).target;
773
+ const want = priv === true ? 'cloudflare' : 'pages';
774
+ if (prevTarget && prevTarget !== want && priv !== null) {
775
+ clack.log.warn(`Visibility changed since last setup (host: ${prevTarget}, repo now ${priv ? 'private' : 'public'}). Pick ${want} below to migrate — see README "Migrating public<->private".`);
356
776
  }
357
- catch {
358
- return '';
777
+ }
778
+ catch { /* first setup — nothing to compare */ }
779
+ if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
780
+ const how = await clack.select({
781
+ message: 'GitHub auth missing. How to log in?',
782
+ options: [
783
+ { value: 'retry', label: 'I ran `gh auth login` (re-check)' },
784
+ { value: 'device', label: 'Device flow (code + browser, 2FA ok)' },
785
+ { value: 'skip', label: 'Skip (I set GITHUB_TOKEN later)' },
786
+ ],
787
+ });
788
+ if (clack.isCancel(how)) {
789
+ clack.cancel('Aborted.');
790
+ process.exit(0);
791
+ }
792
+ if (how === 'device') {
793
+ const s = clack.spinner();
794
+ s.start('Waiting for browser approval…');
795
+ try {
796
+ await cmdLogin();
797
+ s.stop('Logged in.');
798
+ }
799
+ catch (e) {
800
+ s.stop('Login failed: ' + e.message);
801
+ process.exit(1);
802
+ }
803
+ }
804
+ else if (how === 'retry' && !ghAuthed() && !process.env.GITHUB_TOKEN) {
805
+ clack.log.error('Still not logged in. Run `gh auth login` first.');
806
+ process.exit(1);
359
807
  }
360
- })();
361
- if (!slug) {
362
- console.error('No GitHub remote detected. Run setup inside a cloned repo.\n');
363
- process.exit(1);
364
808
  }
365
- console.log(`Repo: ${slug} (detected from git remote)\n`);
366
- // 2. publish viewer (auto-enables Pages, auto-syncs scene)
367
- await new Promise((resolve, reject) => {
368
- const child = spawn('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], { stdio: 'inherit', env: { ...process.env, REPO_SLUG: slug } });
369
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`publish failed (exit ${code})`))));
809
+ // No auto option: GitHub Pages can't serve private repos on free plans
810
+ // and public repos need zero config, so visibility pre-selects the right
811
+ // host — the user just confirms or flips. Non-interactive `--target` still
812
+ // goes through resolveTarget() in cmdSetup.
813
+ const recommended = priv === true ? 'cloudflare' : 'pages';
814
+ const hostAnswer = await clack.select({
815
+ message: 'Where to host the viewer?',
816
+ initialValue: recommended,
817
+ options: [
818
+ { value: 'pages', label: 'GitHub Pages (public repo)', hint: 'zero config' },
819
+ { value: 'cloudflare', label: 'Cloudflare Pages (private repo)', hint: 'needs wrangler login' },
820
+ ],
370
821
  });
371
- // 3. remember the target: agent auto-loads it, no switch_remote needed
372
- try {
373
- const cfgFile = path.join(process.cwd(), CONFIG_NAME);
374
- const cfg = fs.existsSync(cfgFile) ? JSON.parse(fs.readFileSync(cfgFile, 'utf8')) : {};
375
- fs.writeFileSync(cfgFile, JSON.stringify({ port: cfg.port || 3030, canvasUrl: cfg.canvasUrl || 'http://127.0.0.1:3030', ...cfg, remote: slug }, null, 2) + '\n');
376
- console.log(`Remote target remembered in ${CONFIG_NAME} (agent uses it automatically).`);
377
- }
378
- catch { /* non-fatal */ }
379
- // 4. verify: Pages serves the viewer AND the scene file
380
- const [sOwner, sRepo] = slug.split('/');
381
- let verified = false;
382
- for (let i = 0; i < 12; i++) {
383
- await new Promise((r) => setTimeout(r, 10000));
384
- try {
385
- const res = await fetch(`https://${sOwner}.github.io/${sRepo}/canvas.excalidraw`, { signal: AbortSignal.timeout(15000) });
386
- if (res.ok) {
387
- verified = true;
388
- break;
822
+ if (clack.isCancel(hostAnswer)) {
823
+ clack.cancel('Aborted.');
824
+ process.exit(0);
825
+ }
826
+ const target = hostAnswer;
827
+ // Cloudflare deploys run under the USER's own wrangler identity and quota.
828
+ // Verify login up front (with an offer to log in now) instead of failing
829
+ // minutes later mid-deploy.
830
+ if (target === 'cloudflare') {
831
+ let acct = wranglerAccount();
832
+ if (!acct.ok) {
833
+ const login = await clack.confirm({
834
+ message: 'Wrangler is not logged into Cloudflare. Log in now (opens a browser)?',
835
+ });
836
+ if (clack.isCancel(login)) {
837
+ clack.cancel('Aborted.');
838
+ process.exit(0);
839
+ }
840
+ if (login) {
841
+ spawnSync('npx', [...WRANGLER, 'login'], { stdio: 'inherit', env: process.env });
842
+ acct = wranglerAccount();
843
+ }
844
+ if (!acct.ok) {
845
+ clack.log.error('Cloudflare login is required for Cloudflare Pages hosting — pick GitHub Pages, or run `npx -y wrangler@4 login` first.');
846
+ process.exit(1);
389
847
  }
390
848
  }
391
- catch { /* Pages build still running */ }
849
+ clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
392
850
  }
393
- console.log(verified ? 'Verified: viewer + scene live.' : 'Note: Pages still building — check back in a minute.');
394
- // 4. next steps: install the shared app + draw
395
- const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
396
- console.log(`\nDone. Your canvas: https://${sOwner}.github.io/${sRepo}/\n`);
397
- console.log('Two remaining clicks (one time per repo):');
398
- console.log(` 1. Install the Excalidrop app on this repo:\n https://github.com/apps/${appSlug}/installations/new\n`);
399
- console.log(' 2. In your agent: switch_remote { target: "<that canvas URL>" }');
400
- console.log(' then draw — commits land on GitHub, the viewer updates itself.');
401
- console.log('\nLogin on the page: pill → device code → github.com/login/device (2FA via GitHub).');
402
- }
403
- async function cmdLogin() {
404
- const { APP_CLIENT_ID } = await import('./target.js');
405
- const clientId = process.env.GITHUB_OAUTH_CLIENT_ID || APP_CLIENT_ID;
406
- if (!clientId) {
407
- console.error('No GitHub App Client ID configured yet.');
408
- console.error('Set GITHUB_OAUTH_CLIENT_ID (or wait for the shared excalidrop app),');
409
- console.error('or just run `gh auth login` — the MCP picks that token up automatically.');
851
+ const projectRoot = findProjectRoot(process.cwd());
852
+ // Step 1: pick harnesses. Preselect ones already wired to this repo;
853
+ // fall back to the historical default when nothing is installed yet.
854
+ const detectedHarnesses = HARNESSES.filter((h) => HARNESS_EDITORS[h].some((id) => detectEditorState(id, slug, projectRoot).status === 'current'));
855
+ const harnessAnswer = await clack.multiselect({
856
+ message: 'Install the MCP server into which editors?',
857
+ options: HARNESSES.map((h) => ({
858
+ value: h,
859
+ label: `${HARNESS_LABELS[h]} (${harnessSummary(h, slug, projectRoot)})`,
860
+ })),
861
+ initialValues: detectedHarnesses.length > 0 ? detectedHarnesses : ['claude', 'codex'],
862
+ required: false,
863
+ });
864
+ if (clack.isCancel(harnessAnswer)) {
865
+ clack.cancel('Aborted.');
866
+ process.exit(0);
867
+ }
868
+ const harnesses = harnessAnswer;
869
+ // Step 2: per multi-scope harness, pick where to install.
870
+ const editors = [];
871
+ for (const h of harnesses) {
872
+ const ids = HARNESS_EDITORS[h];
873
+ if (ids.length === 1) {
874
+ editors.push(ids[0]);
875
+ continue;
876
+ }
877
+ const current = ids.filter((id) => detectEditorState(id, slug, projectRoot).status === 'current');
878
+ const scopeAnswer = await clack.select({
879
+ message: `Where to install for ${HARNESS_LABELS[h]}?`,
880
+ initialValue: current.length === 1 ? current[0] : ids[0],
881
+ options: [
882
+ ...ids.map((id) => scopeOption(id, slug, projectRoot)),
883
+ { value: '__both__', label: 'Both scopes' },
884
+ ],
885
+ });
886
+ if (clack.isCancel(scopeAnswer)) {
887
+ clack.cancel('Aborted.');
888
+ process.exit(0);
889
+ }
890
+ if (scopeAnswer === '__both__')
891
+ editors.push(...ids);
892
+ else
893
+ editors.push(scopeAnswer);
894
+ }
895
+ // Stale Claude entries can't be overwritten by `add` (it exits 0 with
896
+ // "already exists" and changes nothing) — offer an explicit repoint before
897
+ // the deploy phase so the install below actually lands.
898
+ for (const id of editors) {
899
+ if (id !== 'claude-user' && id !== 'claude-project')
900
+ continue;
901
+ const st = detectEditorState(id, slug, projectRoot);
902
+ if (st.status !== 'stale')
903
+ continue;
904
+ const ok = await clack.confirm({
905
+ message: `${EDITOR_LABELS[id]} points at ${st.repo ?? 'unknown'} — repoint to ${slug}?`,
906
+ });
907
+ if (clack.isCancel(ok)) {
908
+ clack.cancel('Aborted.');
909
+ process.exit(0);
910
+ }
911
+ if (ok) {
912
+ const scope = id === 'claude-project' ? 'project' : 'user';
913
+ runCapture('claude', ['mcp', 'remove', 'excalidrop', '-s', scope]);
914
+ }
915
+ }
916
+ const s = clack.spinner();
917
+ s.start('Publishing viewer + wiring MCP…');
918
+ let viewerUrl;
919
+ let live = false;
920
+ let installs = [];
921
+ try {
922
+ ({ viewerUrl, live, installs } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors, { fromTui: true }));
923
+ s.stop('Setup complete.');
924
+ }
925
+ catch (e) {
926
+ s.stop('Setup failed: ' + e.message);
410
927
  process.exit(1);
411
928
  }
412
- // Dynamic import to avoid loading MCP deps in CLI path
413
- const mod = await import('./target.js');
414
- console.log('Starting GitHub device login…');
415
- const dev = await mod.deviceStart(clientId);
416
- console.log(`\nOpen ${dev.verification_uri} and enter code: ${dev.user_code}\n`);
417
- const repoId = undefined; // unrestricted; pass --repo owner/repo? (future: parse argv)
418
- const token = await mod.devicePoll(clientId, dev.device_code, dev.interval || 5, 300, repoId);
419
- console.log('Logged in — token stored in ~/.config/excalidrop/gh_token');
420
- void token;
929
+ // Always show what ran where: installing to user scope prints the command
930
+ // itself; file writes print the path; stale entries print the update command.
931
+ for (const r of installs) {
932
+ const target = editorTarget(r.id, projectRoot);
933
+ const cmd = editorCommand(r.id, slug).split('\n').join('\n ');
934
+ if (r.outcome === 'added')
935
+ clack.log.success(`MCP → ${target}:\n ${cmd}\n ${r.detail}`);
936
+ else if (r.outcome === 'already-current')
937
+ clack.log.success(`MCP → ${target}: ${r.detail} — skipped.`);
938
+ else if (r.outcome === 'stale')
939
+ clack.log.warn(`MCP → ${target}: ${r.detail}`);
940
+ else
941
+ clack.log.warn(`MCP → ${target} failed: ${r.detail}`);
942
+ }
943
+ // Post-deploy readability probe: one retry — right after a deploy the
944
+ // first API read intermittently fails at TCP level ("fetch failed")
945
+ // while the retry succeeds, and a false "not readable" scares users.
946
+ let scene = null;
947
+ let sceneError = null;
948
+ for (let attempt = 0; attempt < 2 && !scene; attempt++) {
949
+ if (attempt > 0)
950
+ await new Promise((r) => setTimeout(r, 3000));
951
+ try {
952
+ scene = await getScene(slug);
953
+ }
954
+ catch (e) {
955
+ sceneError = e;
956
+ }
957
+ }
958
+ if (scene) {
959
+ clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
960
+ }
961
+ else {
962
+ clack.log.warn(`Scene not readable yet: ${sceneError?.message || sceneError} — draw once to create it.`);
963
+ }
964
+ if (live)
965
+ clack.log.success('Viewer is serving.');
966
+ else
967
+ clack.log.warn('Viewer was not live after 7 minutes — check the Pages/Cloudflare build, then retry the URL.');
968
+ clack.outro(live ? `Canvas live: ${viewerUrl}` : `Canvas pending: ${viewerUrl}`);
421
969
  }
422
970
  function printHelp() {
423
- console.log(`excalidrop — drop an Excalidraw canvas + MCP server into any project
971
+ console.log(`excalidrop — remote GitHub-backed canvas (no local server)
424
972
 
425
973
  Usage:
426
- npx excalidrop init [--port=N] [--no-prompt] pick a free port, write .excalidrop.json + .mcp.json
427
- npx excalidrop up [--port=N] start this project's canvas server
428
- npx excalidrop setup one-command setup on any repo (gh auth + publish + guide)
429
- npx excalidrop login GitHub device-flow login from the terminal
430
- npx excalidrop pages GitHub-truth + gh-pages publish guide
431
- npx excalidrop remote run remote MCP-v2 (GitHub-backed, multi-project)
432
- npx excalidrop publish publish viewer straight to gh-pages (no Actions)
433
- npx excalidrop mcp run MCP stdio server (used by AI agents)
434
- npx excalidrop status show port + canvas health
435
- npx excalidrop add print AI-agent install commands
436
-
437
- Install:
438
- npm i -D excalidrop && npx excalidrop init
974
+ npx excalidrop interactive setup TUI
975
+ npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,claude-user,codex,cursor,cursor-user,desktop,opencode,opencode-user] [--no-prompt]
976
+ npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
977
+ npx excalidrop login [--repo owner/repo] GitHub device-flow login
978
+ npx excalidrop publish owner/repo [--target ..] redeploy viewer only
979
+ npx excalidrop status show repo + auth + scene
980
+ npx excalidrop add [owner/repo] print editor install commands
439
981
 
440
- Each project gets its own port (scanned free from ${DEFAULT_START_PORT}), so several
441
- checkouts can run concurrently. Agent config needs no hardcoded port.`);
982
+ No devDependency needed. Token: GITHUB_TOKEN → gh auth → ~/.config/excalidrop/gh_token.`);
442
983
  }
443
984
  async function main() {
444
985
  const [cmd, ...rest] = process.argv.slice(2);
986
+ if (!cmd) {
987
+ if (!process.stdout.isTTY) {
988
+ printHelp();
989
+ return;
990
+ }
991
+ await runTui();
992
+ return;
993
+ }
445
994
  switch (cmd) {
446
- case 'init':
447
- await cmdInit(rest);
448
- break;
449
- case 'up':
450
- await cmdUp(rest);
995
+ case 'setup':
996
+ await cmdSetup(rest);
451
997
  break;
452
998
  case 'mcp':
453
999
  cmdMcp();
454
1000
  break;
1001
+ case 'login':
1002
+ await cmdLogin();
1003
+ break;
455
1004
  case 'status':
456
1005
  await cmdStatus();
457
1006
  break;
458
- case 'pages':
459
- await cmdPages(rest);
460
- break;
461
- case 'remote':
462
- cmdRemote();
463
- break;
464
- case 'setup':
465
- await cmdSetup(rest);
1007
+ case 'publish':
1008
+ await cmdSetup(['--no-prompt', ...rest]);
466
1009
  break;
467
- case 'login':
468
- await cmdLogin();
469
- break;
470
- case 'publish': {
471
- const child = spawn('bash', [path.join(__dirname, '../scripts/publish-pages.sh')], { stdio: 'inherit', env: process.env });
472
- child.on('exit', (code) => process.exit(code ?? 0));
1010
+ case 'add': {
1011
+ const root = findProjectRoot(process.cwd());
1012
+ const ids = parseEditorsFlag(rest) || [...EDITOR_IDS];
1013
+ const raw = rest.find((a) => a.includes('/') || a.includes('github.com')) || detectSlug() || 'owner/repo';
1014
+ const repo = normalizeRepoSlug(raw) || raw;
1015
+ console.log('Add excalidrop to your AI agent (no install, always latest):\n');
1016
+ for (const id of ids) {
1017
+ const st = repo.includes('/') ? detectEditorState(id, repo, root) : null;
1018
+ const mark = st?.status === 'current' ? ' (installed)' : st?.status === 'stale' ? ` (installed → ${st.repo ?? 'unknown'})` : '';
1019
+ console.log(`## ${EDITOR_LABELS[id]} → ${editorTarget(id, root)}${mark}\n${editorCommand(id, repo)}\n`);
1020
+ }
473
1021
  break;
474
1022
  }
475
- case 'add':
476
- printAgentCommands();
1023
+ case 'init':
1024
+ case 'up':
1025
+ case 'remote':
1026
+ console.error(`\`excalidrop ${cmd}\` was removed (no local canvas). Run: npx excalidrop setup owner/repo`);
1027
+ process.exit(1);
477
1028
  break;
478
1029
  default:
479
1030
  printHelp();
480
- if (cmd && cmd !== '--help' && cmd !== '-h')
481
- process.exitCode = 1;
1031
+ process.exitCode = 1;
482
1032
  }
483
1033
  }
484
- main().catch((err) => {
485
- console.error(`excalidrop: ${err.message}`);
486
- process.exit(1);
487
- });
1034
+ main().catch((err) => { console.error(`excalidrop: ${err.message}`); process.exit(1); });
488
1035
  //# sourceMappingURL=cli.js.map