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.
- package/README.md +58 -131
- package/dist/cli.d.ts +0 -21
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +488 -439
- package/dist/cli.js.map +1 -1
- package/dist/frontend/assets/{advancedFormat-bKUmsig-.js → advancedFormat-CsxROTkH.js} +1 -1
- package/dist/frontend/assets/{ar-SA-G6X2FPQ2-C7K_UR3P.js → ar-SA-G6X2FPQ2-Bu8X6mOi.js} +1 -1
- package/dist/frontend/assets/arc-B9VNLSzj.js +1 -0
- package/dist/frontend/assets/{az-AZ-76LH7QW2-PkhuRuSr.js → az-AZ-76LH7QW2-DpvzY31_.js} +1 -1
- package/dist/frontend/assets/{bg-BG-XCXSNQG7-CgLRSYTl.js → bg-BG-XCXSNQG7-DCtZnLyo.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-38ab4fdb-BFdBRYSn.js → blockDiagram-38ab4fdb-4cfDkUsw.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-68f4deed-Ynbqr7bQ.js → blockDiagram-68f4deed-CsgWL6uC.js} +1 -1
- package/dist/frontend/assets/{bn-BD-2XOGV67Q-CEvkU0mz.js → bn-BD-2XOGV67Q-B-IdLNKT.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-15b5d702-Dm7F1Pn_.js → c4Diagram-15b5d702-7BAqo4qT.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-3d4e48cf-COkK-e67.js → c4Diagram-3d4e48cf-CDW-O-vg.js} +1 -1
- package/dist/frontend/assets/{ca-ES-6MX7JW3Y-DzP6q9bE.js → ca-ES-6MX7JW3Y-DM8olClc.js} +1 -1
- package/dist/frontend/assets/channel-jdNM4v-F.js +1 -0
- package/dist/frontend/assets/{classDiagram-70f12bd4-CRzulwvH.js → classDiagram-70f12bd4-D2U4i4yC.js} +1 -1
- package/dist/frontend/assets/{classDiagram-d40c83e7-DNbA-n-a.js → classDiagram-d40c83e7-DtBHRJc9.js} +1 -1
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-Drykcxns.js +2 -0
- package/dist/frontend/assets/classDiagram-v2-f2320105-D8ruMBA6.js +2 -0
- package/dist/frontend/assets/clone-DFO_zGRO.js +1 -0
- package/dist/frontend/assets/{createText-2e5e7dd3-DB_k52Pn.js → createText-2e5e7dd3-1KPUhDpM.js} +1 -1
- package/dist/frontend/assets/{createText-d213de94-D-G49giw.js → createText-d213de94-C6EhGbUN.js} +4 -4
- package/dist/frontend/assets/{cs-CZ-2BRQDIVT-Bf4TOzpR.js → cs-CZ-2BRQDIVT-DgjTgz6K.js} +1 -1
- package/dist/frontend/assets/{cytoscape-cose-bilkent-DQYypj0P.js → cytoscape-cose-bilkent-BnIHl72R.js} +1 -1
- package/dist/frontend/assets/{da-DK-5WZEPLOC-jO-5kyXb.js → da-DK-5WZEPLOC-uEs3xGKF.js} +1 -1
- package/dist/frontend/assets/{de-DE-XR44H4JA-BkNXWOVh.js → de-DE-XR44H4JA-CBglLw42.js} +1 -1
- package/dist/frontend/assets/drop-BFhuMYSw.png +0 -0
- package/dist/frontend/assets/{edges-332bd1c7-CVvMtkTa.js → edges-332bd1c7-cWCBwcrg.js} +1 -1
- package/dist/frontend/assets/{edges-e0da2a9e-DQBAvC8-.js → edges-e0da2a9e-DKeDUJ_F.js} +1 -1
- package/dist/frontend/assets/{el-GR-BZB4AONW-Dv7Y9hMk.js → el-GR-BZB4AONW-B-_eub00.js} +1 -1
- package/dist/frontend/assets/{elk.bundled-YVS3PsMH.js → elk.bundled-Ch9TeAu4.js} +1 -1
- package/dist/frontend/assets/{erDiagram-880f2ed8-BjBeR38f.js → erDiagram-880f2ed8-CtDf_58r.js} +4 -4
- package/dist/frontend/assets/{erDiagram-9861fffd-B2Bi3R45.js → erDiagram-9861fffd-CAlUeljk.js} +1 -1
- package/dist/frontend/assets/{es-ES-U4NZUMDT-CWhs0SQY.js → es-ES-U4NZUMDT-BIN3bSVD.js} +1 -1
- package/dist/frontend/assets/{eu-ES-A7QVB2H4-BfkFwc3e.js → eu-ES-A7QVB2H4-TyQet3-N.js} +1 -1
- package/dist/frontend/assets/{fa-IR-HGAKTJCU-BgWzSj46.js → fa-IR-HGAKTJCU-DVmjjr57.js} +1 -1
- package/dist/frontend/assets/{fi-FI-Z5N7JZ37-CUyvkum-.js → fi-FI-Z5N7JZ37-ZW9kXLa8.js} +1 -1
- package/dist/frontend/assets/{flowDb-7c981674-BSZyyVBf.js → flowDb-7c981674-B5ppx8EN.js} +1 -1
- package/dist/frontend/assets/{flowDb-956e92f1-BC3-CLkF.js → flowDb-956e92f1-Dqi00Wq-.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-66a62f08-H57DAvmM.js → flowDiagram-66a62f08-v3Liu0m_.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-cbd28bf7-Bw4Z2nBD.js → flowDiagram-cbd28bf7-DJ_YHRwa.js} +3 -3
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-6vlgZ0Bg.js +1 -0
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-FmnI78Vm.js +1 -0
- package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-1kZNdf3X.js → flowchart-elk-definition-36e2d292-BleYul6b.js} +2 -2
- package/dist/frontend/assets/{flowchart-elk-definition-4a651766-DDhG2Esi.js → flowchart-elk-definition-4a651766-D6lLh4Ih.js} +1 -1
- package/dist/frontend/assets/{fr-FR-RHASNOE6-7xzoRwbd.js → fr-FR-RHASNOE6-CxLR66tH.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-04f9e578-CtUSsXCz.js → ganttDiagram-04f9e578-DJSOyr8B.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-c361ad54-K42AklFY.js → ganttDiagram-c361ad54-CJmbCLvp.js} +1 -1
- package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-uW6XA6Y6.js → gitGraphDiagram-21fc4d3e-DiRq_FJc.js} +6 -6
- package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-CCNbhnBg.js → gitGraphDiagram-72cf32ee-MfvDzoAb.js} +1 -1
- package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CXuDDwNP.js → gl-ES-HMX3MZ6V-CxWj2itK.js} +1 -1
- package/dist/frontend/assets/{graph-CYOmcSYN.js → graph-CT_r46y9.js} +1 -1
- package/dist/frontend/assets/{graph-o42dOfI7.js → graph-DJt_Vezo.js} +1 -1
- package/dist/frontend/assets/{he-IL-6SHJWFNN-O9dzh9EM.js → he-IL-6SHJWFNN-5v2WFGO3.js} +1 -1
- package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DlrOXhWs.js → hi-IN-IWLTKZ5I-BJGbvgRu.js} +1 -1
- package/dist/frontend/assets/{hu-HU-A5ZG7DT2-C3eNg-uI.js → hu-HU-A5ZG7DT2-REQT2EcE.js} +1 -1
- package/dist/frontend/assets/{id-ID-SAP4L64H-6iM4pI5c.js → id-ID-SAP4L64H-aZWQYWdg.js} +1 -1
- package/dist/frontend/assets/{index-3862675e-DrvLhypC.js → index-3862675e-BgrvgbsM.js} +1 -1
- package/dist/frontend/assets/{index-6079d271-BgVTRqg2.js → index-6079d271-Cp8jFHTC.js} +1 -1
- package/dist/frontend/assets/{index-HkUXslJy.js → index-DBHHeSXy.js} +4 -4
- package/dist/frontend/assets/index-GTq6y6g6.js +349 -0
- package/dist/frontend/assets/{infoDiagram-4a4f5b27-ZxmAcaW1.js → infoDiagram-4a4f5b27-C1ZJJtWv.js} +3 -3
- package/dist/frontend/assets/{infoDiagram-f8f76790-Bn-PhiFG.js → infoDiagram-f8f76790-GeS0JKxo.js} +1 -1
- package/dist/frontend/assets/{it-IT-JPQ66NNP-B26NQzGp.js → it-IT-JPQ66NNP-EyeA2Ty1.js} +1 -1
- package/dist/frontend/assets/{ja-JP-DBVTYXUO-CB0fvnYJ.js → ja-JP-DBVTYXUO-BLcE45D9.js} +1 -1
- package/dist/frontend/assets/{journeyDiagram-29694f62-ggDKs06f.js → journeyDiagram-29694f62-4Z_lpK4g.js} +5 -5
- package/dist/frontend/assets/{journeyDiagram-49397b02-pdDS-n-z.js → journeyDiagram-49397b02-ChwU3Zo6.js} +1 -1
- package/dist/frontend/assets/{kaa-6HZHGXH3-YNdBdcwJ.js → kaa-6HZHGXH3-CkpTcX9X.js} +1 -1
- package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-D86pwZO0.js → kab-KAB-ZGHBKWFO-CChq0y6o.js} +1 -1
- package/dist/frontend/assets/{kk-KZ-P5N5QNE5-CpG5oheM.js → kk-KZ-P5N5QNE5-CwtoTY4z.js} +1 -1
- package/dist/frontend/assets/{km-KH-HSX4SM5Z-DYOZ3Azw.js → km-KH-HSX4SM5Z-D26sdohj.js} +1 -1
- package/dist/frontend/assets/{ko-KR-MTYHY66A-BHorm6lC.js → ko-KR-MTYHY66A-DvnDcJQA.js} +1 -1
- package/dist/frontend/assets/{ku-TR-6OUDTVRD-CM9RgEHv.js → ku-TR-6OUDTVRD-qdHbLgKg.js} +1 -1
- package/dist/frontend/assets/{layout-DW9Qf-At.js → layout-C-wUeNvb.js} +1 -1
- package/dist/frontend/assets/{layout-CCtkxrAf.js → layout-YRLNN4au.js} +1 -1
- package/dist/frontend/assets/{line-DvZfgg9P.js → line-BEgMClKG.js} +1 -1
- package/dist/frontend/assets/{linear-FwFrGRfa.js → linear-DDHpSdYw.js} +1 -1
- package/dist/frontend/assets/{lt-LT-XHIRWOB4-DvriKXBj.js → lt-LT-XHIRWOB4-DT4LYe_d.js} +1 -1
- package/dist/frontend/assets/{lv-LV-5QDEKY6T-DzQpwk87.js → lv-LV-5QDEKY6T-Dbc3ivwT.js} +1 -1
- package/dist/frontend/assets/{mindmap-definition-ac74a2e8-DsRo8f2w.js → mindmap-definition-ac74a2e8-CY2RhWUN.js} +6 -6
- package/dist/frontend/assets/{mindmap-definition-fc14e90a-dQ9reluI.js → mindmap-definition-fc14e90a-VT4JDi6N.js} +1 -1
- package/dist/frontend/assets/{mr-IN-CRQNXWMA-C7RzxxP5.js → mr-IN-CRQNXWMA-D75p1fVc.js} +1 -1
- package/dist/frontend/assets/{my-MM-5M5IBNSE-B-RRuy29.js → my-MM-5M5IBNSE-DnEoBXyE.js} +1 -1
- package/dist/frontend/assets/{nb-NO-T6EIAALU-B47x16Ik.js → nb-NO-T6EIAALU-BL4ThJxl.js} +1 -1
- package/dist/frontend/assets/{nl-NL-IS3SIHDZ-BxAKoQne.js → nl-NL-IS3SIHDZ-u18nUblY.js} +1 -1
- package/dist/frontend/assets/{nn-NO-6E72VCQL-dfpbEDSZ.js → nn-NO-6E72VCQL-B9LJ0oXc.js} +1 -1
- package/dist/frontend/assets/{oc-FR-POXYY2M6-gv6geBjV.js → oc-FR-POXYY2M6-BKG5kIcO.js} +1 -1
- package/dist/frontend/assets/{pa-IN-N4M65BXN-CY30-qH7.js → pa-IN-N4M65BXN-CmpnjbkL.js} +1 -1
- package/dist/frontend/assets/{pica-DWYGHLQV.js → pica-BcpcM0v1.js} +1 -1
- package/dist/frontend/assets/{pie-CDiWh1rj.js → pie-Swzl5NrI.js} +1 -1
- package/dist/frontend/assets/{pieDiagram-421022e6-Bgy-duyf.js → pieDiagram-421022e6-ndz96BLe.js} +5 -5
- package/dist/frontend/assets/{pieDiagram-8a3498a8-DH7FsbPK.js → pieDiagram-8a3498a8-D5rcV68O.js} +1 -1
- package/dist/frontend/assets/{pl-PL-T2D74RX3-TrHugsIA.js → pl-PL-T2D74RX3-CrR66Rgj.js} +1 -1
- package/dist/frontend/assets/{pt-BR-5N22H2LF-CJxdFp9c.js → pt-BR-5N22H2LF-Crv7jZiH.js} +1 -1
- package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CB-1q6YJ.js → pt-PT-UZXXM6DQ-Bkaa5ESR.js} +1 -1
- package/dist/frontend/assets/{quadrantDiagram-0957ecba-DcwYI1oi.js → quadrantDiagram-0957ecba-DOdyxeTP.js} +3 -3
- package/dist/frontend/assets/{quadrantDiagram-120e2f19-CvHU5a9v.js → quadrantDiagram-120e2f19-beQD7R1C.js} +1 -1
- package/dist/frontend/assets/relay-BIOL2uw2.js +1 -0
- package/dist/frontend/assets/{requirementDiagram-23d650b8-tdagN8HP.js → requirementDiagram-23d650b8-B2i-OB97.js} +1 -1
- package/dist/frontend/assets/{requirementDiagram-deff3bca-BMGM-fSI.js → requirementDiagram-deff3bca-beS-SvfY.js} +1 -1
- package/dist/frontend/assets/{ro-RO-JPDTUUEW-DQq9DBOd.js → ro-RO-JPDTUUEW-DsrnQm7K.js} +1 -1
- package/dist/frontend/assets/{ru-RU-B4JR7IUQ-CWX32rfh.js → ru-RU-B4JR7IUQ-DrnGGdUs.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-04a897e0-3i6zWpY2.js → sankeyDiagram-04a897e0-CVad45bN.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-23345273-ncj1Ol7S.js → sankeyDiagram-23345273-ghWBXXF2.js} +1 -1
- package/dist/frontend/assets/{selectAll-DJwesbwT.js → selectAll-XplaO988.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-17ac3bff-ChyKKRZA.js → sequenceDiagram-17ac3bff-DBI-EdWx.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-704730f1-D57_wP9k.js → sequenceDiagram-704730f1-BnJ__mBt.js} +1 -1
- package/dist/frontend/assets/{si-LK-N5RQ5JYF-Lkc5ZxyG.js → si-LK-N5RQ5JYF-BxAwuRM5.js} +1 -1
- package/dist/frontend/assets/{sk-SK-C5VTKIMK-DyvYXjqZ.js → sk-SK-C5VTKIMK-CnDyiLRf.js} +1 -1
- package/dist/frontend/assets/{sl-SI-NN7IZMDC-D_MvP9Rh.js → sl-SI-NN7IZMDC-BXHXIdFA.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-587899a1-OjFTnFLS.js → stateDiagram-587899a1-D3Ahxsgc.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-9c5f0230-BzU36S5C.js → stateDiagram-9c5f0230-8XbLWvC0.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-CfX1qo7U.js → stateDiagram-v2-51a3dcff-Cqf-MUGD.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-BH7Ud4sU.js → stateDiagram-v2-d93cdb3a-DjGrud9D.js} +1 -1
- package/dist/frontend/assets/{styles-2ab5d517-CA8x9Nh5.js → styles-2ab5d517-B4qdHOgu.js} +3 -3
- package/dist/frontend/assets/{styles-5f03d8d2-BnO2pLhV.js → styles-5f03d8d2-DOD2v5V8.js} +3 -3
- package/dist/frontend/assets/{styles-6aaf32cf-COnot1ea.js → styles-6aaf32cf-bP3XXcmf.js} +1 -1
- package/dist/frontend/assets/{styles-9a916d00-C3QkBgTF.js → styles-9a916d00-QLeuL_Dy.js} +1 -1
- package/dist/frontend/assets/{styles-c10674c1-CPAG2_rr.js → styles-c10674c1-Lpvh1u4f.js} +1 -1
- package/dist/frontend/assets/{styles-edf9a4b0-B8Q9fyu0.js → styles-edf9a4b0-D4W9aPm7.js} +1 -1
- package/dist/frontend/assets/subset-shared.chunk.js +9 -9
- package/dist/frontend/assets/subset-worker.chunk.js +1 -1
- package/dist/frontend/assets/{sv-SE-XGPEYMSR-t6voGQSI.js → sv-SE-XGPEYMSR-YrCQQBuo.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-08f97a94-CdT21LWf.js → svgDrawCommon-08f97a94-CwNv3PjS.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-3ba9043b-D1MU7jAE.js → svgDrawCommon-3ba9043b-DZbT0Bqu.js} +1 -1
- package/dist/frontend/assets/{ta-IN-2NMHFXQM-7VmfDDyV.js → ta-IN-2NMHFXQM-qR82aBSW.js} +1 -1
- package/dist/frontend/assets/{th-TH-HPSO5L25-B5rLEY9P.js → th-TH-HPSO5L25-C65V7hE5.js} +1 -1
- package/dist/frontend/assets/{timeline-definition-7e6b55e7-BIcL44O_.js → timeline-definition-7e6b55e7-GkPy3QeG.js} +3 -3
- package/dist/frontend/assets/{timeline-definition-85554ec2-TgrYYwpU.js → timeline-definition-85554ec2-CCdkBaC0.js} +3 -3
- package/dist/frontend/assets/{tr-TR-DEFEU3FU-0CgOHXTn.js → tr-TR-DEFEU3FU-CFzbwAKV.js} +1 -1
- package/dist/frontend/assets/{uk-UA-QMV73CPH-CzSFsIsx.js → uk-UA-QMV73CPH-ePt5WVz3.js} +1 -1
- package/dist/frontend/assets/{union-CnQAJu0l.js → union-CVZobYBh.js} +1 -1
- package/dist/frontend/assets/{vi-VN-M7AON7JQ-Bhx3rYU9.js → vi-VN-M7AON7JQ-CaPKykgJ.js} +1 -1
- package/dist/frontend/assets/{xychartDiagram-b6496bcd-BMKDop6v.js → xychartDiagram-b6496bcd-CV5WRc81.js} +4 -4
- package/dist/frontend/assets/{xychartDiagram-e933f94c-DAWithrN.js → xychartDiagram-e933f94c-BH-ySdQf.js} +1 -1
- package/dist/frontend/assets/{zh-CN-LNUGB5OW-Br52_wH4.js → zh-CN-LNUGB5OW-CK8AEj8o.js} +1 -1
- package/dist/frontend/assets/{zh-HK-E62DVLB3-Cj6mnIQ9.js → zh-HK-E62DVLB3-CUINiFgH.js} +1 -1
- package/dist/frontend/assets/{zh-TW-RAJ6MFWO-yLEsVBbC.js → zh-TW-RAJ6MFWO-BEBY7iJR.js} +1 -1
- package/dist/frontend/assets/zipObject-W8SLG3F1.js +1 -0
- package/dist/frontend/index.html +168 -235
- package/dist/{index.d.ts → mcp.d.ts} +1 -1
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1183 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +19 -34
- package/skills/excalidraw-skill/SKILL.md +42 -55
- package/skills/excalidraw-skill/references/cheatsheet.md +23 -56
- package/skills/excalidraw-skill/scripts/clear-canvas.cjs +9 -32
- package/skills/excalidraw-skill/scripts/create-element.cjs +18 -61
- package/skills/excalidraw-skill/scripts/delete-element.cjs +12 -42
- package/skills/excalidraw-skill/scripts/export-elements.cjs +17 -44
- package/skills/excalidraw-skill/scripts/gh-helper.cjs +58 -0
- package/skills/excalidraw-skill/scripts/healthcheck.cjs +8 -29
- package/skills/excalidraw-skill/scripts/import-elements.cjs +20 -70
- package/skills/excalidraw-skill/scripts/update-element.cjs +19 -63
- package/tsconfig.json +3 -2
- package/vite.config.js +3 -12
- package/dist/frontend/assets/arc-BI_FJWE7.js +0 -1
- package/dist/frontend/assets/channel-BqCiG5n3.js +0 -1
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-l-GglywH.js +0 -2
- package/dist/frontend/assets/classDiagram-v2-f2320105-B_TY1zoc.js +0 -2
- package/dist/frontend/assets/clone-B6Jqcrku.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CBjbm_oB.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CoQufPp2.js +0 -1
- package/dist/frontend/assets/index-D-cr7jpJ.js +0 -349
- package/dist/frontend/assets/zipObject-BeXj5PVd.js +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2305
- package/dist/index.js.map +0 -1
- package/dist/remote.d.ts +0 -5
- package/dist/remote.d.ts.map +0 -1
- package/dist/remote.js +0 -129
- package/dist/remote.js.map +0 -1
- package/dist/server.d.ts +0 -4
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -1217
- package/dist/server.js.map +0 -1
- package/dist/target.d.ts +0 -41
- package/dist/target.d.ts.map +0 -1
- package/dist/target.js +0 -244
- package/dist/target.js.map +0 -1
- package/dist/types.d.ts +0 -228
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -53
- package/dist/types.js.map +0 -1
- package/dist/utils/auth.d.ts +0 -15
- package/dist/utils/auth.d.ts.map +0 -1
- package/dist/utils/auth.js +0 -68
- package/dist/utils/auth.js.map +0 -1
- package/dist/utils/github.d.ts +0 -26
- package/dist/utils/github.d.ts.map +0 -1
- package/dist/utils/github.js +0 -111
- package/dist/utils/github.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -4
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -25
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/r2store.d.ts +0 -19
- package/dist/utils/r2store.d.ts.map +0 -1
- package/dist/utils/r2store.js +0 -75
- package/dist/utils/r2store.js.map +0 -1
- package/frontend/index.html +0 -338
- package/frontend/src/App.tsx +0 -1091
- package/frontend/src/main.tsx +0 -15
- package/frontend/src/utils/ghSync.ts +0 -171
- package/frontend/src/utils/mermaidConverter.ts +0 -49
- package/frontend/src/utils/webmcp.ts +0 -285
- package/scripts/check-local-bind.mjs +0 -164
- package/scripts/postinstall.mjs +0 -23
- package/scripts/publish-pages.sh +0 -39
- package/scripts/seed-r2.mjs +0 -9
- package/src/cli.ts +0 -559
- package/src/index.ts +0 -2557
- package/src/remote.ts +0 -92
- package/src/server.ts +0 -1349
- package/src/target.ts +0 -208
- package/src/types.ts +0 -338
- package/src/utils/auth.ts +0 -61
- package/src/utils/github.ts +0 -93
- package/src/utils/logger.ts +0 -35
- 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
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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('
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
113
|
-
if (!url)
|
|
114
|
-
return undefined;
|
|
166
|
+
async function isPrivateRepo(repo) {
|
|
115
167
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
|
|
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
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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(`
|
|
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:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
409
|
+
async function runTui() {
|
|
410
|
+
let clack;
|
|
411
|
+
try {
|
|
412
|
+
clack = await import('@clack/prompts');
|
|
313
413
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
|
417
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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 —
|
|
528
|
+
console.log(`excalidrop — remote GitHub-backed canvas (no local server)
|
|
468
529
|
|
|
469
530
|
Usage:
|
|
470
|
-
npx excalidrop
|
|
471
|
-
npx excalidrop
|
|
472
|
-
npx
|
|
473
|
-
npx excalidrop login
|
|
474
|
-
npx excalidrop
|
|
475
|
-
npx excalidrop
|
|
476
|
-
npx excalidrop
|
|
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
|
-
|
|
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 '
|
|
492
|
-
await
|
|
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 '
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|