excalidrop 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +574 -89
- package/dist/cli.js.map +1 -1
- package/dist/frontend/assets/{advancedFormat-CsxROTkH.js → advancedFormat-Be4KR5mp.js} +1 -1
- package/dist/frontend/assets/{ar-SA-G6X2FPQ2-Bu8X6mOi.js → ar-SA-G6X2FPQ2-B6ekz3Ba.js} +1 -1
- package/dist/frontend/assets/arc-DlgXyGPj.js +1 -0
- package/dist/frontend/assets/{az-AZ-76LH7QW2-DpvzY31_.js → az-AZ-76LH7QW2-01MDUsF0.js} +1 -1
- package/dist/frontend/assets/{bg-BG-XCXSNQG7-DCtZnLyo.js → bg-BG-XCXSNQG7--fArB8Ao.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-38ab4fdb-4cfDkUsw.js → blockDiagram-38ab4fdb-CmsVhOwi.js} +1 -1
- package/dist/frontend/assets/{blockDiagram-68f4deed-CsgWL6uC.js → blockDiagram-68f4deed-DU_wNYlO.js} +6 -6
- package/dist/frontend/assets/{bn-BD-2XOGV67Q-B-IdLNKT.js → bn-BD-2XOGV67Q-h8juqzfQ.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-15b5d702-7BAqo4qT.js → c4Diagram-15b5d702-ClZalz0L.js} +1 -1
- package/dist/frontend/assets/{c4Diagram-3d4e48cf-CDW-O-vg.js → c4Diagram-3d4e48cf-BkFVM0tP.js} +1 -1
- package/dist/frontend/assets/{ca-ES-6MX7JW3Y-DM8olClc.js → ca-ES-6MX7JW3Y-DMBR3qVX.js} +1 -1
- package/dist/frontend/assets/channel-BxFR7168.js +1 -0
- package/dist/frontend/assets/{classDiagram-70f12bd4-D2U4i4yC.js → classDiagram-70f12bd4-BXAJnakg.js} +1 -1
- package/dist/frontend/assets/{classDiagram-d40c83e7-DtBHRJc9.js → classDiagram-d40c83e7-DVhipa45.js} +1 -1
- package/dist/frontend/assets/{classDiagram-v2-d5a6b087-Drykcxns.js → classDiagram-v2-d5a6b087-CiHMsbhj.js} +1 -1
- package/dist/frontend/assets/classDiagram-v2-f2320105-1CqIxWOB.js +2 -0
- package/dist/frontend/assets/clone-CMl85WSi.js +1 -0
- package/dist/frontend/assets/{createText-2e5e7dd3-1KPUhDpM.js → createText-2e5e7dd3-D3kSz3YE.js} +1 -1
- package/dist/frontend/assets/{createText-d213de94-C6EhGbUN.js → createText-d213de94-DvJeLlQX.js} +1 -1
- package/dist/frontend/assets/{cs-CZ-2BRQDIVT-DgjTgz6K.js → cs-CZ-2BRQDIVT-BUcdPrAr.js} +1 -1
- package/dist/frontend/assets/{cytoscape-cose-bilkent-BnIHl72R.js → cytoscape-cose-bilkent-CKoSm_2z.js} +1 -1
- package/dist/frontend/assets/{da-DK-5WZEPLOC-uEs3xGKF.js → da-DK-5WZEPLOC-C1nkchuC.js} +1 -1
- package/dist/frontend/assets/{de-DE-XR44H4JA-CBglLw42.js → de-DE-XR44H4JA-DO2_bO1X.js} +1 -1
- package/dist/frontend/assets/{edges-332bd1c7-cWCBwcrg.js → edges-332bd1c7-C5qM1sDU.js} +1 -1
- package/dist/frontend/assets/{edges-e0da2a9e-DKeDUJ_F.js → edges-e0da2a9e-BRt7qomS.js} +1 -1
- package/dist/frontend/assets/{el-GR-BZB4AONW-B-_eub00.js → el-GR-BZB4AONW-CrjSdaU2.js} +1 -1
- package/dist/frontend/assets/{elk.bundled-Ch9TeAu4.js → elk.bundled-Dmec6Rq6.js} +1 -1
- package/dist/frontend/assets/{erDiagram-880f2ed8-CtDf_58r.js → erDiagram-880f2ed8-BI7kP523.js} +1 -1
- package/dist/frontend/assets/{erDiagram-9861fffd-CAlUeljk.js → erDiagram-9861fffd-DMyCDrgI.js} +1 -1
- package/dist/frontend/assets/{es-ES-U4NZUMDT-BIN3bSVD.js → es-ES-U4NZUMDT-rrdjPi-M.js} +1 -1
- package/dist/frontend/assets/{eu-ES-A7QVB2H4-TyQet3-N.js → eu-ES-A7QVB2H4-uYn3S3D5.js} +1 -1
- package/dist/frontend/assets/{fa-IR-HGAKTJCU-DVmjjr57.js → fa-IR-HGAKTJCU-DWW_BVFR.js} +1 -1
- package/dist/frontend/assets/{fi-FI-Z5N7JZ37-ZW9kXLa8.js → fi-FI-Z5N7JZ37-CMXxeZ4W.js} +1 -1
- package/dist/frontend/assets/{flowDb-7c981674-B5ppx8EN.js → flowDb-7c981674-Dsj6xMzr.js} +1 -1
- package/dist/frontend/assets/{flowDb-956e92f1-Dqi00Wq-.js → flowDb-956e92f1-26g7kJju.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-66a62f08-v3Liu0m_.js → flowDiagram-66a62f08-C9eQZtdf.js} +1 -1
- package/dist/frontend/assets/{flowDiagram-cbd28bf7-DJ_YHRwa.js → flowDiagram-cbd28bf7-D051xFrI.js} +1 -1
- 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-BleYul6b.js → flowchart-elk-definition-36e2d292-C9fsvhV-.js} +1 -1
- package/dist/frontend/assets/{flowchart-elk-definition-4a651766-D6lLh4Ih.js → flowchart-elk-definition-4a651766-BkVNuJOw.js} +1 -1
- package/dist/frontend/assets/{fr-FR-RHASNOE6-CxLR66tH.js → fr-FR-RHASNOE6-DIfK_rjW.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-04f9e578-DJSOyr8B.js → ganttDiagram-04f9e578-CZeF2a4Y.js} +1 -1
- package/dist/frontend/assets/{ganttDiagram-c361ad54-CJmbCLvp.js → ganttDiagram-c361ad54-DbkIZ-wU.js} +1 -1
- package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-DiRq_FJc.js → gitGraphDiagram-21fc4d3e-CLqlSEuE.js} +1 -1
- package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-MfvDzoAb.js → gitGraphDiagram-72cf32ee-Cvf9uYWB.js} +1 -1
- package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CxWj2itK.js → gl-ES-HMX3MZ6V-CCxCH2oK.js} +1 -1
- package/dist/frontend/assets/{graph-CT_r46y9.js → graph-MptL1QoF.js} +1 -1
- package/dist/frontend/assets/{graph-DJt_Vezo.js → graph-Mzuu2QFH.js} +1 -1
- package/dist/frontend/assets/{he-IL-6SHJWFNN-5v2WFGO3.js → he-IL-6SHJWFNN-CJuJutJZ.js} +1 -1
- package/dist/frontend/assets/{hi-IN-IWLTKZ5I-BJGbvgRu.js → hi-IN-IWLTKZ5I-BORPH6vU.js} +1 -1
- package/dist/frontend/assets/{hu-HU-A5ZG7DT2-REQT2EcE.js → hu-HU-A5ZG7DT2-Dr7UDbjo.js} +1 -1
- package/dist/frontend/assets/{id-ID-SAP4L64H-aZWQYWdg.js → id-ID-SAP4L64H-D0Widkq7.js} +1 -1
- package/dist/frontend/assets/{index-3862675e-BgrvgbsM.js → index-3862675e-DVT5-vN2.js} +1 -1
- package/dist/frontend/assets/{index-6079d271-Cp8jFHTC.js → index-6079d271-R4KRnW5Z.js} +1 -1
- package/dist/frontend/assets/{index-DBHHeSXy.js → index-B45XzZiR.js} +4 -4
- package/dist/frontend/assets/index-Bd-8869C.js +349 -0
- package/dist/frontend/assets/{infoDiagram-4a4f5b27-C1ZJJtWv.js → infoDiagram-4a4f5b27-BvY1qS17.js} +1 -1
- package/dist/frontend/assets/{infoDiagram-f8f76790-GeS0JKxo.js → infoDiagram-f8f76790-D60YvG33.js} +1 -1
- package/dist/frontend/assets/{it-IT-JPQ66NNP-EyeA2Ty1.js → it-IT-JPQ66NNP-C08Vu0BG.js} +1 -1
- package/dist/frontend/assets/{ja-JP-DBVTYXUO-BLcE45D9.js → ja-JP-DBVTYXUO-Ciu7LaRi.js} +1 -1
- package/dist/frontend/assets/{journeyDiagram-29694f62-4Z_lpK4g.js → journeyDiagram-29694f62-DvrnJXsd.js} +1 -1
- package/dist/frontend/assets/{journeyDiagram-49397b02-ChwU3Zo6.js → journeyDiagram-49397b02-Dy9LU86E.js} +1 -1
- package/dist/frontend/assets/{kaa-6HZHGXH3-CkpTcX9X.js → kaa-6HZHGXH3-GhGTIjpu.js} +1 -1
- package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-CChq0y6o.js → kab-KAB-ZGHBKWFO-B3q4BqYw.js} +1 -1
- package/dist/frontend/assets/{kk-KZ-P5N5QNE5-CwtoTY4z.js → kk-KZ-P5N5QNE5-GWoLZvco.js} +1 -1
- package/dist/frontend/assets/{km-KH-HSX4SM5Z-D26sdohj.js → km-KH-HSX4SM5Z-CnAQJCYr.js} +1 -1
- package/dist/frontend/assets/{ko-KR-MTYHY66A-DvnDcJQA.js → ko-KR-MTYHY66A-B2Rh9krp.js} +1 -1
- package/dist/frontend/assets/{ku-TR-6OUDTVRD-qdHbLgKg.js → ku-TR-6OUDTVRD-C27elmON.js} +1 -1
- package/dist/frontend/assets/{layout-YRLNN4au.js → layout-D1hl6oKi.js} +1 -1
- package/dist/frontend/assets/{layout-C-wUeNvb.js → layout-c-hOA8zE.js} +1 -1
- package/dist/frontend/assets/{line-BEgMClKG.js → line-DtMz1ssZ.js} +1 -1
- package/dist/frontend/assets/{linear-DDHpSdYw.js → linear-D-Y4WtO_.js} +1 -1
- package/dist/frontend/assets/{lt-LT-XHIRWOB4-DT4LYe_d.js → lt-LT-XHIRWOB4-B6jd693w.js} +1 -1
- package/dist/frontend/assets/{lv-LV-5QDEKY6T-Dbc3ivwT.js → lv-LV-5QDEKY6T-sXB-8i6R.js} +1 -1
- package/dist/frontend/assets/{mindmap-definition-ac74a2e8-CY2RhWUN.js → mindmap-definition-ac74a2e8-CawO9I_f.js} +1 -1
- package/dist/frontend/assets/{mindmap-definition-fc14e90a-VT4JDi6N.js → mindmap-definition-fc14e90a-CXMNA68v.js} +4 -4
- package/dist/frontend/assets/{mr-IN-CRQNXWMA-D75p1fVc.js → mr-IN-CRQNXWMA-BJ6QSiPi.js} +1 -1
- package/dist/frontend/assets/{my-MM-5M5IBNSE-DnEoBXyE.js → my-MM-5M5IBNSE-BMdOIRP5.js} +1 -1
- package/dist/frontend/assets/{nb-NO-T6EIAALU-BL4ThJxl.js → nb-NO-T6EIAALU-BVG6JUMN.js} +1 -1
- package/dist/frontend/assets/{nl-NL-IS3SIHDZ-u18nUblY.js → nl-NL-IS3SIHDZ-ZprCvTYR.js} +1 -1
- package/dist/frontend/assets/{nn-NO-6E72VCQL-B9LJ0oXc.js → nn-NO-6E72VCQL-CcljFAGy.js} +1 -1
- package/dist/frontend/assets/{oc-FR-POXYY2M6-BKG5kIcO.js → oc-FR-POXYY2M6-zCuu6Ns2.js} +1 -1
- package/dist/frontend/assets/{pa-IN-N4M65BXN-CmpnjbkL.js → pa-IN-N4M65BXN-D_iftL9v.js} +1 -1
- package/dist/frontend/assets/{pica-BcpcM0v1.js → pica-DsW4eP6T.js} +1 -1
- package/dist/frontend/assets/{pie-Swzl5NrI.js → pie-U_uHwWsL.js} +1 -1
- package/dist/frontend/assets/{pieDiagram-421022e6-ndz96BLe.js → pieDiagram-421022e6-naaI7HiE.js} +1 -1
- package/dist/frontend/assets/{pieDiagram-8a3498a8-D5rcV68O.js → pieDiagram-8a3498a8-m6nTf1O_.js} +1 -1
- package/dist/frontend/assets/{pl-PL-T2D74RX3-CrR66Rgj.js → pl-PL-T2D74RX3-CRQQfZUe.js} +1 -1
- package/dist/frontend/assets/{pt-BR-5N22H2LF-Crv7jZiH.js → pt-BR-5N22H2LF-DLvfG7RR.js} +1 -1
- package/dist/frontend/assets/{pt-PT-UZXXM6DQ-Bkaa5ESR.js → pt-PT-UZXXM6DQ-Chkg6-JC.js} +1 -1
- package/dist/frontend/assets/{quadrantDiagram-0957ecba-DOdyxeTP.js → quadrantDiagram-0957ecba-BJR82kAx.js} +1 -1
- package/dist/frontend/assets/{quadrantDiagram-120e2f19-beQD7R1C.js → quadrantDiagram-120e2f19-CsWgLp5e.js} +1 -1
- package/dist/frontend/assets/relay-BSVGqyIy.js +2 -0
- package/dist/frontend/assets/{requirementDiagram-23d650b8-B2i-OB97.js → requirementDiagram-23d650b8-CHmg-ycX.js} +1 -1
- package/dist/frontend/assets/{requirementDiagram-deff3bca-beS-SvfY.js → requirementDiagram-deff3bca-LIsyxsZZ.js} +1 -1
- package/dist/frontend/assets/{ro-RO-JPDTUUEW-DsrnQm7K.js → ro-RO-JPDTUUEW-B_a5A5-x.js} +1 -1
- package/dist/frontend/assets/{ru-RU-B4JR7IUQ-DrnGGdUs.js → ru-RU-B4JR7IUQ-CY5sI1Ru.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-04a897e0-CVad45bN.js → sankeyDiagram-04a897e0-DN5e16d0.js} +1 -1
- package/dist/frontend/assets/{sankeyDiagram-23345273-ghWBXXF2.js → sankeyDiagram-23345273-BBjQCtHz.js} +1 -1
- package/dist/frontend/assets/{selectAll-XplaO988.js → selectAll-Dcq-dC_E.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-17ac3bff-DBI-EdWx.js → sequenceDiagram-17ac3bff-CIXyQiN6.js} +1 -1
- package/dist/frontend/assets/{sequenceDiagram-704730f1-BnJ__mBt.js → sequenceDiagram-704730f1-BmDwBhsF.js} +1 -1
- package/dist/frontend/assets/{si-LK-N5RQ5JYF-BxAwuRM5.js → si-LK-N5RQ5JYF-DEPKL_1i.js} +1 -1
- package/dist/frontend/assets/{sk-SK-C5VTKIMK-CnDyiLRf.js → sk-SK-C5VTKIMK-XqrGDM2K.js} +1 -1
- package/dist/frontend/assets/{sl-SI-NN7IZMDC-BXHXIdFA.js → sl-SI-NN7IZMDC-Dia3d1Yy.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-587899a1-D3Ahxsgc.js → stateDiagram-587899a1-CY1BCaUa.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-9c5f0230-8XbLWvC0.js → stateDiagram-9c5f0230-gyjWQtSe.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-Cqf-MUGD.js → stateDiagram-v2-51a3dcff-CAIiXqRE.js} +1 -1
- package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-DjGrud9D.js → stateDiagram-v2-d93cdb3a-DvUpzRiQ.js} +1 -1
- package/dist/frontend/assets/{styles-2ab5d517-B4qdHOgu.js → styles-2ab5d517-BE3yBf4Z.js} +1 -1
- package/dist/frontend/assets/{styles-5f03d8d2-DOD2v5V8.js → styles-5f03d8d2-CGBEZiW_.js} +1 -1
- package/dist/frontend/assets/{styles-6aaf32cf-bP3XXcmf.js → styles-6aaf32cf-JdIhb4_Y.js} +1 -1
- package/dist/frontend/assets/{styles-9a916d00-QLeuL_Dy.js → styles-9a916d00-DRmxoyEF.js} +1 -1
- package/dist/frontend/assets/{styles-c10674c1-Lpvh1u4f.js → styles-c10674c1-C3TEvIjw.js} +1 -1
- package/dist/frontend/assets/{styles-edf9a4b0-D4W9aPm7.js → styles-edf9a4b0-B_FeyXdP.js} +1 -1
- package/dist/frontend/assets/subset-shared.chunk.js +1 -1
- package/dist/frontend/assets/subset-worker.chunk.js +1 -1
- package/dist/frontend/assets/{sv-SE-XGPEYMSR-YrCQQBuo.js → sv-SE-XGPEYMSR-CBrvnOWh.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-08f97a94-CwNv3PjS.js → svgDrawCommon-08f97a94-DI4ZG6xI.js} +1 -1
- package/dist/frontend/assets/{svgDrawCommon-3ba9043b-DZbT0Bqu.js → svgDrawCommon-3ba9043b-Dl5HsA0X.js} +1 -1
- package/dist/frontend/assets/{ta-IN-2NMHFXQM-qR82aBSW.js → ta-IN-2NMHFXQM-DGYl9sGK.js} +1 -1
- package/dist/frontend/assets/{th-TH-HPSO5L25-C65V7hE5.js → th-TH-HPSO5L25-Bq3K3L0F.js} +1 -1
- package/dist/frontend/assets/{timeline-definition-7e6b55e7-GkPy3QeG.js → timeline-definition-7e6b55e7-DWL-fZtL.js} +3 -3
- package/dist/frontend/assets/{timeline-definition-85554ec2-CCdkBaC0.js → timeline-definition-85554ec2-C14kwDFd.js} +3 -3
- package/dist/frontend/assets/{tr-TR-DEFEU3FU-CFzbwAKV.js → tr-TR-DEFEU3FU-CSwI35xw.js} +1 -1
- package/dist/frontend/assets/{uk-UA-QMV73CPH-ePt5WVz3.js → uk-UA-QMV73CPH-Dxy7prmb.js} +1 -1
- package/dist/frontend/assets/{union-CVZobYBh.js → union-DQ7m6Pel.js} +1 -1
- package/dist/frontend/assets/{vi-VN-M7AON7JQ-CaPKykgJ.js → vi-VN-M7AON7JQ-BNyYTgjT.js} +1 -1
- package/dist/frontend/assets/{xychartDiagram-b6496bcd-CV5WRc81.js → xychartDiagram-b6496bcd-DWKYL0ZD.js} +1 -1
- package/dist/frontend/assets/{xychartDiagram-e933f94c-BH-ySdQf.js → xychartDiagram-e933f94c-Bw4brW9l.js} +1 -1
- package/dist/frontend/assets/youtubeResume-DPG81gOQ.js +1 -0
- package/dist/frontend/assets/{zh-CN-LNUGB5OW-CK8AEj8o.js → zh-CN-LNUGB5OW-BNhZCPEt.js} +1 -1
- package/dist/frontend/assets/{zh-HK-E62DVLB3-CUINiFgH.js → zh-HK-E62DVLB3-D_rDALSq.js} +1 -1
- package/dist/frontend/assets/{zh-TW-RAJ6MFWO-BEBY7iJR.js → zh-TW-RAJ6MFWO-rUlbmCTA.js} +1 -1
- package/dist/frontend/assets/{zipObject-W8SLG3F1.js → zipObject-Db5-6vIz.js} +1 -1
- package/dist/frontend/index.html +6 -2
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/frontend/assets/arc-B9VNLSzj.js +0 -1
- package/dist/frontend/assets/channel-jdNM4v-F.js +0 -1
- package/dist/frontend/assets/classDiagram-v2-f2320105-D8ruMBA6.js +0 -2
- package/dist/frontend/assets/clone-DFO_zGRO.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-6vlgZ0Bg.js +0 -1
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-FmnI78Vm.js +0 -1
- package/dist/frontend/assets/index-GTq6y6g6.js +0 -349
- package/dist/frontend/assets/relay-BIOL2uw2.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,25 @@ async function ghPut(pathname, body) {
|
|
|
38
38
|
throw new Error(`GitHub PUT ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
|
|
39
39
|
return r.json();
|
|
40
40
|
}
|
|
41
|
+
async function ghPatch(pathname, body) {
|
|
42
|
+
const r = await fetch(`${GH_API}${pathname}`, { method: 'PATCH', headers: ghHeaders(), body: JSON.stringify(body) });
|
|
43
|
+
if (!r.ok)
|
|
44
|
+
throw new Error(`GitHub PATCH ${pathname} → ${r.status}: ${(await r.text()).slice(0, 150)}`);
|
|
45
|
+
return r.json();
|
|
46
|
+
}
|
|
47
|
+
/** Set repo website (homepage) to the canvas viewer URL — only if empty. Best-effort. */
|
|
48
|
+
async function ensureRepoHomepage(repo, viewerUrl) {
|
|
49
|
+
try {
|
|
50
|
+
const info = await ghGet(`/repos/${repo}`);
|
|
51
|
+
if (info.homepage && String(info.homepage).trim())
|
|
52
|
+
return;
|
|
53
|
+
await ghPatch(`/repos/${repo}`, { homepage: viewerUrl });
|
|
54
|
+
console.log(`Repo website set to canvas: ${viewerUrl}`);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.warn(`Could not set repo website (non-fatal): ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
41
60
|
async function ensureMainBranch(repo) {
|
|
42
61
|
const main = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/main`, { headers: ghHeaders() });
|
|
43
62
|
if (main.ok)
|
|
@@ -47,7 +66,7 @@ async function ensureMainBranch(repo) {
|
|
|
47
66
|
return;
|
|
48
67
|
await ghPut(`/repos/${repo}/contents/README.md`, {
|
|
49
68
|
message: 'Initial commit',
|
|
50
|
-
content: Buffer.from(
|
|
69
|
+
content: Buffer.from('').toString('base64'),
|
|
51
70
|
branch: 'main',
|
|
52
71
|
});
|
|
53
72
|
}
|
|
@@ -154,15 +173,25 @@ async function deployViewerPages(repo) {
|
|
|
154
173
|
function projectNameForRepo(repo) {
|
|
155
174
|
return `excalidrop-${repo.replace('/', '-').toLowerCase().replace(/[^a-z0-9-]/g, '-')}`.slice(0, 60);
|
|
156
175
|
}
|
|
157
|
-
function viewerUrlFor(repo, target) {
|
|
176
|
+
function viewerUrlFor(repo, target, project) {
|
|
158
177
|
if (target === 'cloudflare') {
|
|
159
178
|
// Generic viewer build can't infer the repo from a pages.dev host —
|
|
160
179
|
// detectRepo() reads it from ?repo= instead.
|
|
161
|
-
return `https://${projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
|
|
180
|
+
return `https://${project || projectNameForRepo(repo)}.pages.dev/?repo=${repo}`;
|
|
162
181
|
}
|
|
163
182
|
const [owner, name] = repo.split('/');
|
|
164
183
|
return `https://${owner}.github.io/${name}/`;
|
|
165
184
|
}
|
|
185
|
+
/** Cloudflare project names: lowercase alphanumerics + hyphens, ≤63 chars. */
|
|
186
|
+
export function normalizeProjectName(input, fallback) {
|
|
187
|
+
const cleaned = (input || '')
|
|
188
|
+
.trim()
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
191
|
+
.replace(/^-+|-+$/g, '')
|
|
192
|
+
.slice(0, 63);
|
|
193
|
+
return cleaned || fallback;
|
|
194
|
+
}
|
|
166
195
|
async function isPrivateRepo(repo) {
|
|
167
196
|
try {
|
|
168
197
|
const r = spawnSync('gh', ['api', `repos/${repo}`, '--jq', '.private'], { encoding: 'utf8' });
|
|
@@ -183,10 +212,22 @@ async function resolveTarget(repo, explicit) {
|
|
|
183
212
|
return explicit;
|
|
184
213
|
return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
|
|
185
214
|
}
|
|
215
|
+
const WRANGLER = ['-y', 'wrangler@4'];
|
|
216
|
+
// Which Cloudflare identity would a Pages deploy use? whoami fails when
|
|
217
|
+
// logged out; a CLOUDFLARE_API_TOKEN works without a login. Deploys always
|
|
218
|
+
// run under the USER's own account/quota — never the author's.
|
|
219
|
+
function wranglerAccount() {
|
|
220
|
+
if (process.env.CLOUDFLARE_API_TOKEN)
|
|
221
|
+
return { ok: true, display: 'API token (CLOUDFLARE_API_TOKEN)' };
|
|
222
|
+
const r = runCapture('npx', [...WRANGLER, 'whoami']);
|
|
223
|
+
if (r.status !== 0)
|
|
224
|
+
return { ok: false, display: '' };
|
|
225
|
+
const m = r.out.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
|
|
226
|
+
return { ok: true, display: m?.[0] ?? 'logged in' };
|
|
227
|
+
}
|
|
186
228
|
function deployToCloudflarePages(project) {
|
|
187
229
|
// wrangler 4 doesn't auto-create Pages projects and its Workers delegation
|
|
188
230
|
// misfires on explicit asset dirs — create once, then deploy classic with --force.
|
|
189
|
-
const WRANGLER = ['-y', 'wrangler@4'];
|
|
190
231
|
const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
|
|
191
232
|
const createOut = (create.stdout || '') + (create.stderr || '');
|
|
192
233
|
if (create.status !== 0 && !/already exists/i.test(createOut)) {
|
|
@@ -205,6 +246,19 @@ function detectSlug() {
|
|
|
205
246
|
return '';
|
|
206
247
|
}
|
|
207
248
|
}
|
|
249
|
+
/** Accept owner/repo, a GitHub URL, or git@ SSH — always returns owner/repo or ''. */
|
|
250
|
+
export function normalizeRepoSlug(input) {
|
|
251
|
+
const s = (input || '').trim();
|
|
252
|
+
if (!s)
|
|
253
|
+
return '';
|
|
254
|
+
// Full URL / SSH / bare host path → extract the owner/repo tail.
|
|
255
|
+
const m = s.match(/github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\/.*)?$/i);
|
|
256
|
+
const candidate = m ? m[1] : s;
|
|
257
|
+
const cleaned = candidate.replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
|
|
258
|
+
const ok = cleaned.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i);
|
|
259
|
+
return ok ? `${ok[1].toLowerCase()}/${ok[2]}` : '';
|
|
260
|
+
}
|
|
261
|
+
const SLUG_HINT = 'owner/repo or a GitHub link';
|
|
208
262
|
function ghAuthed() {
|
|
209
263
|
return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
|
|
210
264
|
}
|
|
@@ -213,59 +267,314 @@ function commandExists(cmd) {
|
|
|
213
267
|
return probe.status === 0;
|
|
214
268
|
}
|
|
215
269
|
function findProjectRoot(cwd) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
224
|
-
dir = parent;
|
|
270
|
+
// Inside a git checkout the repo top is the root. Never walk up past cwd
|
|
271
|
+
// looking for package.json: from a fresh non-checkout dir (e.g. a folder
|
|
272
|
+
// holding a brand-new canvas repo) the old walk-up climbed into an
|
|
273
|
+
// unrelated parent project and wrote .mcp.json / .excalidrop.json there.
|
|
274
|
+
try {
|
|
275
|
+
const top = execSync('git rev-parse --show-toplevel', { cwd: path.resolve(cwd), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
276
|
+
if (top)
|
|
277
|
+
return top;
|
|
225
278
|
}
|
|
279
|
+
catch { /* not inside a git work tree — fall through to cwd */ }
|
|
280
|
+
return path.resolve(cwd);
|
|
226
281
|
}
|
|
227
282
|
function mcpArgs(repo) {
|
|
228
283
|
return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
|
|
229
284
|
}
|
|
230
|
-
const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'desktop', 'opencode'];
|
|
285
|
+
const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'cursor-user', 'desktop', 'opencode', 'opencode-user'];
|
|
231
286
|
function editorCommand(id, repo) {
|
|
232
287
|
const a = mcpArgs(repo).join(' ');
|
|
233
288
|
switch (id) {
|
|
234
289
|
case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
|
|
235
290
|
case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
|
|
236
291
|
case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
|
|
237
|
-
case 'cursor':
|
|
292
|
+
case 'cursor':
|
|
293
|
+
case 'cursor-user':
|
|
238
294
|
case 'desktop': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
|
|
239
|
-
case 'opencode':
|
|
295
|
+
case 'opencode':
|
|
296
|
+
case 'opencode-user': return JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: { excalidrop: { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true } } }, null, 2);
|
|
240
297
|
}
|
|
241
298
|
}
|
|
242
|
-
function
|
|
299
|
+
function editorScope(id) {
|
|
300
|
+
return id === 'claude-project' || id === 'cursor' || id === 'opencode' ? 'project' : 'user';
|
|
301
|
+
}
|
|
302
|
+
// Where each editor id reads its config from. File-based harnesses support
|
|
303
|
+
// both a project file and a user-global file; CLI-driven ones (claude, codex)
|
|
304
|
+
// are detected through the same files their CLIs write.
|
|
305
|
+
function editorFile(id, root) {
|
|
306
|
+
const home = os.homedir();
|
|
307
|
+
switch (id) {
|
|
308
|
+
case 'claude-user': return { path: path.join(home, '.claude.json'), key: 'mcpServers' };
|
|
309
|
+
case 'claude-project': return { path: path.join(root, MCP_JSON), key: 'mcpServers' };
|
|
310
|
+
case 'cursor': return { path: path.join(root, '.cursor', 'mcp.json'), key: 'mcpServers' };
|
|
311
|
+
case 'cursor-user': return { path: path.join(home, '.cursor', 'mcp.json'), key: 'mcpServers' };
|
|
312
|
+
case 'desktop': return { path: desktopConfigPath(), key: 'mcpServers' };
|
|
313
|
+
case 'opencode': return { path: path.join(root, 'opencode.json'), key: 'mcp' };
|
|
314
|
+
case 'opencode-user': return { path: path.join(home, '.config', 'opencode', 'opencode.json'), key: 'mcp' };
|
|
315
|
+
case 'codex': return null; // TOML (~/.codex/config.toml) — parsed separately
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function desktopConfigPath() {
|
|
319
|
+
const home = os.homedir();
|
|
320
|
+
if (process.platform === 'darwin')
|
|
321
|
+
return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
322
|
+
if (process.platform === 'win32')
|
|
323
|
+
return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
324
|
+
return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
325
|
+
}
|
|
326
|
+
function editorTarget(id, root) {
|
|
327
|
+
const f = editorFile(id, root);
|
|
328
|
+
if (id === 'codex')
|
|
329
|
+
return 'user config (~/.codex/config.toml)';
|
|
330
|
+
const scope = editorScope(id);
|
|
331
|
+
const short = f ? shortenHome(f.path) : id;
|
|
332
|
+
return `${scope} scope (${short})`;
|
|
333
|
+
}
|
|
334
|
+
function shortenHome(p) {
|
|
335
|
+
const home = os.homedir();
|
|
336
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
337
|
+
}
|
|
338
|
+
function repoFromArgs(args) {
|
|
339
|
+
if (!Array.isArray(args))
|
|
340
|
+
return null;
|
|
341
|
+
const i = args.findIndex((a) => a === '--repo');
|
|
342
|
+
const v = i !== -1 ? args[i + 1] : null;
|
|
343
|
+
return typeof v === 'string' && v.includes('/') ? v.toLowerCase() : null;
|
|
344
|
+
}
|
|
345
|
+
// {present, repo} for a JSON MCP config (claude/cursor/desktop/opencode shapes).
|
|
346
|
+
function jsonMcpRepo(file, key) {
|
|
347
|
+
let j;
|
|
243
348
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
349
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return { present: false, repo: null };
|
|
353
|
+
}
|
|
354
|
+
const e = key === 'mcpServers' ? j?.mcpServers?.excalidrop : j?.mcp?.excalidrop;
|
|
355
|
+
if (!e)
|
|
356
|
+
return { present: false, repo: null };
|
|
357
|
+
if (Array.isArray(e.command))
|
|
358
|
+
return { present: true, repo: repoFromArgs(e.command) };
|
|
359
|
+
if (e.command === 'npx' && Array.isArray(e.args))
|
|
360
|
+
return { present: true, repo: repoFromArgs(e.args) };
|
|
361
|
+
return { present: true, repo: null };
|
|
362
|
+
}
|
|
363
|
+
// {present, repo} for Codex's TOML config — crude section parse, no new deps.
|
|
364
|
+
function codexMcpRepo() {
|
|
365
|
+
let txt;
|
|
366
|
+
try {
|
|
367
|
+
txt = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return { present: false, repo: null };
|
|
371
|
+
}
|
|
372
|
+
const sec = txt.match(/\[mcp_servers\.excalidrop\]([\s\S]*?)(?=^\[|\z)/m);
|
|
373
|
+
if (!sec)
|
|
374
|
+
return { present: false, repo: null };
|
|
375
|
+
const m = (sec[1] ?? '').match(/--repo["'\s=,]+([A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+)/);
|
|
376
|
+
return { present: true, repo: m?.[1]?.toLowerCase() ?? null };
|
|
377
|
+
}
|
|
378
|
+
function detectEditorState(id, repo, root) {
|
|
379
|
+
const want = repo.toLowerCase();
|
|
380
|
+
if (id === 'codex') {
|
|
381
|
+
const s = codexMcpRepo();
|
|
382
|
+
if (!s.present)
|
|
383
|
+
return { status: 'missing', repo: null, where: '~/.codex/config.toml' };
|
|
384
|
+
return s.repo === want
|
|
385
|
+
? { status: 'current', repo: s.repo, where: '~/.codex/config.toml' }
|
|
386
|
+
: { status: 'stale', repo: s.repo, where: '~/.codex/config.toml' };
|
|
387
|
+
}
|
|
388
|
+
const f = editorFile(id, root);
|
|
389
|
+
const where = shortenHome(f.path);
|
|
390
|
+
const s = jsonMcpRepo(f.path, f.key);
|
|
391
|
+
if (!s.present)
|
|
392
|
+
return { status: 'missing', repo: null, where };
|
|
393
|
+
return s.repo === want
|
|
394
|
+
? { status: 'current', repo: s.repo, where }
|
|
395
|
+
: { status: 'stale', repo: s.repo, where };
|
|
396
|
+
}
|
|
397
|
+
function updateCommand(id, repo) {
|
|
398
|
+
if (id === 'claude-project' || id === 'claude-user') {
|
|
399
|
+
const scope = id === 'claude-project' ? 'project' : 'user';
|
|
400
|
+
return `claude mcp remove excalidrop -s ${scope} && ${editorCommand(id, repo)}`;
|
|
401
|
+
}
|
|
402
|
+
return editorCommand(id, repo);
|
|
403
|
+
}
|
|
404
|
+
function runCapture(cmd, args) {
|
|
405
|
+
try {
|
|
406
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
407
|
+
return { status: r.status, out: `${r.stdout || ''}\n${r.stderr || ''}` };
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
return { status: 1, out: e.message };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Merge an MCP entry into a JSON config file (creates parent dirs). Returns
|
|
414
|
+
// false when the existing file isn't valid JSON — caller falls back to manual.
|
|
415
|
+
function upsertJsonMcp(file, key, entry) {
|
|
416
|
+
let j = {};
|
|
417
|
+
if (fs.existsSync(file)) {
|
|
418
|
+
try {
|
|
419
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
if (!j || typeof j !== 'object')
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
try {
|
|
429
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
246
430
|
}
|
|
247
|
-
|
|
248
|
-
return
|
|
431
|
+
catch {
|
|
432
|
+
return false;
|
|
249
433
|
}
|
|
250
|
-
|
|
251
|
-
|
|
434
|
+
}
|
|
435
|
+
if (key === 'mcpServers') {
|
|
436
|
+
if (!j.mcpServers || typeof j.mcpServers !== 'object')
|
|
437
|
+
j.mcpServers = {};
|
|
438
|
+
j.mcpServers.excalidrop = entry;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
if (!j.mcp || typeof j.mcp !== 'object')
|
|
442
|
+
j.mcp = {};
|
|
443
|
+
j.mcp.excalidrop = entry;
|
|
444
|
+
if (!j.$schema)
|
|
445
|
+
j.$schema = 'https://opencode.ai/config.json';
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
fs.writeFileSync(file, JSON.stringify(j, null, 2) + '\n');
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function cursorEntry(repo) {
|
|
456
|
+
return { command: 'npx', args: mcpArgs(repo), env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
|
|
457
|
+
}
|
|
458
|
+
function opencodeEntry(repo) {
|
|
459
|
+
return { type: 'local', command: ['npx', ...mcpArgs(repo)], enabled: true, env: { EXCALIDROP_RELAY_URL: process.env.EXCALIDROP_RELAY_URL || RELAY_DEFAULT } };
|
|
460
|
+
}
|
|
461
|
+
function installEditor(id, repo, root) {
|
|
462
|
+
const fail = (detail) => ({ id, outcome: 'failed', detail });
|
|
463
|
+
// Idempotent first: never reinstall over an identical entry, never silently
|
|
464
|
+
// leave a stale one — report it with the exact update command.
|
|
465
|
+
const st = detectEditorState(id, repo, root);
|
|
466
|
+
if (st.status === 'current')
|
|
467
|
+
return { id, outcome: 'already-current', detail: `${st.where} already points at ${repo}` };
|
|
468
|
+
const staleNote = st.status === 'stale' ? ` (currently → ${st.repo ?? 'unknown'})` : '';
|
|
469
|
+
if (id === 'claude-project' || id === 'claude-user' || id === 'codex') {
|
|
470
|
+
const bin = id === 'codex' ? 'codex' : 'claude';
|
|
471
|
+
if (!commandExists(bin))
|
|
472
|
+
return fail(`${bin} CLI not found — run manually:\n${editorCommand(id, repo)}`);
|
|
473
|
+
const cargs = id === 'claude-project'
|
|
474
|
+
? ['mcp', 'add', 'excalidrop', '--scope', 'project', '--', 'npx', ...mcpArgs(repo)]
|
|
475
|
+
: id === 'claude-user'
|
|
476
|
+
? ['mcp', 'add', 'excalidrop', '--scope', 'user', '--', 'npx', ...mcpArgs(repo)]
|
|
477
|
+
: ['mcp', 'add', 'excalidrop', '--', 'npx', ...mcpArgs(repo)];
|
|
478
|
+
const r = runCapture(bin, cargs);
|
|
479
|
+
if (r.status === 0 && !/already exists/i.test(r.out)) {
|
|
480
|
+
return { id, outcome: 'added', detail: editorCommand(id, repo) };
|
|
252
481
|
}
|
|
482
|
+
if (/already exists/i.test(r.out)) {
|
|
483
|
+
// Exit code is 0 even when nothing changed — re-read the file to say
|
|
484
|
+
// honestly whether the existing entry is current or stale.
|
|
485
|
+
const now = detectEditorState(id, repo, root);
|
|
486
|
+
if (now.status === 'current')
|
|
487
|
+
return { id, outcome: 'already-current', detail: `${now.where} already points at ${repo}` };
|
|
488
|
+
return { id, outcome: 'stale', detail: `already exists${now.repo ? ` → ${now.repo}` : ''} — not updated. Run:\n${updateCommand(id, repo)}` };
|
|
489
|
+
}
|
|
490
|
+
return fail((r.out.trim() || `${bin} exited ${r.status}`).slice(0, 300));
|
|
253
491
|
}
|
|
254
|
-
|
|
255
|
-
|
|
492
|
+
// File-based harnesses: JSON merge is safe to write for both add and update.
|
|
493
|
+
const f = editorFile(id, root);
|
|
494
|
+
const entry = id === 'cursor' || id === 'cursor-user' || id === 'desktop' ? cursorEntry(repo) : opencodeEntry(repo);
|
|
495
|
+
if (upsertJsonMcp(f.path, f.key, entry)) {
|
|
496
|
+
return { id, outcome: 'added', detail: `wrote ${shortenHome(f.path)}${staleNote} → ${repo}` };
|
|
497
|
+
}
|
|
498
|
+
return fail(`${shortenHome(f.path)} is not valid JSON — leaving it untouched. Add manually:\n${editorCommand(id, repo)}`);
|
|
256
499
|
}
|
|
257
500
|
const EDITOR_LABELS = {
|
|
258
501
|
'claude-project': 'Claude Code (project scope)',
|
|
259
502
|
'claude-user': 'Claude Code (user scope)',
|
|
503
|
+
codex: 'Codex CLI (user config)',
|
|
504
|
+
cursor: 'Cursor (project file)',
|
|
505
|
+
'cursor-user': 'Cursor (user file)',
|
|
506
|
+
desktop: 'Claude Desktop (user config)',
|
|
507
|
+
opencode: 'OpenCode (project file)',
|
|
508
|
+
'opencode-user': 'OpenCode (user config)',
|
|
509
|
+
};
|
|
510
|
+
// Harnesses group the flat scope-specific ids for the two-step TUI picker:
|
|
511
|
+
// step 1 picks harnesses, step 2 picks the scope per multi-scope harness.
|
|
512
|
+
// Single-scope harnesses (codex, desktop) are user-global — no step 2.
|
|
513
|
+
const HARNESSES = ['claude', 'codex', 'cursor', 'desktop', 'opencode'];
|
|
514
|
+
const HARNESS_EDITORS = {
|
|
515
|
+
claude: ['claude-user', 'claude-project'],
|
|
516
|
+
codex: ['codex'],
|
|
517
|
+
cursor: ['cursor-user', 'cursor'],
|
|
518
|
+
desktop: ['desktop'],
|
|
519
|
+
opencode: ['opencode-user', 'opencode'],
|
|
520
|
+
};
|
|
521
|
+
const HARNESS_LABELS = {
|
|
522
|
+
claude: 'Claude Code',
|
|
260
523
|
codex: 'Codex CLI',
|
|
261
|
-
cursor: 'Cursor
|
|
524
|
+
cursor: 'Cursor',
|
|
262
525
|
desktop: 'Claude Desktop',
|
|
263
|
-
opencode: 'OpenCode
|
|
526
|
+
opencode: 'OpenCode',
|
|
264
527
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
528
|
+
// One-line install-state summary for the harness picker, e.g.
|
|
529
|
+
// "user: installed, project: —" or "installed → other/repo".
|
|
530
|
+
function harnessSummary(h, slug, root) {
|
|
531
|
+
const ids = HARNESS_EDITORS[h];
|
|
532
|
+
const parts = ids.map((id) => {
|
|
533
|
+
const st = detectEditorState(id, slug, root);
|
|
534
|
+
const cur = st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : '—';
|
|
535
|
+
return ids.length > 1 ? `${editorScope(id)}: ${cur}` : cur;
|
|
536
|
+
});
|
|
537
|
+
return parts.join(', ');
|
|
538
|
+
}
|
|
539
|
+
// Scope option for step 2 of the picker: short scope label + live state hint.
|
|
540
|
+
function scopeOption(id, slug, root) {
|
|
541
|
+
const st = detectEditorState(id, slug, root);
|
|
542
|
+
return {
|
|
543
|
+
value: id,
|
|
544
|
+
label: editorTarget(id, root),
|
|
545
|
+
hint: st.status === 'current' ? 'installed' : st.status === 'stale' ? `installed → ${st.repo ?? 'unknown'}` : undefined,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// One honest report for every editor install attempt — always shows the
|
|
549
|
+
// command/config that was (or should be) applied, so "installed" can never
|
|
550
|
+
// mean "already existed but points elsewhere".
|
|
551
|
+
function reportInstalls(results, repo, root) {
|
|
552
|
+
for (const r of results) {
|
|
553
|
+
const target = editorTarget(r.id, root);
|
|
554
|
+
if (r.outcome === 'added')
|
|
555
|
+
console.log(`ok ${EDITOR_LABELS[r.id]} → ${target}:\n ${editorCommand(r.id, repo).split('\n').join('\n ')}\n installed (${r.detail})`);
|
|
556
|
+
else if (r.outcome === 'already-current')
|
|
557
|
+
console.log(`skip ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
|
|
558
|
+
else if (r.outcome === 'stale')
|
|
559
|
+
console.log(`warn ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
|
|
560
|
+
else
|
|
561
|
+
console.log(`fail ${EDITOR_LABELS[r.id]} → ${target}: ${r.detail}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/** Poll a viewer URL until it serves 200 (slow first builds take minutes). */
|
|
565
|
+
async function waitForLive(url, timeoutMs = 7 * 60_000) {
|
|
566
|
+
const deadline = Date.now() + timeoutMs;
|
|
567
|
+
while (Date.now() < deadline) {
|
|
568
|
+
try {
|
|
569
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
570
|
+
if (r.ok)
|
|
571
|
+
return true;
|
|
572
|
+
// 404 during initial propagation — keep waiting; anything else too.
|
|
573
|
+
}
|
|
574
|
+
catch { /* offline / DNS — keep waiting */ }
|
|
575
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
269
578
|
}
|
|
270
579
|
function parseFlag(args, name) {
|
|
271
580
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
@@ -288,11 +597,12 @@ function parseEditorsFlag(args) {
|
|
|
288
597
|
}
|
|
289
598
|
return valid;
|
|
290
599
|
}
|
|
291
|
-
async function cmdSetup(args, editorOverride) {
|
|
292
|
-
const positional = args.find((a) => !a.startsWith('-') && a.includes('/'));
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
600
|
+
async function cmdSetup(args, editorOverride, opts = {}) {
|
|
601
|
+
const positional = args.find((a) => !a.startsWith('-') && (a.includes('/') || a.includes('github.com')));
|
|
602
|
+
const rawSlug = parseFlag(args, 'repo') || positional || detectSlug();
|
|
603
|
+
const slug = normalizeRepoSlug(rawSlug);
|
|
604
|
+
if (!slug) {
|
|
605
|
+
console.error(`Usage: npx excalidrop setup ${SLUG_HINT} [--target pages|cloudflare] [--editor claude-project,codex] [--no-prompt]`);
|
|
296
606
|
process.exit(1);
|
|
297
607
|
}
|
|
298
608
|
if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
|
|
@@ -311,7 +621,7 @@ async function cmdSetup(args, editorOverride) {
|
|
|
311
621
|
}
|
|
312
622
|
const explicitTarget = parseFlag(args, 'target');
|
|
313
623
|
const target = await resolveTarget(slug, explicitTarget);
|
|
314
|
-
console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? ' (explicit)' : ' (auto: private→cloudflare, public→pages)'}\n`);
|
|
624
|
+
console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? (opts.fromTui ? ' (selected)' : ' (explicit)') : ' (auto: private→cloudflare, public→pages)'}\n`);
|
|
315
625
|
await ensureMainBranch(slug);
|
|
316
626
|
const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
|
|
317
627
|
if (!scene.sha) {
|
|
@@ -324,9 +634,39 @@ async function cmdSetup(args, editorOverride) {
|
|
|
324
634
|
viewerUrl = viewerUrlFor(slug, 'pages');
|
|
325
635
|
}
|
|
326
636
|
else {
|
|
327
|
-
|
|
328
|
-
|
|
637
|
+
// Fail fast on missing auth instead of dying mid-deploy. The TUI checks
|
|
638
|
+
// interactively before the spinner; non-interactive runs land here.
|
|
639
|
+
const acct = wranglerAccount();
|
|
640
|
+
if (!acct.ok && !opts.fromTui) {
|
|
641
|
+
throw new Error('Not logged into Cloudflare (wrangler whoami failed). Run `npx -y wrangler@4 login` first, or set CLOUDFLARE_API_TOKEN.');
|
|
642
|
+
}
|
|
643
|
+
console.log(`Cloudflare account: ${acct.display || 'unknown'} — the viewer deploys to YOUR account and quota.`);
|
|
644
|
+
// TUI asks for this (prompt below); --project-name sets it headless.
|
|
645
|
+
// NOTE: renaming the project changes the viewer URL, so a previously
|
|
646
|
+
// registered OAuth callback URL stops matching — re-register login (see
|
|
647
|
+
// README "Browser login") after a rename.
|
|
648
|
+
const project = normalizeProjectName(parseFlag(args, 'project-name') || opts.project || '', projectNameForRepo(slug));
|
|
649
|
+
console.log(`Cloudflare project: ${project} (viewer host ${project}.pages.dev)`);
|
|
650
|
+
deployToCloudflarePages(project);
|
|
651
|
+
viewerUrl = viewerUrlFor(slug, 'cloudflare', project);
|
|
329
652
|
}
|
|
653
|
+
// Don't declare victory until the viewer actually serves — first Pages /
|
|
654
|
+
// Cloudflare builds take minutes, and "Canvas live" on a 404 wastes a debug
|
|
655
|
+
// session. Poll the URL (up to 7 min for slow first builds), then report
|
|
656
|
+
// honestly with the Pages build state attached.
|
|
657
|
+
const live = await waitForLive(viewerUrl);
|
|
658
|
+
if (live) {
|
|
659
|
+
console.log('Viewer is serving.');
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
console.log('Viewer not live yet (HTTP 404) — hosting may still be building; retry the URL in a minute.');
|
|
663
|
+
const info = await pagesBuildInfo(slug, target).catch(() => null);
|
|
664
|
+
if (info)
|
|
665
|
+
console.log(info);
|
|
666
|
+
}
|
|
667
|
+
await ensureRepoHomepage(slug, viewerUrl);
|
|
668
|
+
const appSlug = process.env.EXCALIDROP_APP_SLUG || 'excalidrop';
|
|
669
|
+
console.log(`Note: browser login stays read-only until the Excalidrop app is installed on ${slug}:\n https://github.com/apps/${appSlug}/installations/new`);
|
|
330
670
|
const root = findProjectRoot(process.cwd());
|
|
331
671
|
fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
|
|
332
672
|
const mcpPath = path.join(root, MCP_JSON);
|
|
@@ -346,18 +686,28 @@ async function cmdSetup(args, editorOverride) {
|
|
|
346
686
|
console.log(`MCP entry written to ${mcpPath}`);
|
|
347
687
|
const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
|
|
348
688
|
const noPrompt = args.includes('--no-prompt') || args.includes('-y');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
689
|
+
const installs = editors.map((id) => installEditor(id, slug, root));
|
|
690
|
+
// The TUI prints its own per-editor report (it owns the spinner); the plain
|
|
691
|
+
// CLI prints here so `setup --no-prompt` shows exactly what ran.
|
|
692
|
+
if (!opts.fromTui)
|
|
693
|
+
reportInstalls(installs, slug, root);
|
|
694
|
+
void noPrompt;
|
|
695
|
+
return { repo: slug, viewerUrl, live, installs };
|
|
696
|
+
}
|
|
697
|
+
// Latest GitHub Pages build state for a repo — printed when the viewer URL
|
|
698
|
+
// still 404s after the wait so the user knows whether it's "still building"
|
|
699
|
+
// vs "build errored / Pages misconfigured".
|
|
700
|
+
async function pagesBuildInfo(repo, target) {
|
|
701
|
+
if (target !== 'pages')
|
|
702
|
+
return `Cloudflare deploy is async — check the Pages project dashboard; if still 404, rerun: npx excalidrop publish ${repo}`;
|
|
703
|
+
try {
|
|
704
|
+
const latest = await ghGet(`/repos/${repo}/pages/builds/latest`);
|
|
705
|
+
const err = latest?.error?.message ? ` — ${latest.error.message}` : '';
|
|
706
|
+
return `Pages latest build: ${latest.status}${err} (updated ${latest.updated_at || 'unknown'}). Check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}) — first builds can take several minutes. If stuck, rerun: npx excalidrop publish ${repo}`;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return `Pages build status unknown — check https://github.com/${repo}/settings/pages (source should be branch ${PAGES_BRANCH}).`;
|
|
710
|
+
}
|
|
361
711
|
}
|
|
362
712
|
function cmdMcp() {
|
|
363
713
|
const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
|
|
@@ -419,18 +769,29 @@ async function runTui() {
|
|
|
419
769
|
const detected = detectSlug();
|
|
420
770
|
const repoAnswer = await clack.text({
|
|
421
771
|
message: 'Which repo holds the canvas?',
|
|
422
|
-
placeholder: detected ||
|
|
772
|
+
placeholder: detected || SLUG_HINT,
|
|
423
773
|
initialValue: detected || '',
|
|
424
|
-
validate: (v) => (
|
|
774
|
+
validate: (v) => (normalizeRepoSlug(v) ? undefined : `Use ${SLUG_HINT}`),
|
|
425
775
|
});
|
|
426
776
|
if (clack.isCancel(repoAnswer)) {
|
|
427
777
|
clack.cancel('Aborted.');
|
|
428
778
|
process.exit(0);
|
|
429
779
|
}
|
|
430
|
-
const slug = repoAnswer
|
|
780
|
+
const slug = normalizeRepoSlug(repoAnswer);
|
|
431
781
|
const priv = await isPrivateRepo(slug);
|
|
432
782
|
if (priv !== null)
|
|
433
783
|
clack.log.info(`Repo is ${priv ? 'private' : 'public'} — recommended host: ${priv ? 'Cloudflare Pages' : 'GitHub Pages'}.`);
|
|
784
|
+
// Visibility drift: repo flipped public<->private since last setup, so the
|
|
785
|
+
// stored target + viewer URL no longer match — nudge to re-pick below.
|
|
786
|
+
try {
|
|
787
|
+
const prevRaw = fs.readFileSync(path.join(findProjectRoot(process.cwd()), CONFIG_NAME), 'utf8');
|
|
788
|
+
const prevTarget = JSON.parse(prevRaw).target;
|
|
789
|
+
const want = priv === true ? 'cloudflare' : 'pages';
|
|
790
|
+
if (prevTarget && prevTarget !== want && priv !== null) {
|
|
791
|
+
clack.log.warn(`Visibility changed since last setup (host: ${prevTarget}, repo now ${priv ? 'private' : 'public'}). Pick ${want} below to migrate — see README "Migrating public<->private".`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch { /* first setup — nothing to compare */ }
|
|
434
795
|
if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
|
|
435
796
|
const how = await clack.select({
|
|
436
797
|
message: 'GitHub auth missing. How to log in?',
|
|
@@ -461,75 +822,189 @@ async function runTui() {
|
|
|
461
822
|
process.exit(1);
|
|
462
823
|
}
|
|
463
824
|
}
|
|
464
|
-
|
|
825
|
+
// No auto option: GitHub Pages can't serve private repos on free plans
|
|
826
|
+
// and public repos need zero config, so visibility pre-selects the right
|
|
827
|
+
// host — the user just confirms or flips. Non-interactive `--target` still
|
|
828
|
+
// goes through resolveTarget() in cmdSetup.
|
|
829
|
+
const recommended = priv === true ? 'cloudflare' : 'pages';
|
|
465
830
|
const hostAnswer = await clack.select({
|
|
466
|
-
message:
|
|
831
|
+
message: 'Where to host the viewer?',
|
|
832
|
+
initialValue: recommended,
|
|
467
833
|
options: [
|
|
468
|
-
{ value: '
|
|
469
|
-
{ value: '
|
|
470
|
-
{ value: 'cloudflare', label: 'Cloudflare Pages', hint: 'private repos' },
|
|
834
|
+
{ value: 'pages', label: 'GitHub Pages (public repo)', hint: 'zero config' },
|
|
835
|
+
{ value: 'cloudflare', label: 'Cloudflare Pages (private repo)', hint: 'needs wrangler login' },
|
|
471
836
|
],
|
|
472
837
|
});
|
|
473
838
|
if (clack.isCancel(hostAnswer)) {
|
|
474
839
|
clack.cancel('Aborted.');
|
|
475
840
|
process.exit(0);
|
|
476
841
|
}
|
|
477
|
-
const target =
|
|
478
|
-
|
|
842
|
+
const target = hostAnswer;
|
|
843
|
+
// Cloudflare project name = the *.pages.dev subdomain. Offer the choice
|
|
844
|
+
// up front so the URL is predictable (and re-runnable without surprises).
|
|
845
|
+
let project = projectNameForRepo(slug);
|
|
846
|
+
if (target === 'cloudflare') {
|
|
847
|
+
const nameAnswer = await clack.text({
|
|
848
|
+
message: 'Cloudflare project name (viewer host)?',
|
|
849
|
+
placeholder: project,
|
|
850
|
+
initialValue: project,
|
|
851
|
+
validate: (v) => (normalizeProjectName(v, '') ? undefined : 'Lowercase letters, numbers, hyphens.'),
|
|
852
|
+
});
|
|
853
|
+
if (clack.isCancel(nameAnswer)) {
|
|
854
|
+
clack.cancel('Aborted.');
|
|
855
|
+
process.exit(0);
|
|
856
|
+
}
|
|
857
|
+
project = normalizeProjectName(nameAnswer, project);
|
|
858
|
+
}
|
|
859
|
+
// Cloudflare deploys run under the USER's own wrangler identity and quota.
|
|
860
|
+
// Verify login up front (with an offer to log in now) instead of failing
|
|
861
|
+
// minutes later mid-deploy.
|
|
862
|
+
if (target === 'cloudflare') {
|
|
863
|
+
let acct = wranglerAccount();
|
|
864
|
+
if (!acct.ok) {
|
|
865
|
+
const login = await clack.confirm({
|
|
866
|
+
message: 'Wrangler is not logged into Cloudflare. Log in now (opens a browser)?',
|
|
867
|
+
});
|
|
868
|
+
if (clack.isCancel(login)) {
|
|
869
|
+
clack.cancel('Aborted.');
|
|
870
|
+
process.exit(0);
|
|
871
|
+
}
|
|
872
|
+
if (login) {
|
|
873
|
+
spawnSync('npx', [...WRANGLER, 'login'], { stdio: 'inherit', env: process.env });
|
|
874
|
+
acct = wranglerAccount();
|
|
875
|
+
}
|
|
876
|
+
if (!acct.ok) {
|
|
877
|
+
clack.log.error('Cloudflare login is required for Cloudflare Pages hosting — pick GitHub Pages, or run `npx -y wrangler@4 login` first.');
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
|
|
882
|
+
}
|
|
883
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
884
|
+
// Step 1: pick harnesses. Preselect ones already wired to this repo;
|
|
885
|
+
// fall back to the historical default when nothing is installed yet.
|
|
886
|
+
const detectedHarnesses = HARNESSES.filter((h) => HARNESS_EDITORS[h].some((id) => detectEditorState(id, slug, projectRoot).status === 'current'));
|
|
887
|
+
const harnessAnswer = await clack.multiselect({
|
|
479
888
|
message: 'Install the MCP server into which editors?',
|
|
480
|
-
options:
|
|
481
|
-
|
|
889
|
+
options: HARNESSES.map((h) => ({
|
|
890
|
+
value: h,
|
|
891
|
+
label: `${HARNESS_LABELS[h]} (${harnessSummary(h, slug, projectRoot)})`,
|
|
892
|
+
})),
|
|
893
|
+
initialValues: detectedHarnesses.length > 0 ? detectedHarnesses : ['claude'],
|
|
482
894
|
required: false,
|
|
483
895
|
});
|
|
484
|
-
if (clack.isCancel(
|
|
896
|
+
if (clack.isCancel(harnessAnswer)) {
|
|
485
897
|
clack.cancel('Aborted.');
|
|
486
898
|
process.exit(0);
|
|
487
899
|
}
|
|
488
|
-
const
|
|
900
|
+
const harnesses = harnessAnswer;
|
|
901
|
+
// Step 2: per multi-scope harness, pick where to install.
|
|
902
|
+
const editors = [];
|
|
903
|
+
for (const h of harnesses) {
|
|
904
|
+
const ids = HARNESS_EDITORS[h];
|
|
905
|
+
if (ids.length === 1) {
|
|
906
|
+
editors.push(ids[0]);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const current = ids.filter((id) => detectEditorState(id, slug, projectRoot).status === 'current');
|
|
910
|
+
const scopeAnswer = await clack.select({
|
|
911
|
+
message: `Where to install for ${HARNESS_LABELS[h]}?`,
|
|
912
|
+
initialValue: current.length === 1 ? current[0] : ids[0],
|
|
913
|
+
options: [
|
|
914
|
+
...ids.map((id) => scopeOption(id, slug, projectRoot)),
|
|
915
|
+
{ value: '__both__', label: 'Both scopes' },
|
|
916
|
+
],
|
|
917
|
+
});
|
|
918
|
+
if (clack.isCancel(scopeAnswer)) {
|
|
919
|
+
clack.cancel('Aborted.');
|
|
920
|
+
process.exit(0);
|
|
921
|
+
}
|
|
922
|
+
if (scopeAnswer === '__both__')
|
|
923
|
+
editors.push(...ids);
|
|
924
|
+
else
|
|
925
|
+
editors.push(scopeAnswer);
|
|
926
|
+
}
|
|
927
|
+
// Stale Claude entries can't be overwritten by `add` (it exits 0 with
|
|
928
|
+
// "already exists" and changes nothing) — offer an explicit repoint before
|
|
929
|
+
// the deploy phase so the install below actually lands.
|
|
930
|
+
for (const id of editors) {
|
|
931
|
+
if (id !== 'claude-user' && id !== 'claude-project')
|
|
932
|
+
continue;
|
|
933
|
+
const st = detectEditorState(id, slug, projectRoot);
|
|
934
|
+
if (st.status !== 'stale')
|
|
935
|
+
continue;
|
|
936
|
+
const ok = await clack.confirm({
|
|
937
|
+
message: `${EDITOR_LABELS[id]} points at ${st.repo ?? 'unknown'} — repoint to ${slug}?`,
|
|
938
|
+
});
|
|
939
|
+
if (clack.isCancel(ok)) {
|
|
940
|
+
clack.cancel('Aborted.');
|
|
941
|
+
process.exit(0);
|
|
942
|
+
}
|
|
943
|
+
if (ok) {
|
|
944
|
+
const scope = id === 'claude-project' ? 'project' : 'user';
|
|
945
|
+
runCapture('claude', ['mcp', 'remove', 'excalidrop', '-s', scope]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
489
948
|
const s = clack.spinner();
|
|
490
949
|
s.start('Publishing viewer + wiring MCP…');
|
|
491
950
|
let viewerUrl;
|
|
951
|
+
let live = false;
|
|
952
|
+
let installs = [];
|
|
492
953
|
try {
|
|
493
|
-
({ viewerUrl } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors));
|
|
954
|
+
({ viewerUrl, live, installs } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors, { fromTui: true, project }));
|
|
494
955
|
s.stop('Setup complete.');
|
|
495
956
|
}
|
|
496
957
|
catch (e) {
|
|
497
958
|
s.stop('Setup failed: ' + e.message);
|
|
498
959
|
process.exit(1);
|
|
499
960
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
961
|
+
// Always show what ran where: installing to user scope prints the command
|
|
962
|
+
// itself; file writes print the path; stale entries print the update command.
|
|
963
|
+
for (const r of installs) {
|
|
964
|
+
const target = editorTarget(r.id, projectRoot);
|
|
965
|
+
const cmd = editorCommand(r.id, slug).split('\n').join('\n ');
|
|
966
|
+
if (r.outcome === 'added')
|
|
967
|
+
clack.log.success(`MCP → ${target}:\n ${cmd}\n ${r.detail}`);
|
|
968
|
+
else if (r.outcome === 'already-current')
|
|
969
|
+
clack.log.success(`MCP → ${target}: ${r.detail} — skipped.`);
|
|
970
|
+
else if (r.outcome === 'stale')
|
|
971
|
+
clack.log.warn(`MCP → ${target}: ${r.detail}`);
|
|
972
|
+
else
|
|
973
|
+
clack.log.warn(`MCP → ${target} failed: ${r.detail}`);
|
|
974
|
+
}
|
|
975
|
+
// Post-deploy readability probe: one retry — right after a deploy the
|
|
976
|
+
// first API read intermittently fails at TCP level ("fetch failed")
|
|
977
|
+
// while the retry succeeds, and a false "not readable" scares users.
|
|
978
|
+
let scene = null;
|
|
979
|
+
let sceneError = null;
|
|
980
|
+
for (let attempt = 0; attempt < 2 && !scene; attempt++) {
|
|
981
|
+
if (attempt > 0)
|
|
982
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
983
|
+
try {
|
|
984
|
+
scene = await getScene(slug);
|
|
503
985
|
}
|
|
504
|
-
|
|
505
|
-
|
|
986
|
+
catch (e) {
|
|
987
|
+
sceneError = e;
|
|
506
988
|
}
|
|
507
989
|
}
|
|
508
|
-
|
|
509
|
-
const scene = await getScene(slug);
|
|
990
|
+
if (scene) {
|
|
510
991
|
clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
|
|
511
992
|
}
|
|
512
|
-
|
|
513
|
-
clack.log.warn(`Scene not readable yet: ${
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
const r = await fetch(viewerUrl, { signal: AbortSignal.timeout(15000) });
|
|
517
|
-
if (r.ok)
|
|
518
|
-
clack.log.success('Viewer is serving.');
|
|
519
|
-
else
|
|
520
|
-
clack.log.warn(`Viewer returned HTTP ${r.status} — Pages/Cloudflare may still be building; retry in a minute.`);
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
clack.log.warn('Viewer not reachable yet — hosting may still be building; retry in a minute.');
|
|
993
|
+
else {
|
|
994
|
+
clack.log.warn(`Scene not readable yet: ${sceneError?.message || sceneError} — draw once to create it.`);
|
|
524
995
|
}
|
|
525
|
-
|
|
996
|
+
if (live)
|
|
997
|
+
clack.log.success('Viewer is serving.');
|
|
998
|
+
else
|
|
999
|
+
clack.log.warn('Viewer was not live after 7 minutes — check the Pages/Cloudflare build, then retry the URL.');
|
|
1000
|
+
clack.outro(live ? `Canvas live: ${viewerUrl}` : `Canvas pending: ${viewerUrl}`);
|
|
526
1001
|
}
|
|
527
1002
|
function printHelp() {
|
|
528
1003
|
console.log(`excalidrop — remote GitHub-backed canvas (no local server)
|
|
529
1004
|
|
|
530
1005
|
Usage:
|
|
531
1006
|
npx excalidrop interactive setup TUI
|
|
532
|
-
npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex,cursor,desktop,opencode] [--no-prompt]
|
|
1007
|
+
npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,claude-user,codex,cursor,cursor-user,desktop,opencode,opencode-user] [--no-prompt]
|
|
533
1008
|
npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
|
|
534
1009
|
npx excalidrop login [--repo owner/repo] GitHub device-flow login
|
|
535
1010
|
npx excalidrop publish owner/repo [--target ..] redeploy viewer only
|
|
@@ -564,9 +1039,19 @@ async function main() {
|
|
|
564
1039
|
case 'publish':
|
|
565
1040
|
await cmdSetup(['--no-prompt', ...rest]);
|
|
566
1041
|
break;
|
|
567
|
-
case 'add':
|
|
568
|
-
|
|
1042
|
+
case 'add': {
|
|
1043
|
+
const root = findProjectRoot(process.cwd());
|
|
1044
|
+
const ids = parseEditorsFlag(rest) || [...EDITOR_IDS];
|
|
1045
|
+
const raw = rest.find((a) => a.includes('/') || a.includes('github.com')) || detectSlug() || 'owner/repo';
|
|
1046
|
+
const repo = normalizeRepoSlug(raw) || raw;
|
|
1047
|
+
console.log('Add excalidrop to your AI agent (no install, always latest):\n');
|
|
1048
|
+
for (const id of ids) {
|
|
1049
|
+
const st = repo.includes('/') ? detectEditorState(id, repo, root) : null;
|
|
1050
|
+
const mark = st?.status === 'current' ? ' (installed)' : st?.status === 'stale' ? ` (installed → ${st.repo ?? 'unknown'})` : '';
|
|
1051
|
+
console.log(`## ${EDITOR_LABELS[id]} → ${editorTarget(id, root)}${mark}\n${editorCommand(id, repo)}\n`);
|
|
1052
|
+
}
|
|
569
1053
|
break;
|
|
1054
|
+
}
|
|
570
1055
|
case 'init':
|
|
571
1056
|
case 'up':
|
|
572
1057
|
case 'remote':
|