diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56f18c65..cbacb527 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -646,9 +646,15 @@ importers: hermes-paperclip-adapter: specifier: ^0.2.0 version: 0.2.0 + isomorphic-git: + specifier: ^1.38.0 + version: 1.38.0 jsdom: specifier: ^28.1.0 version: 28.1.0(@noble/hashes@2.0.1) + memfs: + specifier: ^4.57.2 + version: 4.57.2(tslib@2.8.1) multer: specifier: ^2.1.1 version: 2.1.1 @@ -2246,6 +2252,126 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/base64@17.67.0': + resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@17.67.0': + resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@17.67.0': + resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-core@4.57.2': + resolution: {integrity: sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-fsa@4.57.2': + resolution: {integrity: sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-builtins@4.57.2': + resolution: {integrity: sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-to-fsa@4.57.2': + resolution: {integrity: sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-utils@4.57.2': + resolution: {integrity: sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node@4.57.2': + resolution: {integrity: sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-print@4.57.2': + resolution: {integrity: sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-snapshot@4.57.2': + resolution: {integrity: sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@17.67.0': + resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@17.67.0': + resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@17.67.0': + resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@lexical/clipboard@0.35.0': resolution: {integrity: sha512-ko7xSIIiayvDiqjNDX6fgH9RlcM6r9vrrvJYTcfGVBor5httx16lhIi0QJZ4+RNPvGtTjyFv4bwRmsixRRwImg==} @@ -4067,6 +4193,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -4174,6 +4304,9 @@ packages: resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} engines: {node: '>=0.12.0'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -4181,6 +4314,10 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axe-core@4.11.3: resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} engines: {node: '>=4'} @@ -4382,6 +4519,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -4437,6 +4578,9 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-git-ref@2.0.1: + resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==} + clean-set@1.1.2: resolution: {integrity: sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==} @@ -4550,6 +4694,11 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -4790,6 +4939,10 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -4833,6 +4986,9 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diff3@0.0.3: + resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==} + diff@5.2.2: resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} engines: {node: '>=0.3.1'} @@ -5102,9 +5258,17 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -5184,6 +5348,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -5258,6 +5426,12 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-to-regex.js@1.2.0: + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -5276,6 +5450,9 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -5341,6 +5518,10 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5352,6 +5533,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -5405,6 +5590,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -5449,13 +5638,25 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-git@1.38.0: + resolution: {integrity: sha512-gsBFnAT8Fxrpx+53ymG5kEOHSrUDVcSMFl7fCEGVnPpQbPS0aKti3UzZXR+3DKA0yyf+4z6CXJxULlQ5QPxDJw==} + engines: {node: '>=14.17'} + hasBin: true + isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -5732,6 +5933,11 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + memfs@4.57.2: + resolution: {integrity: sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==} + peerDependencies: + tslib: '2' + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -5896,6 +6102,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimisted@2.0.1: + resolution: {integrity: sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==} + minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} engines: {node: '>= 8'} @@ -6044,6 +6253,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -6126,6 +6338,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -6169,6 +6385,10 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -6218,6 +6438,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -6385,6 +6609,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -6506,9 +6734,18 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6730,6 +6967,12 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thingies@2.6.0: + resolution: {integrity: sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} @@ -6769,6 +7012,10 @@ packages: resolution: {integrity: sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==} hasBin: true + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -6781,6 +7028,12 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -6820,6 +7073,10 @@ packages: type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -7126,6 +7383,10 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -8860,6 +9121,133 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-core@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + thingies: 2.6.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-fsa@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + thingies: 2.6.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-builtins@4.57.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-to-fsa@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-fsa': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-utils@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.57.2(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-print@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-snapshot@4.57.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + '@lexical/clipboard@0.35.0': dependencies: '@lexical/html': 0.35.0 @@ -11046,6 +11434,10 @@ snapshots: abbrev@1.1.1: optional: true + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -11151,10 +11543,16 @@ snapshots: async-exit-hook@2.0.1: {} + async-lock@1.4.1: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axe-core@4.11.3: {} b4a@1.8.1: {} @@ -11334,6 +11732,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -11389,6 +11794,8 @@ snapshots: classnames@2.5.1: {} + clean-git-ref@2.0.1: {} + clean-set@1.1.2: {} clean-stack@2.2.0: @@ -11490,6 +11897,8 @@ snapshots: dependencies: layout-base: 2.0.1 + crc-32@1.2.2: {} + crelt@1.0.6: {} cross-env@10.1.0: @@ -11748,6 +12157,12 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@3.0.0: {} defu@6.1.4: {} @@ -11782,6 +12197,8 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 + diff3@0.0.3: {} + diff@5.2.2: {} doctrine@3.0.0: @@ -12045,12 +12462,16 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + event-target-shim@5.0.1: {} + events-universal@1.0.1: dependencies: bare-events: 2.8.2 transitivePeerDependencies: - bare-abort-controller + events@3.3.0: {} + eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -12150,6 +12571,10 @@ snapshots: transitivePeerDependencies: - supports-color + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -12229,6 +12654,10 @@ snapshots: github-from-package@0.0.0: {} + glob-to-regex.js@1.2.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -12251,6 +12680,10 @@ snapshots: hachure-fill@0.5.2: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -12352,6 +12785,8 @@ snapshots: ms: 2.1.3 optional: true + hyperdyperid@1.2.0: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -12362,6 +12797,8 @@ snapshots: ieee754@1.2.1: {} + ignore@5.3.2: {} + imurmurhash@0.1.4: optional: true @@ -12402,6 +12839,8 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-callable@1.2.7: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -12432,12 +12871,32 @@ snapshots: is-promise@4.0.0: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 + isarray@2.0.5: {} + isexe@2.0.0: {} + isomorphic-git@1.38.0: + dependencies: + async-lock: 1.4.1 + clean-git-ref: 2.0.1 + crc-32: 1.2.2 + diff3: 0.0.3 + ignore: 5.3.2 + minimisted: 2.0.1 + pako: 1.0.11 + pify: 4.0.1 + readable-stream: 4.7.0 + sha.js: 2.4.12 + simple-get: 4.0.1 + isomorphic.js@0.2.5: {} jiti@2.6.1: {} @@ -12829,6 +13288,23 @@ snapshots: media-typer@1.1.0: {} + memfs@4.57.2(tslib@2.8.1): + dependencies: + '@jsonjoy.com/fs-core': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.57.2(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + merge-descriptors@2.0.0: {} mermaid@11.12.3: @@ -13175,6 +13651,10 @@ snapshots: minimist@1.2.8: {} + minimisted@2.0.1: + dependencies: + minimist: 1.2.8 + minipass-collect@1.0.2: dependencies: minipass: 3.3.6 @@ -13331,6 +13811,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@1.0.11: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -13410,6 +13892,8 @@ snapshots: picomatch@4.0.3: {} + pify@4.0.1: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -13480,6 +13964,8 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} + postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -13530,6 +14016,8 @@ snapshots: process-warning@5.0.0: {} + process@0.11.10: {} + promise-inflight@1.0.1: optional: true @@ -13763,6 +14251,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@4.1.2: {} real-require@0.2.0: {} @@ -13940,8 +14436,23 @@ snapshots: set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -14264,6 +14775,10 @@ snapshots: transitivePeerDependencies: - react-native-b4a + thingies@2.6.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 @@ -14293,6 +14808,12 @@ snapshots: dependencies: tldts-core: 7.0.26 + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + toidentifier@1.0.1: {} tough-cookie@6.0.1: @@ -14303,6 +14824,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -14343,6 +14868,12 @@ snapshots: type@2.7.3: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typedarray@0.0.6: {} typescript@5.9.3: {} @@ -14717,6 +15248,16 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/server/package.json b/server/package.json index 2ea875b8..0b677ba3 100644 --- a/server/package.json +++ b/server/package.json @@ -68,7 +68,9 @@ "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", "hermes-paperclip-adapter": "^0.2.0", + "isomorphic-git": "^1.38.0", "jsdom": "^28.1.0", + "memfs": "^4.57.2", "multer": "^2.1.1", "open": "^11.0.0", "pino": "^9.6.0", diff --git a/server/src/__tests__/git-source.test.ts b/server/src/__tests__/git-source.test.ts new file mode 100644 index 00000000..4ba1e335 --- /dev/null +++ b/server/src/__tests__/git-source.test.ts @@ -0,0 +1,336 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const listServerRefs = vi.fn(); +const cloneFn = vi.fn(); +const walkFn = vi.fn(); +const readBlobFn = vi.fn(); +const resolveRefFn = vi.fn(); +const treeFn = vi.fn((args: unknown) => ({ __tree: args })); + +vi.mock("isomorphic-git", () => ({ + default: { + listServerRefs: (...args: unknown[]) => listServerRefs(...args), + clone: (...args: unknown[]) => cloneFn(...args), + walk: (...args: unknown[]) => walkFn(...args), + readBlob: (...args: unknown[]) => readBlobFn(...args), + resolveRef: (...args: unknown[]) => resolveRefFn(...args), + TREE: (...args: unknown[]) => treeFn(...args), + }, +})); + +vi.mock("isomorphic-git/http/node", () => ({ + default: { request: vi.fn() }, +})); + +const { parseGitSourceUrl, resolveGitRef, openRepoSnapshot, buildCloneUrl } = + await import("../services/git-source.js"); + +beforeEach(() => { + listServerRefs.mockReset(); + cloneFn.mockReset(); + walkFn.mockReset(); + readBlobFn.mockReset(); + resolveRefFn.mockReset(); + treeFn.mockClear(); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("parseGitSourceUrl", () => { + it("parses a bare github repo URL", () => { + expect(parseGitSourceUrl("https://github.com/anthropics/claude-code")).toMatchObject({ + cloneUrl: "https://github.com/anthropics/claude-code.git", + hostname: "github.com", + owner: "anthropics", + repo: "claude-code", + ref: null, + basePath: "", + filePath: null, + explicitRef: false, + }); + }); + + it("strips trailing .git from the repo segment", () => { + expect(parseGitSourceUrl("https://example.com/o/r.git")).toMatchObject({ + cloneUrl: "https://example.com/o/r.git", + repo: "r", + }); + }); + + it("parses a github tree URL with subpath", () => { + expect( + parseGitSourceUrl("https://github.com/o/r/tree/develop/sub/dir"), + ).toMatchObject({ + ref: "develop", + basePath: "sub/dir", + filePath: null, + explicitRef: true, + }); + }); + + it("parses a github blob URL as a file path", () => { + expect( + parseGitSourceUrl("https://github.com/o/r/blob/main/path/to/file.md"), + ).toMatchObject({ + ref: "main", + basePath: "path/to", + filePath: "path/to/file.md", + explicitRef: true, + }); + }); + + it("parses a gitea src/branch URL with subpath", () => { + expect( + parseGitSourceUrl("https://git.example.com/o/r/src/branch/main/skills"), + ).toMatchObject({ + cloneUrl: "https://git.example.com/o/r.git", + ref: "main", + basePath: "skills", + filePath: null, + explicitRef: true, + }); + }); + + it("parses a gitea src/tag URL", () => { + expect( + parseGitSourceUrl("https://git.example.com/o/r/src/tag/v1.2.3"), + ).toMatchObject({ + ref: "v1.2.3", + basePath: "", + explicitRef: true, + }); + }); + + it("parses a gitea src/commit URL with file", () => { + expect( + parseGitSourceUrl("https://git.example.com/o/r/src/commit/abc123/dir/SKILL.md"), + ).toMatchObject({ + ref: "abc123", + basePath: "dir", + filePath: "dir/SKILL.md", + }); + }); + + it("parses a gitlab tree URL", () => { + expect( + parseGitSourceUrl("https://gitlab.com/group/proj/-/tree/main/sub"), + ).toMatchObject({ + cloneUrl: "https://gitlab.com/group/proj.git", + ref: "main", + basePath: "sub", + explicitRef: true, + }); + }); + + it("parses a gitlab blob URL", () => { + expect( + parseGitSourceUrl("https://gitlab.com/group/proj/-/blob/main/sub/file.md"), + ).toMatchObject({ + ref: "main", + filePath: "sub/file.md", + basePath: "sub", + }); + }); + + it("rejects non-https URLs", () => { + expect(() => parseGitSourceUrl("http://github.com/o/r")).toThrow(/HTTPS/); + }); + + it("rejects URLs without owner/repo", () => { + expect(() => parseGitSourceUrl("https://github.com/o")).toThrow(); + }); + + it("rejects malformed URLs", () => { + expect(() => parseGitSourceUrl("not a url")).toThrow(); + }); +}); + +describe("buildCloneUrl", () => { + it("produces a .git suffix URL on the given host", () => { + expect(buildCloneUrl("git.example.com", "o", "r")).toBe( + "https://git.example.com/o/r.git", + ); + }); +}); + +describe("resolveGitRef", () => { + it("passes through a 40-hex SHA without hitting the network", async () => { + const parsed = parseGitSourceUrl( + "https://github.com/o/r/tree/0123456789abcdef0123456789abcdef01234567", + ); + const result = await resolveGitRef(parsed); + expect(result).toEqual({ + pinnedSha: "0123456789abcdef0123456789abcdef01234567", + trackingRef: "0123456789abcdef0123456789abcdef01234567", + }); + expect(listServerRefs).not.toHaveBeenCalled(); + }); + + it("returns default branch via HEAD symref when ref is absent", async () => { + listServerRefs.mockResolvedValue([ + { ref: "HEAD", oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", target: "refs/heads/main" }, + { ref: "refs/heads/main", oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, + { ref: "refs/heads/chore", oid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + const result = await resolveGitRef(parsed); + expect(result).toEqual({ + pinnedSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + trackingRef: "main", + }); + expect(listServerRefs).toHaveBeenCalledWith( + expect.objectContaining({ + url: "https://git.example.com/o/r.git", + symrefs: true, + protocolVersion: 2, + }), + ); + }); + + it("resolves a named branch to its SHA", async () => { + listServerRefs.mockResolvedValue([ + { ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" }, + { ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" }, + { ref: "refs/heads/develop", oid: "2222222222222222222222222222222222222222" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/branch/develop"); + const result = await resolveGitRef(parsed); + expect(result).toEqual({ + pinnedSha: "2222222222222222222222222222222222222222", + trackingRef: "develop", + }); + }); + + it("prefers a peeled annotated tag over the tag object", async () => { + listServerRefs.mockResolvedValue([ + { ref: "refs/tags/v1.0", oid: "tttttttttttttttttttttttttttttttttttttttt" }, + { ref: "refs/tags/v1.0^{}", oid: "cccccccccccccccccccccccccccccccccccccccc" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/tag/v1.0"); + const result = await resolveGitRef(parsed); + expect(result.pinnedSha).toBe("cccccccccccccccccccccccccccccccccccccccc"); + expect(result.trackingRef).toBe("v1.0"); + }); + + it("resolves a lightweight tag when no peeled entry exists", async () => { + listServerRefs.mockResolvedValue([ + { ref: "refs/tags/v2.0", oid: "dddddddddddddddddddddddddddddddddddddddd" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/tag/v2.0"); + const result = await resolveGitRef(parsed); + expect(result.pinnedSha).toBe("dddddddddddddddddddddddddddddddddddddddd"); + }); + + it("throws when an explicit ref does not exist", async () => { + listServerRefs.mockResolvedValue([ + { ref: "HEAD", oid: "9999999999999999999999999999999999999999", target: "refs/heads/main" }, + { ref: "refs/heads/main", oid: "9999999999999999999999999999999999999999" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/branch/missing"); + await expect(resolveGitRef(parsed)).rejects.toThrow(/Ref 'missing' not found/); + }); + + it("translates network errors into a user-facing message", async () => { + listServerRefs.mockRejectedValue(new Error("ENOTFOUND git.invalid")); + const parsed = parseGitSourceUrl("https://git.invalid/o/r"); + await expect(resolveGitRef(parsed)).rejects.toThrow(/could not connect/i); + }); + + it("translates 401 errors into an auth message", async () => { + listServerRefs.mockRejectedValue(new Error("HTTP Error: 401 Unauthorized")); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await expect(resolveGitRef(parsed)).rejects.toThrow(/authentication/i); + }); + + it("translates 404 errors into a repo-not-found message", async () => { + listServerRefs.mockRejectedValue(new Error("HTTP Error: 404 Not Found")); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await expect(resolveGitRef(parsed)).rejects.toThrow(/repository not found/i); + }); + + it("sends an onAuth callback when a token is supplied", async () => { + listServerRefs.mockResolvedValue([ + { ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" }, + { ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await resolveGitRef(parsed, "tok_abc"); + const callArgs = listServerRefs.mock.calls[0]![0] as { onAuth: () => unknown }; + expect(typeof callArgs.onAuth).toBe("function"); + expect(callArgs.onAuth()).toEqual({ username: "tok_abc", password: "x-oauth-basic" }); + }); + + it("omits onAuth when no token is supplied", async () => { + listServerRefs.mockResolvedValue([ + { ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" }, + { ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" }, + ]); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await resolveGitRef(parsed); + const callArgs = listServerRefs.mock.calls[0]![0] as { onAuth?: unknown }; + expect(callArgs.onAuth).toBeUndefined(); + }); +}); + +describe("openRepoSnapshot", () => { + it("clones at the tracking ref and walks the tree at the resolved SHA", async () => { + cloneFn.mockResolvedValue(undefined); + resolveRefFn.mockResolvedValue("ffffffffffffffffffffffffffffffffffffffff"); + walkFn.mockImplementation(async ({ map }: { map: (filepath: string, entries: Array<{ type: () => Promise }>) => Promise }) => { + await map(".", [{ type: () => Promise.resolve("tree") }]); + await map("README.md", [{ type: () => Promise.resolve("blob") }]); + await map("skills/x/SKILL.md", [{ type: () => Promise.resolve("blob") }]); + await map("skills/x", [{ type: () => Promise.resolve("tree") }]); + }); + readBlobFn.mockResolvedValue({ blob: new TextEncoder().encode("hello") }); + + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + const snap = await openRepoSnapshot(parsed, "main", "ffffffffffffffffffffffffffffffffffffffff", "tok"); + + expect(cloneFn).toHaveBeenCalledWith( + expect.objectContaining({ + url: "https://git.example.com/o/r.git", + ref: "main", + singleBranch: true, + depth: 1, + noCheckout: true, + }), + ); + expect(snap.sha).toBe("ffffffffffffffffffffffffffffffffffffffff"); + + const files = await snap.listFiles(); + expect(files).toEqual(["README.md", "skills/x/SKILL.md"]); + + const content = await snap.readFile("README.md"); + expect(content).toBe("hello"); + expect(readBlobFn).toHaveBeenCalledWith( + expect.objectContaining({ + oid: "ffffffffffffffffffffffffffffffffffffffff", + filepath: "README.md", + }), + ); + }); + + it("falls back to the expected SHA as ref when no tracking ref is known", async () => { + cloneFn.mockResolvedValue(undefined); + resolveRefFn.mockResolvedValue("abc1234567890abc1234567890abc1234567890a"); + walkFn.mockImplementation(async () => {}); + + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await openRepoSnapshot(parsed, null, "abc1234567890abc1234567890abc1234567890a"); + + expect(cloneFn).toHaveBeenCalledWith( + expect.objectContaining({ ref: "abc1234567890abc1234567890abc1234567890a" }), + ); + }); + + it("surfaces a 404 from clone as repository-not-found", async () => { + cloneFn.mockRejectedValue(new Error("HTTP Error: 404 Not Found")); + const parsed = parseGitSourceUrl("https://git.example.com/o/r"); + await expect( + openRepoSnapshot(parsed, "main", "1111111111111111111111111111111111111111"), + ).rejects.toThrow(/repository not found/i); + }); +}); diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 51c2ad0b..3dd8ca88 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -29,7 +29,7 @@ import type { import { normalizeAgentUrlKey } from "@paperclipai/shared"; import { resolvePaperclipInstanceRoot } from "../home-paths.js"; import { notFound, unprocessable } from "../errors.js"; -import { ghFetch, gitHubApiBase, inferGitHostFamily, resolveRawGitHubUrl } from "./github-fetch.js"; +import { openRepoSnapshot, parseGitSourceUrl, resolveGitRef, type ParsedGitSource, type RepoSnapshot } from "./git-source.js"; import { agentService } from "./agents.js"; import { projectService } from "./projects.js"; import { secretService } from "./secrets.js"; @@ -541,106 +541,20 @@ function parseFrontmatterMarkdown(raw: string): { frontmatter: Record { try { return new URL(url).hostname; } catch { return url; } })(); + throw unprocessable(`Could not connect to ${hostname}`); + } if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } -async function fetchJson(url: string, authToken?: string): Promise { - const response = await ghFetch(url, { - headers: { - accept: "application/vnd.github+json", - }, - }, authToken); - if (!response.ok) { - throw unprocessable(`Failed to fetch ${url}: ${response.status}`); - } - return response.json() as Promise; -} - - -async function resolveGitHubDefaultBranch(owner: string, repo: string, apiBase: string, authToken?: string) { - const response = await fetchJson<{ default_branch?: string }>( - `${apiBase}/repos/${owner}/${repo}`, - authToken, - ); - return asString(response.default_branch) ?? "main"; -} - -async function resolveGitHubCommitSha(owner: string, repo: string, ref: string, apiBase: string, authToken?: string) { - const response = await fetchJson<{ sha?: string }>( - `${apiBase}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, - authToken, - ); - const sha = asString(response.sha); - if (!sha) { - throw unprocessable(`Failed to resolve ref ${ref}`); - } - return sha; -} - -function parseGitHubSourceUrl(rawUrl: string) { - const url = new URL(rawUrl); - if (url.protocol !== "https:") { - throw unprocessable("Source URL must use HTTPS"); - } - const parts = url.pathname.split("/").filter(Boolean); - if (parts.length < 2) { - throw unprocessable("Invalid git source URL"); - } - const owner = parts[0]!; - const repo = parts[1]!.replace(/\.git$/i, ""); - const family = inferGitHostFamily(url.hostname); - let ref = "main"; - let basePath = ""; - let filePath: string | null = null; - let explicitRef = false; - if (family === "github") { - if (parts[2] === "tree") { - ref = parts[3] ?? "main"; - basePath = parts.slice(4).join("/"); - explicitRef = true; - } else if (parts[2] === "blob") { - ref = parts[3] ?? "main"; - filePath = parts.slice(4).join("/"); - basePath = filePath ? path.posix.dirname(filePath) : ""; - explicitRef = true; - } - } else if (parts[2] === "src" && (parts[3] === "branch" || parts[3] === "commit" || parts[3] === "tag")) { - // Gitea/Forgejo web URLs: /{owner}/{repo}/src/{branch|commit|tag}/{ref}/{path} - ref = parts[4] ?? "main"; - const tail = parts.slice(5); - const tailJoined = tail.join("/"); - if (tail.length > 0 && /\.[A-Za-z0-9]+$/.test(tail[tail.length - 1]!)) { - filePath = tailJoined; - basePath = path.posix.dirname(tailJoined); - } else { - basePath = tailJoined; - } - explicitRef = true; - } - return { hostname: url.hostname, owner, repo, ref, basePath, filePath, explicitRef }; -} - -async function resolveGitHubPinnedRef(parsed: ReturnType, authToken?: string) { - const apiBase = gitHubApiBase(parsed.hostname); - if (/^[0-9a-f]{40}$/i.test(parsed.ref.trim())) { - return { - pinnedRef: parsed.ref, - trackingRef: parsed.explicitRef ? parsed.ref : null, - }; - } - - const trackingRef = parsed.explicitRef - ? parsed.ref - : await resolveGitHubDefaultBranch(parsed.owner, parsed.repo, apiBase, authToken); - const pinnedRef = await resolveGitHubCommitSha(parsed.owner, parsed.repo, trackingRef, apiBase, authToken); - return { pinnedRef, trackingRef }; -} - function extractCommandTokens(raw: string) { const matches = raw.match(/"[^"]*"|'[^']*'|\S+/g) ?? []; @@ -1081,20 +995,12 @@ async function readUrlSkillImports( return segments.length >= 2 && !parsed.pathname.endsWith(".md"); } catch { return false; } })(); if (looksLikeRepoUrl) { - const parsed = parseGitHubSourceUrl(url); - const apiBase = gitHubApiBase(parsed.hostname); - const { pinnedRef, trackingRef } = await resolveGitHubPinnedRef(parsed, authToken); - let ref = pinnedRef; - const tree = await fetchJson<{ tree?: Array<{ path: string; type: string }> }>( - `${apiBase}/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`, - authToken, - ).catch(() => { - throw unprocessable(`Failed to read GitHub tree for ${url}`); - }); - const allPaths = (tree.tree ?? []) - .filter((entry) => entry.type === "blob") - .map((entry) => entry.path) - .filter((entry): entry is string => typeof entry === "string"); + const parsed = parseGitSourceUrl(url); + const resolved = await resolveGitRef(parsed, authToken); + const snapshot = await openRepoSnapshot(parsed, resolved.trackingRef, resolved.pinnedSha, authToken); + const ref = snapshot.sha; + const trackingRef = resolved.trackingRef; + const allPaths = await snapshot.listFiles(); const basePrefix = parsed.basePath ? `${parsed.basePath.replace(/^\/+|\/+$/g, "")}/` : ""; const scopedPaths = basePrefix ? allPaths.filter((entry) => entry.startsWith(basePrefix)) @@ -1108,13 +1014,13 @@ async function readUrlSkillImports( ); if (skillPaths.length === 0) { throw unprocessable( - "No SKILL.md files were found in the provided GitHub source.", + "No SKILL.md files were found in the provided source.", ); } const skills: ImportedSkill[] = []; for (const relativeSkillPath of skillPaths) { const repoSkillPath = basePrefix ? `${basePrefix}${relativeSkillPath}` : relativeSkillPath; - const markdown = await fetchText(resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, repoSkillPath), authToken); + const markdown = await snapshot.readFile(repoSkillPath); const parsedMarkdown = parseFrontmatterMarkdown(markdown); const skillDir = path.posix.dirname(relativeSkillPath); const slug = deriveImportedSkillSlug(parsedMarkdown.frontmatter, path.posix.basename(skillDir)); @@ -1168,15 +1074,15 @@ async function readUrlSkillImports( if (skills.length === 0) { throw unprocessable( requestedSkillSlug - ? `Skill ${requestedSkillSlug} was not found in the provided GitHub source.` - : "No SKILL.md files were found in the provided GitHub source.", + ? `Skill ${requestedSkillSlug} was not found in the provided source.` + : "No SKILL.md files were found in the provided source.", ); } return { skills, warnings }; } if (url.startsWith("http://") || url.startsWith("https://")) { - const markdown = await fetchText(url, authToken); + const markdown = await fetchPlainText(url); const parsedMarkdown = parseFrontmatterMarkdown(markdown); const urlObj = new URL(url); const fileName = path.posix.basename(urlObj.pathname); @@ -1801,9 +1707,18 @@ export function companySkillService(db: Db) { } const hostname = asString(metadata.hostname) || "github.com"; - const apiBase = gitHubApiBase(hostname); const authToken = await resolveSkillAuthToken(companyId, skill); - const latestRef = await resolveGitHubCommitSha(owner, repo, trackingRef, apiBase, authToken); + const parsed: ParsedGitSource = { + cloneUrl: `https://${hostname}/${owner}/${repo}.git`, + hostname, + owner, + repo, + ref: trackingRef, + basePath: "", + filePath: null, + explicitRef: true, + }; + const { pinnedSha: latestRef } = await resolveGitRef(parsed, authToken); return { supported: true, reason: null, @@ -1843,13 +1758,25 @@ export function companySkillService(db: Db) { const repo = asString(metadata.repo); const hostname = asString(metadata.hostname) || "github.com"; const ref = skill.sourceRef ?? asString(metadata.ref) ?? "main"; + const trackingRef = asString(metadata.trackingRef); const repoSkillDir = normalizeGitHubSkillDirectory(asString(metadata.repoSkillDir), skill.slug); if (!owner || !repo) { throw unprocessable("Skill source metadata is incomplete."); } const authToken = await resolveSkillAuthToken(companyId, skill); const repoPath = normalizePortablePath(path.posix.join(repoSkillDir, normalizedPath)); - content = await fetchText(resolveRawGitHubUrl(hostname, owner, repo, ref, repoPath), authToken); + const parsedSource: ParsedGitSource = { + cloneUrl: `https://${hostname}/${owner}/${repo}.git`, + hostname, + owner, + repo, + ref, + basePath: repoSkillDir, + filePath: null, + explicitRef: true, + }; + const snapshot: RepoSnapshot = await openRepoSnapshot(parsedSource, trackingRef ?? null, ref, authToken); + content = await snapshot.readFile(repoPath); } else if (skill.sourceType === "url") { if (normalizedPath !== "SKILL.md") { throw notFound("This skill source only exposes SKILL.md"); diff --git a/server/src/services/git-source.ts b/server/src/services/git-source.ts new file mode 100644 index 00000000..f52dce46 --- /dev/null +++ b/server/src/services/git-source.ts @@ -0,0 +1,243 @@ +import path from "path"; +import git from "isomorphic-git"; +import http from "isomorphic-git/http/node"; +import { Volume, createFsFromVolume } from "memfs"; + +import { unprocessable } from "../errors.js"; + +export type ParsedGitSource = { + cloneUrl: string; + hostname: string; + owner: string; + repo: string; + ref: string | null; + basePath: string; + filePath: string | null; + explicitRef: boolean; +}; + +export type RefResolution = { + pinnedSha: string; + trackingRef: string | null; +}; + +export type RepoSnapshot = { + sha: string; + listFiles(): Promise; + readFile(repoPath: string): Promise; +}; + +const SHA_REGEX = /^[0-9a-f]{40}$/i; + +export function buildCloneUrl(hostname: string, owner: string, repo: string): string { + return `https://${hostname}/${owner}/${repo}.git`; +} + +export function parseGitSourceUrl(rawUrl: string): ParsedGitSource { + let url: URL; + try { + url = new URL(rawUrl); + } catch { + throw unprocessable("Invalid git source URL"); + } + if (url.protocol !== "https:") { + throw unprocessable("Source URL must use HTTPS"); + } + const segments = url.pathname.split("/").filter(Boolean); + if (segments.length < 2) { + throw unprocessable("Source URL must include an owner and repository"); + } + const owner = segments[0]!; + const repo = segments[1]!.replace(/\.git$/i, ""); + + let ref: string | null = null; + let basePath = ""; + let filePath: string | null = null; + let explicitRef = false; + let tail: string[] = []; + + // Recognise common host-specific URL shapes so users can paste a tree/blob link. + if (segments[2] === "tree" || segments[2] === "blob") { + // github.com style + ref = segments[3] ?? null; + tail = segments.slice(4); + explicitRef = ref !== null; + } else if (segments[2] === "src" && (segments[3] === "branch" || segments[3] === "commit" || segments[3] === "tag")) { + // gitea / forgejo style + ref = segments[4] ?? null; + tail = segments.slice(5); + explicitRef = ref !== null; + } else if (segments[2] === "-" && (segments[3] === "tree" || segments[3] === "blob")) { + // gitlab style: /{owner}/{repo}/-/tree/{ref}/{path} + ref = segments[4] ?? null; + tail = segments.slice(5); + explicitRef = ref !== null; + } else if (segments[2] === "src" && segments.length >= 4) { + // bitbucket style: /{owner}/{repo}/src/{ref}/{path} + ref = segments[3] ?? null; + tail = segments.slice(4); + explicitRef = ref !== null; + } + + if (segments[2] === "blob" || (segments[2] === "-" && segments[3] === "blob")) { + const joined = tail.join("/"); + filePath = joined || null; + basePath = filePath ? path.posix.dirname(filePath) : ""; + if (basePath === ".") basePath = ""; + } else if (tail.length > 0) { + const joined = tail.join("/"); + // Heuristic: if the last segment looks like a file (has an extension), treat as file + const last = tail[tail.length - 1]!; + if (/\.[A-Za-z0-9]+$/.test(last)) { + filePath = joined; + basePath = path.posix.dirname(joined); + if (basePath === ".") basePath = ""; + } else { + basePath = joined; + } + } + + return { + cloneUrl: buildCloneUrl(url.hostname, owner, repo), + hostname: url.hostname, + owner, + repo, + ref, + basePath, + filePath, + explicitRef, + }; +} + +function buildAuthCallback(authToken: string | undefined) { + if (!authToken) return undefined; + // Universal pattern: token-as-username works for GitHub PATs (classic and fine-grained), + // GitLab project/personal access tokens, Gitea/Forgejo tokens, and Bitbucket app passwords + // when used over the git smart-HTTP protocol. + return () => ({ username: authToken, password: "x-oauth-basic" }); +} + +async function withGitErrors(label: string, fn: () => Promise): Promise { + try { + return await fn(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (/HTTP Error: 401/i.test(message)) { + throw unprocessable(`${label}: authentication required or token rejected`); + } + if (/HTTP Error: 403/i.test(message)) { + throw unprocessable(`${label}: access forbidden`); + } + if (/HTTP Error: 404/i.test(message) || /repository not found/i.test(message)) { + throw unprocessable(`${label}: repository not found`); + } + if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT/i.test(message)) { + throw unprocessable(`${label}: could not connect to host`); + } + throw unprocessable(`${label}: ${message}`); + } +} + +export async function resolveGitRef( + parsed: ParsedGitSource, + authToken?: string, +): Promise { + const onAuth = buildAuthCallback(authToken); + + if (parsed.ref && SHA_REGEX.test(parsed.ref.trim())) { + return { + pinnedSha: parsed.ref.trim().toLowerCase(), + trackingRef: parsed.explicitRef ? parsed.ref.trim() : null, + }; + } + + const refs = await withGitErrors(`Resolve refs for ${parsed.cloneUrl}`, () => + git.listServerRefs({ + http, + url: parsed.cloneUrl, + onAuth, + symrefs: true, + protocolVersion: 2, + }), + ); + + const findExact = (fullRef: string) => refs.find((r) => r.ref === fullRef); + + if (!parsed.ref) { + const head = refs.find((r) => r.ref === "HEAD"); + if (!head?.oid) { + throw unprocessable(`Could not determine default branch for ${parsed.cloneUrl}`); + } + const target = head.target?.replace(/^refs\/heads\//, "") ?? null; + return { pinnedSha: head.oid, trackingRef: target }; + } + + const wanted = parsed.ref.replace(/^refs\/(heads|tags)\//, ""); + const branch = findExact(`refs/heads/${wanted}`); + if (branch?.oid) return { pinnedSha: branch.oid, trackingRef: wanted }; + + // Prefer the peeled (annotated) tag oid when present, else the tag object oid. + const peeled = findExact(`refs/tags/${wanted}^{}`); + if (peeled?.oid) return { pinnedSha: peeled.oid, trackingRef: wanted }; + const tag = findExact(`refs/tags/${wanted}`); + if (tag?.oid) return { pinnedSha: tag.oid, trackingRef: wanted }; + + throw unprocessable(`Ref '${parsed.ref}' not found in ${parsed.cloneUrl}`); +} + +export async function openRepoSnapshot( + parsed: ParsedGitSource, + trackingRef: string | null, + expectedSha: string, + authToken?: string, +): Promise { + const volume = new Volume(); + const fs = createFsFromVolume(volume) as unknown as Parameters[0]["fs"]; + const dir = "/repo"; + const onAuth = buildAuthCallback(authToken); + + await withGitErrors(`Clone ${parsed.cloneUrl}`, async () => { + await git.clone({ + fs, + http, + dir, + url: parsed.cloneUrl, + ref: trackingRef ?? expectedSha, + singleBranch: true, + depth: 1, + noCheckout: true, + onAuth, + }); + }); + + // Re-resolve to the actual commit cloned. If upstream moved between resolveGitRef and + // clone, we trust what we cloned (snapshot is self-consistent). + const sha = await git.resolveRef({ fs, dir, ref: "HEAD" }); + + async function listFiles(): Promise { + const out: string[] = []; + await git.walk({ + fs, + dir, + trees: [git.TREE({ ref: sha })], + map: async (filepath, entries) => { + if (filepath === ".") return; + const entry = entries?.[0]; + if (!entry) return; + const type = await entry.type(); + if (type === "blob") { + out.push(filepath); + } + }, + }); + return out; + } + + async function readFile(repoPath: string): Promise { + const normalized = repoPath.replace(/^\/+/, ""); + const { blob } = await git.readBlob({ fs, dir, oid: sha, filepath: normalized }); + return new TextDecoder("utf-8").decode(blob); + } + + return { sha, listFiles, readFile }; +}