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.
- package/README.md +70 -125
- package/dist/cli.d.ts +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +936 -389
- package/dist/cli.js.map +1 -1
- package/dist/frontend/assets/{advancedFormat-tiBAHNGo.js → advancedFormat-Be4KR5mp.js} +1 -1
- package/dist/frontend/assets/{ar-SA-G6X2FPQ2-DVxiWV5Z.js → ar-SA-G6X2FPQ2-B6ekz3Ba.js} +1 -1
- package/dist/frontend/assets/arc-DlgXyGPj.js +1 -0
- package/dist/frontend/assets/{az-AZ-76LH7QW2-Z_2BqDo-.js → az-AZ-76LH7QW2-01MDUsF0.js} +1 -1
- package/dist/frontend/assets/{bg-BG-XCXSNQG7-DrxGJZOG.js → bg-BG-XCXSNQG7--fArB8Ao.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-38ab4fdb-CDnkTFfP.js → blockDiagram-38ab4fdb-CmsVhOwi.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-68f4deed-Bb0W6mxN.js → blockDiagram-68f4deed-DU_wNYlO.js} +6 -6
- package/dist/frontend/assets/{bn-BD-2XOGV67Q-BkGJ-w8k.js → bn-BD-2XOGV67Q-h8juqzfQ.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-15b5d702-ByRV0tka.js → c4Diagram-15b5d702-ClZalz0L.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-3d4e48cf-QSVnzYYK.js → c4Diagram-3d4e48cf-BkFVM0tP.js} +1 -1
- package/dist/frontend/assets/{ca-ES-6MX7JW3Y-BBJvOPE1.js → ca-ES-6MX7JW3Y-DMBR3qVX.js} +1 -1
- package/dist/frontend/assets/channel-BxFR7168.js +1 -0
- package/dist/frontend/assets/{classDiagram-70f12bd4-DDFBNGUt.js → classDiagram-70f12bd4-BXAJnakg.js} +1 -1
- package/dist/frontend/assets/{classDiagram-d40c83e7-cgC3s7Nz.js → classDiagram-d40c83e7-DVhipa45.js} +1 -1
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-CiHMsbhj.js +2 -0
- package/dist/frontend/assets/{classDiagram-v2-f2320105-Dtywkpeg.js → classDiagram-v2-f2320105-1CqIxWOB.js} +2 -2
- package/dist/frontend/assets/clone-CMl85WSi.js +1 -0
- package/dist/frontend/assets/{createText-2e5e7dd3-CqY00v-4.js → createText-2e5e7dd3-D3kSz3YE.js} +1 -1
- package/dist/frontend/assets/{createText-d213de94-D59sBfqq.js → createText-d213de94-DvJeLlQX.js} +4 -4
- package/dist/frontend/assets/{cs-CZ-2BRQDIVT-CK09MAsg.js → cs-CZ-2BRQDIVT-BUcdPrAr.js} +1 -1
- package/dist/frontend/assets/{cytoscape-cose-bilkent-CKy_tuGZ.js → cytoscape-cose-bilkent-CKoSm_2z.js} +1 -1
- package/dist/frontend/assets/{da-DK-5WZEPLOC-V4hdLKuT.js → da-DK-5WZEPLOC-C1nkchuC.js} +1 -1
- package/dist/frontend/assets/{de-DE-XR44H4JA-Dw8qkP9g.js → de-DE-XR44H4JA-DO2_bO1X.js} +1 -1
- package/dist/frontend/assets/{edges-332bd1c7-D-u0KMZB.js → edges-332bd1c7-C5qM1sDU.js} +1 -1
- package/dist/frontend/assets/{edges-e0da2a9e-B3UFqLqY.js → edges-e0da2a9e-BRt7qomS.js} +1 -1
- package/dist/frontend/assets/{el-GR-BZB4AONW-BUdBQbYg.js → el-GR-BZB4AONW-CrjSdaU2.js} +1 -1
- package/dist/frontend/assets/{elk.bundled-BZA3Xcoo.js → elk.bundled-Dmec6Rq6.js} +1 -1
- package/dist/frontend/assets/{erDiagram-880f2ed8-CNxm7e1x.js → erDiagram-880f2ed8-BI7kP523.js} +4 -4
- package/dist/frontend/assets/{erDiagram-9861fffd-C2u_ziAM.js → erDiagram-9861fffd-DMyCDrgI.js} +1 -1
- package/dist/frontend/assets/{es-ES-U4NZUMDT-DS0FAZqX.js → es-ES-U4NZUMDT-rrdjPi-M.js} +1 -1
- package/dist/frontend/assets/{eu-ES-A7QVB2H4-D-D5uidc.js → eu-ES-A7QVB2H4-uYn3S3D5.js} +1 -1
- package/dist/frontend/assets/{fa-IR-HGAKTJCU-mAW_2-jg.js → fa-IR-HGAKTJCU-DWW_BVFR.js} +1 -1
- package/dist/frontend/assets/{fi-FI-Z5N7JZ37-B9mW6nxS.js → fi-FI-Z5N7JZ37-CMXxeZ4W.js} +1 -1
- package/dist/frontend/assets/{flowDb-7c981674-6JB2gd0I.js → flowDb-7c981674-Dsj6xMzr.js} +1 -1
- package/dist/frontend/assets/{flowDb-956e92f1-BbpkWRi7.js → flowDb-956e92f1-26g7kJju.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-66a62f08-DrhrQuXc.js → flowDiagram-66a62f08-C9eQZtdf.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-cbd28bf7-BmNY7bvJ.js → flowDiagram-cbd28bf7-D051xFrI.js} +3 -3
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-DIa4RdMh.js +1 -0
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CnvFT8HV.js +1 -0
- package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-4G1YiIlk.js → flowchart-elk-definition-36e2d292-C9fsvhV-.js} +2 -2
- package/dist/frontend/assets/{flowchart-elk-definition-4a651766-Cm94SrrQ.js → flowchart-elk-definition-4a651766-BkVNuJOw.js} +1 -1
- package/dist/frontend/assets/{fr-FR-RHASNOE6-BXnmUxTA.js → fr-FR-RHASNOE6-DIfK_rjW.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-04f9e578-X0BweAnG.js → ganttDiagram-04f9e578-CZeF2a4Y.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-c361ad54-D3vxZ5MQ.js → ganttDiagram-c361ad54-DbkIZ-wU.js} +1 -1
- package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-wznrpRWn.js → gitGraphDiagram-21fc4d3e-CLqlSEuE.js} +6 -6
- package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-B9zCS3Pw.js → gitGraphDiagram-72cf32ee-Cvf9uYWB.js} +1 -1
- package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CrUx-ab6.js → gl-ES-HMX3MZ6V-CCxCH2oK.js} +1 -1
- package/dist/frontend/assets/{graph-BQZ6SIVr.js → graph-MptL1QoF.js} +1 -1
- package/dist/frontend/assets/{graph-Cda8xSTx.js → graph-Mzuu2QFH.js} +1 -1
- package/dist/frontend/assets/{he-IL-6SHJWFNN-D33eaw6-.js → he-IL-6SHJWFNN-CJuJutJZ.js} +1 -1
- package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DEnStF-g.js → hi-IN-IWLTKZ5I-BORPH6vU.js} +1 -1
- package/dist/frontend/assets/{hu-HU-A5ZG7DT2-BjH2w8Sn.js → hu-HU-A5ZG7DT2-Dr7UDbjo.js} +1 -1
- package/dist/frontend/assets/{id-ID-SAP4L64H-Bc6R-JEz.js → id-ID-SAP4L64H-D0Widkq7.js} +1 -1
- package/dist/frontend/assets/{index-3862675e-CpYfS3Db.js → index-3862675e-DVT5-vN2.js} +1 -1
- package/dist/frontend/assets/{index-6079d271-Y2bgyRpa.js → index-6079d271-R4KRnW5Z.js} +1 -1
- package/dist/frontend/assets/{index-D7eYnAQp.js → index-B45XzZiR.js} +4 -4
- package/dist/frontend/assets/index-Bd-8869C.js +349 -0
- package/dist/frontend/assets/{infoDiagram-4a4f5b27-Bm3Oq-Rq.js → infoDiagram-4a4f5b27-BvY1qS17.js} +3 -3
- package/dist/frontend/assets/{infoDiagram-f8f76790-oNVitBKM.js → infoDiagram-f8f76790-D60YvG33.js} +1 -1
- package/dist/frontend/assets/{it-IT-JPQ66NNP-BcxQABF2.js → it-IT-JPQ66NNP-C08Vu0BG.js} +1 -1
- package/dist/frontend/assets/{ja-JP-DBVTYXUO-D1zIM-nt.js → ja-JP-DBVTYXUO-Ciu7LaRi.js} +1 -1
- package/dist/frontend/assets/{journeyDiagram-29694f62-CamPk_3P.js → journeyDiagram-29694f62-DvrnJXsd.js} +5 -5
- package/dist/frontend/assets/{journeyDiagram-49397b02-D6w3tLtf.js → journeyDiagram-49397b02-Dy9LU86E.js} +1 -1
- package/dist/frontend/assets/{kaa-6HZHGXH3-CoILysVg.js → kaa-6HZHGXH3-GhGTIjpu.js} +1 -1
- package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-B9ZbybBt.js → kab-KAB-ZGHBKWFO-B3q4BqYw.js} +1 -1
- package/dist/frontend/assets/{kk-KZ-P5N5QNE5-Dxi4GIQ7.js → kk-KZ-P5N5QNE5-GWoLZvco.js} +1 -1
- package/dist/frontend/assets/{km-KH-HSX4SM5Z-DaJ7EtO2.js → km-KH-HSX4SM5Z-CnAQJCYr.js} +1 -1
- package/dist/frontend/assets/{ko-KR-MTYHY66A-DG57Ah12.js → ko-KR-MTYHY66A-B2Rh9krp.js} +1 -1
- package/dist/frontend/assets/{ku-TR-6OUDTVRD-DnDf9E8a.js → ku-TR-6OUDTVRD-C27elmON.js} +1 -1
- package/dist/frontend/assets/{layout-Drr-66M9.js → layout-D1hl6oKi.js} +1 -1
- package/dist/frontend/assets/{layout-BsWfhUAF.js → layout-c-hOA8zE.js} +1 -1
- package/dist/frontend/assets/{line-DwQObOhP.js → line-DtMz1ssZ.js} +1 -1
- package/dist/frontend/assets/{linear-C36MjJps.js → linear-D-Y4WtO_.js} +1 -1
- package/dist/frontend/assets/{lt-LT-XHIRWOB4-D_zqS0yg.js → lt-LT-XHIRWOB4-B6jd693w.js} +1 -1
- package/dist/frontend/assets/{lv-LV-5QDEKY6T-owIsIh1b.js → lv-LV-5QDEKY6T-sXB-8i6R.js} +1 -1
- package/dist/frontend/assets/{mindmap-definition-ac74a2e8-Cf9JbrBJ.js → mindmap-definition-ac74a2e8-CawO9I_f.js} +6 -6
- package/dist/frontend/assets/{mindmap-definition-fc14e90a-BpsNCNCp.js → mindmap-definition-fc14e90a-CXMNA68v.js} +4 -4
- package/dist/frontend/assets/{mr-IN-CRQNXWMA-CS9XUj74.js → mr-IN-CRQNXWMA-BJ6QSiPi.js} +1 -1
- package/dist/frontend/assets/{my-MM-5M5IBNSE-BKB060k9.js → my-MM-5M5IBNSE-BMdOIRP5.js} +1 -1
- package/dist/frontend/assets/{nb-NO-T6EIAALU-CkmLUrvm.js → nb-NO-T6EIAALU-BVG6JUMN.js} +1 -1
- package/dist/frontend/assets/{nl-NL-IS3SIHDZ-DdiLyJoz.js → nl-NL-IS3SIHDZ-ZprCvTYR.js} +1 -1
- package/dist/frontend/assets/{nn-NO-6E72VCQL-Yevp6f_Y.js → nn-NO-6E72VCQL-CcljFAGy.js} +1 -1
- package/dist/frontend/assets/{oc-FR-POXYY2M6-D_M6GHup.js → oc-FR-POXYY2M6-zCuu6Ns2.js} +1 -1
- package/dist/frontend/assets/{pa-IN-N4M65BXN-1vuACN9n.js → pa-IN-N4M65BXN-D_iftL9v.js} +1 -1
- package/dist/frontend/assets/{pica-DDDx0iqx.js → pica-DsW4eP6T.js} +1 -1
- package/dist/frontend/assets/{pie-CFIpuQMO.js → pie-U_uHwWsL.js} +1 -1
- package/dist/frontend/assets/{pieDiagram-421022e6-DO7e_Qgw.js → pieDiagram-421022e6-naaI7HiE.js} +5 -5
- package/dist/frontend/assets/{pieDiagram-8a3498a8-BEoqChXo.js → pieDiagram-8a3498a8-m6nTf1O_.js} +1 -1
- package/dist/frontend/assets/{pl-PL-T2D74RX3-Cc6Q5nSh.js → pl-PL-T2D74RX3-CRQQfZUe.js} +1 -1
- package/dist/frontend/assets/{pt-BR-5N22H2LF-3eEiAax3.js → pt-BR-5N22H2LF-DLvfG7RR.js} +1 -1
- package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CgnF0Le7.js → pt-PT-UZXXM6DQ-Chkg6-JC.js} +1 -1
- package/dist/frontend/assets/{quadrantDiagram-0957ecba-C1Eo-ZUY.js → quadrantDiagram-0957ecba-BJR82kAx.js} +3 -3
- package/dist/frontend/assets/{quadrantDiagram-120e2f19-Bdfsl_O7.js → quadrantDiagram-120e2f19-CsWgLp5e.js} +1 -1
- package/dist/frontend/assets/relay-BSVGqyIy.js +2 -0
- package/dist/frontend/assets/{requirementDiagram-23d650b8-DiOK6QW3.js → requirementDiagram-23d650b8-CHmg-ycX.js} +1 -1
- package/dist/frontend/assets/{requirementDiagram-deff3bca-BDJ-7T4h.js → requirementDiagram-deff3bca-LIsyxsZZ.js} +1 -1
- package/dist/frontend/assets/{ro-RO-JPDTUUEW-CbJ3IB7a.js → ro-RO-JPDTUUEW-B_a5A5-x.js} +1 -1
- package/dist/frontend/assets/{ru-RU-B4JR7IUQ-PQGmeGG4.js → ru-RU-B4JR7IUQ-CY5sI1Ru.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-04a897e0-DV25bgN2.js → sankeyDiagram-04a897e0-DN5e16d0.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-23345273-szSb_Sei.js → sankeyDiagram-23345273-BBjQCtHz.js} +1 -1
- package/dist/frontend/assets/{selectAll-DsrPvhqV.js → selectAll-Dcq-dC_E.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-17ac3bff-Dg88bSlc.js → sequenceDiagram-17ac3bff-CIXyQiN6.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-704730f1-DRIjLCxx.js → sequenceDiagram-704730f1-BmDwBhsF.js} +1 -1
- package/dist/frontend/assets/{si-LK-N5RQ5JYF-BtIIudm3.js → si-LK-N5RQ5JYF-DEPKL_1i.js} +1 -1
- package/dist/frontend/assets/{sk-SK-C5VTKIMK-CIwXticU.js → sk-SK-C5VTKIMK-XqrGDM2K.js} +1 -1
- package/dist/frontend/assets/{sl-SI-NN7IZMDC-CXEdg57y.js → sl-SI-NN7IZMDC-Dia3d1Yy.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-587899a1-BwRwgRo6.js → stateDiagram-587899a1-CY1BCaUa.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-9c5f0230-C8n96rER.js → stateDiagram-9c5f0230-gyjWQtSe.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-B7e5E4J7.js → stateDiagram-v2-51a3dcff-CAIiXqRE.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-B18iZQLw.js → stateDiagram-v2-d93cdb3a-DvUpzRiQ.js} +1 -1
- package/dist/frontend/assets/{styles-2ab5d517-CfWubwio.js → styles-2ab5d517-BE3yBf4Z.js} +3 -3
- package/dist/frontend/assets/{styles-5f03d8d2-C-_n19AB.js → styles-5f03d8d2-CGBEZiW_.js} +3 -3
- package/dist/frontend/assets/{styles-6aaf32cf-Dh-DHkVP.js → styles-6aaf32cf-JdIhb4_Y.js} +1 -1
- package/dist/frontend/assets/{styles-9a916d00-Boy0hUEG.js → styles-9a916d00-DRmxoyEF.js} +1 -1
- package/dist/frontend/assets/{styles-c10674c1-BfxiK3Ns.js → styles-c10674c1-C3TEvIjw.js} +1 -1
- package/dist/frontend/assets/{styles-edf9a4b0-T7mKViRF.js → styles-edf9a4b0-B_FeyXdP.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-dVrUAqld.js → sv-SE-XGPEYMSR-CBrvnOWh.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-08f97a94-fsWk6SQc.js → svgDrawCommon-08f97a94-DI4ZG6xI.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-3ba9043b-BJ8X3qS-.js → svgDrawCommon-3ba9043b-Dl5HsA0X.js} +1 -1
- package/dist/frontend/assets/{ta-IN-2NMHFXQM-CtVVZfIT.js → ta-IN-2NMHFXQM-DGYl9sGK.js} +1 -1
- package/dist/frontend/assets/{th-TH-HPSO5L25-Do90CyDN.js → th-TH-HPSO5L25-Bq3K3L0F.js} +1 -1
- package/dist/frontend/assets/{timeline-definition-7e6b55e7-D_iHA485.js → timeline-definition-7e6b55e7-DWL-fZtL.js} +3 -3
- package/dist/frontend/assets/{timeline-definition-85554ec2-nhT8QMsO.js → timeline-definition-85554ec2-C14kwDFd.js} +1 -1
- package/dist/frontend/assets/{tr-TR-DEFEU3FU-MfR-IQke.js → tr-TR-DEFEU3FU-CSwI35xw.js} +1 -1
- package/dist/frontend/assets/{uk-UA-QMV73CPH-CPet8B1O.js → uk-UA-QMV73CPH-Dxy7prmb.js} +1 -1
- package/dist/frontend/assets/{union-BgYwhB7T.js → union-DQ7m6Pel.js} +1 -1
- package/dist/frontend/assets/{vi-VN-M7AON7JQ-3HD06T_8.js → vi-VN-M7AON7JQ-BNyYTgjT.js} +1 -1
- package/dist/frontend/assets/{xychartDiagram-b6496bcd-D5vSaFT2.js → xychartDiagram-b6496bcd-DWKYL0ZD.js} +4 -4
- package/dist/frontend/assets/{xychartDiagram-e933f94c-DLIHXsUi.js → xychartDiagram-e933f94c-Bw4brW9l.js} +1 -1
- package/dist/frontend/assets/youtubeResume-DPG81gOQ.js +1 -0
- package/dist/frontend/assets/{zh-CN-LNUGB5OW-t2RyWPw2.js → zh-CN-LNUGB5OW-BNhZCPEt.js} +1 -1
- package/dist/frontend/assets/{zh-HK-E62DVLB3-Bj22hc7e.js → zh-HK-E62DVLB3-D_rDALSq.js} +1 -1
- package/dist/frontend/assets/{zh-TW-RAJ6MFWO-CBRhqhU1.js → zh-TW-RAJ6MFWO-rUlbmCTA.js} +1 -1
- package/dist/frontend/assets/zipObject-Db5-6vIz.js +1 -0
- package/dist/frontend/index.html +172 -236
- 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 -10
- package/dist/frontend/assets/arc-Cc3ncJ1n.js +0 -1
- package/dist/frontend/assets/channel-BQSIb3i3.js +0 -1
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-BgB8AG2e.js +0 -2
- package/dist/frontend/assets/clone-BoZziNL9.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-Z3H2d3Wj.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CIQOMghC.js +0 -1
- package/dist/frontend/assets/index-DIz4WrSB.js +0 -349
- package/dist/frontend/assets/zipObject-DMfHW2o8.js +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2279
- 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 -1208
- package/dist/server.js.map +0 -1
- package/dist/target.d.ts +0 -38
- package/dist/target.d.ts.map +0 -1
- package/dist/target.js +0 -225
- 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 -45
- package/dist/types.js.map +0 -1
- package/dist/utils/auth.d.ts +0 -14
- package/dist/utils/auth.d.ts.map +0 -1
- package/dist/utils/auth.js +0 -67
- package/dist/utils/auth.js.map +0 -1
- package/dist/utils/github.d.ts +0 -24
- package/dist/utils/github.d.ts.map +0 -1
- package/dist/utils/github.js +0 -132
- 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 -12
- 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/drop.png +0 -0
- package/frontend/index.html +0 -339
- package/frontend/src/App.tsx +0 -1091
- package/frontend/src/main.tsx +0 -15
- package/frontend/src/utils/ghSync.ts +0 -234
- package/frontend/src/utils/mermaidConverter.ts +0 -43
- package/frontend/src/utils/webmcp.ts +0 -265
- 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 -528
- package/src/index.ts +0 -2538
- package/src/remote.ts +0 -92
- package/src/server.ts +0 -1349
- package/src/target.ts +0 -198
- package/src/types.ts +0 -336
- package/src/utils/auth.ts +0 -61
- package/src/utils/github.ts +0 -121
- package/src/utils/logger.ts +0 -35
- 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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
131
|
+
return out;
|
|
47
132
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
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
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
409
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
207
410
|
}
|
|
208
411
|
catch {
|
|
209
|
-
|
|
412
|
+
return false;
|
|
210
413
|
}
|
|
414
|
+
if (!j || typeof j !== 'object')
|
|
415
|
+
return false;
|
|
211
416
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 (
|
|
226
|
-
if (!
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
268
|
-
printAgentCommands();
|
|
567
|
+
return false;
|
|
269
568
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
console.log(`
|
|
314
|
-
console.log(`
|
|
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
|
|
317
|
-
console.log(`
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
743
|
+
async function runTui() {
|
|
744
|
+
let clack;
|
|
745
|
+
try {
|
|
746
|
+
clack = await import('@clack/prompts');
|
|
343
747
|
}
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
849
|
+
clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
|
|
392
850
|
}
|
|
393
|
-
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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 —
|
|
971
|
+
console.log(`excalidrop — remote GitHub-backed canvas (no local server)
|
|
424
972
|
|
|
425
973
|
Usage:
|
|
426
|
-
npx excalidrop
|
|
427
|
-
npx excalidrop
|
|
428
|
-
npx
|
|
429
|
-
npx excalidrop login
|
|
430
|
-
npx excalidrop
|
|
431
|
-
npx excalidrop
|
|
432
|
-
npx excalidrop
|
|
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
|
-
|
|
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 '
|
|
447
|
-
await
|
|
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 '
|
|
459
|
-
await
|
|
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 '
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
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 '
|
|
476
|
-
|
|
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
|
-
|
|
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
|