excalidrop 0.2.1 → 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 +19 -1
- package/dist/cli.d.ts +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +538 -85
- 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
|
}
|
|
@@ -183,10 +202,22 @@ async function resolveTarget(repo, explicit) {
|
|
|
183
202
|
return explicit;
|
|
184
203
|
return (await isPrivateRepo(repo)) === true ? 'cloudflare' : 'pages';
|
|
185
204
|
}
|
|
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
|
+
}
|
|
186
218
|
function deployToCloudflarePages(project) {
|
|
187
219
|
// wrangler 4 doesn't auto-create Pages projects and its Workers delegation
|
|
188
220
|
// misfires on explicit asset dirs — create once, then deploy classic with --force.
|
|
189
|
-
const WRANGLER = ['-y', 'wrangler@4'];
|
|
190
221
|
const create = spawnSync('npx', [...WRANGLER, 'pages', 'project', 'create', project, '--force', '--production-branch=main'], { stdio: 'pipe', encoding: 'utf8', env: process.env });
|
|
191
222
|
const createOut = (create.stdout || '') + (create.stderr || '');
|
|
192
223
|
if (create.status !== 0 && !/already exists/i.test(createOut)) {
|
|
@@ -205,6 +236,19 @@ function detectSlug() {
|
|
|
205
236
|
return '';
|
|
206
237
|
}
|
|
207
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';
|
|
208
252
|
function ghAuthed() {
|
|
209
253
|
return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
|
|
210
254
|
}
|
|
@@ -213,59 +257,314 @@ function commandExists(cmd) {
|
|
|
213
257
|
return probe.status === 0;
|
|
214
258
|
}
|
|
215
259
|
function findProjectRoot(cwd) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
224
|
-
dir = parent;
|
|
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;
|
|
225
268
|
}
|
|
269
|
+
catch { /* not inside a git work tree — fall through to cwd */ }
|
|
270
|
+
return path.resolve(cwd);
|
|
226
271
|
}
|
|
227
272
|
function mcpArgs(repo) {
|
|
228
273
|
return ['-y', 'excalidrop@latest', 'mcp', '--repo', repo];
|
|
229
274
|
}
|
|
230
|
-
const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'desktop', 'opencode'];
|
|
275
|
+
const EDITOR_IDS = ['claude-project', 'claude-user', 'codex', 'cursor', 'cursor-user', 'desktop', 'opencode', 'opencode-user'];
|
|
231
276
|
function editorCommand(id, repo) {
|
|
232
277
|
const a = mcpArgs(repo).join(' ');
|
|
233
278
|
switch (id) {
|
|
234
279
|
case 'claude-project': return `claude mcp add excalidrop --scope project -- npx ${a}`;
|
|
235
280
|
case 'claude-user': return `claude mcp add excalidrop --scope user -- npx ${a}`;
|
|
236
281
|
case 'codex': return `codex mcp add excalidrop -- npx ${a}`;
|
|
237
|
-
case 'cursor':
|
|
282
|
+
case 'cursor':
|
|
283
|
+
case 'cursor-user':
|
|
238
284
|
case 'desktop': return JSON.stringify({ mcpServers: { excalidrop: { command: 'npx', args: mcpArgs(repo) } } }, null, 2);
|
|
239
|
-
case 'opencode':
|
|
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)}`;
|
|
240
391
|
}
|
|
392
|
+
return editorCommand(id, repo);
|
|
241
393
|
}
|
|
242
|
-
function
|
|
394
|
+
function runCapture(cmd, args) {
|
|
243
395
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
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)) {
|
|
408
|
+
try {
|
|
409
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
246
410
|
}
|
|
247
|
-
|
|
248
|
-
return
|
|
411
|
+
catch {
|
|
412
|
+
return false;
|
|
249
413
|
}
|
|
250
|
-
if (
|
|
251
|
-
return
|
|
414
|
+
if (!j || typeof j !== 'object')
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
try {
|
|
419
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return false;
|
|
252
423
|
}
|
|
253
424
|
}
|
|
254
|
-
|
|
255
|
-
|
|
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) };
|
|
471
|
+
}
|
|
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)}` };
|
|
479
|
+
}
|
|
480
|
+
return fail((r.out.trim() || `${bin} exited ${r.status}`).slice(0, 300));
|
|
481
|
+
}
|
|
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}` };
|
|
487
|
+
}
|
|
488
|
+
return fail(`${shortenHome(f.path)} is not valid JSON — leaving it untouched. Add manually:\n${editorCommand(id, repo)}`);
|
|
256
489
|
}
|
|
257
490
|
const EDITOR_LABELS = {
|
|
258
491
|
'claude-project': 'Claude Code (project scope)',
|
|
259
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',
|
|
260
513
|
codex: 'Codex CLI',
|
|
261
|
-
cursor: 'Cursor
|
|
514
|
+
cursor: 'Cursor',
|
|
262
515
|
desktop: 'Claude Desktop',
|
|
263
|
-
opencode: 'OpenCode
|
|
516
|
+
opencode: 'OpenCode',
|
|
264
517
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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}`);
|
|
552
|
+
}
|
|
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.
|
|
563
|
+
}
|
|
564
|
+
catch { /* offline / DNS — keep waiting */ }
|
|
565
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
269
568
|
}
|
|
270
569
|
function parseFlag(args, name) {
|
|
271
570
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
@@ -288,11 +587,12 @@ function parseEditorsFlag(args) {
|
|
|
288
587
|
}
|
|
289
588
|
return valid;
|
|
290
589
|
}
|
|
291
|
-
async function cmdSetup(args, editorOverride) {
|
|
292
|
-
const positional = args.find((a) => !a.startsWith('-') && a.includes('/'));
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
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]`);
|
|
296
596
|
process.exit(1);
|
|
297
597
|
}
|
|
298
598
|
if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
|
|
@@ -311,7 +611,7 @@ async function cmdSetup(args, editorOverride) {
|
|
|
311
611
|
}
|
|
312
612
|
const explicitTarget = parseFlag(args, 'target');
|
|
313
613
|
const target = await resolveTarget(slug, explicitTarget);
|
|
314
|
-
console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? ' (explicit)' : ' (auto: private→cloudflare, public→pages)'}\n`);
|
|
614
|
+
console.log(`Repo: ${slug}\nHost: ${target}${explicitTarget ? (opts.fromTui ? ' (selected)' : ' (explicit)') : ' (auto: private→cloudflare, public→pages)'}\n`);
|
|
315
615
|
await ensureMainBranch(slug);
|
|
316
616
|
const scene = await getScene(slug).catch(() => ({ elements: [], sha: null }));
|
|
317
617
|
if (!scene.sha) {
|
|
@@ -324,9 +624,33 @@ async function cmdSetup(args, editorOverride) {
|
|
|
324
624
|
viewerUrl = viewerUrlFor(slug, 'pages');
|
|
325
625
|
}
|
|
326
626
|
else {
|
|
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.`);
|
|
327
634
|
deployToCloudflarePages(projectNameForRepo(slug));
|
|
328
635
|
viewerUrl = viewerUrlFor(slug, 'cloudflare');
|
|
329
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`);
|
|
330
654
|
const root = findProjectRoot(process.cwd());
|
|
331
655
|
fs.writeFileSync(path.join(root, CONFIG_NAME), JSON.stringify({ remote: slug, target, viewerUrl }, null, 2) + '\n');
|
|
332
656
|
const mcpPath = path.join(root, MCP_JSON);
|
|
@@ -346,18 +670,28 @@ async function cmdSetup(args, editorOverride) {
|
|
|
346
670
|
console.log(`MCP entry written to ${mcpPath}`);
|
|
347
671
|
const editors = editorOverride || parseEditorsFlag(args) || [...EDITOR_IDS];
|
|
348
672
|
const noPrompt = args.includes('--no-prompt') || args.includes('-y');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
695
|
}
|
|
362
696
|
function cmdMcp() {
|
|
363
697
|
const child = spawn(process.execPath, [path.join(__dirname, 'mcp.js'), ...process.argv.slice(3)], { stdio: 'inherit', env: process.env });
|
|
@@ -419,18 +753,29 @@ async function runTui() {
|
|
|
419
753
|
const detected = detectSlug();
|
|
420
754
|
const repoAnswer = await clack.text({
|
|
421
755
|
message: 'Which repo holds the canvas?',
|
|
422
|
-
placeholder: detected ||
|
|
756
|
+
placeholder: detected || SLUG_HINT,
|
|
423
757
|
initialValue: detected || '',
|
|
424
|
-
validate: (v) => (
|
|
758
|
+
validate: (v) => (normalizeRepoSlug(v) ? undefined : `Use ${SLUG_HINT}`),
|
|
425
759
|
});
|
|
426
760
|
if (clack.isCancel(repoAnswer)) {
|
|
427
761
|
clack.cancel('Aborted.');
|
|
428
762
|
process.exit(0);
|
|
429
763
|
}
|
|
430
|
-
const slug = repoAnswer
|
|
764
|
+
const slug = normalizeRepoSlug(repoAnswer);
|
|
431
765
|
const priv = await isPrivateRepo(slug);
|
|
432
766
|
if (priv !== null)
|
|
433
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".`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch { /* first setup — nothing to compare */ }
|
|
434
779
|
if (!ghAuthed() && !process.env.GITHUB_TOKEN) {
|
|
435
780
|
const how = await clack.select({
|
|
436
781
|
message: 'GitHub auth missing. How to log in?',
|
|
@@ -461,75 +806,173 @@ async function runTui() {
|
|
|
461
806
|
process.exit(1);
|
|
462
807
|
}
|
|
463
808
|
}
|
|
464
|
-
|
|
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';
|
|
465
814
|
const hostAnswer = await clack.select({
|
|
466
|
-
message:
|
|
815
|
+
message: 'Where to host the viewer?',
|
|
816
|
+
initialValue: recommended,
|
|
467
817
|
options: [
|
|
468
|
-
{ value: '
|
|
469
|
-
{ value: '
|
|
470
|
-
{ value: 'cloudflare', label: 'Cloudflare Pages', hint: 'private repos' },
|
|
818
|
+
{ value: 'pages', label: 'GitHub Pages (public repo)', hint: 'zero config' },
|
|
819
|
+
{ value: 'cloudflare', label: 'Cloudflare Pages (private repo)', hint: 'needs wrangler login' },
|
|
471
820
|
],
|
|
472
821
|
});
|
|
473
822
|
if (clack.isCancel(hostAnswer)) {
|
|
474
823
|
clack.cancel('Aborted.');
|
|
475
824
|
process.exit(0);
|
|
476
825
|
}
|
|
477
|
-
const target =
|
|
478
|
-
|
|
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);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
clack.log.info(`Cloudflare account: ${acct.display} — the viewer deploys to YOUR account and quota.`);
|
|
850
|
+
}
|
|
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({
|
|
479
856
|
message: 'Install the MCP server into which editors?',
|
|
480
|
-
options:
|
|
481
|
-
|
|
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'],
|
|
482
862
|
required: false,
|
|
483
863
|
});
|
|
484
|
-
if (clack.isCancel(
|
|
864
|
+
if (clack.isCancel(harnessAnswer)) {
|
|
485
865
|
clack.cancel('Aborted.');
|
|
486
866
|
process.exit(0);
|
|
487
867
|
}
|
|
488
|
-
const
|
|
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
|
+
}
|
|
489
916
|
const s = clack.spinner();
|
|
490
917
|
s.start('Publishing viewer + wiring MCP…');
|
|
491
918
|
let viewerUrl;
|
|
919
|
+
let live = false;
|
|
920
|
+
let installs = [];
|
|
492
921
|
try {
|
|
493
|
-
({ viewerUrl } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors));
|
|
922
|
+
({ viewerUrl, live, installs } = await cmdSetup(['--no-prompt', slug, `--target=${target}`], editors, { fromTui: true }));
|
|
494
923
|
s.stop('Setup complete.');
|
|
495
924
|
}
|
|
496
925
|
catch (e) {
|
|
497
926
|
s.stop('Setup failed: ' + e.message);
|
|
498
927
|
process.exit(1);
|
|
499
928
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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);
|
|
503
953
|
}
|
|
504
|
-
|
|
505
|
-
|
|
954
|
+
catch (e) {
|
|
955
|
+
sceneError = e;
|
|
506
956
|
}
|
|
507
957
|
}
|
|
508
|
-
|
|
509
|
-
const scene = await getScene(slug);
|
|
958
|
+
if (scene) {
|
|
510
959
|
clack.log.success(`Scene readable: ${scene.elements.length} elements @${CANVAS_BRANCH}.`);
|
|
511
960
|
}
|
|
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.');
|
|
961
|
+
else {
|
|
962
|
+
clack.log.warn(`Scene not readable yet: ${sceneError?.message || sceneError} — draw once to create it.`);
|
|
524
963
|
}
|
|
525
|
-
|
|
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}`);
|
|
526
969
|
}
|
|
527
970
|
function printHelp() {
|
|
528
971
|
console.log(`excalidrop — remote GitHub-backed canvas (no local server)
|
|
529
972
|
|
|
530
973
|
Usage:
|
|
531
974
|
npx excalidrop interactive setup TUI
|
|
532
|
-
npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,codex,cursor,desktop,opencode] [--no-prompt]
|
|
975
|
+
npx excalidrop setup owner/repo [--target pages|cloudflare] [--editor claude-project,claude-user,codex,cursor,cursor-user,desktop,opencode,opencode-user] [--no-prompt]
|
|
533
976
|
npx -y excalidrop@latest mcp --repo owner/repo run MCP stdio server (editors use this)
|
|
534
977
|
npx excalidrop login [--repo owner/repo] GitHub device-flow login
|
|
535
978
|
npx excalidrop publish owner/repo [--target ..] redeploy viewer only
|
|
@@ -564,9 +1007,19 @@ async function main() {
|
|
|
564
1007
|
case 'publish':
|
|
565
1008
|
await cmdSetup(['--no-prompt', ...rest]);
|
|
566
1009
|
break;
|
|
567
|
-
case 'add':
|
|
568
|
-
|
|
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
|
+
}
|
|
569
1021
|
break;
|
|
1022
|
+
}
|
|
570
1023
|
case 'init':
|
|
571
1024
|
case 'up':
|
|
572
1025
|
case 'remote':
|