diff --git a/.gitea/workflows/vscode-release.yml b/.gitea/workflows/vscode-release.yml new file mode 100644 index 0000000..2e8b705 --- /dev/null +++ b/.gitea/workflows/vscode-release.yml @@ -0,0 +1,73 @@ +# Packaging du VSIX de l'extension VS Code, déclenché UNIQUEMENT par un tag vscode-vX.Y.Z +# (séparé de la release du daemon, qui écoute les tags v*). Le .vsix est exposé en artefact du run +# (toujours) et, en best-effort, attaché à la release Gitea correspondante. +name: VSCode Release + +on: + push: + tags: ['vscode-v*'] + +permissions: + contents: write + +jobs: + package: + name: Package VSIX + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + # Garde-fou : le tag (sans "vscode-v") doit correspondre à la version du manifeste de l'extension. + - name: Verify tag matches extension version + run: | + pkg=$(node -p "require('./packages/vscode/package.json').version") + tag="${GITHUB_REF_NAME#vscode-v}" + if [ "$pkg" != "$tag" ]; then + echo "ERREUR: tag '$tag' != version extension '$pkg'" + exit 1 + fi + echo "OK: tag $tag == version $pkg" + # Build du shared puis bundle esbuild de l'extension (typecheck inclus), puis packaging VSIX. + # --no-dependencies : tout est bundlé dans dist/extension.js → pas de node_modules dans le VSIX. + - name: Build & package + run: | + npm run build:vscode + version=$(node -p "require('./packages/vscode/package.json').version") + cd packages/vscode + npx --yes @vscode/vsce package --no-dependencies -o "git-arboretum-${version}.vsix" + # Artefact du run : canal de distribution fiable, indépendant de l'API release. + # upload-artifact@v3 : Gitea Actions ne supporte pas @v4 (@actions/artifact v2+). + - uses: actions/upload-artifact@v3 + with: + name: vsix + path: packages/vscode/*.vsix + # Best-effort : attache le VSIX à la release Gitea du tag (crée la release si absente). + # Nécessite un secret RELEASE_TOKEN (token Gitea avec write:repository) ; sans lui, l'étape est + # ignorée sans faire échouer le job (continue-on-error). + - name: Attach VSIX to Gitea release + continue-on-error: true + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + if [ -z "$RELEASE_TOKEN" ]; then + echo "::notice::RELEASE_TOKEN absent — VSIX disponible en artefact uniquement." + exit 0 + fi + api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + auth="Authorization: token ${RELEASE_TOKEN}" + version=$(node -p "require('./packages/vscode/package.json').version") + vsix="packages/vscode/git-arboretum-${version}.vsix" + # id de release du tag, sinon création + rid=$(curl -fsSL -H "$auth" "${api}/releases/tags/${GITHUB_REF_NAME}" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).id || ''" || true) + if [ -z "$rid" ]; then + rid=$(curl -fsSL -X POST -H "$auth" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"Arboretum VSCode ${version}\"}" \ + "${api}/releases" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).id || ''") + fi + curl -fsSL -X POST -H "$auth" -F "attachment=@${vsix}" \ + "${api}/releases/${rid}/assets?name=git-arboretum-${version}.vsix" + echo "VSIX attaché à la release ${GITHUB_REF_NAME}." diff --git a/package-lock.json b/package-lock.json index a7e193a..c396f44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,213 @@ "resolved": "packages/web", "link": true }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.2.tgz", + "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", + "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.14.0.tgz", + "integrity": "sha512-Dfl7hPZe9/JJwRhFFXHq2z1oHYBuGubmff3kWXOsd1AGgyXlqjNYAWuN/1JL/ZrcZBs8TKMjGSil6Rcc7E8VPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.9.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.9.0.tgz", + "integrity": "sha512-1MWGjqgUCRAYgLmVFZKp7fs3Rg1TFvIMgywY8ze2olNVvLlJoRThuoziWSDJuwwyJI5L4rnLb9Tyt5D9GvSLPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.5.tgz", + "integrity": "sha512-RUuewWk9JvWJS5Yiy8/74Lm1rQAWlrU/qg/Bgtk1jIauVRtnb9XKwS5Xg0J+Whwjesq9EVrBIFgQEP8vHxgezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.9.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -956,6 +1163,44 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@oxc-project/types": { "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", @@ -1586,6 +1831,193 @@ "win32" ] }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -1858,6 +2290,90 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1904,6 +2420,27 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/web-push": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", @@ -1924,6 +2461,21 @@ "@types/node": "*" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", + "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", @@ -2085,6 +2637,221 @@ "vscode-uri": "^3.0.8" } }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/vsce/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.38", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", @@ -2346,6 +3113,58 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -2368,6 +3187,23 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2397,6 +3233,17 @@ "fastq": "^1.17.1" } }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2426,6 +3273,22 @@ ], "license": "MIT" }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -2452,6 +3315,20 @@ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -2464,6 +3341,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2488,6 +3378,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2503,6 +3403,22 @@ "optional": true, "peer": true }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2513,6 +3429,37 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2530,6 +3477,23 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -2540,12 +3504,99 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2596,6 +3647,36 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2653,6 +3734,59 @@ "node": ">=4.0.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2680,6 +3814,93 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -2701,6 +3922,44 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -2736,6 +3995,39 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2743,6 +4035,35 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -2832,6 +4153,23 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stringify": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", @@ -2957,6 +4295,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-my-way": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", @@ -2971,12 +4322,44 @@ "node": ">=20" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2992,6 +4375,59 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/git-arboretum": { + "resolved": "packages/vscode", + "link": true + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -3015,6 +4451,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3022,12 +4505,110 @@ "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, "node_modules/http_ece": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", @@ -3057,6 +4638,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -3070,6 +4665,19 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3090,6 +4698,29 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3111,6 +4742,84 @@ "node": ">= 10" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", @@ -3123,6 +4832,40 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -3140,6 +4883,29 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -3165,6 +4931,62 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3186,6 +5008,37 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keytar/node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -3495,6 +5348,89 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -3520,6 +5456,101 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.1", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -3532,6 +5563,29 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -3608,6 +5662,13 @@ "dev": true, "license": "MIT" }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -3650,6 +5711,81 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -3668,6 +5804,129 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -3691,6 +5950,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3708,6 +5980,13 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -3791,6 +6070,16 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -3872,6 +6161,53 @@ "once": "^1.3.1" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -3893,6 +6229,65 @@ "rc": "cli.js" } }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4029,6 +6424,43 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4086,6 +6518,38 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -4126,6 +6590,82 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4178,6 +6718,37 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -4221,6 +6792,42 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -4277,6 +6884,60 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -4299,6 +6960,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -4311,6 +6982,76 @@ "node": ">=16" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -4360,6 +7101,23 @@ "node": ">=6" } }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", @@ -4381,6 +7139,29 @@ "node": ">=10" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -4460,6 +7241,29 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toad-cache": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", @@ -4483,8 +7287,17 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -4498,6 +7311,31 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -4512,6 +7350,30 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4519,12 +7381,66 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/vite": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", @@ -4808,6 +7724,30 @@ "node": ">= 16" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4852,9 +7792,79 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, "packages/server": { "name": "@johanleroy/git-arboretum", - "version": "1.6.0", + "version": "1.8.0", "license": "MIT", "dependencies": { "@fastify/cookie": "^11.0.0", @@ -4980,6 +7990,454 @@ } } }, + "packages/vscode": { + "name": "git-arboretum", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@arboretum/shared": "0.1.0", + "@types/node": "^22.10.0", + "@types/vscode": "^1.85.0", + "@types/ws": "^8.5.0", + "@vscode/vsce": "^3.2.0", + "esbuild": "^0.21.0", + "typescript": "^5.7.0", + "ws": "^8.18.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "packages/vscode/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/vscode/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "packages/web": { "name": "@arboretum/web", "version": "0.1.0", diff --git a/package.json b/package.json index 299742f..c11dae6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "dev:web": "npm run dev -w @arboretum/web", "build:site": "npm run build -w @arboretum/site", "dev:site": "npm run dev -w @arboretum/site", - "preview:site": "npm run preview -w @arboretum/site" + "preview:site": "npm run preview -w @arboretum/site", + "build:vscode": "npm run build -w @arboretum/shared -w git-arboretum", + "dev:vscode": "npm run dev -w git-arboretum" }, "devDependencies": { "@types/node": "^22.10.0", diff --git a/packages/vscode/.gitignore b/packages/vscode/.gitignore new file mode 100644 index 0000000..579066e --- /dev/null +++ b/packages/vscode/.gitignore @@ -0,0 +1,2 @@ +dist/ +*.vsix diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore new file mode 100644 index 0000000..03e8166 --- /dev/null +++ b/packages/vscode/.vscodeignore @@ -0,0 +1,9 @@ +# Tout est bundlé dans dist/extension.js par esbuild : on n'embarque que le bundle + la meta. +src/** +test/** +node_modules/** +esbuild.mjs +tsconfig.json +.gitignore +**/*.ts +**/*.map diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md new file mode 100644 index 0000000..1cf9ad0 --- /dev/null +++ b/packages/vscode/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## 0.1.0 + +Initial release — native VS Code integration for the Arboretum daemon: + +- Real-time **Repositories** and **Groups** trees (repos → worktrees → sessions) over the daemon WebSocket. +- **Native terminals** (`Pseudoterminal`) to attach/observe any session, reusing VS Code's rendering and + scrollback. +- **Status-bar** waiting counter and native **notifications** on the rising edge to `waiting`, with + Yes/No answers via the `answer` command. +- Git mutations from the tree: **create worktree, commit, push, promote**; start session / start group + session; resume / fork / kill / hide. +- **Workspace awareness**: reveal the worktree for the open folder; start a session or create a worktree + for the current folder. +- Token auth via `Authorization: Bearer` (REST + WebSocket), token stored in SecretStorage. diff --git a/packages/vscode/LICENSE b/packages/vscode/LICENSE new file mode 100644 index 0000000..c691019 --- /dev/null +++ b/packages/vscode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Johan Leroy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/vscode/README.md b/packages/vscode/README.md new file mode 100644 index 0000000..2769969 --- /dev/null +++ b/packages/vscode/README.md @@ -0,0 +1,73 @@ +# Arboretum for VS Code + +Pilot your git **worktrees** and **Claude Code sessions** from inside VS Code — a real native +integration on top of the [Arboretum](https://git.lidge.fr/johanleroy/arboretum) daemon, not a webview. + +- **Live tree** of Repositories → Worktrees → Sessions (and a Groups view), updated in real time over + the daemon's WebSocket. +- **Native terminals**: attach to any session in a real VS Code terminal (a `Pseudoterminal` bridges the + daemon's PTY) — you get VS Code's own rendering, scrollback, copy/paste and links for free. +- **Waiting alerts**: a status-bar counter and native notifications when a Claude session is waiting for + input — answer **Yes/No** without even opening the terminal (uses the daemon's `answer` command). +- **Git mutations** from the tree: create worktree, commit, push, promote to main. +- **Workspace-aware**: the worktree matching your open folder is highlighted; start a session or create a + worktree for the current folder in one command. + +## Requirements + +- A running Arboretum daemon (`npx @johanleroy/git-arboretum`, or installed as a user service via + `arboretum install`). The extension is a **client** — it does not start the daemon. +- An access token. The bootstrap token is printed once on first daemon start; you can also create one in + the Arboretum dashboard (**Settings → Tokens**). + +## Getting started + +1. Start the daemon and copy a token. +2. In VS Code, run **Arboretum: Sign In** (Command Palette) and paste the token. The token is validated + against the daemon and stored in VS Code's encrypted **SecretStorage**. +3. The **Arboretum** view container appears in the Activity Bar with the Repositories and Groups trees. + +## Settings + +| Setting | Default | Description | +| --- | --- | --- | +| `arboretum.url` | `http://127.0.0.1:7317` | Base URL of the daemon (REST + WebSocket). | +| `arboretum.showExternalSessions` | `false` | Also show Claude sessions started outside Arboretum (CLI). | +| `arboretum.notifyOnWaiting` | `true` | Native notification when a session starts waiting for input. | + +The extension authenticates with `Authorization: Bearer ` on both REST and the WebSocket upgrade. +For remote access, point `arboretum.url` at your Tailscale Serve URL (`https://…`) — the WebSocket is +derived automatically (`wss://`). + +## Building & packaging (private VSIX) + +The extension is bundled with esbuild (`@arboretum/shared` is inlined → the VSIX is self-contained). + +```bash +# from the monorepo root +npm install +npm run build:vscode # builds @arboretum/shared then bundles the extension +cd packages/vscode && npx @vscode/vsce package --no-dependencies +# → git-arboretum-0.1.0.vsix +``` + +Install it with **Extensions: Install from VSIX…** in the Command Palette, or: + +```bash +code --install-extension git-arboretum-0.1.0.vsix # also: codium / cursor +``` + +### Other distribution channels (optional) + +- **Open VSX** (VSCodium, Cursor, Windsurf): `npx ovsx publish *.vsix -p ` after creating an + Open VSX account and namespace. +- **VS Code Marketplace**: create an Azure DevOps publisher + PAT, then `npx @vscode/vsce publish`. + +A Gitea Actions workflow packages the VSIX automatically on a `vscode-vX.Y.Z` tag (see +`.gitea/workflows/vscode-release.yml`) and attaches it to the release. + +## Security + +The extension never weakens the daemon's security model: it speaks the same authenticated protocol as the +web dashboard. A Node client sends no `Origin` header, so it passes the daemon's strict Origin check while +still requiring a valid token. diff --git a/packages/vscode/esbuild.mjs b/packages/vscode/esbuild.mjs new file mode 100644 index 0000000..907373e --- /dev/null +++ b/packages/vscode/esbuild.mjs @@ -0,0 +1,28 @@ +// Build de l'extension : un seul bundle CJS `dist/extension.js`. +// esbuild résout `@arboretum/shared` via le symlink workspace et l'inline dans le bundle +// (VSIX 100 % autonome, sans node_modules embarqué) ; seul `vscode` reste externe (fourni par l'hôte). +import esbuild from 'esbuild'; + +const watch = process.argv.includes('--watch'); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints: ['src/extension.ts'], + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node18', // VSCode 1.85 embarque Electron/Node 18 + outfile: 'dist/extension.js', + external: ['vscode'], // module fourni par l'hôte VSCode, jamais bundlé + sourcemap: true, + minify: !watch, + logLevel: 'info', +}; + +if (watch) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log('[esbuild] watching…'); +} else { + await esbuild.build(options); +} diff --git a/packages/vscode/media/arboretum.svg b/packages/vscode/media/arboretum.svg new file mode 100644 index 0000000..3886643 --- /dev/null +++ b/packages/vscode/media/arboretum.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/vscode/package.json b/packages/vscode/package.json new file mode 100644 index 0000000..55004a1 --- /dev/null +++ b/packages/vscode/package.json @@ -0,0 +1,164 @@ +{ + "name": "git-arboretum", + "displayName": "Arboretum", + "description": "Pilot your git worktrees and Claude Code sessions from VS Code — native terminals, live tree, waiting alerts.", + "version": "0.1.0", + "private": true, + "publisher": "johanleroy", + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "repository": { + "type": "git", + "url": "https://git.lidge.fr/johanleroy/arboretum" + }, + "categories": [ + "SCM Providers", + "Other" + ], + "keywords": [ + "git", + "worktree", + "claude", + "terminal", + "arboretum" + ], + "main": "./dist/extension.js", + "activationEvents": [ + "onStartupFinished" + ], + "contributes": { + "configuration": { + "title": "Arboretum", + "properties": { + "arboretum.url": { + "type": "string", + "default": "http://127.0.0.1:7317", + "markdownDescription": "Base URL of the Arboretum daemon (REST + WebSocket). Defaults to the loopback bind." + }, + "arboretum.showExternalSessions": { + "type": "boolean", + "default": false, + "markdownDescription": "Show Claude sessions discovered outside Arboretum (started from the CLI) in the trees. Off by default to avoid clutter." + }, + "arboretum.notifyOnWaiting": { + "type": "boolean", + "default": true, + "markdownDescription": "Show a native notification when a Claude session starts waiting for input." + } + } + }, + "viewsContainers": { + "activitybar": [ + { + "id": "arboretum", + "title": "Arboretum", + "icon": "media/arboretum.svg" + } + ] + }, + "views": { + "arboretum": [ + { + "id": "arboretum.repos", + "name": "Repositories", + "icon": "media/arboretum.svg" + }, + { + "id": "arboretum.groups", + "name": "Groups", + "icon": "media/arboretum.svg" + } + ] + }, + "viewsWelcome": [ + { + "view": "arboretum.repos", + "contents": "Not connected to an Arboretum daemon.\n[Sign in](command:arboretum.signIn)\n\nMake sure the daemon is running (`npx @johanleroy/git-arboretum`) and paste a token.", + "when": "!arboretum.connected" + } + ], + "commands": [ + { "command": "arboretum.signIn", "title": "Sign In", "category": "Arboretum" }, + { "command": "arboretum.signOut", "title": "Sign Out", "category": "Arboretum" }, + { "command": "arboretum.refresh", "title": "Refresh", "category": "Arboretum", "icon": "$(refresh)" }, + { "command": "arboretum.openDashboard", "title": "Open Dashboard in Browser", "category": "Arboretum", "icon": "$(globe)" }, + { "command": "arboretum.attachSession", "title": "Attach Terminal", "category": "Arboretum", "icon": "$(terminal)" }, + { "command": "arboretum.observeSession", "title": "Observe Terminal (read-only)", "category": "Arboretum", "icon": "$(eye)" }, + { "command": "arboretum.answerSession", "title": "Answer Prompt…", "category": "Arboretum", "icon": "$(comment-discussion)" }, + { "command": "arboretum.killSession", "title": "Kill Session", "category": "Arboretum", "icon": "$(trash)" }, + { "command": "arboretum.hideSession", "title": "Hide Session", "category": "Arboretum", "icon": "$(eye-closed)" }, + { "command": "arboretum.resumeSession", "title": "Resume Session", "category": "Arboretum", "icon": "$(debug-restart)" }, + { "command": "arboretum.forkSession", "title": "Fork Session", "category": "Arboretum", "icon": "$(git-branch)" }, + { "command": "arboretum.createWorktree", "title": "Create Worktree…", "category": "Arboretum", "icon": "$(add)" }, + { "command": "arboretum.commitWorktree", "title": "Commit…", "category": "Arboretum", "icon": "$(git-commit)" }, + { "command": "arboretum.pushWorktree", "title": "Push", "category": "Arboretum", "icon": "$(repo-push)" }, + { "command": "arboretum.promoteWorktree", "title": "Promote to Main", "category": "Arboretum", "icon": "$(arrow-up)" }, + { "command": "arboretum.startSession", "title": "Start Claude Session", "category": "Arboretum", "icon": "$(play)" }, + { "command": "arboretum.startGroupSession", "title": "Start Group Session…", "category": "Arboretum", "icon": "$(play)" }, + { "command": "arboretum.startSessionHere", "title": "Start Session in Current Folder", "category": "Arboretum" }, + { "command": "arboretum.createWorktreeHere", "title": "Create Worktree for Current Folder…", "category": "Arboretum" }, + { "command": "arboretum.revealWorktree", "title": "Reveal Worktree for Current Folder", "category": "Arboretum" } + ], + "menus": { + "view/title": [ + { "command": "arboretum.refresh", "when": "view == arboretum.repos || view == arboretum.groups", "group": "navigation" }, + { "command": "arboretum.openDashboard", "when": "view == arboretum.repos", "group": "navigation" }, + { "command": "arboretum.signOut", "when": "view == arboretum.repos && arboretum.connected", "group": "1_auth" }, + { "command": "arboretum.signIn", "when": "view == arboretum.repos && !arboretum.connected", "group": "1_auth" } + ], + "view/item/context": [ + { "command": "arboretum.createWorktree", "when": "viewItem == arboretum:repo", "group": "inline" }, + { "command": "arboretum.startSession", "when": "viewItem == arboretum:repo", "group": "1_session" }, + { "command": "arboretum.commitWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" }, + { "command": "arboretum.pushWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" }, + { "command": "arboretum.promoteWorktree", "when": "viewItem == arboretum:worktree", "group": "2_git" }, + { "command": "arboretum.startSession", "when": "viewItem == arboretum:worktree", "group": "3_session" }, + { "command": "arboretum.attachSession", "when": "viewItem =~ /arboretum:session:live/", "group": "inline" }, + { "command": "arboretum.observeSession", "when": "viewItem =~ /arboretum:session:live/", "group": "1_term" }, + { "command": "arboretum.answerSession", "when": "viewItem =~ /arboretum:session:live:.*waiting/", "group": "1_term" }, + { "command": "arboretum.killSession", "when": "viewItem =~ /arboretum:session:live/", "group": "9_danger" }, + { "command": "arboretum.resumeSession", "when": "viewItem =~ /arboretum:session:dead/", "group": "inline" }, + { "command": "arboretum.forkSession", "when": "viewItem =~ /arboretum:session/", "group": "2_term" }, + { "command": "arboretum.hideSession", "when": "viewItem =~ /arboretum:session:.*discovered/", "group": "9_danger" }, + { "command": "arboretum.startGroupSession", "when": "viewItem == arboretum:group", "group": "inline" } + ], + "commandPalette": [ + { "command": "arboretum.refresh", "when": "arboretum.connected" }, + { "command": "arboretum.signOut", "when": "arboretum.connected" }, + { "command": "arboretum.attachSession", "when": "false" }, + { "command": "arboretum.observeSession", "when": "false" }, + { "command": "arboretum.answerSession", "when": "false" }, + { "command": "arboretum.killSession", "when": "false" }, + { "command": "arboretum.hideSession", "when": "false" }, + { "command": "arboretum.resumeSession", "when": "false" }, + { "command": "arboretum.forkSession", "when": "false" }, + { "command": "arboretum.createWorktree", "when": "false" }, + { "command": "arboretum.commitWorktree", "when": "false" }, + { "command": "arboretum.pushWorktree", "when": "false" }, + { "command": "arboretum.promoteWorktree", "when": "false" }, + { "command": "arboretum.startSession", "when": "false" }, + { "command": "arboretum.startGroupSession", "when": "false" }, + { "command": "arboretum.revealWorktree", "when": "false" } + ] + } + }, + "scripts": { + "typecheck": "tsc -p . --noEmit", + "build": "npm run typecheck && node esbuild.mjs", + "dev": "node esbuild.mjs --watch", + "vscode:prepublish": "node esbuild.mjs", + "package": "vsce package --no-dependencies" + }, + "devDependencies": { + "@arboretum/shared": "0.1.0", + "@types/node": "^22.10.0", + "@types/vscode": "^1.85.0", + "@types/ws": "^8.5.0", + "@vscode/vsce": "^3.2.0", + "esbuild": "^0.21.0", + "typescript": "^5.7.0", + "ws": "^8.18.0" + } +} diff --git a/packages/vscode/src/api/rest-client.ts b/packages/vscode/src/api/rest-client.ts new file mode 100644 index 0000000..537d5ae --- /dev/null +++ b/packages/vscode/src/api/rest-client.ts @@ -0,0 +1,173 @@ +// Client REST typé du daemon Arboretum — module pur (pas d'import vscode), testable. +// Auth par en-tête `Authorization: Bearer ` (le hook preValidation du serveur l'accepte +// au même titre que le cookie). Un client Node n'envoie pas d'en-tête Origin → il passe le check +// Origin strict côté serveur. Réutilise les types de `@arboretum/shared`. +import type { + ApiError, + CommitWorktreeRequest, + CreateGroupSessionRequest, + CreateSessionRequest, + CreateWorktreeRequest, + CreateWorktreeResponse, + GroupSessionResponse, + GroupsListResponse, + MeResponse, + PromoteWorktreeRequest, + PushWorktreeRequest, + RepoBranchesResponse, + ReposListResponse, + SessionResponse, + SessionsListResponse, + StartRepoSessionRequest, + WorktreeResponse, + WorktreesListResponse, +} from '@arboretum/shared'; +import { normalizeBaseUrl } from '../config.js'; + +/** Erreur normalisée portant le code/HTTP renvoyés par le serveur. */ +export class RestError extends Error { + constructor( + message: string, + readonly status: number, + readonly code: string, + ) { + super(message); + this.name = 'RestError'; + } +} + +type FetchLike = typeof fetch; + +export class RestClient { + private baseUrl: string; + private token: string | null; + + constructor( + opts: { baseUrl: string; token?: string | null }, + private readonly fetchImpl: FetchLike = fetch, + ) { + this.baseUrl = normalizeBaseUrl(opts.baseUrl); + this.token = opts.token ?? null; + } + + setBaseUrl(url: string): void { + this.baseUrl = normalizeBaseUrl(url); + } + + setToken(token: string | null): void { + this.token = token; + } + + get url(): string { + return this.baseUrl; + } + + // ---- auth ---- + /** Valide le token courant ; lève RestError(401) si invalide. */ + me(): Promise { + return this.request('GET', '/api/v1/auth/me'); + } + + // ---- repos & worktrees ---- + listRepos(): Promise { + return this.request('GET', '/api/v1/repos'); + } + + listWorktrees(): Promise { + return this.request('GET', '/api/v1/worktrees'); + } + + getBranches(repoId: string): Promise { + return this.request('GET', `/api/v1/repos/${encodeURIComponent(repoId)}/branches`); + } + + createWorktree(repoId: string, body: CreateWorktreeRequest): Promise { + return this.request('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees`, body); + } + + commitWorktree(repoId: string, body: CommitWorktreeRequest): Promise { + return this.request('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/commit`, body); + } + + pushWorktree(repoId: string, body: PushWorktreeRequest): Promise { + return this.request('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/push`, body); + } + + promoteWorktree(repoId: string, body: PromoteWorktreeRequest): Promise { + return this.request('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/promote`, body); + } + + startRepoSession(repoId: string, body: StartRepoSessionRequest): Promise { + return this.request('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/session`, body); + } + + // ---- sessions ---- + createSession(body: CreateSessionRequest): Promise { + return this.request('POST', '/api/v1/sessions', body); + } + + listSessions(includeHidden = false): Promise { + const q = includeHidden ? '?includeHidden=true' : ''; + return this.request('GET', `/api/v1/sessions${q}`); + } + + resumeSession(id: string): Promise { + return this.request('POST', `/api/v1/sessions/${encodeURIComponent(id)}/resume`, {}); + } + + forkSession(id: string): Promise { + return this.request('POST', `/api/v1/sessions/${encodeURIComponent(id)}/fork`, {}); + } + + hideSession(id: string): Promise<{ ok: true }> { + return this.request<{ ok: true }>('POST', `/api/v1/sessions/${encodeURIComponent(id)}/hide`, {}); + } + + killSession(id: string): Promise<{ ok: true }> { + return this.request<{ ok: true }>('DELETE', `/api/v1/sessions/${encodeURIComponent(id)}`); + } + + // ---- groupes ---- + listGroups(): Promise { + return this.request('GET', '/api/v1/groups'); + } + + createGroupSession(groupId: string, body: CreateGroupSessionRequest): Promise { + return this.request('POST', `/api/v1/groups/${encodeURIComponent(groupId)}/session`, body); + } + + // ---- interne ---- + private async request(method: string, path: string, body?: unknown): Promise { + const headers: Record = { Accept: 'application/json' }; + if (this.token) headers.Authorization = `Bearer ${this.token}`; + if (body !== undefined) headers['Content-Type'] = 'application/json'; + + const init: RequestInit = { method, headers }; + if (body !== undefined) init.body = JSON.stringify(body); + + let res: Response; + try { + res = await this.fetchImpl(`${this.baseUrl}${path}`, init); + } catch (err) { + throw new RestError(`Cannot reach Arboretum at ${this.baseUrl}: ${(err as Error).message}`, 0, 'NETWORK'); + } + + if (!res.ok) { + let code = `HTTP_${res.status}`; + let message = `${method} ${path} → ${res.status}`; + try { + const data = (await res.json()) as ApiError; + if (data?.error) { + code = data.error.code ?? code; + message = data.error.message ?? message; + } + } catch { + /* corps non-JSON : on garde le message générique */ + } + throw new RestError(message, res.status, code); + } + + if (res.status === 204) return undefined as T; + return (await res.json()) as T; + } +} diff --git a/packages/vscode/src/api/ws-client.ts b/packages/vscode/src/api/ws-client.ts new file mode 100644 index 0000000..8aa4d02 --- /dev/null +++ b/packages/vscode/src/api/ws-client.ts @@ -0,0 +1,425 @@ +// Client WebSocket multiplexé Node — portage du client web (packages/web/src/lib/ws-client.ts). +// UNE connexion, plusieurs canaux ; handshake hello/hello_ok, sub des topics, corrélation FIFO des +// 'attached', flow control par ACK (cumul d'octets PAR CANAL, remis à 0 à chaque RESYNC), reconnexion +// avec backoff. Différences vs web : +// - transport injecté (RawSocket) → testable sans réseau ; l'implémentation par défaut utilise `ws` +// avec l'en-tête `Authorization: Bearer` posé à l'upgrade (le navigateur, lui, envoie le cookie). +// - pas de callback de rendu (xterm) : on ACK les octets AU MOMENT où on les remet au sink (onDidWrite +// d'un Pseudoterminal émet de façon synchrone). Un ACK traînant débouncé flushe le reliquat < 64 Ko. +import { + BINARY_FRAME, + FLOW, + PROTOCOL_VERSION, + decodeBinaryFrame, + type ClientMessage, + type ServerMessage, +} from '@arboretum/shared'; +import { wsUrlFromBase } from '../config.js'; + +export type WsStatus = 'idle' | 'connecting' | 'open' | 'reconnecting'; +export type DetachReason = 'client' | 'session_exit' | 'replaced'; + +export type SessionEvent = Extract; +export type WorktreeEvent = Extract; +export type GroupEvent = Extract; + +/** Sink d'un canal : reçoit les octets BRUTS (le décodage UTF-8 est délégué au SessionBridge). */ +export interface AttachmentSink { + data(payload: Uint8Array): void; + reset(): void; + detached(reason: DetachReason): void; + controlChanged(controlling: boolean): void; +} + +export interface AttachOptions { + sessionId: string; + mode: 'interactive' | 'observer'; + cols: number; + rows: number; + sink: AttachmentSink; +} + +/** Transport minimal — abstrait pour les tests ; l'impl par défaut enveloppe `ws`. */ +export interface RawSocket { + send(data: string): void; + close(): void; + onOpen(cb: () => void): void; + onText(cb: (text: string) => void): void; + onBinary(cb: (data: Uint8Array) => void): void; + onClose(cb: () => void): void; + onError(cb: (err: Error) => void): void; +} + +export type SocketFactory = (wsUrl: string, token: string) => RawSocket; + +const BACKOFF_MIN_MS = 500; +const BACKOFF_MAX_MS = 10_000; + +export class Attachment { + channel = -1; + controlling = false; + closed = false; + cols: number; + rows: number; + epoch = 0; + processedBytes = 0; + lastAckBytes = 0; + trailingAckTimer: ReturnType | null = null; + pending: { resolve: (a: Attachment) => void; reject: (err: Error) => void } | null = null; + + constructor( + private readonly client: ArbWsClient, + readonly sessionId: string, + readonly mode: 'interactive' | 'observer', + readonly sink: AttachmentSink, + cols: number, + rows: number, + ) { + this.cols = cols; + this.rows = rows; + } + + sendStdin(data: string): void { + if (this.closed || this.channel < 0) return; + this.client.sendControl({ type: 'stdin', channel: this.channel, data }); + } + + /** Répond à un dialogue Claude sans clavier ; le serveur traduit en keystrokes. */ + answer(action: 'select' | 'confirm' | 'deny', optionN?: number): void { + if (this.closed || this.channel < 0) return; + this.client.sendControl( + action === 'select' && optionN !== undefined + ? { type: 'answer', channel: this.channel, action, optionN } + : { type: 'answer', channel: this.channel, action }, + ); + } + + resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + if (this.closed || this.channel < 0 || !this.controlling) return; + this.client.sendControl({ type: 'resize', channel: this.channel, cols, rows }); + } + + detach(): void { + if (this.closed) return; + this.closed = true; + this.pending = null; + this.client.releaseAttachment(this); + } +} + +export interface ArbWsClientOptions { + socketFactory: SocketFactory; + onStatus?: (status: WsStatus) => void; + onSession?: (e: SessionEvent) => void; + onWorktree?: (e: WorktreeEvent) => void; + onGroup?: (e: GroupEvent) => void; + /** appelé au premier hello_ok après (re)connexion → re-seed du store via REST. */ + onReady?: () => void; + log?: (msg: string) => void; +} + +export class ArbWsClient { + status: WsStatus = 'idle'; + + private baseUrl = ''; + private token = ''; + private socket: RawSocket | null = null; + private ready = false; + private stopped = true; + private backoffMs = BACKOFF_MIN_MS; + private reconnectTimer: ReturnType | null = null; + private readonly attachments = new Set(); + private readonly byChannel = new Map(); + private awaitingAttached: Attachment[] = []; + + constructor(private readonly opts: ArbWsClientOptions) {} + + /** (Re)configure l'URL/token ; si la connexion était active, elle est relancée. */ + configure(baseUrl: string, token: string): void { + const wasActive = !this.stopped; + this.baseUrl = baseUrl; + this.token = token; + if (wasActive) { + this.hardReset(); + this.connect(); + } + } + + connect(): void { + this.stopped = false; + if (this.socket || this.reconnectTimer) return; + if (!this.token) return; // pas authentifié : on n'ouvre rien + this.setStatus('connecting'); + this.openSocket(); + } + + disconnect(): void { + this.stopped = true; + this.hardReset(); + this.setStatus('idle'); + } + + /** résout sur le prochain 'attached' corrélé ; rejette si la session est introuvable/terminée */ + attach(opts: AttachOptions): Promise { + const att = new Attachment(this, opts.sessionId, opts.mode, opts.sink, opts.cols, opts.rows); + const promise = new Promise((resolve, reject) => { + att.pending = { resolve, reject }; + }); + this.attachments.add(att); + this.connect(); + if (this.ready) this.sendAttach(att); + return promise; + } + + sendControl(msg: ClientMessage): void { + if (!this.ready || !this.socket) return; + try { + this.socket.send(JSON.stringify(msg)); + } catch { + /* socket en cours de fermeture : la reconnexion rejouera */ + } + } + + releaseAttachment(att: Attachment): void { + this.attachments.delete(att); + if (att.trailingAckTimer) { + clearTimeout(att.trailingAckTimer); + att.trailingAckTimer = null; + } + if (att.channel >= 0) { + this.byChannel.delete(att.channel); + this.sendControl({ type: 'detach', channel: att.channel }); + att.channel = -1; + } + } + + // ---- interne ---- + + private setStatus(s: WsStatus): void { + this.status = s; + this.opts.onStatus?.(s); + } + + /** ferme la socket courante et invalide tous les états sans toucher au flag stopped. */ + private hardReset(): void { + this.clearReconnectTimer(); + const socket = this.socket; + this.socket = null; + this.ready = false; + for (const att of this.attachments) { + if (att.trailingAckTimer) { + clearTimeout(att.trailingAckTimer); + att.trailingAckTimer = null; + } + att.pending?.reject(new Error('disconnected')); + att.pending = null; + att.closed = true; + att.sink.detached('client'); + } + this.attachments.clear(); + this.byChannel.clear(); + this.awaitingAttached = []; + socket?.close(); + } + + private openSocket(): void { + const socket = this.opts.socketFactory(wsUrlFromBase(this.baseUrl), this.token); + this.socket = socket; + socket.onOpen(() => { + const hello: ClientMessage = { type: 'hello', protocol: PROTOCOL_VERSION }; + socket.send(JSON.stringify(hello)); + }); + socket.onText((text) => this.handleText(text)); + socket.onBinary((data) => this.handleBinary(data)); + socket.onClose(() => this.handleClose(socket)); + socket.onError((err) => this.opts.log?.(`ws error: ${err.message}`)); + } + + private handleClose(socket: RawSocket): void { + if (this.socket !== socket) return; + this.socket = null; + this.ready = false; + this.byChannel.clear(); + this.awaitingAttached = []; + for (const att of this.attachments) { + att.channel = -1; + att.epoch += 1; + if (att.trailingAckTimer) { + clearTimeout(att.trailingAckTimer); + att.trailingAckTimer = null; + } + } + if (this.stopped) { + this.setStatus('idle'); + return; + } + this.setStatus('reconnecting'); + this.scheduleReconnect(); + } + + private scheduleReconnect(): void { + this.clearReconnectTimer(); + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + if (!this.stopped) this.openSocket(); + }, this.backoffMs); + this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS); + } + + private clearReconnectTimer(): void { + if (this.reconnectTimer !== null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + private sendAttach(att: Attachment): void { + this.awaitingAttached.push(att); + this.sendControl({ type: 'attach', sessionId: att.sessionId, mode: att.mode, cols: att.cols, rows: att.rows }); + } + + private handleText(text: string): void { + let msg: ServerMessage; + try { + msg = JSON.parse(text) as ServerMessage; + } catch { + return; + } + this.handleServerMessage(msg); + } + + private handleBinary(data: Uint8Array): void { + if (data.byteLength < BINARY_FRAME.HEADER_BYTES) return; + const frame = decodeBinaryFrame(data); + const att = this.byChannel.get(frame.channel); + if (!att || att.closed) return; + + if (frame.type === BINARY_FRAME.RESYNC) { + // le serveur repart de sentBytes=0 : reset terminal, replay, compteurs à zéro (le replay ne compte pas) + att.epoch += 1; + att.processedBytes = 0; + att.lastAckBytes = 0; + att.sink.reset(); + if (frame.payload.byteLength > 0) att.sink.data(frame.payload); + this.scheduleTrailingAck(att, att.epoch); + return; + } + if (frame.type !== BINARY_FRAME.OUTPUT) return; + + // Émission synchrone vers le sink (onDidWrite) → on ACK directement les octets remis. + const length = frame.payload.byteLength; + att.sink.data(frame.payload); + if (att.channel < 0) return; + att.processedBytes += length; + if (att.processedBytes - att.lastAckBytes >= FLOW.ACK_EVERY_BYTES) { + att.lastAckBytes = att.processedBytes; + this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes }); + } + // Filet : flushe le reliquat < ACK_EVERY_BYTES pour que le serveur sorte toujours de pause. + this.scheduleTrailingAck(att, att.epoch); + } + + private scheduleTrailingAck(att: Attachment, epoch: number): void { + if (att.trailingAckTimer) clearTimeout(att.trailingAckTimer); + att.trailingAckTimer = setTimeout(() => { + att.trailingAckTimer = null; + if (att.closed || att.epoch !== epoch || att.channel < 0) return; + if (att.processedBytes > att.lastAckBytes) { + att.lastAckBytes = att.processedBytes; + this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes }); + } + }, 200); + } + + private handleServerMessage(msg: ServerMessage): void { + switch (msg.type) { + case 'hello_ok': { + this.ready = true; + this.backoffMs = BACKOFF_MIN_MS; + this.setStatus('open'); + this.sendControl({ type: 'sub', topics: ['sessions', 'worktrees', 'groups'] }); + for (const att of this.attachments) { + if (!att.closed) this.sendAttach(att); + } + this.opts.onReady?.(); + return; + } + case 'attached': { + const att = this.awaitingAttached.shift(); + if (!att) return; + if (att.closed) { + this.sendControl({ type: 'detach', channel: msg.channel }); + return; + } + const wasControlling = att.controlling; + att.channel = msg.channel; + att.controlling = msg.controlling; + this.byChannel.set(msg.channel, att); + if (att.pending) { + att.pending.resolve(att); + att.pending = null; + } else if (wasControlling !== msg.controlling) { + att.sink.controlChanged(msg.controlling); + } + return; + } + case 'detached': { + const att = this.byChannel.get(msg.channel); + if (!att) return; + this.byChannel.delete(msg.channel); + att.channel = -1; + if (msg.reason === 'client') return; + att.closed = true; + this.attachments.delete(att); + att.sink.detached(msg.reason); + return; + } + case 'control_changed': { + const att = this.byChannel.get(msg.channel); + if (!att || att.closed) return; + att.controlling = msg.controlling; + att.sink.controlChanged(msg.controlling); + return; + } + case 'session_update': + case 'session_exit': + this.opts.onSession?.(msg); + return; + case 'repo_update': + case 'repo_removed': + case 'worktree_update': + case 'worktree_removed': + this.opts.onWorktree?.(msg); + return; + case 'group_update': + case 'group_removed': + this.opts.onGroup?.(msg); + return; + case 'error': { + if (msg.channel !== undefined) { + this.opts.log?.(`channel ${msg.channel}: ${msg.code} — ${msg.message}`); + return; + } + if ((msg.code === 'NOT_FOUND' || msg.code === 'SESSION_EXITED') && this.awaitingAttached.length > 0) { + const att = this.awaitingAttached.shift(); + if (!att) return; + this.attachments.delete(att); + if (att.pending) { + att.pending.reject(new Error(msg.code)); + att.pending = null; + } else if (!att.closed) { + att.closed = true; + att.sink.detached('session_exit'); + } + return; + } + this.opts.log?.(`${msg.code} — ${msg.message}`); + return; + } + case 'pong': + return; + } + } +} diff --git a/packages/vscode/src/api/ws-socket.ts b/packages/vscode/src/api/ws-socket.ts new file mode 100644 index 0000000..cd6af0d --- /dev/null +++ b/packages/vscode/src/api/ws-socket.ts @@ -0,0 +1,33 @@ +// Implémentation par défaut du transport RawSocket, basée sur le paquet `ws` (Node). +// L'auth se fait en posant `Authorization: Bearer ` dans les en-têtes de l'upgrade HTTP +// (un navigateur enverrait le cookie ; un client Node passe le token ici). Isolé du cœur ws-client +// pour que celui-ci reste injectable et testable sans réseau. +import WebSocket, { type RawData } from 'ws'; +import type { RawSocket, SocketFactory } from './ws-client.js'; + +/** Normalise un RawData (Buffer | ArrayBuffer | Buffer[]) en Uint8Array copiée (offset 0). */ +function toUint8(data: RawData): Uint8Array { + if (Array.isArray(data)) return new Uint8Array(Buffer.concat(data)); + if (data instanceof ArrayBuffer) return new Uint8Array(data); + // Buffer (sous-classe de Uint8Array) : copie pour ne pas dépendre du buffer poolé réutilisé par ws. + return new Uint8Array(data); +} + +export const wsSocketFactory: SocketFactory = (wsUrl: string, token: string): RawSocket => { + const socket = new WebSocket(wsUrl, { headers: { Authorization: `Bearer ${token}` } }); + return { + send: (d) => socket.send(d), + close: () => socket.close(), + onOpen: (cb) => socket.on('open', cb), + onText: (cb) => + socket.on('message', (data: RawData, isBinary: boolean) => { + if (!isBinary) cb(toUint8(data).toString()); + }), + onBinary: (cb) => + socket.on('message', (data: RawData, isBinary: boolean) => { + if (isBinary) cb(toUint8(data)); + }), + onClose: (cb) => socket.on('close', () => cb()), + onError: (cb) => socket.on('error', (err: Error) => cb(err)), + }; +}; diff --git a/packages/vscode/src/auth/auth.ts b/packages/vscode/src/auth/auth.ts new file mode 100644 index 0000000..ad92bf0 --- /dev/null +++ b/packages/vscode/src/auth/auth.ts @@ -0,0 +1,22 @@ +// Stockage du token dans le SecretStorage de VS Code (chiffré par l'hôte). Les tokens du daemon sont +// stockés hashés côté serveur et le bootstrap ne s'affiche qu'une fois → on ne peut pas « lire » un +// token existant : l'utilisateur le saisit une fois (commande Sign In), on le conserve ici. +import type { SecretStorage } from 'vscode'; + +const TOKEN_KEY = 'arboretum.token'; + +export class Auth { + constructor(private readonly secrets: SecretStorage) {} + + getToken(): Thenable { + return this.secrets.get(TOKEN_KEY); + } + + storeToken(token: string): Thenable { + return this.secrets.store(TOKEN_KEY, token); + } + + clearToken(): Thenable { + return this.secrets.delete(TOKEN_KEY); + } +} diff --git a/packages/vscode/src/commands.ts b/packages/vscode/src/commands.ts new file mode 100644 index 0000000..8b85515 --- /dev/null +++ b/packages/vscode/src/commands.ts @@ -0,0 +1,299 @@ +// Enregistrement de toutes les commandes de l'extension. Les handlers contextuels reçoivent le nœud +// d'arbre cliqué (ou via menu) et narrowent dessus ; les mutations passent par le RestClient et l'état +// se met à jour via les events WS (pas de refetch manuel, sauf hide qui sort la session de la liste). +import * as vscode from 'vscode'; +import type { GroupSummary, RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared'; +import { RestError, type RestClient } from './api/rest-client.js'; +import type { ArbWsClient, AttachmentSink } from './api/ws-client.js'; +import type { Store } from './state/store.js'; +import type { SessionTerminalManager } from './terminal/session-pty.js'; +import type { WorkspaceMapper } from './workspace.js'; +import type { AnyNode } from './views/nodes.js'; + +export interface CommandDeps { + store: Store; + rest: RestClient; + ws: ArbWsClient; + terminals: SessionTerminalManager; + workspace: WorkspaceMapper; + signIn(): Promise; + signOut(): Promise; + reseed(): Promise; +} + +// ---- extraction des nœuds ---- +function asSession(node: AnyNode | undefined): SessionSummary | undefined { + return node && (node.kind === 'session' || node.kind === 'group-session') ? node.session : undefined; +} +function asWorktree(node: AnyNode | undefined): { repoId: string; worktree: WorktreeSummary } | undefined { + return node?.kind === 'worktree' ? { repoId: node.repoId, worktree: node.worktree } : undefined; +} +function asRepo(node: AnyNode | undefined): RepoSummary | undefined { + return node?.kind === 'repo' ? node.repo : undefined; +} +function asGroup(node: AnyNode | undefined): GroupSummary | undefined { + return node?.kind === 'group' ? node.group : undefined; +} + +/** Exécute une action en affichant proprement une erreur REST (code + message serveur). */ +async function run(label: string, fn: () => Promise): Promise { + try { + await fn(); + } catch (err) { + const msg = err instanceof RestError ? `${err.code}: ${err.message}` : (err as Error).message; + void vscode.window.showErrorMessage(`Arboretum — ${label}: ${msg}`); + } +} + +/** Répond à un dialogue : via le terminal ouvert si présent, sinon par une attache éphémère. */ +export async function answerSession( + deps: CommandDeps, + session: SessionSummary, + action: 'select' | 'confirm' | 'deny', + optionN?: number, +): Promise { + const open = deps.terminals.getBridge(session.id); + if (open) { + open.answer(action, optionN); + return; + } + const noop: AttachmentSink = { data: () => {}, reset: () => {}, detached: () => {}, controlChanged: () => {} }; + const att = await deps.ws.attach({ sessionId: session.id, mode: 'interactive', cols: 80, rows: 24, sink: noop }); + att.answer(action, optionN); + setTimeout(() => att.detach(), 600); // laisse le serveur traiter avant de fermer le canal éphémère +} + +export function registerCommands(context: vscode.ExtensionContext, deps: CommandDeps): void { + const { store, rest, terminals, workspace } = deps; + + const cmd = (id: string, handler: (...args: unknown[]) => unknown): void => { + context.subscriptions.push(vscode.commands.registerCommand(id, handler)); + }; + + // ---- auth / global ---- + cmd('arboretum.signIn', () => deps.signIn()); + cmd('arboretum.signOut', () => deps.signOut()); + cmd('arboretum.refresh', () => run('refresh', () => deps.reseed())); + cmd('arboretum.openDashboard', () => { + void vscode.env.openExternal(vscode.Uri.parse(rest.url)); + }); + + // ---- terminaux (Phase B) ---- + cmd('arboretum.attachSession', (node) => { + const s = asSession(node as AnyNode); + if (s) terminals.open(s, 'interactive'); + }); + cmd('arboretum.observeSession', (node) => { + const s = asSession(node as AnyNode); + if (s) terminals.open(s, 'observer'); + }); + cmd('arboretum.showWaiting', async () => { + const waiting = store.waitingSessions(); + if (waiting.length === 0) { + void vscode.window.showInformationMessage('Arboretum: no session waiting.'); + return; + } + const pick = await vscode.window.showQuickPick( + waiting.map((s) => ({ label: s.title?.trim() || s.command, description: s.waitingFor ?? s.cwd, session: s })), + { placeHolder: 'Sessions waiting for input' }, + ); + if (pick) terminals.open(pick.session, 'interactive'); + }); + + // ---- answer (Phase C) ---- + cmd('arboretum.answerSession', async (node) => { + const s = asSession(node as AnyNode); + if (!s) return; + const dialogOptions = s.dialog?.options ?? []; + if (dialogOptions.length > 0) { + const pick = await vscode.window.showQuickPick( + dialogOptions.map((o) => ({ label: o.label, option: o })), + { placeHolder: s.dialog?.waitingFor ?? 'Choose an option' }, + ); + if (pick) await run('answer', () => answerSession(deps, s, 'select', pick.option.n)); + return; + } + const pick = await vscode.window.showQuickPick(['Yes', 'No'], { placeHolder: s.waitingFor ?? 'Answer the prompt' }); + if (pick) await run('answer', () => answerSession(deps, s, pick === 'Yes' ? 'confirm' : 'deny')); + }); + + // ---- sessions ---- + cmd('arboretum.killSession', async (node) => { + const s = asSession(node as AnyNode); + if (!s) return; + const ok = await vscode.window.showWarningMessage(`Kill session "${s.title?.trim() || s.command}"?`, { modal: true }, 'Kill'); + if (ok === 'Kill') await run('kill', async () => void (await rest.killSession(s.id))); + }); + cmd('arboretum.hideSession', (node) => { + const s = asSession(node as AnyNode); + if (!s) return; + void run('hide', async () => { + await rest.hideSession(s.id); + await deps.reseed(); // la session masquée sort de la liste (listSessions sans includeHidden) + }); + }); + cmd('arboretum.resumeSession', (node) => { + const s = asSession(node as AnyNode); + if (!s) return; + void run('resume', async () => { + const res = await rest.resumeSession(s.id); + terminals.open(res.session, 'interactive'); + }); + }); + cmd('arboretum.forkSession', (node) => { + const s = asSession(node as AnyNode); + if (!s) return; + void run('fork', async () => { + const res = await rest.forkSession(s.id); + terminals.open(res.session, 'interactive'); + }); + }); + + // ---- worktrees (Phase C) ---- + cmd('arboretum.createWorktree', (node) => { + const repo = asRepo(node as AnyNode); + if (repo) void createWorktreeFlow(deps, repo.id); + }); + cmd('arboretum.commitWorktree', (node) => { + const wt = asWorktree(node as AnyNode); + if (!wt) return; + void run('commit', async () => { + const message = await vscode.window.showInputBox({ prompt: 'Commit message', placeHolder: 'Describe your changes' }); + if (!message) return; + await rest.commitWorktree(wt.repoId, { path: wt.worktree.path, message }); + void vscode.window.showInformationMessage('Arboretum: committed.'); + }); + }); + cmd('arboretum.pushWorktree', (node) => { + const wt = asWorktree(node as AnyNode); + if (!wt) return; + void vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Arboretum: pushing…' }, () => + run('push', async () => { + await rest.pushWorktree(wt.repoId, { path: wt.worktree.path }); + void vscode.window.showInformationMessage('Arboretum: pushed.'); + }), + ); + }); + cmd('arboretum.promoteWorktree', (node) => { + const wt = asWorktree(node as AnyNode); + if (wt) void promoteWorktreeFlow(deps, wt.repoId, wt.worktree); + }); + + // ---- démarrage de session ---- + cmd('arboretum.startSession', (node) => { + const wt = asWorktree(node as AnyNode); + if (wt) { + void run('start session', async () => { + const res = await rest.createSession({ cwd: wt.worktree.path, command: 'claude' }); + terminals.open(res.session, 'interactive'); + }); + return; + } + const repo = asRepo(node as AnyNode); + if (repo) { + void run('start session', async () => { + const res = await rest.startRepoSession(repo.id, { command: 'claude' }); + terminals.open(res.session, 'interactive'); + }); + } + }); + cmd('arboretum.startGroupSession', (node) => { + const group = asGroup(node as AnyNode); + if (group) void startGroupSessionFlow(deps, group); + }); + + // ---- conscience du workspace (Phase D) ---- + cmd('arboretum.startSessionHere', () => { + const repoId = workspace.currentRepoId(); + if (!repoId) { + void vscode.window.showWarningMessage('Arboretum: the current folder is not a known repo/worktree.'); + return; + } + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) return; + void run('start session', async () => { + const res = await rest.createSession({ cwd: folder.uri.fsPath, command: 'claude' }); + terminals.open(res.session, 'interactive'); + }); + }); + cmd('arboretum.createWorktreeHere', () => { + const repoId = workspace.currentRepoId(); + if (!repoId) { + void vscode.window.showWarningMessage('Arboretum: the current folder is not a known repo.'); + return; + } + void createWorktreeFlow(deps, repoId); + }); + cmd('arboretum.revealWorktree', () => workspace.revealCurrent()); +} + +// ---- flows interactifs ---- + +async function createWorktreeFlow(deps: CommandDeps, repoId: string): Promise { + await run('create worktree', async () => { + const branches = await deps.rest.getBranches(repoId); + const existing = [...new Set([...branches.local, ...branches.remote.map((b) => b.replace(/^origin\//, ''))])].sort(); + const NEW = '$(add) New branch…'; + const items = [NEW, ...existing]; + const choice = await vscode.window.showQuickPick(items, { placeHolder: 'Branch for the new worktree (auto: checkout if exists, else create)' }); + if (!choice) return; + let branch = choice; + if (choice === NEW) { + const input = await vscode.window.showInputBox({ prompt: 'New branch name' }); + if (!input) return; + branch = input.trim(); + } + if (!branch) return; + const res = await deps.rest.createWorktree(repoId, { branch, mode: 'auto' }); + void vscode.window.showInformationMessage(`Arboretum: worktree ${res.action} for "${branch}".`); + }); +} + +async function promoteWorktreeFlow(deps: CommandDeps, repoId: string, worktree: WorktreeSummary): Promise { + const label = worktree.branch ?? worktree.path; + const confirm = await vscode.window.showWarningMessage( + `Promote "${label}" to the repo's main checkout? The worktree is removed (branch kept).`, + { modal: true }, + 'Promote', + ); + if (confirm !== 'Promote') return; + try { + await deps.rest.promoteWorktree(repoId, { path: worktree.path }); + void vscode.window.showInformationMessage(`Arboretum: "${label}" promoted to main.`); + } catch (err) { + if (err instanceof RestError && err.status === 409) { + const force = await vscode.window.showWarningMessage( + `Arboretum: working tree is dirty (${err.message}). Promote anyway?`, + { modal: true }, + 'Force promote', + ); + if (force === 'Force promote') { + await run('promote', async () => { + await deps.rest.promoteWorktree(repoId, { path: worktree.path, force: true }); + void vscode.window.showInformationMessage(`Arboretum: "${label}" promoted (forced).`); + }); + } + return; + } + const msg = err instanceof RestError ? `${err.code}: ${err.message}` : (err as Error).message; + void vscode.window.showErrorMessage(`Arboretum — promote: ${msg}`); + } +} + +async function startGroupSessionFlow(deps: CommandDeps, group: GroupSummary): Promise { + await run('start group session', async () => { + const branch = await vscode.window.showInputBox({ + prompt: `Branch to cover in each repo of "${group.label}" (leave empty for the main checkouts)`, + placeHolder: 'feature/cross-repo (optional)', + }); + if (branch === undefined) return; // annulé (chaîne vide = checkouts principaux) + const body = branch.trim() ? { command: 'claude' as const, branch: branch.trim() } : { command: 'claude' as const }; + const res = await deps.rest.createGroupSession(group.id, body); + deps.terminals.open(res.session, 'interactive'); + if (res.skipped.length > 0) { + void vscode.window.showWarningMessage( + `Arboretum: ${res.skipped.length} repo(s) skipped — ${res.skipped.map((s) => s.reason).join('; ')}`, + ); + } + }); +} diff --git a/packages/vscode/src/config.ts b/packages/vscode/src/config.ts new file mode 100644 index 0000000..5cc9228 --- /dev/null +++ b/packages/vscode/src/config.ts @@ -0,0 +1,21 @@ +// Lecture des réglages `arboretum.*` — module pur (aucun import vscode) pour rester testable. +// L'extension fournit un `ConfigSnapshot` lu depuis workspace.getConfiguration ; ce module +// normalise/dérive les valeurs (notamment l'URL WebSocket à partir de l'URL HTTP). + +export interface ConfigSnapshot { + url: string; + showExternalSessions: boolean; + notifyOnWaiting: boolean; +} + +/** Base REST normalisée (sans slash final) ; `http://127.0.0.1:7317` par défaut. */ +export function normalizeBaseUrl(url: string): string { + const trimmed = (url || '').trim().replace(/\/+$/, ''); + return trimmed || 'http://127.0.0.1:7317'; +} + +/** Dérive l'URL du endpoint WebSocket `/ws` (http→ws, https→wss). */ +export function wsUrlFromBase(baseUrl: string): string { + const base = normalizeBaseUrl(baseUrl); + return `${base.replace(/^http/, 'ws')}/ws`; +} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts new file mode 100644 index 0000000..aaacc87 --- /dev/null +++ b/packages/vscode/src/extension.ts @@ -0,0 +1,152 @@ +// Point d'entrée de l'extension : câble auth ↔ REST ↔ WS ↔ store ↔ surfaces natives (arbres, terminaux, +// status bar, notifications, workspace). C'est le SEUL gros consommateur de l'API vscode avec les vues ; +// toute la logique réutilisable (protocole, état) vit dans des modules purs testables. +import * as vscode from 'vscode'; +import { Auth } from './auth/auth.js'; +import { RestClient } from './api/rest-client.js'; +import { ArbWsClient } from './api/ws-client.js'; +import { wsSocketFactory } from './api/ws-socket.js'; +import { Store } from './state/store.js'; +import { SessionTerminalManager } from './terminal/session-pty.js'; +import { ReposTreeProvider } from './views/repos-tree.js'; +import { GroupsTreeProvider } from './views/groups-tree.js'; +import { StatusBar } from './status-bar.js'; +import { WaitingNotifier } from './notifications.js'; +import { WorkspaceMapper } from './workspace.js'; +import { answerSession, registerCommands } from './commands.js'; + +function readUrl(): string { + return vscode.workspace.getConfiguration('arboretum').get('url', 'http://127.0.0.1:7317'); +} +function readShowExternal(): boolean { + return vscode.workspace.getConfiguration('arboretum').get('showExternalSessions', false); +} +function readNotify(): boolean { + return vscode.workspace.getConfiguration('arboretum').get('notifyOnWaiting', true); +} + +async function setConnectedContext(connected: boolean): Promise { + await vscode.commands.executeCommand('setContext', 'arboretum.connected', connected); +} + +export async function activate(context: vscode.ExtensionContext): Promise { + const log = vscode.window.createOutputChannel('Arboretum'); + context.subscriptions.push(log); + + const auth = new Auth(context.secrets); + const store = new Store(); + store.showExternalSessions = readShowExternal(); + const rest = new RestClient({ baseUrl: readUrl() }); + + const reseed = async (): Promise => { + try { + await store.seed(rest); + } catch (err) { + log.appendLine(`seed failed: ${(err as Error).message}`); + } + }; + + const statusBar = new StatusBar(store); + context.subscriptions.push(statusBar); + + const ws = new ArbWsClient({ + socketFactory: wsSocketFactory, + onSession: (e) => store.applySession(e), + onWorktree: (e) => store.applyWorktree(e), + onGroup: (e) => store.applyGroup(e), + onReady: () => void reseed(), + onStatus: (s) => { + const connected = s === 'open'; + statusBar.setConnected(connected); + void setConnectedContext(connected); + }, + log: (m) => log.appendLine(m), + }); + + const terminals = new SessionTerminalManager(ws); + context.subscriptions.push({ dispose: () => terminals.dispose() }); + + const reposProvider = new ReposTreeProvider(store); + const groupsProvider = new GroupsTreeProvider(store); + const reposView = vscode.window.createTreeView('arboretum.repos', { treeDataProvider: reposProvider, showCollapseAll: true }); + const groupsView = vscode.window.createTreeView('arboretum.groups', { treeDataProvider: groupsProvider, showCollapseAll: true }); + context.subscriptions.push(reposView, groupsView); + + const workspace = new WorkspaceMapper(store, reposView, reposProvider); + context.subscriptions.push(workspace); + + const signIn = async (): Promise => { + const token = await vscode.window.showInputBox({ + prompt: `Arboretum token for ${rest.url}`, + password: true, + ignoreFocusOut: true, + placeHolder: 'arb_… (printed once on first daemon start, or created in Settings → Tokens)', + }); + if (!token) return; + // valide le token avant de le stocker (sonde isolée pour ne pas polluer le client principal). + const probe = new RestClient({ baseUrl: rest.url, token }); + try { + await probe.me(); + } catch (err) { + void vscode.window.showErrorMessage(`Arboretum: sign in failed — ${(err as Error).message}`); + return; + } + await auth.storeToken(token); + rest.setToken(token); + ws.configure(rest.url, token); + ws.connect(); + void vscode.window.showInformationMessage('Arboretum: connected.'); + }; + + const signOut = async (): Promise => { + await auth.clearToken(); + rest.setToken(null); + ws.disconnect(); + store.clear(); + await setConnectedContext(false); + void vscode.window.showInformationMessage('Arboretum: signed out.'); + }; + + registerCommands(context, { store, rest, ws, terminals, workspace, signIn, signOut, reseed }); + + // Notifications : ouvrir le terminal ou répondre Yes/No (réutilise le helper transient-attach). + const notifier = new WaitingNotifier(store, { + notifyEnabled: () => readNotify(), + openTerminal: (session) => terminals.open(session, 'interactive'), + answer: (session, action) => { + void answerSession({ store, rest, ws, terminals, workspace, signIn, signOut, reseed }, session, action).catch((err: Error) => + log.appendLine(`answer failed: ${err.message}`), + ); + }, + }); + context.subscriptions.push(notifier); + + // Réglages à chaud : URL (reconfigure REST+WS), sessions externes (refiltre), notif (lu à la volée). + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('arboretum.url')) { + rest.setBaseUrl(readUrl()); + const token = await auth.getToken(); + if (token) ws.configure(rest.url, token); + } + if (e.affectsConfiguration('arboretum.showExternalSessions')) { + store.setShowExternalSessions(readShowExternal()); + } + }), + ); + + // Connexion initiale si un token est déjà mémorisé. + await setConnectedContext(false); + const token = await auth.getToken(); + if (token) { + rest.setToken(token); + ws.configure(rest.url, token); + ws.connect(); + } + + context.subscriptions.push({ dispose: () => ws.disconnect() }); +} + +export function deactivate(): void { + // tout est enregistré dans context.subscriptions → disposé par VS Code. +} diff --git a/packages/vscode/src/notifications.ts b/packages/vscode/src/notifications.ts new file mode 100644 index 0000000..eed6301 --- /dev/null +++ b/packages/vscode/src/notifications.ts @@ -0,0 +1,57 @@ +// Notifications natives sur front montant d'une session vers `waiting` (doublonne utilement le Web +// Push, ici dans l'éditeur). Suit l'activité précédente par session pour ne notifier qu'à la transition, +// pas à chaque mise à jour. Actions : ouvrir le terminal, ou répondre Yes/No via la commande WS `answer`. +import * as vscode from 'vscode'; +import type { SessionSummary } from '@arboretum/shared'; +import type { Store } from './state/store.js'; + +export interface NotificationHooks { + notifyEnabled(): boolean; + openTerminal(session: SessionSummary): void; + answer(session: SessionSummary, action: 'confirm' | 'deny'): void; +} + +export class WaitingNotifier implements vscode.Disposable { + /** dernière activité connue par session → détection du front montant vers 'waiting'. */ + private readonly lastActivity = new Map(); + private readonly unsubscribe: () => void; + + constructor( + private readonly store: Store, + private readonly hooks: NotificationHooks, + ) { + this.unsubscribe = store.onDidChange(() => this.check()); + } + + private check(): void { + const sessions = this.store.allSessions(); + const seen = new Set(); + for (const s of sessions) { + seen.add(s.id); + const prev = this.lastActivity.get(s.id); + const cur = s.live ? s.activity : null; + this.lastActivity.set(s.id, cur); + if (cur === 'waiting' && prev !== 'waiting' && prev !== undefined && this.hooks.notifyEnabled()) { + this.notify(s); + } + // prev === undefined : première observation (au seed) → on n'annonce pas l'historique. + } + for (const id of [...this.lastActivity.keys()]) if (!seen.has(id)) this.lastActivity.delete(id); + } + + private notify(session: SessionSummary): void { + const title = session.title?.trim() || session.command; + const detail = session.waitingFor ? ` — ${session.waitingFor}` : ''; + void vscode.window + .showInformationMessage(`Arboretum: "${title}" is waiting for input${detail}`, 'Open Terminal', 'Yes', 'No') + .then((choice) => { + if (choice === 'Open Terminal') this.hooks.openTerminal(session); + else if (choice === 'Yes') this.hooks.answer(session, 'confirm'); + else if (choice === 'No') this.hooks.answer(session, 'deny'); + }); + } + + dispose(): void { + this.unsubscribe(); + } +} diff --git a/packages/vscode/src/state/store.ts b/packages/vscode/src/state/store.ts new file mode 100644 index 0000000..4c95e81 --- /dev/null +++ b/packages/vscode/src/state/store.ts @@ -0,0 +1,196 @@ +// État mémoire de l'extension — module pur (aucun import vscode), testable. +// Seedé via REST au (re)connect, puis patché par les events WS. Émet onDidChange à chaque mutation ; +// les arbres / status bar / notifications s'y abonnent. Principe « groupe léger » : on ne corrèle pas +// les worktrees aux sessions via la liste embarquée (potentiellement périmée) mais par `cwd`, exactement +// comme `sessionsForCwd` côté serveur. +import type { + GroupSummary, + RepoSummary, + SessionSummary, + WorktreeSummary, +} from '@arboretum/shared'; +import type { GroupEvent, SessionEvent, WorktreeEvent } from '../api/ws-client.js'; +import type { RestClient } from '../api/rest-client.js'; + +/** Normalise un chemin absolu pour la corrélation (retire le slash final). */ +function normPath(p: string): string { + return p.replace(/\/+$/, '') || '/'; +} + +export class Store { + private readonly repos = new Map(); + /** worktrees par repo, indexés par chemin (clé stable). */ + private readonly worktrees = new Map>(); + private readonly sessions = new Map(); + private readonly groups = new Map(); + private readonly listeners = new Set<() => void>(); + + /** affiche aussi les sessions externes (découvertes hors Arboretum). */ + showExternalSessions = false; + + onDidChange(cb: () => void): () => void { + this.listeners.add(cb); + return () => this.listeners.delete(cb); + } + + /** Bascule l'affichage des sessions externes et notifie les abonnés (refiltre les arbres). */ + setShowExternalSessions(value: boolean): void { + if (this.showExternalSessions === value) return; + this.showExternalSessions = value; + this.emit(); + } + + private emit(): void { + for (const cb of this.listeners) cb(); + } + + /** Charge tout l'état depuis le daemon (au premier hello_ok et à chaque reconnexion). */ + async seed(rest: RestClient): Promise { + const [repos, worktrees, sessions, groups] = await Promise.all([ + rest.listRepos(), + rest.listWorktrees(), + rest.listSessions(false), + rest.listGroups(), + ]); + this.repos.clear(); + this.worktrees.clear(); + this.sessions.clear(); + this.groups.clear(); + for (const r of repos.repos) this.repos.set(r.id, r); + for (const w of worktrees.worktrees) this.putWorktree(w); + for (const s of sessions.sessions) this.sessions.set(s.id, s); + for (const g of groups.groups) this.groups.set(g.id, g); + this.emit(); + } + + clear(): void { + this.repos.clear(); + this.worktrees.clear(); + this.sessions.clear(); + this.groups.clear(); + this.emit(); + } + + private putWorktree(w: WorktreeSummary): void { + let byPath = this.worktrees.get(w.repoId); + if (!byPath) { + byPath = new Map(); + this.worktrees.set(w.repoId, byPath); + } + byPath.set(normPath(w.path), w); + // les sessions embarquées dans le worktree alimentent aussi la map sessions (état initial). + for (const s of w.sessions) if (!this.sessions.has(s.id)) this.sessions.set(s.id, s); + } + + // ---- réducteurs d'events WS ---- + + applySession(e: SessionEvent): void { + if (e.type === 'session_update') { + this.sessions.set(e.session.id, e.session); + } else { + // session_exit : marque morte si connue (sinon ignore). + const prev = this.sessions.get(e.sessionId); + if (prev) this.sessions.set(e.sessionId, { ...prev, live: false, status: 'exited', exitCode: e.exitCode }); + } + this.emit(); + } + + applyWorktree(e: WorktreeEvent): void { + switch (e.type) { + case 'repo_update': + this.repos.set(e.repo.id, e.repo); + break; + case 'repo_removed': + this.repos.delete(e.repoId); + this.worktrees.delete(e.repoId); + break; + case 'worktree_update': + this.putWorktree(e.worktree); + break; + case 'worktree_removed': + this.worktrees.get(e.repoId)?.delete(normPath(e.path)); + break; + } + this.emit(); + } + + applyGroup(e: GroupEvent): void { + if (e.type === 'group_update') this.groups.set(e.group.id, e.group); + else this.groups.delete(e.groupId); + this.emit(); + } + + // ---- lecture ---- + + listRepos(): RepoSummary[] { + return [...this.repos.values()] + .filter((r) => !r.hidden && r.valid) + .sort((a, b) => a.label.localeCompare(b.label)); + } + + getRepo(id: string): RepoSummary | undefined { + return this.repos.get(id); + } + + listWorktrees(repoId: string): WorktreeSummary[] { + const byPath = this.worktrees.get(repoId); + if (!byPath) return []; + // worktree principal en tête, puis tri par branche/chemin. + return [...byPath.values()].sort((a, b) => { + if (a.isMain !== b.isMain) return a.isMain ? -1 : 1; + return (a.branch ?? a.path).localeCompare(b.branch ?? b.path); + }); + } + + getWorktree(repoId: string, path: string): WorktreeSummary | undefined { + return this.worktrees.get(repoId)?.get(normPath(path)); + } + + /** sessions corrélées à un worktree par cwd (comme le serveur) ; filtre les externes selon le réglage. */ + sessionsForWorktree(path: string): SessionSummary[] { + const wp = normPath(path); + return [...this.sessions.values()] + .filter((s) => normPath(s.cwd) === wp) + .filter((s) => this.showExternalSessions || s.source === 'managed') + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + } + + getSession(id: string): SessionSummary | undefined { + return this.sessions.get(id); + } + + listGroups(): GroupSummary[] { + return [...this.groups.values()].sort((a, b) => a.label.localeCompare(b.label)); + } + + getGroup(id: string): GroupSummary | undefined { + return this.groups.get(id); + } + + /** sessions de groupe (couvrant plusieurs repos) attachées à un groupe donné. */ + sessionsForGroup(groupId: string): SessionSummary[] { + return [...this.sessions.values()] + .filter((s) => s.groupId === groupId) + .filter((s) => this.showExternalSessions || s.source === 'managed') + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + } + + /** sessions en attente d'input (activity === 'waiting') — pour la status bar et les notifications. */ + waitingSessions(): SessionSummary[] { + return [...this.sessions.values()].filter((s) => s.live && s.activity === 'waiting'); + } + + allSessions(): SessionSummary[] { + return [...this.sessions.values()]; + } + + /** trouve le worktree dont le chemin == folderPath (mapping workspace). */ + findWorktreeByPath(folderPath: string): { repoId: string; worktree: WorktreeSummary } | undefined { + const target = normPath(folderPath); + for (const [repoId, byPath] of this.worktrees) { + const wt = byPath.get(target); + if (wt) return { repoId, worktree: wt }; + } + return undefined; + } +} diff --git a/packages/vscode/src/status-bar.ts b/packages/vscode/src/status-bar.ts new file mode 100644 index 0000000..3a8702c --- /dev/null +++ b/packages/vscode/src/status-bar.ts @@ -0,0 +1,40 @@ +// Status bar : compteur des sessions Claude en attente d'input (`activity === 'waiting'`). +// Clic → quick-pick des sessions en attente (commande arboretum.showWaiting). Masqué si déconnecté +// ou s'il n'y a rien en attente. +import * as vscode from 'vscode'; +import type { Store } from './state/store.js'; + +export class StatusBar implements vscode.Disposable { + private readonly item: vscode.StatusBarItem; + private connected = false; + private readonly unsubscribe: () => void; + + constructor(private readonly store: Store) { + this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + this.item.command = 'arboretum.showWaiting'; + this.unsubscribe = store.onDidChange(() => this.update()); + this.update(); + } + + setConnected(connected: boolean): void { + this.connected = connected; + this.update(); + } + + private update(): void { + const count = this.connected ? this.store.waitingSessions().length : 0; + if (count === 0) { + this.item.hide(); + return; + } + this.item.text = `$(bell-dot) ${count}`; + this.item.tooltip = `Arboretum: ${count} session(s) waiting for input`; + this.item.color = new vscode.ThemeColor('statusBarItem.warningForeground'); + this.item.show(); + } + + dispose(): void { + this.unsubscribe(); + this.item.dispose(); + } +} diff --git a/packages/vscode/src/terminal/session-bridge.ts b/packages/vscode/src/terminal/session-bridge.ts new file mode 100644 index 0000000..4a9aa1c --- /dev/null +++ b/packages/vscode/src/terminal/session-bridge.ts @@ -0,0 +1,85 @@ +// Pont d'une session vers un terminal — module pur (aucun import vscode), testable. +// Le ws-client remet des octets BRUTS (un chunk PTY peut couper un caractère UTF-8 en frontière de +// frame, exactement comme côté web où xterm.write(Uint8Array) gérait le décodage). Ici la cible est +// un Pseudoterminal qui veut des `string` → on décode en streaming avec un TextDecoder, et on recrée +// le décodeur à chaque RESYNC pour ne pas traîner une séquence partielle au-delà d'un reset. +import type { ArbWsClient, Attachment, AttachmentSink, DetachReason } from '../api/ws-client.js'; + +/** Décodeur UTF-8 incrémental : conserve l'octet partiel d'une frontière de frame jusqu'au suivant. */ +export class StreamingUtf8Decoder { + private decoder = new TextDecoder('utf-8'); + + decode(chunk: Uint8Array): string { + return this.decoder.decode(chunk, { stream: true }); + } + + /** Vide l'état partiel et repart à neuf (appelé sur RESYNC). */ + reset(): void { + this.decoder = new TextDecoder('utf-8'); + } +} + +export interface SessionBridgeCallbacks { + /** texte décodé à écrire dans le terminal. */ + onText(text: string): void; + /** RESYNC reçu : le terminal doit se réinitialiser (le texte de replay suit via onText). */ + onReset(): void; + onDetached(reason: DetachReason): void; + onControlChanged(controlling: boolean): void; +} + +/** + * Relie une session Arboretum à un terminal via le ws-client. Instancié par l'adaptateur + * Pseudoterminal ; reste pur pour être testable avec un faux ws-client. + */ +export class SessionBridge { + private attachment: Attachment | null = null; + private readonly decoder = new StreamingUtf8Decoder(); + private detached = false; + + constructor( + private readonly ws: ArbWsClient, + private readonly sessionId: string, + private readonly mode: 'interactive' | 'observer', + private readonly cb: SessionBridgeCallbacks, + ) {} + + /** Ouvre l'attache ; résout quand le canal est établi. Rejette si la session est introuvable. */ + async open(cols: number, rows: number): Promise { + const sink: AttachmentSink = { + data: (payload) => { + if (this.detached) return; + const text = this.decoder.decode(payload); + if (text.length > 0) this.cb.onText(text); + }, + reset: () => { + this.decoder.reset(); + this.cb.onReset(); + }, + detached: (reason) => { + this.detached = true; + this.cb.onDetached(reason); + }, + controlChanged: (c) => this.cb.onControlChanged(c), + }; + this.attachment = await this.ws.attach({ sessionId: this.sessionId, mode: this.mode, cols, rows, sink }); + } + + sendStdin(data: string): void { + this.attachment?.sendStdin(data); + } + + resize(cols: number, rows: number): void { + this.attachment?.resize(cols, rows); + } + + answer(action: 'select' | 'confirm' | 'deny', optionN?: number): void { + this.attachment?.answer(action, optionN); + } + + close(): void { + this.detached = true; + this.attachment?.detach(); + this.attachment = null; + } +} diff --git a/packages/vscode/src/terminal/session-pty.ts b/packages/vscode/src/terminal/session-pty.ts new file mode 100644 index 0000000..dfa6bc7 --- /dev/null +++ b/packages/vscode/src/terminal/session-pty.ts @@ -0,0 +1,86 @@ +// Adaptateur vscode.Pseudoterminal : expose une session Arboretum comme un terminal NATIF de VS Code +// (rendu, scrollback, copier/coller, liens gratuits). C'est la pièce maîtresse de l'intégration — +// pas de xterm-in-webview. Le pont WS/décodage vit dans SessionBridge (pur) ; cet adaptateur ne fait +// que relier les events Pseudoterminal ↔ bridge. +import * as vscode from 'vscode'; +import type { SessionSummary } from '@arboretum/shared'; +import type { ArbWsClient } from '../api/ws-client.js'; +import { SessionBridge } from './session-bridge.js'; + +const ANSI_RESET = '\x1bc'; // RIS : réinitialisation complète du terminal (avant le replay d'un RESYNC) + +export class SessionTerminalManager { + private readonly terminals = new Map(); + private readonly bridges = new Map(); + + constructor(private readonly ws: ArbWsClient) {} + + /** Bridge d'une session si un terminal est ouvert (pour répondre à un dialogue sans ré-attacher). */ + getBridge(sessionId: string): SessionBridge | undefined { + return this.bridges.get(sessionId); + } + + /** Ouvre (ou ré-affiche) un terminal natif attaché à la session. */ + open(session: SessionSummary, mode: 'interactive' | 'observer'): void { + const existing = this.terminals.get(session.id); + if (existing) { + existing.show(); + return; + } + + const writeEmitter = new vscode.EventEmitter(); + const closeEmitter = new vscode.EventEmitter(); + let bridge: SessionBridge | null = null; + + const pty: vscode.Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: (dims) => { + bridge = new SessionBridge(this.ws, session.id, mode, { + onText: (text) => writeEmitter.fire(text), + onReset: () => writeEmitter.fire(ANSI_RESET), + onDetached: () => { + writeEmitter.fire('\r\n\x1b[2m[arboretum: session ended]\x1b[0m\r\n'); + closeEmitter.fire(); + }, + onControlChanged: (controlling) => { + if (!controlling && mode === 'interactive') { + writeEmitter.fire('\r\n\x1b[33m[arboretum: control lost — now observing]\x1b[0m\r\n'); + } + }, + }); + this.bridges.set(session.id, bridge); + bridge.open(dims?.columns ?? 80, dims?.rows ?? 24).catch((err: Error) => { + writeEmitter.fire(`\r\n\x1b[31m[arboretum: cannot attach — ${err.message}]\x1b[0m\r\n`); + closeEmitter.fire(); + }); + }, + close: () => { + bridge?.close(); + this.bridges.delete(session.id); + this.terminals.delete(session.id); + }, + handleInput: (data) => { + if (mode === 'interactive') bridge?.sendStdin(data); + }, + setDimensions: (dims) => bridge?.resize(dims.columns, dims.rows), + }; + + const name = this.terminalName(session, mode); + const terminal = vscode.window.createTerminal({ name, pty, iconPath: new vscode.ThemeIcon('comment-discussion') }); + this.terminals.set(session.id, terminal); + terminal.show(); + } + + private terminalName(session: SessionSummary, mode: 'interactive' | 'observer'): string { + const base = session.title?.trim() || session.cwd.split('/').pop() || session.command; + const prefix = mode === 'observer' ? '👁 ' : ''; + return `${prefix}${base}`; + } + + dispose(): void { + for (const t of this.terminals.values()) t.dispose(); + this.terminals.clear(); + this.bridges.clear(); + } +} diff --git a/packages/vscode/src/views/groups-tree.ts b/packages/vscode/src/views/groups-tree.ts new file mode 100644 index 0000000..bffe438 --- /dev/null +++ b/packages/vscode/src/views/groups-tree.ts @@ -0,0 +1,64 @@ +// Arbre Groups → repos membres → worktrees/sessions, plus les sessions de groupe (multi-repo, --add-dir) +// rattachées directement au groupe. Rafraîchi en temps réel (store.onDidChange). +import * as vscode from 'vscode'; +import type { Store } from '../state/store.js'; +import type { GroupsNode } from './nodes.js'; +import { groupTreeItem, repoTreeItem, sessionTreeItem, worktreeTreeItem } from './tree-items.js'; + +export class GroupsTreeProvider implements vscode.TreeDataProvider { + private readonly changeEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.changeEmitter.event; + + constructor(private readonly store: Store) { + store.onDidChange(() => this.changeEmitter.fire(undefined)); + } + + getTreeItem(node: GroupsNode): vscode.TreeItem { + switch (node.kind) { + case 'group': + return groupTreeItem(node.group); + case 'repo': + return repoTreeItem(node.repo); + case 'worktree': + return worktreeTreeItem(node.worktree); + case 'group-session': + case 'session': { + const item = sessionTreeItem(node.session); + if (node.session.live) { + item.command = { command: 'arboretum.attachSession', title: 'Attach', arguments: [node] }; + } + return item; + } + } + } + + getChildren(node?: GroupsNode): GroupsNode[] { + if (!node) { + return this.store.listGroups().map((group) => ({ kind: 'group', group })); + } + if (node.kind === 'group') { + const children: GroupsNode[] = []; + // sessions de groupe (couvrant plusieurs repos) en tête + for (const session of this.store.sessionsForGroup(node.group.id)) { + children.push({ kind: 'group-session', session, groupId: node.group.id }); + } + // puis les repos membres + for (const repoId of node.group.repoIds) { + const repo = this.store.getRepo(repoId); + if (repo) children.push({ kind: 'repo', repo }); + } + return children; + } + if (node.kind === 'repo') { + return this.store + .listWorktrees(node.repo.id) + .map((worktree) => ({ kind: 'worktree', repoId: node.repo.id, worktree })); + } + if (node.kind === 'worktree') { + return this.store + .sessionsForWorktree(node.worktree.path) + .map((session) => ({ kind: 'session', session, repoId: node.repoId, worktreePath: node.worktree.path })); + } + return []; + } +} diff --git a/packages/vscode/src/views/nodes.ts b/packages/vscode/src/views/nodes.ts new file mode 100644 index 0000000..b9b21e2 --- /dev/null +++ b/packages/vscode/src/views/nodes.ts @@ -0,0 +1,33 @@ +// Types de nœuds des arbres. Les handlers de commandes reçoivent un nœud (clic ou menu contextuel) et +// narrowent sur `kind`. Volontairement plats (données + discriminant) ; la hiérarchie parent/enfant est +// tenue par les providers (maps indexées) pour que reveal()/getParent fonctionnent. +import type { GroupSummary, RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared'; + +export interface RepoNode { + kind: 'repo'; + repo: RepoSummary; +} +export interface WorktreeNode { + kind: 'worktree'; + repoId: string; + worktree: WorktreeSummary; +} +export interface SessionNode { + kind: 'session'; + session: SessionSummary; + repoId: string; + worktreePath: string; +} +export interface GroupNode { + kind: 'group'; + group: GroupSummary; +} +export interface GroupSessionNode { + kind: 'group-session'; + session: SessionSummary; + groupId: string; +} + +export type ReposNode = RepoNode | WorktreeNode | SessionNode; +export type GroupsNode = GroupNode | RepoNode | WorktreeNode | SessionNode | GroupSessionNode; +export type AnyNode = ReposNode | GroupsNode; diff --git a/packages/vscode/src/views/repos-tree.ts b/packages/vscode/src/views/repos-tree.ts new file mode 100644 index 0000000..1ba53f4 Binary files /dev/null and b/packages/vscode/src/views/repos-tree.ts differ diff --git a/packages/vscode/src/views/tree-items.ts b/packages/vscode/src/views/tree-items.ts new file mode 100644 index 0000000..41d0b22 --- /dev/null +++ b/packages/vscode/src/views/tree-items.ts @@ -0,0 +1,95 @@ +// Helpers de construction des TreeItem (icônes d'état, labels, contextValue) partagés par les deux +// arbres (Repositories & Groups). Les `contextValue` pilotent les menus contextuels (cf. package.json) ; +// les regex y matchent des sous-chaînes → format stable `arboretum:session:[:discovered][:waiting]`. +import * as vscode from 'vscode'; +import type { GroupSummary, RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared'; + +export function repoTreeItem(repo: RepoSummary): vscode.TreeItem { + const item = new vscode.TreeItem(repo.label, vscode.TreeItemCollapsibleState.Expanded); + item.id = `repo:${repo.id}`; + item.contextValue = 'arboretum:repo'; + item.iconPath = new vscode.ThemeIcon('repo'); + item.description = repo.defaultBranch ?? ''; + item.tooltip = repo.path; + return item; +} + +export function worktreeTreeItem(worktree: WorktreeSummary): vscode.TreeItem { + const label = worktree.branch ?? (worktree.detached ? '(detached)' : worktree.head.slice(0, 8)); + const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.Expanded); + item.id = `wt:${worktree.path}`; + item.contextValue = 'arboretum:worktree'; + item.iconPath = new vscode.ThemeIcon(worktree.isMain ? 'star-full' : 'git-branch'); + item.description = worktreeStatus(worktree); + item.tooltip = worktree.path; + item.resourceUri = vscode.Uri.file(worktree.path); + return item; +} + +function worktreeStatus(w: WorktreeSummary): string { + const parts: string[] = []; + if (w.isMain) parts.push('main'); + if (w.git.ahead > 0) parts.push(`↑${w.git.ahead}`); + if (w.git.behind > 0) parts.push(`↓${w.git.behind}`); + if (w.git.dirtyCount > 0) parts.push(`●${w.git.dirtyCount}`); + return parts.join(' '); +} + +export function sessionContextValue(s: SessionSummary): string { + let v = `arboretum:session:${s.live ? 'live' : 'dead'}`; + if (s.source === 'discovered') v += ':discovered'; + if (s.live && s.activity === 'waiting') v += ':waiting'; + return v; +} + +export function sessionTreeItem(s: SessionSummary): vscode.TreeItem { + const label = s.title?.trim() || s.command; + const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None); + item.id = `session:${s.id}`; + item.contextValue = sessionContextValue(s); + item.iconPath = sessionIcon(s); + item.description = sessionDescription(s); + item.tooltip = sessionTooltip(s); + return item; +} + +function sessionIcon(s: SessionSummary): vscode.ThemeIcon { + if (!s.live) return new vscode.ThemeIcon('circle-slash'); + switch (s.activity) { + case 'waiting': + return new vscode.ThemeIcon('bell-dot', new vscode.ThemeColor('charts.yellow')); + case 'busy': + return new vscode.ThemeIcon('loading~spin'); + case 'idle': + return new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green')); + default: + return new vscode.ThemeIcon('terminal'); + } +} + +function sessionDescription(s: SessionSummary): string { + const parts: string[] = []; + if (s.live && s.activity) parts.push(s.activity); + else if (!s.live) parts.push('exited'); + if (s.source === 'discovered') parts.push('external'); + if (s.groupId) parts.push('group'); + if (s.waitingFor) parts.push(`— ${s.waitingFor}`); + return parts.join(' '); +} + +function sessionTooltip(s: SessionSummary): string { + const lines = [`${s.command} — ${s.cwd}`, `status: ${s.status}${s.live ? ' (live)' : ''}`]; + if (s.activity) lines.push(`activity: ${s.activity}`); + if (s.addedDirs && s.addedDirs.length > 0) lines.push(`added dirs: ${s.addedDirs.length}`); + return lines.join('\n'); +} + +export function groupTreeItem(group: GroupSummary): vscode.TreeItem { + const item = new vscode.TreeItem(group.label, vscode.TreeItemCollapsibleState.Collapsed); + item.id = `group:${group.id}`; + item.contextValue = 'arboretum:group'; + item.iconPath = new vscode.ThemeIcon('folder-library'); + item.description = `${group.repoIds.length} repos`; + if (group.description) item.tooltip = group.description; + return item; +} diff --git a/packages/vscode/src/workspace.ts b/packages/vscode/src/workspace.ts new file mode 100644 index 0000000..facb28a --- /dev/null +++ b/packages/vscode/src/workspace.ts @@ -0,0 +1,57 @@ +// Conscience du workspace (Phase D) : mappe les dossiers ouverts dans VS Code aux repos/worktrees +// Arboretum par égalité de chemin, met en évidence le worktree correspondant dans l'arbre, et fournit +// le repoId courant aux commandes contextuelles (« start session here », « create worktree here »). +import * as vscode from 'vscode'; +import type { Store } from './state/store.js'; +import type { ReposNode } from './views/nodes.js'; +import type { ReposTreeProvider } from './views/repos-tree.js'; + +function normPath(p: string): string { + return p.replace(/\/+$/, '') || '/'; +} + +export class WorkspaceMapper implements vscode.Disposable { + private readonly disposables: vscode.Disposable[] = []; + + constructor( + private readonly store: Store, + private readonly treeView: vscode.TreeView, + private readonly provider: ReposTreeProvider, + ) { + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.revealCurrent())); + // re-tente après chaque rafraîchissement de l'état (les worktrees arrivent après la connexion). + this.disposables.push({ dispose: store.onDidChange(() => this.revealCurrent()) }); + } + + /** repoId correspondant à un dossier ouvert (via worktree ou racine de repo), pour les actions contextuelles. */ + currentRepoId(): string | undefined { + for (const folder of vscode.workspace.workspaceFolders ?? []) { + const p = normPath(folder.uri.fsPath); + const match = this.store.findWorktreeByPath(p); + if (match) return match.repoId; + const repo = this.store.listRepos().find((r) => normPath(r.path) === p); + if (repo) return repo.id; + } + return undefined; + } + + /** Met en évidence dans l'arbre le worktree du premier dossier ouvert qui matche. */ + revealCurrent(): void { + if (!this.treeView.visible) return; + for (const folder of vscode.workspace.workspaceFolders ?? []) { + const match = this.store.findWorktreeByPath(normPath(folder.uri.fsPath)); + if (!match) continue; + const node = this.provider.worktreeNode(match.repoId, match.worktree.path); + if (node) { + void this.treeView.reveal(node, { select: false, focus: false, expand: true }).then(undefined, () => { + /* reveal est best-effort : un échec (nœud pas encore matérialisé) n'est pas bloquant */ + }); + return; + } + } + } + + dispose(): void { + for (const d of this.disposables) d.dispose(); + } +} diff --git a/packages/vscode/test/config.test.ts b/packages/vscode/test/config.test.ts new file mode 100644 index 0000000..e12a1dd --- /dev/null +++ b/packages/vscode/test/config.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeBaseUrl, wsUrlFromBase } from '../src/config.js'; + +describe('config', () => { + it('normalise la base URL (slash final, défaut)', () => { + expect(normalizeBaseUrl('http://127.0.0.1:7317/')).toBe('http://127.0.0.1:7317'); + expect(normalizeBaseUrl(' http://host:8080// ')).toBe('http://host:8080'); + expect(normalizeBaseUrl('')).toBe('http://127.0.0.1:7317'); + }); + + it('dérive l’URL WebSocket (/ws, http→ws, https→wss)', () => { + expect(wsUrlFromBase('http://127.0.0.1:7317')).toBe('ws://127.0.0.1:7317/ws'); + expect(wsUrlFromBase('https://box.ts.net')).toBe('wss://box.ts.net/ws'); + }); +}); diff --git a/packages/vscode/test/rest-client.test.ts b/packages/vscode/test/rest-client.test.ts new file mode 100644 index 0000000..5473081 --- /dev/null +++ b/packages/vscode/test/rest-client.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from 'vitest'; +import { RestClient, RestError } from '../src/api/rest-client.js'; + +type Captured = { url: string; init: RequestInit }; + +function fakeFetch(response: { ok: boolean; status: number; body: unknown }, captured: Captured[]) { + return vi.fn(async (url: string, init: RequestInit) => { + captured.push({ url, init }); + return { + ok: response.ok, + status: response.status, + json: async () => response.body, + } as unknown as Response; + }) as unknown as typeof fetch; +} + +describe('RestClient', () => { + it('appelle /api/v1/auth/me avec le header Bearer', async () => { + const captured: Captured[] = []; + const rest = new RestClient( + { baseUrl: 'http://127.0.0.1:7317/', token: 'arb_secret' }, + fakeFetch({ ok: true, status: 200, body: { ok: true, tokenId: 't', tokenLabel: 'l', serverVersion: '1.8.0' } }, captured), + ); + const me = await rest.me(); + expect(me.serverVersion).toBe('1.8.0'); + expect(captured[0]?.url).toBe('http://127.0.0.1:7317/api/v1/auth/me'); + const headers = captured[0]?.init.headers as Record; + expect(headers.Authorization).toBe('Bearer arb_secret'); + }); + + it('normalise les erreurs serveur en RestError (code + status)', async () => { + const rest = new RestClient( + { baseUrl: 'http://127.0.0.1:7317', token: 'bad' }, + fakeFetch({ ok: false, status: 401, body: { error: { code: 'BAD_TOKEN', message: 'invalid token' } } }, []), + ); + await expect(rest.me()).rejects.toMatchObject({ code: 'BAD_TOKEN', status: 401 }); + await expect(rest.me()).rejects.toBeInstanceOf(RestError); + }); + + it('encode les ids dans les chemins et envoie le corps JSON', async () => { + const captured: Captured[] = []; + const rest = new RestClient( + { baseUrl: 'http://h:1', token: 't' }, + fakeFetch({ ok: true, status: 200, body: { worktree: {} } }, captured), + ); + await rest.commitWorktree('repo id', { path: '/w', message: 'msg' }); + expect(captured[0]?.url).toBe('http://h:1/api/v1/repos/repo%20id/worktrees/commit'); + expect(captured[0]?.init.method).toBe('POST'); + expect(JSON.parse(String(captured[0]?.init.body))).toEqual({ path: '/w', message: 'msg' }); + }); +}); diff --git a/packages/vscode/test/session-bridge.test.ts b/packages/vscode/test/session-bridge.test.ts new file mode 100644 index 0000000..618a8a0 --- /dev/null +++ b/packages/vscode/test/session-bridge.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { StreamingUtf8Decoder } from '../src/terminal/session-bridge.js'; + +describe('StreamingUtf8Decoder', () => { + it('recompose un caractère UTF-8 coupé en frontière de frame', () => { + const dec = new StreamingUtf8Decoder(); + // 'é' = 0xC3 0xA9, coupé en deux frames + expect(dec.decode(new Uint8Array([0xc3]))).toBe(''); + expect(dec.decode(new Uint8Array([0xa9]))).toBe('é'); + }); + + it('décode l’ASCII normalement', () => { + const dec = new StreamingUtf8Decoder(); + expect(dec.decode(new Uint8Array([0x6c, 0x73, 0x0a]))).toBe('ls\n'); + }); + + it('reset() vide l’état partiel', () => { + const dec = new StreamingUtf8Decoder(); + expect(dec.decode(new Uint8Array([0xe2, 0x9c]))).toBe(''); // début d’un ✓ (3 octets) + dec.reset(); + // après reset, l’octet partiel est oublié → un nouveau caractère complet décode proprement + expect(dec.decode(new Uint8Array([0x41]))).toBe('A'); + }); +}); diff --git a/packages/vscode/test/store.test.ts b/packages/vscode/test/store.test.ts new file mode 100644 index 0000000..7d063f3 --- /dev/null +++ b/packages/vscode/test/store.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest'; +import type { RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared'; +import { Store } from '../src/state/store.js'; + +function repo(id: string): RepoSummary { + return { + id, + path: `/code/${id}`, + label: id, + defaultBranch: 'main', + postCreateHooks: [], + preTrust: false, + createdAt: '2026-01-01T00:00:00Z', + valid: true, + hidden: false, + }; +} + +function worktree(repoId: string, path: string, branch: string): WorktreeSummary { + return { + repoId, + path, + branch, + head: 'abcdef0', + detached: false, + locked: false, + prunable: false, + isMain: branch === 'main', + git: { ahead: 0, behind: 0, dirtyCount: 0, upstream: null }, + sessions: [], + }; +} + +function session(id: string, cwd: string, over: Partial = {}): SessionSummary { + return { + id, + cwd, + command: 'claude', + title: null, + status: 'running', + live: true, + createdAt: '2026-01-01T00:00:00Z', + endedAt: null, + exitCode: null, + clients: 0, + source: 'managed', + claudeSessionId: null, + pid: 123, + resumable: false, + attachable: true, + registryStatus: null, + ...over, + }; +} + +describe('Store', () => { + it('corrèle les sessions au worktree par cwd et filtre les externes', () => { + const store = new Store(); + store.applyWorktree({ type: 'repo_update', repo: repo('app') }); + store.applyWorktree({ type: 'worktree_update', repoId: 'app', worktree: worktree('app', '/code/app', 'main') }); + + store.applySession({ type: 'session_update', session: session('s1', '/code/app') }); + store.applySession({ type: 'session_update', session: session('s2', '/code/app', { source: 'discovered' }) }); + + // externes masquées par défaut + expect(store.sessionsForWorktree('/code/app').map((s) => s.id)).toEqual(['s1']); + store.setShowExternalSessions(true); + expect(store.sessionsForWorktree('/code/app').map((s) => s.id).sort()).toEqual(['s1', 's2']); + }); + + it('liste les repos visibles, triés', () => { + const store = new Store(); + store.applyWorktree({ type: 'repo_update', repo: { ...repo('b'), label: 'beta' } }); + store.applyWorktree({ type: 'repo_update', repo: { ...repo('a'), label: 'alpha' } }); + store.applyWorktree({ type: 'repo_update', repo: { ...repo('h'), label: 'hidden', hidden: true } }); + expect(store.listRepos().map((r) => r.label)).toEqual(['alpha', 'beta']); + }); + + it('session_exit marque la session morte', () => { + const store = new Store(); + store.applySession({ type: 'session_update', session: session('s1', '/x') }); + store.applySession({ type: 'session_exit', sessionId: 's1', exitCode: 0, signal: null }); + expect(store.getSession('s1')?.live).toBe(false); + expect(store.getSession('s1')?.status).toBe('exited'); + }); + + it('waitingSessions ne retient que les sessions live en attente', () => { + const store = new Store(); + store.applySession({ type: 'session_update', session: session('s1', '/x', { activity: 'waiting' }) }); + store.applySession({ type: 'session_update', session: session('s2', '/y', { activity: 'busy' }) }); + store.applySession({ type: 'session_update', session: session('s3', '/z', { activity: 'waiting', live: false, status: 'exited' }) }); + expect(store.waitingSessions().map((s) => s.id)).toEqual(['s1']); + }); + + it('worktree_removed retire le worktree', () => { + const store = new Store(); + store.applyWorktree({ type: 'repo_update', repo: repo('app') }); + store.applyWorktree({ type: 'worktree_update', repoId: 'app', worktree: worktree('app', '/code/app-wt', 'feat') }); + expect(store.listWorktrees('app')).toHaveLength(1); + store.applyWorktree({ type: 'worktree_removed', repoId: 'app', path: '/code/app-wt' }); + expect(store.listWorktrees('app')).toHaveLength(0); + }); +}); diff --git a/packages/vscode/test/ws-client.test.ts b/packages/vscode/test/ws-client.test.ts new file mode 100644 index 0000000..78f0caf --- /dev/null +++ b/packages/vscode/test/ws-client.test.ts @@ -0,0 +1,136 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BINARY_FRAME, FLOW, encodeBinaryFrame, type ClientMessage } from '@arboretum/shared'; +import { ArbWsClient, type AttachmentSink, type RawSocket } from '../src/api/ws-client.js'; + +class FakeSocket implements RawSocket { + sent: string[] = []; + private openCb: (() => void) | null = null; + private textCb: ((t: string) => void) | null = null; + private binaryCb: ((d: Uint8Array) => void) | null = null; + private closeCb: (() => void) | null = null; + + send(data: string): void { + this.sent.push(data); + } + close(): void { + this.closeCb?.(); + } + onOpen(cb: () => void): void { + this.openCb = cb; + } + onText(cb: (t: string) => void): void { + this.textCb = cb; + } + onBinary(cb: (d: Uint8Array) => void): void { + this.binaryCb = cb; + } + onClose(cb: () => void): void { + this.closeCb = cb; + } + onError(): void { + /* non utilisé dans ces tests */ + } + + fireOpen(): void { + this.openCb?.(); + } + fireText(msg: unknown): void { + this.textCb?.(JSON.stringify(msg)); + } + fireBinary(data: Uint8Array): void { + this.binaryCb?.(data); + } + + /** messages de contrôle envoyés, parsés et filtrés par type. */ + controls(type: T): Array> { + return this.sent + .map((s) => JSON.parse(s) as ClientMessage) + .filter((m): m is Extract => m.type === type); + } +} + +function makeNoopSink(): AttachmentSink { + return { data: () => {}, reset: () => {}, detached: () => {}, controlChanged: () => {} }; +} + +describe('ArbWsClient', () => { + let fake: FakeSocket; + let client: ArbWsClient; + + beforeEach(() => { + vi.useFakeTimers(); + fake = new FakeSocket(); + client = new ArbWsClient({ socketFactory: () => fake }); + client.configure('http://127.0.0.1:7317', 'arb_token'); + client.connect(); + fake.fireOpen(); + fake.fireText({ type: 'hello_ok', protocol: 1, serverVersion: '1.8.0' }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('envoie hello puis sub après hello_ok', () => { + expect(fake.controls('hello')).toHaveLength(1); + expect(fake.controls('sub')[0]?.topics).toEqual(['sessions', 'worktrees', 'groups']); + }); + + it('corrèle attach→attached et délivre les OUTPUT au sink', async () => { + const chunks: Uint8Array[] = []; + const sink: AttachmentSink = { ...makeNoopSink(), data: (p) => chunks.push(p) }; + const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink }); + fake.fireText({ type: 'attached', channel: 1, sessionId: 'sess', mode: 'interactive', controlling: true }); + const att = await p; + expect(att.channel).toBe(1); + + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.OUTPUT, 1, new Uint8Array([104, 105]))); // "hi" + expect(chunks).toHaveLength(1); + expect([...(chunks[0] ?? [])]).toEqual([104, 105]); + }); + + it('ACK au seuil ACK_EVERY_BYTES', async () => { + const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink: makeNoopSink() }); + fake.fireText({ type: 'attached', channel: 7, sessionId: 'sess', mode: 'interactive', controlling: true }); + await p; + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.OUTPUT, 7, new Uint8Array(FLOW.ACK_EVERY_BYTES))); + const acks = fake.controls('ack'); + expect(acks).toHaveLength(1); + expect(acks[0]).toMatchObject({ channel: 7, bytes: FLOW.ACK_EVERY_BYTES }); + }); + + it('ACK traînant flushe un reliquat sous le seuil après 200ms', async () => { + const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink: makeNoopSink() }); + fake.fireText({ type: 'attached', channel: 2, sessionId: 'sess', mode: 'interactive', controlling: true }); + await p; + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.OUTPUT, 2, new Uint8Array(1000))); + expect(fake.controls('ack')).toHaveLength(0); // sous le seuil → pas d’ACK immédiat + vi.advanceTimersByTime(200); + expect(fake.controls('ack')).toEqual([{ type: 'ack', channel: 2, bytes: 1000 }]); + }); + + it('RESYNC réinitialise le terminal et les compteurs d’ACK', async () => { + let resets = 0; + const sink: AttachmentSink = { ...makeNoopSink(), reset: () => (resets += 1) }; + const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink }); + fake.fireText({ type: 'attached', channel: 3, sessionId: 'sess', mode: 'interactive', controlling: true }); + await p; + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.OUTPUT, 3, new Uint8Array(FLOW.ACK_EVERY_BYTES))); + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.RESYNC, 3, new Uint8Array([65]))); // reset + replay "A" + expect(resets).toBe(1); + // après resync, le replay ne compte pas : un nouvel OUTPUT au seuil ré-ACK à partir de 0 + fake.fireBinary(encodeBinaryFrame(BINARY_FRAME.OUTPUT, 3, new Uint8Array(FLOW.ACK_EVERY_BYTES))); + const acks = fake.controls('ack'); + expect(acks[acks.length - 1]).toMatchObject({ bytes: FLOW.ACK_EVERY_BYTES }); + }); + + it('envoie stdin/resize sur le bon canal', async () => { + const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink: makeNoopSink() }); + fake.fireText({ type: 'attached', channel: 5, sessionId: 'sess', mode: 'interactive', controlling: true }); + const att = await p; + att.sendStdin('ls\n'); + att.resize(120, 40); + expect(fake.controls('stdin')[0]).toEqual({ type: 'stdin', channel: 5, data: 'ls\n' }); + expect(fake.controls('resize')[0]).toEqual({ type: 'resize', channel: 5, cols: 120, rows: 40 }); + }); +}); diff --git a/packages/vscode/tsconfig.json b/packages/vscode/tsconfig.json new file mode 100644 index 0000000..a38a4f2 --- /dev/null +++ b/packages/vscode/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "module": "ESNext", + "moduleResolution": "Bundler", + "verbatimModuleSyntax": false, + "composite": false, + "declaration": false, + "declarationMap": false, + "noEmit": true, + "types": ["node"], + "lib": ["ES2023"] + }, + "include": ["src", "test"] +} diff --git a/spikes/s5-vscode/STUDY.md b/spikes/s5-vscode/STUDY.md new file mode 100644 index 0000000..ef21c20 --- /dev/null +++ b/spikes/s5-vscode/STUDY.md @@ -0,0 +1,131 @@ +# Spike S5 — Extension VSCode (intégration native) — ÉTUDE : ✅ GO (phasé) + +> **✅ IMPLÉMENTÉ** dans `packages/vscode` (`git-arboretum`, privé) — phases A→D livrées : connexion + +> auth SecretStorage, arbres temps réel (Repos/Groups), terminaux natifs `Pseudoterminal`, status bar + +> notifications `waiting` (commande `answer`), mutations git, conscience du workspace. Bundle esbuild +> (shared inliné), packagé en VSIX (`npm run build:vscode` puis `vsce package --no-dependencies`). +> CI : `.gitea/workflows/vscode-release.yml` sur tag `vscode-vX.Y.Z`. L'étude ci-dessous reste la +> justification de conception. + + +Étude de conception (pas de mesure Go/No-Go : c'est un design + plan d'implémentation phasé). Objectif +validé avec l'utilisateur : **une vraie intégration native dans VSCode, pas un simple webview** — une +connexion réelle avec ce qui se passe dans l'éditeur (terminaux natifs, arbre, statut, notifications, +conscience du workspace). + +## Contexte & contrainte + +Arboretum est déjà un daemon qui sert une SPA et expose : +- une **API REST** (`/api/v1/...`) : repos, worktrees (+ mutations git commit/push/promote), sessions, + groupes, auth ; +- un **protocole WebSocket** multiplexé (`@arboretum/shared` → `protocol.ts`) : attach/stdin/resize, + frames binaires `[type u8][channel u32le][payload]`, flow-control par watermarks (`FLOW`), events + temps réel `sessions`/`worktrees`/`groups`, commande `answer` (dialogues), `hello`/`hello_ok`. + +Un plugin n'a donc **rien à réimplémenter côté logique** : il est un **client** de cette API. La question +n'est pas « comment afficher le dashboard » mais « comment exposer Arboretum avec les primitives natives +de VSCode ». Le webview (iframe du dashboard) reste un *fallback* optionnel ; ce n'est pas le cœur. + +## Pourquoi pas « juste un webview » + +- Un webview = iframe vers `http://127.0.0.1:7317` : lourd, isolé (CSP stricte, `acquireVsCodeApi` + indisponible dans l'iframe), pas d'intégration avec les terminaux/arbres/statut de VSCode, auth à + re-bricoler (cookie/token dans une iframe cross-origin). On retrouve les bugs xterm-in-webview + (rendu, scrollback) qu'on cherche justement à éviter. +- Une intégration native réutilise le **rendu terminal, le scrollback, le copier/coller, les liens** de + VSCode (gratuitement robustes), et place Arboretum là où l'utilisateur travaille déjà. + +## Architecture proposée — `packages/vscode` + +Nouveau workspace privé `@arboretum/vscode`, extension TypeScript, packagée en **VSIX** via `vsce` +(build `esbuild`). Le `@arboretum/shared` est résolu en dev par le symlink workspace et **inliné** au +packaging (même principe que `scripts/inline-shared.mjs` côté serveur) → VSIX autonome. + +### Connexion au daemon +- Réglages : `arboretum.url` (défaut `http://127.0.0.1:7317`), `arboretum.token`. +- **Auth** : commande `Arboretum: Sign in` qui demande le token et le stocke dans **VS Code + SecretStorage** (les tokens daemon sont stockés *hashés* et le bootstrap ne s'affiche qu'une fois → + impossible de « lire » le token existant ; on le saisit une fois). Option avancée : commande qui + **spawn** le daemon (`npx @johanleroy/git-arboretum`) et capture le bootstrap token au premier + démarrage. +- Le check Origin strict du daemon ne s'applique qu'aux navigateurs (en-tête `Origin`) : un client + Node/extension n'envoie pas d'`Origin` → OK ; l'auth par token (en-tête/cookie) reste requise. + +### Réutilisation du protocole (le « vrai » lien) +Importer `@arboretum/shared` : `decodeBinaryFrame`/encodage, `BINARY_FRAME`, `FLOW`, `PROTOCOL_VERSION`, +types de messages et `SessionSummary`/`WorktreeSummary`/`GroupSummary`. Le pont terminal et la liste +restent **synchronisés** avec le serveur par construction. + +### Surfaces natives + +1. **Activity Bar + TreeView(s)** (`vscode.window.createTreeView`) : arbre **Repos → Worktrees → + Sessions**, avec badges d'état fins (`activity` busy/waiting/idle, `dialog`) déjà portés par + `SessionSummary`. Une vue **Groupes** en parallèle. Rafraîchi en temps réel par les events WS + (`onDidChangeTreeData`). + +2. **Terminaux VSCode natifs via `Pseudoterminal`** *(pièce maîtresse)* : + `vscode.window.createTerminal({ name, pty })` où le `Pseudoterminal` ponte la session Arboretum : + - `open()` → `wsClient.attach(sessionId)` ; + - OUTPUT WS (frames binaires) → `writeEmitter.fire(text)` (`onDidWrite`) ; + - `handleInput(data)` → WS `stdin` ; + - `setDimensions({columns, rows})` → WS `resize` ; + - `close()` → `detach`. + On réutilise le **renderer + scrollback natifs de VSCode** : règle au passage, côté VSCode, les + problèmes de rendu et d'historique de la SPA. Flow-control : réutiliser `FLOW`/les ACK du protocole + (envoyer un ACK des octets écrits) pour ne pas saturer. + +3. **Status bar** (`createStatusBarItem`) : compteur des sessions `waiting` (à traiter) ; clic → focus + de l'arbre / quick-pick des sessions en attente. + +4. **Notifications natives** : sur transition d'une session vers `waiting` (event WS), `showInformation + Message` avec actions (« Ouvrir le terminal », « Oui »/« Non » → commande WS `answer`). Doublonne + utilement le Web Push : ici, natif dans l'éditeur. + +5. **Conscience du workspace** : mapper `workspace.workspaceFolders` → repo/worktree Arboretum (par + chemin). Actions contextuelles : « Démarrer une session Claude ici », « Créer un worktree pour ce + repo », et les mutations git existantes (commit/push/promote via REST). Met aussi en évidence dans + l'arbre le worktree correspondant au dossier ouvert. + +6. **Commandes** (`contributes.commands` + palette VSCode) : sign in, ouvrir le dashboard (webview + *optionnel*), rafraîchir, attacher/observer une session, créer worktree, commit/push/promote, + start/hide session, répondre à un dialogue. + +7. **Webview** : seulement en *option* (vue dashboard complète pour qui la veut). UX primaire = native. + +## Découpage phasé (livrable séparé — non implémenté dans ce lot) + +- **A — Lecture & connexion** : auth (SecretStorage) + client REST/WS + TreeView lecture seule + (repos/worktrees/sessions/groupes) en temps réel. *Valeur immédiate, risque faible.* +- **B — Terminaux natifs** : `Pseudoterminal` attach/observe via pont WS réutilisant `@arboretum/shared` + (flow-control inclus). *Le cœur de la valeur ; à faire juste après A.* +- **C — Actions & supervision** : mutations (créer worktree, commit/push/promote, start/hide), status + bar `waiting`, notifications + commande `answer`. +- **D — Workspace-aware** : mapping dossier ↔ repo/worktree + actions contextuelles. + +## Intégration monorepo & packaging + +- `packages/vscode/` : `package.json` d'extension (`engines.vscode`, `activationEvents`, + `contributes.{commands,views,viewsContainers,configuration}`), build `esbuild` (bundle Node), test via + `@vscode/test-electron` (optionnel). Publication : VSIX (`vsce package`) — le registre privé Gitea ne + sert pas d'extensions VSCode, donc distribution par VSIX (et/ou Open VSX) plutôt que le Marketplace si + l'on veut rester privé. +- Le `tsconfig` reste hors du `tsc -b` server/shared actuel (build séparé) pour ne pas alourdir le + typecheck CI existant. + +## Risques / questions ouvertes + +- **Auth bootstrap** : saisie manuelle du token vs spawn+capture. MVP = saisie manuelle (SecretStorage). +- **Flow-control côté extension** : reprendre `FLOW` (ACK par octets) pour les terminaux natifs ; un + `Pseudoterminal` n'expose pas de back-pressure → ACK « ce qui a été émis dans `onDidWrite` ». +- **Remote/SSH/Codespaces** : si l'on veut le webview fallback, prévoir `vscode.env.asExternalUri` + + `portMapping`. Les surfaces natives (REST/WS via `arboretum.url`) restent valides tant que le daemon + est joignable depuis l'hôte de l'extension. +- **Multi-fenêtres VSCode** : une connexion WS par fenêtre (acceptable, outil mono-utilisateur). + +## Verdict + +**GO** sur une extension native `packages/vscode`, en commençant par **A → B** (TreeView temps réel + +terminaux natifs `Pseudoterminal`), qui apportent l'essentiel de la valeur et réutilisent directement +l'API REST + le protocole WS de `@arboretum/shared`. Le webview n'est qu'un fallback optionnel. +Références API : Webview, Tree View, Pseudoterminal/`createTerminal`, StatusBarItem, SecretStorage, +`asExternalUri` (docs officielles VSCode).